
一团乱麻的起点我们为什么需要 Agent-ReachAgent-Reach 不是我们计划中的项目它的诞生确实有点狼狈。团队从单体智能体拆到多智能体架构后第一周还算清净第二周开始线上各 Agent 之间的调用关系图就已经没人能看清了。新同学接手一个需求光排查这个能力到底该找谁拿就要翻半天文档两个 Agent 同时声称自己负责翻译可真发了请求过去返回的字段格式完全对不上。我们抱着再加一层编排就行的心态去填坑结果越填越发现问题不在某个 Agent 本身而是整条链路里缺少一个真正能打通发现、寻址、触达的公共层。这篇文章想把 Agent-Reach 从零到一的过程完整记录下来我们踩了哪些坑、为什么这样设计、每个关键模块背后是什么逻辑以及最后沉淀下来的性能指标和调优手段。如果你也在做多智能体系统或者正被一堆自研 Agent 的相互调用搞得焦头烂额这篇内容大概率能帮你避开不少冤枉路。1. Agent-Reach 要解决的三个核心问题发现、寻址、触达1.1 先说说我们原来的惨状在 Agent-Reach 出现之前我们的架构基本是人肉服务发现加硬编码调用的大杂烩。每个 Agent 启动后会往 Nacos 里注册一个实例但注册进去的只有 IP 和端口能力信息全靠 README 描述。A 团队要调用 B 团队的 Agent第一步是去 git 仓库里翻 B 团队的代码看它暴露了什么接口然后硬编码地址和参数格式。B 团队一重构接口A 团队整个链路直接就断掉了告警炸了一晚才定位到是上游改字段名。有一次事故特别典型翻译 Agent 在凌晨灰度发布时把一个接口路径改掉了当时依赖它的三个下游 Agent 同时开始报错。由于没有统一路由层每个下游团队各自排查耗时四五个小时才找到根因。这让我彻底意识到多智能体系统能不能跑得稳关键不在单个 Agent 有多聪明而在于它们之间能不能被可靠地触达。1.2 我们把触达拆成了三个能力做 Agent-Reach 的第一步我们没有急着写代码而是把触达这个词拆成了三个可落地的能力定义发现Discovery一个 Agent 上线后它提供了什么能力、能力怎么调用、当前有哪些可用实例系统要能自动感知而不是靠人写文档同步。寻址Addressing调用方发来一个请求系统能根据请求意图语义关键词匹配到正确的 Agent然后自动选择具体的实例而不是让调用方自己写死要调谁。触达Reachability请求能不能真正到达目标 Agent 并得到成功响应涉及健康检查、限流、重试、熔断、超时控制等整套保障机制任何一个环节出问题都会导致明明注册了却调不通。这三个能力听起来是分布式系统里的老生常谈但放到 Agent 场景里有一个微妙的不同传统微服务的服务是稳定接口但 Agent 的能力是动态的、语义化的。用户说帮我总结这篇文档可能对应多个 Agent同一个 Agent 也可能有多个不在一个 schema 里的相似能力。所以 Agent-Reach 的核心设计不能照搬 Spring Cloud 那套必须把语义路由作为一等公民。2. 控制面与数据面Agent-Reach 的架构到底怎么切的2.1 为什么坚持控制面和数据面分离架构上我们走了经典的控制面/数据面分离路子这可能是 Agent-Reach 最正确的决定之一。控制面负责管理包括能力注册、健康状态维护、路由规则下发、版本管理数据面负责执行也就是真实请求的转发、负载均衡、重试熔断。两者分离带来的最大好处是策略变更不影响数据通路。第一版尝试里我们把状态管理和请求转发揉在一个进程里确实简单。但随着 Agent 数量增长问题立刻爆发每次路由规则更新都要全量推送配置到每个转发节点节点一多推送延迟就到了秒级。而且控制面一抖动所有请求转发跟着受影响线上代理和服务调用双双超时。拆开之后控制面的变更走异步下发数据面只用本地缓存的路由表处理请求哪怕控制面短暂不可用数据面也能按最后一份有效配置继续工作只是暂时感知不到新上线的 Agent。这对线上稳定性是质的提升。2.2 核心模块与职责边界Agent-Reach 的架构可以划分为四个核心模块注册中心、触达网关、路由引擎、可观测性组件。我用一个表来说明各自的职责和选型依据模块核心职责关键技术选型备注注册中心RegistryAgent 上线/下线登记、能力元数据存储、心跳检测etcd 存储 自研能力编排层没有直接用 Nacos因为能力描述结构需要自定义触达网关Gateway统一入口、请求转发、负载均衡、超时重试Go 自研版 Envoy内嵌 gRPC 与 HTTP 双协议网关无状态水平扩展容易路由引擎Router意图识别、能力匹配、路由打分语义关键词索引 规则引擎结合正则与 NLP 模型兼顾准确性和性能事件总线NATSAgent 间异步事件传递、任务下发与完成通知NATS JetStream异步解耦避免全链路同步阻塞可观测性Monitor触达成功率、路由命中率、调用链追踪、告警Prometheus ClickHouse OpenTelemetry北极星指标见后文2.3 Agent 的能力描述这是全系统最关键的 schemaAgent-Reach 的立身之本是能力描述文件。每一个接入的 Agent 都要提供一个capability.yaml用结构化方式声明自己能做什么、怎么调用。我们设计了这样的核心字段name: doc-summary-agent version: 2.3.1 language: python capabilities: - id: summarize_document display_name: 文档总结 description: 对给定的文本或文档进行结构化摘要支持中英文 type: sync input_schema: document_url: type: string required: true description: 文档链接或文本内容 target_lang: type: string required: false default: zh enum: [zh, en] output_schema: summary: type: string key_points: type: array items: string rate_limit: qps: 50 burst: 100 timeout_ms: 8000 endpoints: - path: /v1/summarize protocol: grpc method: SummarizeText metadata: owner_team: nlp-group sla: p99_under_3s这份文件会成为 Agent-Reach 的能力身份证。有了它路由引擎才知道该把什么请求派给谁网关才知道调用方的参数合法不合法监控才知道每个 Agent 承诺了多少 QPS 和延迟。我们曾经犯过一个低级错误——把超时时间漏了导致网关默认给所有请求设了 30 秒超时某一个慢 Agent 拖慢了整个调用池的连接资源。加了显式timeout_ms声明之后每个 Agent 的负载画像一目了然再也不用心跳加速。3. 从注册到触达通信链路里最关键的实现细节3.1 Agent 注册与心跳保障每个 Agent 接入 Agent-Reach 时SDK 会先读取本地 capability 文件把元数据登记到注册中心之后建立一条长连接gRPC stream做心跳。心跳机制的设计吸取了第一版踩坑的教训。初期我们用短连接 固定 5 秒间隔上报结果网络一抖动大规模误判立刻出现。现在的方案是长连接维持每 5 秒发出一次心跳注册中心连续 3 次收不到心跳才把实例标记为不健康摘除后每 15 秒发起主动探测恢复则自动重新注册。关键细节是心跳消息必须带上 Agent 当前的内存占用、CPU 负载和队列长度这三个数据会被网关在负载均衡时使用。SDK 侧注册逻辑用 Python 示意大概长这样from agent_reach_sdk import Agent, capability_loader cap capability_loader.load(capability.yaml) agent Agent(namecap.name, versioncap.version) agent.register(capability_idsummarize_document) def handle_summarize(request: dict) - dict: doc_url request[document_url] target_lang request.get(target_lang, zh) return summarize_text(doc_url, target_lang) agent.start()这段代码运行后SDK 会自动完成注册、心跳上报、接收路由指令三件事。对业务团队来说他们只需要写自己的业务逻辑不需要关心 Agent-Reach 内部怎么转发。3.2 语义路由引擎怎么从翻译一下匹配到正确的 Agent路由引擎是最有 Agent 特色的部分。传统微服务路由靠 URL 精准匹配但用户发给 Agent 的请求往往带着自然语言的模糊意图比如帮我把这段英文整理成要点并翻译成中文这其实同时涉及总结和翻译两个能力。我们的路由设计采用两阶段阶段一意图识别对请求做语义理解抽取出候选意图标签。这个环节早期用规则和关键词匹配总结/翻译/摘要等词后来升级成向量化语义检索。实际测试下来规则版已经能覆盖 80% 的请求语义检索主要用来兜底长尾表达。阶段二能力打分排序注册中心里的每份能力描述都维护一个语义指纹路由引擎按候选意图与能力指纹的相似度打分同时叠加权重精确匹配能力 ID 得满分描述包含关键词得 50% 分语义相似得 20% 分。综合得分超过阈值的候选能力进入待选列表由网关按负载情况选择具体实例。我记得第一次上线语义路由时压测队列里有人故意发用中文讲这段英文讲稿的重点。规则引擎完全懵了一个候选都没匹配上语义版本则成功打到了 doc-summary-agent 和 translation-agent 两个候选最终交给了 doc-summary-agent 先总结再翻译。这个场景让我们确认了语义路由的必要性——Agent 场景的调用方不可能像调用 REST API 一样严格遵循接口文档。3.3 数据面转发超时、重试与熔断的配合网关的转发逻辑参考了分布式系统经典的快速失败 有限重试原则但在 Agent 场景下做了几个特殊调整。每个请求必须携带 deadline通常是 Agent 声明的 timeout_ms 里取最大值的 60% 作为网关等待时间。比如某 Agent 声明 8000ms 超时网关最多等 4800ms防止下游超时后把错误拖回给用户。重试只允许一次而且不能发给同一台实例必须发给其他候选实例否则一个故障实例会把重试流量也吃下去。熔断用滑动窗口统计近 60 秒的请求成功率连续超过 30% 失败就打开熔断开关之后快速失败而不是继续转发。这套组合最终保证了 Agent-Reach 在最坏情况下的行为是可预期的请求要么成功拿到结果要么在几秒内明确失败不会出现悬而未决的调用挂在网关层把连接池拖垮。4. 四个真实踩坑记录每一个都改写了我们的设计这个章节可能是对你有用的部分。Agent-Reach 不是一次设计成功的下面四个坑我们每一个都踩得很深。4.1 心跳抖动导致大规模误摘除上线第二周网络设备例行变更时出现了分区抖动心跳无法到达注册中心。我们的第一版逻辑是收不到心跳就立刻摘除结果抖动一恢复几十个 Agent 全部被重新注册路由表大面积刷新大量请求在切换过程中失败持续了将近二十分钟。修复方案是我们把摘除条件改成了连续 3 次心跳超时同时注册中心摘除后不急着清路由表而是把实例标记为unknown再保留 30 秒如果探测恢复就直接切回 healthy避免路由表剧烈抖动。这个改动看起来简单但直接消掉了整个系统最大的可用性隐患。4.2 能力描述字段史上最乱的 Schema同一种语义十种写法第一个月接入了十几个 Agent能力描述文件五花八门。翻译 Agent 声明参数叫target_lang另一个总结 Agent 声明了language和to_language还有团队直接写lang_to。路由引擎做匹配时明明语义完全一致的能力因为字段命名不一致打分差异巨大选不到正确实例。后来我们强制所有接入 Agent 必须经过 Schema 校验枚举值和必填项由统一规范定义。同时注册中心做了一层语义归一化每个能力描述文件进来时会把常见同义字段如target_lang、language、to_language自动映射到内部标准字段dest_lang。这个映射一旦生效路由命中率从 71% 直接跳到了 96%。如果你也在做类似的系统请把统一能力 Schema放在第一优先级业务逻辑可以后面迭代但是协议必须一开始就定死。4.3 级联超时的三秒定律我们的多 Agent 任务经常是嵌套调用的A 调 BB 调 CC 处理完返回 BB 加工完返回 A。第一版每个环节都默认 5 秒超时结果 A 的调用方等 15 秒还没拿到结果误以为系统挂了直接把请求取消实际情况却是每个环节都在等下游。解决方法是引入 deadline 从请求头传递每一层都按【剩余时间 / 剩余跳数】重新分配预算。比如总 deadline 为 8 秒A 调用 B 时还剩 8 秒B 调用 C 时假设还剩 6 秒那 B 等待 C 最多 3 秒给自己留 3 秒做整理。每层都要留出处理余量绝不能把超时预算挥霍在等待上。这套机制落地后端到端 p99 延迟从 14 秒降到了 7 秒以内。4.4 负载均衡只看 CPU 等于瞎子摸象一开始我们以为网关只需要看 CPU 负载就能做负载均衡但很快发现一个 Agent 的响应延迟与其 CPU 关系不大真正决定性能的是内存占用和内部任务队列深度。有个 Agent 的 CPU 负载只有 10%但内部队列里堆积了八十万个待处理事件新请求进来排队就要好几分钟。后来我们把健康检查上报的指标组合成一个负载得分负载得分 CPU * 0.3 内存 * 0.3 队列深度 * 0.4网关每次都选负载得分最低的实例。这套加权模型实用了两个多月没有再出现某台机器 CPU 低但延迟高得离谱的情况。如果你有自己的负载定义也可以按自己的领域调整权重但核心是别只盯某一个指标。5. 触达成功率是北极星可观测性与性能调优的实战演进5.1 指标体系建设哪些数字必须实时盯可观测性建设的核心不是有监控就好而是找准北极星指标。对我们来说Agent-Reach 最重要的指标只有一个触达成功率。它的定义是成功返回响应 / 总请求数并按照路由命中、转发超时、Agent 异常、熔断四类失败原因拆分。在这个北极星之下我们还细分了几个关键指标指标定义意义路由命中率匹配到能力且实际调通的请求占比路由引擎质量低于 90% 就要排查语义匹配注册延迟Agent 注册到可调用的时间差控制在秒级否则弹性扩缩容跟不上流量心跳误报率健康/不健康状态切换的错误次数反映心跳设计是否合理端到端 p99 延迟从客户端发起到拿到结果的 99 分位延迟用户体验核心熔断触发次数网关打开熔断的次数结合触发原因定位下游故障这套指标上线后我们第一次可以快速回答系统现在到底行不行。以前只能靠猜现在任何一个异常都能立刻定位到具体环节。5.2 一次注册推送延迟优化从秒级到毫秒级Agent 上线后注册信息要同步到所有网关数据面最初是全量下发几百个 Agent 一次推送要 3 到 5 秒。后来改成增量推送 版本号机制网关本地缓存路由表版本注册中心只推送变更的 Agent 元数据并带上全局版本号网关收到增量后校验版本有缺失就回源拉全量。优化效果非常明显注册推送延迟降到平均 300 毫秒内。除此之外本地缓存还让数据面在不联系控制面的情况下可以独立服务几分钟。5.3 全链路追踪给每个请求挂上 Agent 语义标签传统全链路追踪只记录服务名和端点但 Agent 场景下我们更关心这个请求经历了哪些能力。OpenTelemetry 集成时我们把capability_id和agent_name加进 span 标签同时把deadline预算也刻进 span这样在链路分析里能完整还原整个意图流转过程。有一次线上业务反馈某些请求特别慢但没有超时我们用链路追踪一眼发现请求在 da-vinci-agent 上排队等了 4 秒该 Agent 的队列深度已经超过阈值但负载得分还是正常。修正了队列深度权重后这类问题再也没有复发。如果你准备做 Agent 系统请一定在第一天就把全链路追踪的语义标签设计好事后补的成本远高于一开始就留字段。6. 下一步Agent-Reach 会往这几个方向继续长6.1 从调通到调优自动参数适配目前路由引擎匹配到能力后参数基本靠调用方按 Schema 传格式不匹配时网关会直接拒绝。下一步我们打算在网关层做参数语义适配当调用方传target_lang而目标 Agent 期望dest_lang时网关根据能力描述里的字段语义自动转换而不是直接抛错。这能大幅降低接入成本让新 Agent 几乎不需要改代码就能被下游调用。6.2 跨集群联邦触达眼下 Agent-Reach 只覆盖单集群。我们的 Agent 可能会部署到不同地域的集群而用户希望就近访问、跨区容灾。下一步是给注册中心加联邦模式多个集群共享一份全局能力目录路由引擎按地域优先级和负载两个维度做选择。这个方向的工作量和踩坑规模会比单集群再翻一倍但方向是明确的。6.3 语义路由的自我进化目前语义路由的匹配模型还是离线训练后静态部署的新增能力描述上线后往往要等模型更新才生效。我下一步想做的是在线学习每次路由没命中但用户最终手动选择了某个 Agent 的行为都会被记录下来定时回流训练模型让系统越来越懂用户的实际表达。这不是简单的 RAG 能覆盖的而是一个持续进化的语义路由闭环。另外还有一个短期就能落地的计划把 Agent 的调用计量做出来这样各个团队之间的成本分摊和调用配额会有清晰数据。Agent 化之后最怕的就是每个团队都觉得别人的 Agent 应该免费提供最后变成大锅饭。有了计量才能形成健康的内部协作生态。Agent-Reach 目前在我们的体系里已经稳定跑了半年多日均处理数百万次 Agent 调用。回头来看最有价值的不是系统本身而是那套先定义触达能力再定义架构的方法论。如果你也在做多 Agent 系统的整合工作我建议别急着买现成的编排框架先花两周时间把发现、寻址、触达这三个问题的答案想透彻能省下后面数不清的返工时间。至于是否自研、是否开源那都是后话了。