让 Agent 会思考:规划与推理范式
作者:toy 大多数人第一次看到 ChatGPT 流式输出时,误以为模型在"思考"。实际上它在做一件更机械的事:每次预测下一个 token 的概率分布,然后采样。这个过程里没有回溯,没有规划,没有对全局的感知。一个字一个字往前走,无法停下来说"等等,我刚才的假设可能是错的"。
作者:toy
大多数人第一次看到 ChatGPT 流式输出时,误以为模型在"思考"。实际上它在做一件更机械的事:每次预测下一个 token 的概率分布,然后采样。这个过程里没有回溯,没有规划,没有对全局的感知。一个字一个字往前走,无法停下来说"等等,我刚才的假设可能是错的"。
这就是为什么推理能力的提升,不是靠让模型"变聪明",而是靠重新设计它在生成过程中能做什么。Chain-of-Thought、ReAct、Plan-and-Execute、Tree of Thoughts,这些范式的本质,是在自回归生成的约束下,给模型构造出不同形状的思考空间。
理解这些范式,不是为了收集术语,而是为了在构建 Agent 系统时,知道在什么情况下选哪种形状。
零、为什么这些范式都在解决同一个问题
在深入具体框架之前,需要建立一个统一的认知框架。
LLM 的自回归生成机制决定了它天然是一个"一次性决策"机器:给定上下文,输出下一个 token,然后把这个 token 加入上下文,继续输出下一个。整个过程没有"暂停",没有"重新考虑",没有"从另一个角度试试"。每一步都在对当前上下文做一次前向传播,拿到概率分布,采样一个 token,继续往前。
问题在于,人类解决困难问题的方式不是这样的。我们会在草稿纸上试几条路,发现走不通就划掉;我们会在脑海里同时考虑两个假设,等待新信息来决定取舍;我们会在完成初稿后回头检查,发现前提错误就全部推倒重来。探索、评估、回溯这些能力,在 LLM 的默认生成模式里都不存在。
所有的推理范式,本质上都是在自回归生成的框架内,通过工程手段"模拟"这些能力:
- Chain-of-Thought 让模型把思考过程显式化为 token,相当于"在草稿纸上写步骤"
- ReAct 让模型在思考过程中可以调用工具,相当于"查阅资料再继续推理"
- Plan-and-Execute 把规划和执行分开,相当于"先想清楚再动手"
- Tree of Thoughts 在同一问题上同时探索多条路径,相当于"同时在几张草稿纸上试"
- Self-Consistency 多次采样取众数,相当于"问多个人同一个问题取多数意见"
- Reflexion 把失败经验写成文字再试一次,相当于"复盘后重新来过"
理解了这个共同出发点,选择范式时就不会纠结于"哪个更先进",而是问"哪种模拟方式和我要解决的问题结构最匹配"。
一、在回答之前先想:Chain-of-Thought 的原理
自回归生成与中间步骤的关系
LLM 每次只生成一个 token,这个 token 基于之前所有 token 的上下文。模型生成的内容,本身就成为下一步的输入。Chain-of-Thought(CoT)利用的正是这个性质:通过让模型先输出推理步骤,把"思考过程"物化为 token,再用这些 token 作为上下文来辅助最终答案的生成。
从信息论角度看,中间步骤的存在让模型不需要在单次前向传播里完成所有推理压缩。一个数学题如果要求直接输出答案,模型需要在极其有限的计算中完成所有推导;如果允许写出步骤,每一步都可以作为下一步的依据,错误在步骤间可以被"校正"(虽然这个校正是隐式的,通过条件概率实现的)。
这也解释了为什么 CoT 对需要多步推理的任务效果显著,对简单任务反而可能有害。一个只需要查表或记忆的问题,强行插入推理链条会引入额外的错误概率。生成越多 token,累积出错的机会越多。
Zero-shot CoT 与 Few-shot CoT
Wei et al. 2022 的 Few-shot CoT 是通过在 prompt 里给出带推理步骤的示例,让模型模仿这种格式。它的问题是需要为每个任务类型手工设计示例,成本高且难以泛化。
Kojima et al. 的 Zero-shot CoT 发现一个简单事实:只需要在问题末尾加上 "Let's think step by step",模型就会自发地输出推理步骤。这句话起作用的原因并不神秘,训练数据里充满了"先分析再得出结论"的文本模式,这句话激活了这类模式的先验。
两者的实际差异体现在:Few-shot CoT 在任务边界清晰时更精准,因为示例明确了推理的形式;Zero-shot CoT 更灵活,但推理格式不可控,对下游解析不友好。在需要输出结构化内容的 Agent 系统里,Few-shot CoT 通常更可靠。
CoT 的边界
CoT 不是银弹。以下几类情况它表现不好:
任务本身不需要推理链。如果问题是"法国的首都是哪里",插入推理步骤只会增加出错机会。强制加入推理步骤的模型会在这类问题上展示"巴黎是法国最大的城市,历史上曾多次担任首都……"这样的废话,然后才给出答案,整体反而更容易出错。
推理链过长时会漂移。中间步骤累积后,早期的错误假设会在后续步骤里不断强化。模型缺乏显式的回溯机制,无法发现第三步依赖的第一步前提是错误的。在超过 10 步的推理链里,这种漂移效应变得非常明显,模型会在一个错误的世界观里越走越深,且内部逻辑自洽。
推理与事实还会混淆。CoT 生成的"推理"并不保证是正确的逻辑,模型有时会生成听起来合理但实际上错误的推理链,并据此得出错误结论,且语气依然肯定。ReAct 和 Reflexion 框架各自用不同方式处理这个问题。
语言风格也会影响推理质量,这是一个不太被讨论的现象。同一个逻辑问题,用不同语言或不同措辞描述,CoT 的表现会有显著差异。原因在于模型的推理能力本质上是对训练数据分布的拟合,英文数学题的推理步骤比其他语言的变体在训练数据里丰富得多,英文推理的稳定性通常优于中文,在精确推理任务上需要考虑这个因素。
二、思考 + 行动:ReAct 框架
Reasoning + Acting 的交替循环
ReAct 框架(Yao et al. 2022)的核心思路是:把推理(Reasoning)和行动(Acting)交织在一起,让模型在思考的过程中可以调用工具,工具的返回结果再作为新的上下文继续推理。这形成了一个 Thought → Action → Observation 的循环。
与纯 CoT 相比,ReAct 解决了一个根本问题:模型的知识是静态的,训练截止后的信息它无法获取,复杂计算它无法可靠完成。通过在推理链里插入工具调用,模型获得了与外部世界交互的能力。
三元组的实现非常直接:
- Thought:模型输出对当前情况的分析,决定下一步该做什么
- Action:模型输出一个格式化的工具调用(工具名 + 参数)
- Observation:工具执行结果被注入对话上下文
这个循环持续到模型判断任务完成,输出最终答案。
工具调用打断推理链的处理
ReAct 的工程实现有一个细节:推理链被 Action/Observation 打断后,模型需要能够"拾起"之前的推理线索。这在实现上通过完整保留对话历史来处理,每轮 Thought/Action/Observation 都追加到上下文,下一轮 Thought 的生成基于所有历史。
代价是上下文的持续增长。一个复杂任务经过十几轮循环后,上下文可能已经几千 token,其中大量是中间过程的 Observation(工具返回的原始内容)。Observation Masking(压缩历史 Observation)是常见的工程优化手段。
用 LangChain 实现一个 ReAct Agent
# ReAct Agent 核心实现示例(LangChain 框架)
from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain import hub
from langchain_community.tools.tavily_search import TavilySearchResults
# 工具定义:给 Agent 提供搜索能力
tools = [TavilySearchResults(max_results=3)]
# 从 hub 获取 ReAct prompt 模板
# 模板内包含 Thought/Action/Observation 的格式说明
react_prompt = hub.pull("hwchase17/react")
# 创建底层 LLM
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# 组装 ReAct Agent
# create_react_agent 负责将 tools 的描述注入 prompt
agent = create_react_agent(llm, tools, react_prompt)
# AgentExecutor 负责运行循环:调用模型 → 解析 Action → 执行工具 → 注入 Observation
executor = AgentExecutor(
agent=agent,
tools=tools,
verbose=True, # 打印每轮 Thought/Action/Observation
max_iterations=10, # 防止无限循环
handle_parsing_errors=True # 输出格式错误时自动重试
)
# 运行
result = executor.invoke({
"input": "Tree of Thoughts 论文的主要作者是谁?他们现在在哪里工作?"
})
print(result["output"])
# 典型输出过程(verbose=True 时可见):
# Thought: 我需要搜索 Tree of Thoughts 论文的信息
# Action: tavily_search_results_json
# Action Input: {"query": "Tree of Thoughts paper authors NeurIPS 2023"}
# Observation: [{"content": "Shunyu Yao, Dian Yu, Jeffrey Zhao..."}]
# Thought: 找到了作者,现在需要查询他们的当前工作单位
# Action: tavily_search_results_json
# Action Input: {"query": "Shunyu Yao current affiliation 2024"}
# Observation: [...]
# Final Answer: ...
ReAct 的适用场景是路径不确定的探索性任务,每一步的行动取决于上一步的结果。它不适合步骤已知且可以预先排列的工作流,那种场景用 Plan-and-Execute 更合适。
在实际部署中,ReAct Agent 最容易遇到的工程问题是工具输出的质量控制。Observation 的内容是原样注入上下文的,如果搜索工具返回了一篇 20000 字的网页内容,整个上下文很快就会饱和。标准做法是在工具层做截断和摘要:搜索结果只保留 top-3 摘要段落,网页内容先提取正文再截断到 1000 字以内。这层预处理比调优 Agent 的 System Prompt 对实际性能的影响更大。
三、拆任务再执行:Plan-and-Execute 模式
两阶段架构的设计逻辑
ReAct 的问题在于它是"边走边想"的:每一步 Action 都需要一次完整的 LLM 调用,模型在每一步都既要决定下一步做什么,又要做这一步该做的事。对于长任务,这导致两个成本:token 消耗随轮数线性增长,且每步都在用一个大模型做可以用小模型完成的执行工作。
Plan-and-Execute 模式把规划和执行分开。一个 Planner(通常是能力强的大模型)一次性将目标分解为有序的步骤列表;一个 Executor(可以是小模型,也可以是 ReAct 子 Agent)逐步执行每个步骤。Planner 只在最开始和需要 Replan 时被调用,Executor 的每步调用可以用更便宜的模型。
这个模式的另一个优势是全局视图。ReAct 在第三步时,它的"计划"只是基于前两步 Observation 的隐式推断。Plan-and-Execute 的 Executor 在执行第三步时,可以看到完整的五步计划,知道自己当前步骤的上下游依赖关系。
与 ReAct 的核心区别
| 维度 | ReAct | Plan-and-Execute |
|---|---|---|
| 规划时机 | 每步隐式规划 | 一次性显式规划 |
| LLM 调用次数 | 每步一次(大模型) | 计划时一次大模型,执行时用小模型 |
| 适应动态变化 | 强(每步可调整) | 弱(需要显式 Replan) |
| 全局视图 | 无 | 有 |
| 适合任务 | 路径不确定的探索 | 多步骤结构化工作流 |
这两种模式并不互斥。在 Plan-and-Execute 的架构里,每个 Executor 节点本身可以是一个 ReAct Agent,负责在执行当前步骤时探索局部路径。这种嵌套结构在复杂工作流中很常见:外层 Planner 决定"要做哪五件事",内层 ReAct Agent 负责"怎么把第三件事做完"。
LLMCompiler(ICML 2024,arXiv:2312.04511)在 Plan-and-Execute 基础上引入了 DAG 并行调度:Planner 输出的不是线性列表,而是带依赖关系的 DAG,没有依赖关系的步骤可以并行执行。这一优化实现了 3.6 倍的速度提升。它的核心贡献是让工具调用可以并行化:如果某个任务需要同时查询三个数据源,传统 ReAct 需要串行执行三次 Action,LLMCompiler 可以把这三个调用同时发出,大幅缩短总延迟。
Replan 的触发条件
计划不是一成不变的。当执行步骤的结果与预期严重偏离时,需要触发 Replan:
- Executor 在某步骤遇到预期之外的错误,且错误影响后续步骤的前提
- Observation 揭示了新信息,使原有计划中的某个步骤变得不再必要或需要分拆
- 任务目标在执行中发生了细化(用户追加了约束)
Replan 的代价是调用大模型,因此需要明确的触发条件,不能每步都询问"是否需要重新规划"。
LangGraph 实现
# Plan-and-Execute with LangGraph(三节点:planner → executor → replanner)
from typing import List, Tuple, Union, Annotated
from pydantic import BaseModel
from langchain_openai import ChatOpenAI
from langgraph.graph import StateGraph, END
import operator
# --- Schema ---
class Plan(BaseModel):
steps: List[str]
class State(BaseModel):
input: str
plan: List[str] = []
past_steps: Annotated[List[Tuple], operator.add] = []
response: str = ""
# --- LLM 分层:大模型规划,小模型执行 ---
planner_llm = ChatOpenAI(model="gpt-4o", temperature=0)
executor_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
def plan_step(state: State) -> dict:
"""Planner:将高层目标分解为有序步骤列表"""
result = planner_llm.with_structured_output(Plan).invoke(
f"请将以下任务分解为具体、可执行的步骤:{state.input}"
)
return {"plan": result.steps}
def execute_step(state: State) -> dict:
"""Executor:执行 plan 中的第一个未完成步骤"""
task = state.plan[0]
# 实际场景中 executor_llm 会绑定工具(search, code_exec 等)
result = executor_llm.invoke(
f"完成以下子任务,简洁输出结果:{task}\n"
f"任务背景:{state.input}"
)
return {"past_steps": [(task, result.content)]}
def replan_step(state: State) -> dict:
"""Replanner:根据已完成步骤决定继续还是返回最终答案"""
remaining = state.plan[1:] # 移除已执行的第一步
if not remaining:
# 所有步骤已执行,生成最终答案
return {"response": state.past_steps[-1][1]}
# 根据执行结果动态调整剩余计划
updated = planner_llm.with_structured_output(Plan).invoke(
f"目标:{state.input}\n"
f"已完成步骤及结果:{state.past_steps}\n"
f"原剩余计划:{remaining}\n"
f"请根据执行结果调整剩余计划(无需调整则原样返回):"
)
return {"plan": updated.steps}
def should_end(state: State) -> str:
"""条件路由:有 response 则结束,否则继续执行"""
return "end" if state.response else "continue"
# --- 构建图 ---
graph = StateGraph(State)
graph.add_node("planner", plan_step)
graph.add_node("executor", execute_step)
graph.add_node("replanner", replan_step)
graph.set_entry_point("planner")
graph.add_edge("planner", "executor")
graph.add_edge("executor", "replanner")
graph.add_conditional_edges(
"replanner",
should_end,
{"end": END, "continue": "executor"}
)
app = graph.compile()
# --- 运行 ---
result = app.invoke({"input": "分析 Tree of Thoughts 论文并总结三个核心技术贡献"})
print(result["response"])
四、思维树:ToT 的多路径搜索
从链到树的跃变
CoT 是一条线,一次采样,一条推理路径,贪婪地走到底。这在路径搜索空间大的问题上是致命缺陷:一旦早期选错了方向,后续无论多么"合理"的推理都是在错误的基础上建造。
Tree of Thoughts(Yao et al., NeurIPS 2023,arXiv:2305.10601)把这个线性过程重新建模为树形搜索。每个节点是一个"思维"(thought),即一段自然语言形式的中间推理步骤。从一个节点出发,可以生成多个候选下一步,形成分支。每条路径被独立评估,搜索算法(BFS 或 DFS)决定在哪些分支上继续探索。
这个框架让 Agent 可以系统性地探索问题空间,在发现某条路径无望时及时剪枝,而不是只能沿着一条路走到头。
Game of 24:4% vs 74%
ToT 在论文中用 Game of 24 任务做了基准测试,用四个数字和加减乘除凑出 24。这个任务需要系统性地枚举中间计算结果,贪婪的单路径生成很容易早早走进死胡同。
结果极为悬殊:GPT-4 + CoT(标准链式推理)成功率仅 4%,GPT-4 + ToT(BFS beam=5)成功率达 74%,提升约 18.5 倍。
ToT 为什么在这里有效?Game of 24 的搜索空间是可枚举的,四个数字的排列组合有限,每步中间结果可以被精确验证(计算是否合法)。这让评估函数(Value Function)可以给出精准的 sure/maybe/impossible 分类,剪枝效率极高。
算法伪代码:BFS 与 DFS 两种实现路径
ToT 的核心循环是"生成候选 → 评估 → 选择继续探索的节点"。BFS 和 DFS 在这个框架里只是节点选择策略不同,其余生成-评估结构完全一致。
# ToT 核心逻辑伪代码(Python 风格)
from typing import List, Tuple, Optional
from enum import Enum
class NodeValue(Enum):
SURE = 2 # 确定有希望,优先扩展
MAYBE = 1 # 不确定,保留
IMPOSSIBLE = 0 # 剪枝,不再扩展
# ---- BFS 变体 ----
def tot_bfs(
task: str,
generate_fn, # (state: str, k: int) -> List[str]
evaluate_fn, # (state: str) -> NodeValue
is_terminal_fn, # (state: str) -> bool
beam_width: int = 5,
max_depth: int = 3,
) -> List[str]:
"""BFS:每层保留 beam_width 个最优节点,广度优先。"""
frontier = [task]
for depth in range(max_depth):
candidates: List[Tuple[str, NodeValue]] = []
for state in frontier:
for thought in generate_fn(state, k=3):
new_state = state + "\n" + thought
if is_terminal_fn(new_state):
return [new_state]
value = evaluate_fn(new_state)
if value != NodeValue.IMPOSSIBLE:
candidates.append((new_state, value))
candidates.sort(key=lambda x: x[1].value, reverse=True)
frontier = [s for s, _ in candidates[:beam_width]]
if not frontier:
break
return frontier
# ---- DFS 变体 ----
def tot_dfs(
state: str,
generate_fn,
evaluate_fn,
is_terminal_fn,
max_depth: int = 10,
depth: int = 0,
) -> Optional[str]:
"""DFS:深度优先,找到第一个可接受解即返回。"""
if depth >= max_depth:
return None
if is_terminal_fn(state):
return state
candidates = generate_fn(state, k=5)
evaluated = sorted(
[(c, evaluate_fn(state + "\n" + c)) for c in candidates],
key=lambda x: x[1].value, reverse=True,
)
for candidate, val in evaluated:
if val == NodeValue.IMPOSSIBLE:
continue
result = tot_dfs(
state + "\n" + candidate,
generate_fn, evaluate_fn, is_terminal_fn,
max_depth, depth + 1,
)
if result is not None:
return result
return None # 触发上层回溯
BFS 在每层保留多个候选,保证不遗漏全局最优解,但内存和调用次数随 beam_width 线性增长。DFS 找到第一个可接受解就退出,适合"任何一个满足约束的解都行"的场景。Game of 24 需要精确解选 BFS,创意写作满足约束即可选 DFS。
三个任务上的真实性能数字
论文在三个任务上做了系统测评,数字差异揭示了 ToT 的适用边界:
Game of 24(4 个数字凑 24):GPT-4 + IO 7%,GPT-4 + CoT 4%(推理链锁定错误路径反而更差),GPT-4 + ToT(BFS b=5)74%,提升 18.5 倍。验证函数精确,剪枝效率高,提升最显著。
Creative Writing(4 个随机单词写连贯段落):由 GPT-4 主观打分 1-10。CoT 5.92,ToT(DFS)7.56,提升约 22%。提升远小于 Game of 24,主观评估函数本身不稳定,剪枝精度低,边际收益有限。
Mini Crosswords(5×5 填字):CoT 单词准确率 16%,游戏完成率 0%。ToT 单词准确率 60%,游戏完成率 20%(100 题中 20 题完整解出)。介于两者之间,可以局部验证,但搜索空间更大。
三组数据的规律:评估函数越精确、问题越可枚举,ToT 提升越显著。从 Creative Writing 的 22% 和 Game of 24 的 18.5 倍差距里,可以直接读出"是否值得用 ToT"的判断依据。如果你的评估函数本质上是一个 LLM 的主观评分,ToT 的剪枝优势会大打折扣。
BFS 参数与评估策略
princeton-nlp 的官方实现暴露了三个关键参数,对应生成-评估-筛选三阶段:
# Tree of Thoughts BFS 核心参数配置
import argparse
from tot.tasks import Game24Task
from tot.methods.bfs import solve
args = argparse.Namespace(
backend='gpt-4',
temperature=0.7,
task='game24',
naive_run=False,
prompt_sample='standard', # 'standard' 或 'cot'
method_generate='propose', # 'propose'(链式生成多候选)或 'sample'(独立采样)
method_evaluate='value', # 'value'(独立打分)或 'vote'(集体投票)
method_select='greedy', # 'greedy' 或 'sample'
n_generate_sample=1, # 每个状态生成几个候选思维
n_evaluate_sample=3, # 每个候选独立评估几次(取多数票)
n_select_sample=5, # beam 宽度:每步保留几条路径
)
task = Game24Task()
ys, infos = solve(args, task, 900) # 对第 900 条题目运行 ToT-BFS
print('最终答案候选:', ys)
# 评估分类逻辑:
# sure → 确定能到达 24,优先扩展
# maybe → 不确定,保留在 beam 中
# impossible → 剪枝,不再扩展此分支
method_evaluate 有两种策略:value 模式是对每个候选独立评分(适合 Game of 24 这种可精确验证的任务);vote 模式是让多个评估者对所有候选集体投票(适合创意写作等主观任务)。
计算成本分析
ToT 的代价是 LLM 调用次数的大幅增加。对每个节点,需要调用模型 n_generate_sample 次生成候选,再调用 n_evaluate_sample 次评估,再从 n_select_sample 个 beam 节点里继续扩展。Game of 24 一道题的 ToT 求解可能触发几十到上百次 LLM 调用,而 CoT 只需要一次。
ToT 的适用场景有严格条件:
- 问题有可枚举的搜索空间(数学、代码规划、结构化决策)
- 评估函数的质量足够高,否则 beam 里保留的都是"看起来合理"的错误路径
- 任务对正确率的要求高于对速度和成本的要求
在需要流式生成、实时响应的场景里,ToT 的多轮 LLM 调用延迟是不可接受的。创意写作的质量差距(ToT 7.56 vs CoT 6.93)远不如 Game of 24 显著,性价比低。
五、自我反思与一致性检验
Self-Consistency:并行采样 + 多数投票
Wang et al. 2022(arXiv:2203.11171)提出了一个简洁的思路:与其依赖单次 CoT 生成,不如对同一问题多次采样,得到多条不同的推理路径,取答案中出现频率最高的作为最终结果。
Self-Consistency 在 GSM8K 数学基准上为 CoT 带来 +17.9% 的绝对准确率提升,在 SVAMP 上 +11.0%,AQuA 上 +12.2%。这个改进不需要任何额外的训练或监督信号,只需要在推理时多次采样即可。
它的有效性来自一个假设:如果推理路径是随机的,那么正确的推理路径会在采样集合里占多数(因为通向正确答案的路径虽然形式不同,答案是收敛的),而错误路径会分散在不同错误答案上。多数投票自然过滤了随机错误。
Self-Consistency 适合答案有确定性的推理任务,比如数学、逻辑、事实性问答。它对开放式生成任务效果有限,因为"投票"本身需要答案空间是离散可比的。
Reflexion:用语言代替梯度
Shinn et al.(NeurIPS 2023,arXiv:2303.11366)的 Reflexion 框架解决的是另一类问题:需要与环境多次交互的 Agent 任务(如编程、游戏、问答),在一次尝试失败后,如何利用失败经验改进下次尝试。
传统强化学习通过梯度更新模型参数来实现"从失败中学习"。Reflexion 用了一个更轻量的替代方案:把失败轨迹的分析结果写成自然语言反思,存入情景记忆,下轮 Actor 把这段反思作为上下文输入。本质上是把"经验"外化为文本,而非内化为参数。
三组件架构:
- Actor(Ma):基于当前任务和情景记忆(历史反思)生成行动轨迹
- Evaluator(Me):对轨迹打分,精确匹配(编程任务用测试通过率)、启发式(AlfWorld 用完成判断)、或 LLM 分类
- Self-Reflection(Msr):将稀疏奖励信号(成功/失败)转化为自然语言反思,写入情景记忆
# Reflexion 自反思循环(基于 NeurIPS 2023 论文架构)
from typing import List
class ReflexionAgent:
"""三组件:Actor / Evaluator / Self-Reflection"""
def __init__(self, actor_llm, evaluator_llm, reflection_llm, max_trials=5):
self.actor = actor_llm
self.evaluator = evaluator_llm
self.reflector = reflection_llm
self.max_trials = max_trials
# 情景记忆(跨 episode),上限约 1-3 条 reflection 文本
self.memory: List[str] = []
def run(self, task: str) -> str:
for trial in range(self.max_trials):
# Actor:基于任务 + 历史反思生成完整轨迹
trajectory = self.actor.generate(
prompt=task,
context=self.memory # 历史反思作为上下文
)
# Evaluator:判断是否成功
score, done = self.evaluator.score(trajectory)
if done:
return trajectory.final_answer
# Self-Reflection:失败后生成自然语言反思
reflection = self.reflector.generate(
trajectory=trajectory,
reward=score,
prompt="分析本次失败的根本原因,给出下轮改进的具体建议"
)
# 写入情景记忆,FIFO 淘汰最旧反思(避免 context 无限增长)
if len(self.memory) >= 3:
self.memory.pop(0)
self.memory.append(reflection)
return "max_trials 耗尽,未能完成任务"
Reflexion 的性能数据相当显著:HumanEval(Python 编程)pass@1 从 GPT-4 基线的 80% 提升至 91%,超过此前 SOTA。AlfWorld 决策任务 12 轮迭代后达到 97%(130/134 任务完成),比 baseline 提升 22 个百分点。HumanEval Rust 68% vs GPT-4 直接生成的 60%;LeetcodeHard pass@1 15% vs GPT-4 的 7.5%。
反思失效的情况
Reflexion 不总是有效的。几种典型的失效场景:
反思错误的归因。如果 Evaluator 给出的反馈不够精确(如只说"失败"而不是"第三步的边界条件处理有误"),Self-Reflection 模型生成的反思可能指向错误方向,下轮在错误方向上"改进",反而更差。
反思堆叠也可能导致混乱。情景记忆里累积了多条矛盾的反思(第一轮说要更保守,第二轮说要更激进),Actor 在综合时可能产生更差的行动策略。这是记忆容量上限(约 1-3 条)存在的原因。
环境噪声被归因为自身错误是第三种情况。如果任务环境本身有随机性,偶发的失败被 Reflector 解读为模型决策的问题,生成错误的反思,污染记忆。
Self-Consistency 和 Reflexion 代表两种不同的自改进策略:前者是推理时的并行集成,无需反馈,适合答案确定的单轮任务;后者是跨 episode 的顺序迭代,需要 Evaluator,适合需要多次尝试的 Agent 任务。
六、提示工程:让模型想得更准
System Prompt 的三层结构
一个设计良好的 System Prompt 通常包含三层,顺序很重要:
第一层是角色定义,告诉模型它是谁、任务是什么、有什么能力。这部分影响模型的整体行为基调,应该简洁、明确。"你是一个代码审查助手,专注于 Python 代码的安全性和可维护性"比"你很擅长代码"有效得多,后者没有给出任何可执行的行为指引。
第二层是约束声明,告诉模型不能做什么、边界在哪里。约束的描述需要和可能的违反情况具体对应,"不要编造信息"比"要诚实"更有效,因为后者太抽象。
第三层是输出格式,告诉模型用什么格式返回结果。这层在 Agent 系统里尤其重要,下游工具的调用依赖精确格式,任何格式偏差都会导致解析失败。
一个常见的错误是把三层混合在一起,用非结构化段落描述。这样的 System Prompt 在简单任务上看起来没问题,在边界情况下会暴露歧义。
Few-shot 示例的选择原则
Few-shot CoT 的示例选择遵循一个原则:相关性优于多样性。给三个和当前任务高度相似的示例,效果通常好于给十个覆盖广泛类别的示例。
相关性的几个具体维度:
- 任务结构相似:示例的推理步骤数和当前任务相近
- 语义领域相近:代码任务给代码示例,不要给数学推理示例
- 难度匹配:示例不应比当前任务简单太多,否则模型会"低估"任务复杂度
示例里的"错误示例"是一个双刃剑。有研究表明,加入一个错误的推理过程并标注为错误,有时能帮助模型识别并避免同类错误。但如果标注不清晰,模型可能把错误模式当成正确模式学习。
Meta-prompting:让模型生成提示
Zhang et al. 2024 提出的 Meta-prompting 是一种结构导向(structure-oriented)的方法:与其提供具体内容示例,不如提供抽象的结构框架,让模型按框架填充内容。
与 Few-shot 的内容导向相比,Meta-prompting 更关注"格式模式"而非"具体内容"。它可以用来让模型生成针对特定任务的 Few-shot 示例,再用这些示例作为另一个调用的输入,形成两阶段 pipeline。
实践中,精心设计的 meta-prompt 可以在不修改模型参数的前提下将任务性能提升 40-70%。代价是 pipeline 复杂度增加,需要两次 LLM 调用,且中间生成的 prompt 质量对最终效果影响很大。
常见踩坑
过长 Prompt 会导致注意力稀释。实验数据表明,LLM 的有效注意力是稀疏的,在超长上下文中,中间 40-60% 区域存在"Dumb Zone",这个区域的信息被相对忽略("Lost in the Middle"现象)。关键约束和示例放在 Prompt 开头或结尾,比放在中间可靠得多。一个 200K token 上下文窗口的模型,有效利用的可能只有 80-100K,越靠近中间,信息的提取效率越低。
格式约束与推理自由度之间存在权衡。要求模型严格遵循格式(如"每个步骤必须以数字开头"),会挤压推理的表达空间。在某些任务上,过于严格的格式约束反而降低推理质量,因为模型需要分出注意力去维持格式合规。在不需要精确解析输出的场景里,适当放宽格式约束是更好的选择。
System Prompt 里的角色定义会实质性地影响推理风格。"你是一个严谨的逻辑学家"倾向于生成步骤清晰但保守的推理;"你是一个富有创意的问题解决者"倾向于生成更多可能性但也更容易产生跳跃。对于精确推理任务,角色定义应该强调"先验证前提"和"一步一步推理",而不是强调创意或速度。
CoT 需要生成额外的 token,带来更高的 API 成本和更长的延迟。在高频调用的场景里,每次调用都加上 CoT 会显著增加成本。一种实用的策略是按任务复杂度分级,简单的分类和提取任务不加 CoT,需要多步推理的任务才启用,而不是对所有调用统一使用 CoT。
七、JSON 结构化输出:约束模型的输出格式
为什么需要结构化输出
Agent 系统里的 LLM 不是在和人对话,它的输出会被下游代码解析,作为工具调用的参数,或作为另一个 LLM 的输入。这要求输出格式严格可预期。自由文本哪怕逻辑正确,一旦字段名拼错、括号未闭合,整个流程就会崩溃。
历史上,工程师通过正则表达式或字符串截取来解析 LLM 的"结构化"输出,这是一场持续的噩梦。模型偶发的格式偏差会让脆弱的解析器静默失败,错误的数据悄悄流入下游。
三种实现方式对比
Function Calling(工具调用)让模型以"调用工具"的形式输出结构化数据,工具参数的 JSON Schema 约束输出格式。这是 Agent 系统里工具使用的标准模式,但历史上 JSON 合规率并不稳定。
response_format(直接结构化响应):response_format={"type": "json_schema", "json_schema": {..., "strict": true}},模型直接返回结构化 JSON,不走工具调用链路。适合不需要工具调用、只需要结构化输出的场景。
后处理正则是生成自由文本后用正则提取,这是最脆弱的方式,只在无法使用前两种方法的模型上使用。
OpenAI Structured Outputs 的工作原理
OpenAI 于 2024 年 8 月随 gpt-4o-2024-08-06 发布了 Structured Outputs 功能,采用了 Constrained Decoding 技术:在 token 生成层,限制每一步可选的 token 集合,保证生成的 token 序列在结构上符合 JSON Schema 约束。
这不是"要求模型输出 JSON",而是在解码层做了硬性约束。内部评估显示,在复杂 JSON Schema 的匹配率上,gpt-4o-2024-08-06 达到接近 100%,而此前的 gpt-4-0613 不足 40%。
使用 strict: true 时有两个要求:
- Schema 里所有字段必须出现在
required列表中 - 必须设置
additionalProperties: false(禁止模型输出 Schema 之外的字段)
两种使用形态在单次 API 调用中互斥:要么用 response_format 做直接结构化响应,要么用 tools function calling 做工具调用,不能同时使用两者。
Instructor + Pydantic:工程实践
Instructor 库(github.com/567-labs/instructor)月下载量超 300 万,是目前最流行的结构化输出集成方案。它的核心机制是拦截 LLM 客户端的调用,将 Pydantic BaseModel 自动转换为对应 provider 的格式要求,响应验证失败时自动携带错误信息重试。
# Instructor + Pydantic v2 结构化输出(官方推荐写法)
import instructor
from openai import OpenAI
from pydantic import BaseModel, Field, field_validator
from typing import List, Optional
# 1. 定义输出 Schema
# docstring 和 Field.description 会自动注入到 prompt,引导模型填充
class ResearchPaper(BaseModel):
"""从用户输入中提取研究论文信息"""
title: str = Field(description="论文标题,保持原文不翻译")
authors: List[str] = Field(description="完整作者列表")
year: int = Field(description="发表年份", ge=1900, le=2026)
venue: Optional[str] = Field(description="发表会议或期刊名称", default=None)
key_contribution: str = Field(description="核心贡献,不超过一句话")
@field_validator('year')
@classmethod
def year_must_be_reasonable(cls, v):
if v < 2000:
raise ValueError('仅接受 2000 年之后的论文')
return v
# 2. 初始化 instructor client(v1.0+ 推荐 from_provider)
client = instructor.from_provider("openai/gpt-4o-mini")
# 也可手动指定模式:
# client = instructor.patch(OpenAI(), mode=instructor.Mode.TOOLS)
# 可选模式:TOOLS(默认,走 function calling)/ JSON / MD_JSON
# 3. 调用:response_model 指定输出类型
# max_retries=3:验证失败时自动重试,每次重试会携带 Pydantic 的错误信息
paper = client.create(
response_model=ResearchPaper,
max_retries=3,
messages=[
{
"role": "user",
"content": (
"Tree of Thoughts: Deliberate Problem Solving with Large Language Models, "
"Shunyu Yao, Dian Yu, Jeffrey Zhao, Izhak Shafran, Thomas L. Griffiths, "
"Yuan Cao, Karthik Narasimhan. NeurIPS 2023. Princeton University + Google."
)
}
]
)
print(paper.model_dump())
# 输出示例:
# {
# 'title': 'Tree of Thoughts: Deliberate Problem Solving with Large Language Models',
# 'authors': ['Shunyu Yao', 'Dian Yu', 'Jeffrey Zhao', ...],
# 'year': 2023,
# 'venue': 'NeurIPS',
# 'key_contribution': '将问题求解建模为树形搜索,节点为中间思维步骤,实现 BFS/DFS 系统探索'
# }
OpenAI Structured Outputs strict 模式完整示例
直接使用 OpenAI SDK 的 response_format 加 strict: true,不走 instructor 库。适合不需要 Pydantic 验证、只需要格式保证的场景。
# OpenAI Structured Outputs strict 模式(原生 SDK)
from openai import OpenAI
import json
client = OpenAI()
# 定义 JSON Schema(所有字段必须在 required 中,additionalProperties 必须 false)
paper_schema = {
"name": "research_paper",
"strict": True,
"schema": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "论文标题,保持原文不翻译"
},
"authors": {
"type": "array",
"items": {"type": "string"},
"description": "完整作者列表"
},
"year": {
"type": "integer",
"description": "发表年份"
},
"venue": {
"type": ["string", "null"],
"description": "发表会议或期刊,不确定时填 null"
},
"key_contribution": {
"type": "string",
"description": "核心贡献,一句话"
}
},
# strict: true 要求所有字段列入 required
"required": ["title", "authors", "year", "venue", "key_contribution"],
"additionalProperties": False # 禁止输出 Schema 之外的字段
}
}
response = client.chat.completions.create(
model="gpt-4o-2024-08-06", # strict 模式要求此版本或更新
messages=[{
"role": "user",
"content": (
"Tree of Thoughts: Deliberate Problem Solving with Large Language Models. "
"Shunyu Yao et al. NeurIPS 2023."
)
}],
response_format={
"type": "json_schema",
"json_schema": paper_schema
}
)
# 解析结果:Constrained Decoding 保证 JSON 结构合法,直接 parse 不会抛异常
paper = json.loads(response.choices[0].message.content)
print(paper["title"])
print(paper["authors"])
# strict: true 下不会出现 json.JSONDecodeError
# 但仍需校验业务逻辑(如 year 是否在合理范围内)
strict 模式的关键约束:不支持 $ref(不能引用外部 Schema)、不支持 oneOf/anyOf/not、每个 object 的所有 properties 必须都在 required 里。如果 Schema 用了不支持的特性,API 会报错,不是静默降级。
常见错误类型与重试处理代码
生产系统里结构化输出的失败模式有三类,对应不同的处理策略:
# 结构化输出完整错误处理(带分类重试逻辑)
import json
import time
from openai import OpenAI, BadRequestError, RateLimitError
from pydantic import BaseModel, ValidationError
import instructor
client_raw = OpenAI()
client = instructor.from_provider("openai/gpt-4o-mini")
class MyOutput(BaseModel):
summary: str
confidence: float # 0.0 ~ 1.0
tags: list[str]
def extract_with_retry(text: str, max_attempts: int = 3) -> MyOutput:
last_error = None
for attempt in range(max_attempts):
try:
# instructor 内置重试:ValidationError 时自动携带错误信息重调
result = client.chat.completions.create(
model="gpt-4o-mini",
response_model=MyOutput,
max_retries=2, # instructor 层的自动重试
messages=[{"role": "user", "content": f"提取信息:{text}"}]
)
return result
except ValidationError as e:
# Pydantic 验证失败:instructor 已重试过,说明模型无法满足约束
# 常见原因:confidence 超出 [0,1],tags 含非字符串元素
last_error = f"ValidationError after {max_attempts} retries: {e}"
break # instructor 已处理重试,外层不再重试
except BadRequestError as e:
# Schema 本身不合法(strict 模式下常见:required 字段缺失)
# 这是代码 bug,不是模型问题,不应重试
raise RuntimeError(f"Schema 设计错误,请检查 required/additionalProperties: {e}")
except RateLimitError:
# 限速:指数退避
wait = 2 ** attempt
time.sleep(wait)
last_error = f"RateLimitError, retried after {wait}s"
except json.JSONDecodeError as e:
# 原生 SDK 非 strict 模式下可能出现:模型输出了合法文本但不是 JSON
# 使用 strict: true 或 instructor 后这类错误应消失
last_error = f"JSONDecodeError: {e}"
raise RuntimeError(f"结构化提取失败({max_attempts} 次尝试): {last_error}")
四类错误的处理原则:ValidationError 由 instructor 自动重试,外层不需要再套循环;BadRequestError 是 Schema 设计 bug,应该立即抛出不重试;RateLimitError 用指数退避;JSONDecodeError 在使用 strict 模式后应该不再出现,如果出现说明模型版本不支持 strict,需要降级到 instructor 的 JSON 模式。
错误处理与重试策略
Instructor 的自动重试机制在字段验证失败时会把 Pydantic 的报错信息(哪个字段、什么约束、收到了什么值)拼入下一次请求的 messages,让模型有机会修正自己的输出。这比"重试时发送完全一样的请求"有效得多,后者几乎不可能改变模型行为。
几个需要注意的边界情况:
Schema 约束本身有歧义时,如果字段 description 和 validator 的要求相互矛盾,模型可能在多次重试后依然无法通过验证。这时问题不在模型,在 Schema 设计。
深层嵌套的 Pydantic 模型在验证失败时,错误信息可能非常冗长,把太多噪音注入 context,反而干扰下次生成。对复杂结构,适当扁平化 Schema 可以提升可靠性。
max_retries 不是免费的,每次重试都是一次 LLM 调用,成本和延迟都会叠加。在生产系统里,max_retries=3 意味着最坏情况下的成本是正常情况的 4 倍。需要在 Schema 设计阶段就降低验证失败的概率,而不是依赖重试机制兜底。
八、范式选择的判断框架
七个范式讲完,真实工程场景里如何选择?这里给出一个决策思路,不是流程图,而是几个关键问题的组合。
问题的搜索空间是线性还是树状的?
如果问题有明确的"分叉点",在某个中间步骤存在多个可能的方向,且不同方向对最终结果有显著影响,用 ToT。如果问题是线性的,一步接一步推进,用 CoT 或 ReAct。
任务是单次回答还是多步执行?
单次推理任务(数学、逻辑、问答),用 CoT + Self-Consistency 提升可靠性。需要调用外部工具、与环境交互的任务,用 ReAct 或 Plan-and-Execute。
任务路径是否可以预先规划?
如果任务目标明确、步骤之间依赖关系清晰,Plan-and-Execute 优于 ReAct,因为可以用大模型规划、小模型执行,降低总成本。如果下一步行动强依赖上一步的未知结果,ReAct 的逐步决策更合适。
是否需要跨多次尝试改进?
单次不可靠但多次尝试后收敛,用 Reflexion。一次尝试里答案不确定,用 Self-Consistency 多数投票。
输出是否需要被机器解析?
只要下游有任何程序处理 LLM 的输出,就必须用结构化输出。Instructor + Pydantic 是目前最省力的集成方式,strict: true 模式下的 Constrained Decoding 是对抗格式偶发失败的工程保障。
九、深层问题:推理能力的本质限制
这些范式的改进,从工程角度看是系统设计的胜利。但理解它们的上限,需要回到 LLM 的本质:模型没有独立的"思考"过程,它生成的推理步骤和它生成的故事一样,都是对训练数据模式的统计复现。
Self-Consistency 有效,是因为多条错误路径的答案分散,正确路径的答案聚集。这个假设在训练数据充足的任务类型上成立,在模型从未见过类似问题的分布外任务上不成立。
Reflexion 有效,是因为模型有能力分析自己的输出并生成有效的改进建议。这本身依赖于模型在推理和自我分析任务上的预训练能力,对能力边界之外的任务,它生成的"反思"可能只是措辞不同的重复错误。
ToT 有效,是因为评估函数能够准确判断哪些路径值得继续探索。评估函数本身也是 LLM,它的判断质量是整个框架的瓶颈。如果评估函数可靠,ToT 的搜索效率很高;如果评估函数和 Actor 犯同类型的错误(这在分布外任务上很常见),多路径搜索只是在错误空间里做了更彻底的遍历。
这些框架的核心价值不是给 LLM 增添了不存在的能力,而是更有效地激活和组合了模型已有的能力。在能力边界清晰的场景里,它们是有效的工程杠杆;在把它们应用到模型能力之外的场景时,工程上的精巧设计掩盖不了底层的能力缺口。
十、可执行的行动建议
理解这些范式之后,一个具体的起点比任何总结都有价值。
如果你正在构建一个需要推理的 Agent,按这个顺序实施:
第一步,先用 CoT + Instructor 结构化输出搭出最小闭环。确保 LLM 的输出可以被下游代码可靠解析,这是一切的前提。
第二步,把 CoT 替换成 ReAct(如果任务需要工具)或 Self-Consistency(如果任务需要高可靠性答案)。测量改进幅度是否值得增加的复杂度。
第三步,如果任务是多轮迭代的(编程、调试、决策),接入 Reflexion 框架,设计一个可靠的 Evaluator。Evaluator 的质量决定整个框架的上限,在这里投入的时间比在 Actor 提示词上花的时间更有价值。
第四步,如果特定类别的任务性能瓶颈在于搜索空间的枚举(数学推理、代码规划),在这类任务上试 ToT,但要先建立清晰的成本预算。ToT 的 LLM 调用次数是 CoT 的几十倍,需要明确什么性能提升水平才值得这个代价。
不要在还没有可测量的性能瓶颈时就引入复杂框架。一个可靠的 CoT + 结构化输出,比一个设置不当的 ToT 更有实际价值。先度量,再优化。
附录:各框架关键参数速查
CoT 相关
| 变体 | 激活方式 | 适用场景 |
|---|---|---|
| Zero-shot CoT | "Let's think step by step" | 快速启用,格式不可控 |
| Few-shot CoT | Prompt 中提供带步骤示例 | 任务边界清晰,需控制格式 |
| Self-Consistency | 多次采样 + 多数投票 | 数学/逻辑,需要高可靠性 |
ReAct 相关
ReAct 的关键工程参数:
max_iterations:最大循环轮数,防止无限循环。通常设 10-15,复杂任务可到 20。handle_parsing_errors:模型输出不符合 Thought/Action 格式时的处理策略。设为 True 会尝试重新生成,但会消耗额外的 token 和时间。early_stopping_method:force(强制截断,返回当前最佳答案)或generate(让模型再生成一次总结)。在 token 预算有限时用force。
ReAct 失败的典型原因和对应处理:
工具调用格式错误:System Prompt 里加入工具调用的格式示例(Few-shot),并在 handle_parsing_errors=True 的基础上设计 fallback 逻辑。
Observation 过长导致上下文超限:对工具返回结果做截断或摘要,只保留最关键的信息。搜索工具返回的 HTML 页面通常需要预处理成纯文本才能高效利用。
循环调用同一工具:加入工具调用历史去重,检测到重复调用同一参数时强制退出循环。
Plan-and-Execute 相关
计划的表示格式直接影响执行质量。推荐用结构化 JSON 而非自然语言列表:
{
"goal": "分析 Tree of Thoughts 论文并写成技术博客",
"steps": [
{
"id": 1,
"action": "搜索并下载 arXiv:2305.10601 摘要",
"expected_output": "论文标题、作者、核心贡献的结构化摘要",
"depends_on": []
},
{
"id": 2,
"action": "提取论文中的关键性能数据(Game of 24 成功率等)",
"expected_output": "数值性能对比表格",
"depends_on": [1]
},
{
"id": 3,
"action": "分析 BFS 参数配置对性能的影响",
"expected_output": "n_generate/evaluate/select_sample 参数的敏感性分析",
"depends_on": [1, 2]
},
{
"id": 4,
"action": "撰写 1500 字技术博客初稿",
"expected_output": "Markdown 格式博客文章",
"depends_on": [1, 2, 3]
}
]
}
depends_on 字段让 LLMCompiler 等变体可以识别并行机会,步骤 2 和步骤 3 如果没有依赖关系,可以并发执行,降低总耗时。
ToT 相关
Game of 24 任务中,n_select_sample=5(beam width=5)在成本和性能之间取得了较好的平衡。增大 beam width 会提高成功率,但 LLM 调用次数随之线性增加。实验数据:
| beam width | 成功率 | 相对 CoT 的 LLM 调用倍数 |
|---|---|---|
| 1(等同 CoT) | 4% | 1x |
| 5 | 74% | ~30x |
| 10 | ~78% | ~60x |
从 beam=5 到 beam=10 只带来 4% 的额外提升,但成本翻倍。这个边际递减曲线是设置 beam width 的重要参考。
Reflexion 相关
情景记忆的管理是 Reflexion 在工程实践中最容易出问题的地方。几个实用原则:
保持反思简洁。每条反思建议控制在 100-200 字内,聚焦"具体失败原因"和"下轮改进方向",不要把整个失败轨迹都写进记忆,否则 context 里的历史反思会挤压当前任务的可用空间。
区分战术反思和战略反思。"第 4 步的边界条件处理遗漏了 n=0 的情况"是战术反思,有即时价值;"这类递归问题需要先验证基础情况"是战略反思,可以跨任务复用。战略反思值得更长期保留。
Evaluator 的设计应该先于 Actor 的调优。Reflexion 框架里最容易被忽视的是 Evaluator 的质量。在编程任务里,单元测试通过率是一个精确、客观、可自动化的 Evaluator;在问答任务里,Evaluator 可能需要人工设计评分标准。Evaluator 越精确,Self-Reflection 模型生成的反思越有针对性,整个框架的改进效率越高。
Instructor 模式选择
Instructor 支持三种工作模式,对应不同的场景:
- TOOLS 模式(默认):走 function calling 路径,需要模型支持工具调用,兼容性最广,推荐首选
- JSON 模式:要求模型直接输出 JSON,适合不支持 function calling 的模型(如某些开源模型)
- MD_JSON 模式:输出包裹在 Markdown 代码块里的 JSON,适合某些只能输出 Markdown 格式的模型
对于生产系统,TOOLS 模式加上 OpenAI strict: true 是最可靠的组合。在需要支持多个 LLM 提供商时,Instructor 的 from_provider API 屏蔽了不同提供商 API 格式的差异,切换模型时不需要修改业务代码。
补记:推理范式与模型能力进化的关系
这些范式在 2022-2024 年诞生,有一个重要的历史背景:那时的语言模型(GPT-3.5 时代)原生推理能力弱,需要通过工程手段补偿。ToT 在 GPT-4 上 Game of 24 成功率 74%,在 GPT-3.5 上效果要差很多,因为框架依赖的 Evaluator 本身需要足够强的判断能力。
2025 年之后,随着 o1/o3/R1 等"思考型"模型的出现,情况发生了变化。这些模型在推理阶段内置了类似 CoT 的机制(extended thinking),模型本身会在生成最终答案之前进行大量内部推理。这让某些原本需要外部框架实现的推理能力(尤其是多步数学和代码推理)变成了模型的原生能力。
但这并不意味着本文介绍的框架失去价值。几个维度上,工程框架依然是模型内置推理无法替代的:
工具调用是第一个维度。模型无法自己"调用搜索引擎"或"执行代码",这需要外部的 ReAct 循环或 Plan-and-Execute 架构来协调。即使是最强的推理模型,也需要 Agent 框架来获取外部世界的信息。
多智能体协调是第二个维度。当任务需要多个专门化的 Agent 协作完成时,规划层的设计依然是工程问题,不是模型能力问题。一个代码生成 Agent 和一个代码审查 Agent 之间的协调逻辑,需要明确的架构设计。消息传递格式、状态共享机制、错误传播路径,这些都是 Harness Engineering 要解决的问题,跟模型本身的推理能力无关。
可靠性保障是第三个维度。Self-Consistency 和 Reflexion 提供的可靠性,在关键任务上依然有价值,哪怕模型本身更强。在高风险决策场景里(金融分析、医疗建议、法律文书),额外的验证层是必要的工程保险。一个 95% 准确的系统,在高频使用时每 20 次就会出一次错;通过 Self-Consistency 将准确率提升到 99%,出错频率降低 4 倍,这个差异在生产系统里非常显著。
成本控制是第四个维度。Plan-and-Execute 的大模型规划 + 小模型执行模式,在模型价格差异依然存在的情况下,是有实际经济价值的架构选择。以目前的定价,用 GPT-4o 做规划、用 GPT-4o-mini 做执行,比全程用 GPT-4o 节省约 80-90% 的成本,而规划质量不会有显著下降。规划本质上是"把目标分解为步骤",这个任务的难度远低于执行每个步骤所需的能力。
模型进化不会让推理范式过时,而是会改变每种范式的价值边界。随着模型基础能力的提升,工程层需要做的补偿性工作减少,但协调性工作,也就是让多个模型、工具、资源有机协作,反而会增加。Harness Engineering 这个概念的出现,正是因为工程的重心从"让模型想得更准",逐渐转向"让模型系统跑得更可靠"。
十一、推理范式的组合使用
单一范式在多数真实场景里都不够用。更复杂的 Agent 系统需要把多个范式组合起来,每个范式解决一个维度的问题。
ReAct + Self-Reflection 的组合
ReAct 处理"边走边查"的探索过程,但它缺乏跨 episode 的学习能力。每次任务重新开始时,它对上次犯过的错误一无所知。把 Self-Reflection(Reflexion 的一个组件)叠加到 ReAct 上,可以在不改变 ReAct 轨迹生成逻辑的前提下,添加跨轮次的错误记忆。
组合方式:把 Reflexion 的三组件(Actor/Evaluator/Self-Reflection)中的 Actor 替换为一个 ReAct Agent。ReAct Agent 在每轮内部走 Thought/Action/Observation 循环,外层的 Reflexion 框架负责跨轮次的失败分析和记忆管理。这样既保留了 ReAct 的动态工具调用能力,又添加了 Reflexion 的跨任务学习能力。
这个组合的有效场景是需要反复执行类似任务的 Agent,比如代码修复、数据提取、重复性报告生成。单次任务内,ReAct 的工具调用探索路径;跨任务,Reflexion 的情景记忆避免重蹈覆辙。
ToT + Plan-and-Execute 的适用场景
两者都是"不满足于单路径"的框架,但作用层级不同:ToT 在单个子任务的内部做多路径搜索,Plan-and-Execute 在整体任务的宏观层面做结构化规划。
把两者组合的场景是:任务整体可以被分解为有序步骤(适合 Plan-and-Execute),但其中某几个步骤的内部有搜索空间(适合 ToT)。典型例子是代码架构设计,外层 Planner 把"设计一个分布式缓存系统"分解为"定义数据模型 → 设计一致性策略 → 规划故障转移方案",其中"设计一致性策略"这个步骤本身有多条可行路径(最终一致 vs 强一致),可以在这个子步骤内部用 ToT 探索并评估多种方案。
代价是双重的 LLM 调用开销:外层 Plan-and-Execute 已经需要多次调用,内层 ToT 再叠加多路径生成,总调用次数可能是纯 CoT 的几十倍。这种组合只在以下条件同时满足时才值得:任务对质量要求极高(如关键架构决策)、某些子任务有可枚举的搜索空间、有清晰的 Evaluator 可以给子任务的各路径打分。
任务类型与推理范式的决策矩阵
根据任务特征选择范式,而不是根据"哪个更新"或"哪个更复杂":
| 任务特征 | 推荐范式 | 避免 |
|---|---|---|
| 单次推理,答案确定 | CoT + Self-Consistency | ToT(成本过高) |
| 需要外部工具,路径不确定 | ReAct | Plan-and-Execute(全局视图不需要) |
| 多步骤,路径可预规划 | Plan-and-Execute | ReAct(每步大模型调用浪费) |
| 多步骤 + 并行子任务 | LLMCompiler(DAG 调度) | 线性 Plan-and-Execute |
| 多次尝试,需跨轮改进 | Reflexion | Self-Consistency(单轮内聚合无法跨轮) |
| 搜索空间可枚举,精确验证 | ToT(BFS) | CoT(单路径易死局) |
| 创意任务,找一个可行解 | ToT(DFS)或 CoT | ToT(BFS)(成本高但收益有限) |
| 输出需要机器解析 | Structured Outputs(任何范式都要加) | 自由文本输出 |
几个选型的判断优先级:
可验证性先于搜索策略。在确定是否用 ToT 之前,先问"我的评估函数可靠吗"。一个不可靠的 Evaluator 会让 ToT 的多路径搜索变成在错误空间里的更彻底遍历,不如用便宜的 CoT + Self-Consistency。
成本是硬约束。ToT 的调用次数是 CoT 的几十倍,Reflexion 需要多轮完整交互。在没有明确性能基准的情况下,不应该为了"理论上更强"而引入成本高的框架。先用 CoT 建立基准,测量性能瓶颈在哪里,再有针对性地升级。
组合要有理由。ReAct + Reflexion 组合在重复性任务上有清晰的理由,前者处理单次探索,后者处理跨次学习。如果只是觉得"组合起来功能更全",通常意味着系统复杂度在增加而收益不明确。每加一个范式,都需要一个可以度量的性能指标来验证它的贡献。
结构化输出是所有范式的基础设施。无论选择哪种推理范式,只要输出要被程序处理,Structured Outputs 就是必选项。它不是一个"范式",而是让其他范式能够可靠集成进系统的工程保障。
作者:toy