Агент-инженер по репозиторию · Модуль 1 · Урок 1.1
Зачем агент, а не скрипт: цикл и условие остановки
Зачем (какую проблему чиним)
Мы строим агента-инженера по репозиторию: он берёт задачу, изучает код, правит его, гоняет тесты и открывает MR. Первый вопрос — почему это не просто скрипт «один запрос к модели → один ответ»?
Потому что инженерная задача требует многих шагов с обратной связью: прочитать файл → понять → посмотреть тест → запустить → увидеть ошибку → исправить. Модель не знает заранее, сколько шагов нужно и какие. Значит, нужен цикл (agent loop): модель на каждом ходу либо отвечает финально, либо просит выполнить инструмент; мы выполняем и возвращаем результат; повторяем. Ключевой вопрос версии v0 — когда цикл завершать. Ошибиться здесь — значит либо обрезать корректную работу, либо зациклиться навсегда.
Решение и альтернативы
Решение: завершать цикл строго по структурному признаку — полю stop_reason в ответе API. Если модель прислала stop_reason == "tool_use", она просит инструмент — выполняем и продолжаем. Любой другой stop_reason (end_turn, max_tokens, stop_sequence) означает, что ход модели завершён, — выходим. Дополнительно держим лимит итераций maxSteps как страховку от зацикливания, а не как основной критерий выхода.
Альтернативы, которые мы отвергаем:
- Искать в тексте маркер вроде «DONE». Формулировки недетерминированы; модель может написать «готово» в середине рассуждения или не написать вовсе. Хрупко.
- Фиксированное число итераций как основной критерий. Либо обрежет задачу на середине, либо потратит лишние вызовы. Это страховка, а не логика завершения.
- «Нет блоков tool_use → выходим». Близко к правде, но
stop_reason— прямее и надёжнее: он покрывает иmax_tokens(ответ обрезан — это не успешное завершение, реагировать надо иначе).
DIFF
Заводим модуль и каркас цикла. Вызов модели пока абстрактен (подключим в уроке 1.2) — здесь фиксируем форму цикла и условие остановки.
⚠ Безопасность
Инвариант версии v0, который мы не нарушим до версии v3: агент только читает. Никаких записей в файлы, коммитов и команд, меняющих состояние. Это осознанный порядок: сначала научим агента безопасно наблюдать, и только потом — действовать. Лимит maxSteps — это ещё и предохранитель стоимости: забытый цикл без верхней границы способен сжечь бюджет токенов за минуты.
Проверка
go run . должен собраться и напечатать заглушку хода цикла. Реального вызова модели ещё нет — проверяем, что структура компилируется и цикл корректно выходит по условию остановки на моке.
Глубже
Базовая теория цикла и условия остановки — в курсе «Разработка ИИ-агентов» (agent loop, tool calling, история сообщений). Продакшн-обвязка цикла (бюджеты шагов/токенов, durable-исполнение, идемпотентность) — в курсе «Продакшн-разработка ИИ-агентов», Модуль 1 (гл. 1). Сюда мы вернёмся в версии v5.
mkdir repo-agent && cd repo-agent
go mod init github.com/you/repo-agent
git init+package main
+
+import (
+ "context"
+ "fmt"
+)
+
+// stopReason — структурный признак завершения хода модели. В версии 1.2 это
+// поле придёт из anthropic-sdk-go (resp.StopReason); пока моделируем сами.
+type stopReason string
+
+const (
+ stopToolUse stopReason = "tool_use" // модель просит инструмент — продолжаем
+ stopEndTurn stopReason = "end_turn" // ход завершён — выходим
+)
+
+// step — один ответ модели: либо запрос инструментов, либо финальный текст.
+type step struct {
+ Reason stopReason
+ Text string
+}
+
+// callModel — заглушка. В уроке 1.2 заменим на вызов anthropic-sdk-go.
+func callModel(ctx context.Context, history []string) (step, error) {
+ return step{Reason: stopEndTurn, Text: "(заглушка) пока без модели"}, nil
+}
+
+func runAgent(ctx context.Context, task string) (string, error) {
+ const maxSteps = 25 // страховка от зацикливания, НЕ критерий выхода
+ history := []string{task}
+ for i := 0; i < maxSteps; i++ {
+ s, err := callModel(ctx, history)
+ if err != nil {
+ return "", err
+ }
+ // Единственный корректный критерий завершения хода.
+ if s.Reason != stopToolUse {
+ return s.Text, nil
+ }
+ // (В уроке 1.3) здесь исполним инструменты и добавим результат в history.
+ history = append(history, "(результат инструмента)")
+ }
+ return "", fmt.Errorf("превышен предел в %d шагов", maxSteps)
+}
+
+func main() {
+ out, err := runAgent(context.Background(), "Что делает функция main в main.go?")
+ if err != nil {
+ fmt.Println("ошибка:", err)
+ return
+ }
+ fmt.Println(out)
+}Anti-patterns
| Грабля | Почему плохо | Как правильно |
|---|---|---|
| Завершать цикл по текстовому маркеру («done», «готово») | Формулировки недетерминированы: модель может не написать маркер или вставить его в середину рассуждения | Проверять только stop_reason: tool_use — продолжаем, иначе — выходим |
| Фиксированное число итераций как основной критерий выхода | Молча обрезает корректную работу или жжёт лишние вызовы | maxSteps — только страховка от зацикливания; логика выхода — по stop_reason |
Игнорировать stop_reason == "max_tokens" и считать ход успешным | Ответ обрезан на полуслове; продолжать так, будто всё хорошо, — потерять данные | Любой не-tool_use — это конец хода; max_tokens обрабатывать отдельно (увеличить лимит/попросить продолжить) |
| Цикл без верхней границы шагов | Один баг в логике инструментов — и агент крутится, сжигая бюджет токенов | Жёсткий maxSteps + (в версии v5) бюджет токенов на задачу |
Практическое задание (RA-v0)
- Инициализировать репозиторий агента:
go mod init,git init. - Реализовать
runAgentс циклом, где единственный критерий выхода —stop_reason != tool_use, аmaxSteps— страховка. - Закоммитить каркас:
git add -A && git commit -m "v0: agent loop skeleton". Тег версии поставим в уроке 1.4.
Проверка знаний
Агент-инженер реализован с проверкой: «если в ответе модели нет блоков tool_use — выйти из цикла».
В чём риск такого условия остановки и как сделать надёжнее?
Верный ответ: B
B. Отсутствие tool_use действительно означает конец хода, но не объясняет, какой именно: end_turn (успех) и max_tokens (ответ обрезан) требуют разной реакции. Явная ветка по stop_reason это различает. Увеличение итераций (C) к условию выхода не относится; текстовый маркер (D) — антипаттерн.
Коллега предлагает убрать лимит maxSteps, аргументируя, что stop_reason и так всегда завершит цикл.
Почему лимит шагов всё равно нужен?
Верный ответ: B
B. stop_reason — корректная логика выхода, но она зависит от поведения модели и инструментов. Лимит шагов — независимый предохранитель от зацикливания и неконтролируемых расходов. Он не ускоряет модель (A), не нужен для компиляции (C) и не заменяет stop_reason (D).