Chapter 02

Chain-of-Thought 工程:让模型说出思考

CoT 不只是"一句话提示词",而是一套完整的提示工程体系。掌握它,能让普通模型解决推理模型才能处理的问题。

什么是 Chain-of-Thought

2022 年 Google Brain 发表的论文《Chain-of-Thought Prompting Elicits Reasoning in Large Language Models》证明:让模型在给出答案前先写出推理步骤,准确率会显著提高。这个看似简单的发现,催生了整个 CoT 提示工程体系,也是推理模型(o1、R1)背后的核心洞见。

普通提示(直接答题): 问:Roger 有 5 个网球,买了 2 罐(每罐 3 个),现在有多少? 答:11 ← 直接输出,无推理过程,中途不可纠错 CoT 提示(逐步推理): 问:[同题] 请一步步思考。 答:Roger 开始有 5 个网球。 他买了 2 罐,每罐 3 个,共 2 × 3 = 6 个新球。 5 + 6 = 11 个。 答案是 11。 ← 每步可以被验证和纠错
为什么 CoT 有效?(原理解释)
将复杂问题分解成多个中间步骤,每个步骤都在 LLM 的单步推理能力范围内。类比人类在草稿纸上演算:直接心算 17×23 容易出错,但写下 17×20=340、17×3=51、340+51=391 就很可靠。中间步骤为下一步提供了正确的"起点",避免错误传播。
CoT 的规模效应(Emergent Ability)
研究发现 CoT 对小模型(参数量 <10B)效果有限甚至负面——小模型无法生成有效推理步骤。对大模型(>100B)效果显著。o1/R1 等推理模型通过训练将 CoT 能力内化为模型权重,突破了这一规模限制:即使是蒸馏后的 7B 模型也能表现出强大的推理能力。
显式 CoT vs 隐式 CoT
显式 CoT:推理步骤出现在可见输出中(通过提示工程触发)。隐式 CoT:推理步骤在模型内部发生但不输出(推理模型的 thinking block 中)。推理模型本质上是将显式 CoT 内化为训练目标,让模型自动生成内部推理轨迹。

Zero-shot CoT:最简单的触发词

2022 年 Kojima 等人发现:在问题末尾添加 "Let's think step by step" 就能显著提升推理准确率,无需任何示例。这个发现极为重要——它说明大型 LLM 已经具备了推理能力,只是默认不会"展示"出来。

import anthropic

client = anthropic.Anthropic()

def zero_shot_cot(problem: str) -> str:
    """Zero-shot CoT:在问题末尾添加触发词,无需示例"""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{
            "role": "user",
            "content": f"{problem}\n\n请一步步思考,然后给出最终答案。"
        }]
    )
    return response.content[0].text

# 有效的 Zero-shot CoT 触发词(按效果强弱排序)
triggers = [
    "Let's think step by step.",          # Google Brain 原版,英文通用
    "请一步步分析这个问题。",               # 中文通用
    "Think carefully before answering.",  # 更谨慎的版本
    "Show your reasoning process.",       # 要求显示过程
    "Work through this step by step.",    # 强调步骤性
    "Let me verify my answer step by step.",  # 强调验证
]

# 双步骤 CoT:先推理,再提取答案(降低格式混乱)
def two_step_cot(problem: str) -> tuple[str, str]:
    """Step 1: 自由推理; Step 2: 提取最终答案"""
    # Step 1: 获取推理过程
    reasoning = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        messages=[{"role": "user",
                   "content": f"{problem}\n\n请一步步分析。"}]
    ).content[0].text

    # Step 2: 基于推理提取结构化答案
    answer = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=128,
        messages=[
            {"role": "user", "content": f"{problem}\n\n请一步步分析。"},
            {"role": "assistant", "content": reasoning},
            {"role": "user", "content": "综合以上分析,最终答案是(只输出答案,不要解释):"}
        ]
    ).content[0].text

    return reasoning, answer

Few-shot CoT:用示例教会推理风格

当 Zero-shot CoT 不够精准时,提供 3-8 个带完整推理过程的示例,引导模型模仿特定的推理风格。Few-shot CoT 的关键不是"示例数量多",而是"示例质量高且覆盖不同推理子类型"。

FEW_SHOT_COT_PROMPT = """解题示例:

Q: 一个水箱可以储存 500 升水,现在已装了 60%,再装 50 升后是多少升?
A: 分析:
   当前水量 = 500 × 60% = 300 升
   加水后 = 300 + 50 = 350 升
   验证:350 ≤ 500(未超容量)✓
   最终答案:350 升

Q: 一个工人每天工作 8 小时,月薪 6400 元,时薪是多少?
A: 分析:
   每月工作天数 = 22 天(国内标准工作制)
   每月总工时 = 22 × 8 = 176 小时
   时薪 = 6400 ÷ 176 ≈ 36.36 元
   验证:合理范围(行业最低工资约 20 元/h)✓
   最终答案:约 36.4 元/小时

Q: 一家商店原价 200 元的商品打八折后再优惠 20 元,最终价格是多少?
A: 分析:
   八折后价格 = 200 × 0.8 = 160 元
   再减 20 元 = 160 - 20 = 140 元
   最终答案:140 元

现在请解答:
Q: {question}
A: 分析:"""

# Few-shot 示例设计原则(重要):
# 1. 推理风格要与期望输出格式完全一致
# 2. 示例要覆盖不同子类型(加法、百分比、多步骤)
# 3. 示例中要包含验证步骤,引导模型自我检查
# 4. 示例数量 3-5 个效果最好,太多占用上下文
# 5. 确保示例本身没有错误(错误示例会被模型学习)
Few-shot CoT 的陷阱

Few-shot 示例的推理路径会强烈影响模型的推理方式。如果示例使用了特定方法(如代入法),模型可能强行用同样方法解所有题,即使其他方法更简单。示例设计要多样化,或者明确说明"请选择最适合的方法"。另外,示例中的任何错误都会被模型学习——错误的示例比没有示例更糟糕

Self-Consistency:多路径投票提升可靠性

Single-path CoT 存在随机性——不同的采样可能得到不同答案。Self-Consistency(Wang et al. 2023)通过多次采样取多数票来提高可靠性,本质上是将 CoT 与投票机制结合。

from collections import Counter
import re

def extract_final_answer(text: str) -> str:
    """提取最后一行或"答案是X"格式的答案"""
    # 尝试匹配 "答案是X" 或 "最终答案:X" 格式
    patterns = [
        r'最终答案[::]\s*(.+?)[\s。]?$',
        r'答案是[::]\s*(.+?)[\s。]?$',
        r'= (.+?)$',
    ]
    for pattern in patterns:
        match = re.search(pattern, text, re.MULTILINE)
        if match:
            return match.group(1).strip()
    # 回退:取最后一行非空内容
    lines = [l.strip() for l in text.split('\n') if l.strip()]
    return lines[-1] if lines else ""

def self_consistency(problem: str, n_samples: int = 5) -> dict:
    """
    Self-Consistency:采样 n 次,取多数票答案
    n_samples: 采样次数,推荐 5-20(越多越准确但成本越高)
    """
    answers = []
    reasoning_paths = []

    for i in range(n_samples):
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=512,
            temperature=0.7,  # 适当随机性产生多样的推理路径
            messages=[{
                "role": "user",
                "content": f"{problem}\n\n请一步步思考。最后一行只写最终数字答案,格式:答案:[数字]"
            }]
        )
        text = response.content[0].text
        answer = extract_final_answer(text)
        answers.append(answer)
        reasoning_paths.append(text)

    # 多数投票
    counter = Counter(answers)
    majority_answer, majority_count = counter.most_common(1)[0]
    confidence = majority_count / n_samples

    # 找到与多数答案对应的最佳推理路径(用于解释)
    best_reasoning = reasoning_paths[answers.index(majority_answer)]

    return {
        "answer": majority_answer,
        "confidence": confidence,
        "vote_distribution": dict(counter),
        "best_reasoning": best_reasoning
    }

# 示例输出:
# {"answer": "140", "confidence": 0.8, "vote_distribution": {"140": 4, "160": 1}}
# confidence=0.8 表示 5次中有4次得到相同答案,可信度较高
Self-Consistency 的性价比

在 GSM8K 数学基准上,Self-Consistency (n=40) 比单次 CoT 提升约 10-20 个百分点。对于高风险场景(医疗计算、金融决策),值得投入这额外的成本。实践建议:先用 n=3 快速验证,若置信度低(<67%)再增加到 n=10。置信度高(>80%)可以信任多数结果。

Tree-of-Thought:树状搜索解决复杂问题

Self-Consistency 是"并行的多路径"——所有路径相互独立。Tree-of-Thought(Yao et al. 2023)是主动探索+评估+剪枝的树状搜索,更像人类解决难题时的行为:探索一条路,发现不行就回头换路。

ToT 搜索树示意: 问题 │ ├─ 思路 A(LLM 自评分:7/10) │ ├─ A1(分:8/10)→ 继续展开 → A1a(分:9/10)→ 最终答案 │ └─ A2(分:4/10)→ 剪枝,停止 │ ├─ 思路 B(LLM 自评分:9/10)← 最优起点 │ └─ B1(分:9/10)→ 继续展开 → 找到更优答案 │ └─ 思路 C(LLM 自评分:3/10)→ 剪枝,停止 最终选择:B1 路径的结果(分数最高的完整路径)
def generate_thoughts(problem: str, context: str = "", n: int = 3) -> list[str]:
    """生成 n 个独立的推理思路"""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1000,
        messages=[{"role": "user", "content": f"""
问题:{problem}
{f"已有推理:{context}" if context else ""}

请提供 {n} 个不同的推理思路(每个思路用 === 分隔,每个思路 2-3 句话):"""}]
    )
    return response.content[0].text.split("===")

def evaluate_thought(problem: str, thought: str) -> float:
    """让 LLM 评估某个思路的价值(0-10分)"""
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=100,
        messages=[{"role": "user", "content": f"""
问题:{problem}
推理思路:{thought}

评估这个思路解决问题的可行性(0-10分)。只输出一个整数。"""}]
    )
    try:
        return float(response.content[0].text.strip())
    except:
        return 5.0  # 无法解析时给中等分

def tree_of_thought(problem: str, breadth: int = 3, depth: int = 3) -> str:
    """
    Tree-of-Thought BFS 搜索
    breadth: 每一层保留的最优思路数(剪枝宽度)
    depth: 最大搜索深度
    """
    thoughts = [""]  # 初始思路为空

    for step in range(depth):
        # 1. 展开:每个现有思路生成新的子思路
        new_thoughts = []
        for thought in thoughts:
            next_thoughts = generate_thoughts(problem, thought, n=2)
            new_thoughts.extend(next_thoughts)

        # 2. 评估:为每个思路打分
        scored = [(t, evaluate_thought(problem, t)) for t in new_thoughts]

        # 3. 剪枝:只保留 top-k 思路
        thoughts = [t for t, s in sorted(scored, key=lambda x: -x[1])[:breadth]]

    # 4. 基于最佳推理路径生成最终答案
    response = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        messages=[{"role": "user", "content": f"""
问题:{problem}
最优推理路径:{thoughts[0]}
基于以上推理,给出最终答案:"""}]
    )
    return response.content[0].text
ToT 的成本与适用场景

Tree-of-Thought 需要 breadth × depth 次 LLM 评估调用(生成调用另计),总调用次数约为普通 CoT 的 10-30 倍,成本较高。仅在以下场景使用:(1) 问题有多条可行路径需要探索,(2) 错误成本高(复杂代码设计、数学证明),(3) 明确需要最优解而非可行解。对于简单多步计算,直接用 Self-Consistency 即可。

Least-to-Most:从简到难的问题分解

对于"大问题套小问题"的复杂场景,Least-to-Most Prompting 先识别并解决所有必要的子问题,再综合成最终答案。这是一种"归约"策略:把当前无法直接解决的问题,归约为可以解决的子问题。

LEAST_TO_MOST_DECOMPOSE = """要解决以下问题,需要先解决哪些更简单的子问题?
按难度从低到高列出所有必要的子问题(每行一个):

问题:{problem}

子问题(从最简单开始):"""

LEAST_TO_MOST_SOLVE = """问题:{problem}

已解决的子问题及答案:
{solved_so_far}

现在解决子问题:{current_sub}
答案:"""

def least_to_most(complex_problem: str) -> str:
    """Least-to-Most:先分解子问题,再逐步解决,最后综合"""

    # Step 1: 识别所有子问题
    decompose_resp = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        messages=[{"role": "user",
                   "content": LEAST_TO_MOST_DECOMPOSE.format(problem=complex_problem)}]
    )
    sub_problems = [
        line.strip().lstrip("0123456789.-) ")
        for line in decompose_resp.content[0].text.split("\n")
        if line.strip()
    ]

    # Step 2: 逐步解决每个子问题(每步都用前面的结果)
    solved = []
    for sub in sub_problems:
        solved_text = "\n".join([f"- {q}: {a}" for q, a in solved])
        response = client.messages.create(
            model="claude-sonnet-4-6",
            max_tokens=256,
            messages=[{"role": "user", "content": LEAST_TO_MOST_SOLVE.format(
                problem=complex_problem,
                solved_so_far=solved_text or "(暂无)",
                current_sub=sub
            )}]
        )
        answer = response.content[0].text.strip()
        solved.append((sub, answer))

    # Step 3: 综合所有子答案给出最终结论
    all_solutions = "\n".join([f"- {q}: {a}" for q, a in solved])
    final = client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=500,
        messages=[{"role": "user", "content": f"""
原始问题:{complex_problem}

已解决的所有子问题:
{all_solutions}

请综合以上结果,给出最终完整答案:"""}]
    )
    return final.content[0].text

CoT 方法对比与选择指南

方法适用场景成本(相对单次)实现难度准确率提升
Zero-shot CoT通用推理,快速原型1x极低(一行代码)+10~20%
Few-shot CoT特定格式、领域推理1x(+示例 token)低(设计示例)+15~30%
Self-Consistency数学、有确定答案的推理5x~20x低(循环采样)+20~40%
Tree-of-Thought需要探索多路径的复杂问题10x~50x中(实现搜索树)+30~50%
Least-to-Most有明确层级结构的复合问题3x~8x低(分解+组合)+25~45%
本章小结

Chain-of-Thought 是推理模型背后的核心思想在提示工程层面的体现。Zero-shot CoT 简单有效(加一句触发词),Few-shot CoT 教会推理风格(设计高质量示例),Self-Consistency 用多路径投票提高可靠性,ToT 用树搜索解决需要探索的复杂问题,Least-to-Most 用分解策略处理复合问题。实际选择时以"最简单能解决问题的方法"为原则。下一章解析 DeepSeek-R1 如何用强化学习训练推理能力——将 CoT 内化到模型权重本身。