Почему первый ответ не всегда лучший

Представьте, что вы просите агента написать технический документ или сгенерировать SQL-запрос к незнакомой схеме. LLM отвечает быстро и уверенно — но этот ответ содержит неточности, пропущенные детали или логические ошибки.

Проблема не в том, что модель «не умеет» — она умеет. Проблема в режиме работы: при генерации модель двигается слева направо, токен за токеном, не возвращаясь назад. Это эффективно, но не оптимально для задач, где важно качество, а не скорость первого ответа.

Исследования показывают, что LLM лучше оценивают, чем генерируют с первой попытки. Дать модели задачу «найди ошибки в этом тексте» когнитивно проще, чем «напиши идеальный текст». Reflection использует это свойство: разделяет генерацию и оценку на отдельные API-вызовы, чтобы каждый выполнялся в своём оптимальном режиме.

📄 Академическая база

Паттерн формализован в двух статьях: Self-Refine (Madaan et al., 2023) — итеративное самоулучшение через feedback, и Reflexion (Shinn et al., 2023) — хранение вербальных рефлексий между запусками агента. Оба подхода показали улучшение качества на 10–40% на стандартных бенчмарках.

Как устроен Reflection

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

100%
колёсико — масштаб  ·  зажать и тянуть — перемещение
① ГЕНЕРАЦИЯ ② КРИТИКА ③ РЕЗУЛЬТАТ Задача Генератор LLM Черновик v1 / v2 / … Критик LLM OK? Финальный ответ Критика score · issues · suggestions Ревизор LLM да ✓ нет ревизия v(N+1)

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

Обратите внимание на дизайн: Ревизор — это тот же тип вызова, что и Генератор, но получает на вход не только задачу, а также черновик и структурированную критику. После ревизии цикл начинается заново: новый черновик идёт к Критику.

💡 Генератор и Ревизор — один и тот же LLM

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

Самокритика и внешний критик

Существует два основных подхода к организации критика: самокритика (та же модель, другой промпт) и внешний критик (отдельный вызов с ролью строгого проверяющего). На практике различие размыто — оба случая реализуются через отдельные API-вызовы.

Критерий Самокритика Внешний критик
Системный промпт Тот же или с добавлением «проверь свой ответ» Отдельный промпт с ролью эксперта/редактора
Строгость средняя — модель мягче к своим ответам выше — другая роль снимает самоцензуру
Стоимость ниже — один тип промпта выше — но можно использовать меньшую модель
Предпочтительно для Быстрой проверки, простых задач Задач с чёткими критериями качества

Независимо от подхода, важно отделить вызов критика от вызова генератора в истории сообщений. Если добавить «а теперь проверь своё» в то же сообщение — модель уже зафиксировала ответ и будет склонна его защищать, а не критиковать. Отдельный API-вызов создаёт «чистый лист».

Анатомия хорошей критики

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

Плохая критика
«Текст недостаточно хорошего качества»
«Нужно улучшить стиль и содержание»
«Не полностью раскрыта тема»
Структурированная критика
score: 4/10, passed: false
issues: ["Нет метрик производительности",
  "Не указана целевая аудитория",
  "Отсутствуют примеры использования"]

suggestions: ["Добавь сравнительную таблицу",
  "Укажи время отклика в мс"]

Для получения структурированной критики используем tool_choice: "tool" с принудительным вызовом инструмента-критика — так же, как делали с планировщиком в Plan-and-Execute:

CRITIQUE_TOOL = {
    "name": "critique",
    "description": "Оцени качество текста по заданным критериям.",
    "input_schema": {
        "type": "object",
        "properties": {
            "passed": {
                "type": "boolean",
                "description": "True если качество достаточное (score >= 8)"
            },
            "score": {
                "type": "integer",
                "minimum": 1,
                "maximum": 10,
                "description": "Оценка качества от 1 до 10"
            },
            "issues": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Конкретные проблемы — не 'плохо', а 'нет примеров'"
            },
            "suggestions": {
                "type": "array",
                "items": {"type": "string"},
                "description": "Конкретные предложения по улучшению"
            }
        },
        "required": ["passed", "score", "issues", "suggestions"]
    }
}
⚠️ passed ≠ (score == 10)

Не требуй идеального результата. Определи порог (например, score ≥ 8 и ни одной критической проблемы) и используй его как критерий passed=True. Иначе агент застрянет в бесконечных микроулучшениях.

Реализация цикла

Сначала реализуем два базовых строительных блока — функцию генерации (она же ревизии) и функцию критики. Затем соберём из них полный цикл.

import anthropic

client = anthropic.Anthropic()

GENERATOR_SYSTEM = """Ты — экспертный автор. Создавай качественный, точный контент.
Если в контексте есть результаты предыдущей критики — учти их при написании."""


def generate(task: str, critique_context: str = "") -> str:
    """Генерирует черновик или ревизию (с учётом критики)."""
    system = GENERATOR_SYSTEM
    if critique_context:
        system += f"\n\n─── Предыдущая версия и критика ───\n{critique_context}"

    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=2048,
        system=system,
        messages=[{"role": "user", "content": task}]
    )
    return response.content[0].text


def critique(task: str, draft: str) -> dict:
    """Возвращает структурированную оценку черновика."""
    response = client.messages.create(
        model="claude-opus-4-6",
        max_tokens=512,
        system=(
            "Ты — строгий технический редактор. Оценивай объективно.\n"
            "passed=True только если score >= 8 и нет серьёзных проблем.\n"
            "Указывай конкретные проблемы, не абстракции."
        ),
        tools=[CRITIQUE_TOOL],
        tool_choice={"type": "tool", "name": "critique"},
        messages=[{
            "role": "user",
            "content": f"Задача: {task}\n\nТекст для оценки:\n{draft}"
        }]
    )
    for block in response.content:
        if block.type == "tool_use" and block.name == "critique":
            return block.input
    # fallback если что-то пошло не так
    return {"passed": True, "score": 8, "issues": [], "suggestions": []}

Теперь собираем полный цикл:

def critique_and_revise(task: str, max_rounds: int = 3) -> str:
    """
    Итеративно генерирует, критикует и улучшает текст.
    Возвращает лучший черновик: либо прошедший критику, либо после max_rounds.
    """
    # Раунд 1: первоначальная генерация
    draft = generate(task)
    print(f"[Раунд 1] Черновик: {len(draft)} символов")

    for round_num in range(1, max_rounds + 1):
        result = critique(task, draft)
        score   = result.get("score", 0)
        passed  = result.get("passed", False)
        issues  = result.get("issues", [])
        suggestions = result.get("suggestions", [])

        print(f"[Раунд {round_num}] Критика: score={score}/10, "
              f"issues={len(issues)}, passed={passed}")

        if passed:
            print(f"  ✓ Принято на раунде {round_num}")
            return draft

        if round_num == max_rounds:
            print(f"  ⚠ Достигнут лимит {max_rounds} раундов")
            return draft

        # Формируем контекст для ревизора
        critique_text = (
            f"Черновик (раунд {round_num}):\n{draft}\n\n"
            f"Оценка: {score}/10\n"
            f"Проблемы:\n"
            + "\n".join(f"  - {i}" for i in issues)
            + f"\nПредложения:\n"
            + "\n".join(f"  - {s}" for s in suggestions)
        )
        draft = generate(task, critique_context=critique_text)
        print(f"[Раунд {round_num + 1}] Ревизия: {len(draft)} символов")

    return draft


# Использование
result = critique_and_revise(
    task="Напиши описание продукта для беспроводных наушников с ANC",
    max_rounds=3
)

Посмотрим, как цикл выглядит на конкретном примере:

1
Раунд 1 · Генерация
«Беспроводные наушники с шумоподавлением. Хороший звук и долгая батарея. Купите сейчас!»
Раунд 1 · Критика
score: 3/10 · passed: false
Нет технических характеристик (уровень шумоподавления, ёмкость батареи)
Не указана целевая аудитория и сценарии использования
«Купите сейчас!» — рекламный клише без ценности
Добавь: -40дБ ANC, 30ч battery, задержка 24мс
Опиши реальный сценарий: удалённая работа, перелёты
2
Раунд 2 · Ревизия
«SoundPro ANC — активное шумоподавление -40дБ, 30 часов без подзарядки, задержка 24мс. Для удалённой работы и перелётов.»
~
Раунд 2 · Критика
score: 7/10 · passed: false
Нет эмоционального крючка в начале — читатель не вовлекается
Начни с боли: «Представьте переговоры в шумном кафе…»
3
Раунд 3 · Ревизия
«Переговоры в шумном кафе — и собеседник слышит только вас. SoundPro ANC с шумоподавлением -40дБ превращает любое место в переговорную. 30 ч батарея, задержка 24мс.»
Раунд 3 · Критика → Принято
score: 9/10 · passed: true ✓
Эмоциональный крючок, технические характеристики, сценарий использования — всё на месте.
Критик дешевле генератора

Функция critique() использует max_tokens=512, тогда как generate()max_tokens=2048. Для критика подойдёт более лёгкая модель (например, claude-haiku-4-5), что сокращает стоимость всего цикла при сохранении качества.

Рефлексия между попытками: Reflexion-паттерн

Critique-and-Revise улучшает один ответ за несколько раундов внутри одной задачи. Паттерн Reflexion (Shinn et al., 2023) идёт дальше: после неудачной попытки агент формирует вербальную рефлексию — «что именно пошло не так» — и сохраняет её в памяти. При следующей попытке эти рефлексии добавляются в системный промпт. Агент «помнит свои ошибки».

  Попытка 1 → Неудача → Рефлексия: «Я не проверил граничные случаи»
                              ↓ (сохранено)
  Попытка 2 → Неудача → Рефлексия: «Забыл обработать пустой ввод»
                              ↓ (добавлено к предыдущей)
  Попытка 3 → Успех ✓  (агент уже знает про оба подводных камня)
        

Это особенно полезно для задач, где результат поддаётся объективной проверке: выполнение тестов, SQL-запросы к схеме, алгоритмические задачи. Рефлексия фиксирует не только «что не так», но и «почему» — чтобы следующая попытка шла другим путём, а не повторяла ту же ошибку.

from dataclasses import dataclass, field

@dataclass
class ReflexionAgent:
    """
    Накапливает вербальные рефлексии между попытками.
    Каждая неудача пополняет память, которая инжектируется в следующий запуск.
    """
    base_system: str
    reflections: list[str] = field(default_factory=list)
    max_stored_reflections: int = 5  # не накапливать слишком много

    def _system_with_reflections(self) -> str:
        if not self.reflections:
            return self.base_system
        recent = self.reflections[-self.max_stored_reflections:]
        notes = "\n".join(f"{i+1}. {r}" for i, r in enumerate(recent))
        return (
            f"{self.base_system}\n\n"
            f"─── Уроки из прошлых попыток ───\n"
            f"Я уже пробовал решить подобную задачу. Вот что узнал:\n{notes}"
        )

    def attempt(self, task: str) -> str:
        """Одна попытка решить задачу (здесь — простая генерация, можно заменить ReAct)."""
        response = client.messages.create(
            model="claude-opus-4-6",
            max_tokens=2048,
            system=self._system_with_reflections(),
            messages=[{"role": "user", "content": task}]
        )
        return response.content[0].text

    def generate_reflection(self, task: str, failed_result: str) -> str:
        """Генерирует краткую рефлексию о причинах неудачи."""
        response = client.messages.create(
            model="claude-opus-4-6",
            max_tokens=200,
            system=(
                "Ты анализируешь неудачные попытки. "
                "Напиши 1–2 конкретных предложения: что пошло не так "
                "и как нужно действовать иначе. Без воды."
            ),
            messages=[{
                "role": "user",
                "content": f"Задача: {task}\n\nНеудачный результат:\n{failed_result}"
            }]
        )
        reflection = response.content[0].text.strip()
        self.reflections.append(reflection)
        return reflection

    def run(self, task: str, is_correct, max_attempts: int = 3) -> str:
        """
        Запускает попытки с рефлексией между ними.

        is_correct: callable(result: str) -> bool
        Для объективных задач (тесты, запросы) — проверяй автоматически.
        Для субъективных — используй LLM-критика из critique_and_revise.
        """
        result = ""
        for n in range(1, max_attempts + 1):
            print(f"\n=== Попытка {n}/{max_attempts} ===")
            result = self.attempt(task)

            if is_correct(result):
                print(f"✓ Успех на попытке {n}")
                return result

            if n < max_attempts:
                reflection = self.generate_reflection(task, result)
                print(f"✗ Неудача. Рефлексия: {reflection[:80]}...")

        print("⚠ Лимит попыток исчерпан")
        return result


# Пример: агент пишет Python-функцию и проверяет тестами
agent = ReflexionAgent(
    base_system="Ты — опытный Python-разработчик. Пиши корректный, рабочий код."
)

def passes_tests(code: str) -> bool:
    """Запускает тесты и возвращает True если все прошли."""
    try:
        exec(code)          # упрощённо; в реальности используй subprocess
        return True
    except Exception:
        return False

solution = agent.run(
    task="Напиши функцию merge_sorted_lists(a, b) -> list, которая сливает два отсортированных списка",
    is_correct=passes_tests,
    max_attempts=3
)
🔍 Когда рефлексия работает лучше всего

Reflexion особенно эффективен для задач с бинарной обратной связью: тесты прошли / не прошли, SQL вернул правильный результат / нет. Для субъективных задач (написание текста) лучше использовать обычный Critique-and-Revise с числовой оценкой.

Reflection как мета-паттерн

Reflection не привязан к конкретной архитектуре — это мета-паттерн, который накладывается поверх любого другого: ReAct, Plan-and-Execute, простого pipeline. Можно применять его на разных уровнях:

внешний
Полный перезапуск. Запускаем весь агент, критикуем финальный ответ, перезапускаем с замечаниями в системном промпте.
по шагам
Critiq каждого шага в Plan-and-Execute: перед переходом к следующему шагу проверяем результат текущего.
внутренний
Check-before-answer в ReAct: перед финальным stop_reason="end_turn" делаем дополнительный вызов «проверь свой ответ».

Пример: Plan-and-Execute с внешней критикой финального синтеза. Дорого, но повышает качество итогового ответа без изменения внутренней логики агента:

def plan_execute_with_reflection(task: str, max_rounds: int = 2) -> str:
    """
    Plan-and-Execute с финальной проверкой через Reflection.
    Перезапускает агента только если критик не принял результат.
    """
    # Первый запуск Plan-and-Execute
    draft = plan_and_execute(task)

    for _ in range(max_rounds):
        result = critique(task, draft)
        if result["passed"]:
            return draft

        # Добавляем критику в задачу и перезапускаем
        issues_str = "\n".join(f"- {i}" for i in result["issues"])
        enriched_task = (
            f"{task}\n\n"
            f"ВАЖНО — предыдущий ответ не прошёл проверку:\n"
            f"{issues_str}\n"
            f"Учти эти замечания при составлении нового плана."
        )
        draft = plan_and_execute(enriched_task)

    return draft

Когда применять Reflection

Reflection уместен
  • Задачи, где качество важнее скорости
  • Генерация кода с автоматической проверкой тестами
  • Документы с чёткими критериями (структура, полнота)
  • Задачи с известными граничными случаями
  • Высококонкурентные задачи (написание текстов, резюме)
Reflection избыточен
  • Простые вопросы — первый ответ уже достаточно хорош
  • Realtime-сценарии с жёстким требованием к latency
  • Задачи без чётких критериев качества
  • Бюджетные ограничения — каждый раунд = дополнительные токены
  • Когда ошибки некорректируемы (уже отправлен email, записан файл)
💡 Эмпирическое правило

Если задача важна настолько, что человек перечитал бы черновик перед отправкой — добавь Reflection. Если нет — обойдись без него.

Сравнение с другими паттернами

Критерий ReAct Plan-and-Execute Reflection
Основная цель Многошаговое исследование Сложные задачи с планом Качество результата
Дополнительные вызовы Только инструменты Планировщик + синтезатор +1–3 вызова на итерацию
Улучшает с попытками нет нет да
Latency средняя высокая высокая
Можно комбинировать да да да (мета-паттерн)
Лучше всего для Поиск, анализ данных Отчёты, исследования Написание, кодирование

Типичные ошибки

Критика без конкретики
Промпт критика не требует конкретности — модель выдаёт «текст можно улучшить» вместо именованных проблем. Ревизор получает бесполезный фидбек и делает косметические правки.
→ Укажи в промпте: «Каждая проблема должна быть конкретной и actionable. Не "стиль слабый", а "нет числовых метрик в разделе 2"».
Слишком строгий критерий passed
passed=True только при score=10 приводит к бесконечному циклу мелких улучшений. Каждая итерация стоит токенов, но после 8/10 прирост качества маргинален.
→ Установи реалистичный порог: score ≥ 8 и ни одной критической проблемы.
Один промпт для генератора и критика
Использование одного и того же системного промпта для обеих ролей. Модель «входит в роль автора» и мягко оценивает свой же текст, не замечая проблем.
→ Критику нужен отдельный промпт с ролью строгого редактора, без контекста о том, что текст написала та же модель.
Reflection без выхода по лимиту
Забытый max_rounds может превратить цикл в бесконечный, если критик всегда возвращает passed=False. Это не баг критика — некоторые задачи объективно не имеют «идеального» ответа.
→ Всегда устанавливай max_rounds и возвращай лучший черновик при его достижении.
Reflexion без чистки старых рефлексий
Накопление 10–15 рефлексий засоряет контекст и снижает внимание модели к актуальным проблемам. Старые рефлексии могут противоречить новым.
→ Храни только N последних рефлексий (параметр max_stored_reflections) или фильтруй их по релевантности.

Шпаргалка

  • Reflection = Generate → Critique → Revise (цикл до passed или max_rounds)
  • Критику отделяй от генератора: другой промпт, другой API-вызов
  • Структурированная критика через CRITIQUE_TOOL: passed, score, issues[], suggestions[]
  • tool_choice: "tool" гарантирует структурированный вывод критика
  • passed = score ≥ 8 + нет критических проблем (не score = 10)
  • Critique дешевле Generate: используй меньшую модель и max_tokens=512
  • Reflexion = Critique + память между попытками (хранить ≤ 5 рефлексий)
  • Мета-паттерн: Reflection применяется поверх ReAct / Plan-and-Execute
  • Всегда устанавливай max_rounds — даже идеальный критик может застрять

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

  1. Critique-and-Revise для кода. Реализуй агент, который пишет Python-функцию по описанию, а затем критикует её по трём критериям: корректность (edge cases), читаемость (имена переменных, комментарии), производительность (O-нотация). Запусти 2 раунда и сравни первый и финальный вариант.
  2. Reflexion с тестами. Возьми 3 алгоритмические задачи (сортировка, поиск, работа со строками). Реализуй ReflexionAgent с автоматической проверкой через pytest. Запусти по 3 попытки на каждую задачу и измерь, помогают ли рефлексии с задачи №1 при решении задачи №2 (если использовать одного агента для всех трёх).
  3. Специализированный критик. Реализуй два разных критика для одного и того же текста: «технический рецензент» (проверяет точность фактов и наличие примеров) и «UX-редактор» (проверяет ясность и длину предложений). Запусти оба последовательно, объединяя их issues в один список для ревизора. Сравни качество результата с однокритиковым подходом.