
1. 当多智能体系统开始“互相找不到对方”1.1 从一个诡异的生产事故说起事情是这样的。我们内部做了一个多智能体平台接了大概二十多个大模型驱动的 Agent有写代码的、查数据库的、做审批流的、生成报表的。每个 Agent 都有自己负责的专长域调用方只需要把任务丢给“调度中枢”由中枢决定让谁干。听起来很顺对吧直到某个周一客服机器人突然开始循环报错。日志里全是ERR_REMOTE_SERVICE_UNAVAILABLE但下游服务健康检查显示一切正常。我拉出调用链看了一眼发现 A Agent 要调用 B AgentB Agent 要调用 C AgentC Agent 要查一个内部数据服务而那个数据服务因为负载高把响应时间从 50ms 拉长到了 3s。结果是 A 等 B 等 C 等数据服务层层超时层层重试十分钟内把整个消息队列打爆了。当时第一反应是加超时时间改重试策略。但后来发现根本问题不在网络层而在“触达层”——我们缺少一套机制保证一个 Agent 在某个时刻真的能触达它需要的另一个 Agent 或工具。传统微服务架构里有注册中心、服务发现、熔断降级但 Agent 之间的问题比这个更微妙它不仅要求对方在线还要求对方“当前有能力处理、并且语义上匹配、并且在可接受的成本内”。我把这套机制命名为Agent-Reach核心就是解决 Agent 的触达可达性问题。1.2 Agent 协同中的三类触达问题把问题拆开之后我发现多 Agent 场景里的“触达”要分三层看。第一层是网络触达。这和我们平时讲的服务发现一样Agent 实例在不在、端口通不通、连接池够不够。这一层最容易解决也最不该出问题但很多事故都发生在这一层之外。第二层是语义触达。这是 Agent 场景最特别的地方。一个 Agent 暴露的能力往往是一段自然语言描述比如“我可以帮你查订单”。调用方发的请求却是“查一下最近三天未发货的订单量”。如果这个 Agent 的意图识别模型没有匹配上哪怕它地址是通的、负载是低的这次触达依然是失败的。传统注册中心不会管语义匹配但 Agent-Reach 必须管。第三层是能力触达。即使语义匹配成功Agent 此刻不一定有能力处理。它可能正在等用户补充上下文可能依赖的外部工具已经断连可能自身的上下文窗口已经满了。所以触达判断必须带上对“能力余量”的感知。这三个问题叠在一起不能只靠“ping 一下”解决。我们内部讨论过要不要直接用现成的服务网格比如 Istio 或者 Linkerd做流量治理。但服务网格管的是 L4/L7 流量它不知道你调用的目标是不是一个“此刻不适合干这活”的 Agent。我们需要在业务层做一次语义路由同时兼顾负载、健康、成本。1.3 Agent-Reach 不是什么这里我要先给概念划个边界避免后面误导人。Agent-Reach 不是一个模型也不是一个 Agent 框架本身。我们没有去重写 Agent 的推理循环也没有定义 Prompt 怎么管理。它更像一个中间层夹在 Agent 和 Agent 之间负责回答三个问题找谁能不能找怎么找最优它也不是一个消息队列。消息队列解决的是异步解耦Agent-Reach 解决的是同步或异步场景下的目标选择和触达保障。你可以把它理解成“具备语义感知能力的服务发现 路由 熔断”的组合体。定位想清楚了后面的设计就顺理成章。2. Agent-Reach 的可达性模型从“连得上”到“触得到”2.1 定义 Reachability 的四个维度很多系统把“可用性availability”和“可达性reachability”混为一谈。Agent-Reach 里我强制团队区分一个 Agent 在线不代表它可触达。我们给每个触达目标计算一个 Reachability 分数分数由四个维度合成。Liveness存活度进程是否活着接口是否响应心跳权重占比不高但它是前提。Match匹配度调用方的请求和 Agent 的能力描述在语义上的匹配程度。我们用向量相似度加规则约束一起算权重最高。Capacity能力余量Agent 当前还能接多少任务、有没有处于阻塞状态、依赖的下游工具是否健康权重第二。Cost成本估算这一次触达要花多少 token、多少时间、多少费用权重第三。四个维度的分数最后合成一个 0-1 之间的 Reachability Score。调用方不再直连 Agent而是问 Agent-Reach“谁当前最合适”拿到的不是地址列表而是一个按分数排序的目标列表。2.2 匹配度不仅仅是“活没活着”匹配度是最容易想简单、也最容易翻车的地方。一开始我们天真地以为给每个 Agent 写一段能力描述然后用 embedding 算一下余弦相似度就行。结果效果非常差——用户说“帮我写个正则表达式”代码生成 Agent 的匹配度反而低于数据分析 Agent因为后者描述里出现了“处理文本”字样。后来我们改了算法不只算两个文本的向量相似度还要走一层结构化意图映射。每个 Agent 在上线时必须声明一个能力描述文件里面包含领域标签比如code_generation/data_analysis/approval_flow输入输出 Schema典型示例问法至少 10 条拒绝条件哪些请求它明确不处理调用请求进来后Agent-Reach 先用一个小模型或者规则分桶取决于延迟要求把它映射到可能的领域标签再做候选过滤最后才做向量排序。这样比单靠 embedding 稳定很多也方便做可解释性——我们可以告诉调用方“你为什么匹配到它”。2.3 成本模型把调度变成可计算的问题多 Agent 场景里成本不只是钱的问题。同一个任务让一个轻量 Agent 干可能要多轮提示才能完成让一个重型 Agent 干可能一锤子搞定但 token 消耗巨大。如果系统只盯着匹配度很容易把所有请求都打到最强 Agent 上既浪费钱又拉高延迟。Agent-Reach 的成本模型走了两步第一步是静态成本估算。每个 Agent 在注册时填一个基础成本档案包括模型规格、平均单轮 token 消耗、历史平均耗时。这个值会随着运行数据不断修正。第二步是动态成本惩罚。如果某个 Agent 当前的等待队列已经很长Reachability 分数里会加一个惩罚项让调度自动避开热点。这比轮询负载均衡更聪明的地方在于它知道每个任务对不同 Agent 的“体感负载”是不一样的——一个简单查询对报表 Agent 可能轻如鸿毛但对写代码 Agent 来说意味着拉起一整条工具链。有了这四个维度我们才敢说“触达”这件事是可计算、可调度、可观测的。接下来聊架构。3. 核心架构拆解注册、解析、通道、监控3.1 Reach Registry能力描述与心跳Agent-Reach 的第一块是注册中心但我们没有直接用 Consul 或 Nacos而是在它们之上加了一个Agent 元数据层。这一层存的东西很特别基础地址Agent 实例的 host:port协议类型。能力描述上面说的领域标签、Schema、示例问法。健康信号除了进程心跳还要带上“业务健康度”比如依赖工具是否正常、最近请求成功率。运行统计当前并发数、平均处理时长、Token 消耗速率。注册中心对写操作要求不高但对读操作的要求很高路由决策需要在几十毫秒内拿到候选列表。所以我们在内存里维护了一份全量数据副本用 Watch 机制同步查询完全走本地缓存。3.2 Reach Resolver语义路由怎么做Resover 是 Agent-Reach 里最核心的模块它负责把“请求意图”翻译成一个有序的 Agent 列表。整个流程是流水线式的。意图桶分类先根据请求文本命中规则或分类模型进入一个或多个候选桶比如“写代码”“查数据”“审批”。候选过滤在候选桶里筛掉 Liveness 不达标、Capacity 为负的 Agent。排序评分对剩余候选算 Reachability Score。决策输出默认返回 Top 1但允许调用方指定 Top K用于实现并行竞速或人工兜底。这里我特别想说一下“语义路由和传统负载均衡”的关系。我们保留了一个开关可以退化成纯随机或轮询方便做 A/B 对比。实测下来语义路由在“任务成功率”上的提升是立竿见影的因为很多失败根本不是服务端异常而是把任务派给了压根不是干这事的 Agent。3.3 Reach Channel消息的可靠触达路由找到了目标接下来的问题是怎么把请求送过去并且拿到结果。Agent-Reach 的 Channel 层做了一层带有业务语义的消息通道作用类似 RPC 框架但多了两个东西语义超时普通 RPC 只会设置一个总体超时时间但我们根据目标 Agent 的历史耗时分布动态计算超时。比如某个 Agent 过去 90% 的请求在 5s 内返回超时就设成 8s而不是拍脑袋设 30s。结果回退如果 Top 1 Agent 返回了不满足调用方 Schema 的结果Channel 会自动把同一个请求路由给 Top 2 Agent并合并上下文提示。这个机制显著提高了端到端任务成功率代价是多花了一点成本。通道层不自己实现网络协议默认走 gRPC也支持 HTTP/SSE 这类长流式协议。因为我们不是真的要造一个 RPC 框架而是把超时、重试、回退这些策略统一管起来。3.4 Reach Monitor度量与自愈最后一块是监控但它不是为了出报表而是为了形成闭环。我们记录了每一次触达决策的全过程请求特征原始文本、映射后的桶、候选列表。决策结果选了谁、分数构成。执行结果成功、失败、超时、回退。后续订正调用方对结果满意/不满意。这些数据会反哺注册中心的成本模型和匹配度模型。比如某个 Agent 频繁在能力余量不错的情况下返回低质量结果它的 Match 分就会被自动下调一段时间内被调度的概率就会降低。这就是 Monitor 的“自愈”作用。4. 配置与 API 实践十分钟接入一个 Agent4.1 一个最小配置样例说了这么多原理总要落地。Agent-Reach 的使用方式很直接每个 Agent 在启动时向服务注册一个能力描述文件。下面是一个最小化的 YAML 示例name: order-analysis-agent version: 1.0.0 endpoints: - protocol: grpc address: order-analysis-agent:50051 capabilities: domains: - order_query - data_analysis input_schema: type: object properties: order_id: { type: string } start_date: { type: string } end_date: { type: string } examples: - query: 查一下最近三天的订单量 labels: [order_query, data_analysis] reject_conditions: - 不处理任何涉及修改订单状态的操作 runtime: avg_latency_ms: 1200 avg_token_per_call: 4500 cost_per_call: 0.002调用方接入只需要继承我们提供的客户端 SDK。伪代码大概是这样的from agent_reach import ReachClient client ReachClient(endpointreach-service:8899) resp client.call( agentorder-analysis-agent, # 也可以传语义关键词 payload{start_date: 2025-01-01, end_date: 2025-01-03}, timeoutauto, fallbackTrue ) print(resp.result)如果不指定 agent只传一段自然语言任务描述Agent-Reach 会走 Resolver 自动选目标。SDK 还支持top_k参数用于并行竞速多个 Agent看谁先返回符合 Schema 的结果。4.2 动态注册与离线摘除在开发环境里最坑的一件是Agent 的版本迭代很快能力和行为经常变。如果每次改 Prompt 都要重新注册团队根本维护不过来。我们的做法是给 Agent-Reach 开一个治理接口Agent 可以在每次启动时拉取一遍自身的“能力指纹”并主动上报。运行时允许 Agent 动态调整能力描述但每次调整都会进入审计日志。这个设计对“新旧版本切换”特别友好新版本 Agent 先灰度上线注册中心里同时存在两个版本的元数据。Resolver 默认选中新版本但允许配置 10% 流量打到旧版做对比。一旦新版本指标变差自动回滚到旧版整个过程不需要重启调用方。4.3 调用端超时与重试参数怎么拍超时和重试是我见过最容易出事故的参数也是没有经验的人最喜欢拍脑袋的地方。在 Agent-Reach 体系里我不建议给所有请求配统一超时。更科学的做法是让 Channel 根据 Agent 的avg_latency_ms和p95_latency_ms动态算。我们的默认公式是session_timeout p95_latency * 1.5 buffer其中 buffer 至少 2s给流式输出留足首包时间。重试就更要谨慎我们默认只在以下三种场景自动重试网络连接失败不是超时收到明确的RETRYABLE_ERROR状态调用方显式要求 fallback至于超时引发的重试几乎都要避免。因为多 Agent 调用链路往往很深你在这里重试下游 Agent 同时也在重试瞬间就会放大流量。后面我会讲那个坑。5. 实测踩坑环路风暴、雪崩重试与脑裂5.1 触达环A 找 BB 又找 A上线第二周我们遇到一个典型问题Agent A 是个任务编排类 Agent它会拆解任务并把子任务发给其他 AgentAgent B 是通用助手遇到复杂任务时也会调用 A 帮忙。某一次流量高峰A 接到一个请求拆出一个小任务Resolve 后选中 BB 一看这个东西太专业继续 Resolve又选中 A。两个 Agent 互相踢皮球每次调用都会算入一个新链路的 Reach 分数导致“触达成功”指标一直很高实际上业务卡死在死循环里。修复方案分两层第一层Registry 里为每个 Agent 标记禁止环依赖的拓扑关系Resolver 在决策时自动剪掉那些会让调用链成环的候选。第二层调用链上带上reach_depth字段每经过一次 Agent-Reach 就加一超过最大深度默认 5直接拒绝并提示“触达链路过深”。这属于很基础但必须的防御跟分布式系统里设置 TTL 一样防止消息在拓扑图中无限循环。5.2 重试放大一次超时引发的雪崩这个坑更痛。当时我们为了保险给 Channel 默认开了“最多重试两次”。表面上看没问题但没想过一条链路上有四五层调用每一层都重试两次失败率稍微一高流量瞬间变成原来的 16 倍。那次事故的导火索是数据服务抖动响应时间从 1s 变成 10s。所有相关 Agent 都挂在等待上调用方等不到结果就开始重试Agent-Reach 在 resolver 层又因为候选 Agent 超时给它们标记了健康分下降于是请求开始向剩余的 Agent 转移。结果剩余 Agent 也被挤爆了。后来的教训是重试必须有预算。我们把每个原始请求允许的总重试次数限制在 3 次以内所有子调用共享这个预算而不是每一跳独立重试。超时必须用桶。一旦某个 Agent 的 p99 超时明显恶化Monitor 会暂时把它的 Liveness 分拉低让新请求不再上去而不是等它自己恢复。这两个改动之后系统在依赖抖动时最多只是变慢不再出现雪崩。5.3 脑裂后的注册表不一致Agent-Reach 的 Registry 在设计时借鉴了 Raft 的思想但我们内部没有自研一致性算法而是直接架在现有 KV 存储上。问题出在多机房部署时两个机房之间的网络出现分区各机房都认为自己的主节点是“活的”于是出现了两份不同的能力数据。脑裂的表现很隐蔽同一个 Agent 在 A 机房注册的版本是 v2在 B 机房还是 v1。Resolver 返回的地址可能是旧实例调用方拿到的结果行为不一致甚至出现同一业务在前后端状态对不上。我们的最终方案是把“注册”动作的权威节点收敛到单一逻辑区域跨区域只有只读缓存。也就是说所有 Agent 的注册写操作必须打到主集群其他区域可以本地化读取但不能本地化修改。这个简单粗暴的方案牺牲了一点跨区域扩展性却换来了强一致。对大部分 Agent 协同场景来说这个取舍很合理。6. 调优后的一组真实数据6.1 测试拓扑与压测方法为了验证 Agent-Reach 的价值我们搭了一套 10 个 Agent 的测试拓扑包括 3 个业务 Agent、2 个工具型 Agent、1 个编排 Agent以及若干模拟故障的节点。压测工具是一套自定义的 Go 测试程序能模拟不同意图的请求还能随机对下游注入延迟、超时和错误。对比基准有两套一组直接使用随机路由 固定超时20s作为“传统模式”。一组使用 Agent-Reach 的语义路由 动态超时 回退作为“Reach 模式”。每组跑 30 分钟QPS 控制在 50同时统计成功率、平均响应耗时、Token 成本。6.2 关键指标变化结果如下表所示指标传统模式Agent-Reach 模式任务端到端成功率68.4%94.2%平均端到端耗时18.3s9.6sToken 成本均值83006400因超时导致的重试率12.7%2.1%热点 Agent 的最大并发数3812成功率提升的最大来源是“语义匹配”和“结果回退”。传统模式经常把一个“查订单”的请求发给“写代码”Agent人家硬着头皮用工具也是能搞但效果很差经常返回让用户再清晰一点一来一回就超时了。响应耗时下降主要来自动态超时和热点惩罚请求宁可多等一会儿也不挤到头重脚轻的 Agent 上。成本降低是因为减少了无效调用没有我说的那么玄乎就是少折腾了几次而已。6.3 仍然存在的改进空间Agent-Reach 目前还有一个让我不太满意的地方语义匹配模型在冷启动时效果一般。新 Agent 刚上线时只有几十条示例问法达到稳定匹配效果至少要跑一周。所以我们准备引入一种人工反馈机制——调用方可以用非常轻量的方式标记结果是否满意这些标注会回流到匹配模型中做在线微调。另一个方向是触达成本的实时竞价。现在我们的成本惩罚还是粗粒度的未来希望做成类似“预算上限内选最优 Agent”的机制让每个团队能动态调整自己的成本偏好。最后一件事个人实践体会如果你也在做多 Agent 平台我建议不要一上来就搭大而全的框架先花一周时间统计你现在的“触达失败”到底发生在哪一层。我们最初以为只是超时和负载问题后来发现大量失败是因为调用方根本不知道该调谁甚至调错了。Agent-Reach 这个名字本身就代表了一种思路转变服务发现的目标不是“找到活的节点”而是“让请求真正到达能处理它的智能体”。如果你只在网络和并发层面发力那大概率解决不了语义错配的问题。反过来只要你把语义、容量、成本放进同一个路由决策模型里哪怕模型粗糙一些多 Agent 系统的稳定性和效果都会有质的提升。这套设计里的所有核心代码和配置我们已经全部沉淀到了内部工具库使用方式就是我上面写的那样。如果你正在做类似的东西或者踩过更奇葩的触达问题欢迎一起讨论。