
聊 ReAct 之前先抛一个判断Agent 圈子里大多数所谓“技术壁垒”拆到底就是一套小循环。我见过不少人一上来就铺一堆编排框架、记忆模块、多智能体网关结果最核心的 Thought → Action → Observation 都没想明白跑出来的 Agent 跟个运气型选手一样时灵时不灵改 prompt 全靠玄学。ReActReasoning Acting是 Agent 设计里绕不开的一种范式核心就一句话让模型在“推理”和“行动”之间循环——先想清楚我要什么再动手调用工具看到结果之后继续想下一步。这个设计解决了两个最基本的痛点模型只知道训练数据里的静态知识不知道外部世界的实时状态模型能说话能推理但不具备执行动作的能力。文章标题说了“三分钟说清楚”技术上确实不难难的是把它做稳。这篇文章先把循环本身讲透再给一套能直接跑起来的最小实现最后把我调 Agent 时踩过的坑一并说了。适合两类人一是刚接触 Agent、想快速建立正确心智模型的新手二是已经用框架写了几个 Demo但总觉得不可控、想回到底层搞明白原理的开发者。1. 先搞懂 ReAct 到底在解决什么问题1.1 纯思考的模型和纯行动的模型都走不远先看一个大前提大语言模型本质上是一个静态的知识压缩器加概率推理器。它能回答基于训练数据的问题能写代码、能总结文档但它没有实时感知能力。你问它“现在室外多少度”它只能根据训练数据里的统计规律去猜一个数猜对了算运气猜错了是常态。这就是纯思考路线Chain-of-Thought思维链的软肋模型可以想得头头是道但它没有传感器。反过来如果只让模型去调用工具不强制它先想清楚“为什么调用这个工具、结果如何影响下一步”链路非常脆弱。动作一旦选错后面全部跟着错而且没有纠错机制因为它缺少一个“我错了我要换个方式”的中间推理层。ReAct 的思路很直接把推理和行动耦合进同一个循环。推理负责决策行动负责改变外部状态并拿回新的观察。相当于把一个只会纸上谈兵的理论派变成了一个先动手、看反馈、再修正的实战派。这个概念用一个做菜的类比最好理解纯思考版让一个没做过菜的人看菜谱然后写一篇《如何炒一盘番茄鸡蛋》的作文。纯行动版让他闭着眼睛往锅里倒调料倒完直接出锅。ReAct 版正常人做菜的方式——看菜谱读取已有信息→ 下锅翻炒行动→ 尝一口观察→ 判断咸淡推理→ 决定加不加盐行动→ 再尝观察→ 确认味道对了最终回答。1.2 循环四步Thought、Action、Observation、Final AnswerReAct 的循环只有四个环节没有任何多余的东西Thought思考模型基于当前已知信息判断“我现在缺什么信息下一步该用什么工具去拿”。Action行动模型决定调用哪个工具并给出参数。这一步是模型输出结构化的动作指令。Observation观察工具执行完毕把结果返回给模型。这个结果会作为上下文的一部分交给模型做下一轮思考。Final Answer最终答案当模型觉得信息已经足够不再调用工具直接输出对用户的回答。循环的完整路径是Thought → Action → Observation → Thought → Action → Observation → ... → Final Answer。注意Observation 之后连接的一定是 Thought这一步保证了模型每次拿到新信息后都必须停下来想一步而不是无脑继续调工具。举一个具体例子。用户问“帮我查一下某个三线城市的天气然后建议我今天穿什么。”模型的第一轮输出会是这样Thought: 用户需要一个城市的实时天气信息我的训练数据里没有今天的天气必须调用天气工具。Action: get_weather{city: 遵义}工具返回 Observation: 城市遵义当前温度 22 度多云风力三级湿度 55%。模型拿到这个结果后进入第二轮思考Thought: 22 度多云风力不大体感舒适适合穿长袖 T 恤或薄衬衫。Final Answer: 建议穿长袖 T 恤配一件薄外套白天 22 度多云体感比较舒服但早晚可能稍凉。这个例子看着简单但它完整展示了 ReAct 的核心价值模型没有凭空回答而是先承认自己缺信息再用工具补上最后基于观察结果作答。这个链条如果只用纯 CoT模型会编一个“遵义今天气温 18 度”之类的假数据如果只调用工具而没有 Thought遇到极端情况就不知道怎么换工具。1.3 为什么是这套组合它和别的路线差在哪把 ReAct 和另外两条常见路线放在一起比较能更清楚看到它的位置路线推理能力环境交互纠错能力过程可见性纯 CoT思维链强无无高纯 Act工具调用弱强弱低ReAct强强强高纯 CoT 的问题在于模型无法感知外部世界实时性和事实性都无法保证。纯 Act 的问题是缺少推理层模型不知道为什么调这个工具调错了也没有自我修正的机会。ReAct 把两者的优势拼在一起Thought 给 Action 提供理由Observation 又反过来修正后续的 Thought形成闭环。后来社区里经常提到的“先规划再执行”Plan-then-Execute思路和 ReAct 是两种风格前者适合任务步骤比较确定的场景比如“先查 A 再算 B 再生成报告”后者更适合环境信息不确定、需要边做边看的任务比如搜索资料、多步查询、异常兜底。ReAct 之所以成为多数 Agent 实现的核心范式就是因为它对任务的不确定性容忍度最高。2. 手写一个最小 ReAct Agent全程只用几十行代码2.1 最小需要的四样东西很多人在接触 Agent 时第一句话就问“要不要上某某编排框架”。我的答案永远是第一次做不要。手写一遍你才明白Agent 的本质就是一个带工具的请求-响应循环所谓框架只是在循环外面包了一层更方便的壳。壳是好东西但不能代替你理解里面的核。一个最小可用的 ReAct Agent 只需要四样东西LLM 接口任意支持对话补全的模型服务都可以不要求多强的模型能输出文本、能遵循基本格式就行。工具字典每个工具包含三样东西——名字、描述、执行函数。模型不直接调用函数它只负责输出“我要调用哪个工具”真正执行的是你的代码。循环控制器负责把模型的输出发给工具、把工具的返回值装进上下文、判断什么时候该停下来。Prompt 模板把工具清单和输出格式约束写清楚这是整个 Agent 里影响最大的部分。很多人以为写 Agent 是在写业务逻辑其实写的是“模型行为的约束条件”。业务逻辑只是几行 try-except难点全在让模型每一次输出都恰好落在你设计的格式里。2.2 可以跑起来的完整代码下面这份代码我刻意做了最小化没有任何装饰逻辑只保留 ReAct 的主干。模型调用我用的是兼容/chat/completions的 HTTP 接口你换成自己用的模型服务时只需要改base_url、api_key、model三个变量。import json import re import requests class MinimalReActAgent: def __init__(self, tools, base_url, api_key, model, max_steps6): self.base_url base_url self.api_key api_key self.model model self.tools {t[name]: t for t in tools} self.max_steps max_steps def call_llm(self, messages): resp requests.post( f{self.base_url}/chat/completions, headers{Authorization: fBearer {self.api_key}}, json{ model: self.model, messages: messages, temperature: 0, }, timeout60, ) resp.raise_for_status() return resp.json()[choices][0][message][content] def run(self, user_query): system_prompt build_system_prompt(list(self.tools.values())) history [ {role: system, content: system_prompt}, {role: user, content: user_query}, ] for step in range(self.max_steps): reply self.call_llm(history) history.append({role: assistant, content: reply}) if Final Answer: in reply: return reply.split(Final Answer:, 1)[1].strip() action extract_action(reply) if action is None or action[name] not in self.tools: history.append({ role: user, content: 输出格式不正确。请重新输出第一行 Thought第二行 Action: 工具名{\参数\:\值\}。 }) continue try: obs self.tools[action[name]][fn](**action[args]) except Exception as e: obs fERROR: {type(e).__name__}: {e} if len(obs) 2000: obs obs[:2000] ...[truncated] history.append({role: user, content: fObservation: {obs}}) return Maximum steps reached, no Final Answer. def extract_action(text): 从模型输出中解析 Action 行兼容各种格式噪声。 m re.search(rAction:\s*(\w)\s*(\{.*\})?, text, re.S) if not m: return None name m.group(1) args {} if m.group(2): try: args json.loads(m.group(2)) except json.JSONDecodeError: args {} return {name: name, args: args} def get_weather(city): 示例工具模拟天气查询返回结构化结果。 return json.dumps({ city: city, temperature_c: 22, condition: 多云, humidity: 55, }, ensure_asciiFalse) def calculate(expr): 示例工具表达式计算。演示专用生产环境请用安全解析方案。 try: return str(eval(expr, {__builtins__: {}}, {})) except Exception as e: return fERROR: {e} if __name__ __main__: tools [ {name: get_weather, description: 查询指定城市的实时天气返回温度、天气状况、湿度。city参数使用中文全名。, parameters: {city: string}, fn: get_weather}, {name: calculate, description: 计算数学表达式支持加减乘除和括号。, parameters: {expr: string}, fn: calculate}, ] agent MinimalReActAgent( toolstools, base_urlhttps://your-endpoint.example/v1, api_keyYOUR_KEY, modelsmall-fast-model, ) print(agent.run(帮我查一下西安的天气然后算一下 22 摄氏度等于多少华氏度))代码逻辑拆开看就三条线主循环for step in range(self.max_steps)控制最大步数防止模型钻进死循环。每轮把模型输出追加到history保证多轮对话上下文连续。动作分发extract_action从模型文本里解析出工具名和参数然后查self.tools拿到执行函数。如果模型输出不符合格式就补一条提示让模型重新输出而不是直接崩溃。Observation 回传工具执行结果以Observation: xxx的形式作为用户消息塞回上下文。这一步是 ReAct 循环的关键——模型看到的不是 Python 函数的返回值而是“对话里的新消息”这样它才能把工具结果当成新的已知信息继续推理。需要注意演示代码里的calculate用了eval仅用于教学生产环境必须换成安全表达式解析或者调用计算库否则会有代码注入风险。工具执行函数要自己做异常兜底因为模型给参数经常不按套路出牌。2.3 Prompt 才是这个 Agent 的灵魂循环代码写熟之后Agent 的效果几乎全部由 prompt 决定。同一个循环给模型的工具清单和格式约束写得清楚它就是靠谱的 Agent写得笼统它就是瞎猜的鹦鹉。下面是我实际在用的模板def build_system_prompt(tools): tools_json json.dumps( [{name: t[name], description: t[description], parameters: t[parameters]} for t in tools], ensure_asciiFalse ) return f你是一个运行在 ReAct 循环中的 Agent。你可以使用以下工具 {tools_json} 输出格式严格遵守 - 如果需要使用工具只输出两行 Thought: 你基于当前信息的思考 Action: 工具名{{参数: 值}} - 如果已有足够信息回答问题只输出一行 Final Answer: 你的完整回答 规则 1. Thought 和 Action 必须成对出现一行一个。 2. 工具执行结果会以 Observation 开头作为下一条消息发给你你基于观察继续思考。 3. 不要重复调用同一个工具且参数完全相同除非 Observation 给出了新信息。 4. 回答必须基于 Observation禁止编造数据。 5. 如果某个工具连续两次返回 ERROR换一个工具或换一种参数不要硬试。这里面每一条规则都是踩坑换来的逐条说工具列表用 JSON 展示模型的训练数据里见过大量 JSON 格式它理解成本最低。“只输出两行”是硬性限制模型在格式约束强的情况下远比自由发挥可靠。第 3 条“不要重复调用同一个工具且参数完全相同”非常关键。实验证明不加这条模型经常会在 Observation 信息不足时反复打同一个电话像个卡住的机器人。第 4 条“禁止编造数据”是对抗幻觉的第一道防线。第 5 条是给模型一个“失败后的行动建议”否则模型遇到 ERROR 会懵然后开始瞎编。2.4 文本协议和工具调用接口怎么选上面采用的是“文本协议”的方式也就是要求模型输出一段带标记的文本再用正则解析。这是 ReAct 论文最早的样子优点是模型无关——只要是能听话输出格式的聊天模型都能跑这套流程调试时每一轮 Thought 都暴露在对话历史里问题出在哪一目了然。另一种方式是直接用模型服务商的“工具调用”接口很多平台叫 function calling 或 tool calling。这种方式不需要模型输出文本再解析API 层保证给你结构化的工具名和参数解析稳定性更高。它适合生产环境尤其是 Agent 的工具数量多、参数复杂、对容错要求高的场景。我的建议是学习和调试阶段先用文本协议因为你能完整看到模型的推理链成熟之后再切换到工具调用接口稳定性更好。两者不是互斥的最佳实践是主干用工具调用、日志里同时记录模型的推理摘要。3. 让它稳定跑起来的配置和经验3.1 工具设计模型是“照着说明书挑工具”的Agent 里的工具本质上不是函数而是给 LLM 看的“说明书”。模型看不到你函数的内部实现它只能根据工具描述来决定用不用、什么时候用。所以工具描述写得清不清楚直接决定模型选得对不对。我归纳了工具设计的三层原则第一描述要具体到参数级别。不要写“查询天气”要写“查询指定城市的实时天气返回温度、天气状况、湿度city 参数使用中文全名”。模型不知道你的代码约定它只会照着描述里的字面意思去猜。描述里的信息越细猜错的概率越低。第二参数要少而准。每个工具暴露给模型的参数最好控制在两三个以内。参数太多模型需要理解每个字段的含义出错的概率会指数级上升。如果确实需要复杂参数可以用一个结构化的 JSON 对象整体传进去而不是拆成七八个散装字段。第三输出长度要可预估。工具返回的内容会直接塞进模型上下文如果返回一个大段 HTML 或者巨型 JSON一两轮循环就能把上下文撑爆。工具在设计时就要考虑截断——搜索引擎只返回前十行、数据库查询只返回前五十条宁可让模型多查一次也不能一把梭全量数据。项目模糊描述坏精确描述好天气查询查询天气查询指定城市的实时天气返回温度、天气状况、湿度。city 参数用中文全名如“成都”网络搜索搜东西用搜索引擎返回结果最多 5 条每条包含标题、链接和 20 字摘要query 使用简洁关键词数据库查询查数据库查询某张订单表中近 30 天的订单总额返回订单 ID、金额、下单日期按金额倒序最多 20 条3.2 三个关键参数最大步数、温度、上下文预算跑 Agent 时最常调的就是三个参数每个都有明确的选择逻辑。最大步数max_steps单个任务最合理区间是 3 到 8。步数太少多跳任务根本走不完步数太多模型在后期会开始“遗忘”早期信息而且每多一步都是 token 消耗。设定原则是先按任务的最长路径估算一个值再往上加两步余量。比如一个任务需要“查客户 → 查订单 → 算金额 → 生成提醒”四条链路需要四步max_steps 设 6 比较合理。如果模型经常跑到第 5、6 步还没出结果问题往往不在步数而在于任务拆分太粗或者工具接口设计不合理。温度temperature这种推理型循环推荐直接设 0。Agent 不是文案生成器你要的是可复现的工具选择和判断。温度一高同样上下文里模型可能输出不同的 Action一次是查天气下一次变成了算数。生产环境不建议超过 0.3。上下文预算一个粗略计算公式——每轮模型输出Thought Action消耗约 120 到 200 tokenObservation 平均 200 到 1000 token取决于工具。假设一个 6 步任务平均每轮 Observation 500 token加上 system prompt 的 800 token总消耗约 6×(150500)800 ≈ 4700 token8K 上下文完全够用。但如果 Observation 单轮就 2000 token6 轮下来直接破 12K很多模型就扛不住了。这就是为什么工具输出必须截断从源头控制。参数推荐值调整倾向max_steps6任务链路长就加到 8超过 10 说明拆分有问题temperature0最高不超过 0.3再高不可控上下文窗口8K~16KObservation 偏长时优先截断工具输出而不是换大窗口3.3 生产环境还得补上这几样东西Demo 和生产的差距从来不在于算法高明而在于多做了多少防御。全量日志每一轮循环的模型请求、原始回复、Action 解析结果、工具返回值全部落盘。没有日志Agent 一出问题就是黑盒你想修都不知道从哪下手。失败重试API 超时、网络抖动、工具执行报错都要有重试策略。注意重试上限必须小于 max_steps否则重试本身就把循环耗尽。预算熔断循环里累计 token 消耗超过预设值直接终止返回当前已有的中间结果。防止一次复杂查询把单量大模型调用打到天价。人工确认支付、删除、发送消息这类高风险动作必须在 Action 执行前插一道人工确认闸口。这在工程上不是妥协是基本常识。4. 调试 Agent 时我踩过的那些坑4.1 模型不按格式输出最经典的现象你让它输出两行它给你输出一大段“好的让我来帮你查询一下……”然后就是没有 Action。还有时候 Thought 和 Action 挤在一行或者 Action 后面跟的不带花括号。原因通常是模型对格式要求理解不够“严格遵守”四个字对很多模型就是浮云。这里有两个有效手段第一在 system prompt 里加一个 few-shot 示例也就是给一个“输入 → 正确输出”的完整样例。光说“按格式输出”没用给例子最管用。第二解析失败后不要直接退出把错误提示塞回上下文让模型重新输出。我的代码里就是这个处理方式——格式错了就告诉它“你错了重新来”给它一次改过的机会。生产环境如果频繁出现格式问题不用再纠结 prompt 了直接换工具调用接口。API 层提供的结构化输出比任何正则都可靠。4.2 同一个动作反复调用这个坑我遇到太多次了。模型第一次调用搜索工具拿到了结果但它觉得结果不够“完美”于是又用一模一样的参数搜了一遍拿回来的还是同样的结果它再搜一遍直到 max_steps 耗尽。问题根源有两个一是 Observation 里没有足够的“新信息”让模型判断该停止二是模型缺少“重复无效”的自我感知。我在 prompt 里的第 3 条规则就是干这个事的除非 Observation 给了新信息否则不要重复调用。正面例子是第一次搜索返回了三条结果模型觉得不够这时候应该换个搜索词而不是原地重复。代码层面还有一个防御工具层做去重。同一个工具的同一个参数组合第二次执行时自动在返回值里标注“本次调用与第 N 次相同结果相同请更换参数或工具”模型看到这句话通常会换思路。4.3 Observation 太长上下文被撑爆第三种高频事故是工具返回了一个巨型 JSON直接把后续对话淹没。模型早期的信息被挤占开始“失忆”回答牛头不对马嘴严重一点 API 直接报上下文超限。这个问题的解法要分两层。第一层是源头限制工具内部做截断和精简只返回必要的字段。第二层是在循环代码里强制加保护像我上面写的if len(obs) 2000: obs obs[:2000] ...[truncated]无论如何不让单轮 Observation 撑爆预算。真正需要长文本的场景就设计成让模型先调用“摘要工具”把长文压缩成几个要点再进下一轮循环。4.4 模型很自信地给出假答案最危险的坑是模型 Thought 写得头头是道Action 也正常执行了但最后给出的答案跟 Observation 里的数据完全对不上。比如 Observation 显示温度 22 度模型 Final Answer 写“今天最高气温 35 度请穿短袖”。这种幻觉在低温度下也会发生小模型尤其严重。我的防御心态是这样的你永远不会得到一个 100% 不幻觉的模型所以要在流程上让幻觉承担代价。具体做法是在 system prompt 里反复强调“必须基于 Observation 回答如果缺少信息就继续调用工具或者直接告诉用户你不确定”。高危场景还要加最后一公里校验——从 Observation 里抽取关键数字或字段和 Final Answer 做比对对不上就打回重试。4.5 问题速查表现象优先排查方向治本手段不按格式输出查看原始回复是否包含 Action 行few-shot 示例、解析失败重试、换工具调用接口同一个动作反复调用检查 Observation 是否包含新增信息prompt 加禁止重复调用规则、工具层去重上下文被撑爆看工具返回值的字符长度源头截断、循环里强加长度保护、加摘要工具答案与 Observation 矛盾检查 Final Answer 生成轮的上下文system prompt 强调必须基于观察、关键字段比对跑完 max_steps 没出结果看每一轮 Thought 是否在原地打转把任务拆细、减少工具参数、调整工具描述最后分享一条我自己被坑出来的经验不要一边想算法一边铺框架。第一次做 Agent就用两个工具、几十行代码把最小闭环跑通然后打开日志一行行看模型每一步有没有按套路走。只有把“思考-行动-观察”这个循环打正了再去研究记忆、规划和多智能体那些高级话题才有意义。工具永远可以加框架随时可以换但这个最小循环一旦走顺了后面所有东西都是在这一层地基上往上搭。