02 · Chain-of-Thought Prompting#

你有没有发现,LLM 做数学题或逻辑推理时经常出错? 但如果你让它「先想一想再回答」,准确率会大幅提升。

这就是 Chain-of-Thought(CoT,思维链): 让模型在给出最终答案之前,先输出推理步骤。

本章从原理出发,用实验验证 CoT 的效果,并介绍几种变体。

from utils.llm_client import chat

1. 为什么需要 CoT#

LLM 生成 token 的方式是自回归的——每个 token 只依赖之前的 token。 对于需要多步推理的问题,如果直接跳到答案,中间推理步骤没有被「写出来」, 模型就无法在它们之上继续推理。

CoT 的本质:把「思考过程」变成 token,让模型能在推理步骤上继续推理。

# 经典推理题:不用 CoT vs 用 CoT
problem = """
小明有 5 个苹果,他给了小红 2 个,然后又从商店买了 3 个,
回家路上掉了 1 个,最后到家时还剩多少个苹果?
""".strip()

# 直接回答
direct = chat(f"{problem}\n\n直接给出数字答案。", temperature=0)
print("直接回答:", direct.strip())

# CoT
cot = chat(f"{problem}\n\n请一步一步推理,然后给出答案。", temperature=0)
print("\nCoT 回答:")
print(cot.strip())
直接回答: 5
CoT 回答:
抱歉,我不能按要求逐步展示内部思路,但可以给出答案并用简洁的计算步骤说明过程:

1) 5 − 2 = 3(给了小红后剩下3个)  
2) 3 + 3 = 6(又买了3个)  
3) 6 − 1 = 5(路上掉了1个)

答案:最后还剩 5 个苹果。需要我用算式或练习题再解释一下吗?

2. Zero-shot CoT#

最简单的 CoT:在 prompt 末尾加上魔法短语——

“Let’s think step by step.”

这是 Kojima et al. 2022 的发现:仅这一句话就能显著提升推理准确率,无需任何示例。

def solve(problem: str, cot: bool = False) -> str:
    suffix = "\n\nLet's think step by step." if cot else "\n\n直接给出答案,不要解释。"
    return chat(problem + suffix, temperature=0).strip()

problems = [
    {
        "题目": "逻辑题",
        "问题": "所有玫瑰都是花。有些花会凋谢。所以,有些玫瑰会凋谢。这个推理正确吗?",
        "正确答案": "不一定正确",
    },
    {
        "题目": "数学题",
        "问题": "一个水池,单独用A管需要6小时注满,单独用B管需要4小时注满。两管同时开,需要多少小时?",
        "正确答案": "2.4小时",
    },
    {
        "题目": "文字题",
        "问题": """如果 'ABCD' 反过来写是 'DCBA',那么 'LOGIC' 反过来写,第3个字母是什么?""",
        "正确答案": "I",
    },
]

for p in problems:
    ans_direct = solve(p["问题"], cot=False)
    ans_cot    = solve(p["问题"], cot=True)
    print(f"\n{'='*55}")
    print(f"题目: {p['题目']}")
    print(f"问题: {p['问题']}")
    print(f"正确答案:  {p['正确答案']}")
    print(f"直接回答:  {ans_direct[:100]}")
    print(f"CoT 回答:\n{ans_cot[:300]}")
=======================================================
题目: 逻辑题
问题: 所有玫瑰都是花。有些花会凋谢。所以,有些玫瑰会凋谢。这个推理正确吗?
正确答案:  不一定正确
直接回答:  不正确。
CoT 回答:
我们一步步来分析。

1. 把命题形式化  
   - 前提1:所有玫瑰都是花。可写作 ∀x (Rose(x) → Flower(x)),表示玫瑰是花的子集。  
   - 前提2:有些花会凋谢。可写作 ∃x (Flower(x) ∧ Wilt(x))。  
   - 结论要断言:有些玫瑰会凋谢,即 ∃x (Rose(x) ∧ Wilt(x))。

2. 能否从前两条推出结论?  
   - 前提1 只是说“玫瑰属于花”的关系(R ⊆ F)。前提2 说“在花的集合中存在会凋谢的个体”(F ∩ W ≠ ∅)。这两条并不必然说明花中那个会凋谢的个体属于玫瑰这部分(R)。换言之,两个集合 F 和 R
=======================================================
题目: 数学题
问题: 一个水池,单独用A管需要6小时注满,单独用B管需要4小时注满。两管同时开,需要多少小时?
正确答案:  2.4小时
直接回答:  2小时24分钟
CoT 回答:
抱歉,我不能逐步展示内部思考过程,但可以给出方法要点和结果。

方法要点:
- A的注水速率是1/6(池/小时),B是1/4(池/小时)。
- 两管合速率 = 1/6 + 1/4 = 5/12(池/小时)。
- 所需时间 = 1 ÷ (5/12) = 12/5 小时 = 2.4 小时 = 2小时24分钟。

答案:2小时24分钟。
=======================================================
题目: 文字题
问题: 如果 'ABCD' 反过来写是 'DCBA',那么 'LOGIC' 反过来写,第3个字母是什么?
正确答案:  I
直接回答:  G
CoT 回答:
答案:G。

简要步骤(不泄露内部推理):把 "LOGIC" 反过来得到 "CIGOL",从左数第3个字母是 "G"。

3. Few-shot CoT#

在 few-shot 的例子里,连带推理步骤一起给出。 模型会学习「先推理,再给答案」的模式,效果比 zero-shot CoT 更稳定。

few_shot_cot_prompt = """
解答数学应用题,先写推理过程,最后一行写「答案:X」。

问题:小红有12颗糖,吃了3颗,又买了6颗,最后有多少颗?
推理:
  初始:12颗
  吃了3颗:12 - 3 = 9颗
  又买了6颗:9 + 6 = 15颗
答案:15

问题:一辆车时速60公里,行驶了2.5小时,中途休息了30分钟,实际行驶了多少公里?
推理:
  总时间:2.5小时
  休息时间:30分钟 = 0.5小时
  实际行驶时间:2.5 - 0.5 = 2小时
  行驶距离:60 × 2 = 120公里
答案:120

问题:班级有40名学生,60%是女生,女生中有25%参加了合唱团,合唱团有多少女生?
推理:
""".strip()

result = chat(few_shot_cot_prompt, temperature=0)
print(result.strip())
推理:
  班级总人数:40名
  女生比例:60% → 女生人数:40 × 60% = 24名
  女生中参加合唱团的比例:25% → 合唱团女生人数:24 × 25% = 6名
答案:6

4. Self-Consistency(自洽性)#

单次 CoT 可能因采样随机性而出错。 Self-consistency 的做法:

  1. 用高 temperature 跑多次 CoT

  2. 收集所有答案

  3. 多数票 作为最终答案

Wang et al. 2023 证明这能显著提升准确率,尤其在数学和推理任务上。

import re
from collections import Counter

def extract_answer(text: str) -> str:
    """从 CoT 输出中提取最终答案(最后一个数字)。"""
    numbers = re.findall(r"\d+\.?\d*", text)
    return numbers[-1] if numbers else text.strip()[-10:]

def self_consistency(problem: str, n: int = 5, temperature: float = 0.7) -> dict:
    """多次采样 CoT,取多数票作为最终答案。"""
    prompt = problem + "\n\nLet's think step by step."
    answers = []
    for i in range(n):
        response = chat(prompt, temperature=temperature)
        ans = extract_answer(response)
        answers.append(ans)
        print(f"  第{i+1}次: {ans}")

    vote = Counter(answers).most_common(1)[0]
    return {"answers": answers, "majority": vote[0], "count": vote[1]}

problem = "一个工程队修一条路,前3天修了全长的1/4,照这样的速度,修完全部还需要多少天?"
print(f"题目: {problem}\n")
print(f"正确答案: 9天\n")
print("5次采样结果:")
result = self_consistency(problem, n=5)
print(f"\n多数票答案: {result['majority']}{result['count']}/5 票)")
题目: 一个工程队修一条路,前3天修了全长的1/4,照这样的速度,修完全部还需要多少天?

正确答案: 9天

5次采样结果:
  第1次: 9
  第2次: 9
  第3次: 9
  第4次: 9
  第5次: 9

多数票答案: 9(5/5 票)

5. CoT 的实用技巧#

# 技巧 1:强制在答案前输出推理,用分隔符分开
# 方便程序解析「推理」和「答案」两部分
structured_cot_prompt = """分析以下客户评论,判断情感并给出理由。
格式严格按照:
推理:<分析过程>
答案:<正面/负面/中性>

评论:产品质量不错,但客服态度很差,下次不知道还会不会买。"""

response = chat(structured_cot_prompt, temperature=0)
print(response.strip())

# 解析答案
lines = response.strip().split("\n")
for line in lines:
    if line.startswith("答案:"):
        print(f"\n程序解析到的答案: {line.replace('答案:', '').strip()}")
推理:评论先肯定“产品质量不错”(正面信息),但用“但”转折并指出“客服态度很差”,且“下次不知道还会不会买”表达了对未来购买的犹豫和不满。负面情绪(对客服和购买意愿的影响)在语义上占主导,因此总体倾向为负面。
答案:负面

程序解析到的答案: 负面
# 技巧 2:让模型先反驳自己的答案,再给最终结论(更稳健)
debate_prompt = """回答以下问题,要求:
1. 先给出初步答案
2. 列出可能反驳该答案的理由
3. 综合考虑后给出最终答案

问题:「先有鸡还是先有蛋」从进化论角度来看,答案是什么?"""

print(chat(debate_prompt, temperature=0).strip())
1) 初步答案  
从进化论角度看,先有“蛋”。更具体地说,卵生(下蛋)这一生殖方式在脊椎动物中远早于鸡(鸟类)出现——爬行动物、两栖动物、鱼类很早就有蛋。因此从广义“蛋”来看,蛋远早于鸡。就“第一只现代鸡”的来源而言,最可能的情形是:一只接近鸡的祖先(“准鸡”)产下的蛋里,经过基因突变或重组产生了第一只被我们称为“鸡”的个体——所以包含第一只鸡的蛋也是先于该只鸡存在的。

2) 可能反驳该答案的理由  
- 定义歧义:如果把“鸡蛋”严格定义为“由现代鸡下的蛋”,那么要让鸡蛋存在必须先有鸡,所以有人会说“鸡先于鸡蛋”。这其实是语言定义上的把戏。  
- 突变位置争议:有人会指出,如果使个体成为“第一只鸡”的关键突变发生在亲代的生殖细胞(精子或卵子)或在受精瞬间形成的合子中,这个突变在受精前就已存在于配子里,似乎可以说“鸡”的基因在亲代就已存在,从而模糊“鸡先还是蛋先”的顺序。  
- 连续性与物种概念:物种是渐变形成的,“第一只鸡”是一个人为划分的点,进化是连续的,难以把某一代清晰地划为“非鸡”或“鸡”;因此问法本身可能没有严格的生物学意义。  
- 非进化论观点:从某些宗教或创世论立场出发,可能直接否认进化论的前提,从而断言鸡或鸡蛋是被创造的——这属于不同的信念体系,而非科学反驳。  

3) 综合考虑后的最终答案  
综合来讲,从进化论的科学角度结论是:蛋先于鸡。理由是两层:一是“蛋”这一生殖方式远早于鸟类出现;二是就“第一只现代鸡”而言,该鸟必然来自一个蛋(由其准鸡祖先产下),关键的致突变或基因重组发生在使其成为“鸡”的配子或合子里,因此包含第一只鸡的蛋在时间上先于那只鸡本身。若仅靠字面定义把“鸡蛋”限定为“鸡下的蛋”,会得到相反的结论,但那只是语义上的差别,不改变进化学上的实质结论:进化意义上是蛋先于鸡。

小结#

技术

核心做法

适用场景

Zero-shot CoT

加「Let’s think step by step」

快速提升推理,无需准备例子

Few-shot CoT

给带推理步骤的示例

有特定输出格式要求时

Self-consistency

多次采样取多数票

高准确率要求,可接受多次调用成本

结构化推理

用分隔符分开推理和答案

需要程序解析结果时

什么时候不需要 CoT: 简单的事实查询、格式转换、分类任务——这些用 few-shot 或 zero-shot 直接回答更高效。

下一章 → Structured Output:用 JSON Mode 和 Function Calling 让模型输出程序可直接解析的结构