Агент-инженер по репозиторию · Модуль 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
main.go: каркас agent loop с выходом по stop_reason (модель пока на моке)
+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 — выйти из цикла».

В чём риск такого условия остановки и как сделать надёжнее?

  • A Риска нет, это эквивалентно проверке stop_reason
  • B Условие не отличает успешный end_turn от обрезанного max_tokens; надёжнее ветвиться по stop_reason явно
  • C Нужно увеличить число итераций
  • D Нужно искать маркер «DONE» в тексте

Коллега предлагает убрать лимит maxSteps, аргументируя, что stop_reason и так всегда завершит цикл.

Почему лимит шагов всё равно нужен?

  • A Он ускоряет работу модели
  • B Это страховка: при баге в инструментах или зацикливании модели лимит не даёт сжечь бюджет токенов бесконечно
  • C Без него код не скомпилируется
  • D Лимит заменяет проверку stop_reason