如果你也在折腾Agent一定见过这种场景模型明明很聪明查资料的时候却像愣头青一样横冲直撞让它做个简单的任务判断它偏要选最笨的那条路走到底。这个问题我啃了很久直到把ReAct这套框架真正吃透才明白Agent缺的不是智商而是思考和行动之间的那个闭环。ReAct把推理Reasoning和行动Acting编成一条循环链让每一步决策都有迹可循。这篇文章是我从零摸爬滚打出来的经验总结不扯玄乎的概念直接拆到Thought、Action、Observation这三个齿轮的内部配合完整可跑的代码和排坑记录。适合刚接触Agent框架的同学也适合那些已经搭过原型、但总觉得Agent“不够聪明”的开发者。1. 先搞清楚Agent为什么从“聪明”变成“莽夫”1.1 只会动手不会动脑的“手脚派”很多初版Agent实现长这样把用户问题拼进提示词让大模型直接输出一个工具调用的结果然后把返回内容丢给用户。这种方案看起来高效实际上有一个致命问题——模型生成的是“一次性决策”它没有机会根据中间结果修正自己。举个例子。你想让Agent帮你查“最近三个月不同渠道的广告投放ROI对比”传统做法是让它一次性调用一个报表接口拿回一堆数字直接总结。问题出在哪如果接口返回的数据里有两个渠道的数据缺失或者报表结构和你预期不一致模型没机会发现这些异常。它只会把残缺数据当成完整数据一顿分析最后输出一个煞有介事但根本不准的结论。我管这叫“手脚派Agent”——充满了执行力但缺乏判断力。就像让一个实习生去打杂活是干了但干到什么程度、干得对不对全看运气。1.2 只会空想不行动的“学院派”后来又流行过一种思路既然直接调工具有问题那就让模型先推理。做法是在提示词里加上“请一步步思考”于是模型噼里啪啦输出一大段推理过程但没有调用任何外部工具。这同样有硬伤。模型的知识是有截止日期的它的推理再严密也拿不到实时数据。你让它推理“今天出门要不要带伞”它能煞有介事地分析出一堆气象学原理但就是不帮你查天气API。等于请了个只会纸上谈兵的谋士分析得头头是道落到实地上毫无用处。“学院派Agent”的问题不是不聪明而是它把推理和行动割裂了。推理再长也只是在模型已有的知识范围内打转没有新的信息输入思维链条再漂亮也等于原地空转。1.3 “谋士”的正确姿势边想边干ReAct这套框架的核心思想就是把上面两种分裂的能力缝合起来。它在模型的每一步输出里强制要求三个部分Thought思考解释当前这一步为什么这么做模型对全局的判断。Action行动决定调用哪个工具、传入什么参数。Observation观察工具执行之后返回的真实结果。模型看到Observation之后会基于这个新信息再次输出Thought再决定下一步Action如此循环直到它能给出Final Answer最终答案。这套机制的关键在于模型在每个决策点都拥有重新评估的机会。第一次想错了没关系Observe之后它会发现“哦原来这条路径走不通”然后自动修正。这就是从“莽夫”到“谋士”的本质转变——不是模型变聪明了而是它的每一步行动都被挂上了反馈回路犯了错也来得及回头。提示ReAct里的Thought不是给用户看的“思考过程展示”它是给模型自己看的决策依据。所以它必须真实、具体而不是“好的我将按步骤进行分析”这种废话。2. ReAct的核心机制拆解三个齿轮怎么咬合2.1 提示词模板整个框架的地基接触ReAct之后我才明白一件事ReAct不是一个需要安装的库而是一套提示词工程加循环解析策略。它的灵魂就在提示词模板里。一个标准的ReAct提示词模板长这样你是一个智能助手你可以使用以下工具来帮助你回答问题 - search(query): 搜索互联网信息适用于查询实时数据、新闻、百科等内容。 - calculator(expression): 计算数学表达式例如 calculator((35)*2)。 - get_time(): 获取当前日期和时间。 你的回答必须严格遵循以下格式 Thought: 描述你当前的想法和推理过程。 Action: 工具名称必须是上述工具列表中的一项。 Action Input: 工具对应的输入参数必须是合法的JSON字符串。 当你有足够的信息回答用户问题时请输出 Thought: 我已经获得了足够的信息。 Final Answer: 给用户的最终回答。 注意一次只能执行一个Action必须等待Observation返回后再进行下一步。这里每一步都藏着细节。为什么要强制要求先写Thought再写Action因为如果你允许模型直接输出Action它很容易跳过思考直接蒙一个工具又变回“莽夫”。先逼它想一步再让它做一步错误率会显著下降。Action Input为什么要求用JSON字符串这是为了在代码里稳定解析。如果你不加以约束模型可能输出“查询北京的天气”这种乱七八糟的格式解析阶段会让你头疼到怀疑人生。工具描述里的那句“适用于查询实时数据”也很重要。大模型是通过工具描述来理解工具的用途的描述写得含糊它就不知道该在什么时候用这个工具。把边界条件、适用场景直接写进描述里相当于给模型发了一份工具说明书。2.2 循环是怎么转起来的模板定好之后运行机制其实是一个简单的while循环。我画一条逻辑线把用户问题拼上提示词模板塞给大模型。模型返回一段文本形如“Thought: ... Action: ... Action Input: ...”。解析这段文本判断是否包含Final Answer。如果有循环结束把答案返回给用户。如果没有Final Answer提取Action和Action Input在代码里找到对应的工具函数并执行。把工具返回的结果拼成一个Observation字段追加到对话历史里再次发给模型。重复第2步直到模型给出Final Answer或者达到最大迭代次数。这看起来简单但有几个关键点要注意。第一Observation文本用什么前缀开头这个前缀要和模板里的格式严格一致。如果你的模板里写的是“Observation:”那代码里拼历史时也要用“Observation:”开头。模型看到这个标记就知道刚才的工具调用已经结束接下来该推理了。第二循环的终止不仅靠模型自觉还要靠代码兜底。模型有时候会陷入死循环同一个Thought翻来覆去地输出。所以必须设置最大迭代次数。我在项目里一般设置5到8轮超过就直接终止返回当前的部分结果或报错。第三模型输出的文本可能有多余的空格、换行、甚至标点符号。解析的时候要做容错处理最好是正则匹配而不是严格的字符串查找。2.3 这套循环凭什么比“莽夫式”稳定从直观感受上大家都觉得多轮调用大模型会更慢、更费钱。确实如此ReAct的每一步思考行动都要发起一次模型调用一个复杂任务可能要跑四五轮延迟肯定比一次性输出高。但这个代价换来的是任务成功率的质的提升。核心原因是ReAct把长任务的误差控制在了“单步决策”的范围内。一次复杂任务如果一口气完成中间任何一步出错都会污染最终结果而且这个错误是隐蔽的、难以察觉的。ReAct通过每步反馈把错误暴露出来模型自己就会修正。打个比方传统Agent是一次性闭卷考试交卷了才知道得分ReAct是开卷考试每做一道题就可以翻一次书、对照一次答案。同样是最笨的模型后者考出来的分数绝对高得多。我实测过一个差异很大的场景让Agent查某个月的销售数据按地域维度做排序并给出前三个地区贡献占比。用命令式Agent它经常直接把接口返回的原始数据全都列出来或者排序逻辑出错。用ReAct循环模型在Thought里自己会发现“这个排序不对我要在Action里加上排序字段重新取数”Observation返回后它再修正思路。这种纠错能力是命令式方案完全不具备的。3. 从零搭一个ReAct Agent完整实操记录3.1 准备环境和提示词模板下面这套代码是我实际在项目里用过的骨架去掉业务相关部分之后做了精简。不依赖任何特定的模型SDK抽象成fetch_llm函数你只要把它换成你想用的模型接口就能跑起来。先说工具集。为了完整演示我准备三个工具搜索、计算器、取当前时间。搜索用本地模拟数据做演示实际项目中你可以对接自己的搜索服务。import json import re import datetime def search(query: str) - str: # 实际场景中替换为真实搜索服务 if 天气 in query and 北京 in query: return 北京今天晴气温18~26摄氏度空气质量良。 if 天气 in query and 上海 in query: return 上海今天多云气温20~27摄氏度空气质量优。 return 未找到相关结果请尝试换个关键词。 def calculator(expression: str) - str: try: # 注意eval有安全风险仅供演示生产环境请解析表达式 return str(eval(expression)) except Exception as e: return f计算错误: {e} def get_time() - str: return datetime.datetime.now().strftime(%Y-%m-%d %H:%M:%S)工具集内部实现其实无所谓关键是每个函数都接收一个字符串参数返回一个字符串。这样一来Agent循环解析和执行就非常简单了——不管是什么工具都走同一个调用和返回通道。提示词模板直接采用第2节里的模板。我建议把工具列表动态生成这样以后加工具不用改模板只改tools字典就能同步更新到提示词里。TOOLS { search: search, calculator: calculator, get_time: get_time, } TOOL_DESCRIPTION - search(query): 搜索互联网信息适合查询天气、新闻、百科等实时内容。 - calculator(expression): 计算数学表达式输入如 calculator((35)*2)。 - get_time(): 获取当前日期和时间。 3.2 核心循环的Python实现循环逻辑我用一个类封装便于复用。class ReActAgent: def __init__(self, llm_func, max_iterations6): self.llm_func llm_func self.max_iterations max_iterations self.template 你是一个智能助手可以使用工具回答问题。可用工具 {TOOL_DESCRIPTION} 你的回答必须严格遵循以下格式 Thought: ... Action: 工具名称 Action Input: 工具入参(JSON字符串) 当有足够信息时请输出 Thought: 我已经获得了足够的信息 Final Answer: 最终回答 def run(self, user_query: str): history [{role: user, content: self.template.format( TOOL_DESCRIPTIONTOOL_DESCRIPTION )}] history.append({role: user, content: f用户问题{user_query}}) for step in range(self.max_iterations): response self.llm_func(history) print(f--- Step {step 1} ---) print(response) print() # 1. 检查是否给出最终答案 if Final Answer: in response: final response.split(Final Answer:)[-1].strip() return final # 2. 解析 Action 和 Action Input action_match re.search(rAction:\s*(\w), response) input_match re.search(rAction Input:\s*(.), response) if not action_match or not input_match: # 模型输出格式异常时给它一个观察结果让它修正 observation 格式错误未找到Action或Action Input请严格按照指定格式输出。 else: action_name action_match.group(1) action_input_raw input_match.group(1).strip() if action_name not in TOOLS: observation f未知工具{action_name}可用工具为{list(TOOLS.keys())} else: try: # 解析 JSON去掉可能存在的引号包裹 action_input json.loads(action_input_raw) observation TOOLS[action_name](action_input) except json.JSONDecodeError: observation Action Input解析失败请确保它是合法的JSON字符串。 except Exception as e: observation f工具执行出错{e} # 3. 把 Observation 拼接进历史继续下一轮 history.append({role: assistant, content: response}) history.append({role: user, content: fObservation: {observation}}) return 达到最大迭代次数未能生成最终答案。这段代码有几个细节值得单独说。解析Action Input时我用了json.loads。如果模型输出的是calculator((35)*2)这种带引号的字符串json.loads会解析失败。所以我在工具函数实现里直接接收字符串但模型输出时我还要求它传JSON字符串。有个取巧的办法如果json.loads失败尝试用ast.literal_eval或者干脆去掉最外层的引号再解析。项目里我通常写两套解析逻辑第一套严格JSON第二套宽松处理。观察异常时的处理也很有讲究。当模型输出格式错了不要直接终止程序而是把“格式错误”作为Observation塞回去让模型自己纠正。这个操作我一开始也没想到后来发现这是提升稳定性的关键一招——模型看到Observation是报错信息下一轮会老老实实按格式输出比你在代码里强行纠偏要自然得多。3.3 实测案例Agent怎么从“想错”到“想对”我用一个多步推理问题来测试这个循环问题如下“今天是哪一天北京和上海今天的温差是多少让Agent给出日期和温差结论。”第一轮模型输出Thought: 用户想知道日期和两地温差。我应该先获取当前日期然后搜索两地的天气。 Action: get_time Action Input: {}Observation返回“2025-06-15 14:30:00”。模型接着输出Thought: 我已经知道了日期。现在需要获取北京和上海的天气。 Action: search Action Input: {query: 北京今天天气}Observation返回北京的天气后模型再思考Thought: 拿到了北京的天气18~26度。还需要上海的数据。 Action: search Action Input: {query: 上海今天天气}最后一步模型发现两地温差可以算了于是输出Thought: 北京18~26度上海20~27度高低温差分别是8度和7度。平均温差为1度。 Final Answer: 今天是2025-06-15北京今天晴18~26度上海多云20~27度。两地平均气温相差1度。这个过程看起来平平无奇但其实每一个Thought都在指导下一步的Action。模型没有一次性把三个工具全调一遍而是按需逐个调用拿到一个新信息就更新一次判断。这就像剥洋葱一样一层一层接近答案。实际跑的时候你会遇到模型输出风格不一致的情况比如Action Input里带了多余文字、Thought里顺便把最终的答案也写出来了等。后面第4节我会专门写这些坑的排查方法。4. 实战中踩过的坑排查与优化实录4.1 循环不收敛同一个Thought反复出现这是我最早遇到的问题也是最让人崩溃的问题。模型每轮都输出差不多的Thought“我需要搜索更多信息来回答这个问题”但Action迟迟不执行或者执行了同一个搜索十几次。根因通常是提示词里没有强调“每轮必须推进”。模型把ReAct当成了普通的对话以为只要表达思考过程就能交差。解决办法是在模板里加一句“每一轮必须执行一个Action不要重复已经执行过的Action。”另外把temperature调低也有奇效我一般在0到0.3之间。我在代码里还加了一个去重逻辑如果连续三轮的Thought相似度超过阈值直接终止循环。简易版可以用“最后一次Action的输入是否和上一轮完全相同”来判断。这个兜底逻辑救了我很多次。4.2 上下文越塞越长直接爆掉Model的上下文窗口是有限的。ReAct每跑一轮就要把这一轮的Thought、Action加上Observation全量拼回历史里。如果你让工具返回一份超长的报表有两三轮迭代上下文直接就爆了。我的做法是给工具返回加一个“摘要层”。新闻搜索返回原始正文前先截断到500字以内报表工具返回前先做一次字段裁剪。在ReAct循环里Observation对模型而言只是“下一步决策的依据”它不需要看到全量细节只需要看到能支撑下一步判断的关键信息。如果任务确实需要完整大文本我建议先让模型生成一个摘要存到外部存储里然后把摘要塞回上下文完整内容等最终输出时再从存储里取。这一步对长文档场景几乎是必须的。4.3 工具调用抛异常Agent开始胡编工具出错在所难免但你得想想模型会怎么应对。如果某个工具抛了异常而你把异常信息原样塞进Observation里模型的常见反应是在下一轮Thought里道歉然后尝试换个工具或者重新请求。但也有部分模型会直接开始编造一个看似合理的答案把错误糊弄过去。我踩过最深的坑是某个数据接口报错模型在下一轮输出“根据我的知识库这个数据应该是……”——完全是在胡说八道。后来我在Observation里加了一行固定话术“如果工具持续报错请尝试更换策略或告知用户无法获取数据不得编造。”这行话对抑制幻觉有显著效果。另一个排查点是Action Input解析失败。模型经常会把输入写成带引号的形式比如Action Input: 北京天气而不是JSON。我的宽松解析逻辑是在json.loads失败之后尝试直接去掉首尾的引号和花括号再匹配开发者定义的参数占位。项目上线之后这套容错逻辑处理了大约三成的解析异常。4.4 ReAct常见问题排查速查表现象可能原因快速解法模型每轮只输出Thought不输出Action提示词未强调必须执行Action模板中明确“每轮必须行动”降低temperature同一个Action反复执行模型没有意识到已经查过同个信息加去重检测提示“不要重复已有查询”输出格式经常不标准提示词约束力不够或模型版本弱严格模板格式错误Observation回灌上下文超限报错每轮全量拼接历史工具返回过长截断Observation历史只保留最近N轮工具报错后模型开始编答案异常信息没引导模型正确处理Observation后追加“不得编造”提示超过最大迭代次数无结果问题太复杂或模型思路发散增大次数或引导模型拆解子任务5. 下一步怎么玩ReAct的变体与工程化建议5.1 ReAct和Plan-and-Execute怎么选ReAct不是Agent工程里唯一的框架思路。另一个常见方案是Plan-and-Execute先让模型一次性生成完整计划再逐条执行执行完一条就汇报结果最终汇总。这两套方案的取舍很直接。Plan-and-Execute适合任务步骤清晰、环境变化小的场景。比如“每天早上9点抓取数据、清洗、生成报表再发邮件”这个流程是固定的用计划先行模式执行效率高而且省掉了大量不必要的模型推理调用。ReAct则适合探索性任务比如“帮我分析这个竞品的定价策略”过程中需要不断搜索、对比、调整方向。这类任务你没法提前规划所有步骤因为每一步都可能发现意想不到的信息。ReAct的优势在于它的计划是隐式的、流动的分布在一个个Thought里随时可以被新Observation推翻。我个人的项目经验是如果任务步骤之间耦合度高前一步结果会决定下一步方向优先ReAct如果任务流水线特征明显Plan-and-Execute更稳、更快。两者也可以混用外层用Plan定方向内层对每个子任务用ReAct循环处理。这种组合我管它叫“定向巡航局部避障”实战里效果很不错。5.2 多Agent协作场景下ReAct怎么放多Agent系统里ReAct经常被用作单个Worker的内部行为逻辑。顶层有调度型Agent负责拆解任务下面每个Worker独立跑ReAct循环完成自己的子任务后把结果汇总给调度层。这种结构的好处是每个Worker只需处理自己领域内的工具集工具描述可以写得非常精确模型的决策负担大幅下降。比如一个Worker只负责搜索和抓取另一个Worker只负责数据分析它们各自的ReAct循环都短而高效不会出现一个Worker既要搜索又要写代码又要做报表的混乱局面。但多Agent协作也带来了新问题Worker之间传递中间结果时信息格式往往不统一。我在设计时给每个Worker的输出强制加了结构化字段比如summary、source、confidence。调度层只看这些字段做决策不直接读大段文本避免上下文里塞满无用的推理过程。5.3 给“莽夫”变“谋士”的几条工程建议最后把我折腾了大半年的经验浓缩成几条建议照着做能少走弯路。第一工具数量宁少勿多。ReAct每轮只选择一个工具执行工具列表太长以后模型需要花更多时间做选择而且选错的概率也会变大。我把单个Worker的工具数量控制在5个以内必要时拆多个Worker。第二工具描述要写清边界。我在工具描述里不仅写了用途还写了“什么情况下不要用这个工具”。比如搜索工具描述后加一句“不要用本工具查询用户私有数据”模型就会在Thought阶段主动放弃错误的工具调用。这比在代码里做一堆拦截要省事得多。第三限制过度思考。ReAct虽然是谋士路线但谋士想太多也不一定是好事。模型有时候会为了凑一个Thought格式输出大段无关推理。我后来在模板里加了“Thought必须简洁不超过两句话”的约束实测对减少token消耗和提升收敛速度都有明显帮助。第四做好成本控制。ReAct的模型调用次数是动态的一次复杂任务跑10轮也很常见。上线前一定要设置兜底的最大轮数和单轮最大token数。如果任务平均轮数超过预期优先优化工具设计而不是换更贵的模型。第五Final Answer也要校验。模型可能给出格式正确的Final Answer但内容根本无法回答用户问题比如答非所问。我在代码里加了简单的校验逻辑如果Final Answer长度小于5个字或者包含“无法回答”之类的字样会再追加一轮“请尝试用其他工具获取信息”的提示让模型再走一轮而不是直接结束。我自己在实际项目中最大的体会是ReAct真正的难点不在跑通Demo而在把每个环节的容错边界都补齐。一开始我总想着让模型一次答对后来发现更好的策略是“让它犯错但确保它有机会改错”。观察轮次越多模型的决策越靠谱但也越费钱这个度需要针对你自己的任务去调。如果你刚搭好一套ReAct Agent建议先用10个典型问题做回归测试记录每个问题消耗的轮数和失败点。不要急着上模型优化先把工具描述、模板格式、观察截断这三件事打磨到位收获往往比换更大的模型还要明显。这套框架的价值不在于它多新奇而在于它给了模型一种“反悔”的机会——而这恰恰是传统Agent方案最缺的东西。