ReAct:让模型学会边想边做
系列:ToyAgent 智能体知识库 03/26
一句话核心:Thought → Action → Observation → 循环,直到 Final Answer——几乎所有智能体框架的内核。
为什么需要
基础对话模型有两个致命短板:
- 幻觉:一本正经地编造实时信息。问它天气,它会自信地给一个貌似合理的假数据。
- 一次成型:复杂问题需要拆步骤求解,模型却总想一口气答完,中间环节全靠脑补,错一步全盘皆错。
ReAct(Reasoning + Acting)的解法是强制模型一次只走一小步:先说想法,再说行动,然后停下等人执行,拿到观察结果再继续想。把"大问题"切成"小步 + 真实反馈",两个短板同时被治住。
核心机制
协议约定:让模型输出结构化 JSON
靠系统提示词约定输出格式,每轮二选一:
private static final String SYSTEM_PROMPT = """
你是一个使用 ReAct 模式解决问题的智能体。
每一轮只输出一个 JSON 对象,格式二选一:
1. 继续行动:{"thought": "...", "action": "工具名", "action_input": "参数"}
2. 给出答案:{"thought": "...", "final": "最终答案"}
可用工具:
- get_weather(city): 查询城市天气
- calculator(expression): 计算四则运算
""";
循环体:20 行写完 ReAct
for (int step = 1; step <= MAX_STEPS; step++) {
String raw = model.chat(messages); // 1. 模型思考
Object parsed = Json.parse(raw);
String fin = Json.str(parsed, "final");
if (fin != null) return fin; // 2. 有答案 → 结束
String action = Json.str(parsed, "action"); // 3. 有行动 → 执行工具
String observation = executeTool(action,
Json.str(parsed, "action_input"));
messages.add(Message.assistant(raw)); // 4. 结果喂回模型
messages.add(Message.user("Observation: " + observation));
}
return "已达最大步数,循环终止"; // 5. 保险丝
灵魂所在:第 4 步
把工具结果作为新消息追加进对话再发给模型。模型没有"感知世界"的能力,它的一切认知都来自上下文——上下文里有什么,它就"活"在什么世界里。忘记回填 Observation,模型就会基于想象继续推理,幻觉立刻回来。
stateDiagram-v2
[*] --> Thought
Thought --> Action : 输出工具名与参数
Action --> Observation : 系统执行工具
Observation --> Thought : 回填进上下文
Thought --> FinalAnswer : 输出 final
FinalAnswer --> [*]
图示:ReAct 的思考-行动-观察状态循环,每轮二选一——继续行动或给出最终答案,Step1 与 Step2 就是在这个环上各走一圈。
举例:一个完整循环的对话轨迹
用户:北京今天适合穿短袖吗?
--- Step 1 ---
Thought: 需要先查北京实时气温才能判断。
Action: get_weather | action_input: {"city": "北京"}
系统执行工具 → "晴,气温 29°C"
Observation 追加进消息列表。
--- Step 2 ---
Thought: 29°C 高于 26°C,适合短袖。
Final: 北京今天晴,29°C,适合穿短袖,注意防晒。
注意这个轨迹里模型没有一步是靠猜的:气温来自工具返回的真实数据,结论建立在 Observation 之上。如果工具返回"城市未收录",模型下一步会看到这个失败并尝试换参数或如实告知——这就是"失败可读"让模型能自我纠错。
反例:若第 4 步忘记回填 Observation,Step 2 时模型的上下文里没有任何真实气温,它只能编一个"大概 25 度左右吧"——幻觉复现。
为什么必须设 MAX_STEPS
如果模型反复要求调用同一个工具,或工具一直返回错误,循环就会无限烧钱。生产系统里这个保险丝叫 Budget Guard(步数 / Token / 时间三重预算),是 Agent Runtime 的标配。教学版用最简单的步数上限表达这个思想。
graph TD
S1["Step 1 思考"] --> S1a["Action 查天气"]
S1a --> S1b["Observation 晴 29 度"]
S1b --> S2["Step 2 思考"]
S2 --> S2a["Final 结论 适合短袖"]
S2a --> D["结束"]
S1b --> X["步数超过 MAX_STEPS"]
X --> F["保险丝终止 返回兜底提示"]
图示:一个真实的两步循环轨迹,以及 MAX_STEPS 保险丝在循环失控时的兜底出口。
面试考点
- "ReAct 和直接 Function Calling 有什么区别?"——ReAct 是一种推理-行动交替的模式,可以用提示词协议实现;Function Calling 是模型厂商提供的结构化能力(原生
tool_calls字段)。生产框架的 ReAct 通常跑在原生 Function Calling 上,而不是让模型手写 JSON。 - "如何防止 ReAct 循环失控?"——三重预算(步数/Token/时间)+ 重复动作检测(连续相同 action 提前熔断)+ 工具失败可读化。
生产化路径
- 别手写 JSON 协议,用原生 Function Calling(见 Step04 / LangChain4j 的
@Tool):教学版提示词协议帮你理解语义,生产版用结构化字段避免模型输出格式漂移导致的解析失败。 - 把 Budget Guard 做进 Runtime 层而非业务代码:步数、Token、超时三项预算统一配置、统一告警(参考本项目 learn 模块的输出 Token 上限配置实践)。
- Observation 写入要完整:工具失败信息、错误码、耗时都应进入上下文,模型纠错能力直接取决于它看到的反馈质量。