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

发布时间:2026/5/25 0:31:03

ReAct Agent:手写 Thought-Action-Observe 循环,从工具调用到真正的 Agent 系列导读本系列共 8 篇带你从零到一构建完整的 RAG Agent 项目。第 1 篇最小 RAG 实现纯 numpy无任何 AI 框架第 2 篇接入 Ollama 本地大模型实现真实语义检索第 3 篇接入 ChromaDB 持久化向量数据库第 4 篇用 LangChain 重构 多轮对话第 5 篇LangGraph 多步推理工作流第 6 篇MCP 工具调用协议集成第 7 篇多智能体 RAGCrewAI 核心思想第 8 篇本文ReAct Agent真正的 Agent 实现一、工具调用 ≠ Agent很多人把第 6 篇的 MCP 工具调用叫做 “Agent”但严格来说它只是Tool Calling。两者的本质区别工具调用step6 问题 → LLM 一次决定所有工具 → 批量执行 → 汇总回答 决策次数1次固定三步 ReAct Agentstep8 问题 → [Thought] 思考当前有什么信息下一步做什么 [Action] 选择一个工具调用 [Observe] 观察工具返回结果 → 回到 Thought基于新结果再思考 → 直到信息足够输出 Final Answer 决策次数动态每步重新决策关键差异ReAct 每执行一个工具都会看到结果然后再决定下一步。这使得一个工具的输出可以成为下一个工具的输入参数。二、ReAct 是什么ReActReasoning Acting来自 2022 年 Google 论文ReAct: Synergizing Reasoning and Acting in Language Models核心思想让 LLM 在行动调用工具和推理思考之间交替进行形成轨迹TrajectoryThought: 需要知道今天的日期才能计算 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行动通过条件边实现循环。defbuild_react_graph()-StateGraph:graphStateGraph()graph.add_node(think,node_think)graph.add_node(act,node_act)graph.set_entry_point(think)# act 完成后固定回到 thinkgraph.add_edge(act,think)# think 完成后根据情况路由defroute_after_think(state):ifstate.get(next_action)answer:returnendifstate.get(step_count,0)MAX_STEPS:# 防死循环returnendreturnactgraph.add_conditional_edges(think,route_after_think,{act:act,end:END})returngraph四、核心ReAct Prompt 设计ReAct 能工作的关键是Prompt 格式约定必须让 LLM 严格按照固定格式输出才能被解析defbuild_react_prompt(question,trajectory,tools):tools_desc\n.join(f-{t[name]}:{t[description]}fortintools)history\n.join(trajectory)iftrajectoryelse无returnf你是一个智能助手通过「思考→行动→观察」循环来回答问题。 ## 可用工具{tools_desc}## 输出格式严格遵守 格式一需要调用工具时 Thought: 分析当前情况决定下一步行动 Action: 工具名称 Action Input: {{参数名: 参数值}} 格式二信息足够可以回答时 Thought: 已有足够信息可以回答 Final Answer: 完整的最终回答 ## 规则 - 每轮只调用一个工具 - Action Input 必须是合法 JSON --- ## 问题{question}## 历史轨迹{history}## 下一步设计要点trajectory历史轨迹每轮都追加进 PromptLLM 能看到所有历史两种输出格式互斥LLM 必须选一个Action Input要求合法 JSON便于解析五、轨迹解析defparse_react_output(text:str): 解析 LLM 输出返回三种情况 (action, tool_name, arguments) → 需要调用工具 (answer, final_answer, None) → 任务完成 (unknown, raw_text, None) → 解析失败 # 优先检查 Final AnswerifFinal Answer:intext:answertext.split(Final Answer:,1)[1].strip()return(answer,answer,None)# 解析 Action Action Inputaction_name,action_inputNone,Noneforlineintext.split(\n):ifline.startswith(Action:):action_nameline[len(Action:):].strip()elifline.startswith(Action Input:):rawline[len(Action Input:):].strip()try:action_inputjson.loads(raw)exceptjson.JSONDecodeError:# 容错提取 {} 内容matchre.search(r\{.*\},raw,re.DOTALL)action_inputjson.loads(match.group())ifmatchelse{}ifaction_name:return(action,action_name,action_inputor{})return(unknown,text,None)六、两个核心节点node_think推理节点defnode_think(state):LLM 根据当前轨迹决定下一步promptbuild_react_prompt(state[question],state.get(trajectory,[]),server.list_tools())llm_outputollama.generate(modelCHAT_MODEL,promptprompt)[response]kind,value,argsparse_react_output(llm_output)# 把这步输出追加进轨迹new_trajectorystate.get(trajectory,[])[llm_output]ifkindanswer:return{trajectory:new_trajectory,next_action:answer,final_answer:value,step_count:state[step_count]1}elifkindaction: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行动节点defnode_act(state):调用工具把 Observation 写入轨迹resultserver.call(state[pending_tool],state[pending_args])# Observation 追加进轨迹下一轮 think 能看到observationfObservation:{result}new_trajectorystate.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_STEPSReAct 如果没有终止条件可能会一直循环。两种终止方式MAX_STEPS6# 全局步数上限defroute_after_think(state):ifstate.get(next_action)answer:returnend# LLM 主动说我有答案了ifstate.get(step_count,0)MAX_STEPS:returnend# 被动兜底超过步数强制结束returnact实际项目中MAX_STEPS通常设为 5-10根据任务复杂度调整。总结ReAct Agent 的三个核心组件组件作用关键代码ReAct Prompt约定 Thought/Action/Observe 格式build_react_prompt()轨迹Trajectory记录所有历史每步追加state[trajectory]循环图think ↔ act 交替执行StateGraphadd_edge(act, think)运行python3 step8_react_agent.py至此整个系列完整覆盖了从 RAG 到 Agent 的完整技术栈RAG检索增强 → 工具调用MCP → 工作流LangGraph → 多智能体协作Multi-Agent → ReAct 推理循环Agent

相关新闻