
做了大半年 AI Agent 项目我最大的感受是大模型从来都不缺脑子缺的是手脚。你让它写一首诗、总结一份文档、解释一段代码它都能干得不错可一旦涉及帮我把这份数据存进数据库帮我查一下这个接口的通话状态根据这个表格自动生成下周的排班表这类需要真实操作外部系统的活儿它就卡住了——因为它只会输出文本。那文本怎么变成一次真实的 API 调用怎么变成一条 SQL 的执行这正是我启动 Agent-Reach 这个项目时最想解决的问题把一个只会说话的模型变成一个说到做到的执行者。Agent-Reach 本质上是一层工具触达中间件夹在大模型和外部系统之间。它接收模型的输出意图匹配对应的工具执行真实的调用再把执行结果送回模型做下一轮决策。听起来其实不复杂但当你真正把 Agent 接到生产环境时会发现这一层要做好得涉及协议设计、参数校验、安全边界、异常兜底、上下文管理等一大堆细节。这篇文章就把我这段时间的设计思路、踩坑记录和一些实测数据整理出来给正在做同类项目的朋友当个参考。1. 大模型不缺脑子缺的是手脚Agent-Reach 要补的那块短板1.1 只靠对话生成Agent 走不到真实业务里先聊一个很多刚接触 Agent 开发的人容易忽略的点。大模型本身是文本进、文本出的闭环你输入一段 prompt它输出一段答案。这个闭环对聊天、写作、分析这些场景完全够用但现实业务不是这样的。现实业务里有数据库、有第三方服务、有定时任务、有审批流每一项都是动作不是话术。我见过不少团队做出来的 Agent demo看起来挺惊艳你说帮我查一下这个客户的信息它真的在回答里写出一段看似合理的客户资料。但仔细追究会发现这些资料全是模型自己编出来的因为它根本没权限去查真实数据库。Demo 没问题生产环境就完蛋了你不可能让一个胡编数据的 Agent 对接真实客户系统。所以 Agent-Reach 的第一步设计思路很明确把模型和外部系统彻底隔离模型永远不知道数据库密码、没有 API Key、不直接接触任何真实外部资源。它只做一件事——输出结构化调用意图剩下的事情全部由触达层去完成。这个隔离其实就是 Agent 能不能上生产的关键分界线。1.2 触达能力的四个层次我在设计 Agent-Reach 的过程中把 Agent 的触达能力拆成了四个层次这也是后来整个架构的底层逻辑工具层封装好的函数比如查天气发邮件生成工单。这是最细粒度的能力单元。API 层对已有外部服务的 HTTP 封装比如调用企业微信接口、调用内部订单系统接口。工具层是通用的API 层是面向具体系统的。环境层让 Agent 能够感知当前上下文比如当前用户是谁、当前时间、当前文件目录里有什么。很多人会把环境信息直接写死在 prompt 里但动态维护会更灵活。反馈闭环触达不只是执行一次动作还要把动作结果拿回来、让模型基于结果做二次决策。这一步做不好Agent 就永远是单发指令不是真正的自动化。这四个层次是我后续所有开发工作的基础框架。每个层都有一个独立的模块来负责模块之间通过统一的协议通信后面我会详细讲这个协议的设计。2. Agent-Reach 的骨架把模型意图翻译成外部动作2.1 工具注册一切能力都变成同一套 schema开发 Agent-Reach 的时候我做的第一件事就是设计工具注册机制。目的很简单让每个外部能力以统一的、计算机可读的描述方式暴露给触达层再由触达层暴露给模型。这个描述不是给人看的是给模型看的——模型必须理解有哪些工具、每个工具接受什么参数才能正确发起调用。我用的是 OpenAPI 的 JSON Schema 风格。比如一个查询工单状态的工具注册信息大概是这样的{ type: function, function: { name: query_ticket_status, description: 根据工单编号查询当前处理状态用于客服响应客户催单场景, parameters: { type: object, properties: { ticket_id: { type: string, description: 工单编号格式为 TKT-2024-XXXX } }, required: [ticket_id] } } }字段不多但有一个很容易被忽视的地方description写的好坏直接决定模型选择工具的准确率。我在实测中发现如果 description 写得含糊比如只写查询工单状态模型在查询订单状态查询物流状态这些相似工具之间就会频繁选错。后来我把 description 全部加上调用场景提示比如用于客服响应客户催单场景准确率有明显提升。所有工具统一注册到一个中心列表里触达层每次请求模型时把这个列表注入系统 prompt。工具数量少的时候没问题工具一旦多了就要考虑动态裁剪后面我会专门说这个问题。2.2 意图路由模型怎么知道该用哪个工具工具注册表有了之后核心问题就变成了模型给出的意图如何被正确路由到对应工具的执行逻辑上。路由这块我试过两条路线。第一条是完全依赖模型的 Function Calling 能力让模型直接输出一个结构化的tool_calls对象指定工具名和参数。第二种是自己实现一个语义匹配路由层先用 embedding 对用户请求和工具描述做向量召回缩小候选范围后再让模型做最终决策。实际用下来两条路线不是互斥关系。当工具数量在 5 个以内纯 Function Calling 就够用了响应快、不用维护向量索引。工具数量超过 10 个尤其是工具之间功能有重叠的时候模型开始频繁选错这时候就需要先用语义路由缩小候选集再把筛选后的 5 个左右工具交给模型精调选择。我目前的方案是混合式的。请求进来先经过一个轻量级路由模块根据工具的 name、description、历史调用频率做 top-K 筛选然后把筛选后的工具 schema 拼进 prompt 让模型做最终决策。这样既控制了 prompt 体积又保持了模型决策的准确性。2.3 执行引擎与结果回填让 Agent看得见自己的动作结果意图翻译成工具调用之后接下来是执行环节。这一步看起来简单实际是最容易埋坑的。Agent-Reach 的执行引擎维护着一个线程安全的工具执行器每个工具都是一个实现了统一接口的 handler。接口定义只有两个方法validate(params)和execute(context)。validate做参数格式校验execute接收一个上下文对象这个对象里装着用户身份、会话 ID、追踪 ID、上一轮的调用结果等。执行完的结果不直接返回给用户而是要回填给模型。这是 Agent 能否连续干活的核心。我最初的实现一愣工具执行完只把最终结果拿回来拼在回复里。比如工具返回一个 JSON{status: processing}模型就会直接对用户说您的工单正在处理中。看起来没问题但遇到复杂任务时就会出问题如果后续还要根据这个状态做进一步操作模型的中间推理过程就断掉了。我的做法是在结果回填时提供两个文本块action_result和action_summary。action_result是工具返回的原始结构化数据action_summary是系统自动生成的一段摘要或者由工具开发者手动指定。这两块都会拼进模型上下文模型既能拿到精确数据又不会因为原始数据过长而迷路。3. Function Calling 没你想的那么玄触发机制与参数校验的实战细节3.1 模型如何决定该调用工具了很多刚接触 Agent 开发的朋友会有一个错觉模型调用工具是它自己想通了要用。其实背后就是一个概率分布的问题。模型在训练阶段就学会了这样的模式当上下文中存在可供调用的工具列表并且用户请求与某个工具的 description 高度相关时模型会以更高的概率输出一个特定的结构化调用指令。触发这个行为的关键因素有三个。第一是系统提示词里是否明确说明你是一个可以使用工具完成任务的助手当需要实时信息或需要操作外部系统时请调用工具。第二是工具 schema 的准确性尤其是 description 部分不能有歧义。第三是生成参数的设置temperature建议设置在 0 到 0.3 之间温度越高模型越容易自由发挥输出一些不在工具列表里、或者参数格式不标准的伪调用指令。我踩过的一个坑是把 temperature 设置为 0.7结果模型虽然说出了我要调用查询接口这句话但输出的是普通文字完全没有按照 tool_calls 的结构化格式来。整条链路直接断掉解析器找不到合法的 tool_call 对象。排查了半天最后发现就是温度太高。3.2 参数约束与校验宁可让模型多问一次也不要传错参数参数校验是 Agent-Reach 里我最重视的环节之一。模型输出了工具调用并不代表参数就一定正确。实际上模型经常出现参数缺失参数格式偏了传了一个根本不存在的字段这三类问题。有一个很典型的例子工具定义里ticket_id是必填参数格式是TKT-2024-XXXX。模型有时会直接传20240715这样的数字。如果执行器不做校验直接把参数透传给下游系统轻则接口报错重则可能因为类型不匹配造成脏数据。所以我的校验策略分两层。第一层是结构校验用 schema 里的type、required、pattern做基础拦截。这一层不过直接返回参数错误给模型让它重新生成调用指令。第二层是语义校验在工具 handler 内部做比如格式为TKT-2024-XXXX的工单号我会在业务逻辑里再检查一下前缀和年份。两层都过才允许真实调用外部系统。这里我想强调一个原则当模型给的参数不够确定的时候宁可让它多追问用户一次也不要擅自用默认值去执行。擅自补参数是 Agent 开发里最危险的行为之一因为你不知道那个默认值会把操作带向哪里。Agent-Reach 的校验失败返回信息设计得也很明确直接告诉模型参数 t icket_id 不合法请向用户确认正确的工单编号避免模型在错误参数上原地打转。3.3 实测平行工具太多时指令遵循会退化怎么解决工具数量对模型指令遵循的影响我做过一组对比测试。同一个小型开源模型背景下工具数量从 3 个增加到 15 个时模型正确调用工具的准确率从 97% 左右掉到了 82% 左右。工具数量到 25 个时掉到了 71% 上下。这个退化趋势非常明显。原因不复杂工具列表越长模型在生成时需要注意力覆盖的信息就越多而上下文空间是有限的。排在列表尾部的工具模型往往会看不见或者想不起来。还有一个更隐蔽的问题工具描述之间存在语义干扰比如创建订单和确认订单这两个工具description 里都有订单两个字模型就容易混淆该选哪个。针对这个问题我的解决思路是前面提到的混合路由。动态裁剪工具列表每次只保留与当前用户请求最相关的 top-K 个工具。这样模型的注意力被聚焦在少数候选上准确率能够稳定回到 95% 左右。代价是每次请求需要额外做一次检索大约增加几十毫秒的延迟对于绝大多数业务场景来说完全可以接受。4. 触达不等于乱跑超时、重试、沙箱与权限收敛4.1 外部调用失败的 N 种姿势与兜底策略Agent 一旦开始真实触达外部系统失败就成了常态不是例外。我在 Agent-Reach 里总结了外部调用的几类典型故障每类都要有专门的兜底处理。第一类是超时。外部接口响应慢模型会一直等在那里。我最初的实现是同步等待结果遇到一个下游接口偶尔要跑 30 秒的情况整个 Agent 会话被拖住用户体验极差。后来我统一给所有外呼设置了超时时间默认 8 秒超时直接返回一个标准超时错误给模型让模型告诉用户系统繁忙请稍后再试。第二类是幂等性。工具被重试的时候得确保不会因为重复调用造成重复扣费、重复建单。这个必须在工具 handler 层每个参数上带上一个全局唯一的trace_id下游系统要支持基于trace_id的幂等判断。如果你的下游系统不支持幂等那就只能人工兜底或者干脆不要做自动重试。第三类是部分成功。举个例子Agent 要批量给 10 个用户发送通知发到第 5 个时接口报错了。这时候正确的做法是记录成功的 4 个、失败的 1 个把部分成功的状态如实回填给模型让模型自行决定是重新尝试失败的单个用户还是放弃并告知用户整体情况。千万不要让整体任务因为局部失败而全部回滚——这在有些场景下成本太高而且 Agent 天然应该具备局部容错的调度能力。我设置了一个统一异常类型表整理了几种最常见的兜底策略直接内嵌在触达层的配置里异常类型默认策略说明网络超时重试一次上限 8 秒重试仍然失败则返回明确错误限流(429)退避重试一次等 1 秒再试再失败则放弃参数不合法不重试返回校验反馈让模型重新生成参数下游 5xx重试一次若工具定义里标明支持幂等可重试两次业务规则拒绝不重试返回业务原因比如订单状态不允许取消让模型转述原因4.2 把 Agent 关进笼子最小权限与审计日志让 Agent 触达外部系统之前必须想清楚权限边界。Agent 不是一个普通用户它的操作速度快、并发高、行为可能充满试探性。给 Agent 分配权限应该坚持最小权限原则。比如一个 Agent 被用来处理售后客服那它应该只能查询工单、提交仲裁申请、修改工单备注而绝对不应该有删除工单、修改退款金额的权限。我遇到过一些团队图省事直接把一个管理员权限的 API Key 挂在 Agent 配置里结果 Agent 的一次误调用把所有测试订单全删了这种事故真的不少。Agent-Reach 的做法是在触达层维护一个权限路由表每个工具都标注需要的权限级别每次调用执行前都会校验当前会话的授权范围。而且我不会把真实的下游 API Key 直接暴露给工具 handler而是在触达层将 Key 统一在 vault 里管理handler 只面向密钥存储的引用做操作映射关系对 agent 会话来说是不可见的。审计日志同样不能省略。Agent-Reach 每一次工具调用都会记录谁会话 ID、调用了哪个工具、传了什么参数、返回了什么结果、耗时多久。日志往结构化存储里写既方便线上问题回溯也能用来做 Agent 行为切割分析后面你赵调 prompt 或评测模型能力时这份日志就是最真实的样本集。4.3 一个真实生产事故的复盘死循环调用烧掉 token说一个我印象特别深刻的线上问题。当时 Agent-Reach 接了一个内部的订单状态查询工具工具逻辑是如果订单状态变化则调用消息通知工具通知用户。看起来很正常对吧但某个异常订单的数据里状态字段反复在两个值之间跳动导致 Agent 每查一次就会触发一次下发通知通知完又被用户反馈触发新的查询形成了死循环。等我发现的时候这次事故烧掉了快 12 万个 token下游的消息服务也被连续打了几轮。复盘时总结出三个教训工具调用次数必须设置上限。我在触达层加了一个全局的max_iterations限制默认每个会话最多执行 8 次工具调用超出后模型必须转入总结现有信息并回复用户的模式。副作用类操作必须二次确认。像发消息这种会产生外部影响的操作最好定义一个confirmation机制在模型发起这类调用时默认先触发会话确认而不是直接执行。状态判断不能只看瞬时值。对于状态反复跳变的场景要给 Agent 一个稳定观察的机会比如连续两次查询状态一致才认为变化实际发生了。死循环这个问题如果不在触达层预设防护往上依赖模型自己意识到循环是完全不可靠的。Agent 执行行为的最后一道战线必须建在基础设施上。5. 从单工具到复合任务Agent-Reach 的多步编排实测5.1 任务拆解大任务切成小工具调用单工具调用跑通之后Agent 的价值其实才刚显现。真实业务几乎全是复合任务。比如帮我查一下这周的销售订单里有哪些客户还没有发货然后给这些客户的对接人发一封催货提醒。这一句话里就包含了查订单列表、筛选未发货、查客户对接人信息、生成提醒内容、逐条发送消息至少五个步骤。Agent-Reach 不试图直接让模型一次生成完整步骤序列因为实测下来预生成长步骤序列在执行中途一旦外部状态变化整条链路就崩了。我采用的是动态规划式的多步编排模型每一步只生成下一个工具调用执行完拿到真实结果后再决定接下来怎么做。这更贴近人在真实工作时的思考方式——你在处理事务的时候也是拿一步的结果再推进下一步而不是一上来就背完所有动作。5.2 上下文记忆别再每步都重新理解用户意图多步执行最容易出现的一个问题是上下文丢失。模型每执行一次工具调用中间就会插入一批来自工具的结果文本。等到第三个工具执行完模型很可能已经忘了最开始用户提出的原始诉求中某个关键细节。不少 Agent 项目做出来的多步调用越跑越像无头苍蝇就是这个原因。我解决这个问题用的是记忆压缩的思路。在每次工具结果回填时同时回填一个由较早期的系统摘要和原始用户请求的前置摘要组成的锚点。具体做法是会话开始时就生成一段非常简短的目标摘要每次模型要开始下一步决策时这段摘要必然存在于上下文的最前面。工具结果放在中段最新的模型指令放在尾部。这样做的好处是无论中间插入了多少工具调用结果模型每轮的注意力焦点始终有一个稳定参照物不会跑偏。5.3 我实测过的两个典型链路这里放两个我在开发 Agent-Reach 过程中实际跑过的链路作为参考。第一个是客服工单分类与自动回复链路。用户提交工单后Agent 首先调用文本分类工具判断工单类型然后调用相似工单检索工具找到历史类似 Case再把检索结果和历史解决方案交给模型最后由模型生成拟回复内容。整个链路下来一次完整任务平均要调用 3 到 4 次工具耗时在 6 到 10 秒之间其中耗时大头在模型推理工具执行本身基本在毫秒级。第二个是跨系统数据核对链路。需求是核对 CRM 系统中的客户地址和物流系统中的收货地址是否一致。Agent 先调用 CRM 查询接口拿地址 A再调用物流查询接口拿地址 B对比后向用户返回不一致的字段。这个链路的难点在于模型需要对两个结构化 JSON 做字段级对比如果直接让模型读原始 JSON地址字符串里的细微差异比如XX路 1 号和XX路1号会被模型认为是不同地址。我的做法是在工具层就做好地址归一化再交给模型做最终判断。能交给工具的清洗工作不要留给模型的眼力。6. 接生产前必须做的事落地清单与我的几点体会6.1 上线前必须检查的清单把 Agent-Reach 这类能力接到生产环境之前我整理了一份自检清单这里分享出来给大家参考。每一条都是我踩过或用实际案例验证过的不一定适用于所有团队但用来做 baseline 还是可以的每个工具调用是否有审计日志日志是否包含请求 ID、调用时间、参数摘要、结果摘要是否所有外呼接口都设置了统一超时是否梳理过每个工具的幂等能力不幂等的工具是否禁止自动重试是否设置了单会话最大工具调用次数是否定义了副作用类工具的二次确认机制是否存在 Agent 默认参数兜底策略防止擅自补参行为生产环境的 Agent 权限是否收到了最小必要范围是否对模型做了工具注入数量的评估超过 10 个工具时是否配置了动态筛选是否每一个工具 schema 的 description 都写清了调用场景和参数格式示例上线后的评测集是否包含输入参数不完整外部接口超时工具调用失败这三类反例其中最后一项经常会被忽略。很多团队只拿正常的成功样例来评测 Agent结果一上线面对各种异常场景就措手不及。没有失败样例的评测集不能用于 Agent 生产评估这一点我认为应该成为团队的基本共识。6.2 我的几点体会整个 Agent-Reach 做下来最大的体会是Agent 开发的前期精力投入要放到触达层而不是模型本身。模型能力和推理策略当然重要但对于大多数业务场景外接一个中学配置的模型完全够用真正决定 Agent 能不能干活的是工具触达层的质量——工具粒度是否恰当、schema 是否准确、异常处理是否完整、权限边界是否清晰。这些工作朴素、繁琐不会出现在论文里但它们才是生产级 Agent 的地基。另外一个感受是触达层的设计要预留人机协作的接口。不是所有动作都适合让 Agent 全自动执行某些高风险操作比如发送批量邮件、删除数据、修改金额保留人工确认入口会让业务方更愿意接受 Agent 这个新同事。项目还在持续迭代后续我打算重点搞两块无关趣味的部分让 Agent 在做复合任务时拥有更长跨度的局部记忆能力以及基于真实历史调用日志构建一套更细粒度的工具评测指标集。希望这篇分享能给同样在折腾 Agent 触达能力的朋友带来一些参考。