Продакшн-разработка ИИ-агентов · Модуль 3 · Урок 3.1

Глава 5. Паттерны агентного рассуждения

Цели главы

После этой главы вы сможете:

  • Различать пять базовых паттернов рассуждения — ReAct, plan-and-execute, рефлексию (reflection), routing и orchestrator-workers — и понимать, какую проблему решает каждый.
  • Выбирать паттерн по инженерным критериям (предсказуемость задачи, длина, цена ошибки, латентность, стоимость), а не по моде.
  • Видеть минусы ReAct и понимать, почему «всегда ReAct» — антипаттерн уровня архитектуры.
  • Реализовать на Go routing (LLM-классификатор → ветвление на нужный путь/модель/инструмент) и reflection-петлю (генерация → критика → доработка).
  • Останавливать петли рассуждения по бюджету шагов и порогу качества, а не «по ощущению».

Что нового (дельта к базовому курсу)

В базовом курсе агент был один цикл (agentic loop): спросили модель → выполнили инструмент → вернули наблюдение → повторили. Этого достаточно, чтобы агент работал, и неявно это и есть ReAct (reasoning + acting).

Здесь мы делаем шаг вверх: цикл — это не единственная форма рассуждения, а лишь одна из нескольких. Паттерн рассуждения становится осознанным проектным решением. Дельта в трёх вещах. Во-первых, мы перестаём считать ReAct ответом по умолчанию и разбираем его слабые места. Во-вторых, добавляем паттерны, которых в базовом курсе не было: plan-and-execute (сначала весь план, потом исполнение), рефлексию (агент критикует и переделывает собственный вывод), routing (классификатор направляет запрос на нужный путь) и orchestrator-workers (координатор декомпозирует задачу и раздаёт куски). В-третьих, вводим критерии выбора — таблицу, по которой паттерн подбирается под задачу, а не наоборот.

ReAct: чередование мысли, действия и наблюдения

ReAct (reasoning + acting) — это паттерн, где модель чередует три вида шагов: мысль (thought) — рассуждение о том, что делать; действие (action) — вызов инструмента; наблюдение (observation) — результат инструмента, возвращённый обратно в контекст. Затем снова мысль, действие, наблюдение — пока задача не решена. Именно это и происходит в базовом agentic loop: модель сама на каждом ходу решает следующий шаг, опираясь на накопленные наблюдения.

Почему ReAct так популярен: он гибок и не требует знать план заранее. Для открытых задач (исследование, отладка, навигация по незнакомым данным), где число и состав шагов неизвестны до запуска, это идеальная форма — агент реагирует на промежуточные результаты.

Но у ReAct есть реальные минусы, о которых базовый курс умалчивает. - Близорукость (myopia). Модель решает следующий шаг локально, не держа в голове весь маршрут. На длинных задачах это ведёт к блужданию, тупикам и зацикливанию. - Накопление контекста. Каждое наблюдение оседает в окне. Через 20–30 шагов контекст распухает, растёт стоимость и проявляется context rot — модель «забывает» начало задачи. - Latency и цена. Каждый шаг — отдельный round-trip к модели. Десять шагов — десять вызовов, последовательно. Для предсказуемой задачи это расточительно. - Слабая воспроизводимость. Траектория недетерминирована; один и тот же запрос может пойти разными путями, что усложняет отладку и тестирование.

Вывод: ReAct хорош там, где гибкость важнее предсказуемости. Если задача предсказуема — есть формы лучше.

Plan-and-execute: сначала план, потом исполнение

Plan-and-execute разносит рассуждение и действие во времени. Сначала модель строит полный план — упорядоченный список шагов для решения задачи. Затем отдельная фаза исполняет шаги (каждый шаг может быть простым вызовом инструмента или даже мини-ReAct-петлёй). План при необходимости пересматривается, если шаг провалился (re-planning).

Зачем так. Главная болезнь ReAct — близорукость; план лечит её глобальным взглядом: модель один раз продумывает маршрут целиком, держа всю задачу в голове, и дальше просто следует ему. Плюсы для длинных задач: меньше блужданий, понятная структура (план можно показать человеку и согласовать до запуска), исполнение шагов можно распараллелить и удешевить (мелкие шаги — на дешёвой модели). Минус: план составляется по неполной информации. Если реальность расходится с планом, нужен механизм пере-планирования, иначе агент упрямо исполняет устаревший маршрут. Поэтому plan-and-execute уместен, когда задача длинная и её структура в целом предсказуема, а не когда каждый шаг радикально меняет картину.

Рефлексия и самокоррекция (reflection / self-critique)

Рефлексия — это петля, где агент проверяет собственный вывод и переделывает его. Минимальная форма из трёх ролей: генератор производит черновой ответ; критик оценивает его по заданным критериям и формулирует конкретные замечания; генератор дорабатывает ответ с учётом критики. Цикл повторяется, пока критик не удовлетворён или не исчерпан бюджет итераций.

Почему это работает. Модели систематически лучше находят ошибки в готовом тексте, чем пишут безошибочно с первого раза — проверять легче, чем сочинять. Разнесение ролей даёт «свежий взгляд»: критику полезно давать в отдельном контексте (или отдельным вызовом), чтобы он не был ангажирован собственным черновиком. Где рефлексия особенно ценна: код (можно прогнать тесты и вернуть ошибки как критику — это объективный сигнал, а не мнение модели), генерация по строгому формату, фактологические ответы.

Грабли: рефлексия удваивает-утраивает стоимость и латентность, и без объективного критика (тесты, валидатор схемы, эталон) рискует превратиться в «эхо» — критик хвалит черновик, не находя реальных проблем. Поэтому критерии критики должны быть конкретными и проверяемыми, а число итераций — ограниченным.

Routing и orchestrator-workers

Routing (маршрутизация) — это не петля рассуждения, а развилка перед ней. Лёгкий классификатор (отдельный быстрый вызов модели или даже простая модель) смотрит на входящий запрос и решает, по какому пути его пустить: какой системный промпт, какая модель, какой набор инструментов, какой агент. Зачем: разные классы запросов требуют разного — отправлять простой вопрос в дорогой многошаговый агент расточительно, а сложную задачу в дешёвую односложную модель — провально. Routing разделяет потоки и позволяет каждому пути быть оптимальным по цене и качеству. Это один из самых дешёвых и недооценённых паттернов: один маленький классификатор экономит больше, чем любая микрооптимизация внутри агента.

Orchestrator-workers — это динамическая декомпозиция: координатор (orchestrator) на лету разбивает задачу на подзадачи и раздаёт их воркерам (workers), затем собирает результаты. В отличие от plan-and-execute, где план фиксирован заранее, здесь состав подзадач определяется во время выполнения самим координатором. Это мощно для задач, чья структура неизвестна до старта (например, исследование, где число источников заранее не угадать). Прод-аспекты координации, разделяемого состояния и handoffs — тема Главы 6; здесь важно зафиксировать, что это отдельный паттерн рассуждения, а не «просто много агентов».

Как выбирать паттерн: инженерные критерии

Главная мысль главы: выбор паттерна — инженерное решение, а не мода. «Всегда ReAct» — такой же антипаттерн, как «всегда микросервисы». Решайте по характеристикам задачи.

  • Предсказуемость задачи. Структура известна заранее → plan-and-execute или фиксированный пайплайн. Неизвестна → ReAct или orchestrator-workers.
  • Длина (число шагов). Короткая (1–3 шага) → одного вызова или routing хватает. Длинная → план снижает близорукость.
  • Цена ошибки. Высокая (код в прод, юридический текст, финансы) → добавьте рефлексию с объективным критиком. Низкая → лишняя проверка только жжёт деньги.
  • Латентность. Жёсткий бюджет времени → минимизируйте число round-trip: routing на дешёвый путь, избегайте многошаговых петель.
  • Стоимость. Routing отсекает дорогие пути для простых запросов; каскад моделей (дешёвая → дорогая по эскалации) — продолжение той же идеи.

И железное правило из базового курса: берите минимально сложный паттерн, который решает задачу. Один вызов модели лучше ReAct, ReAct лучше orchestrator-workers — пока хватает простого. Сложность добавляйте только под доказанную потребность.

Routing: LLM-классификатор направляет запрос на нужный путь (anthropic-sdk-go v1.x; слаги моделей — сверьтесь с актуальными доками)
// route — один из заранее известных путей обработки запроса.
type route string

const (
    routeSimpleQA  route = "simple_qa"  // короткий вопрос → дешёвая модель, без инструментов
    routeResearch  route = "research"   // открытая задача → агент с поиском (ReAct)
    routeCoding    route = "coding"     // правка кода → агент с рефлексией и тестами
)

// classify — лёгкий отдельный вызов модели: он только КЛАССИФИЦИРУЕТ запрос,
// не решает задачу. Дёшево и быстро, потому что работает на маленькой модели
// и просит ровно один токен-ярлык.
func classify(ctx context.Context, client anthropic.Client, query string) (route, error) {
    // tool_choice с единственным инструментом-классификатором заставляет модель
    // вернуть строго один из enum-вариантов — это надёжнее, чем парсить свободный текст.
    classifier := anthropic.ToolParam{
        Name:        "route",
        Description: anthropic.String("Выбери путь обработки пользовательского запроса"),
        InputSchema: anthropic.ToolInputSchemaParam{
            Properties: map[string]any{
                "route": map[string]any{
                    "type": "string",
                    "enum": []string{
                        string(routeSimpleQA), string(routeResearch), string(routeCoding),
                    },
                    "description": "simple_qa — короткий факт; research — открытое исследование; coding — правка кода",
                },
            },
            Required: []string{"route"},
        },
    }

    resp, err := client.Messages.New(ctx, anthropic.MessageNewParams{
        // Дешёвая быстрая модель: классификация не требует «ума».
        Model:     anthropic.ModelClaudeHaiku4_5,
        MaxTokens: 256,
        Tools:     []anthropic.ToolUnionParam{{OfTool: &classifier}},
        // Принуждаем вызвать именно классификатор, а не отвечать текстом.
        ToolChoice: anthropic.ToolChoiceUnionParam{
            OfTool: &anthropic.ToolChoiceToolParam{Name: "route"},
        },
        Messages: []anthropic.MessageParam{
            anthropic.NewUserMessage(anthropic.NewTextBlock(query)),
        },
    })
    if err != nil {
        return "", err
    }

    for _, block := range resp.Content {
        if variant, ok := block.AsAny().(anthropic.ToolUseBlock); ok {
            var out struct {
                Route string `json:"route"` // тег в обратных кавычках — здесь показан конкатенацией
            }
            if err := json.Unmarshal([]byte(variant.JSON.Input.Raw()), &out); err != nil {
                return "", err
            }
            return route(out.Route), nil
        }
    }
    return routeSimpleQA, nil // безопасный дефолт, если модель не вызвала инструмент
}

// dispatch — само ветвление: каждый путь обрабатывается своим способом.
func dispatch(ctx context.Context, client anthropic.Client, query string) (string, error) {
    r, err := classify(ctx, client, query)
    if err != nil {
        return "", err
    }
    switch r {
    case routeResearch:
        return runResearchAgent(ctx, client, query) // ReAct-петля с поиском
    case routeCoding:
        return runCodingAgent(ctx, client, query)   // генерация + рефлексия (см. ниже)
    default:
        return runSimpleQA(ctx, client, query)      // один дешёвый вызов
    }
}
Reflection-петля: генерация → критика → доработка с объективным критиком и бюджетом итераций
// critique — результат проверки черновика критиком.
type critique struct {
    OK       bool     // `json:"ok"` — критик доволен, доработка не нужна
    Issues   []string // `json:"issues"` — конкретные, проверяемые замечания
}

// reflectLoop реализует рефлексию: чередуем генерацию и критику, пока критик
// не удовлетворён или не исчерпан бюджет итераций. Бюджет ОБЯЗАТЕЛЕН — без него
// петля может крутиться вечно и сжигать деньги.
func reflectLoop(
    ctx context.Context,
    client anthropic.Client,
    task string,
    maxIters int, // жёсткий потолок итераций — условие остановки по бюджету
    objectiveCheck func(draft string) *critique, // объективный критик: тесты, валидатор схемы
) (string, error) {
    draft, err := generate(ctx, client, task, nil) // первый черновик, без замечаний
    if err != nil {
        return "", err
    }

    for i := 0; i < maxIters; i++ {
        // Сначала — ОБЪЕКТИВНАЯ проверка (компиляция, тесты, схема). Это не «мнение
        // модели», а факт: если она проходит, останавливаемся без лишнего вызова.
        if c := objectiveCheck(draft); c != nil {
            if c.OK {
                return draft, nil
            }
            // Замечания объективного критика возвращаем генератору как контекст.
            draft, err = generate(ctx, client, task, c.Issues)
            if err != nil {
                return "", err
            }
            continue
        }

        // Если объективной проверки нет — критикуем отдельным вызовом модели.
        // Отдельный вызов = «свежий взгляд»: критик не ангажирован своим черновиком.
        c, err := critiqueDraft(ctx, client, task, draft)
        if err != nil {
            return "", err
        }
        if c.OK {
            return draft, nil // критик доволен — выходим до исчерпания бюджета
        }
        draft, err = generate(ctx, client, task, c.Issues) // доработка по замечаниям
        if err != nil {
            return "", err
        }
    }
    // Бюджет исчерпан: возвращаем лучший черновик, но честно сигналим вызывающему.
    return draft, errMaxIterations
}

// critiqueDraft просит модель найти КОНКРЕТНЫЕ проблемы. Расплывчатая критика
// («сделай лучше») бесполезна — требуем список проверяемых замечаний.
func critiqueDraft(ctx context.Context, client anthropic.Client, task, draft string) (*critique, error) {
    prompt := "Задача:\n" + task + "\n\nЧерновик:\n" + draft +
        "\n\nНайди конкретные ошибки и недочёты. Если их нет — подтверди готовность. " +
        "Не хвали и не переписывай — только список проверяемых замечаний."
    resp, err := client.Messages.New(ctx, anthropic.MessageNewParams{
        Model:     anthropic.ModelClaudeOpus4_8, // на критику не экономим: цена ошибки высока
        MaxTokens: 2048,
        Messages: []anthropic.MessageParam{
            anthropic.NewUserMessage(anthropic.NewTextBlock(prompt)),
        },
    })
    if err != nil {
        return nil, err
    }
    return parseCritique(resp), nil
}

Anti-patterns

Типичные грабли паттернов рассуждения
ГрабляПочему плохоКак избегать
«Всегда ReAct» по умолчаниюReAct близорук и дорог: на предсказуемых задачах гоняет лишние round-trip и блуждает там, где хватило бы плана или одного вызова.Выбирать паттерн по критериям (предсказуемость, длина, цена ошибки, латентность). Простое — простым вызовом, длинное предсказуемое — планом.
Рефлексия без объективного критикаКритик-модель хвалит собственный черновик («эхо»): итерации идут, стоимость растёт, качество — нет.Привязывать критику к проверяемому сигналу: тесты, валидатор схемы, эталон. Замечания должны быть конкретными, а не «сделай лучше».
Петля рассуждения без бюджета шаговReAct или reflection крутятся вечно: зацикливание, неограниченная стоимость и латентность, риск зависания в проде.Жёсткий потолок итераций и бюджет токенов; выход по достижении порога качества ИЛИ исчерпании бюджета — что наступит раньше.
Классификатор маршрутизации на дорогой моделиRouting должен экономить, а не добавлять стоимость: тяжёлая модель на классификацию съедает выигрыш от разделения путей.Классифицировать дешёвой быстрой моделью с tool_choice и enum-ярлыками; один токен-решение, а не свободный текст.
План исполняется без пере-планированияPlan-and-execute упрямо идёт по устаревшему маршруту, когда реальность разошлась с планом, — и проваливает задачу.Между шагами проверять, актуален ли план; при провале шага запускать re-planning, а не продолжать вслепую.
Парсинг решения классификатора из свободного текстаМодель форматирует ответ непредсказуемо; регэксп ломается, ветвление уходит в неверный путь незаметно.Структурированный вывод через инструмент с enum в схеме; парсить tool_use input как JSON, а не строкой.

Практическое задание

  • Возьмите задачу из своего домена и явно классифицируйте её по пяти критериям выбора (предсказуемость, длина, цена ошибки, латентность, стоимость).
  • Реализуйте routing: классификатор на дешёвой модели (tool_choice + enum) и ветвление минимум на три пути — простой вопрос, открытое исследование (ReAct), правка с проверкой.
  • Для пути «правка» реализуйте reflection-петлю с объективным критиком (например, прогон тестов для кода или валидатор JSON-схемы для структурированного вывода) и бюджетом итераций.
  • Добавьте условия остановки: выход по достижении порога качества ИЛИ по исчерпанию бюджета шагов/токенов; залогируйте, по какой причине вышли.
  • Сравните две стратегии на 10–20 примерах: «всегда ReAct» против routing+рефлексия — измерьте число вызовов модели, латентность и качество.
  • Сделайте вывод в README: для каких классов запросов какой паттерн оказался оптимален и почему — это и есть инженерное обоснование выбора.

Проверка знаний

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

Какой паттерн рассуждения здесь уместнее и почему?

  • A Оставить ReAct, но увеличить max_tokens — близорукость лечится размером контекста.
  • B Plan-and-execute: структура предсказуема, глобальный план снимает близорукость ReAct.
  • C Orchestrator-workers: координатор на лету решит, какие разделы нужны.
  • D Рефлексия: добавить критика, который будет переделывать каждый шаг ReAct.

В reflection-петле для генерации маркетингового текста критиком выступает та же модель, что и генератор, без каких-либо объективных проверок. За три итерации стоимость утроилась, а качество почти не изменилось.

В чём корневая причина и как её устранить?

  • A Слишком мало итераций — нужно поднять потолок до 10.
  • B Критик-модель «эхает» — хвалит черновик; нужен объективный или независимый критик с конкретными критериями.
  • C Нужно убрать критика и вернуться к одному вызову генератора.
  • D Проблема в модели — взять более дорогую и для генератора, и для критика.

Сервис получает поток разнородных запросов: 70% — короткие факты, 20% — открытые исследования, 10% — правки кода. Сейчас всё идёт через один дорогой многошаговый агент, счёт за токены большой.

Какой паттерн даст наибольшую экономию при сохранении качества?

  • A Routing: дешёвый классификатор направляет каждый класс запроса на свой оптимальный путь и модель.
  • B Plan-and-execute для всех запросов — план всегда дешевле.
  • C Рефлексия на всех путях — качество важнее экономии.
  • D Один ещё более мощный агент, чтобы он справлялся со всем сразу.