ИИ-агенты и prompt engineering Продвинутые техники: chain-of-thought, self-consistency, tree-of-thoughts, chain-of-verification
0%

Продвинутые техники: chain-of-thought, self-consistency, tree-of-thoughts, chain-of-verification

Продвинутые техники: 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 раз выше входных это часто основная статья расходов.


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 базовая
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 (репозиторий) формализует это как поиск по дереву, где узел — частичное решение («мысль»), а сама модель играет три роли:

  1. Генератор — предлагает b продолжений текущего состояния.
  2. Оценщик — оценивает каждое состояние (sure / maybe / impossible либо число 0–1).
  3. Стратегия поиска — 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) борется не с ошибками рассуждения, а с галлюцинациями фактов. Четыре шага:

  1. Baseline — обычный ответ.
  2. Plan verifications — модель сама формулирует проверочные вопросы к своему ответу.
  3. Execute verifications — на каждый вопрос отвечает независимо, не видя ни исходного ответа, ни остальных проверок.
  4. Final — модель переписывает ответ с учётом расхождений.

Критичная деталь — factored-режим на шаге 3. Если проверочные вопросы задавать в том же контексте, где лежит первичный ответ, модель просто повторяет свою же галлюцинацию: она обусловлена на неё. Изоляция контекста — вся суть метода.

Цифры из статьи: на задаче «перечисли известных людей, родившихся в X» точность (precision) выросла с 0.17 (few-shot) до 0.36 (factored CoVe); на длинных биографиях FactScore — с 55.9 до 71.4.

Реализация на 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. Типичные ошибки

  1. Считать долю голосов вероятностью. 5/5 согласия на систематически неправильном ответе — это уверенная ошибка, а не уверенная правота. Калибруйте на своём датасете или не используйте вообще.
  2. Верификация в том же контексте. «Проверь свой ответ» в том же диалоге — почти всегда подтверждение собственной галлюцинации. Изоляция контекста обязательна.
  3. Показывать CoT как объяснение решения. Цепочка не обязана быть причиной ответа (Turpin et al.). В регулируемом домене это юридический риск, а не фича.
  4. ToT там, где есть внешний верификатор. Если решение можно прогнать через тест, солвер или интерпретатор — делайте это, а не спрашивайте модель, хорош ли её шаг.
  5. Переносить старые CoT-леса на reasoning-модели. Подробная процедура, написанная под слабую модель, ограничивает сильную. Проверяйте гипотезу «убрать всё и сравнить».
  6. Применять тяжёлую технику ко всему трафику. Без роутинга по сложности любая из них умножает счёт на константу без пропорционального выигрыша.
  7. Мерить на 20 примерах. Приросты в 3–7 п.п. неразличимы на маленькой выборке. Нужен датасет от нескольких сотен примеров и доверительные интервалы.
  8. Игнорировать parse_failures. Траектория без разбираемого ответа — тоже сигнал: если их > 5 %, проблема в формате промпта, а не в рассуждении. Лечится структурированным выводом.

Мини-итог

  • Все четыре техники — способы обменять вычисления на этапе вывода на качество. Разница только в топологии: длиннее, шире, с ветвлением или с проверкой.
  • CoT даёт модели рабочую память. Работает на символьных и многошаговых задачах, почти не работает на здравом смысле, и не является объяснением ответа.
  • Self-consistency гасит случайный разброс и бессилен против систематической ошибки. Логарифмическая отдача — в проде k = 3…5.
  • Tree-of-Thoughts выигрывает там, где нужен бэктрекинг, и живёт качеством оценщика. Дорог; вытесняется агентами, где оценка заменена реальным исполнением.
  • Chain-of-Verification борется с галлюцинациями фактов; работает только при изолированном контексте проверок и радикально усиливается внешним источником истины.
  • На reasoning-моделях половина промпт-инжиниринга заменена параметром усилия; фактологическая верификация — единственная техника из четырёх, чья ценность не уменьшилась.

Источники

Что дальше

Все техники этой статьи упираются в один и тот же практический вопрос: как надёжно достать ответ из свободного текста. Голосование требует канонизируемого значения, ToT — разбираемого списка кандидатов, CoVe — структурированного набора проверок. Дальше — Структурированный вывод: JSON-схемы, function calling, валидация и восстановление.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов