Продвинутые техники: chain-of-thought, self-consistency, tree-of-thoughts, chain-of-verification
В основах промптинга мы разбирали, как объяснить модели, что делать. Эта статья — про то, как дать ей больше вычислений на само думание. Все четыре техники ниже — это разные способы потратить вычислительный бюджет на этапе вывода (test-time compute), чтобы поднять качество там, где одного прохода не хватает.
Интуиция: почему одного прохода мало
Трансформер тратит фиксированное количество вычислений на генерацию одного токена. Сколько бы ни была сложна задача, путь от входа к следующему токену — это одно и то же число слоёв. Поэтому «посчитай в уме 17 × 24 и сразу назови ответ» — это буквально требование решить задачу за константное время.
Единственный способ дать модели больше вычислений — дать ей писать больше токенов. Промежуточные токены становятся рабочей памятью: модель сама себе выкладывает черновик, и каждый следующий шаг обусловлен уже вычисленным. Формально это доказано: трансформеры с промежуточными шагами вычислительно строго мощнее трансформеров без них (Merrill & Sabharwal, «The Expressive Power of Transformers with Chain of Thought», arXiv:2310.07923).
Отсюда естественная классификация: можно удлинить одну траекторию (CoT), размножить траектории и проголосовать (self-consistency), ветвить и отсекать (tree-of-thoughts), либо проверить готовый ответ отдельным проходом (chain-of-verification).
1. Chain-of-Thought: заставить модель писать черновик
Что это
CoT — это промптинг, при котором модель обязана выдать промежуточные рассуждения до финального ответа. Введён в Wei et al., «Chain-of-Thought Prompting Elicits Reasoning in Large Language Models», arXiv:2201.11903.
Ключевая цифра из статьи: PaLM 540B на GSM8K (школьные текстовые задачи) даёт 17.9 % при обычном few-shot промптинге и 56.9 % при few-shot CoT. Это не тюнинг, не дообучение — только формат промпта.
Важнейшее наблюдение той же работы: эффект эмерджентный. На моделях меньше ~10B параметров CoT часто ухудшает результат — модель пишет правдоподобную, но неверную цепочку и уверенно приходит к неправильному ответу.
Два варианта
Zero-shot CoT (Kojima et al., arXiv:2205.11916) — одна фраза-триггер. Оригинальная — Let's think step by step; на GSM8K с text-davinci-002 это подняло 10.4 % → 40.7 %.
Few-shot CoT — примеры с уже развёрнутыми рассуждениями. Дороже по токенам входа, но задаёт формат и глубину рассуждения, а не только сам факт его наличия.
Практическая рекомендация 2026 года: на современных моделях голая фраза «think step by step» почти ничего не добавляет — они и так рассуждают. Ценность few-shot CoT сместилась в навязывание конкретной методики: не «подумай», а «пройди эти пять шагов в этом порядке».
Рабочий промпт: структурированный CoT
Ты — аналитик тарифных планов. Для каждого запроса пройди строго эти шаги.
<procedure>
1. FACTS: выпиши из входных данных только числа и условия,
относящиеся к вопросу. Не считай ничего на этом шаге.
2. RULES: назови применимые правила тарификации из <policy>,
каждое с цитатой.
3. COMPUTE: посчитай пошагово. Одна арифметическая операция на строку.
4. CHECK: проверь порядок величин и единицы измерения.
Если результат не проходит проверку — вернись к шагу 3.
5. ANSWER: одна строка вида `ИТОГО: <сумма> ₽`.
</procedure>
Не пропускай шаги. Не выводи ANSWER раньше CHECK.
Почему это работает лучше, чем «think step by step»:
- Разделение извлечения и вычисления. Классическая ошибка — модель одновременно ищет число в тексте и умножает его. Шаг FACTS изолирует извлечение.
- Одна операция на строку. Каждая строка — это отдельный контекст для следующего токена; длинные строки со свёрнутой арифметикой дают больше ошибок.
- Явный якорь для парсинга. Строка
ИТОГО:— детерминированная точка извлечения. Не идеальный способ (см. структурированный вывод), но надёжнее, чем разбирать свободный текст.
Когда CoT не помогает — и когда вредит
Это самая недооценённая часть темы. Мета-анализ Sprague et al., «To CoT or not to CoT?», arXiv:2409.12183 агрегировал более 100 работ и 14 моделей и обнаружил: основной прирост от CoT сосредоточен в математике и символьном рассуждении. Средний прирост на задачах, содержащих = или явные символьные операции, — заметный; на задачах здравого смысла и извлечения знаний — в пределах шума, а иногда отрицательный.
Второе ограничение — неверность цепочки (unfaithfulness). В Turpin et al., arXiv:2305.04388 показано: если в few-shot примерах систематически ставить правильный ответ на позицию (A), модель начинает выбирать (A) и в тесте — а в CoT пишет убедительное обоснование, никогда не упоминая реальную причину. Точность падала до 36 % при том, что рассуждения выглядели безупречно. Смежная работа Anthropic — Lanham et al., «Measuring Faithfulness in Chain-of-Thought Reasoning», arXiv:2307.13702 — измеряет, насколько ответ реально зависит от написанной цепочки.
Практический вывод: CoT — это не аудит-лог. Нельзя показывать цепочку пользователю как «объяснение решения» в регулируемом домене и утверждать, что это и есть причина ответа.
Третье — рост стоимости и латентности. CoT удлиняет выход в 5–20 раз. При цене выходных токенов в 5 раз выше входных это часто основная статья расходов.
многошаговая,
арифметическая?} B -->|Нет| C[Прямой ответ
без CoT] B -->|Да| D{Модель — reasoning
с встроенным thinking?} D -->|Да| E[Включить thinking,
управлять effort] D -->|Нет| F[Few-shot CoT
с явной процедурой] E --> G{Требуется устойчивость
к разбросу?} F --> G G -->|Нет| H[Один вызов] G -->|Да| I[Self-consistency: k выборок] I --> J{Ответ — короткая
каноническая величина?} J -->|Да| K[Голосование большинством] J -->|Нет| L[Universal SC:
модель-судья выбирает
наиболее согласованный]
2. Self-consistency: голосование вместо жадной декодировки
Идея
CoT берёт одну траекторию. Но у трудной задачи много верных путей и много разных неверных. Если сэмплировать k независимых цепочек и взять ответ большинством голосов, случайные ошибки размазываются, а правильный ответ — общий аттрактор.
Введено в Wang et al., «Self-Consistency Improves Chain of Thought Reasoning», arXiv:2203.11171. PaLM 540B на GSM8K: 56.5 % → 74.4 % (+17.9 п.п.) при 40 выборках. Аналогичные приросты на SVAMP, AQuA, ARC.
Почему это работает — и где потолок
Пусть p — вероятность того, что одна траектория даёт верный ответ, и пусть ошибки независимы и распределены по разным неверным значениям. Тогда вероятность верного большинства из k выборок:
$$P_{\text{maj}}(k) = \sum_{i=\lfloor k/2 \rfloor + 1}^{k} \binom{k}{i}, p^{i},(1-p)^{,k-i}$$
При p = 0.6 и k = 5 это ≈ 0.68; при k = 21 — ≈ 0.83. Классический закон больших чисел.
Но обе предпосылки в реальности нарушаются:
- Ошибки не независимы. Модель — один и тот же априор. Если она неправильно поняла условие, все k траекторий поймут его одинаково неправильно.
- Ошибки концентрируются. Если 60 % выборок сходятся на одном и том же неверном ответе (типичная ловушка задачи), голосование его и выберет — причём с высокой «уверенностью».
Отсюда главное правило: доля голосов ≠ вероятность правильности. Использовать её как калибровку нельзя без отдельной валидации на своём датасете (см. оценку и бенчмарки).
Проблема разнообразия на современных API
Оригинальная работа сэмплировала с temperature=0.7, top-k=40. Здесь начинается практическая тонкость, о которой мало пишут: на новых reasoning-моделях параметры сэмплирования убраны из API. У Claude Opus 4.8 и 4.7 передача temperature, top_p или top_k возвращает 400 (документация Anthropic). Аналогично у o-серии OpenAI temperature игнорируется.
Значит, разнообразие приходится создавать на уровне промпта, а не декодера:
- перестановка порядка few-shot примеров;
- разные «роли» / углы захода в системном промпте;
- разные формулировки процедуры (сверху вниз vs. снизу вверх, прямой ход vs. проверка подстановкой);
- разные подмножества контекста (если он избыточен).
Это к тому же даёт более полезное разнообразие: перестановка порядка вычислений ловит другие классы ошибок, чем шум температуры.
"""Self-consistency через промптовое разнообразие (Anthropic SDK)."""
from __future__ import annotations
import asyncio
import re
from collections import Counter
from dataclasses import dataclass
import anthropic
client = anthropic.AsyncAnthropic()
MODEL = "claude-opus-4-8"
# Разные «углы захода» — источник разнообразия вместо temperature.
STRATEGIES = [
"Реши задачу прямым ходом: от известных величин к искомой.",
"Реши задачу обратным ходом: предположи ответ и проверь подстановкой.",
"Сначала выпиши все величины с единицами измерения, потом считай.",
"Оцени порядок величины прикидкой, затем посчитай точно и сверь.",
"Разбей задачу на минимальные подзадачи и реши каждую отдельно.",
]
SYSTEM = """Ты решаешь количественные задачи.
Рассуждай пошагово: одна арифметическая операция на строку.
Последняя строка ответа — строго `ANSWER: <число>` без единиц и пробелов внутри числа."""
ANSWER_RE = re.compile(r"ANSWER:\s*(-?\d+(?:[.,]\d+)?)", re.IGNORECASE)
@dataclass(frozen=True)
class Vote:
answer: str | None
output_tokens: int
def normalize(raw: str) -> str:
"""Каноникализация: 1 000,50 и 1000.5 — один и тот же ответ."""
return f"{float(raw.replace(' ', '').replace(',', '.')):.4f}"
async def one_path(question: str, strategy: str) -> Vote:
resp = await client.messages.create(
model=MODEL,
max_tokens=4096,
system=SYSTEM,
thinking={"type": "adaptive"}, # у Opus 4.8 — только adaptive
output_config={"effort": "medium"}, # low | medium | high | xhigh | max
messages=[{"role": "user", "content": f"{strategy}\n\nЗадача:\n{question}"}],
)
text = "".join(b.text for b in resp.content if b.type == "text")
m = ANSWER_RE.search(text)
return Vote(
answer=normalize(m.group(1)) if m else None,
output_tokens=resp.usage.output_tokens,
)
async def self_consistency(question: str, k: int = 5) -> dict:
strategies = [STRATEGIES[i % len(STRATEGIES)] for i in range(k)]
votes = await asyncio.gather(*(one_path(question, s) for s in strategies))
valid = [v.answer for v in votes if v.answer is not None]
if not valid:
return {"answer": None, "agreement": 0.0, "reason": "no parsable answers"}
counts = Counter(valid)
answer, hits = counts.most_common(1)[0]
return {
"answer": answer,
"agreement": hits / len(valid), # НЕ вероятность правильности
"distribution": dict(counts),
"parse_failures": k - len(valid),
"output_tokens": sum(v.output_tokens for v in votes),
}
Сложность: O(k) вызовов по времени при последовательном исполнении и O(1) при asyncio.gather (латентность = самая медленная траектория). Память — O(k) на хранение ответов, полные тексты держать не нужно.
Экономика
| k | Относительная стоимость | Латентность (параллельно) | Типичный прирост точности |
|---|---|---|---|
| 1 | 1× | 1× | базовая |
| 3 | ~3× | ~1.2× | +3…7 п.п. |
| 5 | ~5× | ~1.3× | +5…10 п.п. |
| 10 | ~10× | ~1.4× | +7…13 п.п. |
| 40 | ~40× | ~1.6× | +10…18 п.п. (насыщение) |
Кривая логарифмическая: между k = 5 и k = 40 разница гораздо меньше, чем между k = 1 и k = 5. В проде почти всегда останавливаются на k = 3…5 и включают их только для запросов, помеченных как трудные.
Ранняя остановка экономит заметно: сэмплируйте по 2–3 и прекращайте, когда достигнут порог согласия (например, 3 совпадения подряд). На простых задачах это вырождается в 2–3 вызова вместо 5.
Universal Self-Consistency
Голосование требует канонизируемого ответа: число, метка класса, SQL-запрос после нормализации. Для свободного текста (резюме, объяснение, план) прямое сравнение бесполезно.
Обходной путь (Chen et al., «Universal Self-Consistency», arXiv:2311.17311): собрать k кандидатов, подать их модели одним промптом и попросить выбрать наиболее согласованный с остальными. Модель здесь выступает не как судья качества, а как детектор консенсуса — это заметно более простая и надёжная задача.
3. Tree-of-Thoughts: явный поиск с отсечением
Когда линейной цепочки принципиально мало
CoT и self-consistency не умеют возвращаться. Если на третьем шаге модель выбрала тупиковую ветку, она пройдёт до конца и выдаст ответ. Есть класс задач, где обязательны бэктрекинг и оценка промежуточного состояния: планирование, комбинаторные головоломки, синтез с ограничениями, поиск контрпримеров.
Yao et al., «Tree of Thoughts», arXiv:2305.10601 (репозиторий) формализует это как поиск по дереву, где узел — частичное решение («мысль»), а сама модель играет три роли:
- Генератор — предлагает b продолжений текущего состояния.
- Оценщик — оценивает каждое состояние (
sure/maybe/impossibleлибо число 0–1). - Стратегия поиска — BFS с лучом ширины k, DFS с отсечением, MCTS.
Флагманская цифра из статьи, Game of 24 на GPT-4: обычный промпт — 7.3 %, CoT — 4.0 %, CoT-SC со 100 выборками — 9.0 %, ToT с шириной луча b = 5 — 74 %.
Разрыв между CoT-SC(100) и ToT показателен: сто параллельных цепочек хуже, чем пять с отсечением. Голосование не спасает, когда почти все траектории ошибочны; выигрывает именно отбраковка тупиков на ранних шагах.
Псевдокод (BFS с лучом)
ФУНКЦИЯ ToT_BFS(задача, b, k, D, бюджет):
луч ← [пустое_состояние]
ДЛЯ глубина ОТ 1 ДО D:
кандидаты ← []
ДЛЯ КАЖДОГО s В луч:
кандидаты += Генератор(задача, s, b) # 1 вызов LLM на s
ЕСЛИ вызовы > бюджет: ВЕРНУТЬ лучшее(луч)
оценки ← Оценщик(задача, кандидаты) # 1 вызов на батч
кандидаты ← отбросить(кандидаты, оценка == impossible)
ЕСЛИ существует c: терминально(c) И валидно(c): ВЕРНУТЬ c
луч ← top_k(кандидаты, по оценкам, k)
ЕСЛИ луч пуст: ВЕРНУТЬ провал
ВЕРНУТЬ лучшее(луч)
Сложность. Узлов раскрывается O(k · b · D). Вызовов LLM — O(k · D) на генерацию плюс O(D) на батчевую оценку, то есть O(k · D). Память — O(k · b) на текущий уровень (в отличие от полного BFS с O(b^D)). Для Game of 24 с k = 5, b = 8, D = 3 это порядка 20–60 вызовов на одну задачу.
Реализация
"""Tree-of-Thoughts: BFS с лучом. Каркас, не привязанный к домену."""
from __future__ import annotations
import json
from dataclasses import dataclass, field
import anthropic
client = anthropic.Anthropic()
MODEL = "claude-opus-4-8"
@dataclass
class Node:
steps: list[str] = field(default_factory=list)
score: float = 0.0
@property
def trace(self) -> str:
return "\n".join(f"{i + 1}. {s}" for i, s in enumerate(self.steps)) or "(пусто)"
def propose(task: str, node: Node, b: int) -> list[Node]:
"""Генератор: b продолжений текущего частичного решения."""
resp = client.messages.create(
model=MODEL,
max_tokens=2048,
thinking={"type": "adaptive"},
output_config={
"effort": "low", # генерация дешевле оценки — экономим здесь
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"steps": {"type": "array", "items": {"type": "string"}}
},
"required": ["steps"],
"additionalProperties": False,
},
},
},
messages=[{"role": "user", "content": (
f"Задача:\n{task}\n\nУже сделано:\n{node.trace}\n\n"
f"Предложи ровно {b} РАЗНЫХ вариантов следующего шага. "
f"Каждый — одно конкретное действие, не план."
)}],
)
payload = json.loads(next(x.text for x in resp.content if x.type == "text"))
return [Node(steps=node.steps + [s]) for s in payload["steps"][:b]]
def evaluate(task: str, nodes: list[Node]) -> list[float]:
"""Оценщик: одним вызовом на весь уровень — так дешевле и калибровка ровнее."""
listing = "\n\n".join(f"[{i}]\n{n.trace}" for i, n in enumerate(nodes))
resp = client.messages.create(
model=MODEL,
max_tokens=2048,
thinking={"type": "adaptive"},
output_config={
"effort": "high", # качество отсечения — узкое место всего метода
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"scores": {
"type": "array",
"items": {
"type": "object",
"properties": {
"index": {"type": "integer"},
"verdict": {"enum": ["sure", "maybe", "impossible"]},
},
"required": ["index", "verdict"],
"additionalProperties": False,
},
}
},
"required": ["scores"],
"additionalProperties": False,
},
},
},
messages=[{"role": "user", "content": (
f"Задача:\n{task}\n\nЧастичные решения:\n{listing}\n\n"
"Для каждого оцени, может ли оно привести к корректному решению.\n"
"impossible — доказуемо тупик; maybe — не ясно; sure — почти наверняка ведёт к цели."
)}],
)
weights = {"impossible": 0.0, "maybe": 0.5, "sure": 1.0}
scores = [0.0] * len(nodes)
for item in json.loads(next(x.text for x in resp.content if x.type == "text"))["scores"]:
if 0 <= item["index"] < len(nodes):
scores[item["index"]] = weights[item["verdict"]]
return scores
def tot_bfs(task: str, is_solution, b: int = 4, k: int = 3,
depth: int = 4, max_calls: int = 40) -> Node | None:
beam, calls = [Node()], 0
for _ in range(depth):
candidates: list[Node] = []
for node in beam:
if calls >= max_calls:
return max(beam, key=lambda n: n.score, default=None)
candidates += propose(task, node, b)
calls += 1
for c in candidates:
if is_solution(c): # внешняя, детерминированная проверка
return c
scores = evaluate(task, candidates)
calls += 1
for c, s in zip(candidates, scores):
c.score = s
beam = sorted(
[c for c, s in zip(candidates, scores) if s > 0.0],
key=lambda n: n.score,
reverse=True,
)[:k]
if not beam:
return None
return max(beam, key=lambda n: n.score, default=None)
Trade-offs и честная оценка применимости
- Оценщик — узкое место. Если модель плохо отличает тупик от перспективной ветки, ToT деградирует до дорогого случайного поиска. Проверяйте оценщик отдельно: разметьте 50 состояний вручную и посчитайте precision/recall по классу
impossible. - Терминальная проверка должна быть внешней.
is_solutionв примере — детерминированная функция (интерпретатор, солвер, юнит-тест), а не запрос к модели. Иначе метод замкнётся сам на себя. - Стоимость. 20–60 вызовов на задачу — это x20…x60 к цене. Оправдано, когда цена ошибки высока и задача редкая (планирование миграции, генерация сложного запроса), а не в горячем пути чата.
- ToT в 2026 частично вытеснен агентными циклами. Когда у модели есть инструменты, «оценка состояния» превращается в реальный запуск теста или солвера — а это гораздо надёжнее, чем модель, оценивающая саму себя. См. агенты и ReAct. ToT остаётся актуальным там, где внешнего верификатора нет.
4. Chain-of-Verification: отдельный проход на проверку
Механика
CoVe (Dhuliawala et al., arXiv:2309.11495) борется не с ошибками рассуждения, а с галлюцинациями фактов. Четыре шага:
- Baseline — обычный ответ.
- Plan verifications — модель сама формулирует проверочные вопросы к своему ответу.
- Execute verifications — на каждый вопрос отвечает независимо, не видя ни исходного ответа, ни остальных проверок.
- Final — модель переписывает ответ с учётом расхождений.
Критичная деталь — factored-режим на шаге 3. Если проверочные вопросы задавать в том же контексте, где лежит первичный ответ, модель просто повторяет свою же галлюцинацию: она обусловлена на неё. Изоляция контекста — вся суть метода.
Цифры из статьи: на задаче «перечисли известных людей, родившихся в X» точность (precision) выросла с 0.17 (few-shot) до 0.36 (factored CoVe); на длинных биографиях FactScore — с 55.9 до 71.4.
проверяющий НЕ видит черновик par параллельно App->>M3: q1 (чистый контекст) M3-->>App: a1 and App->>M3: q2 (чистый контекст) M3-->>App: a2 and App->>M3: q3 (чистый контекст) M3-->>App: a3 end App->>M4: черновик + (q,a) пары → удали неподтверждённое M4-->>App: финальный ответ + список отклонённых фактов
Реализация на TypeScript
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic();
const MODEL = "claude-opus-4-8";
async function ask(system: string, user: string, effort: "low" | "medium" | "high" = "medium") {
const r = await client.messages.create({
model: MODEL,
max_tokens: 4096,
system,
thinking: { type: "adaptive" },
output_config: { effort },
messages: [{ role: "user", content: user }],
});
return r.content
.filter((b): b is Anthropic.TextBlock => b.type === "text")
.map((b) => b.text)
.join("");
}
export async function chainOfVerification(question: string) {
// Шаг 1 — черновик.
const draft = await ask("Отвечай кратко и по существу.", question);
// Шаг 2 — план проверок. Просим атомарные, проверяемые вопросы.
const planRaw = await ask(
`Ты — фактчекер. По данному ответу сформулируй 3–6 проверочных вопросов.
Правила: каждый вопрос — про ОДИН атомарный факт; вопрос должен быть понятен
БЕЗ исходного ответа (никаких «он», «эта компания»); только проверяемые факты.
Выведи по одному вопросу на строку, без нумерации.`,
`Исходный вопрос: ${question}\n\nОтвет для проверки:\n${draft}`,
);
const questions = planRaw.split("\n").map((s) => s.trim()).filter(Boolean).slice(0, 6);
// Шаг 3 — factored: каждый вопрос в ЧИСТОМ контексте.
// Это главное отличие CoVe от наивного «перечитай свой ответ».
const answers = await Promise.all(
questions.map((q) =>
ask(
`Отвечай только на заданный вопрос. Если не уверен — ответь ровно "НЕ ЗНАЮ".
Не додумывай и не пытайся быть полезным сверх вопроса.`,
q,
"low",
).then((a) => ({ q, a })),
),
);
// Шаг 4 — сведение расхождений.
const evidence = answers.map(({ q, a }) => `В: ${q}\nО: ${a}`).join("\n\n");
const final = await ask(
`Перепиши ответ, оставив только то, что подтверждено проверками.
Утверждение, по которому проверка дала "НЕ ЗНАЮ" или противоречие, — УДАЛИ
и явно отметь как неподтверждённое. Не добавляй новых фактов.`,
`Вопрос: ${question}\n\nЧерновик:\n${draft}\n\nПроверки:\n${evidence}`,
"high",
);
return { draft, questions, answers, final, calls: 2 + questions.length };
}
Стоимость: 2 + n вызовов, где n — число проверок; при n = 4 это 6 вызовов вместо одного. Латентность — 3 последовательных этапа (проверки параллельны), то есть ≈ 3× к одиночному вызову, а не 6×.
Когда CoVe не работает
Здесь нужно быть жёстким, потому что вокруг self-correction много завышенных ожиданий.
Huang et al., «Large Language Models Cannot Self-Correct Reasoning Yet», arXiv:2310.01798 показывает: без внешней обратной связи самокоррекция на задачах рассуждения чаще ухудшает результат — модель «исправляет» верные ответы на неверные. Stechly et al., arXiv:2310.12397 получают то же на раскраске графов: итеративная самокритика GPT-4 не улучшает решения, а внешний верификатор — улучшает.
Разница между CoVe и «просто самокритикой» — в двух вещах:
- CoVe работает по фактам, а не по рассуждениям. Факт можно проверить независимым запросом; шаг рассуждения — нет.
- CoVe изолирует контекст. Это не «перечитай себя», а «ответь на вопрос заново, не зная своего прошлого ответа».
Отсюда правило: чем внешнее источник истины, тем лучше работает верификация. Если вместо шага 3 бить проверочными вопросами в поиск, в базу или в RAG — эффект кратно выше. Модель, проверяющая модель, — компромисс на случай, когда внешнего источника нет. Логичное развитие темы — вынести проверяющего в отдельного агента-критика: мультиагентные системы.
5. Что меняют reasoning-модели
С 2024–2025 годов появился класс моделей с обученным внутренним рассуждением: o-серия OpenAI, Claude с extended/adaptive thinking, DeepSeek-R1 (arXiv:2501.12948). У них CoT не наводится промптом, а выучен через RL с наградой за верный итог.
Что это меняет практически:
| Техника | На обычных моделях | На reasoning-моделях |
|---|---|---|
| Zero-shot CoT («think step by step») | заметный прирост | ≈ 0, иногда мешает |
| Структурированный few-shot CoT | сильный прирост | умеренный; полезен для методики, не для «думай» |
| Self-consistency | сильный прирост | прирост есть, но меньше; дорого вдвойне |
| Tree-of-Thoughts | сильный на search-задачах | частично поглощён внутренним поиском |
| Chain-of-Verification | сильный на фактах | сохраняется полностью — фактология не лечится рассуждением |
Управление бюджетом (effort, thinking) |
нет такого рычага | основной рычаг качества/цены |
Главный практический сдвиг: параметр усилия заменил половину промпт-инжиниринга. Вместо того чтобы писать «подумай тщательно», вы ставите effort: "high" и получаете больше внутренних токенов рассуждения. У Claude это output_config.effort со значениями low / medium / high / xhigh / max в паре с thinking: {"type": "adaptive"}; у DeepSeek-R1 и o-серии — свои аналоги.
Второй сдвиг: у reasoning-моделей внутренние рассуждения не возвращаются в сыром виде (у Claude — только summary при display: "summarized", у o-серии — не возвращаются вовсе). Значит, паттерны, которые парсили промежуточные шаги CoT из текста, надо переписывать: рассуждения теперь не ваш артефакт.
Третий: промпты, написанные под старые модели, часто вредят новым. Развёрнутые пошаговые скрипты, которые компенсировали слабость модели, теперь ограничивают её — она следует вашей (худшей) процедуре вместо своей. При миграции первым делом попробуйте убрать леса и сравнить.
6. Сводная таблица
| Техника | Вызовов | Латентность | Где выигрывает | Где бесполезна |
|---|---|---|---|---|
| Zero-shot CoT | 1 | 3–10× токенов | слабые/средние модели, арифметика | reasoning-модели, простые запросы |
| Few-shot CoT с процедурой | 1 | 3–10× | доменные методики, единый формат | творческие и открытые задачи |
| Self-consistency (k = 5) | k | ~1.3× (параллельно) | канонизируемые ответы, разброс велик | свободный текст, систематическая ошибка |
| Universal SC | k + 1 | ~1.5× | свободный текст с фактическим ядром | чисто творческие задачи |
| Tree-of-Thoughts | 20–60 | 10–30× | поиск, планирование, комбинаторика | всё, где нет ветвления |
| Chain-of-Verification | 2 + n | ~3× | фактология, перечисления, биографии | арифметика, логика |
| Внешний верификатор (тесты, солвер) | 1 + прогоны | зависит | код, SQL, всё исполнимое | неисполнимые домены |
7. Практика продакшена
Роутинг по сложности
Держать k = 5 и effort: max на всём трафике — самый быстрый способ сжечь бюджет. Рабочая схема: дешёвый классификатор сложности перед основным вызовом.
TIERS = {
"trivial": {"k": 1, "effort": "low", "cove": False},
"standard": {"k": 1, "effort": "medium", "cove": False},
"hard": {"k": 3, "effort": "high", "cove": False},
"critical": {"k": 5, "effort": "high", "cove": True},
}
def route(question: str, meta: dict) -> dict:
"""Классификатор — быстрая модель или эвристики, НЕ основная модель."""
if meta.get("money_at_risk") or meta.get("regulated_domain"):
return TIERS["critical"]
if meta.get("multi_step") or meta.get("numeric"):
return TIERS["hard"]
if len(question) < 120 and not meta.get("needs_context"):
return TIERS["trivial"]
return TIERS["standard"]
На типичном распределении трафика (70 % trivial/standard, 25 % hard, 5 % critical) средняя стоимость выходит порядка 1.5× от базовой вместо 5×. Детали по стоимости и наблюдаемости — в ИИ в продакшене.
Кеширование префикса
Все техники этой статьи многократно шлют один и тот же длинный системный промпт и few-shot блок. Кеширование префикса даёт ~90 % экономии на этой части. Ключевое ограничение: кеш — это префиксное совпадение, любое изменение байта инвалидирует всё после него. Значит, стабильное (инструкции, примеры, схема) — вперёд, изменчивое (сам вопрос, стратегия сэмплирования, метки времени) — назад.
resp = client.messages.create(
model=MODEL,
max_tokens=4096,
system=[
{"type": "text", "text": PROCEDURE + FEW_SHOT,
"cache_control": {"type": "ephemeral"}}, # стабильный префикс
],
messages=[{"role": "user", "content": f"{strategy}\n\n{question}"}], # изменчивое
)
assert resp.usage.cache_read_input_tokens > 0, "кеш не сработал — ищите инвалидатор"
Типичный «тихий инвалидатор» — подстановка текущей даты или UUID в начало системного промпта. Проверяется одной строкой: cache_read_input_tokens должен быть ненулевым на повторных запросах.
Наблюдаемость
Минимальный набор метрик, который надо снимать с любой из этих техник:
agreement(доля голосов) и распределение ответов — по перцентилям, не по среднему;parse_failures— сколько траекторий не дали разбираемого ответа;verification_rejections— сколько фактов CoVe отклонил (рост = деградация базовой генерации);calls_per_requestиoutput_tokens_per_request— раздельно по тирам;- доля запросов, ушедших в
critical, — если она ползёт вверх, роутер сломан.
Отдельно: логируйте не финальный ответ, а полную траекторию для сэмпла ~1 % трафика. Без неё разбирать инциденты невозможно.
8. Типичные ошибки
- Считать долю голосов вероятностью. 5/5 согласия на систематически неправильном ответе — это уверенная ошибка, а не уверенная правота. Калибруйте на своём датасете или не используйте вообще.
- Верификация в том же контексте. «Проверь свой ответ» в том же диалоге — почти всегда подтверждение собственной галлюцинации. Изоляция контекста обязательна.
- Показывать CoT как объяснение решения. Цепочка не обязана быть причиной ответа (Turpin et al.). В регулируемом домене это юридический риск, а не фича.
- ToT там, где есть внешний верификатор. Если решение можно прогнать через тест, солвер или интерпретатор — делайте это, а не спрашивайте модель, хорош ли её шаг.
- Переносить старые CoT-леса на reasoning-модели. Подробная процедура, написанная под слабую модель, ограничивает сильную. Проверяйте гипотезу «убрать всё и сравнить».
- Применять тяжёлую технику ко всему трафику. Без роутинга по сложности любая из них умножает счёт на константу без пропорционального выигрыша.
- Мерить на 20 примерах. Приросты в 3–7 п.п. неразличимы на маленькой выборке. Нужен датасет от нескольких сотен примеров и доверительные интервалы.
- Игнорировать
parse_failures. Траектория без разбираемого ответа — тоже сигнал: если их > 5 %, проблема в формате промпта, а не в рассуждении. Лечится структурированным выводом.
Мини-итог
- Все четыре техники — способы обменять вычисления на этапе вывода на качество. Разница только в топологии: длиннее, шире, с ветвлением или с проверкой.
- CoT даёт модели рабочую память. Работает на символьных и многошаговых задачах, почти не работает на здравом смысле, и не является объяснением ответа.
- Self-consistency гасит случайный разброс и бессилен против систематической ошибки. Логарифмическая отдача — в проде k = 3…5.
- Tree-of-Thoughts выигрывает там, где нужен бэктрекинг, и живёт качеством оценщика. Дорог; вытесняется агентами, где оценка заменена реальным исполнением.
- Chain-of-Verification борется с галлюцинациями фактов; работает только при изолированном контексте проверок и радикально усиливается внешним источником истины.
- На reasoning-моделях половина промпт-инжиниринга заменена параметром усилия; фактологическая верификация — единственная техника из четырёх, чья ценность не уменьшилась.
Источники
- Wei et al. Chain-of-Thought Prompting — arXiv:2201.11903
- Kojima et al. Large Language Models are Zero-Shot Reasoners — arXiv:2205.11916
- Wang et al. Self-Consistency — arXiv:2203.11171
- Chen et al. Universal Self-Consistency — arXiv:2311.17311
- Yao et al. Tree of Thoughts — arXiv:2305.10601, код
- Dhuliawala et al. Chain-of-Verification — arXiv:2309.11495
- Sprague et al. To CoT or not to CoT? — arXiv:2409.12183
- Turpin et al. Language Models Don’t Always Say What They Think — arXiv:2305.04388
- Lanham et al. Measuring Faithfulness in CoT Reasoning — arXiv:2307.13702
- Huang et al. LLMs Cannot Self-Correct Reasoning Yet — arXiv:2310.01798
- Stechly et al. GPT-4 Doesn’t Know It’s Wrong — arXiv:2310.12397
- Lightman et al. Let’s Verify Step by Step — arXiv:2305.20050
- Merrill & Sabharwal. The Expressive Power of Transformers with Chain of Thought — arXiv:2310.07923
- DeepSeek-AI. DeepSeek-R1 — arXiv:2501.12948
- Anthropic. Adaptive thinking и параметр effort — документация
- Anthropic. Prompt caching — документация
Что дальше
Все техники этой статьи упираются в один и тот же практический вопрос: как надёжно достать ответ из свободного текста. Голосование требует канонизируемого значения, ToT — разбираемого списка кандидатов, CoVe — структурированного набора проверок. Дальше — Структурированный вывод: JSON-схемы, function calling, валидация и восстановление.