Когда одного шага недостаточно
Представь: ты просишь агента спланировать трёхдневную командировку с учётом бюджета, расписания встреч и предпочтений по отелям. Если агент просто «выстреливает» ответом — он либо игнорирует часть ограничений, либо делает внутренне противоречивый план.
Проблема не в мощности модели — она в архитектуре генерации. LLM создаёт ответ слева направо, токен за токеном, без возможности вернуться назад. Каждый следующий токен — результат одного «прохода» через сеть с учётом всего предыдущего текста. Нет шагов, нет итераций, нет «черновика».
Решение простое по идее и мощное на практике: заставить модель генерировать промежуточные шаги прежде чем дать финальный ответ. Эти шаги — скратчпад (scratch pad, черновик). Их содержимое попадает в контекст и буквально «видно» модели при генерации следующего токена.
Scratch pad — блокнот для черновиков, «блокнот математика»: место, где пишут промежуточные вычисления, которые не входят в итоговый ответ. В контексте LLM — это токены, которые модель генерирует «для себя», а не для пользователя.
Вот конкретный пример. Задача: «Сколько секунд в 2 неделях и 3 днях?»
Без промежуточных шагов модель часто ошибается при многоступенчатых вычислениях — не потому что «не умеет считать», а потому что весь путь от вопроса до ответа должен уместиться в одном «прыжке». Скратчпад разбивает этот прыжок на шаги, каждый из которых надёжен.
Как chain-of-thought работает внутри трансформера
Чтобы понять, почему скратчпад работает — нужно понять один ключевой факт об архитектуре трансформеров: модель не «думает» больше при сложных задачах, она просто генерирует больше токенов. Количество вычислений на один токен — фиксировано.
Это создаёт фундаментальную асимметрию: вопрос «каков корень из 144?» занимает 5 токенов, но при генерации ответа модель расходует ровно столько же вычислений, сколько на вопрос «как пройти в библиотеку?». Для простых задач этого хватает. Для сложных — нет.
Ключевой механизм: когда модель генерирует токены шага 2, эти токены уже находятся в контексте при генерации шага 3. Attention-механизм позволяет каждому новому токену «смотреть» на все предыдущие — включая промежуточные шаги скратчпада. Именно поэтому chain-of-thought работает: рассуждение само становится контекстом для следующего рассуждения.
Иными словами — длина генерации пропорциональна количеству «вычислительных шагов». Скратчпад — это способ купить больше вычислений за счёт большего количества токенов, не меняя архитектуру модели.
Насколько это улучшает результаты
Исследование Wei et al. (2022) «Chain-of-Thought Prompting Elicits Reasoning in Large Language Models» показало: для задач, требующих многошаговых рассуждений, CoT кардинально улучшает точность. Причём разрыв тем больше, чем сложнее задача.
Важно последнее: на простых вопросах CoT почти не даёт прироста. Добавлять скратчпад везде — не нужно. Он нужен только тогда, когда задача требует нескольких логических шагов.
Эффект chain-of-thought значительно усиливается с размером модели. У небольших моделей (до ~7B параметров) CoT иногда даже снижает качество — модель начинает «рассуждать» бессвязно и запутывается. Для агентов на базе Claude это не проблема, но держи в виду при работе с open-source моделями.
Три вида памяти агента: где скратчпад
Прежде чем переходить к реализации — зафиксируем, как скратчпад соотносится с другими типами памяти. Это разные инструменты для разных задач.
| Тип | Хранится где | Живёт как долго | Объём | Задача |
|---|---|---|---|---|
| Краткосрочная | Context window (messages) | Текущая сессия | До 200 к токенов | История диалога, контекст разговора |
| Рабочая (scratchpad) | Context window (одно сообщение) | Один ответ | Сотни–тысячи токенов | Промежуточные рассуждения, черновик |
| Долгосрочная | Внешнее хранилище (Vector DB) | Между сессиями | Миллионы документов | Факты, документы, прошлые разговоры |
Скратчпад — самый эфемерный тип: он живёт только во время генерации одного ответа. После того как ответ сформирован, скратчпад либо показывается пользователю (явный CoT), либо скрывается (extended thinking), либо сохраняется в историю как часть диалога.
Три вида скратчпада: как выбрать
Существует три принципиально разных способа реализовать рабочую память агента. Выбор зависит от того, нужен ли тебе доступ к процессу рассуждения, и как агент интегрирован в систему.
Триггер через промпт: «Думай пошагово» или «Let's think step by step». Модель сама решает, как рассуждать. Рассуждение видно в ответе.
- Простая интеграция — одна фраза в промпте
- Пользователь видит ход мыслей
- Нет контроля над структурой
В промпте задаётся структура через XML-теги: <thinking>...</thinking>.
Рассуждение парсится и при необходимости скрывается от пользователя.
- Полный контроль — ты парсишь результат
- Можно логировать, отлаживать, отображать
- Требует более сложного системного промпта
Встроенный механизм Claude: модель получает скрытый буфер токенов для рассуждений перед генерацией ответа. Контролируется параметром API.
- Максимальная глубина рассуждений
- Thinking блоки в ответе (опционально скрыты)
- Доступно только в Claude API
Extended Thinking в Claude: встроенный скратчпад
Extended Thinking — это API-фича, которая даёт Claude специальный буфер токенов исключительно для рассуждений. Эти токены генерируются до финального ответа, но не отображаются пользователю по умолчанию. Ты как разработчик получаешь к ним доступ через ответ API.
Когда включён Extended Thinking, ответ содержит два типа контент-блоков:
Шаг 1: Определю даты. Вторник — прилёт, пятница — вылет. Это 4 ночи.
Шаг 2: Бюджет. Перелёт туда-обратно ~12 000 руб. Остаток: 38 000 руб.
Шаг 3: Отель. 4 ночи × 8 000 = 32 000 руб. Вписывается в бюджет...
Вот как это выглядит в коде с Anthropic SDK:
import anthropic
client = anthropic.Anthropic()
def think_and_answer(question: str, budget_tokens: int = 8000) -> dict:
"""
Задаёт вопрос с включённым Extended Thinking.
Возвращает thinking и финальный ответ отдельно.
budget_tokens: количество токенов для рассуждений (минимум 1024).
Чем сложнее задача — тем больше бюджет.
"""
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=budget_tokens + 4096, # max_tokens > budget_tokens — обязательно
thinking={
"type": "enabled",
"budget_tokens": budget_tokens
},
messages=[{"role": "user", "content": question}]
)
result = {"thinking": None, "answer": None}
for block in response.content:
if block.type == "thinking":
result["thinking"] = block.thinking
elif block.type == "text":
result["answer"] = block.text
return result
# Пример: сложная логическая задача
q = """
Есть 5 домов разного цвета. В каждом живёт человек разной национальности.
Каждый пьёт свой напиток, курит свои сигареты, держит своё животное.
Норвежец живёт в первом доме. Красный дом стоит слева от белого...
[знаменитая загадка Эйнштейна]
Кто держит рыбок?
"""
result = think_and_answer(q, budget_tokens=10000)
# Логируем thinking для отладки
print("=== РАССУЖДЕНИЯ МОДЕЛИ ===")
print(result["thinking"][:500] + "...") # первые 500 символов
print("\n=== ФИНАЛЬНЫЙ ОТВЕТ ===")
print(result["answer"])
Когда и сколько токенов выделять
budget_tokens — не жёсткий лимит, а верхняя граница.
Если задача простая, модель потратит меньше. Если сложная — до лимита.
Рекомендации:
- 1 024–4 000 — анализ данных, сравнение вариантов, умеренно сложная логика
- 4 000–10 000 — многошаговое планирование, код-ревью, задачи с несколькими ограничениями
- 10 000–32 000 — сложные математические задачи, стратегический анализ, задачи олимпиадного уровня
При включённом thinking нельзя использовать:
temperature и top_p (не поддерживаются),
stream=True работает, но thinking-блоки приходят целиком по завершении.
Также thinking несовместим с prompt caching на уровне messages.
Extended Thinking в агентском цикле
При использовании tool calling с Extended Thinking — мышление происходит перед каждым вызовом инструмента. Это позволяет агенту обдумать, какой инструмент вызвать и с какими аргументами:
tools = [
{
"name": "search_web",
"description": "Поиск актуальной информации в интернете",
"input_schema": {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Поисковый запрос"}
},
"required": ["query"]
}
},
{
"name": "calculate",
"description": "Точные арифметические вычисления",
"input_schema": {
"type": "object",
"properties": {
"expression": {"type": "string", "description": "Математическое выражение"}
},
"required": ["expression"]
}
}
]
def agent_with_thinking(user_message: str) -> str:
"""Агент с Extended Thinking и tool calling."""
messages = [{"role": "user", "content": user_message}]
while True:
response = client.messages.create(
model="claude-opus-4-6",
max_tokens=16000,
thinking={"type": "enabled", "budget_tokens": 8000},
tools=tools,
messages=messages
)
# Добавляем полный ответ (с thinking-блоками) в историю
# Это важно: thinking-блоки нужно передавать обратно для корректной кэшируемости
messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn":
# Извлекаем только текстовый ответ для пользователя
return next(
b.text for b in response.content if b.type == "text"
)
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type == "tool_use":
# Выполняем инструмент
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})
При многоходовом диалоге с Extended Thinking необходимо передавать
thinking-блоки обратно в messages вместе с ответом ассистента.
API требует этого для внутренней консистентности. Но содержимое thinking
не попадает к пользователю — только в твои логи.
Явный скратчпад через промпт-инжиниринг
Extended Thinking — не единственный способ добавить рабочую память. Иногда нужно большее: сохранить рассуждения в историю, отобразить их пользователю, использовать в другой модели. Тогда используют явный скратчпад через XML-теги в промпте.
Принцип: в системный промпт добавляем инструкцию писать рассуждения
внутри тегов <thinking>...</thinking>, а финальный
ответ — в <answer>...</answer>. Потом парсим теги.
import re
import anthropic
client = anthropic.Anthropic()
SYSTEM_PROMPT = """Ты — аналитический ассистент.
При ответе на любой сложный вопрос ОБЯЗАТЕЛЬНО:
1. Сначала запиши рассуждения внутри тегов ...
2. Потом дай финальный ответ внутри тегов ...
Формат ответа:
[Здесь — пошаговые рассуждения, промежуточные вычисления, анализ вариантов]
[Здесь — чёткий и конкретный финальный ответ для пользователя]
"""
def parse_scratchpad(response_text: str) -> tuple[str, str]:
"""
Парсит явный скратчпад из ответа модели.
Возвращает (thinking, answer).
"""
thinking_match = re.search(
r'(.*?) ',
response_text,
re.DOTALL
)
answer_match = re.search(
r'(.*?) ',
response_text,
re.DOTALL
)
thinking = thinking_match.group(1).strip() if thinking_match else ""
answer = answer_match.group(1).strip() if answer_match else response_text.strip()
return thinking, answer
def ask_with_scratchpad(question: str, show_thinking: bool = False) -> str:
"""
Задаёт вопрос с явным скратчпадом.
show_thinking=True — логирует рассуждения для отладки.
"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=4096,
system=SYSTEM_PROMPT,
messages=[{"role": "user", "content": question}]
)
thinking, answer = parse_scratchpad(response.content[0].text)
if show_thinking:
print("── РАССУЖДЕНИЯ ──")
print(thinking)
print("── ОТВЕТ ──")
return answer
# Использование
answer = ask_with_scratchpad(
"Если я инвестирую 100 000 руб. под 12% годовых, "
"сколько у меня будет через 5 лет с учётом реинвестирования?",
show_thinking=True
)
Структурированный скратчпад для агентов
Для агентов, которые используют инструменты, можно встроить скратчпад прямо в схему вызова инструмента. Это позволяет агенту «думать» перед каждым вызовом — прямо в JSON-объекте:
tools_with_thinking = [
{
"name": "search_database",
"description": """Поиск по базе данных клиентов.
ВАЖНО: перед вызовом заполни поле 'reasoning' — объясни,
почему именно такой запрос поможет решить задачу.""",
"input_schema": {
"type": "object",
"properties": {
"reasoning": {
"type": "string",
"description": "Твоё рассуждение: зачем нужен этот запрос, какую информацию ожидаешь найти"
},
"query": {
"type": "string",
"description": "SQL-запрос или поисковое выражение"
},
"filters": {
"type": "object",
"description": "Опциональные фильтры"
}
},
"required": ["reasoning", "query"]
}
}
]
# Теперь каждый вызов инструмента будет содержать reasoning:
# {
# "reasoning": "Нужно найти всех клиентов с задолженностью > 30 дней,
# чтобы определить объём проблемной дебиторки",
# "query": "SELECT * FROM clients WHERE overdue_days > 30",
# "filters": {"status": "active"}
# }
# Это можно логировать, отображать в UI отладки, или использовать
# для объяснения действий агента пользователю.
Явный скратчпад лучше подходит когда: (1) нужно сохранять рассуждения в историю диалога для последующего анализа; (2) рассуждения должны быть отображены пользователю в интерфейсе (например, «агент думает...»); (3) агент работает с нестандартными моделями. Extended Thinking — когда важна максимальная глубина рассуждений и не нужен полный контроль над форматом.
Скратчпад в ReAct-цикле
ReAct (Reason + Act) — паттерн, в котором агент чередует рассуждение и действие. «Reason» — это и есть скратчпад: перед каждым действием агент явно фиксирует свои мысли. Между итерациями рассуждения накапливаются в истории, создавая контекст для следующего шага.
Разберём полную реализацию ReAct-агента с явным скратчпадом и Extended Thinking:
from dataclasses import dataclass, field
from typing import Any
import anthropic
client = anthropic.Anthropic()
@dataclass
class AgentStep:
"""Один шаг ReAct-цикла с сохранённым скратчпадом."""
step_num: int
thought: str # рассуждение агента
action: str # название инструмента
action_input: dict # аргументы инструмента
observation: str # результат выполнения
thinking: str = "" # Extended Thinking (если включён)
@dataclass
class AgentTrace:
"""Полная трасса выполнения агента для отладки."""
task: str
steps: list[AgentStep] = field(default_factory=list)
final_answer: str = ""
def show(self) -> None:
"""Выводит читаемую трассу выполнения."""
print(f"\n{'='*60}")
print(f"ЗАДАЧА: {self.task}")
print('='*60)
for step in self.steps:
print(f"\n[Шаг {step.step_num}]")
if step.thinking:
print(f" Extended Thinking: {step.thinking[:200]}...")
print(f" Мысль: {step.thought}")
print(f" Действие: {step.action}({step.action_input})")
print(f" Результат: {step.observation[:200]}")
print(f"\nФИНАЛЬНЫЙ ОТВЕТ: {self.final_answer}")
REACT_SYSTEM = """Ты — агент, который решает задачи методом Reason-Act-Observe.
На каждом шаге ты ДОЛЖЕН:
1. Написать свои рассуждения в поле 'thought' (scratchpad)
2. Выбрать инструмент и заполнить его аргументы
3. Проанализировать результат инструмента
В поле 'thought' пиши честно: что ты знаешь, чего не знаешь, почему
выбираешь именно этот инструмент. Это поможет тебе принять лучшее решение.
Когда у тебя достаточно информации — вызови инструмент 'finish' с ответом."""
TOOLS = [
{
"name": "search",
"description": "Поиск информации по запросу",
"input_schema": {
"type": "object",
"properties": {
"thought": {"type": "string", "description": "Твоё рассуждение перед поиском"},
"query": {"type": "string", "description": "Поисковый запрос"}
},
"required": ["thought", "query"]
}
},
{
"name": "calculate",
"description": "Вычислить математическое выражение",
"input_schema": {
"type": "object",
"properties": {
"thought": {"type": "string", "description": "Зачем нужно вычисление"},
"expression": {"type": "string", "description": "Выражение Python: eval(expression)"}
},
"required": ["thought", "expression"]
}
},
{
"name": "finish",
"description": "Завершить выполнение с финальным ответом",
"input_schema": {
"type": "object",
"properties": {
"thought": {"type": "string", "description": "Итоговое рассуждение"},
"answer": {"type": "string", "description": "Финальный ответ для пользователя"}
},
"required": ["thought", "answer"]
}
}
]
def execute_tool(name: str, args: dict) -> str:
"""Выполняет инструмент и возвращает строку-результат."""
if name == "search":
# Здесь — реальный поиск (SerpAPI, Brave Search и т.д.)
return f"[Результаты поиска по '{args['query']}': ...]"
elif name == "calculate":
try:
result = eval(args["expression"], {"__builtins__": {}})
return str(result)
except Exception as e:
return f"Ошибка вычисления: {e}"
return ""
def run_react_agent(task: str, max_steps: int = 10, use_thinking: bool = True) -> AgentTrace:
"""
Запускает ReAct-агента с явным скратчпадом в каждом шаге.
use_thinking=True включает Extended Thinking дополнительно к явному CoT.
"""
trace = AgentTrace(task=task)
messages = [{"role": "user", "content": task}]
for step_num in range(1, max_steps + 1):
kwargs = {
"model": "claude-sonnet-4-6",
"max_tokens": 8192,
"system": REACT_SYSTEM,
"tools": TOOLS,
"messages": messages
}
if use_thinking:
kwargs["model"] = "claude-opus-4-6"
kwargs["thinking"] = {"type": "enabled", "budget_tokens": 4000}
kwargs["max_tokens"] = 12000
response = client.messages.create(**kwargs)
messages.append({"role": "assistant", "content": response.content})
# Извлекаем Extended Thinking если есть
extended_thinking = ""
if use_thinking:
for block in response.content:
if block.type == "thinking":
extended_thinking = block.thinking
if response.stop_reason == "tool_use":
tool_results = []
for block in response.content:
if block.type != "tool_use":
continue
if block.name == "finish":
trace.final_answer = block.input["answer"]
trace.steps.append(AgentStep(
step_num=step_num,
thought=block.input.get("thought", ""),
action="finish",
action_input=block.input,
observation="[завершение]",
thinking=extended_thinking
))
return trace
# Выполняем инструмент
observation = execute_tool(block.name, block.input)
trace.steps.append(AgentStep(
step_num=step_num,
thought=block.input.get("thought", ""),
action=block.name,
action_input=block.input,
observation=observation,
thinking=extended_thinking
))
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": observation
})
messages.append({"role": "user", "content": tool_results})
else:
# stop_reason == "end_turn" — модель решила отвечать текстом
text = next((b.text for b in response.content if b.type == "text"), "")
trace.final_answer = text
break
return trace
# Запуск
trace = run_react_agent(
"Сколько шагов нужно сделать за 30-минутную прогулку, если средний шаг — 75 см?",
use_thinking=True
)
trace.show()
Обрати внимание: в каждом вызове инструмента есть поле thought.
Это явный скратчпад — агент обязан написать рассуждение
перед действием. Плюс, при use_thinking=True, дополнительно
работает Extended Thinking. Это двойной скратчпад: один для логики выбора
инструмента, другой — для глубокого анализа ситуации.
AgentTrace — не просто дебаг-инструмент. Сохранённые скратчпады
позволяют: (1) обнаружить где агент «сбился» при ошибках; (2) улучшить промпт
на основе реальных рассуждений; (3) показать пользователю прозрачный процесс
решения («агент думал вот так»). Логируй трассы в продакшне.
Типичные ошибки
messages полный response.content (список всех блоков), а не только текст.split() вместо regextext.split("<thinking>")[1].split("</thinking>")[0]
ломается, если модель напишет thinking-блок два раза, вставит пробел в тег,
или вовсе не напишет его. В продакшне это происходит чаще, чем кажется.
re.search(r'<thinking>(.*?)</thinking>', text, re.DOTALL) и обрабатывай случай отсутствия тега.budget_tokens=1024 для задачи, требующей длинного рассуждения.
Модель «обрезает» мышление на середине и выдаёт неполный ответ —
но ошибки в API нет, всё выглядит нормально.
Шпаргалка
- Scratchpad — рабочая память для одного ответа: промежуточные токены, которые агент генерирует «для себя»
- Механика CoT: каждый шаг рассуждения попадает в контекст и «виден» через attention при следующем шаге
- CoT помогает на сложных задачах; на тривиальных — лишние токены без пользы
- Extended Thinking:
thinking={"type": "enabled", "budget_tokens": N},N ≥ 1024,max_tokens > N - Ответ с Extended Thinking содержит блоки
type="thinking"иtype="text" - Thinking-блоки нужно передавать обратно в
messagesпри многоходовом диалоге - Явный скратчпад: XML-теги в промпте (
<thinking>) +re.search()для парсинга - Поле
reasoning/thoughtв схеме инструмента — встроенный скратчпад для каждого вызова - В ReAct-цикле поле
thoughtв каждом шаге — это скратчпад агента - Логируй
AgentTraceв продакшне: скратчпад — главный инструмент отладки - Extended Thinking + явный
thoughtв инструментах — двойной скратчпад для максимальной надёжности
Практические задания
- Сравни качество с CoT и без. Составь список из 5 задач: 2 простых (фактический вопрос, перефразирование) и 3 сложных (многошаговая математика, планирование с ограничениями, логическая головоломка). Запусти каждую с CoT и без. Замерь точность и длину ответа. Убедись, что CoT помогает только там, где ожидалось.
- Явный скратчпад с визуализацией. Реализуй агента с явным скратчпадом (через XML-теги). Добавь веб-интерфейс (Flask или FastAPI), где рядом с ответом отображается «ход рассуждений» в раскрывающемся блоке. Пользователь должен видеть, что агент думал, прежде чем ответить.
-
Extended Thinking для отладки ReAct.
Возьми любого ReAct-агента, добавь Extended Thinking.
Сохраняй thinking-блоки в
AgentTrace. Найди задачу, на которой агент делает ошибку (например, вызывает не тот инструмент). Изучи thinking-блок этого шага — что именно пошло не так в рассуждениях? Исправь это через уточнение системного промпта.