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

从零实现ReAct Agent:大模型推理与工具调用的实战指南

从零实现ReAct Agent:大模型推理与工具调用的实战指南 ★ FEATURED ARTICLE
1. 为什么单纯的思维链不够用ReAct的出发动机我第一次认真研究Agent时心里一直有一个别扭的感觉大模型明明很聪明能写诗、能解数学题、能答常识问题但一旦让它干点实事比如帮我查个实时股价、做个加减乘除之外的计算、把某个操作在本地环境真正执行一遍它立刻就露馅了。原因很简单——模型的知识是静态的它的世界里没有执行这个概念。它可以描述怎么做但它做不了。这种落差是所有想往Agent方向进阶的人都会撞上的第一堵墙。当时我第一个想法是那用思维链Chain of ThoughtCoT不就行了吗让模型一步步思考把问题分解比如首先我要查天气然后决定要不要带伞最后写一封出行通知。这看起来已经很接近了但问题在于CoT只能产生推理轨迹它不会和外界交互。推理的终点还是推理模型没有一个机制去调用真实工具、接受真实反馈、根据反馈修正下一步。换句话说思维链解决的是怎么想的问题ReAct解决的是怎么想而后做、做了再想的问题。ReAct这个思路不是凭空冒出来的。它的核心洞察特别朴素既然模型既有推理能力又可以通过文本指令去调用工具那么就让推理和行动交替执行好了。模型每走一步先思考当前状态、决定需要什么信息然后输出一个行动系统执行这个行动并把结果Observation返回给模型模型再基于这个新信息继续思考。整个过程就在这条思考—行动—观察的闭环里循环直到模型认为问题已经解决。我当时读到这个思路时第一反应是这有什么稀奇的不就是把问答系统加个循环吗但真正动手去实现一个最小版本之后我才意识到魔鬼全在细节里。你如何告诉模型该输出Thought还是Action了你如何保证模型的输出能被程序稳定地解析工具返回一大段乱七八糟的文本时模型怎么从中提取关键信息这些问题在论文里只是一句话在实际代码里却要一行一行调。这篇文章我就把自己从零实现ReAct Agent的全过程摊开讲包括那些让我抓狂的坑。2. Thought/Action/Observation三要素一次完整循环的内部拆解2.1 三条核心语句三个角色ReAct的循环可以用三条带格式的文本记录来概括Thought、Action、Observation。这里的Thought不是模型内心的意识流而是它对外输出的一段推理轨迹以思考开头Action是它决定做的具体动作通常包括动作名称和参数Observation是工具执行动作后返回的结果。程序每次看到Action就去执行对应的工具然后把结果组装成Observation填回对话历史里模型再基于历史继续生成下一轮Thought。这个过程很像一个实习生干活先想清楚要什么信息Thought然后打电话给相关部门问Action部门把资料发回来Observation实习生继续判断下一步怎么办。没有Observation的Agent就像打了电话但不听对方说话只顾自己往下念稿子没有Thought的Agent就像无头苍蝇乱拨号码连自己在做什么都不知道。只有三个环节全部打通Agent才具备根据现实反馈动态调整的基本素质。2.2 为什么观察这一步经常被忽略很多教程讲ReAct时重点都放在Thought和Action上把Observation当成一个可有可无的回显。我自己实验后可以负责任地说Observation的设计直接决定了Agent智商的上限。模型并不具备真实的感知它只能通过文本反馈来理解这个世界现在变成什么样了。如果工具返回的结果含糊、字段缺失、格式混乱模型就只能在错误的地图上继续推演越走越偏。举个例子我让Agent查询某个产品的库存状态工具返回的原始结果是一大段JSON里面有仓库ID、SKU、更新时间、备注字段。如果没有对Observation做预处理模型很容易被备注字段里的无关文本带偏重点放到了一句不知道谁写的今天发货有点慢上而忽略了真正需要的库存数值。后来我的做法是在工具函数返回结果之前就把它整理成对推理最有用的摘要句式比如SKU-10023库存量为37件状态正常。这相当于替模型把原始噪音过滤掉只留关键信息。Observation的质量才是Agent稳定性的隐形瓶颈。2.3 ReAct和先规划再执行有什么本质区别还有一个我一开始没想清楚的问题ReAct和常见的Plan-then-Execute先规划、再执行方案到底有什么区别两者的差别很像照着施工图干活和边施工边改图。Plan-then-Execute会让模型第一轮就输出一个完整的分步计划然后按照计划逐步执行工具执行过程中一般不允许大改计划。这种方式在需求明确、环境稳定的场景下效率很高但只要中途任何一个步骤的假设破灭了整套计划就只能作废重来。ReAct则相反它走一步看一步。模型不需要在一开始就预判所有未来状态它可以根据每次Observation的结果实时修正下一步行动。这种设计换来的是更强的容错性和对不同任务形态的适配力代价是更多的交互轮次、更大的Token消耗和更长的响应时间。所以不存在哪个方案绝对更好只存在哪个方案更匹配你手里的问题。ReAct更适合那种问题边界模糊、工具返回结果不可预判的场景比如在多个信息源之间交叉核验的问答任务。3. 手把手实现一个最小ReAct Agent代码全解3.1 整体设计四段式Prompt的构思动手写代码之前我先把整个系统的边界画清楚。一个最小可用的ReAct Agent至少需要四样东西一份任务描述、一套工具清单、一个循环控制逻辑、一个格式化解析器。工具清单我用模拟函数实现不让它依赖任何外部框架这样你可以直接跑通理解核心机制后再替换成真实API。核心的提示词模板我设计成四段式第一段固定说明你是一个需要反复推理和行动来回答问题的人工智能第二段给出可用工具列表每个工具包括名称和描述第三段用一个示例演示Thought、Action、Observation的循环格式第四段开始正式的提问和历史记录。这个顺序不能乱尤其是示例段落它相当于给模型做一次格式示范能不能稳定输出规范格式很大程度取决于示范样例够不够清晰。3.2 工具模拟一个搜索API和一个计算器工具这块先写两个最通用的一个用来搜索信息一个用来执行计算。搜索工具内部用一个字典模拟结果计算工具就直接调用Python的表达式求值。注意真实项目里绝不建议直接用eval做计算会有严重的安全风险这里只是演示循环机制才这么干的你换成安全函数即可。def search_info(query: str) - str: # 模拟搜索引擎返回结果 data { 某产品价格: 该产品官方建议零售价为799元, 某公司财报: 该公司第二季度营收同比增长12%, 天气: 某城市今日多云转晴气温21到28摄氏度, } return data.get(query, 抱歉没有找到该查询的相关结果。) def calculator(expression: str) - str: try: result eval(expression) # 演示用实际项目禁止直接eval return f计算结果: {result} except Exception as e: return f计算表达式有误错误信息: {e}注意到每个工具函数都返回字符串这是有意为之。Observation的统一类型就是字符串模型只认文本。无论你背后是数据库、HTTP API还是本地文件交到模型手里的一定得是一段它能够直接放进上下文里继续推理的干净文本。3.3 核心循环代码解析、执行、追加历史循环控制是整个Agent的中枢神经。每轮循环做四件事把历史记录和用户问题拼接成完整提示词发送给模型拿到模型输出后按正则规则提取其中的Thought和Action如果Action存在就去执行对应工具把Observation追加到历史里如果Action不再出现说明模型已经给出了最终答案循环结束。import re history [] MAX_STEPS 5 def parse_model_output(text: str): thought_match re.search(r思考[:](.*?)(?行动[:]|$), text, re.DOTALL) action_match re.search(r行动[:](.*), text, re.DOTALL) thought thought_match.group(1).strip() if thought_match else action action_match.group(1).strip() if action_match else return thought, action tools {搜索: search_info, 计算: calculator} def run_agent(question: str): history.append(f问题: {question}) for step in range(MAX_STEPS): prompt build_prompt(history) response call_llm(prompt) # 这里封装你的模型调用 thought, action parse_model_output(response) if not action: print(最终答案:, response) return # 解析动作名与参数格式约定为 工具名(参数) action_pattern r^(\w)\((.)\)$ match re.match(action_pattern, action) if not match: history.append(f思考: {thought}\n行动: {action}\n观察: 行动格式非法请重新输出) continue tool_name, tool_arg match.group(1), match.group(2) if tool_name not in tools: history.append(f思考: {thought}\n行动: {action}\n观察: 找不到工具{tool_name}请从可用工具中选择) continue result tools[tool_name](tool_arg) history.append(f思考: {thought}\n行动: {action}\n观察: {result}) print(已达最大轮次停止迭代)这里的call_llm我故意留成接口不绑定具体模型。你换成任何支持对话补全的模型接口都可以只要把history列表按对话消息的格式组装进去就行。真正影响成败的往往不是调哪个模型而是build_prompt里的历史整理方式。3.4 我踩过的格式化解析坑解析这一步看起来只有几行正则却是我整个开发过程中翻车最多的部分。模型经常不按剧本出牌比如它会把Thought和Action写在同一行会在Action后面偷偷多写一句解释甚至偶尔直接输出中文的思考和行动之间的冒号变化。为了适配这些情况我反复调整了几轮正则最终采用了一个更宽松的解析策略不要求正则一次性完美匹配而是先用一个宽松的模式提取候选文本再按顺序拼接回历史记录。这在工程上叫尽量宽容输入严格控制输出模板。另外一个教训是尽量在每次循环都把模型完整的原始输出追加进历史而不要只追加解析出来的Thought和Action。因为原始输出里可能包含它对一个问题的自我纠正比如突然意识到上面的计算不对这种自我纠错文本如果被过滤掉模型就丧失了从错误中往回拉的能力。保留原文模型才能看到自己上一轮到底说了什么。4. 从Demo到可用输出解析、工具描述与循环控制4.1 工具描述写得好不好决定了Agent是牛还是障这是我最想强调的一点在ReAct Agent里工具函数本身的实现固然重要但模型完全看不到你的代码它唯一能接触到的是你在Prompt里写的工具描述。描述写得含糊再好的工具函数也发挥不出来。我早期给搜索工具写的描述只有一句话搜索工具参数为查询字符串结果模型经常把参数传成半句话、带标点、甚至把整个问题塞进去。后来我总结出工具描述的五要素工具名称、功能边界、参数格式、一个成功示例、一个失败返回语义。比如搜索工具的合格描述是搜索工具适用于查找实时信息或静态知识库中没有的信息。参数为简短关键词不要使用完整的自然语言问句。示例搜索(某产品价格)。当无结果时会返回抱歉没有找到相关结果此时请换一个关键词重试不要重复相同搜索。这样写下来模型对何时调用、如何传参、失败后干什么都有了明确预期调用质量立刻好了不止一个档次。4.2 轮次上限与终止条件安全网必不可少ReAct循环如果不加约束模型完全可能陷入搜索-没结果-再搜索-还是没结果的无限循环。Token在烧用户在等系统却毫无进展。因此轮次上限不是可选项而是必需的。以我的经验通用问答场景5到8轮足够带多步骤验证的复杂任务可以放宽到10轮但不要太多轮次越长上下文越臃肿模型的注意力越容易涣散总成本也在同步膨胀。更优雅的终止条件之一是当模型连续两轮输出的Action完全相同时强制终止。这通常意味着它已经弹尽粮绝——所有可尝试的行动都用过了再试也只是重复劳动。此时程序可以直接提示无法通过当前工具完成该请求这比让它无意识空转要体面得多。另一个我认为很有用的设计是给Observation设置长度上限超过一定字符就截断并追加一句结果过长已截断请聚焦关键字段。这能防止某个工具一次性返回巨大文本彻底压垮模型的上下文窗口。4.3 历史消息的组织方式与Token成本ReAct的长期记忆完全靠上下文堆叠每一轮循环历史记录都会增加几百到上千Token。10轮下来光历史就有几千Token这还没算系统提示和工具描述。很多人在Demo里跑得很欢一上生产就被账单吓一跳。控制Token的手段我常用三条尽量精简工具描述、把超长Observation摘要化、以及定期压缩前面轮次的历史只保留关键结论而丢弃中间过程。后面这条操作起来比较讲究压缩的时机和策略要反复试验压缩太狠会丢掉推理线索压缩太晚则浪费空间。我的建议是先跑通不加压缩的版本再根据实际轮次分布去优化。5. 实测最常踩的坑死循环、误调工具与虚假推理5.1 死循环的三种典型形态与应对死循环不只有反复搜同一个词这一种。我实测下来模型还有一种更隐蔽的死法它每隔几轮换一个新工具但每次换工具前不做任何基于Observation的推理变化像是为了执行而执行。比如先搜产品价格、再算一遍零售价、然后回去再搜那个产品价格。这种循环从日志上看每一步都不一样但实际上毫无进展。要抓出它必须把每轮的思考文本也打出来看才能发现模型其实一直在原地兜圈子。针对这类问题我加了一条观察规则如果连续三轮的Thought关键词相似度极高就当死循环处理。5.2 工具参数错乱模型把它当成了人类口述另一个高频问题是模型传参太口语化。它不会老老实实按格式传参而是把参数写成帮我查一下某产品价格吧这种完整句子。我的搜索函数用字典匹配整句输入几乎必然返回没有找到。解决思路有两条一是把工具函数改造成支持关键词抽取对参数先做一轮清洗提取核心实体二是强化Prompt里对参数格式的约束甚至给出正反两个示例。我把第一种方案叫工具侧容错第二种叫提示侧约束两边的效果不太一样——工具侧容错能提升鲁棒性但清洗逻辑写复杂了会增加维护负担提示侧约束更干净但对模型的服从度有要求。5.3 虚假推理模型可能在编造Observation后的反应最让我警惕的是模型的虚假推理。模型在看到Observation前就已经有了先入为主的答案于是它在阅读理解Observation时会强行把它往自己的结论上靠。比如Observation写该产品当前无货预计三周后补货模型却在Thought里写根据最新信息该产品目前货源充足可以正常购买。这种错误不是你Prompt写得不够好而是模型本身存在确认偏误它太想给你一个答案了。针对这种问题我能给的有效手段有限但有用第一在系统提示里强调观察结果代表事实必须严格基于观察修改你的结论第二在Observation前加上醒目标记比如一行分隔符让模型更容易把外部事实和自己脑补的推理区隔开第三对高风险场景设置矛盾检测当模型结论与Observation明显冲突时强制它重新阅读Observation。虽然这也不能百分之百拦截但至少能减少一大半错误回答。6. ReAct不是银弹边界、成本与替代方案6.1 成本和延迟是绕不开的现实ReAct对Token的消耗远高于单次问答这是它的原生短板。一个复杂问题跑下来模型可能要调用五六个工具、阅读五六轮Observation上下文累积上万Token单次任务的成本可能是普通问答的十倍。如果你的应用面临高并发、低延迟的要求就必须在每一层做优化精简Prompt、限制轮次、给观察结果做摘要甚至考虑把部分常见问题直接写成规则脚本用不上Agent的地方坚决不让Agent上。6.2 什么时候直接写规则比ReAct更香我接手过一个内部信息提取需求看起来很适合Agent因为要处理不同格式的报告。但后来我发现报告的字段虽然不同格式规律却很强用正则加关键字规则就能解决处理速度是毫秒级成本几乎为零。这件事给我很大触动技术选型应该以任务复杂度为准而不是以技术热度为准。如果你面对的是一组固定流程、边界清晰的操作直接写代码判断比让模型自由发挥要可靠得多。ReAct的价值在于应对非结构化、不可预测的任务而不是包打天下。6.3 从ReAct往前走一步规划器与记忆体如果你已经熟练掌握了ReAct下一步自然会有两个方向一是给Agent增加规划模块让它在启动前先输出一个粗略计划再在每轮循环中用Observation修正计划二是给它加上持久化记忆把中间轮次的关键结论写入外部存储避免上下文被撑爆。这两个方向各有各的坑规划模块容易和ReAct的循环角色重叠记忆体的写入策略则会直接影响模型的后续推理依据。我建议先把ReAct循环调扎实再慢慢引入这些增强组件不要一步到位。6.4 关于上下文设计的一个补充感悟最后分享一条我用血泪换来的经验ReAct Agent的Prompt不要写得像说明书要写得像带一个聪明但容易飘的实习生。你需要在约束和自由度之间找到平衡太松它乱跑太紧它丧失灵活性。我通常会给足示例但不会过度列举禁止事项而是用一个正例和反例暗示边界。模型对示例的模仿能力很强你想要的格式和节奏都可以通过精心构造示例来传递。这比在提示词里写一百句不要做某某有效得多。
阅读完成 · 觉得有帮助?
咨询建站