AI · Python · LangGraph · Оркестрация
Облачный агент-кодогенератор: роли, контекст и контроль выполнения
Проектирование облачного агента, который получает задачу, исследует репозиторий, планирует изменения и доводит их до проверяемого результата. Здесь — устройство ролей, инструментов, памяти и ограничений, которые управляют этим процессом.
Что считается выполненной задачей
Генерация патча — только часть работы. До неё нужно разобраться в существующем коде и выбрать место изменения; после — проверить реализацию, обработать замечания и решить, можно ли создавать merge request. Между этими этапами модель может потерять важное условие, повторять неудачный вызов или объявить работу законченной раньше времени.
В основе решения — Python, LangGraph и единое состояние TaskState. RabbitMQ связывает worker с внешним источником задач; Git хранит изменения, SQLite — checkpoints графа и поисковые индексы. Основной целевой стек изменяемых проектов — PHP. Роли, провайдеры моделей, каталог prompts/, стандарты проекта и команды проверок задаются конфигурацией: один граф можно переносить на другой репозиторий без переписывания оркестрации.
Один длинный агент с общим контекстом плохо разделяет исследование и право менять код, смешивает бюджеты tools и усложняет точечные обратные переходы. Отдельные узлы с разным набором инструментов, памятью и лимитами дают явные границы фаз; между ними код фиксирует инварианты и запускает обязательные gates, которые модель не может «переголосовать» текстом.
Результат определяется выбранным режимом: изменения с MR, отчёт об анализе или отчёт ревью. Для каждого есть маршрут, выходные данные и условия завершения. Короткие режимы анализа и ревью ещё развиваются; их маршруты и тесты уже реализованы. Ни один из режимов не делает оценку модели доказательством корректности кода.
Роли связаны контрактами и обратными переходами
Decomposer превращает описание в критерии приёмки. Analyzer исследует код и возвращает область влияния, файлы и подтверждающий контекст. Planner строит шаги, а Architect проверяет план до начала изменений. Coder выполняет работу; Reviewer сопоставляет результат с критериями, архитектурой и требованиями безопасности.
Это логические роли в графе, а не обещание параллельной команды независимых агентов. Они могут использовать разные модели и настройки. Разделение задаёт, какие данные получает этап, какие tools доступны и кто вправе принимать следующее решение. Например, исследовательские роли не получают инструмент применения патчей, а Coder не получает произвольный терминал.
Между ролями передаются структурированные результаты: критерии, пути, пункты плана, замечания и статусы проверок. Schemas и Pydantic-валидаторы проверяют форму и отдельные инварианты этих данных. Смысловую полноту плана оценивает Architect; его отказ возвращает обратную связь Planner. После реализации единый Reviewer проверяет и требования, и архитектуру — отдельного посткодового этапа Architect здесь нет.
- Decomposer → AnalyzerКритерии приёмки, исследование репозитория, область изменений и контекст
- Planner ↔ ArchitectПлан, проверка подхода и ограниченные циклы пересмотра до генерации кода
- Coder sessionChecklist, применение патчей, проверки пунктов и фиксация изменений
- semantic_sync → gates → ReviewerСинхронизация индекса при включённом semantic search; заблокированная или зациклившаяся сессия → planner_amend; завершённая → настроенные gates и Reviewer
- MR или обратная связьОдобренный результат идёт к созданию MR; замечания — в исправление или пересмотр плана
Три режима на общей инфраструктуре
Анализ задачи и проверка уже написанного кода полезны сами по себе. Поэтому operation выбирает маршрут до запуска генерации. Исследование не обязано заканчиваться патчем, а ревью — автоматически исправлять найденное.
У коротких маршрутов свой контракт незавершённости. Если не хватает информации, они возвращают заблокированный отчёт с вопросами. Полный маршрут может отдать need_info, сохранить состояние и продолжиться после уточнения. Для review_only нужна существующая target_branch: предмет проверки уже должен быть в Git.
| Режим | Работа | Результат |
|---|---|---|
| full | Исследование, планирование, реализация, проверки и ревью | Изменения и MR при успешном завершении |
| analyze_only · развивается | Decomposer → init_env → analyzer → отчёт | Критерии, область влияния, файлы и контекст; без coder и MR |
| review_only · развивается | Decomposer (или готовые критерии) → init_env → тесты → smoke → syntax gate → reviewer | Замечания и вердикт по уже существующей ветке; без coder, semantic_sync и MR |
«Без генерации кода» описывает маршрут, а не изоляцию файловой системы: init_env готовит окружение, а проверки могут создавать служебные файлы. Ограничение полномочий роли и изоляция процесса — разные уровни.
Поиск по смыслу, символам и фактическому коду
Один способ поиска плохо покрывает все вопросы к репозиторию. Когда известно имя метода, нужен поиск символа и его связей. Когда известна строка конфигурации — точный текстовый поиск. Когда известно только назначение, например место обработки повторных попыток, полезен semantic_search.
В semantic_search объединены полнотекстовый поиск SQLite FTS5 и векторный поиск sqlite-vec. Результаты сводятся через weighted Reciprocal Rank Fusion: место кандидата в каждой выдаче вносит вклад в общий рейтинг. Индекс хранит фрагмент, путь, строки и символ; результат можно проверить чтением исходника. Это механизм отбора контекста, не исчерпывающий анализ поведения.
CodeGraph дополняет поиск определениями, вызывающими и вызываемыми символами, областью влияния и пакетным explore. search_code ищет точные совпадения, get_file_outline показывает структуру крупного файла, read_file_snippet даёт тело нужного участка. При настроенном подключении доступны list_database_tables, get_table_schema и run_readonly_sql — можно исследовать реальную схему, а не угадывать её по ORM.
Базовый semantic-индекс готовится для worker, затем для задачи создаётся отдельная копия. Изменения синхронизируются с индексом задачи; это отделяет её рабочее состояние от базового. Поисковый индекс остаётся производным от репозитория: его актуальность нужно контролировать, а критичные выводы привязывать к текущему коду.
Live injection: контекст следует за изменениями
После нескольких патчей ранее прочитанный файл уже может не соответствовать диску. Если продолжать опираться на старый tool output, модель строит замену для состояния, которого больше нет. Live injection подставляет актуальное содержимое целевых файлов в системный контекст.
Механизм не ограничен Coder: тот же суффикс получают роли с tool-loop (Analyzer, Architect, Reviewer и подграф Coder), а Planner и planner_refine — один раз на вызов узла, без цикла tools, но с теми же tier-правилами. Чаще всего диск меняется в сессии Coder, поэтому там подстановка обновляется перед каждым вызовом модели; у Reviewer и Architect набор путей обычно фиксируется на сессию, у Analyzer первый inject часто строится как snapshot по seed задачи и уже собранному контексту. Live injection не означает непрерывное наблюдение за всем репозиторием.
Есть два уровня: небольшой файл подставляется целиком, крупный — в виде outline. Для полностью подставленного файла повторное чтение блокируется с пояснением, где уже лежит содержимое; для outline чтение тел методов остаётся доступным. Лимиты числа файлов и размера, а также метрики подстановки делают стоимость этого механизма видимой.
В общем invoke_llm_with_tools перед новым вызовом старое напоминание о файлах в контексте удаляется и добавляется одно актуальное. В текущем run_session_loop Coder отдельного такого напоминания нет: перед каждым вызовом модели обновляется системное сообщение с актуальными файлами.
Память исследования и память выполнения
История вызовов растёт быстрее полезных выводов. У Analyzer, Architect и Reviewer есть write_scratchpad: роль сохраняет найденные факты, пути и открытые вопросы вне скользящего окна сообщений. При pruning старые пары вызов–ответ удаляются целиком; tombstone сообщает об удалении и указывает на scratchpad.
Tombstone отмечает удаление старых tool calls и ответов, но не пересказывает их содержимое. Найденные факты сохраняются отдельно в scratchpad; при пустом scratchpad перед pruning отправляется предупреждение. Для итогового синтеза код собирает handoff summary: критерии приёмки, сохранённые заметки, прочитанные диапазоны и заявленные файлы. На основе этой сводки Analyzer формирует структурированный результат без передачи всей истории tools.
У coder-сессии другая единица памяти — закрытый пункт checklist. После его завершения история заменяется сводкой прогресса: checklist, завершённые пункты и заметки к ним, прочитанные файлы и следующий пункт. Сводка собирается кодом, без отдельного вызова LLM для суммаризации. Изменённые файлы снова подставляются с диска; нужные подробности можно прочитать заново.
План как исполняемый checklist
Текущий Coder работает с планом как с checklist в одной сессии. complete_item запускает проверку пункта и, при включённой фиксации шагов, создаёт commit. Ошибка возвращается в ту же сессию; изменения остаются на диске, чтобы исправить их на месте. Успешно закрытые пункты не нужно генерировать повторно при обычном пересмотре остатка плана.
Для адаптации есть явные действия. update_plan добавляет, удаляет или переставляет пункт с причиной; request_file запрашивает расширение области записи. report_plan_blocker сообщает, что предпосылка плана неверна, с указанием доказательств в коде. Эти решения сохраняются и доходят до Reviewer.
finish разрешён, когда открытых пунктов нет и рабочее дерево чистое. already_done тоже проверяется: нельзя закрыть задачу одной фразой о том, что всё уже реализовано. Система различает код, существовавший до задачи, и результат предыдущего commit этой задачи.
- Открытый пункт + актуальные файлыChecklist определяет текущую работу, live injection — видимое состояние кода
- Tools → complete_itemПатчи применяются на диск; проверка пункта возвращает конкретную причину отказа
- Ошибка → исправление в той же сессииКод остаётся на диске; повторение без изменений контролируется loop breaker
- Успех → commit → запись о прогрессеПри включённых commit шаг фиксируется; transcript закрытого пункта уплотняется
- Следующий пункт или finishДля завершения checklist должен быть закрыт, рабочее дерево — чистым
Что проверяет код, а что остаётся решением модели
Часть отказов определяется без LLM. До архитектурного review проверяются пустые шаги, отсутствие плана при заданных критериях и ссылки на отсутствующие файлы из области изменения. Валидация схем проверяет структуру результатов и разделение изменяемых и контекстных путей. Нет смысла тратить вызов модели на условие, которое можно однозначно проверить кодом.
В complete_item проверяются фактические изменения, lint изменённых файлов и недопустимые TODO-only патчи. У отчётного пункта должен быть отчёт и не должно быть незавершённых изменений исходников. Проверки целей плана могут вернуть блокировку или предупреждение; наличие символа не доказывает, что его поведение правильно. Замечания передаются дальше для смысловой проверки.
Отдельный механизм касается переопределения методов. По индексу кода находится родительский метод и извлекаются видимые выходы: возврат значения, ранний возврат без значения, исключение, делегирование родителю. Контракт показывается при планировании и проверке изменения. Если родитель останавливает выполнение без возвращаемого false, override не должен случайно продолжить обработку.
Это адресная проверка риска, а не полноценный анализ всех путей исполнения: динамические связи и нераспознанные конструкции остаются вне её возможностей. При отсутствии индекса контекст явно говорит, что проверка не выполнена. Для найденного контракта Coder должен описать обработку выходов; окончательное соответствие проверяется тестами и review.
После Coder граф проходит через semantic_sync. При включённом semantic search узел обновляет индекс задачи; ошибка синхронизации отражается в состоянии как деградация поиска. Заблокированная или зациклившаяся coder-сессия передаёт открытые пункты в planner_amend; завершённые пункты сохраняются. После успешного завершения сессии запускаются настроенные gates: test_runner, smoke (если команда задана), syntax/lint gate и затем Reviewer. Неуспешный обязательный gate не заменяется одобрением Reviewer. Тесты могут быть отключены настройкой или не запуститься — такие состояния различаются в результате, а не обозначаются как пройденный набор тестов.
Оркестрационные промпты как часть протокола
Системный промпт описывает роль и постоянные ограничения. Оркестрационный фрагмент сообщает о конкретном событии выполнения: закончился бюджет исследования, отклонён патч, нужен пересмотр плана или пора вернуть структурированный результат. Эти сообщения хранятся отдельно от кода и подставляются в момент события.
Например, ADJUST_PLAN сообщает Planner, что завершённые шаги уже применены, и передаёт completed_plan_steps вместе с failed_step_context. Planner должен составить только оставшуюся работу. Для FULL_RESET нужен другой контекст: состояние после сброса отличается, и одинаковое сообщение привело бы к неверным предпосылкам.
При исчерпании inspection-бюджета Reviewer получает число доступных вызовов и требование перечислить непроверенные критерии. Если проверка не закончена, результат не должен становиться approved. Код дополнительно обрабатывает исчерпание лимита; текст объясняет модели причину и ожидаемую форму ответа.
Так же оформлены сообщения об ошибках patch validation, повторах tools и пустом scratchpad перед pruning. Они дают действие для исправления, а не просто «попробуй ещё раз». Общие правила не нужно копировать в каждый фрагмент; их согласованность с доступными tools и валидаторами проверяется тестами.
ADJUST_PLAN
completed_plan_steps → preserve applied work
failed_step_context → revise remaining work
INSPECTION BUDGET EXHAUSTED
unverified criteria → incomplete review
approved → false
Роль, правила пайплайна, стандарты проекта и исследуемые данные разделены явно. Содержимое репозитория и tool output не должны менять полномочия роли или отменять проверки. XML-границы помогают обозначить происхождение данных; реальные ограничения обеспечиваются также составом tools, валидацией путей и кодом выполнения. Одна разметка не даёт гарантии защиты от prompt injection.
Динамические лимиты и circuit breaker
Одинаковый бюджет не подходит короткому исследованию и задаче с несколькими критериями. Для Analyzer, Architect и Reviewer лимит tools вычисляется как base + per_criterion × число критериев, с верхней границей. У текущей Coder-сессии бюджет зависит от числа открытых пунктов. Это настраиваемая эвристика распределения ресурсов; количество критериев не является точной мерой сложности.
Лимит ограничивает стоимость, но сам по себе не распознаёт тупик. ToolLoopGuard отслеживает повторяющиеся сигнатуры tools и аргументов, включая чередующиеся последовательности. Отдельно обнаруживаются одинаковые ошибки tools, избыточные чтения и последовательные микрочтения. Правила учитывают роль и изменения на диске, чтобы повтор после патча не путать с повтором на неизменном состоянии.
На уровне Coder есть ещё один сигнал: одинаковый отказ complete_item при неизменном diff. Такая сессия завершается как looping и передаёт проблему planner_amend. У review findings отслеживается отсутствие прогресса между раундами исправления; закрытие других замечаний не считается тем же тупиком.
Реакция зависит от причины. Исследование может завершиться синтезом накопленных данных без новых tools. Неполная проверка не получает автоматического одобрения. Неверный пункт возвращается на пересмотр; инфраструктурный отказ не маскируется под необходимость переписать алгоритм. Есть пределы рестартов, amendments и раундов review, а также общий token budget и предельная глубина графа.
research_budget = min(max_calls, base + per_criterion * criteria_count)
coder_budget = min(max_calls, base + per_item * open_items)
Ранние выходы и сохранение полезной работы
Успешное исследование заканчивается через finish_exploration: проверяется заявленная область файлов, после чего результат передаётся на структурированный синтез. Нет требования израсходовать все tool calls. В полном маршруте недостаток данных может привести к need_info, а ложная предпосылка плана — к blocker с привязкой к исходнику.
Planner_amend сохраняет завершённые пункты и меняет открытые. В стратегическом пересмотре ADJUST_PLAN сохраняет уже сделанный код, а FULL_RESET сбрасывает рабочее состояние и строит план заново. Это разные решения с разной ценой; обычный отказ проверки не должен автоматически уничтожать весь прогресс.
Пауза для уточнения и доставка результата — ещё один уровень. Checkpoint графа позволяет продолжить полный маршрут после ответа пользователя. В RabbitMQ входящее сообщение подтверждается после успешной публикации результата; временный отказ публикации может привести к повторной доставке. Это at-least-once, поэтому task_id нужен потребителю как ключ идемпотентности, а не как обещание exactly-once исполнения.
Масштабирование организовано несколькими worker-процессами с отдельными Git-окружениями. Один процесс выполняет одну задачу за раз; параллельные graph.invoke внутри него не являются поддерживаемой моделью. Эта граница важна для состояния, ContextVar и соединения с checkpoint-хранилищем.
Как проверяется сама система
Тестирование сгенерированного приложения и тестирование worker — разные задачи. Для worker проверяются маршруты графа, схемы результатов, loop breakers, pruning, актуальность подставляемых файлов, правила закрытия пунктов и ограничения tools. В CI есть pytest, Ruff, Mypy и Gitleaks; LLM и embeddings в обычных тестах замоканы.
Такие тесты позволяют воспроизводимо проверить, что review_only не попал в coder, повторная ошибка остановила сессию, а незакрытый checklist не позволил вызвать finish. Они не измеряют, насколько хорошо реальная модель найдёт ошибку или реализует незнакомую задачу. Для этого нужен отдельный eval на задачах с проверяемыми результатами.
Для eval есть отчёт с отпечатками кода, промптов и конфигурации. Сравнение доли успешных прогонов имеет смысл только при известном составе задачи, модели, проверок и настроек. Образцы отчётов в репозитории синтетические; они иллюстрируют формат и не являются измерениями эффективности.
Есть и тест размера постоянной части промптов по ролям. Новое правило конкурирует с кодом задачи за контекст; его добавление должно укладываться в бюджет. Рост промптов, как и рост числа retries, не должен быть незаметным способом компенсировать каждую неудачу.
Для разбора прогонов включено сквозное трассирование выполнения: переходы узлов, вызовы моделей и tools, снимки состояния на границах. По журналу можно восстановить последовательность вызовов, переданный контекст, результаты tools и причины переходов между этапами. Интеграция с Langfuse в планах; она дополнит уже существующую картину, а не заменит смысл отдельных gates и отчётов.
Границы решения
Это не универсальный «один промпт — один агент» на любую задачу, не замена code review человеком и не доказательство корректности кода силой модели. Coder не получает произвольный shell; роли в графе идут по контрактам и gates, а не как параллельная «команда» независимых агентов. Язык и стек — это профиль целевого репозитория: в конфиге проекта задаются установка зависимостей, команда тестов, smoke, discovery inline-lint и пресет классификатора падений тестов; в промптах и standards — правила языка и команды. Сейчас основной боевой профиль PHP, но граф тот же: меняется связка конфиг + языковые стандарты, а не оркестратор.
Мультиролевой граф требует дополнительных вызовов и времени. Раннее завершение исследования и настраиваемые бюджеты ограничивают затраты внутри выбранного маршрута. Режимы analyze_only и review_only выбираются по требуемому результату: анализ задачи или проверка существующих изменений без запуска генерации кода. Разделение ролей не гарантирует независимости их ошибок, особенно если они используют одну модель и общий исходный контекст.
В работе над целевым проектом ответственность разделена: детерминированные проверки контролируют структуру плана, изменения файлов и результаты команд; Architect оценивает предложенный подход до реализации; Reviewer проверяет, соответствуют ли изменения требованиям, архитектуре и ограничениям безопасности, а тесты — проверяемому поведению. Эвристические замечания и предупреждения сохраняются в отчёте отдельно от результатов обязательных проверок.
В этом решении я считаю важным сохранять связь между действием и основанием: какой код исследован, какой пункт закрыт, какая проверка прошла, почему понадобился пересмотр и почему выполнение остановилось. Именно эта связь позволяет разбирать неудачи и менять механизм на конкретном участке, вместо добавления ещё одного общего запрета в промпт.