Skip to content

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 流程可能是:

  1. 生成 3 个解决方案草案
  2. 分别评估每个草案的可行性
  3. 选择最好的 1-2 个继续展开
  4. 最后合并或选择最终答案

它适合规划、搜索、复杂推理这类“第一条思路不一定对”的任务。缺点也很直接:调用次数增加,延迟和成本都会上升。普通问答、简单工具调用没必要上 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 的面试题通常会追问到实现细节,回答时别只说“让模型一步步想”:

  1. CoT 原理:CoT 通过输出中间推理 token,让后续答案受到前文推理过程影响;它对多步推理更有帮助,对简单问答可能增加噪声。
  2. Zero-shot vs Few-shot CoT:Zero-shot 只加一步步推理提示,成本低但不稳定;Few-shot 给出带推理链的示例,能约束题型和推理粒度,但占上下文。
  3. Tree-of-Thought:ToT 会探索多条候选推理路径并评估选择,适合规划和复杂搜索,代价是更多调用和更高延迟。
  4. ReAct 链路:典型循环是 Thought -> Action -> Observation -> Answer,适合需要外部信息或工具执行的任务。
  5. LangGraph 里的 ReAct:LangGraph 把模型调用、工具调用和循环条件拆成图节点,适合做人审、持久化、分支和中断控制。
  6. 死循环处理:用最大轮数、重复 Action 检测、工具错误提示、超时和 token 预算控制 Agent,不要把停止条件完全交给模型。

面向开发者的 AI 实战路线——Vibe Coding 与 AI 应用开发