从去年年底开始,“Agent”这个词几乎被聊烂了,但真正把Agent落到生产环境的人都有一个共同的感受:模型再聪明,如果够不到外面的系统,它就是一只被关在玻璃房里的鹦鹉。我手里好几个项目,早期最大的瓶颈根本不在推理能力,而是Agent根本没有“触达”外部世界的通道。后来我们围绕“Agent能碰到什么、能操作什么、能影响什么”这一连串问题做的所有工作,说白了都是在解决同一个问题——Agent-Reach。
这个概念我在内部讲了很久:Agent-Reach,直译是“智能体的触达范围”,本质上衡量的是一个Agent能对多少外部资源发起有效行为的能力边界。它决定了你的Agent是只能聊天,还是真的能干活。
这篇文章不聊虚的,我把我们在Agent-Reach方向上的实践、踩过的坑、走过的弯路一次性整理出来。如果你正在做Agent类产品,或者准备接工具调用、准备让Agent连企业内部系统,这篇文章应该能帮你少走一周的弯路。
1. 先从“为什么你的Agent总在装死”说起
1.1 一个会思考但摸不到任何东西的Agent,本质上是个残废
我见过太多团队,花大力气微调模型、拼提示词,结果Agent一问三不知、一让干活就卡壳。为什么?因为大模型本身是个“没有手”的系统。你让它“查一下订单状态”,它脑子里知道订单系统里大概有什么字段,但它摸不到数据库,调不了API,连发个HTTP请求都做不到。它只能靠训练时学到的“记忆”假装回答你,这就是Agent装死的本质。
所以Agent-Reach的第一步,就是给Agent装上“手”——一组真实可执行的工具接口。这个道理很简单,但落地时你会发现,真正难的不是“装手”,而是想清楚这只手能伸多远、伸出去之后怎么不被夹到。
1.2 Reach的四个层次,你的Agent在哪一层
我习惯把Agent-Reach分成四个层次,每个层次对应不同的能力边界,也对应不同的工程复杂度:
| 层次 | 能力描述 | 典型场景 | 工程难点 |
|---|---|---|---|
| L1 本机触达 | Agent只能访问运行环境内的文件、进程、本地服务 | 读取配置文件、生成报表、批量处理文档 | 几乎为零,但作用有限 |
| L2 API触达 | Agent能调用外部HTTP API、RPC服务 | 查订单、发消息、操作SaaS平台 | 鉴权、协议转换、参数校验 |
| L3 跨系统触达 | Agent能穿透多个内部系统完成链路操作 | 下单后同时更新库存、通知财务、触发物流 | 分布式事务、状态一致性、幂等 |
| L4 协作触达 | Agent能与其他Agent互相通信、委托任务 | 多角色协同完成复杂流程 | 信任机制、消息路由、结果仲裁 |
我这里的大部分项目都卡在从L2到L3的跃迁上。L2只要接几个API就行,但到了L3,你会发现每个系统都有自己的协议、自己的鉴权方式、自己的一套“潜规则”,这才是Agent-Reach真正的分水岭。
1.3 为什么大模型本身解决不了Reach问题
有人问:GPT-4这种级别的模型,不是已经能调用工具了吗?确实,模型现在可以理解工具描述、生成参数JSON,但这只是“理解”,不是“触达”。真正执行时,你还是要自己搞定网络连接、认证握手、超时重试、错误恢复。模型负责做决策,Reach负责做执行。这两件事在工程上是完全解耦的。
你完全可以换一套Reach框架,Agent还是同一个模型,但触达能力可能天差地别。这也是为什么我一直强调:Reach不是模型的附属品,它是一套独立的、需要认真设计的基础设施。
2. 触达能力的三层架构,我们是这样设计的
2.1 工具注册层:把外部能力变成Agent能看懂的“说明书”
让Agent触达外部系统,第一件事是把每个外部能力描述成一段结构化的元信息,这就是工具注册层。我们内部管它叫“工具说明书”,格式大概长这样:
{ "tool_name": "query_order_status", "description": "根据订单号查询订单当前状态,包括待支付、已支付、发货中、已完成、已取消", "parameters": { "type": "object", "properties": { "order_id": { "type": "string", "description": "订单号,格式如ORD-2025-0001" } }, "required": ["order_id"] }, "returns": { "type": "object", "properties": { "status": { "type": "string" }, "updated_at": { "type": "string" } } } }这层看起来简单,但有一个特别容易被忽视的问题:工具描述写得不清楚,Agent就会乱选工具。我见过最夸张的一次,两个工具一个叫“query_user_balance”,一个叫“query_user_points”,描述里都写着“查询用户账户信息”,结果Agent随机选了一个,用户体验稀碎。
后来我们定了三条铁律:
- 每个工具的description必须说明“能做什么”“什么时候用”“不能做什么”,一句话说完,不要含糊。
- 参数名和取值范围必须和真实API对齐,不能写别名,否则Agent生成参数时容易飘。
- 每个工具挂一个“confident threshold”,置信度低于阈值宁可让Agent说“不知道”,也不要瞎猜一个工具调用。
2.2 网关接入层:统一鉴权与协议转换的关键节点
工具说明书只是“看得见”,要“摸得着”还得过网关。这一层我踩过最大的坑,是低估了企业内网的协议多样性。我们接的第一个内部系统用的是老的SOAP接口,第二个用的是gRPC,第三个干脆是FTP传文件。如果让Agent直接对接这些五花八门的协议,Reach能力就废了一半。
所以我们在中间加了一层网关,核心职责有三个:
- 统一入口:所有工具调用都走同一个HTTPS入口,内部再做协议转换,Agent不需要关心后端是HTTP还是gRPC。
- 身份鉴权:网关统一维护Access Token、API Key、以及服务间调用的mTLS证书。Agent只认一个网关凭证,网关再按目标系统映射具体凭证。
- 流量管控:每个工具设置独立的QPS上限和超时时间,防止Agent某个操作把下游压垮。
网关层还有个隐藏好处:审计。每个工具调用都会留下完整的日志——谁触达了哪个系统、传了什么参数、返回了什么结果。这在排查问题的时候简直救命。
2.3 策略层:不是“能不能触达”,而是“该不该触达”
很多做Agent的人把Reach简单地理解成“连接”,但我觉得真正的Reach是“有策略的连接”。有些操作,技术上完全能触达,但从业务角度不该触达。
举个我们真实遇到的例子:有个Agent被授权可以修改订单备注,后来发现它还顺手把订单金额改了。模型为什么这么干?因为工具列表里确实有一个“update_order_amount”的工具,Agent看了看权限列表,发现自己有token,就执行了。它不知道这个操作需要二次审批。
后来我们在策略层加了两道闸:
- 操作分级:读操作(查询)直接执行;写操作(修改)走管理组审批;危险操作(删除、退款)必须双人复核。
- 上下文约束:工具调用要带上“意图链”,也就是Agent为什么要调这个工具。如果意图属于模糊地带,策略层直接拦截。
注意:策略层一定不能放在Agent内部,否则模型被提示词注入之后就形同虚设。必须放在网关之外、由独立的策略服务控制,才能形成真正的硬边界。
2.4 状态管理层:触达之后不能“失忆”
Reach不只是“调一下”就完事,很多触达是一次长链路操作的开端。Agent查完订单状态,下一步要改状态、发通知、更新库存,每个操作之间是有状态依赖的。
我们最开始没做状态管理,结果Agent查完一个订单,下一步对话时忘了订单号,又去问用户。体验极其弱智。后来我们引入了@SessionContext机制,把关键上下文状态(当前订单号、当前用户ID、当前操作步骤)单独存起来,Agent每次调用工具前自动注入这些上下文。
状态管理还解决了一个隐蔽问题:对话中段的恢复。用户离开两小时再回来,Agent要能接上之前的触达链路,而不是重新问一遍。
3. 跑通第一条触达链路:从意图到执行的全过程
3.1 最小可用的工具编排流程
下面这条流程我们内部跑了N遍,每一步都用最简单的方式验证过了,你可以直接抄作业。
完整流程分五步,核心时序如下:
- 用户说“帮我把订单ORD-2025-0001标记为已发货”。
- Agent解析意图,匹配到“mark_order_shipped”工具,生成参数JSON。
- 网关校验Token和操作权限(写操作需要确认上下文是否包含该订单)。
- 工具执行,调用WMS系统API,更新发货状态。
- 返回结果“订单ORD-2025-0001已标记为已发货”,Agent整理成自然语言回复用户。
看起来简单,但每一步拆开都有细节。我重点说第2步和第3步,这两个环节最容易翻车。
3.2 让Agent“选对工具”的工程技巧
模型选工具不是靠魔法,实际上是靠语义匹配。描述得越具体,“选对”的概率就越高。我们做过一组对比实验:
- 工具A描述:
查询用户积分,工具B描述:查询用户余额 - 用户问:“我账上还有多少钱?”
- 描述模糊时,Agent选A/B的概率大约五五开。
后来我们把描述改成:
- 工具A:
查询用户积分。积分是商城消费时累积的奖励值,可以在积分商城兑换礼品,单位是分。 - 工具B:
查询用户余额。余额是账户中可提现或可直接用于支付的资金,单位是元。当用户询问"多少钱""账户里还有多少"时,优先使用此工具。
改完之后,“我账上还有多少钱”这类问题选Balance工具的准确率直接提到95%以上。经验就是:描述里要写清楚边界,尤其是容易混淆的边界,而不是只写功能。
3.3 参数生成的三个校验规则
参数是另一个重灾区。模型生成JSON时,经常把“2025-03-01”这种日期格式传成“2025年3月1日”,或者把手机号中间加空格。我们在网关层加了参数校验规则,主要三条:
- 格式校验:按每个参数定义的type和pattern做正则校验,不匹配就拒绝。
- 范围校验:枚举值必须在允许列表内,数值必须落在min/max之间。
- 业务校验:有些参数光格式对还不够,比如order_id前缀必须是ORDER或ORD,这类规则从业务系统同步过来。
校验不通过时,不要直接报错,要把错误信息返回给Agent,让它自己修正参数再调一次。这一步很重要,因为模型是根据错误反馈做自我纠正的,一次不行就两次,我们设置了最多3次重试。
4. 触达边界上的坑,每一个都是真金白银换来的
4.1 权限过宽:一条事故复盘
有一次,Agent在测试环境执行了一个“清除缓存”的工具,结果因为权限配置太宽,把生产环境Redis里的部分缓存也清了。虽然没造成数据丢失,但高峰时段部分接口延迟飙升,直接被业务方投诉。
复盘后发现根因:Agent使用同一个服务账号,测试和生产用的是同一套凭证。我们在权限模型上做了三处整改:
- 环境隔离:测试环境、预发环境、生产环境使用完全不同的凭证和网关入口。
- 最小权限:Agent默认不拥有任何写权限,每个工具的读写权限单独申请、单独审批。
- 危险操作拦截:高危工具(删除、批量改、清缓存)在网关层做二次确认,需要传入审批单号才能执行。
这三点改完之后,类似事故再也没有发生过。在Reach体系里,权限做宽一分,事故概率翻一倍,没有例外。
4.2 协议适配:你永远猜不到下游API长什么样
接WMS系统的时候我们吃了大亏。对方文档写的是“更新发货状态,传状态码”,结果真实接口的请求体是:
{ "action": "update", "data": { "order_id": "ORD-2025-0001", "status_code": "SHIPPED", "operator": "agent" } }而错误响应也很有意思,成功时HTTP 200不代表处理成功,业务失败的响应体是:
{ "code": 200, "success": false, "message": "status_code无效" }HTTP 200 + business fail这种设计,让我们的重试逻辑一开始完全失效。后来做了一层“业务状态映射”,把下游的code段统一翻译成agent_ok、agent_biz_error、agent_timeout三类,Agent只认这三类。遇到biz_error就不再盲目重试,而是把错误原因拿回去分析,让大模型决定下一步。
4.3 超时与重试的隐性陷阱
Agent调用工具,面临的超时问题和人类调API完全不一样。人类调用接口超时,重试一次大概率没问题,但Agent会重复调用同一个接口,如果下游不是幂等的,就会造成重复扣款、重复发货、重复通知。
我们处理这个问题的方案是加幂等键。每个工具调用请求在网关层生成一个幂等ID,下游接收端记录已处理的幂等ID,重复请求直接返回上次结果,不再执行。这个设计在对接支付、库存、通知这类核心系统时,属于必须项。
超时配置也很有讲究,我们默认分三档:
- 读操作:超时5秒,重试2次,间隔1秒。
- 写操作:超时10秒,不自动重试,幂等ID兜底。
- 异步操作:提交后立即确认,结果走回调通知,不占同步链路。
4.4 Agent“自说自话”:触达结果没有被正确反馈给模型
还有一种情况:工具执行成功了,但Agent忘了把结果“内化”进自己的上下文,导致用户在下一轮问的时候,Agent回答“我还没查到”。这本质上是个状态管理缺陷,模型的上下文里根本没有工具返回的结果。
我们的做法是强制结果回填:工具调用完成后,不管成功还是失败,返回内容必须写回到对话上下文,并且用结构化的方式标记“这是工具结果,可信”。除此之外,我们还给每条工具结果设置一个有效期限(TTL),比如10分钟内有效,过期后Agent必须重新触达,避免用旧数据回答新问题。
5. 多Agent协作:当Reach从一个Agent变成一群Agent
5.1 为什么单Agent触达仍然不够
单Agent的Reach再强,也解决不了“多角色并行”的问题。一个客服Agent既要查订单,又要跟进退款,还要和用户沟通,全塞在一个Agent里,工具列表越来越长,指令越来越混乱,出错的概率指数级上升。
我们把一个“全知全能”的Agent拆成了三个专业Agent:客服Agent、订单Agent、售后Agent。每个Agent的Reach范围被限定在自己的领域内,客服Agent只能调用查询类工具和会话类工具,订单Agent才能操作状态变更,售后Agent负责退款流程。拆完之后,单Agent的工具数量从24个降到6~8个,准确率提高了不少。
5.2 轻量级Agent间协作方案:事件总线加回调
Agent之间怎么协作?我们试过HTTP直接调用,试过消息队列,最终选了最轻的方案:事件总线加回调。
核心流程是这样的:
- 客服Agent发现用户要退款,生成一个“退款申请事件”发布到事件总线。
- 售后Agent订阅了退款事件,收到后开始处理退款流程。
- 售后Agent处理完成后,通过回调接口通知客服Agent。
- 客服Agent收到回调,组织语言告诉用户退款进度。
这套方案的好处是解耦:客服Agent不需要知道售后Agent的内部逻辑,只需要知道“发布退款事件,等待回调”。事件结构、回调地址、超时时间这些都是配置化的,新增Agent时不用改老Agent的代码。
5.3 信任问题:Agent如何不被另一个Agent带偏
多Agent协作带来的一个新问题,是信任。Agent收到的“另一个Agent传来的信息”,到底可不可信?如果一个Agent被提示词注入,输出的恶意事件被另一个Agent无条件执行,整个Reach体系就被人拿捏了。
我们的对策是签名加白名单:
- 事件总线上所有事件都带发布者签名,接收方只信任签名在白名单里的Agent。
- 跨Agent传递的参数必须过一遍接收方的参数校验,和外部API的参数一样对待。
- 高危操作无论来自哪里,都必须走审批,不能因为“是另一个Agent说的”就放行。
这些规则看起来保守,但正是它们让系统在多次对抗测试中稳住了。Agent协作是未来,但未来的前提是边界足够清晰、信任足够可控。
6. 关于Reach,我最后的几点心得
做完这几轮Agent-Reach的改造,我最深的感受是:Agent的能力上限,不由模型决定,而是由它够得着多少资源、够得着的方式有多稳决定的。再聪明的模型,如果Reach层设计得一塌糊涂,它也只是一个“聪明的残废”;反过来,Reach层做得够稳,哪怕模型能力弱一点,整个系统也能干活、能交付。
几个实操层面的小建议,你们可以收藏备用:
- 先做L1本地触达,把文件读写、简单工具调用跑通,再碰外部API,不要一上来就画大图。
- 网关这一层不要省钱,能力越强越需要一个统一入口做鉴权、审计和限流。
- 把“该不该做”的判断放在Agent外部,策略服务独立部署,这是安全感的来源。
- 幂等键在所有写操作里都是标配,等出了事故再补就晚了。
- 每个工具的描述都当作文案来写,边界写清楚,比你多调几个版本的prompt都管用。
我其实还挺看好Agent-Reach这个方向的,它把Agent从一个“会聊天的接口”变成了“能干活的系统”。后面的路还很宽,但目前这套方法论已经足够支撑好几类真实业务了。你们如果有正在做的事和这套框架相互印证,欢迎来聊,我很想听听你们在触达层踩过哪些更离谱的坑。