Agent-Reach:多智能体系统的可达性基础设施 1. Agent-Reach是什么先解决那个“够不着”的问题做多智能体系统最让人头疼的往往不是模型效果而是Agent之间根本够不着。Agent-Reach这个项目就是我为这类问题造的一个轮子。它把散落在不同服务、不同语言、不同团队手里的智能体统一注册成节点再通过一套可编程的路由机制让它们互相发现、按需触达、安全通信。简单说它不管Agent怎么思考、怎么调模型只管Agent之间能不能稳定地找到对方、把消息送到。我最早被这个问题折磨是在一个跨团队协作的项目里。A组用Python起了一个文本摘要AgentB组用Node.js写了一个邮件处理Agent两边要联动处理客户工单。正常流程很简单摘要Agent产出结论邮件Agent把结论发出去。但当时为了打通这两个服务我们拉了整整一下午会议最后定了HTTP接口加JSON格式。写完联调发现全是边界情况字段大小写不统一、超时策略对不上、重试把消息重复发了三遍。那一刻我突然意识到多Agent系统真正缺的不是某个更聪明的模型而是一层标准化的“连接基础设施”。1.1 一个让我决定造轮子的现场当时那个项目里一共有五个Agent分别来自三个小组。文本摘要、意图识别、邮件发送、工单分配、报表生成逻辑上可以串成一条很顺的流水线。可实际上每个Agent的启动方式不同、暴露协议不同、鉴权方式不同有些走内部HTTP接口有些用消息队列还有一个直接用共享目录轮询。联调了一星期每天都有新问题某个Agent实例重启后注册地址变了另一个Agent的请求体里字段名和别人对不上还有两个Agent互相调用来回把数据格式转了三遍性能损耗比干活本身还大。更崩溃的是每当新增一个Agent负责接入的人就要去联系所有可能用到它的团队挨个告诉别人“我们的接口长什么样、怎么鉴权、参数是什么”。这本质上变成了一个手工维护全连接矩阵的问题。我当时就想能不能让每个Agent只面对一个统一入口把自己能干什么、接收什么格式、输出什么格式说清楚剩下的交给系统自动完成匹配和通信。1.2 一句话定位与核心语义Agent-Reach的定位很简单它是多智能体系统里的可达性基础设施。所谓“可达”包含三个层面一是发现可达Agent能被正确找到二是协议可达不同技术栈的Agent能互相理解消息三是策略可达该允许谁调用、该拒绝谁调用系统能守住边界。这三个层面单独拿出来都有现成方案但组合在一起、并且专门为AI Agent设计才是这个项目真正在做的事。我把它设计成一个独立于业务代码的基础层。业务团队不需要改自己的Agent内部逻辑只需要通过SDK做一次声明式接入声明“我是谁、我会什么、我接收什么参数、我返回什么结果”剩下的路由、寻址、协议转换、鉴权校验全都交给Agent-Reach处理。这样Agent之间的协作关系从代码耦合变成了配置驱动加新Agent、改旧Agent都不需要四处通知。1.3 适合谁用不适合谁用如果你正在做一个多团队协作的多Agent系统各个模块语言不统一、部署位置分散、经常需要互相调用那Agent-Reach的思路可以直接抄作业。它尤其适合已经跑通单Agent能力、开始往多Agent协作方向走的团队因为这类团队最缺的不是算法而是工程规范。如果你在做LLM工作流编排比如用某些编排框架把“规划、调用、总结”串起来也可以把Agent-Reach作为底层通信层让编排框架专注于任务分解本身。反过来如果你只是在一个进程里写一个Demo或者你的Agent数量长期保持在两三个以内、互相之间直接用函数调用就够了那我建议别上这套东西。任何基础设施都有成本Agent-Reach的设计目标是解决网状调用带来的混乱而不是给单机脚本增加复杂度。过早引入它只会让你多维护一套配置。2. 整体架构与设计思路Agent-Reach的整体架构可以拆成三个角色接入端SDK、路由中枢、转发网关。接入端SDK负责让每个Agent完成注册、心跳保活、发出调用请求路由中枢是控制面负责维护全局的Agent目录、能力索引、路由策略和权限规则转发网关是数据面负责把实际消息按路由结果送到目标Agent。这个控制面和数据面分离的思路是我在项目初期反复权衡后定的。2.1 控制面与数据面分离最早我的想法很简单做一个中心化消息转发服务所有Agent跟它保持长连接消息全部从它这里中转。这样做的优点是实现简单但很快被性能问题劝退。Agent之间的消息里经常夹着比较长的中间结果比如一篇文章、一段代码、一组检索出来的文档如果全部经过中心节点它很快会成为带宽瓶颈而且单点故障会拖垮所有Agent。所以我把系统拆成了两半。注册、发现、鉴权、路由决策这些低频但要求一致性的操作统一走路由中枢它的数据量小、可以多做校验也可以做集群高可用。真实的消息内容则走转发网关网关拿到路由中枢下发的路由表之后直接在数据面把消息转发过去甚至在同一机房内可以用内网直连绕过网关。这种设计在大型微服务架构里很常见但在Agent通信场景里往往被忽略很多人一上来就搞中央总线最后全卡在流量上。2.2 为什么不是全网状直连可能有人会问既然怕中心化瓶颈干嘛不干脆让Agent点对点直连这样不是最快吗表面上看确实快但代价是每个Agent都要维护一份“谁在哪、谁需要我、我需要谁”的全量信息。五个Agent要维护五份不同的对端连接配置五十个Agent就是五十乘四十九的潜在连接关系这种网状结构在规模起来之后会让排查问题变成噩梦。我倾向于把路由关系收敛到中枢让Agent保持简单。每个Agent只需要认识路由中枢和数据网关不需要知道其他Agent的物理地址、端口和鉴权材料。当A需要调用B时A向中枢发起一次寻址中枢根据B声明的能力和当前健康状态返回一条可用连接路径A再按这条路径把请求发出去。路径信息有缓存不会每次都打中枢所以刚接入时会慢一点稳定之后基本接近于直连的效率。2.3 能力路由不找IP找技能Agent-Reach最核心的概念是能力路由。传统服务发现是找地址你告诉我“用户服务在10.0.0.5:8080”我直接连过去。但Agent场景里不一样我往往不关心具体是哪个实例在提供能力我只关心“谁能把这段文本摘要出来”。所以每个Agent在注册时必须提交一份能力描述文件声明它接收什么类型的任务、输入输出结构是什么、大概的延迟和吞吐量、是否需要特定上下文。路由中枢维护的是能力索引而不是简单的IP列表。这个设计带来的直接好处是故障转移和水平扩展变得很自然。当某个Agent实例挂了中枢只需要把它的能力从可用列表里摘掉下一次同样的请求会路由到另一个具备相同能力的实例调用方完全无感知。新增Agent也是这样它注册完能力后天然能被所有匹配的调用方发现不需要人工通知任何对端。3. 关键模块的实操实现3.1 注册与发现给Agent一个“自我介绍”Agent接入Agent-Reach的第一步是提供能力描述文件。我们用YAML格式定义因为可读性好、方便评审。下面是一个最小示例agent: name: text-summarizer version: 1.2.0 description: 对长文本生成摘要支持中文和英文 capabilities: - id: text.summarize input: text: string max_length: integer (optional) output: summary: string tokens_used: integer qos: max_qps: 20 max_latency_ms: 3000 transport: type: grpc endpoint: 10.20.30.40:9100 health_check: /healthz auth: scope: ai:text:read字段不多但每项都有讲究。capabilities.id是整个路由寻址的锚点必须遵循团队约定的命名规范推荐用“领域.动作”的格式比如text.summarize、email.send。qos字段不光是声明路由中枢会用它做基于负载的调度比如两个Agent都能做摘要一个QPS上限高一个低中枢会优先把流量分给上限高的。SDK端的使用体验我是按“装饰器即注册”的思路设计的。业务代码里只需要加一个装饰器Agent的启动和注册逻辑就自动完成from agent_reach import Agent, capability from agent_reach.transport import grpc_server app Agent(nametext-summarizer, version1.2.0) capability(text.summarize, max_qps20) def summarize_payload(payload: dict) - dict: text payload[text] max_length payload.get(max_length, 200) summary your_summarization_model(text, max_lengthmax_length) return {summary: summary, tokens_used: count_tokens(text)} if __name__ __main__: app.serve(grpc_server(endpoint0.0.0.0:9100))装饰器会把方法名、入参出参的schema、QPS限制自动扫描出来注册时和YAML文件合并成最终的能力描述。这里有个我踩过的坑如果YAML里的描述和代码里的实际签名不一致最好在注册阶段直接校验失败而不是等到线上调用时才暴露省得后面排查半天。3.2 路由决策一条消息怎么找到对的人当一个Agent发起调用请求时它只需要说要调用的能力ID和参数剩下的事情由SDK和路由中枢完成。路由中枢的决策逻辑大概分三步语义匹配、条件过滤、负载择优。语义匹配解决的是“这个名字是不是同一个能力”的问题。虽然我要求能力ID用规范命名但实际项目中总有人写出summarize_text、text-summarize、TextSummary这种变体。这一步可以用简单的文本相似度加上同义词表兜底不直接拒绝而是给一个匹配置信度。条件过滤则是把可用实例按能力声明、版本、地域、权限范围这些硬性条件筛一遍。最后是负载择优按当前QPS使用率、历史延迟、健康状态综合打分选最合适的一个实例。路由规则可以用配置文件做细粒度控制。比如我想让耗时敏感的任务优先走内网机器或者想让某些调用方只能使用指定版本的Agent都可以通过规则表达routes: - from_agent: intent-classifier capability_id: text.summarize version_requirement: 1.2.0, 2.0.0 prefer_zone: internal failover: true timeout_ms: 5000注意failover: true这个选项。它表示如果首选实例超时SDK会自动重试到备用实例。重试策略默认是单次重试加指数退避避免把重试风暴带回源。我的经验是不要把failover的层级做得太深最多重试一次就能覆盖大部分瞬时限流导致的失败再多反而会因为重复执行造成副作用。3.3 数据面转发与协议适配Agent-Reach的数据面协议选型我花了挺多心思。Agent之间通信和普通服务间调用不太一样它天然是双向的、交互式的一个Agent发起请求后可能需要持续接收流式输出比如摘要生成是一段一段吐字的或者工具调用链中Agent需要边处理边回报中间结果。这让我果断放弃了纯HTTP短连接方案优先考虑gRPC双工流和WebSocket。最终我选择了gRPC作为默认传输协议。原因很实际gRPC有完整的流式支持有内置的deadline传播机制还有多语言代码生成Python、Go、Node.js都支持得很好。我们的SDK里内置了一个JSON到Protobuf的映射层业务方如果不想定义proto文件可以继续用JSON写输入输出SDK在运行时自动转换。这样既保证了内部通信的强类型又降低了接入者的心智负担。转发网关在拿到消息时会根据能力描述里声明的input schema做一次快速校验字段类型不对、必填字段缺失的直接拒绝。这样一个不起眼的校验能省掉下游Agent大量解析脏数据的成本。实际运行中加入这道校验后下游Agent因参数错误导致的异常率降了七成。3.4 安全与权限控制谁能触达谁权限这块是我在Agent-Reach里最坚持的部分。Agent之间互相调用绝不能默认放行否则一旦某个Agent被注入恶意提示影响会顺着调用链无限蔓延。我为每个Agent分配了一个运行时身份用短期凭证替代长期密钥。凭证过期时间默认十分钟SDK会自动在后台刷新对业务代码完全透明。权限模型我没有设计得很复杂用的是基于作用域的RBAC。每个Agent可以声明它需要访问哪些能力作用域路由中枢在发起调用前校验调用方的凭证是否包含对应作用域。例如agents: - name: intent-classifier scopes: - ai:text:read - ai:text:summarize allow_call: - email:send:write - ticket:assign:writeallow_call字段用于控制两个具体Agent之间的调用关系比单纯按作用域卡更严格。比如意图分类Agent只能调用邮件发送和工单分配就算它拿到了整个ai作用域的凭证也不能去调用报表生成Agent。这套策略匹配在路由中枢完成转发网关只认中枢签发的内部令牌流量到了网关这一层不再重复做业务鉴权只校验令牌合法性和过期时间性能开销很小。4. 接入实践与落地清单4.1 最小可用的五步接入新Agent接入Agent-Reach我总结成五步按这个顺序做完就能跑通最小链路。第一步写能力描述文件。别急着写代码先把你这个Agent会干什么、输入是什么、输出是什么说清楚。这个文件是整个系统的“寻址锚点”写不清楚后面路由一定会出问题。第二步在代码里加入SDK依赖给核心处理函数加上能力装饰器。第三步本地起一个Agent跑SDK自带的注册自检命令确认它能成功连接路由中枢、能力描述能被正确解析。第四步在路由中枢的控制台查看Agent状态是否变成ready健康检查路径能否被网关探活成功。第五步用一个测试调用方发起一次实际请求确认消息能按预期路由到目标Agent并拿到结果。整个流程如果顺利大概半小时能完成。我自己遇到过最多的卡点不在代码而在“YAML里声明的能力和代码实际行为不一致”比如声明接收JSON数组实际只处理了对象。自检命令里我突然加入了一个能力回环测试调用方Agent先注册然后尝试调用目标Agent的echo能力能通才算注册成功。4.2 核心参数与调优建议接入过程中有几个参数值得重点关注它们直接影响系统的稳定性和性能表现。参数名默认值作用调优建议heartbeat_interval5sAgent向路由中枢上报健康状态的频率5s够用如果组网规模超千节点可以考虑拉大到10s减少中枢压力discovery_timeout3s调用方寻址的超时时间本地机房内建议3s跨地域或公网场景放宽到8sroute_cache_ttl30s路由结果在SDK侧的缓存时间实例变动频繁的Agent密集场景缩小到10s追求性能可放宽到60smessage_size_limit8MB网关单条消息体积上限传大文本够用传文件建议改用对象存储引用而不是直接塞消息体credential_ttl10min短期凭证有效期安全要求高的场景缩短到5min同时确认SDK自动刷新逻辑正常关于message_size_limit要特别提醒一句Agent之间经常需要交换长文档直接把文档内容塞进消息体看起来省事但会拖垮网关和下游的内存。更好的做法是把文档传到对象存储消息里只带引用ID目标Agent按需读取。这条路我在项目里走过三回冤枉路才扭过来。4.3 观察指标与运行状态判断接入完不代表结束了线上运行一定要有可观测性。我维护的监控面板上有四个核心指标路由命中率、注册成功率、平均寻址时延、消息流转失败率。路由命中率指的是调用方发起的寻址请求中能成功找到至少一个可用实例的比例。正常应该在99%以上如果低于95%多半是能力描述不规范或版本约束太严格导致候选实例被全部过滤掉。注册成功率关注的是新Agent上线后能否在一分钟内进入ready状态这个指标波动大通常和路由中枢或健康检查路径的稳定性有关。平均寻址时延在缓存命中的情况下应该低于5ms首次寻址因为要打中枢可以接受几十毫秒但如果每次都几十毫秒说明SDK侧缓存可能没生效。消息流转失败率按多种错误类型分别统计连接超时、鉴权失败、业务异常是三类主要桶。我习惯给每类错误单独配告警一旦某类错误在五分钟内持续上升就触发通知。前段时间我们线上出过一次事故就是因为某个Agent升级后把输入参数名从text改成了content网关校验直接拦截了所有请求如果没有按错误类型分桶的告警排查起来会非常痛苦。5. 常见问题与排查技巧实录5.1 连接超时先别查代码按这条链路走Agent-Reach接入后最常见的报错就是连接超时。很多同事一看到超时就去翻业务代码其实超时问题九成不在业务逻辑里。我自己的排查顺序固定是先看路由中枢控制台上这个Agent的健康状态是不是ready再看数据网关能否访问Agent声明的endpoint然后校验两端证书和凭证是否都在有效期内最后才去看业务代码的处理耗时。有一次线上告警显示某个Agent连续五分钟连接超时我按这个链路一步步查发现这个Agent所在机器因为磁盘写满进入了只读状态健康检查端口能够正常返回但真正处理业务请求时已经卡死了。如果只看健康检查通过就判定服务正常就会漏掉这种假活场景。后来我在SDK里加了一个“软健康”机制健康检查不仅要探活端口还要额外执行一次轻量级自检比如检查内存余量、磁盘状态、模型有没有加载完成才把状态上报为ready。5.2 消息循环风暴自治系统的“死循环”多Agent系统里还有一个特别隐蔽的问题Agent之间的循环调用。A调用BB调用CC又回过头来调用A如果每次调用都会产生新的任务实例这个循环就永远不会停最后把整个系统的资源吃光。传统的超时机制根本挡不住这种风暴因为每一跳看起来都是在正常处理业务。我在设计上做了两道防线。第一道是路由中枢在每次下发路由结果时为整条调用链生成一个trace ID并在trace里附带最大深度限制默认八层超过就直接丢弃并返回错误。这样即使业务逻辑上有循环也不会无限蔓延。第二道是配置层面的强制约定任何Agent的能力描述里如果声明的输出类型会触发对上游Agent的调用必须人工在路由规则里标注“允许回环”并设置独立深度上限否则中枢直接拒绝这条路由规则。5.3 鉴权失败里最容易被忽略的时钟漂移Agent-Reach用短期凭证做鉴权这意味着节点之间的时钟必须保持同步。曾经有一次我们新增了一批Agent机器这些机器没有接入NTP服务系统时间比集群慢了六分钟结果所有凭证刷新后都被判定为过期网关日志里全是鉴权失败。我排查了半天最后一看系统时间才反应过来。这个坑在开发环境基本不会出现因为开发机通常会自动对时。但生产环境里总有机器处于隔离网络时间同步容易被忽略。建议在所有Agent机器上强制部署NTP同步并把SDK内置凭证的有效期校验放宽三十秒的时钟偏移容忍窗口。另外鉴权失败的错误日志里一定要带上Agent身份ID和当前系统时间方便在异常时快速判断是不是时间问题。5.4 路由命中率低问题常出在能力描述上最后一个常见问题是路由命中率低明明目标Agent在线且健康但其他Agent就是找不到它。排下来大部分原因都出在能力描述写得“太高级”或“太随意”。有人会把能力ID写成一段自然语言描述比如“帮我处理所有文本相关的任务”这种描述在语义匹配阶段容易产生歧义有人则会声明极其严格的版本约束比如version_requirement精确锁定到某个补丁版本导致升级后老调用方全部失配。我的建议是把能力ID当公共API来对待起名规则、字段类型、版本演化都要走评审。每次调整输入输出schema必须保留一个兼容版本用版本约束做渐进迁移不要直接原地修改已有能力定义。路由命中率低的时候第一步应该是去路由中枢后台查“被过滤的实例及原因”而不是猜问题。这个功能是我后来特意加上的它能明确告诉你某个实例是因为版本不匹配、地域不匹配、权限不足还是QPS满负荷被淘汰排查效率能提升一个数量级。最后分享一个我自己的习惯每接入一个新Agent我都会在路由中枢里跑一遍“可达性全览”把当前所有Agent以及它们之间的调用关系可视化地列出来看看有没有不该存在的链路有没有声明了能力却长期没有被调用的Agent。这套系统的价值不只是让Agent之间互相找到更是让整个多Agent协作的边界变得清晰可见。没有这层控制面Agent越多系统只会越混乱。