系列导读:本系列共 8 篇,带你从零到一构建完整的 RAG + Agent 项目。
- 第 1 篇:最小 RAG 实现,纯 numpy,无任何 AI 框架
- 第 2 篇:接入 Ollama 本地大模型,实现真实语义检索
- 第 3 篇:接入 ChromaDB 持久化向量数据库
- 第 4 篇:用 LangChain 重构 + 多轮对话
- 第 5 篇:LangGraph 多步推理工作流
- 第 6 篇:MCP 工具调用协议集成
- 第 7 篇:多智能体 RAG(CrewAI 核心思想)
- 第 8 篇(本文):ReAct Agent,真正的 Agent 实现
一、工具调用 ≠ Agent
很多人把第 6 篇的 MCP 工具调用叫做 "Agent",但严格来说它只是 Tool Calling。
两者的本质区别:
工具调用(step6):
问题 → LLM 一次决定所有工具 → 批量执行 → 汇总回答
决策次数:1次,固定三步
ReAct Agent(step8):
问题 →
[Thought] 思考:当前有什么信息,下一步做什么?
[Action] 选择一个工具调用
[Observe] 观察工具返回结果
→ 回到 Thought,基于新结果再思考
→ 直到信息足够,输出 Final Answer
决策次数:动态,每步重新决策
关键差异:ReAct 每执行一个工具,都会"看到"结果,然后再决定下一步。这使得一个工具的输出可以成为下一个工具的输入参数。
二、ReAct 是什么
ReAct = Re asoning + Acting,来自 2022 年 Google 论文:
ReAct: Synergizing Reasoning and Acting in Language Models
核心思想:让 LLM 在行动(调用工具)和推理(思考)之间交替进行,形成轨迹(Trajectory):
Thought: 需要知道今天的日期才能计算
Action: get_current_date
Action Input: {}
Observation: 2026年03月20日 (周五,2026-03-20)
Thought: 今天是周五,请假到本周五就是今天,共1个工作日
Action: calculate_leave_days
Action Input: {"start_date": "2026-03-20", "end_date": "2026-03-21"}
Observation: 从 2026-03-20 到 2026-03-21,共 2 个工作日
Thought: 已有足够信息,可以回答了
Final Answer: 今天(周五)开始请假到下周一,共2个工作日...
每一步的 Observation 都会追加进上下文,LLM 在下一轮 Thought 时能看到所有历史。
三、用 StateGraph 实现循环
ReAct 的循环结构天然适合用图来表达:
┌──────────────────────────┐
↓ │
[think] [act]
有答案了 ↓ 需要工具 ↓ ↑
END [act] ───────────┘
超过步数 ↓
END
只有两个节点:think(推理)和 act(行动),通过条件边实现循环。
python
def build_react_graph() -> StateGraph:
graph = StateGraph()
graph.add_node("think", node_think)
graph.add_node("act", node_act)
graph.set_entry_point("think")
# act 完成后固定回到 think
graph.add_edge("act", "think")
# think 完成后根据情况路由
def route_after_think(state):
if state.get("next_action") == "answer":
return "end"
if state.get("step_count", 0) >= MAX_STEPS: # 防死循环
return "end"
return "act"
graph.add_conditional_edges("think", route_after_think,
{"act": "act", "end": "END"})
return graph
四、核心:ReAct Prompt 设计
ReAct 能工作的关键是 Prompt 格式约定,必须让 LLM 严格按照固定格式输出,才能被解析:
python
def build_react_prompt(question, trajectory, tools):
tools_desc = "\n".join(f"- {t['name']}: {t['description']}" for t in tools)
history = "\n".join(trajectory) if trajectory else "(无)"
return f"""你是一个智能助手,通过「思考→行动→观察」循环来回答问题。
## 可用工具
{tools_desc}
## 输出格式(严格遵守)
格式一(需要调用工具时):
Thought: 分析当前情况,决定下一步行动
Action: 工具名称
Action Input: {{"参数名": "参数值"}}
格式二(信息足够,可以回答时):
Thought: 已有足够信息,可以回答
Final Answer: 完整的最终回答
## 规则
- 每轮只调用一个工具
- Action Input 必须是合法 JSON
---
## 问题
{question}
## 历史轨迹
{history}
## 下一步"""
设计要点:
trajectory(历史轨迹)每轮都追加进 Prompt,LLM 能看到所有历史- 两种输出格式互斥,LLM 必须选一个
Action Input要求合法 JSON,便于解析
五、轨迹解析
python
def parse_react_output(text: str):
"""
解析 LLM 输出,返回三种情况:
("action", tool_name, arguments) → 需要调用工具
("answer", final_answer, None) → 任务完成
("unknown", raw_text, None) → 解析失败
"""
# 优先检查 Final Answer
if "Final Answer:" in text:
answer = text.split("Final Answer:", 1)[1].strip()
return ("answer", answer, None)
# 解析 Action + Action Input
action_name, action_input = None, None
for line in text.split("\n"):
if line.startswith("Action:"):
action_name = line[len("Action:"):].strip()
elif line.startswith("Action Input:"):
raw = line[len("Action Input:"):].strip()
try:
action_input = json.loads(raw)
except json.JSONDecodeError:
# 容错:提取 {} 内容
match = re.search(r'\{.*\}', raw, re.DOTALL)
action_input = json.loads(match.group()) if match else {}
if action_name:
return ("action", action_name, action_input or {})
return ("unknown", text, None)
六、两个核心节点
node_think:推理节点
python
def node_think(state):
"""LLM 根据当前轨迹决定下一步"""
prompt = build_react_prompt(state["question"],
state.get("trajectory", []),
server.list_tools())
llm_output = ollama.generate(model=CHAT_MODEL, prompt=prompt)["response"]
kind, value, args = parse_react_output(llm_output)
# 把这步输出追加进轨迹
new_trajectory = state.get("trajectory", []) + [llm_output]
if kind == "answer":
return {"trajectory": new_trajectory, "next_action": "answer",
"final_answer": value, "step_count": state["step_count"] + 1}
elif kind == "action":
return {"trajectory": new_trajectory, "next_action": "tool",
"pending_tool": value, "pending_args": args,
"step_count": state["step_count"] + 1}
else: # 解析失败,强制结束
return {"trajectory": new_trajectory, "next_action": "answer",
"final_answer": f"解析失败:{value}", "step_count": state["step_count"] + 1}
node_act:行动节点
python
def node_act(state):
"""调用工具,把 Observation 写入轨迹"""
result = server.call(state["pending_tool"], state["pending_args"])
# Observation 追加进轨迹(下一轮 think 能看到)
observation = f"Observation: {result}"
new_trajectory = state.get("trajectory", []) + [observation]
return {"trajectory": new_trajectory,
"pending_tool": None, "pending_args": None,
"last_observation": result}
关键 :node_act 把工具结果以 Observation: ... 格式追加进轨迹,下一轮 node_think 调用时,这个结果已经在 Prompt 的 历史轨迹 里了。
七、完整运行示例
问题:「请假需要提前几天申请?如果今天申请,最早哪天能开始休?」
[节点: think]
Thought: 需要先查公司请假政策,了解提前天数要求
Action: search_knowledge_base
Action Input: {"query": "请假提前申请天数"}
[节点: act]
调用工具: search_knowledge_base({"query": "请假提前申请天数"})
工具结果: [相似度:0.891] 请假流程:登录OA系统,提前3个工作日提交。
[节点: think]
Thought: 需要提前3个工作日,还需要知道今天是哪天才能算出最早日期
Action: get_current_date
Action Input: {}
[节点: act]
调用工具: get_current_date({})
工具结果: 2026年03月20日 (周五,2026-03-20)
[节点: think]
Thought: 今天是2026-03-20(周五),提前3个工作日后是2026-03-25(周三)
Action: calculate_leave_days
Action Input: {"start_date": "2026-03-20", "end_date": "2026-03-25"}
[节点: act]
调用工具: calculate_leave_days(...)
工具结果: 从 2026-03-20 到 2026-03-25,共 4 个工作日
[节点: think]
Thought: 已有足够信息
Final Answer: 根据公司规定,请假需提前3个工作日申请。
今天(2026年3月20日,周五)提交申请,最早可从2026年3月25日(周三)开始休假。
step6 做不到这个:step6 第一步就要决定所有工具,但不知道"提前3天"具体是哪天,因为还没查知识库------这个信息依赖是链式的,只有 ReAct 循环才能处理。
八、与前几篇的对比
| 步骤 | 技术 | 能力 |
|---|---|---|
| Step 5 LangGraph | 条件工作流 | 流程可分支、可重试,但步骤预定义 |
| Step 6 MCP | 工具调用 | 一次决定所有工具,无链式依赖 |
| Step 7 Multi-Agent | 角色协作 | 多专家分工,但每个 Agent 内部仍是单步 |
| Step 8 ReAct | 推理循环 | 动态多步,工具输出影响后续决策 |
ReAct 是单 Agent 能力的天花板,它不关心有几个角色,而是让一个 Agent 通过循环完成复杂推理链。
九、防止死循环:MAX_STEPS
ReAct 如果没有终止条件,可能会一直循环。两种终止方式:
python
MAX_STEPS = 6 # 全局步数上限
def route_after_think(state):
if state.get("next_action") == "answer":
return "end" # LLM 主动说"我有答案了"
if state.get("step_count", 0) >= MAX_STEPS:
return "end" # 被动兜底:超过步数强制结束
return "act"
实际项目中 MAX_STEPS 通常设为 5-10,根据任务复杂度调整。
总结
ReAct Agent 的三个核心组件:
| 组件 | 作用 | 关键代码 |
|---|---|---|
| ReAct Prompt | 约定 Thought/Action/Observe 格式 | build_react_prompt() |
| 轨迹(Trajectory) | 记录所有历史,每步追加 | state["trajectory"] |
| 循环图 | think ↔ act 交替执行 | StateGraph + add_edge("act", "think") |
运行:
bash
python3 step8_react_agent.py
至此,整个系列完整覆盖了从 RAG 到 Agent 的完整技术栈:
RAG(检索增强)
→ 工具调用(MCP)
→ 工作流(LangGraph)
→ 多智能体协作(Multi-Agent)
→ ReAct 推理循环(Agent)