思维链进阶-Tree-of-Thoughts与Self-Consistency
> **本文适合谁**
思维链进阶:Tree of Thoughts 与 Self-Consistency
本文适合谁
已经熟悉基础 CoT、但在复杂推理任务上准确率仍不稳定的开发者。ToT 和自洽采样是 CoT 的进阶版,适合数学、逻辑分析、需要探索多条路径的任务。
标准 CoT 让模型在回答前写出推理步骤,效果比直接给答案好得多。但它有一个根本性的局限:推理是线性的,一旦某个中间步骤走偏,后续步骤会继续沿着错误的方向推进,最终给出一个"看起来有理有据但结论错误"的回答。
这种现象在数学题和多步逻辑推理中尤为明显。本章介绍两种进阶技术:Self-Consistency(自洽采样,让模型多次独立回答同一问题,取多数结果)和 Tree of Thoughts(树状思维,让模型主动探索多条推理路径并评估选优),它们用不同的方式解决这个问题。
标准 CoT 的局限
三种推理策略对比——CoT 线性推理、ToT 树状探索、Self-Consistency 多路投票,各有适用场景
一个具体例子说明问题所在。给模型一道推理题:
"John 有 5 个苹果,给了 Mary 2 个,Mary 又给了 Tom 1 个,Tom 原来有 3 个。现在 John 和 Tom 合计有多少个?"
标准 CoT 可能的推理路径:
1. John 原有 5 个,给了 Mary 2 个,John 剩 5-2=3 个 ✓
2. Mary 收到 2 个,又给了 Tom 1 个,Tom 得到 1 个 ✓
3. Tom 原来有 3 个,加上 1 个,Tom 有 3+1=4 个 ✓
4. John 有 3 个,Tom 有 4 个,合计 3+4=7 个 ✓
这道题 CoT 能正确处理。但如果题目更复杂,步骤更多,中间某步出错了,后续全部连锁错误,而模型不会回头检查——因为它生成 token 是单向的,没有"后悔"机制。
标准 CoT 的核心局限在于:它是一条不可回溯的单路径。就像走迷宫时只能向前走、不能退回来,一旦选错了路口,就会在错误的方向上越走越远。
Self-Consistency:多次采样取众数
统计原理:错误是发散的,正确是收敛的
Self-Consistency 的思路很直接:既然单次推理可能走偏,那就多推理几次,取最多人同意的答案。
这背后有一个重要的统计直觉:错误答案的分布是发散的,正确答案的分布是收敛的。
想象一道数学题,正确答案是 42。可能出错的方式有很多——计算第一步时出错得到 38,算第三步时出错得到 51,误解题意得到 65……错误路径是多种多样的,每种错误各自出现的频率相对较低。但正确路径只有一条(或有限几条),通向唯一正确答案 42。
当你进行 N 次独立采样时(每次用稍高的 temperature 引入随机性),正确答案 42 会在多次采样中反复出现,而各种错误答案由于分散,单个错误的出现频率较低。通过投票取多数,就能以较高概率选出正确答案。
这个原理和**集成学习(Ensemble Learning)**中的 Bagging 策略非常类似:单个分类器的错误是随机的、不相关的,多个分类器的多数投票结果比单个分类器更可靠。
原理如下:用较高的 temperature(温度参数,值越高模型输出越随机多样)多次调用模型,得到多条独立的推理路径,各路径可能走不同的"弯路",但正确答案应该在多数路径里出现。错误的推理路径是发散的(有很多种错法),正确的推理路径是收敛的(答案是唯一的)——这是 Self-Consistency 有效的数学直觉。
Self-Consistency 实现
import os
import re
from collections import Counter
from typing import Optional
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
# temperature 设高一点,让每次采样路径不同
# 如果 temperature=0,每次结果一样,投票没有意义
temperature=0.7,
)
cot_template = ChatPromptTemplate.from_messages([
("system", "你是一个精确的推理助手。请一步一步分析,最后给出答案。答案格式为:最终答案:[数字或结论]"),
("human", "{question}"),
])
def extract_answer(response: str) -> Optional[str]:
"""从推理结果中提取最终答案"""
# 匹配"最终答案:xxx"或"答案是xxx"等模式
patterns = [
r"最终答案[::]\s*(.+?)(?:\n|$)",
r"答案[是为][::]?\s*(.+?)(?:\n|$)",
r"所以.{0,5}[是为有]\s*(.+?)(?:\n|$)",
]
for pattern in patterns:
match = re.search(pattern, response)
if match:
return match.group(1).strip()
# 如果没有匹配到标准格式,取最后一行作为答案
lines = [l.strip() for l in response.strip().split("\n") if l.strip()]
return lines[-1] if lines else None
def self_consistency(question: str, num_samples: int = 5) -> dict:
"""
Self-Consistency 推理:多次采样,取众数答案
num_samples=5 是实践中常用的数量,再多收益递减但成本线性增加
"""
answers = []
reasoning_paths = []
for i in range(num_samples):
messages = cot_template.format_messages(question=question)
response = llm.invoke(messages).content
answer = extract_answer(response)
reasoning_paths.append(response)
if answer:
answers.append(answer)
# 统计各答案出现次数
answer_counts = Counter(answers)
majority_answer, majority_count = answer_counts.most_common(1)[0]
return {
"question": question,
"majority_answer": majority_answer,
"confidence": majority_count / num_samples,
"answer_distribution": dict(answer_counts),
"reasoning_paths": reasoning_paths,
"num_samples": num_samples,
}
# 验证目的:观察多次采样的答案分布,验证多数投票是否能纠正偶发错误
question = """
小明有24块糖,他给了小红1/3,小红又把自己的糖的一半给了小李。
如果小李原来有8块糖,现在小李有多少块糖?
"""
result = self_consistency(question, num_samples=5)
print(f"问题:{result['question'].strip()}")
print(f"最终答案:{result['majority_answer']}")
print(f"置信度:{result['confidence']:.0%}({result['num_samples']} 次采样中的多数)")
print(f"答案分布:{result['answer_distribution']}")
Self-Consistency 在数学推理任务上通常能提升 5-15% 的准确率,代价是 N 倍的 API 调用成本和延迟。num_samples=5 是一个常用的平衡点,再增加到 10 次,收益已经明显递减。
Tree of Thoughts:主动探索多条路径
ToT 的理论动机:允许回溯的树状搜索
Self-Consistency 改进了推理的结果质量(通过多次采样投票),但每条推理链本身仍然是线性的、不可回溯的。Tree of Thoughts 解决的是另一个层次的问题:推理过程本身就需要探索和回溯。
把推理过程类比为路径搜索:
- 标准 CoT 是深度优先、不允许回溯的单路径搜索——一旦选了一个方向就走到底
- Self-Consistency 是多条独立的单路径并行搜索,最后投票
- ToT 是真正的树状搜索——在每个决策点分叉,评估各分支的前景,优先探索有希望的分支,遇到死路可以回溯
ToT 适合的场景是那些"中途需要做出关键决策,而且早期决策错误会导致后续全部白费"的问题。比如写作规划(选错了大纲方向,写了一半发现不对,需要推翻重来)、代码调试(选错了假设的原因,debug 了半天走了弯路)、逻辑谜题(某步推断错误,后续全部崩溃)。
ToT 把问题分解为"思维步骤",每一步可以产生多个候选想法,再对这些想法进行评估(打分),选出最有前途的继续深入,剪掉明显错误的分支(类似棋类游戏中剪枝的策略,不值得探索的路径直接舍弃)。整个过程形成一棵树。
简化版 ToT 实现
完整的 ToT 需要实现树的数据结构和搜索算法(BFS,广度优先搜索,按层逐层展开所有节点;MCTS,蒙特卡罗树搜索,用随机模拟评估各路径价值再选优)。以下是一个简化但可运行的版本,演示核心思想(验证目的:验证"生成-评估-剪枝"的循环机制是否正常运作,以及最优路径的回溯是否正确):
import os
from dataclasses import dataclass, field
from typing import List, Optional
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
# 两个模型:一个生成想法,一个评估想法
# 实际场景中可以用同一个模型,也可以用强弱两个模型
generator_llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
temperature=0.8, # 生成时温度高,鼓励多样性
)
evaluator_llm = ChatOpenAI(
model="deepseek-chat",
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com",
temperature=0, # 评估时温度低,要求一致稳定
)
@dataclass
class ThoughtNode:
content: str # 这个思维节点的内容
score: float = 0.0 # 评估得分(0-10)
depth: int = 0 # 在树中的深度
children: List["ThoughtNode"] = field(default_factory=list)
parent: Optional["ThoughtNode"] = None
# 生成候选思路的 Prompt
generate_prompt = ChatPromptTemplate.from_messages([
("system", """你是一个严谨的逻辑推理助手。
给定一个问题和当前的推理状态,生成 {n_thoughts} 个不同的下一步思路。
每个思路独立一行,用"思路N:"开头。思路要具体可执行,不要泛泛而谈。"""),
("human", """问题:{problem}
当前推理状态:
{current_state}
请生成 {n_thoughts} 个不同的下一步思路:"""),
])
# 评估思路质量的 Prompt
evaluate_prompt = ChatPromptTemplate.from_messages([
("system", """你是一个严格的推理质量评估者。
评估给定思路是否是解决问题的有效步骤。
只输出一个 0-10 的整数分数(10 分最好),不要其他内容。"""),
("human", """问题:{problem}
当前思路:{thought}
评分(0-10):"""),
])
def generate_thoughts(problem: str, current_state: str, n_thoughts: int = 3) -> List[str]:
"""生成多个候选思路"""
messages = generate_prompt.format_messages(
problem=problem,
current_state=current_state,
n_thoughts=n_thoughts,
)
response = generator_llm.invoke(messages).content
# 解析"思路N:"格式
thoughts = []
for line in response.split("\n"):
line = line.strip()
if line and ("思路" in line or line[0].isdigit()):
# 去掉"思路N:"前缀
thought = line.split(":", 1)[-1].strip() if ":" in line else line
if thought:
thoughts.append(thought)
return thoughts[:n_thoughts]
def evaluate_thought(problem: str, thought: str) -> float:
"""让 LLM 对一个思路打分"""
messages = evaluate_prompt.format_messages(
problem=problem,
thought=thought,
)
response = evaluator_llm.invoke(messages).content.strip()
try:
score = float(re.search(r"\d+", response).group())
return min(10.0, max(0.0, score))
except (AttributeError, ValueError):
return 5.0 # 解析失败时给中间分
def tot_solve(problem: str, max_depth: int = 3, n_thoughts: int = 3, beam_width: int = 2) -> str:
"""
Tree of Thoughts 求解
beam_width:每层保留的最优节点数(Beam Search)
max_depth:最大探索深度
"""
import re
# 初始状态:空
root = ThoughtNode(content="开始分析", depth=0)
current_level = [root]
for depth in range(1, max_depth + 1):
next_level_candidates = []
for node in current_level:
# 从当前节点生成子思路
current_state = f"已有推理:{node.content}" if node.content != "开始分析" else "尚未开始"
thoughts = generate_thoughts(problem, current_state, n_thoughts)
for thought_text in thoughts:
score = evaluate_thought(problem, thought_text)
child = ThoughtNode(
content=thought_text,
score=score,
depth=depth,
parent=node,
)
node.children.append(child)
next_level_candidates.append(child)
if not next_level_candidates:
break
# Beam Search:只保留得分最高的 beam_width 个节点继续探索
next_level_candidates.sort(key=lambda x: x.score, reverse=True)
current_level = next_level_candidates[:beam_width]
print(f"第 {depth} 层,保留 {len(current_level)} 个节点:")
for node in current_level:
print(f" [分数 {node.score:.1f}] {node.content[:60]}...")
# 找到得分最高的叶节点,回溯整条推理链
best_node = max(current_level, key=lambda x: x.score)
# 回溯路径
path = []
node = best_node
while node is not None and node.content != "开始分析":
path.append(node.content)
node = node.parent
path.reverse()
return "\n".join([f"步骤 {i+1}:{step}" for i, step in enumerate(path)])
# 测试
problem = "一个公司有 100 名员工,其中 60% 会编程,40% 会设计,25% 两者都会。只会编程不会设计的有多少人?"
print(f"问题:{problem}\n")
print("ToT 推理过程:")
solution = tot_solve(problem, max_depth=2, n_thoughts=3, beam_width=2)
print(f"\n最优推理路径:\n{solution}")
CoT vs Self-Consistency vs ToT 对比
| 方法 | 推理方式 | 适用场景 | API 调用次数 | 延迟 |
|---|---|---|---|---|
| 标准 CoT | 单条线性推理 | 中等复杂度推理 | 1 | 低 |
| Self-Consistency | 多次独立采样投票 | 数学题、有唯一正确答案的推理 | N(通常 5-10) | 高(但可并发) |
| ToT | 树状探索 + 剪枝 | 开放性问题、需要回溯的复杂规划 | N×M(深度×宽度) | 很高 |
计算代价与适用场景的权衡
Self-Consistency 和 ToT 都属于**推理时扩展(Inference-Time Scaling)**技术:通过在模型推理阶段投入更多计算资源来提升效果,而不是训练更大的模型。这与训练时扩展(更多数据、更大模型)是两个不同的维度。
理解计算代价有助于做出合理的选择:
Self-Consistency 的代价是线性的:5 次采样就是 5 倍的 API 调用成本。但各次采样是独立的,可以并发发出,延迟不会线性增加——5 个并发请求的延迟接近单次请求的延迟。在成本和延迟之间,它以成本换来了更高的准确率。
ToT 的代价是指数级的:深度 3、每层分叉 3 个、保留 2 条路径,粗估需要 3×3×2=18 次左右的 LLM 调用(生成 + 评估)。而且这些调用大多有依赖关系(下一层依赖上一层的结果),并发度有限,延迟会显著增加。
用标准 CoT:任务需要推理,但不是高风险场景,单次结果可接受,对延迟和成本敏感。
用 Self-Consistency:
- 数学计算、逻辑推理,答案有唯一正解
- 应用错误代价高(医疗、法律、金融决策)
- 可以并发发出多个请求,延迟不会增加太多
用 ToT:
- 问题需要"尝试-回溯"的探索,比如迷宫求解、代码调试
- 中间步骤的评估可以大幅剪枝,避免无效探索
- 延迟和成本不是主要约束
实践中,Self-Consistency 的性价比远高于 ToT。ToT 实现复杂,调用次数多,目前主要是研究方向,生产环境落地案例不多。对于大多数需要提升推理准确率的场景,Self-Consistency 是更务实的选择。
原则是:不要盲目用最复杂的技术。从 Zero-shot 开始,加 CoT,还不够再考虑 Self-Consistency;只有真正需要多步骤回溯探索时才考虑 ToT。
下一篇讨论角色扮演与人设设计——如何让 Agent 在不同场景下保持一致的行为风格,以及如何防止角色崩坏。