首页 / 资讯中心 / 文章详情

ReAct循环工程实现深度剖析:大模型Agent推理与工具调用实战

ReAct循环工程实现深度剖析:大模型Agent推理与工具调用实战 ★ FEATURED ARTICLE
02:Agent Loop 深度剖析ReAct 循环的工程实现如果你做过大模型应用开发一定遇到过这样的场景模型回答得头头是道但一涉及实时数据、外部操作就露馅了。比如让 Agent 帮你查天气它告诉你“今天多云”其实它的训练数据里根本没有今天的天气。ReAct 循环就是来解决这个问题的——它让模型在“思考”和“行动”之间来回切换通过调用外部工具获取真实信息再基于观察结果继续推理。这篇文章我想和你掰开揉碎聊聊 ReAct 的工程实现。网上讲 ReAct 原理的文章不少但大多停留在概念层面真正落地时会遇到一堆细节问题循环什么时候停如何解析模型的输出工具调用失败怎么恢复上下文爆炸怎么处理这篇文章会从零开始搭建一个可运行的 ReAct Agent 循环把这些工程细节全部过一遍。适合谁看如果你正在用大模型做 Agent 开发或者打算自研一套工具调用框架又或者只是好奇 LangChain 这类框架底层到底怎么跑的这篇文章都能给你一些实在的参考。我会用 Python 手写一个精简但完整的实现不依赖任何重量级框架这样你能看清每一个环节的运作逻辑。1. 整体设计与思路拆解1.1 ReAct 循环的核心思想ReAct 这个词是 Reasoning and Acting 的缩写核心思路就一句话让大模型交替进行推理和行动每一步行动的结果都会成为下一步推理的输入。这个思路其实模仿了人类解决问题的过程。你面对一个复杂任务时不会一口气做完而是先想一步、做一步、看看结果、再调整。比如你要安排一次出差行程先查航班行动看到航班信息后思考哪个时间合适推理再查酒店行动再思考预算和位置推理最后形成方案。ReAct 把这个过程形式化为一个循环思考Thought → 行动Action → 观察Observation → 思考 → 行动 → 观察 → ... → 最终答案Final Answer每一步中模型输出结构化文本系统解析这些文本并执行相应的工具调用把结果作为观察信息返回给模型。循环往复直到模型输出最终答案或达到最大迭代次数。1.2 为什么选择 ReAct 而不是其他方案在做 Agent 工具调用时有几条技术路线可以选择我各自试过之后才选定了 ReAct 作为基础框架。第一种是纯工具预选方案。在对话开始前根据用户问题手动指定要用哪些工具然后把工具说明拼进 Prompt。这个方案简单但灵活性很差不同问题需要不同的工具组合没法动态适配。第二种是函数调用方案。让模型直接输出结构化 JSON指定函数名和参数系统解析后执行。这是目前 API 级别最常用的方案但有一个问题函数调用格式往往让模型丧失了自由推理的空间。第三种就是 ReAct。它让模型以自然语言输出思考和行动可读性强便于调试而且天然支持多轮工具调用。更重要的是ReAct 的推理过程是显式的你能清楚地看到模型每一步在想什么、为什么调用某个工具这在排查问题时极其有用。我实际开发中发现ReAct 方案的容错性也更好。函数调用格式一旦出错参数缺失、JSON 格式错误整个调用就失败而 ReAct 的文本格式即使稍有偏差也可以通过解析规则兜底。1.3 工程实现的最小闭环一个可运行的 ReAct Agent 循环至少需要以下组件一个支持对话补全的大模型LLM一套工具注册与分发机制一段 Prompt 模板告诉模型 ReAct 的交互格式一个循环控制器负责驱动思考-行动-观察的循环一个输出解析器从模型输出中提取行动和参数这五个组件缺一不可。我把它们拆成五个类来组织代码后面每一部分都单独讲清楚。2. 核心细节解析与实操要点2.1 Prompt 模板ReAct 循环的灵魂ReAct 循环能不能跑通Prompt 模板占了六成以上的功劳。模板不仅要告诉模型“你可以调用工具”还要教会它完整的交互格式。我使用的基础模板长这样你是一个智能助手可以通过推理和调用工具来完成任务。 你可以使用以下工具 {tools} 请按照以下格式输出 Thought: 你当前的想法分析情况并决定下一步 Action: 要调用的工具名称必须是以上工具之一 Action Input: 传给工具的参数JSON格式 如果得到最终答案请输出 Thought: 我已经获得足够信息 Final Answer: 最终答案这段模板的关键在于三点。第一工具说明必须列表化、结构化让模型能准确选择。工具说明我一般写成工具名称: search_web 功能描述: 在互联网上搜索信息返回相关网页结果 参数: {query: 搜索关键词, max_results: 可选返回结果数量}第二示例 Demos 要精心设计。ReAct 循环是 Few-shot 机制没有示例引导模型很容易乱输出。我通常会放两个完整示例一个展示多步推理加工具调用另一个展示直接输出最终答案。第三要明确规定输出格式的边界。如果不加限制模型可能一次输出多个 Action或者 Action 和 Final Answer 混在一起。我在模板里会用|end_of_turn|这样的特殊标记来分隔每一轮输出。注意模板里对工具的命名一定要统一。有些模型对 CamelCase 和 snake_case 特别敏感我统一用 snake_case并且在 Prompt 里明确强调“工具名称区分大小写严格按列表中的名称输出”。2.2 输出解析从文本到结构化行动模型输出的是自然语言文本要执行工具调用就得从文本里抽出行动命令。这一步的健壮性直接决定整个 Agent 的稳定性。我的解析策略分三个层级第一层先检查是否包含最终答案标记。如果输出里有 Final Answer直接进入收尾阶段后面的文本就是答案。第二层检查是否包含行动标记。用正则匹配 Action: 和 Action Input: 两个字段提取工具名称和参数 JSON。这里要注意模型可能输出多行或带前缀我用宽松匹配的方式只取第一个匹配项。第三层兜底策略。如果既没有最终答案也没有行动标记说明模型输出异常此时有两种选择把这个输出当作一次“思考”反馈给模型或者中止循环回到用户。我倾向于前者因为模型有时候只是忘了写 Action给它一次机会可能就能纠偏。import re import json def parse_llm_output(output: str) - dict: 解析模型输出返回结构化动作或最终答案 final_match re.search(rFinal Answer:\s*(.), output, re.DOTALL) if final_match: return {type: final, content: final_match.group(1).strip()} action_match re.search(rAction:\s*(\w), output) input_match re.search(rAction Input:\s*(\{.*?\}), output, re.DOTALL) if action_match and input_match: try: params json.loads(input_match.group(1)) except json.JSONDecodeError: params {raw_input: input_match.group(1).strip()} return {type: action, name: action_match.group(1), params: params} return {type: unknown, content: output}这里有个实战中反复踩坑的点模型生成的 Action Input 经常不是严格合法的 JSON——多了注释、少了引号、用了单引号。我的做法是第一轮容忍宽松解析如果工具执行时报参数错误把错误信息反馈给模型让它自行修正。2.3 停止条件与循环控制ReAct 循环不能无限跑下去必须设置明确的停止条件。我总结出三个必须同时生效的条件第一个是答案就位。模型输出了 Final Answer这是最自然的结束信号。第二个是最大迭代次数。无论模型是否给出最终答案循环达到上限就必须强制停止。我默认设 8 轮复杂的多步任务最多 15 轮。第三个是工具连续失败次数。如果同一工具连续失败 3 次基本可以断定进入了死循环应该停止并回退到用户。MAX_ITERATIONS 8 MAX_TOOL_FAILURES 3 class ReActLoop: def __init__(self, max_iterationsMAX_ITERATIONS): self.max_iterations max_iterations self.iteration 0 self.tool_failures 0 self.history [] def step(self, output: str): parsed parse_llm_output(output) self.iteration 1 if parsed[type] final: return {status: done, answer: parsed[content]} if parsed[type] action: result self.execute_tool(parsed[name], parsed[params]) if result.get(success): self.tool_failures 0 else: self.tool_failures 1 if self.iteration self.max_iterations: return {status: max_iterations, partial_output: output} if self.tool_failures MAX_TOOL_FAILURES: return {status: tool_failure, last_result: result} return {status: continue, observation: result}停止条件的逻辑看起来简单但工程落地时还有两个隐藏问题。第一最大迭代次数不能让模型知道。如果你在 Prompt 里写了“你有 8 次机会”模型会倾向于用完所有轮次后直接强行给答案反而不认真推理。我的做法是不告诉模型上限让循环控制器静默计数。第二当模型输出 Final Answer 但内容质量明显低劣时比如只有“我不知道”四个字不要直接返回。我增加了“答案质量校验”钩子对空答案、过短答案、重复历史答案做拦截强制模型重新推理。2.4 工具注册与执行机制工具注册是整个循环的“物理载体”设计上要考虑到扩展性和容错性。我维护了一个工具注册表用装饰器把函数注册进去元信息名称、描述、参数 Schema会被自动提取。class ToolRegistry: def __init__(self): self._tools {} self._descriptions [] def register(self, name, description, params_schemaNone): def decorator(func): self._tools[name] { func: func, description: description, params_schema: params_schema or {} } return func return decorator def execute(self, name, params): if name not in self._tools: return {success: False, error: f未知工具: {name}} try: result self._tools[name][func](**params) return {success: True, result: result} except Exception as e: return {success: False, error: str(e)}工具执行有几个工程要点每个工具函数必须有超时控制。理论上 Agent 一调用一个可控的结果应该在几秒内返回不能因为网络请求阻塞整个循环。工具结果要转换成文本反馈给模型。不管返回的是 JSON、列表还是其他结构一律序列化成字符串。工具结果要截断。如果某个工具返回的内容特别长比如一个网页全文直接塞给模型会占大量上下文窗口也容易让模型迷失重点。我按工具类型分别设置截断上限默认 1500 字符。3. 实操过程与核心环节实现3.1 搭建最小可运行系统下面我们动手搭建一个完整的 ReAct Agent。为了让你能直接跑起来我选择使用一组模拟工具而不是真实 API 服务这样不用申请任何密钥也能看到完整的循环过程。首先定义两个基础工具一个模拟天气查询一个模拟信息检索。registry ToolRegistry() registry.register( nameget_weather, description查询指定城市的当前天气情况, params_schema{city: 城市名称字符串} ) def get_weather(city): 模拟天气查询工具 weather_data { 北京: 晴24摄氏度微风, 上海: 多云28摄氏度东南风3级, 深圳: 小雨26摄氏度南风2级, 广州: 雷阵雨30摄氏度南风3级 } if city in weather_data: return weather_data[city] return f没有找到城市 {city} 的天气数据 registry.register( namesearch_info, description检索指定主题的公开信息返回相关知识点, params_schema{query: 搜索关键词字符串, limit: 可选返回条数默认为3} ) def search_info(query, limit3): 模拟信息检索工具 knowledge_base { 大语言模型: [ 大语言模型LLM是一种基于深度学习的自然语言处理模型, GPT、Claude、Llama 等都属于大语言模型, LLM 通过海量文本训练具备生成和理解自然语言的能力 ], Agent: [ Agent 是能够感知环境并采取行动以实现目标的智能体, ReAct 是一种结合推理与行动的 Agent 实现范式, Agent 可以调用外部工具扩展模型自身的能力边界 ], ReAct: [ ReAct 全称 Reasoning and Acting让模型交替进行推理与行动, ReAct 循环包括 Thought、Action、Observation 三个核心环节, ReAct 的关键优势是可解释性和对工具调用的动态规划 ] } matched knowledge_base.get(query, [f没有找到和 {query} 相关的信息]) return matched[:limit]这两个工具虽然简单但足够演示 ReAct 的核心逻辑。一个负责查询结构化数据一个负责检索文本信息。接下来实现核心的 Agent 循环控制器。这里我用一个简化版的 LLM 客户端真实项目中你只需要替换成对应厂商的 SDK 调用即可。class ReActAgent: def __init__(self, llm, registry, max_iterations8): self.llm llm self.registry registry self.max_iterations max_iterations self.history [] def build_prompt(self, user_query): tools_desc self.registry.get_descriptions() return f 你是一个智能助手可以通过调用工具来回答问题。 可用工具 {tools_desc} 工作方式你需要循环思考Thought、行动Action、观察Observation直到获得足够信息后输出最终答案Final Answer。 示例 用户问题北京今天天气怎么样 Thought: 用户想知道北京的天气我需要调用 get_weather 工具。 Action: get_weather Action Input: {{city: 北京}} Observation: 晴24摄氏度微风 Thought: 我已经获得了北京的天气信息可以给用户回答了。 Final Answer: 北京今天晴24摄氏度微风。 用户问题{user_query} .strip() def run(self, user_query): prompt self.build_prompt(user_query) self.history.append({role: user, content: prompt}) for iteration in range(self.max_iterations): response self.llm.chat(self.history) parsed parse_llm_output(response) if parsed[type] final: return parsed[content] if parsed[type] action: tool_result self.registry.execute(parsed[name], parsed[params]) observation tool_result[result] if tool_result[success] else f错误: {tool_result[error]} self.history.append({role: assistant, content: response}) self.history.append({role: user, content: fObservation: {observation}}) else: # 模型输出异常把当前输出作为思考继续推进 self.history.append({role: assistant, content: response}) self.history.append({role: user, content: 请继续根据之前的推理输出下一个 Action 或 Final Answer。}) return 达到最大迭代次数无法完成请求。请尝试简化问题或补充信息。3.2 循环过程的可视化跟踪工程实现中最让开发者头疼的就是不知道循环内部发生了什么。我给 ReActAgent 增加了跟踪日志每一步都打印当前状态。def run_with_trace(self, user_query): prompt self.build_prompt(user_query) self.history.append({role: user, content: prompt}) steps [] for iteration in range(self.max_iterations): response self.llm.chat(self.history) parsed parse_llm_output(response) step_log { iteration: iteration 1, model_output: response, parsed_type: parsed[type] } if parsed[type] final: step_log[status] 完成 steps.append(step_log) return parsed[content], steps if parsed[type] action: tool_result self.registry.execute(parsed[name], parsed[params]) step_log[tool_name] parsed[name] step_log[tool_params] parsed[params] step_log[tool_result] tool_result step_log[status] 工具调用 observation tool_result[result] if tool_result[success] else f错误: {tool_result[error]} self.history.append({role: assistant, content: response}) self.history.append({role: user, content: fObservation: {observation}}) else: step_log[status] 需要纠偏 self.history.append({role: assistant, content: response}) self.history.append({role: user, content: 请继续输出 Action 或 Final Answer}) steps.append(step_log) return 达到最大迭代次数, steps跑一个实际例子看看循环过程用户问题深圳的天气怎么样顺便告诉我 ReAct 是什么。 --- 第 1 轮 --- 模型输出: Thought: 用户问了两个问题一个是深圳的天气一个是 ReAct 的概念。我先查天气。 Action: get_weather Action Input: {city: 深圳} 工具结果: 小雨26摄氏度南风2级 --- 第 2 轮 --- 模型输出: Thought: 天气信息已经拿到。接下来我需要查 ReAct 的概念。 Action: search_info Action Input: {query: ReAct} 工具结果: [ReAct 全称 Reasoning and Acting让模型交替进行推理与行动, ...] --- 第 3 轮 --- 模型输出: Thought: 两个问题都已经获取到信息我可以把答案整合后告诉用户。 Final Answer: 深圳今天小雨26摄氏度南风2级。ReAct 是一种结合推理与行动的 Agent 实现范式让模型交替进行思考、行动和观察从而完成复杂任务。这个运行过程非常直观地展示了 ReAct 循环的核心价值模型按照“先规划、再行动、再规划”的顺序推进每一步都有依据。3.3 上下文管理的工程细节在 ReAct 循环中每一轮对话都会往 history 里追加内容。随着迭代次数增加上下文长度线性增长最终会把大模型的上下文窗口塞满。我的处理方案是双重策略。第一历史压缩。当历史记录超过一定长度时把最早的 Observation 内容做摘要。比如一次工具的全文搜索结果很长我会保留其中的关键实体和数字把修饰性文本删掉。第二固定窗口滑动。循环超过一定轮数后把早期的 Thought-Action-Observation 三元组压缩成一行摘要只保留最近几轮的完整记录。这个策略在长任务场景下非常管用。def manage_context(self, history, max_len4000): 控制上下文长度防止窗口溢出 current_len sum(len(item[content]) for item in history) if current_len max_len: return history compressed [] for item in history: if len(item[content]) 500: # 对过长的内容做截断保留首尾关键信息 item[content] item[content][:200] ... item[content][-100:] compressed.append(item) return compressed这个方案的核心思路是牺牲部分历史细节保留当前推理链路的完整性。因为 Agent 越到后面越依赖最近几轮的观察结果早期的信息已经被吸收进模型的推理状态里了。4. 常见问题与排查技巧实录4.1 模型陷入死循环反复调用同一工具这是我开发过程中遇到频率最高的问题。模型在同一状态下反复输出同一个 Action比如每次都查询同一个城市的天气工具返回了一样的结果模型还是继续查。排查思路查看观察信息是否被正确反馈到模型。如果工具结果太长被截断了模型看不到完整结果就会觉得信息不足继续调用同一个工具。检查 Prompt 里是否给了模型“可以停止”的信号。有些模型会进入“尽力服务”模式不确认所有信息都拿到就不停。确认工具结果的变化情况。如果多次调用结果完全一致说明这个工具对这个问题没有增量信息。我常用的修复手段是在历史记录中标记“重复观察”如果工具名和参数都相同就在 Observation 前面加一句“注意你之前已经查过这个数据结果如下”让模型意识到自己在重复操作。4.2 模型输出了非法 JSON 参数模型生成的 Action Input 经常不严格符合 JSON 规范常见问题包括使用单引号替代双引号、多了一个尾逗号、内部包含了换行符。我的容错解析方案分三层先用标准 json.loads 解析如果成功直接使用。如果失败尝试用正则替换修复把单引号替换为双引号、去掉尾逗号。如果还失败把整个 Action Input 作为单个字符串参数传给工具。第三层方案能不能用取决于工具函数是否支持接收字符串参数。我在设计工具时对所有工具函数增加了raw_input兜底参数避免解析失败导致整个循环崩溃。def safe_json_parse(text): 安全解析 JSON支持常见修复策略 try: return json.loads(text), True except json.JSONDecodeError: pass # 修复单引号 fixed text.replace(, ) try: return json.loads(fixed), True except json.JSONDecodeError: pass # 去掉尾逗号 fixed re.sub(r,\s*}, }, text) try: return json.loads(fixed), True except json.JSONDecodeError: pass return {raw_input: text}, False4.3 工具结果过多撑爆上下文某些工具会返回大量文本比如搜索接口返回几十条结果或者数据库查询返回一大张表。直接把全部内容塞进 Observation轻则影响模型推理质量重则超出模型上下文限制。我建议做一个独立的工具结果处理模块针对不同工具的结果类型做策略化处理结构化数据JSON、表格只保留前 3 行或前 500 字符同时附上总行数提示。文本数据做摘要提取保留核心关键句。图片或文件不直接送入上下文Agent 只拿到文件路径和大小元信息。4.4 模型在思考阶段“戏精附体”有时候模型会输出一大段“思考”文本看似在分析问题但实际上什么决策都没有。比如Thought: 用户想了解大语言模型这个话题历史悠久涉及面广。我需要考虑模型的基本原理、训练方法、应用场景等多个方面。大语言模型是一种...这种输出过于宽泛没有任何行动意图白白消耗一轮迭代。排查发现这种问题往往出现在 Prompt 中引导语太开放式的时候。我在模板里加了一条硬约束Thought 部分不要超过 3 句话必须明确包含下一步行动意图。同时我在循环控制里加了超时机制如果连续两轮模型都没有输出 Action 或 Final Answer就主动插入提醒要求模型立即输出行动指令。4.5 LLM 依赖问题直接用基础模型的坑如果你直接调用基础模型的 Chat Completion API 来实现 ReAct可能会遇到两个问题。第一指令遵循能力不足。小参数模型或某些国产模型在长指令和复杂格式下的表现不稳定经常输出格式错误。我的建议是至少使用对话能力储备较强的模型或者对模型进行简单的格式指令微调。第二模型固化了示例格式。如果示例中使用了某种特定句式模型可能只愿意复刻那个句式导致工具调用时参数名写错。我通常准备两到三套不同风格的示例循环交替使用。4.6 常见问题速查表问题现象可能原因排查优先级解决方案循环不停止停止条件未生效高检查最终答案判定逻辑确认模型输出中包含明确的 Final Answer 标记同样工具反复调用工具结果没反馈或无效高给 Observation 增加重复提醒压缩工具返回JSON 参数解析失败模型输出格式不规范中多层解析容错raw_input 兜底上下文溢出历史累积过长中对历史做压缩和滑动窗口截断模型输出纯思考没行动Prompt 引导不明确低限制思考长度强制输出行动指令工具名不存在模型幻觉或无中生有低在错误信息里给出可用工具列表引导模型重新选择5. 工程化优化与进阶方向5.1 从单步 ReAct 到 Plan-and-Execute标准的 ReAct 是单步循环想一步、做一步。对于复杂的多步骤任务这种逐层递进的效率偏慢而且模型容易在中途迷失目标。工程上更稳健的优化是 Plan-and-Execute 架构先把大任务分解成几个子步骤每个子步骤内部跑一个小 ReAct 循环。这种架构的好处是第一个 Planner 环节会把任务拆解成明确的三到四步后续每个步骤的执行不会被无关信息干扰。用户问题: 分析某城市的旅游建议 Planner 输出: 1. 查询该城市天气 2. 查询该城市主要景点 3. 根据天气和景点生成旅游建议 Executor 1: 查询天气 → 观察 → 继续查询景点 → 汇总 Executor 2: 依据前两个结果生成答案这种模式现在被很多 Agent 框架采用我自己的工具链中也从纯 ReAct 迁移到了 ReAct Planner 的组合方案。5.2 引入反思机制ReflexionReAct 循环最大的短板是“一条路走到黑”一旦前面某一步推理错了后面很难纠正。我做了个很小的改进在每轮循环的观察阶段额外让模型做一个“当前推理是否合理”的自检如果发现前置信息矛盾就主动回退修正。def validate_reasoning_chain(self, history): 检查推理链是否自洽 recent_steps extract_recent_steps(history) contradictions find_contradictions(recent_steps) if contradictions: return {valid: False, reason: contradictions} return {valid: True}这个自检不是每轮都调模型做的那样开销太大。我设了一个规则当工具调用链长度超过 4 步时强制触发一次自检。这个机制能在相对低成本的前提下避免 Agent 越走越偏。5.3 多 Agent 协作与专用循环在实际项目中单一的 ReAct Agent 很难应付所有问题类型。我把不同的推理模式拆成了多个专用 Agent一个负责信息检索型任务一个负责数学计算型任务一个负责代码执行型任务。每个 Agent 内部的 ReAct 循环针对其领域做优化比如代码执行 Agent 的工具集中包含代码解释器和文件写入器而检索 Agent 的工具集中包含搜索引擎和知识库。这样做的好处是每个循环都很专注Prompt 模板不用塞入所有工具的说明模型做选择时干扰更少正确率明显提升。6. 写在最后的经验总结我从零开始实现 ReAct 循环已经有段时间了踩过不少坑也积累了一些自己的心得体会。最核心的感受是ReAct 不是一个“写出来就能用”的算法而是一个需要工程细节打磨的系统。Prompt 的措辞、工具结果的处理方式、循环控制器的容错策略每一个细节都会影响最终效果。很多人在网上看到 ReAct 原理觉得简单自己实现后却发现跑不通根本原因就在于这些隐藏在细节里的工程问题。另外一个实践认知是不要迷信任何一种固定的 Agent 范式。ReAct 适合需要多步推理且每步依赖外部信息的场景但如果你的任务只是简单的知识问答直接调用模型就好根本不需要循环。工具调用不是越多越好每多一轮循环就多一次出错的机会。如果你现在正准备搭建自己的 Agent 系统我建议你先从最小的 ReAct 循环跑通不要一开始就引入复杂的框架。用我上面那个精简版本把全流程跑一遍然后在实际业务中逐步加上工具、加上上下文管理、加上反思机制。这样你能对每一个组件的运作原理有最直接的理解后续排错也会从容很多。最后分享一个小技巧我给循环控制器的每个环节都加了耗时统计在本地开发时可以直观看到每一步的耗时分布。实测下来大部分时间花在模型推理上工具执行反而是零头。如果你觉得 Agent 响应太慢优先考虑的是优化模型推理耗时而不是优化工具调用速度。
阅读完成 · 觉得有帮助?
咨询建站