ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多Agent协作触达层设计:动态路由、并发控制与排障实践

多Agent协作触达层设计:动态路由、并发控制与排障实践 1. 从一个线上事故说起为什么需要 Agent-Reach先说个让我印象深刻的真实场景。某个周三下午我们正在准备一次多模型协作的功能演示演示链路里串了五个 Agent一个负责从邮件里抽取需求一个负责把需求拆成任务一个负责调用工具生成草稿一个负责质检还有一个负责归档。没跑两轮链路就卡死了。查了半天发现是第三个 Agent 调用工具超时超时后它既没有重试也没有把失败告诉下游更没有告诉调度方。第四个 Agent 傻等了整整两分钟然后把一个半成品当成结果传了下去。第五个 Agent 收到脏数据直接抛异常。那次演示最后是人工一条条把消息转录进下一个系统才勉强走完流程。这种问题在 AI 工程化里太典型了模型本身越做越聪明但模型与模型之间、模型与已有系统之间的触达能力反而成了瓶颈。所谓Agent-Reach我们内部给它的一个解释是智能体触达层——它解决的不是单个 Agent 的聪明程度而是 Agent 与 Agent、Agent 与外部工具、Agent 与人工处理之间能不能可靠、可控、低延迟地互相找到对方并完成协作。我写这篇文章的目的是把 Agent-Reach 从构思到落地、再到压测调优的完整过程做一个复盘。适合谁看一类是正在做多 Agent 协作编排但发现消息总线、事件驱动那套传统方案在 AI 场景下越用越别扭的工程师另一类是准备把 Agent 接入内部系统工单、审批、数据查询但不确定该用什么协议、什么注册机制、什么容错策略的开发者。本文不讨论某一个具体大模型怎么调重点在调度与触达层这是很多项目做到后期才会意识到真正难的地方。Agent-Reach 最核心的设计目标有三条动态可达合作方 Agent 的位置、能力、负载状态是动态变化的不能死在配置文件里。可控触达调用方能明确指定这次触达我要什么 QoS、什么超时、什么熔断策略而不是一把梭。可观测每一次触达都有链路 ID、执行轨迹、成本信息和失败原因方便回放。2. Agent-Reach 的架构骨架注册中心、路由矩阵、会话桥2.1 不再用配置文件写死调用关系第一版设计里我们走了弯路用的是传统微服务那套每个 Agent 启动的时候往注册中心基于 Etcd注册自己的服务和端口调用方通过服务名去查实例列表然后负载均衡选一个发起调用。这套东西跑起来没问题但用到 AI 场景里很快就暴露了不适配。原因在于普通的微服务注册服务名基本等同于能力比如 order-service你知道它能处理订单但 Agent 不一样。一个客服助手Agent它既能查订单又能算价格还能写话术你通过服务名根本判断不了它当前是不是有足够上下文来处理你这轮会话。也就是说传统注册中心里服务是稳定能力实例是无状态计算单元而 Agent 协作里能力是动态上下文的函数实例是带记忆和状态的工作体。这两种假设模型完全不同。所以 Agent-Reach 没有直接用现成注册中心而是在注册层之上加了一个语义能力矩阵。每个 Agent 在注册时除了写服务名和地址还提交一份能力声明——它擅长的任务类型、能够接受的输入格式、需要多长的上下文窗口、内置哪些工具以及当前可以接受的并发强度。这些信息以结构化的 JSON Schema 存放便于路由层做匹配。2.2 路由矩阵从找到服务到找到合适的 Agent路由矩阵是 Agent-Reach 相对传统注册中心最大的改动。它本身是一个内存中的有序结构每个进入的请求不是直接查 IP 列表而是先做一轮能力匹配 上下文匹配 优先级排序。我用一个很粗的伪代码来说明核心逻辑def route_request(req): candidates capability_index.query( task_typereq.task_type, required_toolsreq.required_tools, min_context_windowreq.total_tokens_estimate ) candidates filter_by_tenant_and_sla(candidates, req.tenant_id, req.sla_level) if req.preferred_agent_id: candidates sort_by_preferred(candidates, req.preferred_agent_id) candidates sort_by_load_and_affinity(candidates, req.session_id) return candidates[0]这段代码里排序策略比看上去重要得多。load不是简单看实时 QPS而是看这个 Agent 实例当前的连续性负载——比如它正维护着 20 个多轮会话就算它 QPS 很低也不适合再接入新的聊天反过来说一个 Agent 可能在跑批处理不需要维护会话状态就可以大胆把无状态请求塞给它。而affinity是按session_id做哈希固定路由尽量让同一个任务的多次触达落到同一个 Agent 实例上。这对 AI 场景是刚需——因为 Agent 的上下文在本地换一个实例往往意味着上下文要重新加载有时候还会带上脏历史造成输出不一致。2.3 会话桥让不同技术栈的 Agent 共享一套会话上下文多 Agent 协作里还有一个反直觉的坑两个 Agent 明明都在同一个系统里工作但它们可能一个用 Python 的 async/await一个用 Java 的线程模型还有一个是跑在 Node 里的脚本。如果只做 HTTP 调用会话上下文很难自然传递。Agent-Reach 的解决方案是会话桥所有跨 Agent 的交互消息都会被包一层标准的请求/响应封装关键字段包括session_id会话标识贯穿整条任务链。parent_span_id/span_id继承关系用于追踪一个子任务被哪个父任务触达。context_path上下文存储的引用位置可以指向内存缓存、Redis也可以是一个对象存储里的索引。qos调用方声明本轮触达的优先级与策略。会话桥本身不存 Agent 的业务数据它只存元数据和状态机真正的内容通过引用让各 Agent 按需读取。这样做的直接好处是消息量小传输快而且不会在多个 Agent 之间复制同一份巨大上下文。3. 触达链路的核心调度器与并发控制3.1 有界并发为什么不能有多少触达放多少触达Agent 和普通 API 有个很大的差异它不只是响应它可能内部在想在想的过程中还要调工具、查数据、生成中间结果。所以一个 Agent 实例能同时承接的触达数量绝对不能按普通服务的线程池思路来算。我们第一版直接把每个 Agent 的 HTTP 并发设成了 20结果线上出现了诡异的慢者恒慢现象。后来逐步压测发现一个模型推理进程并发一旦超过某个阈值单请求的响应时间会被拉长到接近无并发时的 3-5 倍。这是因为 GPU 或推理服务本身存在排队更高的并发并没有换来更高的吞吐反而让每个请求都处于半排队状态。所以 Agent-Reach 的做法是每个 Agent 注册时必须声明自己的建议并发上限调度器以此为基准再动态调整。在调度器内部我实现了一个带令牌桶 排队预算的并发控制器。每个进入调度的触达请求先判断目标 Agent 当前占用槽位是否还有余量。如果没有不是立刻拒绝而是看这个请求的排队预算还剩多少。预算由 QoS 决定比如urgent可以等 3 秒normal可以等 20 秒background几乎可以无限等。只有预算耗尽才会触发失败或降级。3.2 优先级抢占让重点任务先走在多 Agent 协作里经常会同时有两类任务混在一起一类是用户正在交互的在线任务一类是后台的批量梳理任务。如果不做优先级隔离在线任务可能会被批量任务堵在队列里用户感知就是机器人变笨了半天不回话。Agent-Reach 对队列做的是分级处理不是简单的先进先出。快速描述一下- 在线触达QoSurgent走单独的高优队列调度器每 50ms 检查一次。 - 准实时触达QoSnormal走普通队列额外带一个最长等待时间。 - 后台触达QoSbackground走低优队列允许被前台任务挤掉挤掉后支持延迟重放。这样设计的好处是即使后台批量任务很多在线任务也几乎不会被阻塞。代价是后台任务可能出现饥饿但 AI 协作里后台任务的时效性本来就是分钟级完全可以接受。3.3 超时与熔断触达失败要分清楚是不能触达还是暂时触达不了Agent 协作里最恼人的问题不是失败而是失败的归因。一个上游 Agent 发现下游调用超时了它不知道是下游真的卡死了还是网络抖动还是下游只是慢但并不需要重试。Agent-Reach 在超时模型里引入了两段式判定。第一段是网络触达超时就是 TCP 建立和 HTTP 响应头返回的时间一般设置比较短比如 3 秒。第二段是内容生成超时也就是 Agent 已经开始干活、接受到了请求但迟迟没返回内容这个通常会给到 60 秒甚至更长。这两种超时语义完全不同网络触达超时可以安全重试内容生成超时则要非常小心——如果 Agent 内部其实正在生成内容只是慢你重试反而会造成两份任务同时执行污染下游状态。所以我给出的经验是默认不自动重试内容生成超时的请求而是把它标记为慢触达交给专门的兜底处理。熔断逻辑也做过一次重构。初版是标准的三次失败开断路后来发现三个连环 Agent 串联协作时中间一个偶发超时熔断会迅速向上传导引发雪崩。所以现在 Agent-Reach 的熔断是区分方向的按下游 Agent ID 触达类型做维度细分到具体能力而不是整个 Agent 一刀切。比如一个 Agent 的文本总结能力熔断了它还能继续承接实体抽取的请求。4. 部署与配置里的细节那些文档里不写但要命的参数4.1 实例注册的优雅下线机制传统微服务的下线通常是一个通知实例从注册中心摘掉然后等请求排干。但 Agent 场景里摘掉这件事含义完全不同。因为一个 Agent 实例可能正处理着用户的 20 个多轮会话你不能把它从路由表摘掉之后就置之不理否则那 20 个会话就永远处于悬空状态。Agent-Reach 处理优雅下线的流程很长但很有必要实例主动置为 Drain 状态路由层不再派发新触达。实例等待当前活跃任务完成最多等一个最大切入等待时间我们设的是 30 秒。如果有任务超过等待时间实例把任务转交给会话接管器这是一个专门干恢复工作的组件负责把上下文和进度重新发给备用实例。所有会话任务清零后实例才真正注销。这个机制上线后我们升级 Agent 版本时的用户无感率提升了非常多之前是每次升级都会出现零星报错。4.2 心跳里的看不见的 Agent顽固实例识别还有一个比较隐蔽的坑是心跳超时阈值的设置。AI 推理 Agent 在长任务处理时比如阅读一个几百页的 PDF 并总结它可以持续几十秒不发送心跳因为这期间它确实在处理没有空闲。如果你用传统微服务的心跳超时比如 10 秒来判定它是否存活很可能会误杀。我们的做法是把心跳分成两类进程级心跳Agent 宿主进程活着但可能正忙和任务级心跳Agent 当前处理到了哪一步附带阶段信息。注册中心只看进程级心跳保活而任务的整体活性则由调度器通过任务级心跳与超时机制共同判断。这样既不会杀错实例又能在任务真的卡死时及时触发兜底。4.3 启动配置清单里我建议重点关注的三项如果你要自己搭建一个类似的 Agent 协作触达层我在配置上总结出三项最值得盯住的参数参数建议初始值调整依据队列最大长度500/Agent主要看下游 Agent 的峰值处理能力和最大容忍时间在线触达最长等待5s用户对在线交互的耐心阈值超过这个时间用户会开始怀疑系统坏了内容生成超时60s取决于模型推理速度和任务复杂度不建议一开始就设很长5. 压测数据与最容易忽略的三个性能陷阱5.1 压测总体数据为了说明 Agent-Reach 的调度开销我跑过一轮基准测试。场景是 100 个并发调用方持续 3 分钟目标是三个 Agent 实例模型推理是本地部署的单个 Agent 的最高并发上限设为 8。结果如下调度器本身从请求进入到路由决策完成不包含下游处理平均耗时 2.1 msP99 是 6.8 ms。包含网络协议开销和上下文引用的整体触达平均耗时 14.5 msP99 是 31.2 ms。在 8 并发限制下三个 Agent 实例单轮的处理能力大约能支撑 15 个并发会话任务而不出现排队堆积。不难看出调度器本身完全不是一个瓶颈瓶颈始终在模型推理和下游工具调用上。这也从侧面说明Agent-Reach 这类触达层的设计与优化重心不应该放在调度更快上而应该放在调度更准和失败更少上。5.2 陷阱一上下文引用的序列化开销有一次压测发现整体延迟比预期高很多仔细一查是上下文引用对象在传递时被重复序列化了。原因很常见Agent A 把上下文引用字符串当作普通 JSON 字段传给 Agent BB 又把它包进新的请求体再传给 Agent C。每一次传递这个字段都被json.dumps一次。压测里 500 次循环引用序列化耗时暴涨。解决办法是给上下文引用定义专门的消息类型在协议层做引用解析与传递优化而不是在业务代码里反复序列化。看似细节实际在高频触达场景下这一项能省掉 30% 以上的无效开销。5.3 陷阱二超时配置的连锁放大效应我们真实踩过的一个坑系统里有 A - B - C 的协作链路。A 调 B 超时设了 20 秒B 调 C 超时设了 10 秒。意思是B 最多花 10 秒等 C但 A 愿意花 20 秒等 B。这样看起来 A 能兜住 B 的慢但实际上 B 如果因为某种原因在等 C 的同时没有及时响应 A 的我在线吗探测A 就会判定 B 超时并重试结果 B 还在处理上一个请求新的请求就来了。链路越长这种超时叠加越容易引起混乱。我的经验是相邻两级的超时时间要呈递减关系上游要比下游大至少 50%并且一定要把探测超时和内容生成超时区分开否则探测超时会干扰正常的内容生成流程。5.4 陷阱三Agent 状态定的重试雪崩最后一个陷阱是在做故障恢复时发现的。如果某个 Agent 实例意外宕机它承载的会话上下文会瞬间丢失。Agent-Reach 的恢复策略是把这部分任务标记为待恢复重新路由到备用实例。结果恢复正常了又来了大量重放请求把整体负载瞬间拉高。后来改了策略恢复任务不一次性注入而是采用速率限制式重放比如每秒只恢复 20 个任务并且任务重放前先做幂等判断——如果该会话在备用实例上已经推进到后续阶段就跳过当前步骤。这么做之后再没出现过恢复导致的二次事故。6. 触达失败的归因与排查一次真实排障记录排障是任何系统绕不开的日常Agent 协作系统尤其复杂因为失败可能是模型侧的、网络侧的、调度侧的也可能是业务语义导致的。我记录一次真实的排查过程方便你在遇到类似问题时有个参照。现象用户触发一次双 Agent 协作任务A Agent负责意图识别表现正常B Agent负责执行工具调用迟迟不出结果。A 那边显示已成功触达 BB 那边日志显示从未收到 A 的请求。两边各执一词信息对不上。排查思路是这样的先从链路 ID 入手。在 Agent-Reach 里每个触达请求进入调度器时都会生成一个全局唯一的reach_id从入口到出口全链路携带。拿到reach_id后去查询调度器的决策日志发现请求确实被路由到了 B 的一个实例路由决策耗时 2ms没有任何异常标记。继续追网络层发现请求被成功发送到 B 实例所在的节点端口TCP 层已经确认但 B 的业务进程没有从内核缓冲里取出来处理。再看这个 B 实例的容器状态发现它当时的线程池已经完全占满所有线程都卡在等待一个因超时未释放的外部工具调用上。问题定位了B 的业务线程池被外部工具调用超时拖死了而不是被 Agent-Reach 拖死了。但 A 看到的却是触达成功没有响应。这次排障教会我一个重要经验在 AI 协作系统里把触达成功和业务成功分开记录。两者有完全不同的语义合在一起记账出了问题你根本查不清。Agent-Reach 在协议层做了这个区分触达成功记录在传输层业务成功记录在应用层两者通过reach_id关联而不是合并成一条日志。这条设计帮我省去了后面大量对线时间。7. 踩坑之后的取舍为什么现在才敢讲触达层值得单独做自己折腾过一轮之后我对 Agent-Reach 的价值有了更具体的体会。它不是万能的甚至一开始我也怀疑过不就是消息队列加路由吗非要做个新东西但经过几次线上事故和压测我的结论是触达层值得单独做因为它解决的是一类occupancy问题——不是简单的消息必达而是在正确的时间、用正确的语义把任务交给正确的 Agent并且在失败时给出可解释的归因。有几个取舍值得单独说说第一为什么不做成纯消息队列因为 Agent 协作不是一个发出去就不管的场景调用方通常需要知道下游的处理结果、进度、上下文变化。纯消息队列无法很好地表达带状态的请求-响应。第二为什么不做成一个通用的服务网格因为服务网格的优势在透明接入而 Agent 协作需要显式的路由策略和上下文传递透明接入反而会把关键信息藏起来。第三为什么要在注册中心上花那么大功夫因为 Agent 的注册信息不只是地址还是能力与负载的结构化描述这是决定调度质量的基础。这些取舍回头看很简单但当时都是花了大量时间在真实场景里试错才逐渐确定的。如果你也在做一个 Agent 编排系统我不建议直接照搬我们的配置但强烈建议你先回答清楚一个问题你的 Agent 之间到底是通过公共消息交流还是通过交换工作成果交流这个问题的答案决定了你的触达层该偏消息总线设计还是偏调度矩阵设计。Agent-Reach 选的是后者这也是它命中我们场景需求的核心原因。
返回列表