免费获取学习方案
ARTICLE DETAIL

资讯详情

深耕编程基础知识与建站技术分享的一线实战洞察。

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

多智能体协作触达编排:服务发现、路由与容错的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 可以直接调 BB 又可以调 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 一次完整调用的数据流把上面几个模块串起来一次完整触达大致是这样调用方 agent 通过 SDK 发出reach.invoke(crm.order.query, payload, options)。Agent-Reach client 先从本地路由缓存里找到crm.order.query对应的 provider 列表同时带上调用链追踪 ID。路由引擎做一次筛选选定目标 provider。传输适配器根据 provider 的类型构造实际请求并发送。目标 agent 执行完逻辑返回结果适配器把响应统一包装成内部 Result 结构包含状态码、数据体、耗时、错误信息。调用方拿到 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 或者用事件补偿这已经超出了触达层的范围但作为整体设计必须提前思考。二是上下文漂移。多智能体协作经常需要把一个大上下文传给多个 agentAgent-Reach 自己不做上下文存储全靠调用方在 payload 里携带。上下文一大传输开销就上去了。后续我打算引入一个共享上下文存储服务但这又会引入新的依赖和一致性问题还在权衡。三是人为干预通道。让系统完全自动处理可能出错的情况我始终不放心。现在 Agent-Reach 支持管理员手工上下线某个 provider、手工切换流量但在极端的线上事故中这个操作还是不够快捷。未来做成“一键止血按钮”会更安心。6.5 如果我重做一次会在哪一步投入更多这个问题我无数次问过自己。如果让我重新开始我会把第一版的能力契约设计做得更严格花更多时间和各团队对齐 JSON Schema 和命名规范。触达层本身的技术实现并不难真正难的是让所有人都愿意遵守同一套契约。一旦契约混乱后续的路由、权限、trace 全部跟着倒霉。另外我会更早地搭建沙箱调试环境。第一版几乎是在 staging 环境里靠手工构造请求来测效率极低。后来沙箱建好后整个迭代速度明显变快很多策略可以在几分钟内验证完而不是等环境、造数据、再验证。前期的“慢”和后期的“快”之间确实需要慢慢找到平衡点。
返回列表