多智能体触达层Agent-Reach:从能力注册到可靠路由的落地实践
发布时间:2026/10/27 23:08:57 锦皓数字建站

做多智能体系统做到第三周的时候我发现团队里最吵的词不是“提示词”也不是“模型幻觉”而是“我的智能体怎么调不到你的智能体”。每个人都有自己部署的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 注册与心跳在线状态不是玄学有了ManifestAgent启动时的第一步就是向Agent-Reach的注册中心上报。完整流程是Agent SDK读取本地Manifest文件向注册中心发起身份握手用Agent私钥对注册请求做签名注册中心验签、校验Manifest格式与权限策略把Agent写入能力目录Agent开启心跳定时上报存活状态和负载信息注册中心发现异常时主动下线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全链路追踪IDfrom_id / to_id源和目标Agent标识target_capability请求触达的能力名typerequest / response / submit / event / frame / ack / errorpayload实际业务负载expires_at消息截止时间过期自动丢弃同步请求走request和response比较适合“帮我查一下当前余额”这种快速问答异步任务走submit先返回一个ack和任务ID任务完成后再通过event把结果推给调用方适合“帮我生成一份季报”这种长耗时操作流式输出则把一段完整的响应拆成多个frame接收方拼完最后一个is_lasttrue的帧才算完整收到适合“边分析日志边输出根因候选”。协议本身不复杂但有了这套统一的信封Agent之间就不需要再为“传输层格式”扯皮。大家只要实现Agent-Reach的SDK通信语义就自动对齐了。3.2 路由策略意图匹配、就近调度和故障转移路由引擎要做的事情是把一次“我想用某某能力”的请求转成“具体调用哪个Agent实例”。路由过程分四步第一步是意图匹配。首先按target_capability做精确匹配但现实往往是模糊的——调用方自己都可能说不准该用哪个能力名。比如一个请求想“分析财务数据”而系统里有两个能力都叫data_query一个查业务库一个查工单库。这时路由就不能只看能力名还要结合Manifest里的description和请求上下文做语义匹配。我们在实现中引入了向量化检索把能力描述向量化请求进来后先算语义相似度再结合精确匹配做召回。第二步是约束过滤。把并发已满的、负载过高的、当前成功率太差的Agent踢出候选如果请求带了数据域约束比如data_domainfinance则只保留声明支持该域的Agent。第三步是打分排序。打分公式大致是这样score w1 * (1 - load / 100) w2 * latency_norm w3 * success_rate w4 * affinityw1到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 AgentC处理完了响应却在回传时丢了。也就是说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的输出端在处理一个超长日志分析任务时模型推理卡住了既不报错也不关流所有调用方都把连接挂在那里等。半天之后接收端的连接池直接被打满新的触达请求全部被拒。透视之后发现问题不是流量太大而是没人给流式传输设“终止条件”。现在的做法是所有流式调用必须设总时间预算默认最长5分钟超时主动断开并通知调用方引入背压控制接收方处理不过来时主动告诉发送方“慢一点”而不是被动吞消息缓冲区水位线分级处理。水位线控制的伪代码很直白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端虽然有本地注册列表缓存但代码里默认“每次路由都要实时读注册中心”缓存放那儿等于摆设二是注册列表变更通知依赖读操作做强一致注册中心压力也被推得很大。修复分三步落地Agent端启用本地缓存注册列表每30秒做一次增量同步路由请求允许缓存降级容忍最多2分钟内的旧注册数据注册中心改成多副本部署主节点挂了立即切从节点。这次重构让我意识到一个原则连接层必须比业务层更抗打。业务层挂了影响的只是单个Agent连接层挂了全网Agent都变成孤岛。所以在Agent-Reach里注册中心永远不能成为系统的阿克琉斯之踵。5.4 后续可能扩展的方向Agent-Reach在目前这个版本里还不是终点我们接下来的计划主要有三个方向。第一个方向是跨集群触达。目前Agent-Reach还限定在单集群内跨集群需要额外搭一层集群间的接入网关把路由和身份信任关系延伸出去。第二个方向是能力订阅与信任背书让一个Agent可以订阅另一个Agent的能力变化不用老是全量拉取注册中心同时让可信Agent个给其他Agent做能力背书降低模糊匹配的出错概率。第三个方向是触达SLA把每个能力的可用性指标变成路由选择时的硬条件比如一个能力承诺P99小于500毫秒路由在打分时就能把延迟作为强约束而不是事后才发现。这套触达层上线跑了三个多月给我的最大体会其实是那句说了很多次的老话别把连接层当配置它是正经基础设施。Agent的智能度可以慢慢调但触达层的坑不会因为你模型调得好就自动消失。与其等Agent之间互相找不到了再补连接不如一开始就把Agent-Reach这样的触达层设计进去。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。