Почему первый ответ не всегда лучший
Представьте, что вы просите агента написать технический документ или сгенерировать SQL-запрос к незнакомой схеме. LLM отвечает быстро и уверенно — но этот ответ содержит неточности, пропущенные детали или логические ошибки.
Проблема не в том, что модель «не умеет» — она умеет. Проблема в режиме работы: при генерации модель двигается слева направо, токен за токеном, не возвращаясь назад. Это эффективно, но не оптимально для задач, где важно качество, а не скорость первого ответа.
Исследования показывают, что LLM лучше оценивают, чем генерируют с первой попытки. Дать модели задачу «найди ошибки в этом тексте» когнитивно проще, чем «напиши идеальный текст». Reflection использует это свойство: разделяет генерацию и оценку на отдельные API-вызовы, чтобы каждый выполнялся в своём оптимальном режиме.
Паттерн формализован в двух статьях: Self-Refine (Madaan et al., 2023) — итеративное самоулучшение через feedback, и Reflexion (Shinn et al., 2023) — хранение вербальных рефлексий между запусками агента. Оба подхода показали улучшение качества на 10–40% на стандартных бенчмарках.
Как устроен Reflection
Reflection — это цикл из трёх фаз: генерация черновика, критика этого черновика, ревизия с учётом критики. Цикл повторяется до тех пор, пока критик не признаёт результат удовлетворительным или не будет достигнут лимит итераций.
Ключевая идея: генерация и оценка — разные задачи. Разделение их на отдельные API-вызовы позволяет каждому вызову работать с полным вниманием к своей роли. Генератор не думает об ошибках, критик не думает о том, как написать «правильно» — он только находит «неправильно».
Обратите внимание на дизайн: Ревизор — это тот же тип вызова, что и Генератор, но получает на вход не только задачу, а также черновик и структурированную критику. После ревизии цикл начинается заново: новый черновик идёт к Критику.
На практике обе роли выполняет одна и та же модель, просто с разным контекстом. Разделение на «Генератор» и «Ревизор» — концептуальное, а не технологическое. Критик же часто выносят в отдельный вызов с другим системным промптом (и иногда — другой, более дешёвой моделью).
Самокритика и внешний критик
Существует два основных подхода к организации критика: самокритика (та же модель, другой промпт) и внешний критик (отдельный вызов с ролью строгого проверяющего). На практике различие размыто — оба случая реализуются через отдельные API-вызовы.
| Критерий | Самокритика | Внешний критик |
|---|---|---|
| Системный промпт | Тот же или с добавлением «проверь свой ответ» | Отдельный промпт с ролью эксперта/редактора |
| Строгость | средняя — модель мягче к своим ответам | выше — другая роль снимает самоцензуру |
| Стоимость | ниже — один тип промпта | выше — но можно использовать меньшую модель |
| Предпочтительно для | Быстрой проверки, простых задач | Задач с чёткими критериями качества |
Независимо от подхода, важно отделить вызов критика от вызова генератора в истории сообщений. Если добавить «а теперь проверь своё» в то же сообщение — модель уже зафиксировала ответ и будет склонна его защищать, а не критиковать. Отдельный API-вызов создаёт «чистый лист».
Анатомия хорошей критики
Разница между работающей и не работающей критикой — в конкретности. Расплывчатый фидбек («текст слабый», «можно лучше») не даёт ревизору ничего конкретного для улучшения. Хорошая критика содержит три компонента: оценку (числовую или булеву), проблемы (конкретные, именованные), предложения (что именно исправить).
«Нужно улучшить стиль и содержание»
«Не полностью раскрыта тема»
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"]
}
}
Не требуй идеального результата. Определи порог (например, 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
)
Посмотрим, как цикл выглядит на конкретном примере:
Нет технических характеристик (уровень шумоподавления, ёмкость батареи)
Не указана целевая аудитория и сценарии использования
«Купите сейчас!» — рекламный клише без ценности
Добавь: -40дБ ANC, 30ч battery, задержка 24мс
Опиши реальный сценарий: удалённая работа, перелёты
Нет эмоционального крючка в начале — читатель не вовлекается
Начни с боли: «Представьте переговоры в шумном кафе…»
Эмоциональный крючок, технические характеристики, сценарий использования — всё на месте.
Функция 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. Можно применять его на разных уровнях:
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
- Задачи, где качество важнее скорости
- Генерация кода с автоматической проверкой тестами
- Документы с чёткими критериями (структура, полнота)
- Задачи с известными граничными случаями
- Высококонкурентные задачи (написание текстов, резюме)
- Простые вопросы — первый ответ уже достаточно хорош
- Realtime-сценарии с жёстким требованием к latency
- Задачи без чётких критериев качества
- Бюджетные ограничения — каждый раунд = дополнительные токены
- Когда ошибки некорректируемы (уже отправлен email, записан файл)
Если задача важна настолько, что человек перечитал бы черновик перед отправкой — добавь Reflection. Если нет — обойдись без него.
Сравнение с другими паттернами
| Критерий | ReAct | Plan-and-Execute | Reflection |
|---|---|---|---|
| Основная цель | Многошаговое исследование | Сложные задачи с планом | Качество результата |
| Дополнительные вызовы | Только инструменты | Планировщик + синтезатор | +1–3 вызова на итерацию |
| Улучшает с попытками | нет | нет | да |
| Latency | средняя | высокая | высокая |
| Можно комбинировать | да | да | да (мета-паттерн) |
| Лучше всего для | Поиск, анализ данных | Отчёты, исследования | Написание, кодирование |
Типичные ошибки
max_rounds может превратить цикл в бесконечный,
если критик всегда возвращает passed=False. Это не баг критика —
некоторые задачи объективно не имеют «идеального» ответа.
max_rounds и возвращай лучший черновик при его достижении.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— даже идеальный критик может застрять
Практические задания
- Critique-and-Revise для кода. Реализуй агент, который пишет Python-функцию по описанию, а затем критикует её по трём критериям: корректность (edge cases), читаемость (имена переменных, комментарии), производительность (O-нотация). Запусти 2 раунда и сравни первый и финальный вариант.
-
Reflexion с тестами.
Возьми 3 алгоритмические задачи (сортировка, поиск, работа со строками).
Реализуй
ReflexionAgentс автоматической проверкой черезpytest. Запусти по 3 попытки на каждую задачу и измерь, помогают ли рефлексии с задачи №1 при решении задачи №2 (если использовать одного агента для всех трёх). - Специализированный критик. Реализуй два разных критика для одного и того же текста: «технический рецензент» (проверяет точность фактов и наличие примеров) и «UX-редактор» (проверяет ясность и длину предложений). Запусти оба последовательно, объединяя их issues в один список для ревизора. Сравни качество результата с однокритиковым подходом.