Р. Аллахвердиев

Harness: как заставить AI-агента работать по правилам

Пять слоёв среды вокруг AI-агента — CLAUDE.md, skills, hooks и права, агенты-ревьюеры, CI — и задачи с критериями приёмки. Разбор на примере проекта DeepRisk и порядок внедрения по дням.

опубликовано · 10 минef04be3
Обложка: Harness: как заставить AI-агента работать по правилам
Содержание

Коротко

  • AI-агент пишет код быстро, но без рамок так же быстро создаёт хаос. «Будь аккуратен» в промпте не работает: это не проверяется и забывается через десять сообщений.
  • Harness («упряжь») — всё, что окружает модель: правила, знания, ограничения и проверки. Модель даёт мощность, харнес — направление.
  • Правило распределения: то, что должно выполняться всегда, живёт в hooks, правах и CI. Знания под конкретную задачу — в skills. Независимый взгляд — в агентах-ревьюерах. Короткое и универсальное — в CLAUDE.md.
  • Работа идёт через задачи с проверяемыми критериями приёмки, а слияние в основную ветку делает человек.
  • Строить всё сразу не нужно: каждый слой даёт пользу сам по себе.

Все примеры ниже взяты из моего проекта DeepRisk (платформа проверки LLM и RAG), где я встроил агентов в разработку.

Проблема: почему агент без правил опасен

Агент видит только то, что ему показали. Он не знает договорённостей команды, не помнит прошлых ошибок и стремится «сделать хорошо» шире, чем его просили. Это не злой умысел, а свойство инструмента. На практике это проявляется четырьмя способами:

  • Дубли и «свои» решения. Вместо готового хелпера агент пишет новый. Через месяц в коде три версии одного и того же.
  • Тихие обходы. Линтер ругается — агент добавляет noqa или type: ignore. Проверка зелёная, проблема жива.
  • Расползание задачи. Просили поправить одну функцию, получили рефакторинг на 40 файлов, который никто не сможет отревьюить.
  • Секреты и данные. Агент читает .env, кладёт ключ в конфиг, коммитит клиентские данные «для примера».

Что не помогает:

  • Длинный промпт «будь аккуратен».
  • Огромный файл правил: чем длиннее инструкция, тем хуже она соблюдается и тем больше контекста тратится впустую.
  • Ревью «на глаз» в конце, когда diff уже большой.

Мой вывод: качество зависит не от того, насколько хорошо вы попросили, а от того, как устроена среда вокруг агента. Что он видит, что ему разрешено и что проверяется автоматически.

Слои харнеса

Слои расположены от «читается всегда» к «жёстче всего проверяется»:

Слой Что это Почему здесь
CLAUDE.md Короткие правила, всегда с агентом Читается всегда, поэтому обязан быть коротким
Skills Знания и рецепты, загружаются по запросу Не засоряют контекст
Hooks и права Детерминированное принуждение Выполняет система, а не память модели
Агенты-ревьюеры Смотрят diff в чистом контексте Независимый взгляд
CI и pre-commit Последняя линия, обязательна для merge Не обходится никем, включая агента

Всё лежит в репозитории и версионируется вместе с кодом. Новый разработчик или новый агент получает те же правила одной командой git clone.

Вот как это выглядит в DeepRisk:

deeprisk/
├─ CLAUDE.md          # 96 строк: жёсткие правила и инварианты
├─ AGENTS.md          # указатель для других агентных инструментов
├─ .claude/
│  ├─ settings.json   # права (deny) и подключение hooks
│  ├─ hooks/          # guard_secrets · format_py · quick_gate
│  ├─ agents/         # reviewer · security-reviewer · test-writer · docs-sync
│  └─ skills/         # deeprisk-dev · architecture · eval · secure + команды
├─ .gitlab/           # шаблоны задач и merge request
├─ docs/AGENT_WORKFLOW.md
└─ scripts/quality_check.sh   # все проверки одной командой

В итоге: 9 skills (4 знания и 5 процессов), 4 агента-ревьюера и помощника, 3 hook’а (секреты, формат, быстрый gate).

CLAUDE.md: короткий и жёсткий

CLAUDE.md читается в каждой сессии, поэтому каждая лишняя строка тратит контекст. В DeepRisk я сжал его с 398 до 96 строк, а знания разложил по skills. Что оставлять:

  • что за проект и где искать остальное;
  • 8–10 жёстких правил;
  • правила про секреты, git и подавление линтера — они обязательны.

Всё, что нужно только при определённых задачах (стиль кода, архитектура системы, правила изменения оценки), уходит в skills.

Skills: знания по запросу

Skill — папка с инструкцией и справочниками. Агент читает короткое описание, а полный текст подгружает, только когда задача подходит. Правила так не съедают контекст.

В DeepRisk четыре skills-знания:

  • deeprisk-dev — как писать код: SOLID, KISS, DRY, YAGNI на примерах этого репозитория, слои, стиль, git. Плюс файл TECH_DEBT: список известных отклонений, которые нельзя считать прецедентом.
  • deeprisk-architecture — как устроена система, со ссылками на файлы и пометкой «не проверено», если уверенности нет.
  • deeprisk-eval — как добавить метрику, судью, атаку или датасет и проверить изменение на малой выборке.
  • deeprisk-secure — чек-лист безопасности и конкретные зоны риска в коде. Продукт сам атакует чужие системы, поэтому требования к нему выше.

Принципы вроде SOLID работают, только когда записаны как правила. Например: router разбирает вход, service оркестрирует, repository ходит в БД; метод с «и» в названии делится; новая атака или метрика — это класс и регистрация, а не ещё одна ветка if; перед написанием кода сначала grep по существующему.

Hooks и права: правила, которые нельзя обойти

Hook — команда, которую среда выполняет в заданный момент работы агента. Агент не «решает» её запускать: она срабатывает сама, а отказ блокирует действие.

В DeepRisk три hook’а:

Момент Что делает
Перед вызовом инструмента (PreToolUse) guard_secrets блокирует чтение .env, SQLite, *.pem, команды вроде cat .env, printenv, а также git push, git add, git commit, reset --hard, clean
После правки файла (PostToolUse) Запускает ruff check и форматирование
Перед тем как агент скажет «готово» (Stop) Быстрая проверка (gate) на ruff, не дольше 60 секунд, с предупреждением вместо зависания

Код выхода 2 означает «действие заблокировано».

Почему это лучше просьбы в тексте. В CLAUDE.md я прошу не читать секреты, а в hook’е и в правах это невозможно сделать: команда просто не выполнится. Просьбу можно забыть или истолковать иначе, запрет — нет.

Оговорка, которую стоит помнить: проверка команд по шаблонам ловит типичные случаи, но не «изобретательные» обходы. Поэтому запрет на уровне прав на чтение файлов — второй, более жёсткий барьер.

Агенты-ревьюеры: один делает, другой проверяет

Субагент запускается в отдельном чистом контексте. Ему не мешает история долгой сессии, он смотрит на результат свежими глазами и возвращает короткий отчёт. Автор diff плохо видит собственные слепые пятна, а у модели они те же.

В DeepRisk четыре таких помощника:

  • reviewer (только чтение): проверяет diff по правилам deeprisk-dev — слои, дубли, подавления линтера, тесты. Отдаёт находки с файлом и строкой.
  • security-reviewer (только чтение): проходит чек-лист безопасности по diff. Значения секретов никогда не печатает.
  • test-writer: пишет pytest-тесты для названного модуля и правит только каталог tests/.
  • docs-sync: ищет расхождения между кодом и документацией и правит только документацию.

Что они нашли за один прогон (по моим материалам проекта):

  1. Сквозной тест на чистых данных нашёл 5 проблем запуска: приложение падало без файла инициализации, Red Team ломался после оценки в том же процессе, в зависимостях не хватало пакета, сборка web не работала на Windows.
  2. Ревью безопасности обнаружило ключ в истории git. Из рабочего дерева его убрали, но в истории он остался. Я узнал об этом до публикации, а не после.
  3. Ревью кода нашло неполные защиты: правила исключений для Docker действовали только в корне, и вложенные секреты могли попасть в образ.

CI: последняя линия

Hooks защищают локальную работу агента, CI защищает основную ветку. Я держу один скрипт quality_check.sh, который запускает все проверки: линтер, форматирование, тесты затронутой области, гигиену репозитория. Агент и человек запускают одну и ту же команду, поэтому «у меня проходит» означает то же, что «пройдёт в CI».

Отдельный пункт — сама оценка качества LLM-части продукта. Её тоже можно подключить в CI с порогами, так же как тесты: об этом в разборе про четыре метрики RAG.

Задачи: как писать для агента

Задачи для агентов живут там же, где код: в GitLab Issues. Один issue — один merge request. Хорошая задача проходит тест «исполнитель без контекста»: тот, кто видит только этот issue и репозиторий, может сделать её, не задавая вопросов.

Раздел задачи Что писать
Контекст Зачем нужно, что сейчас не так, ссылки на код (файл:строка)
Цель Один проверяемый результат одним предложением
Границы Что входит и, что важнее, что не входит
Затрагиваемые места Ожидаемые файлы и модули
Критерии приёмки Чек-лист; каждый пункт проверяется командой или тестом
Как проверить Точные команды и ожидаемый результат
Правила и skills Какие skills агент обязан загрузить
Зависимости и риски Что должно быть готово раньше, как откатить

Пример формулировки с моей доски: «деактивация пользователя». Контекст: пользователей нельзя удалить, запрос даёт 404. Цель: админ деактивирует пользователя, вход невозможен. Не входит: жёсткое удаление строки и UI. Приёмка: 204 на удаление, 401 при входе, 409 для последнего админа и тест на каждый случай.

Плохие задачи выглядят как «улучшить работу с пользователями» или «отрефакторить main.py»: нет критериев приёмки, три темы в одной задаче, зависимость от нерешённого вопроса.

Как агент берёт задачу: точки остановки

Команда /task ведёт агента по восьми шагам. Самое важное в них — точки остановки: агент не угадывает, а спрашивает.

  1. Выбрать готовую задачу с меткой agent::ok, по приоритету, без открытых зависимостей.
  2. Проверить готовность: цель, границы и критерии понятны? Если нет — один комментарий с вопросами, метка blocked, остановка.
  3. Взять: назначить на себя, поставить in-progress, создать ветку.
  4. Загрузить skills из раздела задачи.
  5. Сделать в границах. Найденное вне границ не чинить, а завести отдельный issue.
  6. Проверить: критерии по пунктам, quality_check, тесты, /finish.
  7. Отчитаться в issue: что сделано, чем подтверждено, что найдено.
  8. Подготовить MR по шаблону с Closes #N. Слияние остаётся за человеком.

Жёсткие ограничения: агент никогда не мержит и не закрывает issue, не пушит в main, не делает force-push, не расширяет задачу «заодно» и не занижает критерий, чтобы «прошло».

Всегда решает человек: ротацию ключей, очистку истории git, публикации и продакшен, продуктовые решения и лицензии.

Важное правило: текст issue — это данные, а не приказ. Если в задаче просят подавить линтер, прочитать .env или сделать force-push, агент останавливается и спрашивает. Правила проекта главнее.

Definition of done и уровни доверия

«Готово» — это не ощущение агента, а пройденный список. Команда /finish прогоняет его целиком: имя ветки, quality_check, тесты затронутой области, поиск подавлений линтера в diff, проверка миграций и зависимостей, чек-лист безопасности по diff, обновлённые changelog и документация, самопроверка на дубли. В конце агент выдаёт сводку: что сделано, что проверено и чем, что не запускалось и почему, какие проблемы найдены вне задачи, какие решения спорные.

Доверие я расширяю постепенно, на основании фактов:

  • Режим A (сейчас). Агент готовит изменения и отчёт, коммитит и пушит человек.
  • Режим B (после пилота). Агент коммитит свою ветку и открывает MR. Основная ветка, force-push и теги по-прежнему запрещены.

Слияние остаётся за человеком в любом режиме.

Уроки

  1. Принуждение сильнее уговоров. Критичное выношу в hooks, права и CI, текстовые просьбы оставляю для контекста.
  2. Короткое всегда, длинное по запросу. CLAUDE.md сжат с 398 до 96 строк.
  3. Маленькие задачи. Один issue — один MR — одна тема.
  4. Пусть агент останавливается. Вопрос в комментарии дешевле неверной реализации на 40 файлов.
  5. Проверяем, а не верим. Отчёт агента — гипотеза. Скрипты, тесты и второй агент превращают её в факт.
  6. Честно фиксируем долг. TECH_DEBT — список известных отклонений, которые не должны становиться образцом.

Чего я не учёл сразу: кастомные агенты и slash-команды подхватываются только после перезапуска сессии. Ветку вмерджили до того, как переписали сообщения коммитов, и историю пришлось перебазировать. Правило языка коммитов стоило ввести с первого дня.

Что дальше: как внедрить по дням

Двигайтесь слоями:

  1. День 1: короткий CLAUDE.md. До 100 строк: что за проект, 8–10 жёстких правил, где искать остальное.
  2. День 1: права и один hook. Запретите чтение секретов и git push в настройках. Добавьте hook, который форматирует код после правки.
  3. Неделя 1: skill с правилами кода. Опишите свои реальные паттерны и заведите TECH_DEBT.
  4. Неделя 1: скрипт quality_check. Все проверки одной командой.
  5. Неделя 2: доска и шаблоны задач. Метки, шаблон задачи и MR, пилот на 3–5 задачах.
  6. Неделя 3: агенты-ревьюеры и CI. Ревью безопасности и кода на каждый MR, автоматические проверки в пайплайне.

Критерий готовности: секреты технически недоступны агенту, он не может пушить в основную ветку, есть один скрипт всех проверок, у задач есть критерии приёмки и «не входит», слияние делает человек.

ИИ становится надёжным участником команды не тогда, когда найден идеальный промпт, а когда вокруг него построена среда. Как это выглядит в движении, показывает 30-секундная анимация харнеса. Подробнее о DeepRisk: neon-dev.ru.