Agent 推理模式:ReAct 是什么?具体怎么实现?

一、ReAct 不是"模型自己循环"

ReAct 的核心很简单:让模型在回答复杂问题时,不是一口气把答案吐出来,而是按"思考、行动、观察"的节奏推进任务。它先判断下一步该做什么,再调用工具拿结果,然后把工具结果作为新的上下文继续思考,直到能给出最终答案。

真正容易误解的地方在这里:LLM 本身不会自动循环。模型每次只输出一段内容,可能是一个 Thought,也可能是一个 tool call。循环是外面的应用代码或 Agent 框架驱动的:解析模型输出、执行工具、回填 Observation、再次调用模型。

模型负责判断下一步,代码负责把下一步跑起来。

二、为什么普通 LLM 需要"推理模式"?

LLM 的生成过程是顺序的。它根据已有上下文,一个 token 一个 token 往后预测。简单问题可以直接回答,但多步问题不一样:中间状态如果没有显式写出来,就像心算一样,很容易在某一步悄悄出错。

推理模式的价值,就是把原本藏在模型内部的中间过程显式化。前一步写出来的内容会进入上下文,成为下一步生成的依据。这样并不等于模型一定不会错,但它至少给了系统检查、修正、接入外部工具的机会。

三、从 Direct 到 CoT,再到 ReAct

最基础的方式是 Direct:用户问什么,模型直接答什么。这种方式快,成本低,但遇到复杂问题容易跳步。CoT,也就是 Chain of Thought,把推理步骤写出来,再给出答案。它能改善多步推理,但仍然局限在纯文本世界里。

ReAct 则是在 CoT 的基础上加入 Action。它不是只让模型"想",而是允许模型在推理过程中调用工具,比如搜索、数据库、计算器、代码执行、RAG 检索等。工具返回的结果再进入下一轮推理,这就形成了 Thought -> Action -> Observation 的闭环。

四、ReAct 到底解决了 CoT 的什么问题?

CoT 能让模型把推理写出来,但它拿不到实时信息,也不能执行真实动作。比如你问"某家公司现在市值是多少",纯 CoT 只能依赖训练数据或上下文,很可能已经过时。ReAct 的优势是,它能在推理过程中"出去拿数据"。

ReAct 论文把 reasoning traces 和 task-specific actions 交织起来:推理轨迹帮助模型跟踪计划、更新计划、处理异常;行动让模型连接知识库或外部环境,拿到额外信息。论文也指出,这种方式能提升可解释性,并在一些问答、事实验证和交互决策任务上减少幻觉与错误传播。

五、ReAct 的经典实现:Prompt 约束 + 代码循环

经典 ReAct 往往靠固定格式的 Prompt 来约束模型输出。模型需要按指定格式输出 Thought、Action、Action Input。系统拿到 Action 后,执行工具,再把工具返回结果写成 Observation 追加到历史里。

Prompt 可以长这样:

复制代码
你是一个 AI 助手,可以使用以下工具:
- search(query): 搜索互联网获取最新信息
- calculator(expr): 计算数学表达式

回答时请严格按照以下格式:
Thought: 先分析当前任务,决定下一步
Action: 工具名称
Action Input: 工具参数
Observation: 这一行由系统填入工具返回结果
... 以上步骤可以重复多轮 ...
Final Answer: 当你确定可以回答时,在这里给出最终答案

应用代码负责把这个格式跑起来。重点不是代码多复杂,而是职责分工要清楚:模型只决定下一步,控制器负责解析、执行、追加历史和判断终止。

复制代码
def react_agent(question: str, tools: dict, max_steps: int = 8):
prompt = build_react_prompt(question, tools)
history = []

for step in range(max_steps):
# 1. 调用模型,让它输出下一步 Thought + Action
response = llm.generate(prompt + "
".join(history))

# 2. 如果模型给出最终答案,循环结束
if "Final Answer:" in response:
return response.split("Final Answer:", 1)[-1].strip()

# 3. 解析工具名称和工具入参
action, action_input = parse_action(response)

# 4. 执行工具,拿到真实结果
if action not in tools:
observation = f"工具 {action} 不存在,请重新选择可用工具"
else:
observation = tools[action](action_input)

# 5. 把模型输出和工具结果都放回历史,进入下一轮
history.append(response)
history.append(f"Observation: {observation}")

return "超过最大步数,任务未完成"

上面这段代码里,max_steps 很重要。ReAct 如果没有步数限制,遇到坏工具、坏检索结果或目标不清的问题,就可能一直转下去。工程上还要配合 timeout、token budget 和工具错误处理。

六、现代 Tool Calling:不再靠解析 Action 文本

现在很多模型已经原生支持 Function Calling / Tool Calling。开发者把工具用 JSON Schema 描述给模型,模型如果判断需要工具,会返回结构化的 tool_call,而不是在文本里写 Action: search。

这让 ReAct 的实现更稳:工具名、参数、类型都更清晰,也更方便做参数校验、权限控制和审计。思想没有变,仍然是"思考 -> 行动 -> 观察",只是 Action 的表达方式从自由文本升级为结构化调用。

复制代码
tools = [{
"type": "function",
"function": {
"name": "search_company_market_cap",
"description": "查询公司当前市值",
"parameters": {
"type": "object",
"properties": {
"company": {"type": "string", "description": "公司名称"}
},
"required": ["company"]
}
}
}]

# 模型可能返回:
# tool_call: search_company_market_cap({"company": "Apple"})
# 系统执行工具后,再把 tool_result 回传给模型继续生成。

七、ReAct 的两个坑:漂移和错误传播

ReAct 很灵活,但也因为灵活,容易在长任务里失控。第一个问题是循环漂移:每一步都根据当前历史重新决策,历史越长,无关信息越多,模型越可能被旁枝信息带跑。

第二个问题是错误传播:如果某一步工具返回了错误数据,后续推理会把这个错误当成事实继续使用。ReAct 默认是前向推进的,它本身没有强制回头检查机制。

八、解决思路:Plan-and-Execute + ReAct

如果任务很短,ReAct 很好用。如果任务很长,最好先加一层全局规划。这就是 Plan-and-Execute:先让 Planner 制定整体计划,再让 Executor 按计划执行。每一个子任务内部仍然可以用 ReAct,但执行器始终知道自己处在整体计划的哪一步。

这个组合很适合深度研究、复杂数据分析、代码修复、长文写作和多工具协作任务。它的优势是方向更稳,缺点是多了一次规划开销,所以简单任务不需要强行套。

复制代码
def plan_and_execute(question: str, tools: dict):
# 第一阶段:全局规划,只想清楚步骤,不执行工具
plan = llm.generate(f"""
请为下面任务制定分步计划:
任务:{question}
要求:步骤具体、可执行、可校验。
""")
steps = parse_plan(plan)
results = []

# 第二阶段:逐步执行,每一步可以用 ReAct 执行
for i, step in enumerate(steps):
step_result = react_executor(
task=step,
tools=tools,
context=f"当前是第 {i + 1} 步,共 {len(steps)} 步",
previous_results=results
)
results.append(step_result)

# 如果实际结果偏离预期,动态调整剩余计划
if need_replan(step, step_result, steps[i + 1:]):
new_plan = llm.generate(f"""
原计划:{steps}
已完成结果:{results}
请重新规划剩余步骤。
""")
steps = steps[:i + 1] + parse_plan(new_plan)

return summarize_results(results)

九、实际项目中怎么选?

不是所有任务都要用 ReAct。很多稳定流程,用普通 Workflow 反而更可靠。判断标准可以看三个问题:任务是否需要多步推理?是否需要外部工具?是否允许模型自由决定下一步?

如果只是改写、总结、分类,Direct 或普通 Workflow 足够。如果需要分步分析,用 CoT 或结构化推理。如果需要实时信息、工具调用、数据库查询,就用 ReAct。如果任务很长、目标很明确、多工具协作,就用 Plan-and-Execute + ReAct。

十、ReAct 上线前必须加的护栏

Agent 能跑通,不代表能上线。上线最怕的不是不会调工具,而是乱调工具。尤其是发消息、下单、转账、删除数据、修改配置这类动作,不能让模型直接自由执行。

生产环境里,ReAct 至少要有六类护栏:工具白名单、参数校验、循环上限、结果校验、可观测性和人工介入。否则一旦循环漂移或工具结果错误,问题会很难排查。

十一、面试里的标准回答

ReAct 是 Reasoning and Acting 的缩写,本质是让 LLM 在推理过程中穿插真实行动。它不是单纯让模型一步步想,而是让模型在每一轮输出下一步 Thought 和 Action,外部代码执行工具后,把 Observation 回填到上下文里,再进入下一轮。

它相对 CoT 的优势是能连接外部世界,拿实时数据、调 API、查数据库、执行计算;相对 Act-only 的优势是每一步行动都有推理链条支撑,过程更可解释。

它的局限也很明显:长任务容易循环漂移,错误 Observation 会向后传播,所以生产环境通常会搭配 Plan-and-Execute、工具白名单、步数限制、结果校验和人工确认。