Appearance
CoT 与 ReAct
在做 Agent 开发之前,你可能已经注意到一件事:让模型直接给答案,和让模型先把思路写出来再给答案,准确率差距很大。这就是 Chain-of-Thought 在做的事。ReAct 在这个基础上加了"行动"这一层,是现代 Agent 的核心执行模式。
这页接着 Agent 基础原理 往下,理解了这两个模式,再看 LangChain 的 Agent 实现就会清晰很多。
Chain-of-Thought:让模型"想一步再写一步"
CoT 的基本思路是:在 prompt 里让模型把推理过程也写出来,而不是直接跳到答案。
没有 CoT 的 prompt:
问题:一个班有 32 个学生,其中 3/4 参加了运动会,参加运动会的学生里有 1/3 获奖。获奖的学生有几个?
答:加了 CoT 的 prompt:
问题:一个班有 32 个学生,其中 3/4 参加了运动会,参加运动会的学生里有 1/3 获奖。获奖的学生有几个?
让我一步步想:
- 参加运动会的学生:32 × 3/4 = 24 人
- 获奖的学生:24 × 1/3 = 8 人
答:8 人在较新的模型上(如 GPT-4o、Claude 3.5),不需要每次都在 prompt 里明确写"让我一步步想",可以直接说 think step by step,或者不说,模型自己会推理。但对于早期模型或复杂问题,显式的 CoT 提示仍然有用。
CoT 的本质
CoT 不是在教模型"怎么思考",而是给模型一个输出中间步骤的机会。模型在 token 层面是自回归生成的——每个 token 依赖前面所有 token。把推理过程写出来,就是让前面的 token(推理过程)成为后面 token(答案)的 conditioning context。
这也解释了为什么 CoT 在简单问题上没什么效果:简单问题没有需要中间推理的步骤,直接给答案反而更干净。
Zero-shot CoT 和 Few-shot CoT
Zero-shot CoT 是不给示例,只在问题后加一句类似 Let's think step by step 的提示。它适合你还没有整理示例、但问题明显需要多步推理的场景。
text
问题:如果一个接口平均 120ms,串行调用 5 次大概需要多久?
请一步步推理,再给出最终答案。Few-shot CoT 会先给模型几个“问题 + 推理过程 + 答案”的示例,再让它处理新问题。它不只是让模型慢慢想,还在示范你希望它按什么粒度拆解问题。
text
示例 1:
问题:3 个请求串行执行,每个 100ms,总耗时多少?
推理:串行执行不能并发,耗时相加,3 × 100ms = 300ms。
答案:约 300ms。
现在回答:
问题:5 个请求串行执行,每个 120ms,总耗时多少?两者的差异在于“是否用示例约束推理路径”。Zero-shot CoT 便宜、灵活,但输出风格不一定稳定;Few-shot CoT 更适合固定题型,比如数学题、规则判断、代码审查步骤。代价也明显:示例会占上下文,示例质量差时还会把模型带偏。
在生产系统里,我通常不会让模型把完整推理链原样展示给用户。更稳的做法是让模型在内部完成推理,只输出必要依据和结论;如果要做审计,也应该记录结构化中间状态,而不是把长篇 Thought 当成最终答案的一部分。
什么时候用 CoT
| 场景 | 是否有效 |
|---|---|
| 多步数学推理 | 明显有效 |
| 逻辑判断(多个条件) | 有效 |
| 代码生成 | 有效(让模型先描述思路再写代码) |
| 简单问答 | 无明显效果 |
| 创意写作 | 无明显效果 |
Tree-of-Thought 简介
Tree-of-Thought(ToT)可以看成 CoT 的扩展。CoT 通常是一条推理链,模型沿着一条路径往下走;ToT 会让模型同时探索多个候选思路,再评估哪条路径更值得继续。
一个简化的 ToT 流程可能是:
- 生成 3 个解决方案草案
- 分别评估每个草案的可行性
- 选择最好的 1-2 个继续展开
- 最后合并或选择最终答案
它适合规划、搜索、复杂推理这类“第一条思路不一定对”的任务。缺点也很直接:调用次数增加,延迟和成本都会上升。普通问答、简单工具调用没必要上 ToT。
ReAct:Reason + Act 的循环
CoT 解决了"怎么思考",但思考本身不能执行操作。ReAct(Reasoning + Acting)把工具调用加进来,让模型可以在思考中途真正去查询信息、执行操作,再根据结果继续推理。
基本循环是:
Thought: 我需要知道今天的天气才能回答这个问题
Action: search_weather(city="北京")
Observation: 北京今天晴,最高气温 28°C,最低 15°C
Thought: 现在我知道了天气,可以给出建议了
Answer: 北京今天天气晴朗,建议...每一轮包含三个部分:
- Thought:模型的推理,判断下一步要做什么
- Action:调用一个工具,传入参数
- Observation:工具返回的结果
这个循环可以进行多轮,直到模型判断已经有足够信息生成最终答案。
ReAct 和纯 CoT 的区别
纯 CoT 是在模型内部推理,不能获取外部信息。ReAct 的 Thought 也是内部推理,但中间可以"暂停"去执行操作,然后把操作结果带回推理链。
这意味着 ReAct 适合需要实时信息或执行操作的任务,纯 CoT 适合模型已经有足够知识、只需要推导的任务。
在 LangChain 里的体现
LangChain 的 Agent 本质上就是 ReAct 的封装。它处理了工具定义、循环调用、结果解析这些机械性的工作:
python
from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain_core.tools import tool
from langchain import hub
import ast
import operator
@tool
def get_weather(city: str) -> str:
"""查询指定城市的今日天气"""
# 实际项目里调用天气 API
return f"{city}今天晴,气温 28°C"
ALLOWED_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
}
def safe_eval_math(expression: str) -> float:
node = ast.parse(expression, mode="eval").body
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return float(node.value)
if isinstance(node, ast.BinOp) and type(node.op) in ALLOWED_OPERATORS:
left = safe_eval_math(ast.unparse(node.left))
right = safe_eval_math(ast.unparse(node.right))
return ALLOWED_OPERATORS[type(node.op)](left, right)
raise ValueError("只支持数字和 + - * / 四种运算")
@tool
def calculate(expression: str) -> str:
"""计算简单数学表达式,只允许数字和 + - * /。"""
return str(safe_eval_math(expression))
llm = ChatOpenAI(model="gpt-4o")
tools = [get_weather, calculate]
# hub 里有预定义的 ReAct prompt 模板
prompt = hub.pull("hwchase17/react")
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools, verbose=True)
result = executor.invoke({
"input": "北京今天天气适合户外活动吗?如果气温超过 25°C,需要注意防晒。"
})verbose=True 时可以看到每一步的 Thought/Action/Observation,出了问题方便排查。
工具示例里不要用 eval() 直接执行模型生成的参数。ReAct 的 Action 往往来自用户输入和模型推断,宿主系统必须把工具能力限制在白名单里。上面这个计算工具故意只允许四则运算,就是为了让“模型决定调用什么”和“系统允许执行什么”分开。
在 LangGraph 里的体现
AgentExecutor 更像一个封装好的循环:给它模型、工具和 prompt,它自己负责跑到结束。LangGraph 的思路不一样,它把 Agent 执行拆成图节点和边,循环条件由图控制。
如果只想快速做一个 ReAct Agent,LangGraph 也有预构建版本:
python
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from langgraph.prebuilt import create_react_agent
@tool
def get_order_status(order_id: str) -> str:
"""查询订单状态。"""
return f"订单 {order_id} 已发货,预计明天到达"
model = ChatOpenAI(model="gpt-4o-mini")
agent = create_react_agent(model, tools=[get_order_status])
result = agent.invoke({
"messages": [
("user", "帮我查一下订单 ORD-001 的状态,并说明是否需要催物流")
]
})
print(result["messages"][-1].content)这段代码看起来也很短,但背后已经是一个图:模型节点决定是否调用工具,工具节点执行 Action,Observation 回到模型节点,再由模型决定继续调用还是结束。
LangGraph 比 AgentExecutor 更适合这些场景:
- 你要限制最多调用几轮工具
- 你要在人审节点暂停,让人确认后再继续
- 你要把不同工具调用分到不同分支
- 你要把 Agent 状态持久化,失败后从某个节点恢复
如果任务只是“问一句、查一个工具、返回答案”,AgentExecutor 或预构建 ReAct 足够了。真正需要 LangGraph 的时候,通常是你要控制循环,而不是只想把工具接进去。
ReAct 死循环怎么处理
ReAct 最麻烦的问题之一是模型卡在同一个 Action 上反复调用。比如搜索工具一直返回无关结果,模型没有意识到信息不足,就连续搜索 用户问题、用户问题 最新、用户问题 详细解释,直到耗尽预算。
工程上不要指望模型自己每次都知道停。可以加几层硬约束:
1. 最大轮数
这是最基础的保护。超过 max_iterations 或图里的递归上限就停止,返回“无法完成,需要补充信息”。
python
from langchain.agents import AgentExecutor
executor = AgentExecutor(
agent=agent,
tools=tools,
max_iterations=4,
early_stopping_method="generate",
verbose=True,
)2. 重复动作检测
记录最近几次工具名和参数,如果连续出现相同调用,就中断或改写提示。判断不要只看工具名,还要看参数,否则 search("A") 和 search("B") 会被误判成重复。
python
recent_actions = [
("search", '{"query":"ORD-001"}'),
("search", '{"query":"ORD-001"}'),
]
if len(set(recent_actions[-2:])) == 1:
raise RuntimeError("agent repeated the same action twice")3. 工具返回可行动的错误
工具不要只返回 not found。更好的 Observation 是:“没有查到订单,请确认订单号是否包含 3 位前缀”。模型拿到这种结果,更容易转向追问用户,而不是继续盲查。
4. 成本和超时预算
给每个任务设置 token、时间和工具调用预算。预算不是为了省一点钱,而是避免 Agent 在异常状态下占住队列,影响其他请求。
LangGraph 里这些限制可以放在状态字段和条件边里。比如每次工具调用后让 step_count += 1,如果超过阈值就跳到 finish 节点;如果最近两次 action 一样,就跳到 ask_user 节点。
常见误区
以为 ReAct 在"思考"
ReAct 的 Thought 不是真正的"内省",它是模型根据 prompt 结构生成的文本。模型并不知道它在"思考",只是按格式生成 Thought: ... 这段文本,然后生成 Action: ...。
工具设计太复杂
工具的 docstring 是模型决定"要不要用这个工具、怎么用"的主要依据。工具功能越聚焦、描述越清晰,模型选择越准确。一个工具做两件事,不如拆成两个工具。
忘记 Observation 的限制
工具返回的 Observation 会进入模型上下文。如果你的工具返回大量数据(比如搜索返回 10 篇全文),会迅速消耗上下文窗口,也会让模型抓不住关键信息。工具应该返回精炼的结果,而不是原始数据。
在 LangGraph 里,ReAct 的循环可以被更精细地控制:你可以决定在什么条件下跳出循环、多个 Agent 之间怎么传递 Thought。这部分在 LangChain 与 LangGraph 里有详细介绍。
常见面试考点
CoT 和 ReAct 的面试题通常会追问到实现细节,回答时别只说“让模型一步步想”:
- CoT 原理:CoT 通过输出中间推理 token,让后续答案受到前文推理过程影响;它对多步推理更有帮助,对简单问答可能增加噪声。
- Zero-shot vs Few-shot CoT:Zero-shot 只加一步步推理提示,成本低但不稳定;Few-shot 给出带推理链的示例,能约束题型和推理粒度,但占上下文。
- Tree-of-Thought:ToT 会探索多条候选推理路径并评估选择,适合规划和复杂搜索,代价是更多调用和更高延迟。
- ReAct 链路:典型循环是 Thought -> Action -> Observation -> Answer,适合需要外部信息或工具执行的任务。
- LangGraph 里的 ReAct:LangGraph 把模型调用、工具调用和循环条件拆成图节点,适合做人审、持久化、分支和中断控制。
- 死循环处理:用最大轮数、重复 Action 检测、工具错误提示、超时和 token 预算控制 Agent,不要把停止条件完全交给模型。