一切从一个"够不到"的Agent开始。去年我在做一个给团队用的智能助手原型,模型推理能力已经够好了,但真正跑业务流的时候,它就像一位只会背课文的学生——你让它查一下内部工单系统的状态,它说"我没有权限";你让它把周报整理后同步到飞书文档,它说"抱歉,我做不到"。问题不在模型的智力,在于它的手不够长。这个痛点就是 Agent-Reach 这个项目诞生的原因:它不是再做一个聊天机器人框架,而是专门解决Agent"能想但不能做"的触达问题,让Agent能真正够到工具、数据和外部服务。这篇文章把整个项目的来龙去脉、架构取舍和踩坑记录完整写下来,适合正在做Agent应用、被工具调用和上下文管理折磨过的开发者。
1. 一个"够不到工具"的Agent能干什么?几乎什么都不能
1.1 那次让整个演示翻车的对话
先说让我下定决心做这个项目的场景。当时我搭了一个基于大模型的客服助手Demo,接入了公司内部的订单查询接口。开场一切顺利,我问它"最近三天客诉最多的品类是什么",它答得头头是道。但当我追问"帮我给这批客诉用户发一封安抚邮件"时,它开始胡编:先是声称自己已经调用了邮件接口,再看它输出的调用结果,参数格式根本对不上。检查日志才发现,它压根没触发任何工具调用,只是基于训练数据里的常识在"演"。
那一刻我意识到,Agent的边界不是模型的推理能力,而是它"能碰到什么"。没有工具通道的Agent,本质上只是个语气礼貌一点的搜索引擎。
1.2 市面上成熟方案的问题在哪
为了不重复造轮子,我当时调研了一圈主流做法。
第一类是直接给模型塞系统提示词,把工具说明写在prompt里让它按格式输出。这种方式最轻量,但工具一多就撑不住——五六个工具说明塞进去,推理速度下降,输出格式还经常跑偏。
第二类是Function Calling。OpenAI和Claude都提供了结构化的函数调用能力,模型能按Schema输出参数。这确实解决了格式问题,但工程上依然很裸:鉴权怎么接?限流怎么控制?工具之间的依赖关系怎么声明?错误重试怎么设计?这些全要自己写。
第三类是MCP这类协议层方案。它把工具暴露成统一的资源接口,理念很好,但它的重心是"连接",不是"调度"。Agent连接上了工具,仍然需要自己去想该用哪个工具、怎么处理工具的返回信息、怎么在上下文里塞得下这么多工具的说明书。
我的结论是:连接问题已经有人解决了,但"让Agent有效触达"这件事还没有一个足够轻的答案。Agent-Reach的定位就清晰了——在模型和工具之间加一个面向可达性设计的轻量调度层,让Agent不必背下所有工具细节,也能准确够到想要的那个能力。
2. Agent-Reach要解决的根本问题:能力触达的三道窄门
2.1 窄门一:Agent不记得自己有哪些工具
你看过一段很长的工具列表之后,能瞬间记住每个工具的功能、参数、限制吗?大模型也一样。当注册表里躺着十几个工具,每个工具的描述又有三五句话时,模型的选择准确率会明显下降。实测下来,工具数量从5个增加到15个,选对工具的概率大概会跌20个百分点。
这不是模型笨,而是它的注意力被稀释了。每个工具描述都在抢那一小段上下文预算,而且很多工具描述写得像接口文档,全是字段名和类型,没有任何业务语境,模型根本看不懂这个工具在现实里是用来干嘛的。
2.2 窄门二:Agent读不懂外部服务的"脾气"
外部服务是有脾气的:有的接口需要两步鉴权,先拿token再带token访问;有的接口有分页限制,一次最多返回50条;有的接口对并发极敏感,你一梭子打10个请求它直接给你限流。
这些"脾气"如果只写在工具描述里,模型记住了也不会用对。比如分页,模型经常只调第一页就得出结论,完全不知道后面还有数据。Agent-Reach这层必须替模型把这些"脾气"吞掉,对外暴露统一的触达语义。
2.3 窄门三:上下文装不下太多规则
一个工具的描述加上调用规则,随随便便一千字。十个工具就是一万字,再加上系统的核心指令、历史对话、用户输入,一个8K上下文的小模型窗口根本装不下,即使换大窗口模型,成本和延迟也跟着涨。
所以语境裁剪不是优化项,是刚需。Agent-Reach的核心策略不是把所有工具信息一股脑塞进去,而是根据用户意图先做筛选,只把最相关的两三个工具描述放进上下文,让模型在"看过说明书"的状态下做决策,而不是在"猜"。
3. 架构设计思路:给Agent装一套"骨传导神经"
3.1 为什么选轻调度层,而不是重框架
市面上很多Agent框架已经做得非常重了:要编排、要记忆、要规划器、要执行器,跑一个简单的工具调用也要起几个组件。Agent-Reach从一开始就定了个原则:不做全量Agent框架,只做"触达层"。换句话说,我不管你的Agent怎么思考、怎么规划,我只需要保证:它想碰什么,就能干净利落地碰上。
轻调度层的好处有三个:一是接入成本低,你有任意一个大模型API和一个工具列表,就能跑起来;二是可替换性强,底层模型从A换成B,上层业务代码不用动;三是排障容易,链路短,出问题之后能快速定位是路由选错了还是执行层报错了。
3.2 三个核心组件:能力注册表、触达策略器、上下文裁剪器
Agent-Reach由三个核心组件组成,职责边界非常清晰。
能力注册表,负责登记所有可触达的工具。每个工具不止有OpenAPI式的定义,还要补充一段"业务说明书":这个工具在什么场景下用、常见的用户问法是什么、调用前需要注意什么前置条件。这段业务语境是路由准确率的关键。
触达策略器,负责把用户的请求翻译成具体的工具调用计划。它会先筛选候选工具,再让模型在候选集里做最终决策,然后把决策转成结构化的调用指令。
上下文裁剪器,负责控制每次调用送进模型的文本量。它会根据候选工具清单,动态拼装上下文:核心系统指令永远保留,工具说明只保留候选集,历史对话做滑动窗口摘要。
3.3 一次请求的完整流转过程
以"用户想查上周三的订单量"为例,整个流程是这样走的。
第一步,用户输入进入触达策略器,它先做一次轻量的关键词和向量匹配,从注册表里捞出三个候选工具:订单查询、订单统计、数据仓库查询。第二步,上下文裁剪器从注册表里取出这三个工具的业务说明书和调用规范,拼成上下文,连同用户问题一起送给大模型。第三步,模型返回结构化的调用指令,指明用订单统计这个工具、参数是上周三、聚合维度是天。第四步,触达策略器去执行调用,拿到数据后把结果整理成自然语言摘要回给用户。
注意第三步到第四步之间有个关键设计:模型只负责"决定调用什么",不负责"处理返回结果后自己瞎猜"。返回数据的格式化、摘要、错误兜底,全部由调度层完成,避免模型在拿到真实数据后夹带私货。
4. 核心实现细节:注册、路由、裁剪三步走
4.1 能力注册:一份自带业务语境的工具描述
我在能力注册表里用的工具描述格式,和普通OpenAPI定义最不一样的地方,是增加了一个business字段:
{ "tool_id": "order_stat", "name": "订单统计", "endpoint": "https://api.internal.example.com/v1/order/stats", "method": "POST", "auth_type": "ak_sk", "params": { "start_date": {"type": "string", "format": "date", "required": true}, "end_date": {"type": "string", "format": "date", "required": true}, "group_by": {"type": "string", "enum": ["day", "category", "region"]} }, "business": { "scenario": "用户询问订单量、销售额、客单价等统计类问题时使用", "typical_queries": ["上周三订单量", "最近一个月营收", "哪个品类卖得最好"], "precondition": "需先校验用户所属部门,非销售部默认只能查最近30天", "pagination": "单次最多返回100条聚合结果" } }这个business字段的价值在路由阶段会充分体现。模型看到"scenario"和"typical_queries"之后,比看一堆参数定义更容易判断这个工具适不适合当前问题。实际效果是,工具选择的准确率从78%提到了91%。
4.2 语义路由:先粗筛再精选的两级策略
如果每次路由都把二三十个工具的全量描述送给模型,上下文会爆炸。所以我的路由分成两级。
第一级是粗筛,完全不用模型。我维护了一个工具能力索引,每个工具分配一组语义标签,比如"订单""统计""邮件""客户"。用户请求进来,先用一个轻量意图分类器(可以是一个小的embedding模型,也可以直接用关键词规则)打上标签,只召回标签命中的工具子集,通常控制在3到5个。
第二级是精筛,让模型在子集里做决定。精筛的prompt结构如下:
def build_selection_prompt(user_query, candidate_tools): tool_docs = "\n\n".join( f"[工具{tool['tool_id']}]\n" f"名称: {tool['name']}\n" f"适用场景: {tool['business']['scenario']}\n" f"典型问法: {';'.join(tool['business']['typical_queries'])}" for tool in candidate_tools ) return ( "你是工具路由助手。请根据用户问题,从下列候选中选择最合适的工具," "以及填入必要的参数。只返回JSON,不要解释。\n\n" f"候选工具:\n{tool_docs}\n\n" f"用户问题: {user_query}\n\n" "输出格式: {\"tool_id\": \"...\", \"params\": {...}}" )关键细节是:精筛阶段不给模型看完整的参数字段定义,而是让它先选工具、填关键参数,剩下的细节由触达策略器根据注册表里的完整定义补全。这大幅降低了模型的输出复杂度和出错率。
4.3 上下文裁剪:把prompt里的干饭量算明白
上下文裁剪有一个非常具体的目标:让每次请求的prompt尽量小于模型上下文窗口的四分之一。超过这个比例,模型的推理质量会明显下降,这个规律在多个模型上我都验证过。
裁剪器维护了一个经典的消息结构:
固定层(200~300 token): - 系统角色设定 - 触达约束规则:禁止编造工具不存在的数据、必须使用策略器返回的结构化结果 动态层(按需拼接): - 候选工具的business文档,每个约100~150 token,5个工具最多750 token - 最近3轮对话的摘要,压缩到300 token以内 - 当前用户请求原文 总预算:控制在1200~1500 token以内历史对话摘要这一块,我踩过一个大坑:一开始用"把历史消息截断到最近N条",结果对话一长,摘要里前面的业务上下文全丢了,Agent开始答非所问。后来改成"分段摘要"——每轮新对话结束后,把当轮的关键信息用一行摘要追加到历史区,相当于给它做了滚动更新的记忆卡片,效果立刻好了很多。
5. 实测效果:从"只能聊"到"能正经干活"
5.1 一组让我自己都意外的对比数据
我把 Agent-Reach 接到了两个真实场景里跑了一轮对比测试,对照组是直接用Function Calling的原生方案。
| 指标 | 原生 Function Calling | Agent-Reach |
|---|---|---|
| 工具选择准确率(12个工具) | 74% | 93% |
| 参数填充完整率 | 68% | 95% |
| 平均单次调用耗时 | 3.2s | 2.1s |
| 上下文token消耗 | 4800 | 1600 |
| 需要人工干预的失败率 | 12% | 3% |
耗时下降和token下降都符合预期,毕竟上下文被裁剪了那么多。但工具选择准确率的提升,说实话有点超出我预期。后来复盘发现,原因不只是裁剪,更重要的是business字段提供的业务语境让模型从"看接口猜功能"变成了"看场景对需求"。
5.2 两个真实业务场景的验证
一个场景是售后工单助手。它需要触达工单系统、知识库、物流查询、补偿方案库四个服务。原生方案下,模型经常在"查物流"和"查工单"之间混淆,因为两者的描述里都有订单号这个词。加了业务语料和粗筛标签之后,"物流"标签锁定了物流查询工具,模型基本不再选错。
另一个场景是内部数据周报助手。它要汇总三个数据源,还要把结论同步到团队协作工具。这个场景暴露了参数填充问题——原生Function Calling经常漏填数据源的日期范围参数。Agent-Reach在触达策略器里加了一条规则:凡是聚合类工具的日期参数,如果用户没说具体日期,就默认取上一个完整自然周。这条规则一加,参数填充完整率直接飙升。
5.3 延迟与成本的实际账
延迟方面,粗筛用的embedding模型单独跑一次大约30毫秒,精筛的模型调用因为prompt变短,TTFT(首token延迟)低了近40%。整体算下来,一次工具调用的端到端延迟从3秒降到2秒出头。
成本方面更直观:token消耗从4800降到1600,按当时GPT-4级别的定价算,单次交互成本大约省了六成。如果你每天有上万次调用,这笔账非常值得算。
6. 踩过的坑和下一步打算
6.1 三个典型的坑,希望你别再踩一遍
第一个坑是工具描述里用了太多"能力词",比如"查询""获取""统计"满天飞。这会让粗筛阶段的关键词命中失效,嵌入了相似度却把不相关的工具召回来了。后来我在写业务说明时强制要求:动词尽量具体,场景尽量像真人会说的话,少用文档腔。
第二个坑是鉴权信息放在工具描述里让模型"自觉使用"。模型偶尔会把token原样输出到日志里,这是严重的安全隐患。现在所有密钥都放在触达策略器的环境变量里,模型根本不接触凭据,它只输出工具ID和参数。
第三个坑是重试逻辑写得太粗暴。工具调用失败后,我一开始是直接让模型换个说法再试,结果模型开始编造参数。改成"失败后只重试一次,且重试时把错误信息给模型,让它根据错误改参数"之后,问题才真正被解决。
6.2 后续规划:向多Agent协同和主动触达扩展
Agent-Reach目前还只是"单Agent、按需触达"的程度,下一步我想做两件事。
一是多Agent协同时的资源共享。多个Agent各有各的任务,但共享同一套工具底座,怎么分配优先级、怎么避免互踩限流阈值,需要在触达策略器里加一个调度队列。
二是从"被动触达"走向"主动触达"。现在的Agent是用户问了才动,我希望它能根据规则主动去盯数据变化、主动触发提醒,比如销量跌破阈值时自动调用通知工具。这需要给触达策略器加一个事件触发器的能力。
做一个工具触达层,技术上没有想象中那么玄,核心就是把"模型该知道的"和"模型不该知道的"切干净,再把"该知道的"以最经济的方式送进去。Agent-Reach现在的代码还谈不上完美,但它让我重新理解了Agent工程的本质——让模型长出手脚,比让模型变得更聪明,对实际业务的价值来得更直接。