开头先讲一个场景。你有没有遇到过这种情况:同一个底座模型,别人搭出来的Agent指哪打哪,你搭出来的Agent问东答西,偶尔还凭空捏造一个成功结果。模型本身的差异可能没那么大,真正拉开差距的,是模型之外那层“触达”能力——也就是从理解意图到真正拿到目标信息、执行完目标动作的整条链路。最近我把一个内部项目收敛成了Agent-Reach,一个专注于解决“智能体能不能可靠触达目标”的轻量框架。这篇文章不打算从Agent概念讲起,直接聊聊我在架构取舍、执行链路和调优指标上的实战经验,希望对正在搭或者准备搭Agent的同学有点参考价值。
Agent-Reach这个名字有两层含义:一是让智能体触达外部工具、内部知识库、以及用户真正关心的结果;二是让整个触达过程可以被度量、被复现、被优化。在项目推进过程中,我把大部分精力从提示词调优转移到了触达链路的稳定性上,因为绝大多数看似“模型不聪明”的问题,根因都在触达层。下面按我在Agent-Reach里实际踩过的路线来展开。
1. 为什么我把Agent-Reach定义成“触达框架”而不是“对话框架”
1.1 对话能力溢出,触达能力稀缺
现在开源社区里的Agent框架五花八门,绝大多数都在解决“怎么和模型对话、怎么组织角色指令、怎么处理流式输出”这一类问题。但真正在企业场景里跑过Agent的人会很快发现,一道最常见的坎是:模型确实理解了用户问题,也确实给出了一个计划,但计划里的每一步能不能真正执行成功,完全是另一回事。
拿一个我实际遇到的例子来说。用户问“帮我统计上个月所有部门提交的报销单里,金额超过五千的有多少笔”。模型端到端可以很流畅地生成一段话,说“好的,我已经统计完成,共有27笔”。但代码里,那个查询接口因为传错了日期格式,压根没有执行;或者更隐蔽一些,接口执行了,但只扫了财务系统里第一页的数据,后面的数据根本没触达。模型没撒谎,它是真的“以为”自己完成了。问题出在模型和真实世界之间缺少一层可靠的触达保证。
Agent-Reach的核心主张是:把“生成对话”和“触达目标”拆开。对话能力负责理解与表达,触达能力负责确认“某个动作是否真实发生、某个信息是否真实到位”。前者已经严重溢出,后者才是稀缺能力。
1.2 Agent-Reach不做“生成”,只做“到达”
设计Agent-Reach时我给自己定的原则很简单,这个框架不负责写优美的回复,不负责调度大模型的角色扮演,它只负责回答一个看似简单的问题:一个目标,从起点到终点,有没有可靠地到达。
这个转变影响很大。一旦你把“到达”当作核心目标,架构上就会出现很多和对话框架不一样的要求。比如对话框架里,模型说一句“好的,我去查询”,任务就算推进了;但在触达框架里,“去查询”只是一个动作意图,必须等工具返回可验证的结构化结果,任务才算往前推了一步。再比如对话框架关心上下文连贯性,触达框架更关心每一步的目标是否明确、是否可验证、是否在超时范围内完成。
我甚至砍掉了Agent-Reach里所有关于“人格设定”的功能。不是人格不重要,而是人格不应该由触达框架来绑定。Agent-Reach只保留四样东西:请求进入后的目标解析、触达路径规划、动作执行、结果验证。其余全部从外部配置接入。
1.3 一套可复用的触达抽象:Source、Router、Actuator、Feedback
为了让“触达”这个概念可落地,Agent-Reach把整条链路抽象成了四个角色,这套抽象在内部跑通之后,我放到其他项目里复用也很顺。
- Source(来源):定义一次触达从哪里开始。可能是用户对话、消息队列里的事件、定时任务产生的信号,甚至另一个Agent发来的内部请求。Source只负责把原始输入转成统一格式的目标对象。
- Router(路由):负责决定这条触达请求应该走哪条路径。Router会根据目标类型、可用工具、历史成功率,把请求分配到直连、级联或广播三种触达模式中的一种。
- Actuator(执行器):真正去调用工具、查询系统、发送消息的组件。Actuator内部有一个状态机,管理每个触达步骤的待执行、执行中、成功、失败、重试、放弃这些状态。
- Feedback(反馈):验证触达结果是否真实有效。它不轻信模型说“成功”,也不轻信工具返回的“200 OK”,而是通过规则、交叉验证、人工兜底等方式确认结果真的符合预期。
举个生活化的类比:如果模型是那个写地址的人,把目的地写得清楚漂亮,那么Source就是包裹进系统的入口,Router是分拣中心决定走哪条运输干线,Actuator是那个真的开车送件的快递员,Feedback则是收件人签字确认。很多人做Agent,把精力全花在“让写地址的人更会写”,却忽略了后面三条链路,结果自然是寄十个包裹丢六个。
2. Agent-Reach的核心链路:一次请求如何被送到终点
2.1 请求进入后的第一站:意图压缩与目标解析
大多数Agent框架上来就让模型直接输出一个“行动计划”,Agent-Reach不一样,它要求先做目标解析:把模糊的自然语言输入压缩成一个结构化的目标对象,然后再谈计划。
目标对象长什么样?我采用的是四字段结构:
{ "goal_id": "goal_20250101_0001", "action": "query", # 动作类型:query / execute / notify / transform "target": "finance_db", # 触达的目标系统 "success_criteria": { "field": "total_amount", "operator": "gt", "value": 5000, "coverage": "all_pages" # 必须覆盖全部分页,这是防止“只查第一页”的关键 } }action字段决定Router要走什么模式的候选集,target字段决定Actuator要查找哪个工具网,success_criteria字段是Feedback用来判断是否“真到达”的依据。这套结构的好处是,模型只负责把用户的话“翻译”成结构化目标,而具体怎么执行交给后面的状态机和路由策略。翻译错了可以靠校验模板和人工反馈修正,比让模型直接编一个完整的执行步骤要可控得多。
2.2 触达路径规划:工具网与依赖关系
目标解析完成后,Router会读取一个“工具网”的注册信息。工具网是一张有向图,节点是工具能力,边是依赖关系。比如“统计报销单金额”这个目标,依赖“登录财务系统”和“读取报销分页接口”,“读取报销分页接口”又依赖“获取日期范围令牌”。
为什么要把工具关系建成图而不是简单列一个清单?因为触达路径的本质是依赖链:一个动作能不能执行,取决于它依赖的前置动作是否已经真实成功。图结构能让Router快速判断,一条路径中哪些前置条件可以并行触达,哪些必须串行等待。
在Agent-Reach早期版本里,我让模型直接生成JSON步骤数组,结果频繁出现“模型写出的第2步依赖第1步结果,但第1步实际返回的是错误对象,模型却按照成功对象继续往下走”的问题。后来改成工具网与状态机结合,只有当前节点的依赖状态全部为success,节点才会进入可执行集合。这一步改动把端到端的任务完成率提升了大概两成。
2.3 执行:带状态机的Actuator循环
Actuator不是一个简单的“调用工具然后返回”,它内部跑的是一个有限状态机循环。这个循环的骨架我用Python描述出来大概是这个样子:
while not executor.is_finished(): plan = executor.next_step() # 从可执行集合里取下一个节点 if plan is None: break tool = tool_registry.match(plan.tool_name) if tool is None or not tool.is_available(): executor.mark_skipped(plan, reason="tool_not_found") continue result = tool.invoke(plan.args, timeout=timeout_ms) verified = feedback.verify(plan.success_criteria, result) if verified: executor.mark_success(plan) else: # 不直接放弃,先走恢复策略 executor.apply_recovery(plan, result, feedback.last_errors())注意几个细节。
一是超时参数必须逐工具配置,而不是全局统一。数据库查询和HTTP请求的超时特性完全不同,统一超时会导致慢工具频繁被打断或快工具长时间挂起。Agent-Reach里,每个工具注册时可以声明自己的建议超时时间,Actuator在执行的时候取一个综合值。
二是mark_success不是看工具是否没有抛异常,而是看feedback.verify的结果。一个HTTP接口可能返回了200,但response body里的data是null,这就不算触达成功。只有success_criteria里的字段和覆盖率条件都满足,状态机才会把节点标记为success。
三是重试必须区分“可重试失败”和“不可重试失败”。权限不足、参数格式根本性错误这类问题,重试一万次也没有意义;而网络抖动、临时限流这类失败,重试几次的回报非常明显。我在工具注册表里给每个工具加了retryable字段,状态机只对标记为可重试的失败做指数退避重试。
2.4 返回链路:如何验证“真的触达了”
执行循环的最后一环是返回链路。很多Agent框架跑完工具调用就直接把result拼进上下文,让模型总结一段话返回给用户。Agent-Reach在这里多了一道关卡:结果必须由Feedback组件做一次“到达验证”,验证通过之后才会写入最终触达记录。
Feedback的验证策略不是拍脑袋定的,我在Agent-Reach里实现了三层验证:
| 验证层级 | 具体手段 | 解决的问题 |
|---|---|---|
| 规则层 | 校验返回结构、数值范围、覆盖率标记 | 防止字段缺失、空值、只查了第一页 |
| 一致性层 | 对同一目标做两次独立触达并比对结果 | 防止数据源返回抖动或缓存不一致 |
| 人工层 | 把低置信度的结果推给人工确认 | 防止规则无法覆盖的语义级错误 |
一致性验证在金融、库存类场景里特别有用。比如查询某个SKU的可用库存,一次触达返回“剩余100”,另一次独立触达返回“剩余50”,那说明这两次触达里至少有一次没有真正落到正确的数据快照上,这时Agent-Reach会标记为触达异常,而不是盲目取平均值。
这套返回链路的代价是增加了一些延迟,但它直接斩断了“模型一本正经地汇报错误结果”这条最让用户失去信任的路径。我个人经验是,宁可让用户多等一两秒,也不能让用户拿到一个漂亮但错误的结果。
3. 三种触达模式的选型:直连、级联与广播
3.1 直连触达:简单任务的最优解
当目标只有一个工具就能完成的时候,Router会走直连触达。比如用户问“当前北京天气怎么样”,工具网里只有一个weather_query节点,那就不需要任何分叉和组装,直接解析目标参数,调用工具,验证结果,返回。
直连触达最容易被低估,因为架构听起来太简单了。但实践证明,直连触达是稳定性最高的模式,也是整个体系的地基。我做Agent-Reach时有一条硬性要求:任何任务,能直连就不要故意拆成多步;一个工具能完成的动作,不要为了“显得智能”而让模型编一个复杂步骤。复杂度每增加一分,失败面就扩大一分。
直连触达的另一个关键点是参数映射。用户说“北京”和工具期望的“101010100”之间需要一个转换层,这个转换层很容易被模型直接幻化。Agent-Reach的做法是内置一个轻量实体库,先做实体归一化,再校验目标系统是否认识这个实体ID,校验不通过就触发实体消歧问答,而不是直接把猜测值塞进工具。
3.2 级联触达:把一个目标拆成依赖链
当目标需要多个工具按顺序协作时,Router会走级联触达。典型场景是“查询某位客户的未结订单并汇总金额”——先要调客户检索工具拿到客户ID,再拿客户ID去调订单查询工具,最后把结果喂给金额汇总工具。每一步的输出都是下一步的输入,任何一步失败,后续步骤都没有意义。
级联触达在Agent-Reach里被实现成一条依赖链,Actuator状态机严格按依赖顺序推进。这里要特别强调一个经验:级联链路上,每一步的success_criteria都必须包含对输入参数的确认,而不仅仅是对输出结构的确认。举个例子,订单查询工具接收customer_id,如果上一步客户检索工具返回了一个相似但不完全一致的客户记录,比如名字相同但有另一个客户ID,订单查询依然会返回一个看似合理的订单列表,但它属于另一个人。这个错误在最终结果里几乎不可能被模型自己发现,因为所有字段看起来都严丝合缝。
我在实践中的解决方案,是把每条级联步骤之间的数据映射声明为一个显式的schema,并且让Feedback检查每条映射是否满足引用一致性。具体来说,就是下游工具收到的customer_id,必须与上游工具返回的那个customer_id完全相等。这个看似“啰嗦”的校验,避免了大量数据串号事故。
3.3 广播触达:多源扩散后的投票收敛
广播触达是最重、成本最高的模式,但它解决的是单个工具无法给出可靠答案的问题。典型场景是“这个供应商的信用评级到底是多少”——单一系统可能没有数据,可能数据过期,可能口径不一致,这时候Agent-Reach会同时触达多个独立数据源,然后在汇聚层做投票收敛。
投票收敛不是简单的少数服从多数。因为不同数据源的口径和时效可能完全不同,Agent-Reach会给每个数据源配置一个权威度权重,再结合数据新鲜度做加权决策。比如银行核心系统的数据权重高于第三方灰名单接口,两小时前的快照权重高于三个月前的快照。
广播触达最大的坑是资源消耗。一次广播可能同时发出去十几个请求,如果每个都等满超时时间,端到端延迟会爆炸。我现在对广播模式的超时策略是“快速失败+慢速兜底”:先给所有请求设定一个比较短的统一超时,比如1.5秒;在等待期间,先到的结果进入缓冲区;超时未返回的按失败处理;最后如果投票收敛不出来,再对高权重数据源发起一次单独的慢查询。这样既保持了活跃度,又不会让一次广播拖垮整个链路。
3.4 选型判断表:什么情况走什么模式
Router不是靠规则硬编码决定模式的,而是根据目标对象里的一组特征做打分判断。我在内部沉淀了一张选型判断表,分享出来供参考:
| 判断维度 | 直连 | 级联 | 广播 |
|---|---|---|---|
| 目标依赖的工具数 | 1个 | 多个且存在依赖关系 | 多个且相互独立 |
| 对一致性的要求 | 中等 | 高 | 极高 |
| 实时性要求 | 高 | 中 | 中低 |
| 预算/成本约束 | 不敏感 | 较敏感 | 极敏感 |
| 典型场景 | 天气、计算器、查汇率 | 客户信息聚合、订单流转 | 信用评估、风险核查 |
需要注意的是,这张表不是死的。我在Agent-Reach里把选型判断本身也做成了可配置策略,允许外部传入一个偏好参数,比如某些成本敏感的渠道会强制把广播模式降级为“单源直连+延迟兜底”,防止每个请求都烧掉大量API费用。
4. 实测中的触达损耗:五个真实踩坑记录
4.1 坑一:模型告诉你“已经成功”,其实什么都没发生
这是最早遇到、也最典型的问题。单元测试里,工具注册表都是正常的,模型无论怎么调都返回预期结果;一上真实环境,模型经常跳步。它可能压根没调用工具,直接靠着系统提示词里的历史样本编了一个答案,也调用了工具但传的参数是错的,结果工具返回400,模型却把400错误对象当作“调用成功”。
排查链路是这样的:先是发现接通率异常,然后我从日志里调出单次会话的完整动作序列,发现工具调用记录里根本没有那条“查询订单”的记录,但最后呈献给用户的文本里却包含了“已查询成功,共找到3笔订单”这样的句子。这让我意识到,模型输出的自然语言不能当作执行结果,只有在动作记录里真实存在的、状态为success的工具调用,才能进入最终上下文。
修复手段是在Agent-Reach里增加了一条硬规则:最终回复模板只能引用那些通过了Feedback验证的触达记录,模型不能自由编造“我查询了”“我完成了”这类描述。凡是模型试图引入未经验证的结论,都会被打回,要求它重新引用触达记录。
4.2 坑二:上下文被无关触达记录污染
一开始为了让模型有足够信息,我把每次工具调用的原始返回全都塞进上下文窗口。结果是该方法在简单任务上表现不错,但复杂任务上越跑越偏:一个需要查询库存的任务,系统里前三次触达是查物流、查价格、查供应商的返回记录,这些无关的数据堆在一起,模型在总结时经常张冠李戴,甚至拿物流时效去回答库存余量问题。
这个问题的本质是上下文里的信噪比太低。Agent-Reach现在的做法是上下文精简策略:工具返回必须经过一个抽取器,只把success_criteria相关的关键字段放进上下文,其余原始结果按需归档,不进入模型视野。对于那些需要中间过程推导的级联任务,我会额外保留一份结构化的“触达轨迹”摘要,而不是原始JSON堆砌。
经过这个调整之后,端到端准确率上升得特别明显。我现在有一个很坚决的判断:上下文里每多一条无关返回,最终结果的出错概率就非线性上升。宁可让模型说“我缺少某项信息”,也不要让它被无关数据干扰着说一个自信的错误答案。
4.3 坑三:超时设置不合理导致全链路雪崩
早期版本给所有工具统一设置了5秒超时。看起来挺合理,但真实环境里有两类请求会把系统拖垮:一是慢数据源的复杂聚合查询,本来需要10秒以上,被5秒超时切掉之后,状态机判定失败并触发重试,重试又等5秒,最终一个查询拖了30秒才返回失败;二是外部服务偶发的高延迟,5秒超时根本没等到恢复窗口,重试全部积压在队列里,导致后面的正常请求也被阻塞。
排错的过程也很有意思。现象是端到端成功率不低,但P95延迟高得离谱。我一开始以为是模型推理慢,后来把延迟拆分到单工具粒度,才发现瓶颈全在几个慢工具的连续超时重试上。修复方式是分工具细化超时设置,并且为重试加上最大并发限制和抖动退避,避免所有失败的请求抱团重试,把下游直接打挂。
这里我要再强调一个细节:超时后不要一味地重试同一路径。Agent-Reach在第二次重试失败后,会触发“路径变更”,也就是Router重新计算一条替代路径。比如主数据库超时了,第二次就去查只读副本;聚合API超时了,第二次就退化成多个独立查询再在应用层聚合。这种“换路不换车”的思路,比死磕同一条路管用得多。
4.4 坑四:权限模型太粗导致工具调用经常被拒
Agent-Reach的Actuator对接了多个内部系统,某些系统的接口要求特定角色令牌。我第一次做权限接入时偷了个懒,把所有服务账号放进同一个大的授权组,结果确实能调用所有接口,但安全审计直接不通过,而且更隐蔽的问题是,某些系统的细粒度权限校验只认特定的上下文标签,我的统一令牌虽然在“能不能调”的层面上通过了,但拿到的数据范围是残缺的。
之后我把权限模型改成了“按目标对象绑定权限”:每个Agent-Reach任务在目标解析阶段就要声明自己将访问哪些目标系统,Router拿着这个声明去向权限中心申请最小化令牌。工具调用时,令牌作用域里只包含必需的目标,不包含无关系统。这个改动虽然增加了申请令牌的耗时,但换来的是调用成功率更稳定,因为每个系统都更愿意给一个“只请求它需要的数据”的令牌放行,而不是一个看起来什么都要的超级令牌。
排查这类问题,最关键的动作是看工具返回的失败码里的sub_code,而不是只看出不出错误。很多内部系统的授权失败藏在sub_code里,主错误码可能被统一包装成了200或500,直接看主码会被误导。
4.5 坑五:目标漂移——一路触达,最终跑偏
目标漂移是我在长链路任务里才注意到的问题。任务的初始目标是“统计上月华东区销售额”,前面几步执行得很好,但到了第五步,Router离线兜底时把“华东区”解释成“华东和华南”,再往后,所有下游数据的口径就都偏了。偏得不多,但如果不用一个锚点去约束,模型和工具都会顺着这个漂移目标一路执行下去,最后返回一个看起来完美、实际上跑偏的结果。
根本原因是目标对象在链路传递过程中被不断重新解释。修复方法是引入目标锚点机制:每一个工具调用的输入参数,都需要和目标对象里的原始约束做一次自动比对。比率对的具体做法是维护一组锚点字段,比如region=“east_china”,要求链路中任何一步对region的重定义都必须显式声明,且必须取得任务级仲裁通过。否则这条路径会被判定为“目标变更”并触发任务回滚,而不是默默接受。
这个机制让我在排查问题时省了无数心力。以前目标漂移非常隐蔽,用户反馈“结果好像不太对”的时候,日志里每一单步看起来都是成功的;有了锚点比对之后,漂移在发生时就会被记录和暴露,问题可追溯性完全不一样。
4.6 排错链路示例:一个完整的排查过程
结合上面几个坑,我列一个综合的排查过程做个演示。有一次线上反馈“某个SKU的可用库存查询频繁返回0,但实际仓库里明明有货”。
第一层排查:看Actuator状态。发现工具确实被调用成功了,返回结果是一个JSON,content里的stock字段是0。这一层没有异常。
第二层排查:看Feedback验证。规则层校验通过,因为stock字段存在,类型是number,且覆盖率标记也是all_pages。所以反馈层也没报错。
第三层排查:看参数映射。我逐步比对Source到Actuator的参数传递链,发现库存查询工具的warehouse_id是从上下文里一个旧的触达记录里取的,那个记录属于另一个不相关的仓库。问题就出在上下文污染导致的参数复用错误:模型把上一次查询某个仓库的warehouse_id直接当作了本次的参数,然后工具查询了一个正确的仓库,但那个仓库的库存确实是0。
修复方式是在参数传递链路上增加来源标注,每一个传入Actuator的参数必须携带一个provenance字段,标明它来自哪个触达记录的哪个字段。Feedback验证时,会检查这个来源是否与当前目标的仓库约束一致。这个排错过程走下来,我只花了一个多小时就定位到了根因,以前没有这套链路的时候,这种问题可能要查一整天。
5. Agent-Reach的调优指标:用触达成功率说话
5.1 核心指标:触达成功率、触达深度与触达时延
对话框架调优爱看语言质量和用户满意度,Agent-Reach的关注点完全不同,我只盯着三个核心指标。
第一个是触达成功率。定义是:一个任务的目标对象最终被Feedback验证为完全满足success_criteria的比例。分子是真正到达终点的任务数,分母是所有进入系统的目标任务数。这个数字不会很高,因为它把“模型回复漂亮但工具没执行”的情况全部视为失败。
第二个是触达深度。这个概念很多人没在意过,指的是一个目标从Source出发到最终验证通过,经过了多少个成功工具节点。触达深度过低通常意味着Router把一个复杂任务过度简化了,比如该走级联却走了直连;触达深度过高则意味着路径规划不够清爽,可能引入了不必要的中间工具,增加了失败概率。我会同时统计平均触达深度和深度分布,来评估Router的规划质量。
第三个是触达时延。这个按触达模式的差异分别统计:直连P50时延、级联P95时延、广播P95时延三者完全不是一个量级,放在一起没有任何意义。Agent-Reach优化时延的方法不是盲目提速,而是先看时延分布里哪个环节贡献最大,是模型结构化输出太慢、工具本身慢、还是重试机制引入了额外延迟。
5.2 用一个综合公式来评估触达质量
三个指标单独看都容易误导,我把它们组合成一个综合触达质量指数,内部叫T-Reach:
T-Reach = (触达成功率) / (1 + 平均触达深度 * 0.1 + 归一化触达时延 * 0.2)公式本身没有复杂理论,它的意义在于惩罚“用深度和时延换成功率”的情况。比如某个任务把所有工具都广播一遍,成功率可能略微提升,但深度和时延都上去了,T-Reach反而是下降的。这样调优时就能逼着自己去寻找那条“最短的成功路径”。
需要特别提醒的是,T-Reach不适合作为线上实时指标,它的价值更多是在离线评测和版本迭代对比时使用。线上监控我更推荐直接盯原始三个指标,再配合一个“零真实结果率”:回答里没有任何经Feedback验证触达记录的任务占比,这个比例一旦超过某个阈值,说明链路出现了系统性故障。
5.3 调优顺序建议和个人体会
如果要我给一套可执行的调优顺序,我的建议是优先处理零真实结果率,再提触达成功率,最后才压时延。因为一个经常拿捏造结果糊弄用户的Agent,对话再流畅也没有意义;成功率不稳定的时候去压时延,会把脆弱的路由策略进一步推向崩溃边缘。
具体操作上,第一步先给链路全面打点,记录每一个Source触达记录、每一步工具调用状态、每一次Feedback验证结果和失败原因。第二步针对失败原因做帕累托分析,把TOP 3失败类型专项治理。第三步才是调整路由策略和超时参数。
Agent-Reach这个项目目前还在持续迭代,我下一步打算把Router的选型策略从静态规则升级成离线召回模型,把历史触达记录作为训练数据,让系统自己学习“哪条路径在当前上下文下更容易成功”。不过这些都是后话了,当前最实用的建议仍然是先把触达链路打点打全、把Feedback校验规则夯实。别急着上复杂架构,先把一条请求从进入到验证通过的每一跳都看清楚,Agent的质量自然会上去。