ReAct Agent:手写 Thought-Action-Observe 循环,从工具调用到真正的 Agent

系列导读:本系列共 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)
相关推荐
一直会游泳的小猫2 小时前
CC-Switch使用指南
ai·claude code·ai配置管理工具
weixin_449290012 小时前
端到端智能对话系统架构文档
ai
夏白分享社2 小时前
OpenClaw 本地模型终极实战:vLLM 部署优化完整教程!
ai·开源软件·openclaw
鸡吃丸子2 小时前
如何编写一个高质量的AI Skill
前端·ai
夏星印2 小时前
学习吴恩达课程机器学习笔记
人工智能·笔记·学习·机器学习·ai
威化饼的一隅2 小时前
【大模型LLM学习】天池Deep Research Agent开发赛
大模型·agent·智能体·deep research·深度研究智能体·deep search
qq_211387473 小时前
基于LangGraph多agent
开发语言·前端·javascript·agent·langgraph
智算菩萨3 小时前
AGI神话:人工通用智能的幻象如何扭曲与分散数字治理的注意力
论文阅读·人工智能·深度学习·ai·agi
范特西林3 小时前
从GPT到OpenClaw:AI智能体演进的五个阶段与范式革命
openai·agent