news 2026/10/9 6:32:22

多智能体协作触达编排:服务发现、路由与容错的Agent-Reach实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
多智能体协作触达编排:服务发现、路由与容错的Agent-Reach实践

去年下半年我在重构一个内部的多智能体协作平台时,被一个问题反复折磨:每个智能体单独拎出来都能干活,但一旦让它们互相调用、共享上下文、协同完成任务,整个系统就变成一团乱麻。有的 agent 不知道去找谁要数据,有的 agent 被重复调用导致负载飙升,还有的 agent 一超时,整条任务链就崩掉。说白了,我缺的不是更强的单点能力,而是一层能管住“谁触达谁、怎么触达、触达不了怎么办”的基础设施。于是我自己动手搭了一层专门做触达编排的中间层,内部代号就叫Agent-Reach。

这个项目里 Agent-Reach 并不是一个类似“万能 API 网关”的转发工具,它更像给所有智能体配了一张统一的“通讯录 + 路由表 + 容错策略包”。从设计之初它要解决的就是 multi-agent 场景下最 boring 但最致命的问题:服务发现、路由选择、调用容错和全链路观测。这篇文章把整个项目的设计思路、核心模块和我实测中踩过的坑完整拆出来,如果你是做 AI Agent 应用、或者正在被多智能体协作搞得头疼,应该能直接拿走不少经验。

1. 从“单兵作战”到“触达网络”:Agent-Reach 要解决的问题

1.1 多智能体协作为什么总是卡在“互相找”这件事上

先说说我遇到的问题场景。单个智能体跑一个任务,本质上是一条直线:接收请求,调工具,出结果。但多智能体不一样,它是一个网状结构。比如一个客服系统里,有意图识别 agent、情绪分析 agent、知识库检索 agent、订单查询 agent,用户问一句“我之前买的东西为什么还没发货”,意图识别 agent 判断出这是售后查询,接着要把上下文转给订单 agent,订单 agent 又要调知识库 agent 获取物流政策,最后还要把情绪分析结果一起组装成回答。

听起来很简单,真做起来全是问题。最直接的就是“谁在哪儿”:每个 agent 部署地址不一样,有的是 HTTP 服务,有的走 gRPC,有的是内部函数直调,还有的其实是另一个团队维护的第三方接口。如果每个 agent 都各自维护一张 endpoint 清单,代码里到处写死 IP 和路径,改一次部署就要通知所有人连夜改配置。

更麻烦的是调用关系没有规则。agent A 可以直接调 B,B 又可以调 A,形成了隐藏的循环依赖;某个 agent 压力一大,响应从 200ms 涨到 5s,下游调用方还在用固定 3 秒超时,于是整条调用链路不断报错。等到你想排查问题的时候,日志是各个 agent 自己打的,trace 上下文断了一路,根本不知道是哪一环出了问题。

Agent-Reach 就是冲着这些问题去的。它的核心职责只有三块:告诉调用方“谁能提供我要的能力”、用统一规则完成一次触达、在触达失败时按策略优雅降级。它不干预单个 agent 内部的推理和逻辑,只专注管好 agent 和 agent 之间、agent 和外部工具之间的那条“连接”。

1.2 Agent-Reach 的逻辑定位

我一开始也纠结过,要不要直接用一个现成的服务网格或者 API 网关来做这件事。后来发现它们解决的是另一层问题:服务网格管的是流量,API 网关管的是对外暴露接口,但多智能体之间的触达是有“语义”的——调用方要找的不是某个 URL,而是某种“能力”,比如“查订单状态”“翻译文本”“识别用户情绪”。具体哪个 agent 能提供这个能力,此刻这个 agent 是否健康,用同步调用还是异步消息,这些都需要一个更上层的编排。

所以 Agent-Reach 的定位更像一个面向智能体的语义触达层。它向上对 agent 暴露一组简单的 SDK 接口,向下管理各种 agent 和工具的实际 endpoint。调用方只需要说“我要调用 order.query 这个能力”,Agent-Reach 负责把它翻译成具体请求、发送到正确的目标、处理超时和重试、并返回统一格式的结果。

这样设计有一个额外的好处:agent 之间的耦合从“代码级”降到了“契约级”。你和另一个团队合作,不用再跑到对方仓库里看接口实现,只要双方约定好能力标识和数据格式,触达的事交给 Agent-Reach 就行。

2. 触达层的三个核心模块与调用链设计

2.1 能力注册中心:先解决“谁能干什么”

整个 Agent-Reach 最早起步的模块就是一个能力注册中心。每个 agent 启动时需要向注册中心上报自己提供的能力清单,包括能力名称、版本、入参出参的 JSON Schema、endpoint 类型、健康检查地址等。注册中心把这些信息聚合起来,形成一张能力索引表。

我最初版本用的是简单的数据库表 + 定期心跳上报,后来改成 etcd 存储,因为需要对上线下线有更实时的事件通知。每当有 agent 实例上线,注册中心会推送一条变更事件给所有订阅了该能力的调用方。这么做的好处是调用方本地会缓存一份能力路由表,Agent-Reach 本身挂掉或者网络抖动,已经建立的路由关系还能继续工作一段时间,不会全校瘫痪。

这里有个容易踩坑的点。能力名称的命名规范必须从第一天就定好,而且要强制约束。我们用的格式是域.动作.对象,比如crm.order.query、nlp.sentiment.analyze。千万不要让每个团队按自己喜欢的方式起名,什么queryOrder、OrderQueryService、get_order_info混在一起,注册中心直接就变成垃圾场了。哪怕格式严格一点,后面做权限管理和路由匹配都会省心很多。

2.2 路由与编排引擎:不只是一个转发器

注册中心告诉你“有哪些能力”,路由编排引擎负责决定“这一次调谁”。这里的策略不是简单的负载均衡,而是要考虑多约束条件。

我的实现里,每个能力可以注册多个 provider(不同的 agent 或工具都声明自己能提供同一个能力)。路由引擎按以下顺序筛选:

  • 上下文亲和性:如果调用方已经指定了要优先使用某个 provider,比如之前对话会话锁定了某一个 agent,那么路由引擎保持会话内的连续触达,不切换到别的实例。这对一致性问题很重要,尤其是涉及到多轮对话状态的智能体。
  • 健康状态:拉取每个 provider 最近的心跳和错误率,过滤掉已经熔断或处于降级状态的实例。
  • 权重与优先级:支持两种模式,一种是主备模式(primary + backup),一种是负载模式(round-robin / 最少连接)。主备用于核心链路,负载用于可以水平扩展的服务。

其中上下文亲和性是我真正遇到问题后才加进来的。早期版本不管这些,每次调用都用负载均衡,结果智能体 A 先调了一次“查订单”能力,准备再接着调“退款申请”能力时,路由引擎把请求分给了另一个 provider,那个 provider 没有对应的会话状态,直接返回“找不到上下文”。后来在能力契约里增加了一个可选的 affinity key 字段,路由引擎用这个 key 做哈希分配,才把问题解决。

2.3 传输适配器:屏蔽协议差异的最后一公里

触达层面向调用方提供的 SDK 接口只有那么几个,但底层为什么能同时接 HTTP、gRPC、消息队列、甚至直接进程内调用?靠的就是传输适配器层。

每一种底层通信方式都实现同一个接口,这个接口大致只有三个方法:Invoke(request)用于同步请求-响应,Cast(request)用于异步发送-不等待,InvokeStream用于流式返回(跑大模型的时候特别有用,文本是一块一块生成的)。适配器内部负责把契约数据转成目标服务需要的具体格式,比如对方是 protobuf 定义的接口,适配器就按 proto 序列化;对方是 JSON-RPC,适配器就封装成 JSON-RPC 请求。

我一开始想把所有底层协议都统一成 HTTP + JSON,省事。结果发现很多现有的 agent 是 Python 写的 gRPC 服务,强行改成 HTTP 意味着要改一堆既有代码。适配器模式的好处是既能兼容老系统,又能让新服务直接走最高效的通信方式。传输层花的时间不值得省,它是保证触达层能被顺利推下去的关键。

2.4 一次完整调用的数据流

把上面几个模块串起来,一次完整触达大致是这样:

  1. 调用方 agent 通过 SDK 发出reach.invoke("crm.order.query", payload, options)。
  2. Agent-Reach client 先从本地路由缓存里找到crm.order.query对应的 provider 列表,同时带上调用链追踪 ID。
  3. 路由引擎做一次筛选,选定目标 provider。
  4. 传输适配器根据 provider 的类型,构造实际请求并发送。
  5. 目标 agent 执行完逻辑,返回结果,适配器把响应统一包装成内部 Result 结构,包含状态码、数据体、耗时、错误信息。
  6. 调用方拿到 Result,再决定是继续下一步还是走失败兜底。

这条链路我测过无数次,正常情况下一次触达的额外开销控制在 1~2 毫秒左右(不包括目标 agent 本身的耗时),因为路由筛选和适配器转换都是轻量计算,真正的网络耗时还是占主导。但如果加了重试和熔断检查,这个耗时会有波动,后面专门讲容错的部分。

3. 命名服务与路由策略:最容易被低估的部分

3.1 为什么说“找得到”比“调得快”更重要

很多人听说 Agent-Reach 做得是个路由,第一反应是“哦,就是个负载均衡”。但实测下来,多智能体场景路由的难度和传统微服务完全不在一个量级。

传统微服务的路由非常成熟,目标服务基本是稳定的 IP + 端口,路由本质上就是挑一个可用的实例。但多智能体触达层面对的是会动态变化的能力列表。同一个能力,可能上午只有一个 agent 提供,下午另一个团队上线了新版 agent 也声明了同一个能力。能力版本升级期间,老版本和新版本要同时在线跑一段时间,路由层还要支持按版本灰度。

这让我意识到,路由策略的核心不是“选一台机器”,而是“选一个能够完成用户语义目标的 agent 实例,并且这个选择的依据本身要可解释、可调试”。没有命名服务作为基础,后面所有策略都是空中楼阁。所以我在 Agent-Reach 里单独做了命名服务模块,它不光管服务名,还管能力名、版本规则和标签。

3.2 基于能力而非端点的路由规则

我把 Agent-Reach 的路由设计成两层。第一层是“能力解析”,第二层是“实例选择”。

能力解析做的事,是把调用方请求里的能力名(如crm.order.query)匹配到一组 provider 集合。这个过程需要处理 aliases(别名)和版本范围。比如crm.order.query@^2.0表示只要 2.x 版本的 provider,但如果系统里只有一个 1.8 版本的 agent,路由引擎会返回失败,而不是自动降级到旧版本——语义结果可能完全不同,自动降级比失败更危险。

实例选择则是第二层筛选,逻辑比较简单,除了上面说的健康状态和权重,我还会加上一个 locality 策略。同一个数据中心的 agent 优先互调,避免跨机房触达带来的高延迟。这个策略对效果提升很明显,刚开始没做,测试环境没感觉,上了生产发现跨机房专属链路调用动辄多出几十毫秒 RTT,加了 locality 后整体延迟降了一个档次。

还有一个容易被忽略的设计:空结果不一定是错误。有些 agent 比如“情绪分析”,面对中性文本时会返回一个低置信度的中性结果,这不算失败。但路由层如果只按 response code 判断,可能会触发重试,导致重复消耗算力。所以我在契约里给 Result 加了一个definitely_success标志,供上层判断是否值得重试。

3.3 路由失败时的回退策略

路由可能失败的原因很多:没有 provider、所有 provider 都不健康、没有匹配版本、provider 超时等。Agent-Reach 对每种失败类型都定义了明确的行为:

  • 找不到 provider:直接返回错误码,调用方不重试,因为它重试一万次也没用。
  • provider 全部不健康:如果配置了 fallback 链(比如主用订单 agent,备用走一个简化的接口服务),执行 fallback;否则快速失败。
  • 超时或者响应异常:按调用方指定的重试策略执行,默认最多重试一次,且只允许选择另一个 provider。

我在实际运行中发现,fallback 链的配置最能体现业务对可用性的容忍度。有些场景允许降级返回一个粗糙结果(比如“订单查询失败,请稍后再试”),有些场景不行(比如支付失败,必须让用户明确知道失败原因)。所以 fallback 不该是一刀切的配置,每个能力都要能独立设置 fallback 行为和降级文案。

4. 超时、重试与熔断:让多个智能体协作不失控

4.1 超时时间不能拍脑袋,要有分层设计

多智能体协作里最让人头疼的故障源,就是超时设置。之前团队的做法是每个调用方自己定超时时间,结果五花八门,有的设 2 秒,有的设 30 秒,链路一长,整体耗时完全不可控。

Agent-Reach 引入了一套分层超时机制。每个触达请求可以带一个总的 deadline,整个链路内的所有子调用必须在这个 deadline 内完成。比如一次客服回复生成任务,总 deadline 设为 10 秒,分配给意图识别 1.5 秒、情绪分析 1 秒、订单查询 2.5 秒、知识库检索 3 秒、大模型生成 2 秒,加起来正好 10 秒。任何子调用如果提前完成,剩余时间可以传递给下游;如果子调用超时,立刻返回失败,不再无谓等待。

这套机制实现起来并不复杂,就是在请求 context 里塞一个 deadline 时间戳。但收益巨大。调用方不会因为某个 agent 卡住而无限等下去,整条任务链的可预测性大大增强。强推 deadline 机制后,我们线上 P95 耗时直接从原来的 15 秒以上降到了稳定 8 秒左右,用户体验改善非常明显。

4.2 重试策略:不是所有失败都值得重来一次

重试看起来简单,实际是最容易引发雪崩的操作。一个 agent 已经负载很高了,响应超时,如果所有调用方都立刻重试,等于给这个 agent 又来一波猛攻,直接打挂。

Agent-Reach 的重试策略有几个默认原则。第一,只重试安全的调用,也就是幂等的操作。写操作默认不重试,除非调用方显式声明这是幂等的。第二,重试必须换 provider。同一个 provider 刚才能超时,马上再打一次大概率还是超时,换一个健康的实例才有意义。第三,重试次数有上限,默认 1 次最多 2 次,控制整体延迟增长。

另外我加了 Exceeded deadline 检查:如果当前剩余时间已经不足总 deadline 的 20%,直接放弃重试,不管重试多少次都来不及在 deadline 内返回了,不如快速失败让上层做兜底。

4.3 熔断与隔离:别让一个坏 agent 拖垮全局

熔断这块我借鉴了微服务里比较成熟的 Circuit Breaker 模式,但根据大模型类 agent 的特点做了一些调整。大模型 agent 的失败模式往往不是“完全不可用”,而是“间歇性高延迟”。所以触发熔断的条件不仅是失败率,还包括慢调用率。比如在滑动窗口内,如果有超过 30% 的调用耗时超过某个阈值(比如 3 秒),就认为该 provider 处于亚健康状态,打开熔断开关,后续请求快速走 fallback,不再进入这个 provider。

熔断还有一个半开状态,隔一段时间放少量试探请求进去。如果试探成功,慢慢恢复流量;如果继续失败,继续维持熔断。

我在实践中的感受是:熔断参数一定要按 provider 实际表现单独调,通用的默认值只能保底。有的 agent 是纯规则服务,正常耗时 50ms,那 200ms 就值得警惕;有的 agent 底层是 GPT 类大模型,一次生成要 2~5 秒,那阈值就得放宽到 10 秒。参考熔断组件 Hystrix 的经验,我对每个能力维护了一套独立的熔断配置,尽管运维成本高了一些,但整体稳定性提升显著。

4.4 限流与背压:防止调用方打爆下游

触达层还有一个隐性作用,就是当多个调用方同时请求同一个能力时,避免瞬间流量打爆单一 provider。Agent-Reach 提供了两层限流:一是调用方维度的并发限制,防止某一个调用方霸占某能力;二是 provider 维度的并发保护,超过阈值的请求直接返回资源忙错误,而不是排队等。

排队这件事我特意不做。排队看着是友好,实际上在 deadline 机制下毫无意义——排到一半已经超时了,白白浪费了 provider 的处理资源。直接快速失败,让调用方决定是降级还是另找 provider,比在队列里干等更合理。

5. 可观测性与调试:多智能体排障的真实体验

5.1 全链路追踪:把一盘散沙串成一条线

多智能体系统最反人类的排查体验,是问题不知道出在哪一环。用户报了一个 bug,你说“我来看看”,然后开始翻 20 个 agent 的日志。如果每个 agent 的日志系统还不统一,通常需要开 5 个终端窗口同时 tail,最后发现在中间的某一个调用里数据参数传错了。

Agent-Reach 在早期就把 tracing 内建进去了。每一次触达调用都携带一个全局 trace ID,路由引擎在每个节点记录 span 信息,包括 provider 名称、耗时、状态、错误信息,统一上报到日志平台。前端调试时只需要一个 trace ID,就能看到完整的调用时间线。

这个设计最值钱的地方不在于“好看”,而在于它让“触达失败”从一团迷雾变成一个可定位的事件。我见过太多多智能体项目死在 debug 阶段,不是能力不行,是出了问题没人能说清楚在哪一步挂的。可观测性不是锦上添花,是这类系统的生死线。

5.2 本地沙箱与录制回放

为了高效调试 Agent-Reach 本身的路由逻辑,我还在本地搭了一个沙箱模式。沙箱里跑的不是真实 agent,而是 mock 服务,每个 mock 可以配置固定延迟、固定错误率、特定异常行为。路由引擎的各种超时、熔断、fallback 策略,先在沙箱里跑一遍测试用例,再上到 staging 环境验证。

更有用的是录制回放功能。线上实际请求经过 Agent-Reach 时,会把请求和响应上下文录制一份(脱敏后)存下来,可以拿到沙箱里回放,用来复现某个特定路由决策是否合理。比如线上有一个请求走了 fallback,但你不确定这个 fallback 决策对不对,于是回放请求,调整不同策略参数看结果差异。这个功能成了我后来调优路由策略的主要工具。

5.3 告警规则的经验值

可观测性不只是事后排查,还需事前预警。我总结了两个经验值,直接分享给读者参考:

  • 当某个能力的 P95 耗时在 10 分钟内连续上升超过 40%,说明这个 provider 可能出问题了,需要告警。
  • 当熔断从关闭到打开的切换次数在 1 小时内超过 3 次,说明熔断参数设置不合理或者 provider 状态飘忽,需要人为介入。

告警规则弄太多反而是负担,运维同学会被告警疲劳淹没。我只保留了这三四个高价值的告警项,剩下的交给 dashboard 自行查看,实践中效果比原来全量告警好了太多。

6. 落地实践中的取舍与未尽事项

6.1 什么时候你其实不需要 Agent-Reach

任何架构都有边界,Agent-Reach 也一样。如果你的系统里只有两三个 agent,调用关系基本固定,互相之间直接写调用代码完全可以,引入触达层就是徒增复杂度。如果你的 agent 都跑在同一个 Python 进程里,用函数直接调函数,那更不需要。

我的判断标准是:当你的 agent 数量超过 5 个,或者不同 agent 由不同团队维护,或者需要支持动态伸缩时,才去考虑搭一个触达层。过早引入会产生不必要的抽象,反而拖慢开发。

6.2 协议选型:JSON-RPC 还是一个自研绑定协议

Agent-Reach 内部传输协议默认是 JSON-RPC 风格,每个请求包含 method、params、id 三个字段,响应也遵循 JSON-RPC 规范。选它是为了简单、通用、跨语言友好。Python、Go、Node 都很好解析,浏览器端调试也方便。

但随着传输的数据量变大,尤其是大模型流式输出的场景,JSON 序列化和反序列化的开销开始有点扎眼。后来我对流式场景单独做了二进制分帧协议,普通 JSON 场景保持不动。这个折中方案效果最好。换协议这块千万不要一刀切,按调用类型实际流量大概率是混合的。

6.3 身份认证与权限控制

多智能体之间的互调一样要管好权限。Agent-Reach 在每个能力注册时就要声明访问控制列表(ACL),只有被授权的调用方 agent 才能调用。比如知识库检索能力可以被意图识别 agent 调用,但不能被用户画像 agent 调用,防止内部数据被无关模块拉走。

认证实现比较简单,在触达请求里带上调用方 agent 的身份凭证,Agent-Reach 校验凭证是否对该能力有调用权限,无权限直接拒绝。这个机制看起来多余,等你的平台开放给第三方 agent 接入时就明白有多必要了。

6.4 现在还存在的问题

整个平台目前能稳定支撑线上业务,但还是有几个问题我暂时没有完全解决。

一是复杂跨域事务。一个用户请求可能会触发多个 agent 协作,如果中间某个环节写完数据库但在返回时超时了,整个事务的一致性怎么保证?Agent-Reach 目前只保证触达层的可靠性,业务上的一致性需要 agent 自己实现 Sagas 或者用事件补偿,这已经超出了触达层的范围,但作为整体设计必须提前思考。

二是上下文漂移。多智能体协作经常需要把一个大上下文传给多个 agent,Agent-Reach 自己不做上下文存储,全靠调用方在 payload 里携带。上下文一大,传输开销就上去了。后续我打算引入一个共享上下文存储服务,但这又会引入新的依赖和一致性问题,还在权衡。

三是人为干预通道。让系统完全自动处理可能出错的情况,我始终不放心。现在 Agent-Reach 支持管理员手工上下线某个 provider、手工切换流量,但在极端的线上事故中,这个操作还是不够快捷。未来做成“一键止血按钮”会更安心。

6.5 如果我重做一次,会在哪一步投入更多

这个问题我无数次问过自己。如果让我重新开始,我会把第一版的能力契约设计做得更严格,花更多时间和各团队对齐 JSON Schema 和命名规范。触达层本身的技术实现并不难,真正难的是让所有人都愿意遵守同一套契约。

一旦契约混乱,后续的路由、权限、trace 全部跟着倒霉。

另外我会更早地搭建沙箱调试环境。第一版几乎是在 staging 环境里靠手工构造请求来测,效率极低。后来沙箱建好后,整个迭代速度明显变快,很多策略可以在几分钟内验证完,而不是等环境、造数据、再验证。前期的“慢”和后期的“快”之间,确实需要慢慢找到平衡点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 6:30:40

Claude Code 三套配置体系:settings.json、CLAUDE.md 与 memory 分层详解

1. 三套配置体系到底在管什么很多人第一次接触 Claude Code,装完之后发现能跑起来就以为万事大吉了,结果用着用着就发现不对劲:每次对话都要重新交代项目背景,团队里每个人的行为风格不一样,换个项目又得从头调教。这些…

作者头像 李华
网站建设 2026/10/9 6:30:39

蝴蝶显微图像数据集:电子显微镜超分与去噪实战指南

简介:本资源是面向深度学习研究者与计算机视觉方向学生的显微图像专用数据集,聚焦电子显微镜图像质量提升任务,特别适用于超分辨率重建、图像去噪、细节增强等模型训练与验证。数据源自论文《Deep learning super-resolution electron micros…

作者头像 李华
网站建设 2026/10/9 6:29:54

分布式存储实战:优势、挑战与选型避坑指南

1. 单机存储撑不住的时候,分布式存储到底在解什么题1.1 先从一次“存储扩容事故”说起几年前我在团队里负责一个数据平台,业务跑着跑着,单机MySQL加上业务日志,容量已经到了十几TB。当时所有人的第一反应是“再加两块大盘子进去”…

作者头像 李华
网站建设 2026/10/9 6:28:23

Agent-Reach:解决智能体可达性缺失的轻量框架实践

如果你正在做 Agent 类应用,大概率遇过这样一种情况:模型本身能力不错,逻辑推理也到位,任务却还是莫名其妙地失败。不是它不会做,而是它“够不着”——上下文窗口被历史记录塞满,该找的资料找不到&#xff…

作者头像 李华
网站建设 2026/10/9 6:27:57

t3code:一句话生成可复用代码,打通团队代码资产沉淀闭环

如果你让我用一个词概括过去半年里对我日常编码习惯改变最大的东西,我会说 t3code。起因特别简单:我们团队每次接入新项目,都要在聊天记录里翻来翻去找“上次发过的那段鉴权代码”;每次写日期格式化,都要从旧工程里把那…

作者头像 李华
网站建设 2026/10/9 6:27:56

Loop Engineering 实战:让 AI 编程工具从能跑到跑得稳

1. 从"能跑"到"跑得稳":Loop Engineering 到底在解决什么问题大多数人第一次接触 Loop Engineering 这个词,是在折腾 Claude Code、Codex、Cursor 这类 AI 编程工具的时候。工具装好了,模型接上了,单次对话也…

作者头像 李华