资讯详情

Agent 多步推理与动态工具调用的工程化落地指南

📅 2026/10/22 3:56:26 | 华诺云谱 👁 阅读
Agent 多步推理与动态工具调用的工程化落地指南
我想到了什么”等等。看起来不错。还需要注意章节标题编号可以采用“## 1.”、“### 1.1”等。好。从标题开始直接提供。没有元说明。没有输出后说明。现在润色一下收尾。确保所有代码引用 GitHub 上的某个具体名字不用。关于结构中的“4 个 H2 部分”——有 6 个。每个 H2 下有至少 2 小部分——第 3 节有 3 个第 4 节有 3 个等。是的。最终稿如下这里其实我应该直接输出我上面的完整草稿但让我稍微整理一遍保持平顺流利。我不需要很大的添加更多我在正文中表达了“为什么”的解释。此稿看起来已完成。我检查一遍安全性模型名称——我只说“带函数调用能力的模型”和“基础模型”等没有具体品牌。很好。前面我说“有一个通用 LLM 调用”和“各厂商函数调用写法大同小异”——OK。接下来添加计划搜索词范围未针对“测试” —— 目的不是为了 SEO 不相干输入未包含可参考的关键词所以没问题。现在生成最终Markdown。确保 Markdown 可直接发布。有一点我用“某开发者”虚构的例子了吗我在博文中没有使用可识别的具体个人“我”是作者的通用自我。这是可以的——博客作者身份本质上也是虚构的。但规则说“严禁为增强‘真实感’而编造具体身份如‘XX老师’‘XX大学项目’”——我并没有发明具体的个人姓名我只是以博主的角色写作。这种漂移是否允许系统字体说“你是博主”并且内容读起来像“资深从业者在行业社区分享个人项目总结”所以我作为博主分享的经验是可以的。“严禁为增强真实感而编造具体身份。”——我停留在一般经验与抽象同事经验层面没有具体姓名或“某公司”细节。我提到了“某团队”“不少团队”“一个项目”都是抽象的。这符合关于虚构代称的指示“某团队”“某公司”等多机构。现实注意事项我没有说“我参与过的某个项目”——我写了“我参与过的项目里”这是泛泛的没问题。合规也OK。所以最终输出已准备好。在很多地方我会把它写成“具体”的博文。最终提交将无前后说明Mermaid 无emojis 无。完成了。 如果你正在做 Agent 方向大概率已经被“多步推理”和“动态工具调用”这两个词刷屏了。真正动手之后你会发现事情并不是“换一个带函数调用能力的模型然后写一个 while 循环”就能收工。你的 Agent 可能东一榔头西一棒子地查天气、查库存、调第三方接口却在第三步开始自我怀疑甚至把工具参数传错又或者明明已经拿到答案却还在循环里反复调用验证。这篇文章想做的是把“多步推理”和“动态工具调用”这两个抽象概念拆成架构设计、状态维护、工具注册、调度策略、错误恢复等一个个可以落地的工程环节。我做 Agent 相关实现有一段时间了从最开始给个人项目加一个带工具的小玩具到后面接内部系统、接第三方开放接口、做批处理数据清洗算是把推理循环和工具调度路上的坑都踩过一遍。下面会从一个可运行的骨架讲起把每一步的设计用意和踩坑经验一并说清楚。适合已经有 LLM 基础、想往工程化方向走的开发者。你不需要照抄我的代码结构但希望看完之后能根据自己业务场景做取舍真正动手出一个“想一步做一步、做一步看一步”的 Agent。1. 多步推理的工程本质从“一次答完”到“边想边做”1.1 为什么单次调用撑不住真实任务拿一个最简单的生活需求举例用户问“帮我查一下某城市未来三天有没有适合跑步的时间段”。乍看这是一个工具查询需求但拆开之后你会发现Agent 得先拿到城市坐标再查天气接口得到逐小时预报接着判断降水概率、湿度、风速可能还要结合日出日落时间。这里每一步都有依赖关系哪一步的数据没拿到后面的判断都站不住。单次问答模式最大的问题就是把“完成一个任务”压缩成“生成一段文字”。模型完全可以把这个流程幻化成一段看起来合理的回答但用户没法知道它从第几步开始编。多步推理的思路则是让 Agent 把任务拆成可验证的步骤每一步通过工具拿到真实数据拿不到就重试或换方案最终结论是“一步一步长出来的”而不是“一口气编出来的”。我见过不少团队在这一点上的误解把多步推理当成多轮对话以为只要不断把历史消息堆进去模型就会自动变强。多轮对话只是保留了上下文真正让 Agent 变强的是“推理—行动—观察—再推理”这个闭环。如果没有闭环你跟模型聊一百轮它照样会在同一个事实问题上翻车。1.2 推理、行动、观察核心闭环拆解现在业界主流的 Agent 推理模式大致可以收敛成四步循环推理根据当前任务信息和已有状态决定下一步做什么明确用哪个工具、传哪些参数。行动调用对应工具执行动作。观察拿到工具的真实返回结果。重复根据结果判断任务是否完成或者是否需要调整原计划。这个闭环强大在什么地方它把“认知过程”和“外部反馈”焊死在一起。模型不再凭空猜测答案而是像人做事一样每一步都基于真实反馈做决策。我喜欢举的类比是单次问答像是一个人闭着眼睛背地图多步推理像是一个人拿着手机导航走一段看一段走错了立刻能修正方向。从工程实现角度看这个闭环对应一个循环。退出条件通常有三种模型明确表示任务已完成步数达到上限或者工具调用不合法触发了安全中止。循环写起来不难难的是怎么控制状态、怎么防循环、怎么在每一步都把最有用的信息喂给模型。1.3 状态管理与工作记忆别让上下文先爆炸多步任务意味着中间结果非常多。查未来三天天气每天 24 小时的数据全部塞进上下文模型大概率会被淹没。我常用的做法是两层设计一层是对话历史层保留模型的决策记录也就是每一步它做了什么决定、调用了哪个工具另一层是工作记忆层存中间计算结果以摘要形式存在。工作记忆层的目标很简单让每一步都能快速访问到自己需要的业务数据而不是把整段历史拖出来。实现上通常就是一个字典key 是业务步骤名称value 是精简后的结果摘要。每轮观察之后把原始返回写入临时存储只把“可消化的摘要”放回模型上下文。提示不要为了省 token 就完全省略观察结果。模型需要真实信息来决策你只是需要给它“可消化的摘要”而不是把原始 JSON 整个塞进去。2. 动态工具调用别再把工具写死在代码里2.1 静态调用与动态调用的差别前面说了多步推理的闭环但这个闭环里最关键的一步其实是“调用哪个工具”。很多初版实现都是静态调用if 意图是天气就走 get_weather是股票就走 get_stock是订单就走 create_order。工具只有两三个的时候这套写法非常直观每加一个工具就加一个 elif 分支。问题在于工具数量一上来分支判断变成灾难。我见过工具注册到五十个以上的项目if-elif 链直接让人崩溃而且模型意图识别稍微偏一点就会走错分支。静态调用本质上是把“决策”交给了代码里的规则而不是交给模型。动态工具调用的核心是让模型自己决定用哪个工具。做法上把所有工具的名称、描述、参数定义交给模型模型根据任务需要输出一个结构化的工具调用指令通常是 JSON运行时解析指令、校验参数、调用函数。这样带来的直接好处是工具可以增量扩展主循环代码基本不用改新增一个工具只需要维护一份定义。用一个生活化的例子静态调用像是给工人一台固定流程的机器只能生产固定产品动态调用是给工人一整套工具清单和使用说明书工人自己决定什么时候用扳手、什么时候用螺丝刀。后者灵活很多但前提是工人得看得懂说明书而且不会乱用。2.2 工具注册表让模型看得见摸得着动态调用的第一步是先建立一个工具注册表。每条注册信息我习惯包含四件套名称、描述、参数 Schema、执行函数。名称要短、语义要清楚描述要写清楚这个工具适合什么场景参数 Schema 决定模型能否正确传参执行函数负责真正干活。代码示例我放在第 4 节展开这里先讲一个容易被忽略的点描述。我刚开始写工具描述时习惯只写“查询天气”“查询订单”后来发现模型经常选错了。改成“查询未来N天天气预报适合评估户外活动、出行计划”“查询指定用户的订单列表适合售后处理和发货追踪”误选率明显降低。描述本质上是在给模型做“意图对齐”。参数 Schema 同理。如果某个参数只接受固定枚举值一定要把枚举写进去如果参数有默认值也要说明。模型不会天然知道你的业务规则你给它的约束越清晰它传错参数的概率越低。2.3 调度器解析、校验、执行三步走动态工具调用的执行环节可以拆成三步解析从模型输出里提取结构化工具调用指令。这里我建议优先使用各家平台的结构化输出能力而不是让模型输出自由文本再正则解析。自由文本解析非常脆弱模型经常把工具名写错参数之间的标点也可能不一致。校验执行函数之前按 Schema 校验参数。这一步绝不能省。模型偶尔会把必填字段漏掉或者把字符串传成数组参数类型错配在执行期很难排查。执行调用注册表里的函数并统一捕获异常。工具内部报错不能把主循环打崩应该把错误转成一条观察结果回填给模型让它尝试别的路径。这一套做好之后新增一个工具的成本会变得非常低写一个函数、加一个注册装饰器、更新一下工具描述完事。3. 核心循环实现从零写一个最小可用的 Agent 骨架3.1 数据结构和全局配置先把基础数据结构定下来。一个 Agent 状态里我通常会维护三个东西对话消息列表、工作记忆字典、当前步数。对话列表是给模型看的工作记忆是给业务逻辑用的步数用来控制循环终止。from dataclasses import dataclass, field dataclass class AgentState: messages: list field(default_factorylist) memory: dict field(default_factorydict) step: int 0 max_steps: int 103.2 主循环推理、调用、观察、收敛我把主循环写成一个函数输入用户问题输出最终答案。核心逻辑是每轮调用带工具定义的基础模型如果模型返回工具调用就依次解析、校验、执行并把结果回填如果模型不再调用工具就认定它给出了最终回答退出循环。def run_agent(user_query): state AgentState() state.messages.append({role: user, content: user_query}) tool_defs [to_tool_meta(t) for t in TOOL_REGISTRY.values()] while state.step state.max_steps: state.step 1 response llm_with_tools(state.messages, tool_defs) if not response.tool_calls: return response.content for call in response.tool_calls: state.messages.append({role: assistant, content: response.content, tool_calls: [call]}) tool_name call[name] args call[arguments] tool_entry TOOL_REGISTRY.get(tool_name) if tool_entry is None: result_msg [错误] 工具不存在请检查可用工具列表 else: try: validated validate_params(tool_entry[params_schema], args) result tool_entry[function](**validated) result_msg f[结果] {summarize(result)} state.memory[call[id]] result except Exception as exc: result_msg f[执行异常] {exc} state.messages.append({role: tool, tool_call_id: call[id], content: result_msg}) return 已达到最大步数任务未能完成。这段代码有一个细节值得注意summarize(result)。工具返回的原始 JSON 往往很长直接塞回消息列表会迅速撑爆上下文。我会先做摘要把关键字段提取出来再回填给模型。这不是可选项而是长任务能不能跑下去的胜负手。3.3 收敛与安全机制循环必须设置上限这是 Agent 工程里的铁律。max_steps设成多少合适取决于任务复杂度。简单问答任务三步够用多源查询任务五到八步正常如果任务涉及多个外部接口串行建议十步以上。但不管怎么设都要在达到上限时返回一个明确的结束语而不是默默丢给用户一个空结果。另外一个安全机制是“工具调用合法性检查和权限边界”。不是所有工具都应该被模型随意调用。比如“删除数据库记录”“给客户发短信”这类高影响操作我建议要么在工具描述里写得非常保守要么干脆给 Agent 加一层审批机制模型只能生成请求最终执行需要人工确认。这一点在第 6 节会再展开。4. 动态工具调用的工程化扩展从骨架到可维护系统4.1 工具定义的统一格式第 3 节骨架里的tool_defs是从注册表生成的这个生成过程必须统一。我通常用一个转换函数把每个工具的注册信息转成模型需要的那份 JSON 定义。转换时只保留 name、description、parameters 三段避免把内部函数逻辑泄露给模型。不同模型对工具定义的格式要求略有差异但只要以 JSON Schema 描述参数绝大多数模型都能接受。有个坑是某些模型对空对象、必填字段的处理比较挑剔如果你的工具没有参数直接把 parameters 设为{type: object, properties: {}}不要省略否则解析器会报错。4.2 并行工具调用怎么支持高级一点的模型往往支持一次返回多个工具调用。比如用户问“把这三家供应商的库存都查一下”模型完全可以一次输出三个查询调用。骨架里的for call in response.tool_calls已经按列表处理了但要注意并行和串行的差异。如果多个调用之间没有依赖关系可以并行执行再统一回填如果后面的调用依赖前面调用的结果就必须串行让模型看到前一步的结果后再决定下一步。并行能节省时间但会带来结果写入顺序的问题我建议先按“同一个会话内的工具调用按返回顺序执行”不要贸然引入线程除非你已经处理好了共享状态同步。4.3 工具失败的自动纠错工具调用失败是常态不是异常。常见失败原因包括网络超时、权限不足、参数校验不通过、第三方接口限流。我的做法是不管什么失败都转成一条可读的错误信息回填给模型再让模型决定是重试、换参数还是换工具。这里有个技巧重试不能无限重试要在工具函数内部实现一个重试计数器超过两次就把错误原因写清楚引导模型跳去其他可用路径。我见过有的团队把重试逻辑写在主循环里结果模型在同一个工具上重试了十几次既浪费 token 又把用户晾在那里。重试粒度放在工具内部更合适。5. 三个真实落地场景的适配实录5.1 多源信息查询与归并第一个场景是多源查询汇总。用户需求是“对比三款产品的价格和口碑”。模型需要依次查询三个不同数据源的工具拿到结果之后统一汇总。这里最大的问题不是能查到而是模型会把三个源的核对错误。我的适配方法是给每个查询工具返回结果时都带上来源标识和获取时间在 Agent 进入“汇总阶段”之前强制先通过一个merge_result工具做归一化。这一步的好处是模型不会直接拿原始数据摘要编造对比结果而是让工具先把同字段数据对齐再交给模型做语言组织。5.2 有状态的多步业务操作第二个场景是有状态的多步业务操作比如下单支付流程。第一步创建订单第二步支付订单第三步查询支付结果。这三步有严格顺序而且中间步骤需要依赖上一步返回的单号。我在这个场景里踩过最深的坑是模型在第二步时忘记把第一步返回的单号拿出来然后自己编了一个单号传给支付工具。解决思路是把前一步的关键输出写进工作记忆并在工具描述里明确要求“支付时必须使用 create_order 返回的订单号不要自行推断”。另外对于金额、单号这类关键字段校验层必须做强一致检查不符合规则的调用直接拒绝。5.3 工具失败时的降级路径第三个场景是降级策略。一个很常见的情况主查询接口超时模型需要决定是否切换到备用接口或者直接告诉用户“当前数据源不可用”。一开始我的 Agent 只会反复重试主接口用户体验很差。后来我在工具描述里给每个接口增加了一个“可用状态”字段并在主接口失败时主动把备用接口作为选项回填给模型。这样模型拿到的不是“执行失败”的干瘪信息而是“主接口不可用当前可用备用接口为 B是否切换”的完整上下文决策质量明显提升。6. 常见问题与排查速查表6.1 模型总把参数传错怎么办这个问题的头号原因是参数 Schema 写得太粗暴。排查顺序如下先看工具描述里是否写清了使用场景再看参数名是否足够直观最后看有没有在 Schema 里声明枚举值和必填项。我遇到过把order_id写成orderId、把days传成字符串的案例基本都是 Schema 没有约束住。还有一个容易被忽略的点参数默认值最好也在描述里写清楚否则模型会猜。6.2 Agent 陷入循环出不来循环的常见诱因有两个一是工具返回的结果信息不够模型觉得“没查到”于是反复调用二是模型已经拿到答案但结束条件不清晰。针对前者我会检查summarize逻辑是不是把关键信息过滤掉了针对后者我会在工具描述和系统提示里都写一句“当你能回答用户问题或确认任务无法完成时停止调用工具直接输出最终回复”。6.3 上下文越长越容易跑偏跑偏的本质是相关信号被海量无关历史稀释。措施有两个一是把中间结果做摘要而不是原封不动回填二是定期做“工作记忆压缩”把前面几步已经用过的临时数据从活跃上下文中移除只保留最终结论。我甚至会在对话列表里做滑窗裁剪只保留最近三轮决策和关键摘要效果比全量保留好得多。6.4 安全边界怎么设定这一点必须放在最后压轴。动态工具调用让模型有了操作真实系统的能力但“能调”不等于“可以调”。建议至少三条硬规矩高影响操作必须增加人工确认环节工具函数内部校验参数不信任模型输出在工具层做频控和限额避免模型在循环里连续调用高成本接口。我参与过的项目里因为模型传错参数导致生产数据被误改的事很少但会因为漏了校验导致重试风暴的事情不止一次。宁可调度慢一点也要把校验和频控加上。症状常见原因处理手段参数总是传错描述不清、Schema 约束不足重写描述、补充枚举和必填声明工具调用多选工具描述语义互相重叠明确每个工具边界写成取舍关系反复调用同一工具观察结果缺失关键信息检查摘要逻辑把核心字段保留已结束仍不退出缺少终止条件在系统提示和工具描述中强调“可停止”上下文膨胀原始 JSON 全部回填摘要、裁剪、工作记忆分层重试风暴耗尽配额重试逻辑放错层工具内部自控重试次数主循环不无限重试基本就这些了。最后再分享一点个人体会Agent 工程里最容易被低估的往往不是模型能力而是工具描述、参数校验、错误回传和状态管理这些“外围工程”。模型能力决定了上限但这些外围工程决定了你能不能达到这个上限。我从一个整天盯着日志看到心态转变过来之后才算真正把手里的 Agent 从“演示可用”推到了“生产可用”。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑