搞了大半年Agent项目,我最大的感受是:模型本身不是瓶颈,瓶颈是它“够不着”东西。你让大模型聊业务方案,它能说得头头是道,但真要它去查一下库存、发一条审批、改一行线上配置,它就卡住了。不是模型不够聪明,而是从“模型说出意图”到“系统真实执行动作”之间,缺了一整层工程化的东西。Agent-Reach这个项目,就是专门补这一层的——我把它理解为智能体的触达能力建设。
这半年里我踩了不少坑,也推翻过两版设计,最后沉淀下来的这套方案,基本解决了Agent“能说不能做”的问题。无论是你在做企业内部的知识库问答机器人,还是想搞一个能自动操作后台系统的个人助理,这篇内容里提到的链路设计、权限模型、重试策略和调参经验,应该都能直接用上。
1. Agent-Reach要解决的断层问题:模型输出与真实操作之间为什么总差一步
1.1 三层触达模型的由来
先说我观察到的现象。很多团队做Agent,第一步都是接大模型API,然后让模型输出JSON格式的“工具调用指令”,再拿这个指令去执行某个函数。听起来很顺,但实际跑起来会发现三个很扎心的问题:
第一,模型经常“编造”参数。它明明没有查过某个订单号,却会在调用查单接口时自动填一个看起来很像真的订单号。第二,模型对“什么动作能做”没有边界感。它可能在一个只需要只读查询的场景里,擅自调用了删除接口。第三,也是最普遍的,模型调用工具之后,面对返回的一大堆数据不知道怎么消化。工具返回了200行JSON,模型直接懵了,要么复述原文,要么开始胡说。
这三个问题不是换一个更强的模型就能解决的,因为它们本质上不是模型能力问题,而是工程架构问题。Agent-Reach最初的定位,就是在这中间加一道可控的“触达层”,把模型输出的意图转换成安全、可靠、可回退的真实操作。
我把它拆成了三层触达模型:
- 语义触达:模型能否理解用户的请求对应到哪个系统能力,对应到哪个参数。比如“查一下张经理上周的报销单”,需要拆出“张经理”“上周”“报销单”三个要素,落到查询接口的参数上。
- 接口触达:模型生成的调用指令能否被正确执行,包括格式校验、必填参数补齐、接口鉴权、超时处理。这一步是纯粹的工程活,也是最容易出bug的地方。
- 业务触达:执行完接口之后,结果能否回到业务上下文里,支撑后续的追问和多轮操作。比如查完报销单,用户接着问“其中交通费多少”,这时Agent要能基于上一次的查询结果继续分析,而不是重新查一遍。
1.2 为什么Reach这一层最容易被低估
很多团队把精力放在提示词工程上,给模型写几百行的角色设定,告诉它“你是一个智能助手”。这当然有用,但提示词只能影响模型“想做什么”,无法保证模型“能做成什么”。Reach层的价值在于,它把“想”和“做”之间的鸿沟用工程手段填上。
我见过一个挺典型的案例。某个项目让Agent调用CRM系统的接口去更新客户信息,提示词写得非常完美,模型也每次都输出正确的工具名称。但上线第一天就出事了:模型在更新客户电话时,把另一个字段的值也一起覆盖了。原因是CRM的更新接口是整行覆盖模式,而模型并不知道这个特性,它以为自己在改单个字段。这就是典型的缺少接口触达层导致的业务事故。
Agent-Reach的核心原则之一就是:永远不要信模型的“自觉”,要用代码去兜底。模型可以负责表达意图,但参数校验、字段保护、权限检查、操作确认这些事,必须由Reach层强制完成。
2. Agent-Reach的链路设计:意图路由、工具网关与状态缓存的职责划分
2.1 四个核心组件的分工
Agent-Reach的整体架构,我一开始画了很复杂的图,后来越改越简单。最后稳定下来的核心组件就四个:意图路由器、工具网关、状态缓存、审计日志。每个组件做一件非常明确的事。
意图路由器负责接收模型输出的结构化指令,做三件事:校验工具名是否存在、校验参数是否符合该工具的JSON Schema、判断该工具当前是否对该用户可见。任何一个校验不通过,直接返回格式化错误,不进入下一步执行。
工具网关是真正发起HTTP调用或执行本地函数的地方。它管着所有跟外部系统打交道的脏活:接口鉴权、超时控制、重试策略、幂等处理。工具网关还会对返回值做一次“瘦身”,把大段JSON截断、精简成模型能高效处理的结构。
状态缓存解决的是多轮对话中的上下文连续性问题。比如Agent第一轮查了订单列表,第二轮用户说“看第三个”,状态缓存里存着上一轮的结果摘要和候选列表,意图路由器可以直接把“第三个”映射到具体的订单ID,而不是让模型去猜。
审计日志这层一开始我没当回事,后来救了我好几次。每个工具调用,无论成功失败,都会记录完整的入参、出参、耗时、token消耗。排查问题的时候,没有这份日志基本等于盲人摸象。
下面是我早期画架构时沉淀下来的一张职责表,比文字直观一些:
| 组件 | 核心职责 | 关键设计点 |
|---|---|---|
| 意图路由器 | 指令校验、参数映射 | 对模型输出零信任,全部走Schema校验 |
| 工具网关 | 执行动作、管鉴权 | 统一超时、统一重试、统一幂等 |
| 状态缓存 | 保存多轮上下文的工具结果 | 按会话隔离,设置TTL自动过期 |
| 审计日志 | 记录每次调用的全链路数据 | 入参出参、耗时、token、错误码全留存 |
2.2 工具描述规范:让模型“看得懂”比“看得多”更重要
接Agent-Reach的第一步,是把你的系统能力整理成一份“工具清单”喂给模型。这个环节我发现一个高频误区:大家喜欢把接口文档直接丢给模型,参数名、枚举值、分页格式一股脑全上。结果模型输出调用时,经常在枚举值上出错,因为真正的业务枚举和接口层枚举经常不一致。
我的做法是为每个工具写一份专门的描述文件,用自然语言说明用途,用结构化JSON描述参数。一份合格的工具描述大概长这样:
{ "name": "query_order_detail", "description": "根据订单号查询订单详情,用于售后客服场景。订单号格式为13位数字,以'ORD'开头。只读操作,不产生任何修改。", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "pattern": "^ORD\\d{10}$", "description": "完整的订单号,形如ORD1234567890,必须完整,不能省略前缀。" } }, "required": ["order_id"] } }这里面有几个细节值得注意。第一,description字段里我明确写了“只读操作,不产生任何修改”,这是在给模型喂安全边界。第二,参数描述里把格式规则直接写进去,减少模型猜测空间。第三,required字段强制模型必须提供order_id,否则意图路由器会拒绝执行。
有个真实案例可以说明这套描述的价值。最初我们的订单号参数描述只写了“订单号”三个字,结果模型有时候传纯数字、有时候把前缀改成小写、有时候中间加空格。加了pattern和完整格式说明后,这类错误基本绝迹。所以我的经验是:工具描述写得越“啰嗦”,模型执行越准确,token成本看似高了,实际因为少了几轮纠错,总体反而更省。
2.3 上下文管理:工具结果不能一股脑全塞给模型
工具执行完拿到结果后,怎么把结果放回对话上下文里,这是个很容易被忽略的细节。我见过有人把一次查询的2万行结果直接拼进对话历史,然后模型就废了——不是超token限制,就是被海量无效信息干扰判断。
Agent-Reach的做法是给返回值做三层处理:
- 裁剪:只保留前N条关键数据,超出部分用“以下省略M条记录”代替。
- 摘要:用一次轻量模型调用,把结构化数据压缩成一段自然语言摘要。
- 标记:给每份工具结果打一个“结果ID”,关联到状态缓存中,后续用户追问细节时再按ID取全量数据。
例如查询订单列表返回了50条记录,Agent-Reach不会把50条全发给模型,而是发给模型一段话:“共查到50条订单,以下展示前5条,概要信息为……最晚的一笔是12月28日。”用户如果追问“那笔最晚的订单的收货地址是哪里”,意图路由器从状态缓存里捞全量数据,再调一次详情接口精准回答。这样既控制了上下文长度,又保证了多轮追问的准确性。
3. 接入真实业务系统的关键取舍:权限分级、幂等重试与错误反馈
3.1 权限分级:先让Agent在“笼子”里跑
Agent-Reach上线之前,我最担心的事就是权限失控。模型一旦拿到一个能改数据的工具,谁也没法保证它在复杂的多轮对话里不干出意料之外的事。所以权限分级是我坚持得最死的一个设计。
我把工具权限分成了五级:
| 级别 | 能力范围 | 典型工具 | 适用场景 |
|---|---|---|---|
| L0 | 无外部调用 | 纯文本问答、知识检索 | 超纲问题兜底 |
| L1 | 只读查询 | 查订单、查库存、查报表 | 客服助手、数据分析 |
| L2 | 受限写入 | 草稿保存、状态标记 | 协助整理、预填写 |
| L3 | 受控修改 | 更新字段、发起审批流 | 需要用户确认后执行 |
| L4 | 高危操作 | 删除、批量改价、资金操作 | 需要双重确认+专人复核 |
每个工具创建时就固定它的权限级别,不在运行期动态修改。用户维度上也可以做限制,比如普通员工只能触发L1和L2,主管才能触发L3和L4。
这层设计的另一个关键点是“执行前确认”。对于L3和L4的工具,Agent-Reach默认开启一个拦截开关:模型输出调用指令之后,先不真正执行,而是把指令内容翻译成自然语言,回给用户确认。比如“我将为你发起一笔金额为580元的报销审批,收款账户是招行尾号8848,确认执行吗?”用户回复确认后,工具网关才真正发起调用。
有人担心这样会降低效率,我的实测结论是:对于低频高风险的修改类操作,多一次确认的耗时完全可以接受,但换来的是整个系统敢放开给Agent跑的安全性。金融、医疗、企业后台场景尤其推荐这个机制。
3.2 幂等与重试:动作失败后Agent如何自救
工具调用不可能100%成功,网络抖动、服务超时、数据校验不过,都是家常便饭。最开始我的方案很简单:失败了就重试一次,不行就报错结束。但很快发现两个问题。
第一个是重复提交问题。某个写入接口第一次超时了,实际服务端已经处理成功,只是响应没回来。Agent自动重试,结果同一笔数据被写入了两次,用户在后台看到两条一模一样的记录。这属于典型的缺少幂等设计事故。
第二个问题是重试策略太粗暴。所有工具用一套重试参数,对于只读查询可以快速重试两三次,但对于写入操作,盲目重试的风险远大于收益。
后续我定的规则是:
- 所有写入类工具必须支持幂等键,调用方生成一个会话级requestId,服务端根据这个ID去重。没有幂等条件的接口,要么改造,要么禁止接入Agent-Reach。
- 重试次数按工具级别区分:只读工具最多重试3次,间隔2秒、5秒、10秒递增;写入工具默认不自动重试,而是把失败状态反馈给用户,由用户决定是否继续。
- 超时时间不能一刀切。查询类接口给8秒左右,写入类接口给15秒以上,因为写入操作可能涉及数据库事务。
举一个实际场景:Agent在帮用户提交一个合同审批,调用写入接口时网关超时。此时Agent-Reach不会自动重试,而是立即查一次审批单状态接口,确认这笔审批到底有没有创建成功。如果查到了,就继续走流程;如果没查到,再把失败告诉用户并附带重试建议。这个“先查后报”的套路,帮我避免了不少重复单事故。
3.3 反馈信息格式化:让失败变成模型能读懂的信号
工具调用失败后,网关返回给模型的错误信息,直接决定了模型下一步能不能正确自救。我见过很多团队的网关直接把HTTP状态码和原始报错堆栈丢给模型,比如“500 Internal Server Error”,模型根本不知道该怎么处理,只能硬着头皮胡说。
Agent-Reach对失败反馈做了一层统一格式化,所有非预期异常都转换成下面这种结构:
{ "success": false, "error_code": "ORDER_NOT_FOUND", "message": "未查询到订单号为ORD1234567890的记录,可能原因:订单号已过期或输入有误。", "suggestions": ["请核对订单号是否完整", "可尝试查询该用户最近7天的订单列表"] }这个结构里最关键的是error_code和suggestions。error_code让模型快速判断错误类型,suggestions则是直接给模型指路。实测下来,有了suggestions之后,模型自动进行修正式调用的成功率提升了接近三成。
反过来讲,错误信息最怕写得模糊。比如“系统异常,请稍后再试”,模型没办法,只能原样转述给用户,用户体验非常糟糕。我的原则是:错误信息里必须包含“发生了什么”“为什么发生”“下一步能做什么”三要素,否则不允许直接抛给模型。
4. 压测和上线后的踩坑复盘:用数据说话的设计调整
4.1 一组实测数据:接入前后到底差了多少
Agent-Reach内部跑通之后,我做了一组对比测试。选的是同一个业务场景:用Agent完成“查库存-生成补货单-发给审批人”这样一条包含三个工具调用的完整链路。测试维度选了三项:任务完成率、平均调用轮次、单任务token成本。
| 维度 | 未接Agent-Reach直接裸调 | 接入Agent-Reach后 |
|---|---|---|
| 任务完成率 | 41% | 82% |
| 平均调用轮次 | 5.7次 | 3.1次 |
| 单任务token成本 | 基准值 | 下降约35% |
| 需人工介入比例 | 每3个任务介入1次 | 每8个任务介入1次 |
数据本身不意外,但有一个点让我印象很深:完成率翻倍,不是因为模型变聪明了,而是因为链路里的“错误恢复”变得可预期了。裸调的时候,模型一旦在工具调用上出错,基本就是滚雪球式的连环失败;而Agent-Reach在每一步都给了模型清晰的反馈和出路,模型总能从错误中拉回来。
还有一点值得关注:轮次从5.7降到了3.1,说明用户和Agent的“拉扯”变少了。本质上是因为工具描述清晰了、错误反馈有效了,模型不用反复试探,一步到位的情况变多了。省下来的token成本其实是个副产品,真正的收益是用户体验的稳定性。
4.2 踩坑一:模型对时间类参数的理解偏差
第一个让我印象深刻的问题是时间参数。刚开始,工具网关发现模型经常把“上周”翻译成当前日期往前推7天,而业务上的“上周”是一个自然周(周一到周日)。比如周三说“上周”,用户期望的是上周一到上周日,模型却翻译成了今天往前推7天,结果差了整整两天。
这问题用提示词很难根治,因为模型对“模糊时间词”的解析天然具有随机性。我的解决方案是在工具网关层加了一个时间解析器:所有涉及时间参数的调用,先经过一个专门的时间归一化模块,把“上周”“上个月”“最近三天”这类表达统一换算成业务日历上的明确起止日期,再进行后续调用。这一步之后,时间类错误基本清零。
4.3 踩坑二:工具描述写得太“全面”反而误导模型
有一次我给一个报表工具写了很长的描述,把十几个可选参数都列全了,还贴心地标注了每个参数的默认值。结果模型在用户没有要求的情况下,把能填的参数全填了一遍,有的参数还填了错误值。后来我意识到,模型会把描述里出现的所有字段都当作“应该尽量填充”的信号。
修正方法是把参数分成必填和可选两类,可选参数在Schema里用oneOf或anyOf限制,并且明确写成“不传时系统自动使用默认值,大多数场景无需填写”。这个小小的改动,大幅减少了模型画蛇添足式的调用。
4.4 调参阶段的三个意外发现
最后分享三个在压测阶段发现的意外情况,这些都不是设计时能提前想到的。
第一个:把工具结果摘要塞回上下文的时机太早,会干扰模型对用户原始意图的记忆。后来我把摘要从“立即注入”改成“延迟注入”,也就是等模型输出初始计划之后再追加,效果明显变好。
第二个:有些业务接口需要从登录态里拿用户身份,但Agent-Reach最初统一从配置里取一个服务号身份,导致操作人的记录全部变成了机器人。后来改成每个用户会话绑定独立的身份映射,审计日志才真正有追溯价值。这个问题在测试阶段不暴露,一上线就被运营投诉了。
第三个:工具网关的并发数需要单独限流,不能完全依赖外部接口的限流。因为Agent在重试时可能瞬间打出一波并发,如果网关层不做本地限流,上游系统会被打得措手不及。我现在会在每个Agent会话上加一个单位时间内的最大调用次数,超出之后进入排队状态,而不是直接报错。
5. 写在最后:Agent能力的上限,取决于触达层的工程强度
Agent-Reach做下来,我的一个明显体会是:大模型本身的能力边界正在快速扩展,但真正决定一个Agent能不能在业务里站稳脚跟的,往往是那些看起来很“不酷”的工程细节——权限怎么控、错了怎么恢复、上下文怎么省、日志怎么留。
如果你正准备在自己的项目里搭Agent能力,我的建议是从Reach层开始想:先别急着调提示词,先把“模型能调用什么”“调用的边界是什么”“调用出错了怎么办”这三件事定下来。这比任何精心设计的system prompt都重要。
另外补充一点个人习惯:我习惯在大版本改动后,把一次完整对话链路的所有工具调用日志逐个过一遍,像看回放一样看模型每一步的选择。这个动作看起来费时间,但往往能发现最隐藏的问题。Agent跑得稳不稳,回放日志里都写着。