☰
多智能体触达层Agent-Reach:从能力注册到可靠路由的落地实践
2026/10/27 23:08:56 网站建设 项目流程

做多智能体系统做到第三周的时候,我发现团队里最吵的词不是“提示词”,也不是“模型幻觉”,而是“我的智能体怎么调不到你的智能体”。每个人都有自己部署的Agent,能查日志、能写代码、能处理工单,但真要它们配合干活时,谁都没有对方服务的准确地址,接口格式也各搞一套。我后来做了一个叫Agent-Reach的触达层,解决的就是这个最朴素却最磨人的问题:让任何一个Agent能被其他Agent找到、连上、可靠调用。这篇文章会把Agent-Reach从设计到生产落地的完整过程,以及中间踩过的坑,都整理出来。如果你正在做多 Agent 编排、智能体网关,或者只是想把两三个Agent连起来协作,这篇内容应该能帮上忙。

1. 为什么多智能体系统里“触达”反而成了最棘手的问题

先说结论:单个Agent的时候,大家关注的是模型效果、上下文长度、工具调用能力;但Agent一多,问题就变了——系统里每个Agent都像一个独立团队,各自部署、各自写代码、各自定义接口,结果谁也不知道还有谁在、谁能干什么、怎么找对方。这种“智能体孤岛”才是多Agent系统上线之后最先撞上的墙。

1.1 智能体孤岛:每个Agent都在单打独斗

我们当时的实际情况是这样的:A组做了一个日志分析Agent,输入是一堆日志,输出是异常根因候选;B组做了一个工单处理Agent,能自动归类、分派和回复工单;C组做了一个代码审查Agent。单跑都没问题,但等我们把它们接到一起时立刻傻眼。

工单处理Agent需要调用日志分析Agent来给工单附一个“根因判断”,结果两边的人协调了一周才接上。原因很真实:日志Agent的接口是内部HTTP端口,工单Agent的调用方根本拿不到那个内网地址;日志Agent用的是“一次性传全部日志”的同步接口,而工单场景需要边拉日志边出结论;更麻烦的是两边对“错误”的理解不一样,日志Agent返回404表示“没日志”,工单Agent把它当成了“服务不存在”,直接把请求重试了三遍。

这种问题本质上不是某一个Agent做得不好,而是整个系统缺了一层“连接基础设施”。每个Agent都在单打独斗时,功能再强也就是一个信息孤岛。

1.2 “触达”这件事的三个维度:可发现、可连接、可互操作

我后来把AIlog这个问题拆成三个维度,Agent-Reach的所有设计都是围绕这三个维度展开的。

可发现(Discoverability):我怎么知道系统里有谁?谁能做日志分析?谁有权限查财务数据?这需要一个类似“能力目录”的东西,而不是靠人肉写文档。

可连接(Connectivity):找到对方之后,怎么建立通道?传输协议是什么?断线了怎么办?连接怎么保持?针对Agent之间频繁的短连接和长任务流式通信,连接层必须能同时扛住两种模式。

可互操作(Interoperability):连上之后,双方对消息格式、错误码、超时语义、数据Schema的理解是否一致?这个问题最容易被低估。往往是接口打通了,数据却对不上;对上了,错误处理又各写各的。

这三个维度缺一个,Agent之间就谈不上真正的“触达”。只解决可连接,充其量是互通了地址,但两个Agent还是会因为语义不一致而互相“鸡同鸭讲”。

1.3 Agent-Reach想清楚的第一件事:只做连接,不做编排

刚开始跟团队讨论Agent-Reach时,大家很自然地问:要不要顺带把编排逻辑也做了?比如“先查日志,再更新工单,最后推送通知”。我反对了这个想法,坚持Agent-Reach只做“触达层”,不做“编排层”。

这两者的边界在于:编排关心的是“流程怎么走、先调谁后调谁、失败怎么重试”;触达关心的只是“A怎么找到B、消息怎么可靠地送到B手里”。如果把这层逻辑混在一起,调度规则和连接细节会纠缠不清,业务一改,连接层也得跟着改。

我的原则是让Agent-Reach更像电话交换网络:它知道谁在和谁通话、通话持续多久、质量如何,但它不关心你们在聊什么、聊完要不要再去打第三个人。编排逻辑可以完全建在触达层之上,这样Agent-Reach就能沉淀成一套通用基建,而不是某个业务的专用工具。

2. 能力声明与注册:让每个智能体先把自己“讲清楚”

想让人能被找到,前提是这个人得先自我介绍;想让Agent能被找到,前提是Agent得用一种机器能读懂的方式自我描述。Agent-Reach的第一个核心组件是Capability Manifest(能力清单),它是整个触达层的“名片”。

2.1 Capability Manifest:一张能让机器读懂的自我介绍

每一份Manifest都声明了一个Agent的身份、地址、能力边界和约束。一开始我们写得很随意,后来发现不行,于是逐步收敛成下面这套结构:

agent_id: "log-insight" endpoint: url: "ws://agent.internal/log-insight/socket" protocol: "agent-reach/1.0" identity: pubkey_fingerprint: "sha256:1a2b3c..." capabilities: - name: "log_anomaly_detect" description: "从日志流中检测异常模式,返回根因候选列表" call_type: "stream" input: type: "object" properties: logs: type: "array" items: { type: "string" } window_minutes: type: "integer" output: type: "object" properties: anomalies: { type: "array" } constraints: max_concurrency: 4 budget_ms: 120000 cost_level: "medium" negative_capabilities: - "不执行任何数据写入操作" - "不处理非日志类文本数据"

字段并不复杂,但每个字段都有用。agent_id负责唯一身份;endpoint告诉别人怎么连;capabilities是核心,其中name是用来做索引的关键字,description用来做语义匹配,input和output的Schema用来做参数校验和路由过滤,constraints里的并发上限、耗时预算、成本等级则交给路由引擎做调度。negative_capabilities是我踩坑之后加进去的,后面会专门讲为什么它很有用。

打个比方,能力声明就像招聘网站上的岗位JD:职位名给索引用,职责描述给人看,任职要求用来筛简历,还得写清楚“不做什么”,不然什么人都来投,最后全是不匹配。

2.2 注册与心跳:在线状态不是玄学

有了Manifest,Agent启动时的第一步就是向Agent-Reach的注册中心上报。完整流程是:

  1. Agent SDK读取本地Manifest文件;
  2. 向注册中心发起身份握手,用Agent私钥对注册请求做签名;
  3. 注册中心验签、校验Manifest格式与权限策略,把Agent写入能力目录;
  4. Agent开启心跳,定时上报存活状态和负载信息;
  5. 注册中心发现异常时主动下线Agent,并通知给所有订阅方。

在线状态看起来简单,但实现时全是细节。默认情况下,我设置了10秒一次的应用层心跳,连续3次没有收到心跳,就把Agent标记为“可疑”;再连续3次没有收到,才会标记为“离线”。为什么不把心跳间隔设成5秒?因为Agent数量一多,频繁的心跳本身就会占用不少带宽和处理器资源;为什么不设成30秒?因为故障转移的感知速度会太慢,一次路由重试要等半分钟,用户早就超时了。

还有个很容易踩的坑:传输层心跳和应用层心跳要区分开。WebSocket底层可以一直保活,但Agent进程可能已经阻塞在模型推理上不能响应业务消息了。所以Agent-Reach的做法是:传输层心跳负责保连接,应用层心跳负责判断“这个Agent逻辑上还活着吗”,两层不能互相替代。

2.3 健康度与负载信息上报:路由决策的地基

注册中心里只有“在线/离线”两个状态是远远不够的。在线不代表可服务,一个Agent完全可能在线,但已经被某个大请求打到CPU满载、消息队列堵到冒烟。所以Agent-Reach的心跳消息不只是“我活着”,还会顺带携带负载状态:

{ "ts": 1712345678, "agent_id": "log-insight", "load": 37, "queue_depth": 2, "success_rate_15m": 0.996 }

load是0到100的负载等级,queue_depth是当前排队中的请求数,success_rate_15m是最近15分钟触达调用的成功率。这三个字段不需要实时精确到毫秒,10秒上报一次就已经足够路由引擎做决策了。真正让我后怕的是,如果只看“在线”状态,路由会把大量请求派给一个已经忙不过来的Agent,然后整个调用链都被拖慢。有了负载和成功率数据,路由才能避开“僵尸可用”的实例。

3. 连接协议与路由引擎:从“按名字找服务”升级到“按能力找智能体”

光有注册中心还不够,Agent-Reach真正的核心是连接协议和路由引擎。如果把注册中心比作“通讯录”,那连接协议就是“电话线路”,路由引擎就是“总机转接逻辑”。三者缺一,触达都落不了地。

3.1 一种能承载同步、异步和流式的触达协议

Agent之间的调用场景比传统服务间调用更复杂:有的请求适合同步问答案,有的任务要异步跑很久,有些输出需要边生成边返回。Agent-Reach选择基于WebSocket做双向通道,并自定义了一套消息信封格式:

字段含义
msg_id消息唯一ID,用于幂等
trace_id全链路追踪ID
from_id / to_id源和目标Agent标识
target_capability请求触达的能力名
typerequest / response / submit / event / frame / ack / error
payload实际业务负载
expires_at消息截止时间,过期自动丢弃

同步请求走request和response,比较适合“帮我查一下当前余额”这种快速问答;异步任务走submit,先返回一个ack和任务ID,任务完成后再通过event把结果推给调用方,适合“帮我生成一份季报”这种长耗时操作;流式输出则把一段完整的响应拆成多个frame,接收方拼完最后一个is_last=true的帧才算完整收到,适合“边分析日志边输出根因候选”。

协议本身不复杂,但有了这套统一的信封,Agent之间就不需要再为“传输层格式”扯皮。大家只要实现Agent-Reach的SDK,通信语义就自动对齐了。

3.2 路由策略:意图匹配、就近调度和故障转移

路由引擎要做的事情,是把一次“我想用某某能力”的请求,转成“具体调用哪个Agent实例”。路由过程分四步:

第一步是意图匹配。首先按target_capability做精确匹配,但现实往往是模糊的——调用方自己都可能说不准该用哪个能力名。比如一个请求想“分析财务数据”,而系统里有两个能力都叫data_query,一个查业务库,一个查工单库。这时路由就不能只看能力名,还要结合Manifest里的description和请求上下文做语义匹配。我们在实现中引入了向量化检索:把能力描述向量化,请求进来后先算语义相似度,再结合精确匹配做召回。

第二步是约束过滤。把并发已满的、负载过高的、当前成功率太差的Agent踢出候选;如果请求带了数据域约束,比如data_domain=finance,则只保留声明支持该域的Agent。

第三步是打分排序。打分公式大致是这样:

score = w1 * (1 - load / 100) + w2 * latency_norm + w3 * success_rate + w4 * affinity

w1到w4是权重,latency_norm是最近调用P50延迟的归一化值,affinity是数据亲和性的加分项——比如目标Agent本地缓存了相关上下文,就给它加一点分。

第四步是产生候选集。系统不会只挑第一名,而是保留前3个候选,当首选调用失败时自动重试候选集。这一步太关键了,网络抖动和单点故障是常态,没有候选集,一次路由失败就只能抛给上层人工处理。

3.3 会话管理:哪些状态该放在哪个层

Agent之间往往不是单次请求就结束,而是需要在一个会话里多次交互。Agent-Reach的会话管理器负责维护:session_id、会话状态机(connecting / active / idle / closed)、空闲回收时间。

我要特别说明一个边界:会话管理器只管“这个通话还活着”,不负责保存业务上下文。Agent和Agent之间聊到第几轮、记忆了什么、临时缓存了什么,那是双方业务自己的事;Agent-Reach只知道会话现在还活跃、有多久没消息了。这就好比电话公司会记录通话时长和挂断时间,但它不知道你们通话内容是什么。

参数方面,默认情况下空闲会话在15分钟后回收;长任务场景下,调用方可以申请lease续租,在任务开始时声明预计时长,会话管理器据此把空闲阈值延长,避免流式任务跑到一半被误杀。同时所有消息用msg_id做幂等键,重复投递由接收端去重,路由重试才不会引发重复处理。

4. 身份认证、权限边界和可观测性:连接变容易之后,麻烦也变多了

连接变得顺畅之后紧接着就是安全问题。以前Agent之间靠人肉维护地址,访问其实靠“网络隔离”硬扛;现在Agent-Reach把每个Agent都暴露在统一的触达网络里,攻击面和失控面都被放大了。没有身份认证、权限控制和全链路追踪,这网越连越危险。

4.1 让每个Agent都有可验证的身份

Agent-Reach给每个Agent发了一套非对称密钥,注册时提交公钥,私钥留在Agent本地。每次发起触达请求时,消息都用私钥签名;接收方从注册中心拿到公钥做验签。这样每个消息都有不可抵赖的来源身份。

选择应用层签名而不是完全依赖mTLS,主要是考虑跨环境部署:Agent可能跑在不同机房、不同云、甚至不同团队的Kubernetes集群里,证书签发和信任关系管理在跨组织边界时成本很高。统一在应用层做签名,无论底层网络怎么变,我都能把“谁对谁说了什么”这件事完整审计下来。

密钥轮换也不能忘。我们默认90天轮换一次私钥,轮换后旧公钥在注册中心保留7天再清理,避免刚刚发出的消息在途时验签失败。

4.2 权限模型:能力粒度授权

最简单的想法是“Agent A能调用Agent B”这种节点级权限,但我很快就放弃了。因为不同能力意味着不同风险,A可能被允许调用B的“查余额”,但绝不能被允许调用B的“执行转账”。Agent-Reach的权限模型是RBAC加能力级规则,策略集中管理:

policies: - principal: "agent:ops" allow_capability: "data_query" constraints: data_domain: ["finance"] max_qps: 10

这条策略表示agent:ops这个调用方只能触达data_query能力,且请求里的数据域只能是finance,每秒最多10个请求。这个约束是在运行时校验请求参数之后才生效的,而不只是看调用方是谁。

机器到机器之间的调用没有人在屏幕前盯着,所以权限边界必须按“最弱Agent”来设计:任何一个Agent被攻破,都不能让它变成穿透全网的中转点。为了这个目标,我宁可策略配得严一些,也不想等出了事故再补救。

4.3 trace贯穿触达全过程:出了问题十分钟内定位

多Agent系统出了问题时最痛苦的不是“怎么修”,而是“问题到底在哪一段”。一个请求会经历注册中心查地址、路由器做匹配、接入层转发、目标Agent处理、响应回传等多个环节,任何一个环节出问题都会导致最终失败,而失败的Agent往往觉得自己是无辜的。

Agent-Reach从上到下统一打了trace_id,每跳转一次就记一条span,日志长这样:

{ "ts": 1712345678, "trace_id": "tr-7f3a", "span": "router.match", "agent_id": "log-insight", "msg_id": "m-8891", "duration_ms": 12 }

这套链路直接接进了OpenTelemetry,配合Prometheus做指标告警。我给的告警阈值很直接:触达成功率低于99%要告警,P99延迟超过2秒要告警,单次请求路由重试超过3次要告警。

有一次线上问题让我彻底信了trace的必要性:A Agent反馈调用B Agent超时,B团队咬定自己没收到请求,两边差点吵起来。最后拉出trace,发现路由把“财务数据查询”匹配到了一个能力名相似但数据域不符的C Agent,C处理完了,响应却在回传时丢了。也就是说B从头到尾就没见过这个请求。没有trace,这个问题只能靠两个团队互相猜忌收场。

5. 真实落地记录:从Demo到生产会遇到哪些坑

Agent-Reach从Demo跑到生产环境大概用了三个月,期间踩了不少坑。每一坑都让我对“触达”这件事的理解更深一层,这里挑四个最有代表性的记录下来。

5.1 能力清单写得“太满”导致的幻觉路由

第一个坑是在Agent上线初期。为了让市场展示时显得能力强,团队在Manifest里写了很多描述性的能力词,比如“数据分析”、“智能查询”。结果路由引擎频繁把这些模糊请求匹配到能力最接近、但实际上下文完全不符的Agent上,导致调用失败率暴涨。

比如有一次请求“分析客服工单数据”,系统里有一个报表Agent和一个日志Agent。报表Agent的Manifest里写了“数据分析”,日志Agent也写了“日志数据深度分析”,于是路由把请求分给了日志Agent,结果日志Agent拿客服工单数据当日志做了一通分析,输出完全没法用。

解决办法有两步。第一步是把Manifest里的描述改得极其具体,在description里写明输入数据源的边界和预期输出,绝不写模棱两可的营销词;第二步是在Manifest里增加negative_capabilities,明确声明“不处理非日志类文本数据”、“不执行任何数据写入操作”。路由匹配时,这两个字段会被当成硬约束,直接过滤掉不匹配的候选。

这一条我强烈建议每个做Agent接入的人都记住:能力声明不是越丰富越好,而是越精确越好。模糊描述最后坑的一定是路由和调用方。

提示:写能力清单时,宁可在description里多花两句话说明边界,也不要为了“展示能力”把服务描述得大而全。语义匹配在边界清晰的文字上才会可靠。

5.2 流式传输超时和背压控制的教训

第二个坑来自一次流式调用。上线初期,我们给流式输出没有设置整体时间预算,想着反正两边连接还在,慢慢传就行。结果某个Agent的输出端在处理一个超长日志分析任务时,模型推理卡住了,既不报错也不关流,所有调用方都把连接挂在那里等。半天之后,接收端的连接池直接被打满,新的触达请求全部被拒。

透视之后发现,问题不是流量太大,而是没人给流式传输设“终止条件”。现在的做法是:

  1. 所有流式调用必须设总时间预算,默认最长5分钟,超时主动断开并通知调用方;
  2. 引入背压控制,接收方处理不过来时主动告诉发送方“慢一点”,而不是被动吞消息;
  3. 缓冲区水位线分级处理。

水位线控制的伪代码很直白:

if buffer_usage > 0.8: send_pause() # 告知对端降速 elif buffer_usage > 0.95: abort_stream() # 超出容忍阈值,强制断开 elif buffer_usage < 0.4: send_resume() # 恢复全速

为什么要先Pause而不是直接断开?因为把正在处理的任务硬断掉会引发连锁失败,先让发送方降速给接收方争取处理时间,直到真的不可控了才断开重连。这个分级策略上线后,再也没有出现过连接池被占满导致全网不可用的事故。

5.3 注册中心本身成为单点:一次故障复盘

第三个坑来自一次让我印象深刻的故障。某一天注册中心所在的节点宕机了,Maker结果是:全网Agent互相找不到!Agent自己的功能都还在,但因为所有路由请求都实时访问注册中心,注册中心一挂,整个触达网络直接瘫痪。

复盘后发现根因有两个。一是Agent端虽然有本地注册列表缓存,但代码里默认“每次路由都要实时读注册中心”,缓存放那儿等于摆设;二是注册列表变更通知依赖读操作做强一致,注册中心压力也被推得很大。

修复分三步落地:

  1. Agent端启用本地缓存,注册列表每30秒做一次增量同步;
  2. 路由请求允许缓存降级,容忍最多2分钟内的旧注册数据;
  3. 注册中心改成多副本部署,主节点挂了立即切从节点。

这次重构让我意识到一个原则:连接层必须比业务层更抗打。业务层挂了,影响的只是单个Agent;连接层挂了,全网Agent都变成孤岛。所以在Agent-Reach里,注册中心永远不能成为系统的阿克琉斯之踵。

5.4 后续可能扩展的方向

Agent-Reach在目前这个版本里还不是终点,我们接下来的计划主要有三个方向。

第一个方向是跨集群触达。目前Agent-Reach还限定在单集群内,跨集群需要额外搭一层集群间的接入网关,把路由和身份信任关系延伸出去。第二个方向是能力订阅与信任背书,让一个Agent可以订阅另一个Agent的能力变化,不用老是全量拉取注册中心,同时让可信Agent个给其他Agent做能力背书,降低模糊匹配的出错概率。第三个方向是触达SLA,把每个能力的可用性指标变成路由选择时的硬条件,比如一个能力承诺P99小于500毫秒,路由在打分时就能把延迟作为强约束,而不是事后才发现。

这套触达层上线跑了三个多月,给我的最大体会其实是那句说了很多次的老话:别把连接层当配置,它是正经基础设施。Agent的智能度可以慢慢调,但触达层的坑不会因为你模型调得好就自动消失。与其等Agent之间互相找不到了再补连接,不如一开始就把Agent-Reach这样的触达层设计进去。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询