去年我给几个业务方做智能体应用,最常听到的反馈是:这 AI 脑子是有的,手不行。模型能理解用户想要什么,也能把答案组织得头头是道,可一旦需要真正去查订单、拉起流程、写回数据,立刻开始花式失败。不是模型一行代码没写好,也不是 API 文档写得不清楚,而是缺了一层把"意图"变成"动作"的稳定结构。我后来花了几个周末把这层单独抽出来重写,做了个项目,代号Agent-Reach,专门解决智能体对真实业务系统的触达问题。
这套东西说复杂也不复杂:定义了一套工具接入协议,加一个路由决策层,再配上执行兜底和全链路日志,让 LLM 只需要专注"该干什么",剩下"怎么干、干不动怎么办"全部由工程结构接管。如果你正在做 AI 应用、Function Calling、RAG 工具调用,或者被"模型怎么又答非所问了""工具明明选了为什么还是失败"这类问题折磨过,这篇内容应该能给你不少可以直接抄的作业。
1. 先看问题:Agent 的"最后一公里"断在哪里
智能体的核心价值不只是"能聊",而是"能办"。用户说完一句话,系统要依次完成意图解析、工具匹配、参数填充、请求发送、结果收口、话术回复。任何一个环节出问题,前面模型再聪明都白费。大多数项目翻车,不是翻在某个单点,而是翻在整条触达链路没有工程化。
1.1 一个典型的卡壳现场
我接手过一个查询场景,用户原话是:"帮我查一下上周三那笔退款到账没有。"
第一版系统跑起来之后,出现了好几种离谱表现。有些时候,模型判断需要查询退款单,但工具清单里只有"查订单",没有"查退款",它只能硬着头皮用订单查询接口假装能查出退款状态。有些时候,工具清单已经补上了"查退款",模型又在参数上犯迷糊,不知道该传退款单号还是订单号。最气人的一次是,模型工具选对了、参数也填对了,工具接口返回了 HTTP 500,模型直接把报错原文抛给用户:"服务器错误:Internal Server Error"。
用户当然一脸懵。他根本不知道什么叫 Internal Server Error,他只知道自己要查的事没查成。
这个案例特别典型:每一环单独看都不是大问题,但串起来,就是一次彻底失败的触达。模型不负责工具注册,不负责参数规范,也不负责错误兜底,它只负责在给定条件下做决策。你让它做的越多,它越容易在没人管的地方自由发挥。
1.2 触达的全链路拆解
我把智能体的工作流拆成四段,每一段对应一类工程问题:
- 意图侧:用户输入进来,先判断这句话属于哪个业务场景,圈定候选工具集。
- 决策侧:从候选工具里选一个,并生成完整的调用参数。
- 执行侧:按协议发请求,处理超时、重试、幂等和并发。
- 反馈侧:拿到结构化结果后,转成用户能听懂的话,并判断是否要继续调用下一步。
模型真正擅长的是决策侧。意图侧可以靠分类或检索兜底,执行侧必须靠代码兜底,反馈侧则需要约束模板兜底。Agent-Reach 管的就是决策之后的所有环节,我把这一整套东西叫"触达层"。
你如果也遇到过"模型说要做 A,结果系统做了 B"或者"模型说做完了,但实际根本没生效"的情况,大概率不是模型笨,而是触达层缺失。
2. Agent-Reach 的三层引擎:接入、路由、执行
Agent-Reach 内部不是一个大模块,而是拆成三层各司其职的引擎:Reach Channel(接入层)、Reach Router(路由层)、Reach Executor(执行层)。每一层解决一类问题,层与层之间只通过结构化数据通信,模型不直接碰底层 API,底层 API 也不关心模型长什么样。
2.1 Reach Channel:把外部能力统一成一种语言
所有外部能力——HTTP 接口、数据库操作、内部 RPC、甚至浏览器自动化脚本——在 Agent-Reach 里都被封装成一个Channel。一个 Channel 对外只暴露三样东西:能力声明、入参 Schema、执行函数。
我用一个订单查询的例子说明。注册一个"查询退款单"的 Channel,核心代码长这样:
register_channel( name="query_refund_order", description="当用户想了解某笔交易的退款状态时,根据退款单号查询退款进度,返回退款状态与预计到账时间", parameters={ "type": "object", "properties": { "refund_id": { "type": "string", "description": "退款单号,用户原话中常以'退款单'或'退单号'形式出现" } }, "required": ["refund_id"] }, handler=query_refund_order_handler, idempotent_key="refund_id", timeout_seconds=5, retry_times=2 )不管底层是调 REST API 还是查 MySQL,上层看到的都是同样的结构。这样做的好处是,LLM 侧只需要面对一份"工具清单",不需要知道目标系统内部长什么样,也不用关心鉴权方式、数据格式转换这些脏活。
我踩过的第一个坑就是,一开始允许每个工具自带一套调用逻辑,结果每接一个新系统都要写一堆胶水代码。统一成 Channel 协议之后,新增一个能力基本就是写一个 handler 加一段 Schema 声明,半小时内能搞定。
2.2 Reach Router:从意图到工具的决策
选工具这件事,有两种常见做法。
方案 A 是把所有工具描述全部塞给模型,让 Function Calling 直接选。工具少的时候没问题,工具一多,上下文变长之后,模型反而容易选错。我实测下来,超过 50 个工具时,全量注入的方案工具选择准确率会出现肉眼可见的下滑。
方案 B 是先做语义检索召回候选工具,再让模型从候选集里做最终选择。Agent-Reach 用的就是方案 B,内部叫"召回-重排-决策"三步:
| 步骤 | 动作 | 说明 |
|---|---|---|
| 召回 | 语义相似度检索,从全量工具里取 Top 10 | 用 embedding 做粗筛,关键词过滤做辅助 |
| 重排 | 结合用户意图和上下文,压缩到 Top 6 | 过滤掉意图明显冲突的工具 |
| 决策 | 把 Top 6 工具描述交给 LLM 选择 | 上下文可控,准确率更稳定 |
这套组合拳跑下来,在我那个 60 多个工具的项目里,工具选择准确率从全量注入的 71% 提到了 93%。召回层牺牲了一点点延迟,换来的是选择和后续执行的大幅稳定。
这里有个细节值得多说一句:重排阶段不要只依赖模型打分,要结合业务规则。比如用户明确说"退款",就把所有不含退款语义的工具直接过滤掉,这不是模型能力问题,是规则兜底。
2.3 Reach Executor:稳定执行与失败兜底
Router 决定"做什么",Executor 负责"做稳"。这里处理的是四件具体的脏活:幂等、超时、重试、结果归一化。
幂等是我早期最惨痛的教训。之前做自动化流程,用户点了一次"创建工单",因为网络抖动,Executor 自动重试了一次,结果系统里生成了两张一模一样的工单。后来所有 Channel 都要求声明idempotent_key,重试之前先检查这个键对应的操作是否已经执行过,执行过就直接返回上一次的结果。
超时也做了分级。单工具调用默认 5 秒超时,超过 5 秒进入降级分支;超过 15 秒直接标记失败,这时候模型不会硬编一个答案,而是明确告诉用户"系统暂时没有响应,请稍后重试"。宁可诚实失败,不能制造幻觉。
结果归一化则是把工具返回的各种格式全部转成统一的{status, data, error}结构。有了这层约束,后面做日志、做指标、做反馈模板,全都省心很多。
3. 最容易翻车的工具协议设计:从 JSON Schema 里学到的事
如果说三层引擎是骨架,那工具协议就是血肉。我后期几乎所有的调优工作,最后都集中在工具描述和参数 Schema 上。这块看起来简单,其实是最容易被低估的地方。
3.1 描述写得越口语,模型越不会用
工具描述不是给人看的说明书,是给模型看的"使用提示"。我第一次写的描述是学术派风格:"按用户信息检索交易记录"。结果模型真的以为这个工具的核心功能是"用户信息检索",实际场景里经常用错。
后来我改成业务导向的描述:"当用户想了解某笔交易的退款状态时,根据退款单号查询退款进度,返回退款状态与预计到账时间。若用户提供了日期而非退款单号,必须主动追问退款单号。"准确率立刻不一样。
参数描述同理。比如日期参数,不要只写"日期",要写"当用户说上周三时,需要把日期格式化为 yyyy-MM-dd 格式后传入"。这叫把业务规则前置到协议层,不让模型自己猜。
3.2 参数 Schema 的粗细:既要能填,又要填对
参数的 Schema 粒度是个平衡活。
填太粗,字段名是transaction_id: string,模型根本不知道去哪拿这个 ID。填太细,每个字段都设成 required,模型在真实对话里拿不到就直接失败。
我后来定下三条经验:
- required 只放必然可得的字段,也就是从用户原话、会话上下文、系统记忆里一定能拿到的信息。
- 枚举值尽量给全,比如状态字段写上
["pending", "processing", "completed", "failed"],模型就不会乱编状态。 - 拿不到但非必需的字段,交给下游容错,不在 Schema 层做强约束。
一句话总结:Schema 的作用是引导模型正确触达,不是逼着模型在信息不足时强行填表。
3.3 容错与降级:宁可明确失败,不要瞎编结果
工具总有异常的时候。关键在异常时返回什么。
我要求所有 Channel 的返回结构里必须带status字段。部分成功也算成功,但要标明"部分结果"。错误信息必须对模型可读,比如:"已超过查询上限,请让用户缩小日期范围再试",而不是"HTTP 400 Bad Request"。
模型拿到可读的错误信息,才可能做出正确的下一步动作,比如换一种方式帮用户解决问题。你给它一段乱码,它就只能把乱码复述给用户看。这点我在项目初期没有注意,白白被用户吐槽了很久。
4. 一次真实接入实录:给内部工单系统加"手"
理论讲完,说一次真实的接入过程。我们接了一个内部工单系统,Agent 需要帮用户查工单、建工单、催办,整套流程走下来,踩了两个很典型的坑。
4.1 第一个坑:三个工具边界不清,"都想管、都管不好"
一开始我按功能拆了三个 Channel:create_ticket、escalate_ticket、query_ticket。测试时发现,用户一句"我的工单怎么还没人处理",模型有时选escalate_ticket,有时选query_ticket,还有时候会调用两次create_ticket,自己给自己新建了单子。
查了半天日志,根因不在模型,在工具边界。escalate_ticket和query_ticket的区分,在用户语境里压根就不明显——催办的本质,在我这个系统里其实就是"查一下进度并标记催办",它俩应该合并成一个动作。
我把催办合并进了查询响应的处理逻辑里,让工具从三个收敛成两个:query_ticket(查询并触发催办标记)和create_ticket(新建工单)。工具选择准确率立刻回升,因为模型的分辨压力变小了。
经验是:**工具设计的粒度应该对齐用户的自然表达,而不是对齐后端接口的职责边界。**接口拆得细没问题,但给模型看到的工具,要按业务场景收敛。
4.2 第二个坑:模型选对了工具,执行还是失败
修完上面那个,又出现一个新的:模型选了query_ticket,参数也填了ticket_id,但系统返回"工单不存在"。
我起初怀疑模型传参有误,把调用日志和用户原话放在一起对比,才发现用户原话是"那单 20240412 的",模型把20240412当成了工单编号。实际上那是日期。
根因不在模型,在参数前置规则缺失。工具描述里没有写清楚"工单号识别规则:优先匹配系统工单号格式,如果上下文里的数字可能是日期、金额等其他信息,必须主动追问确认"。换句话说,是我没有在协议层教育模型"哪些数字可以用,哪些不能猜"。
这次之后,我对"模型问题"的判断谨慎了很多。智能体出问题时,先看执行链路日志,再下结论。很多所谓模型能力问题,其实是触达层缺失参数规范,模型在规则空白区自由发挥。
4.3 给整条链路加追踪:没有日志就没有调优
这个工单系统接入的同时,我把全链路追踪也建好了。一次请求从进来开始,就生成一个request_id,贯穿意图识别、工具召回、路由决策、参数生成、执行调用、回复生成六个环节。每一环节都记录耗时和中间产物。
有了这套日志,我才能看到"模型决策只用了 300 毫秒,工具执行却花了 4 秒还重试了两次"这类真相。没有 trace,所有优化都是拍脑袋。
5. 触达效果怎么度量:关键指标与调优手记
没有量化就没有优化。我最后沉淀了一套度量体系,核心是三个指标,缺一不可。
5.1 三个核心指标
| 指标 | 含义 | 计算方式 |
|---|---|---|
| 工具选择准确率 | 模型选中的工具是否真的对应用户意图 | 人工标注抽样 / 结果回溯 |
| 执行成功率 | 选中工具后,调用是否返回非错误结果 | 按状态字段统计 |
| 端到端满意度 | 用户最终有没有得到想要的答复 | 结合回复模板命中率与用户反馈 |
我的项目里,工具选择准确率从 67% 提到 93%,执行成功率稳定在 97%,端到端满意度一开始却只有 70% 左右。这个数据非常有意思。
5.2 调优优先级
建议不要三个指标一起优化,排序很重要。
第一优先调执行成功率。执行失败大多是协议或参数问题,属于确定性缺陷,工具描述写清楚、Schema 定合理就能治,见效快。
第二调工具选择准确率。核心抓手是工具边界、通道描述以及召回质量。这块需要多个版本迭代,急不来。
端到端满意度最后看。它是前两者的综合,前两个上去了,它大概率会跟着涨。如果没涨,问题往往在反馈侧,就是我下面要说的。
5.3 一个奇怪现象的复盘
工具选择准确率 93%、执行成功率 97%,但端到端满意度只有 70%,这个反差让我查了很久。
翻 trace 发现,部分请求里工具执行完全正常,是模型在把结构化结果翻译成用户话术的时候翻了车。比如工具返回"退款状态:已回退",模型给用户说的是"您的退款已到账"。一个词之差,意思天壤之别。
这说明反馈侧的"翻译"不能只交给模型自由发挥。我给关键 Channel 加了result_template,约束固定字段怎么转述,比如"状态为已回退时,默认话术是'资金已退回原支付渠道,预计 1-3 个工作日到账'"。模型只负责填充变量,不负责重新编剧。加了这层约束后,端到端满意度才被拉回正常区间。
6. 用 Agent-Reach 沉淀下来的几条实战经验
项目跑到现在,我总结了几条实际操作的教训,对做类似智能体项目的团队应该都有参考价值。
6.1 权限和操作确认不能最后补
触达意味着 Agent 有了真实操作能力,创建、修改、删除类的 Channel,权限校验和操作确认必须第一时间设计进去。尤其注意:没有任何保护的高风险操作,用户随口一句气话可能被当成指令执行。我见过用户说"把这单给我取消了算了",如果系统直接就执行删除,后果很麻烦。Agent-Reach 里,高风险动作默认开启confirm闸门:Agent 先说明"我将执行 XX 操作,请确认",确认后再真正调用执行层。
6.2 可观测性越早越好
没 trace 的 Agent 项目,出了问题只能靠猜。从第一天就在每层埋点,哪怕先粗糙一点,也要把链路串起来。关键字段至少包含:请求 ID、会话 ID、工具名、调用参数、返回状态、耗时、重试次数、模型回复摘要。这些字段不一定要一次齐,但日志结构不能散。
6.3 下一步想做的三个方向
这套体系还有很多可以扩展的地方。一是多智能体协作,把每个 Agent 也包装成一个 Channel,让 Agent 之间互相调用、互相兜底;二是动态工具注册,通过配置中心热加载新工具,不用重新发布系统;三是模拟用户压测,用一批典型话术做自动回归,防止新增一个工具之后,把旧工具的选择准确率带崩。
回看这个项目,我最大的体会是:智能体的能力上限由模型决定,但能力下限由触达层决定。模型再聪明,如果工具协议一塌糊涂、执行链路没有兜底、日志追踪一片空白,用户面对的就是一个"想法很多、办不成事"的助手。Agent-Reach 没有什么高深算法,就是把"触达"当作一个正经工程问题对待——定义协议、拆解环节、埋好日志、量化指标。
如果你也在做 Agent 应用,我建议先别急着换更强的模型,把触达层梳理一遍,很多"模型不听话"的问题会自己现形。最后再分享一个小习惯:每次新增一个工具,先用手写脚本直接调用一次底层能力,再用 Agent 跑一遍同样的请求。两边结果对不上时,问题几乎都出在协议描述上,而不是模型上。这个排查习惯帮我省下了大量时间。