Agent-Reach:多AI代理触达与调度中间层的设计与实践

发布时间:2026/10/27 21:31:47
Agent-Reach:多AI代理触达与调度中间层的设计与实践 如果你也试过把两个以上的AI代理拼在一起干活你大概率碰到过这种场景代理A从用户问题里抽出了一个订单号需要让代理B去查数据库C去调用售后接口D负责汇总输出。问题是A怎么找到BB的接口地址写在哪调用失败了要不要重试B忙不过来会不会把A卡死C返回的结果格式和A预期不一致怎么办这些问题单靠“给每个代理写死对方地址”能撑过前期验证规模一上来就全崩。Agent-Reach就是我在这个背景下折腾出来的一套代理间触达与调度中间层这篇文章我会把它的设计思路、核心实现、实测数据和踩过的大坑完整记录下来。先交代一下背景我所在的团队做的是面向企业客户的客服智能化系统原来只有一个大模型代理在跑意图识别和问答。随着业务扩张这个“全能代理”开始暴露问题——单代理需要挂几十个工具每次请求的上下文几乎被撑爆响应延迟肉眼可见地上升而且任意一个工具出错都会影响整体可用性。于是我们决定拆成多个专职代理订单代理、售后代理、知识库代理、工单代理、报表代理等等每个代理只负责自己那一亩三分地。代理数量从1个变成十几个再到几十个之后原先“把所有逻辑写在一个进程里”的做法彻底行不通了。Agent-Reach这个项目就是为了解决“代理之间如何可靠地互相触达”而生的。1. 为什么需要Agent-Reach单代理很强代理之间很乱1.1 代理编排的三种“常规做法”及各自的极限在动手写Agent-Reach之前我把市面上常见的多代理协作方案挨个试了一遍大致可以归成三类。第一类是中心化调度器。也就是用一个总控Agent接收所有请求再由它判断该调用哪个子代理把结果辗转传递。这种做法在代理数量不超过五六个的时候很好用因为调度器的上下文能覆盖所有子代理的能力说明。但代理一多调度器自己就变成了新的瓶颈——它既要理解每个子代理的能力边界又要维护它们的状态上下文消耗比原来的单代理方案还夸张。第二类是直连调用也就是代理A的代码里直接写死代理B的HTTP地址用最原始的方式互相调接口。好处是快坏处是维护成本随代理数量呈指数级上升。我实际数过十几个代理两两之间如果都要打通连线条数能到几十条每次改一个代理的接口格式波及面大得吓人。这种方案纯粹是“能跑”和“能维护”之间的距离在拉大。第三类是消息队列广播。所有代理订阅同一个Topic谁听了谁知道。这个方案解耦是解耦了但带来了两个新问题一是消息无差别广播每个代理都要把每条消息过一遍上下文去判断“这跟我有关吗”浪费极其严重二是没有响应的概念A问B要数据B算完了怎么把结果准确送回给A简单场景可以靠correlation ID硬凑但一旦涉及多跳传递、条件分支这套机制基本不够看。1.2 Agent-Reach的核心设计前提分工自治、触达统一折腾完上面三种方案我得出的结论是多代理系统需要的是代理之间保持自治但触达路径统一。什么意思所谓自治就是每个代理内部可以有自己独立的模型配置、提示词体系、上下文管理策略和工具集我不需要也不应该把它们强行统一成一个模板。所谓触达统一则是代理之间的“寻址、路由、消息传递、状态同步、错误处理”这五件事不能由每个代理各自实现一套必须收敛到一个公共层来做。Agent-Reach的定位就是这个公共层。它不替代任何代理的决策能力也不约束代理内部怎么运行它只负责两件事第一让每个代理能够注册自己的能力和当前可用状态第二让任意代理发出的一条触达请求能够在合适的时机、以合适的方式送达合适的代理并拿到响应。这个设计原则听着简单落地的时候牵扯出的细节非常多。后面几节我会逐一拆开讲这里先把一个最容易犯的误区说掉很多人一听“公共层”就觉得要搞成ESB企业服务总线那样的大而全中间件把消息转换、协议适配、流程编排全部塞进去。我个人的强烈建议是——千万别这么做。Agent-Reach只做“触达”层面的活凡是涉及到业务语义转换、数据格式适配的东西都推给代理自己处理。公共层越薄越容易稳定。2. Agent-Reach的架构与协议设计2.1 控制面与数据面分离的总体思路Agent-Reach的架构沿用了网络设备里很经典的控制面/数据面分离思路。控制面负责“知道该往哪发”数据面负责“把消息实实在在传过去”。两者分开部署、分开扩容互不拖累。控制面核心是一个注册中心。每个代理启动时向注册中心发送自己的能力声明Capability Declaration内容包括代理ID、可处理的请求类型、当前负载等级、健康状态、回调地址等。注册中心把这些信息整理成路由表同时通过心跳机制持续探测每个代理的存活状态。这里有一个关键设计路由表不是由某个中心节点“下发”给代理而是代理在需要触达时实时查询。我最初做成下发模式结果每次路由表更新都要推送几十个代理消息风暴和处理延迟都上来了。改成查询模式之后只有真正发出触达请求的代理才会去拉取路由信息压力小了一个量级。数据面则是一套轻量级消息传输通道。每个代理通过Agent-Reach SDK与本地的Reach Node我习惯叫它触达节点简称RN建立长连接触达请求统一交给RN转发。RN之间可以点对点直连也可以走中转具体路径由控制面路由表决定。这里有个细节值得展开触达消息走的路径和路由查询走的路径是完全独立的两条链路。路由查询是HTTP同步请求触达消息则是异步长连接推送。为什么这么设计因为代理的触达消息往往带有明显的请求-响应特征但又不像传统RPC那样要求毫秒级返回——AI代理处理一条消息动辄几秒钟同步阻塞等待的代价极高。异步推送配合后续的回执机制才能把复杂的触发链式调用撑起来。2.2 能力注册与发现让代理学会“自我介绍”注册中心里存储的不是简单的“代理地址表”而是能力目录。每个代理在注册时必须用一段结构化描述声明自己能处理什么类型的请求。比如订单代理可以声明{ agent_id: agent-order-01, capabilities: [ { type: query_order, params_schema: { order_id: string, include_items: boolean }, return_schema: { status: string, order_info: object }, estimated_latency: 3s }, { type: cancel_order, params_schema: { order_id: string, reason: string }, danger_level: high, require_approval: true } ], load: 0.4, status: healthy }为什么必须用结构化schema而不用自然语言描述我一开始确实试过用自然语言写“我能查订单、能取消订单、能改收货地址”让发出的代理通过大模型去理解。效果非常不稳定同一个描述在上下文长度不同的时候被解析出来的结果差异很大。后来改成JSON Schema路由匹配用规则判断而不是模型推理准确率一下就到了99%以上。这也印证了前面说的公共层的逻辑要尽量用确定性算法把大模型的灵活性留给代理内部。2.3 触达消息的三类模式请求、广播、回执Agent-Reach的触达消息定义了三种模式覆盖了我在实际业务中遇到的所有协作场景。第一种是请求/响应模式。代理A向代理B发送一条带唯一消息ID的请求B处理后回复对应ID的结果。这是最基础的模式适合“帮我查一下订单状态”“把这份工单转给售后”这类一对一的协作。第二种是广播/聚合模式。代理A向满足某个能力条件的多个候选代理同时广播消息然后收集所有回复再做聚合。这个模式特别适合做方案对比比如用户问“最快能什么时候发货”发货代理可以同时触达库存代理、物流代理和订单代理把三家返回的时间拼在一起算最优解。需要注意广播模式对候选代理的筛选必须严格依赖能力目录不要搞成全量广播否则上下文和计算资源都会被白白吃掉。第三种是回执/回调模式。代理A发出触达请求后不等待立即返回而是告诉对方“算完了你回调我”。这种模式适合耗时长、需要多步处理的任务比如售后代理让报表代理生成一份复杂的分析报告报表代理可能要跑好几分钟不可能让触达通道一直保持占用。Agent-Reach里的回执是可以携带上下文的也就是说B在回调A时A可以要求附带上一次交互的会话片段避免A这边已经聊过好几轮之后B却只拿着一份孤立结果回来导致A上下文断层。3. 核心实现路由、队列与超时控制这部分是整个Agent-Reach最花功夫的地方因为我从一开始就认定理论设计再漂亮最终都要落实到具体的路由策略、队列策略和超时策略上这三样东西直接决定了系统跑起来顺不顺。3.1 路由表的构建能力匹配而非硬编码地址Agent-Reach的路由匹配分两层。第一层是类型匹配根据请求里的capability_type在能力目录里找到所有声明支持的代理。第二层是约束过滤过滤条件包括代理当前负载、健康状态、距离上次心跳的时间、以及调用方指定的优先级。约束过滤里面有一个很实用的手段给每个代理设置“最大并发触达数”。一个正在处理五条请求的忙碌代理按理说应该从路由候选中摘掉但实操中发现完全摘掉也不行——有些能力只有这个代理有摘掉了请求就直接失败。所以我改成了软摘除机制不是不让它接活而是给它降权。路由返回的候选列表会带上每个代理的负载优先级发起方可以自己决定是等待还是换一个。这个“降权而非摘除”的设计在代理数量少、能力高度专精的场景里非常救命。路由表还有个隐藏问题缓存一致性。每个触达节点都会本地缓存一份路由表来减少对注册中心的高频查询但代理上下线、负载变化是随时发生的缓存必然有滞后。我的做法是给每条缓存记录加一个TTL同时注册中心在代理状态发生跳变时主动推送一个增量更新信号。TTL设成5秒推送负责秒级响应两者配合实测下来路由信息的误差窗口能控制在几百毫秒以内。3.2 队列与背压别让慢代理拖垮全局代理之间的处理速度天然是不均衡的。知识库代理查一篇文档可能只要一两秒报表代理跑一份聚合分析可能要几十秒如果前者疯狂向后者的队列里塞触达消息后者的内存和上下文池很快就爆了。Agent-Reach在每个触达节点里维护了一个有界队列队列长度上限是可配置的默认1000条。当队列塞满时新到的触达请求不会直接丢而是进入“背压模式”——向发送方的RN返回一个明确的backpressure信号发送方收到这个信号后会暂缓发送同时降低自己的请求频率。这里有个容易踩坑的细节很多初版实现为了省事背压信号和正常响应用的是同一个返回码结果发送方傻傻分不清是“处理完了”还是“别发了”直接把语义搞乱。Agent-Reach里背压和正常响应在协议层就是两种不同的消息类型发送方一看到背压就立刻切换策略绝不能混在一起。队列的调度策略我最终选了带优先级的WQ加权队列改进型而不是最简单的FIFO。原因是客服场景里有些触达请求是用户在线等着答案的有些则是后台异步任务两者混在一个FIFO里会导致在线体验被后台任务堵死。我给在线类请求一个高优先级权重后台任务虽然也能进队列但出队速率会被限流。实测下来高优先级请求的P99延迟从6秒降到了2秒左右后台任务的总吞吐量只损失了不到10%。3.3 超时与重试参数一组实测下来的稳妥配置超时策略大概是这个项目里被问得最多、也最容易被拍脑袋决定的部分。我给出自己测过的一组数据供参考但强烈建议大家按自己业务调整别直接抄。首先是单跳触达超时从A的RN发出请求到B的RN确认接收这一段的超时我设的是3秒。注意这里只算“确认接收”不算“处理完成”因为B接收之后可能要花很久做内部推理那不属于触达层的职责。其次是处理完成超时A允许B在多长时间内返回结果我设的是60秒超过这个时间A会默认这次触达失败并自行处理。这个值定得比较宽因为报表类代理确实可能跑很久宁可在业务层容忍慢也不能在触达层过早掐断。然后是重试参数这里踩坑最深。早期我设的是“失败后重试3次间隔1秒、2秒、4秒”听着还算合理但在故障场景下这组参数会害死人。举例订单代理因为数据库连接池耗尽而拒绝新触达结果另外五个代理同时发现它失败每个代理按自己的节奏重试叠加起来等于每秒钟有几十条重试请求涌向订单代理本来还能慢慢恢复的连接池直接被击穿。后来我把重试策略改成了指数退避抖动首次重试等2秒之后按指数翻倍并在上下30%范围内随机抖动单次任务的总体重试上限是3次。同时加了一条关键约束——重试只允许在发送方RN这一层发生不允许业务代理无限重试。你可能会问为什么不让业务代理感知重试因为业务代理感知得越多每一层都可能自作主张重试重试风暴就是这么来的。把重试收敛到公共层之后行为可控多了故障期间的请求量立刻平稳下来。4. 我踩过的三个大坑及完整排查过程4.1 循环调用A找BB找CC又找回A这个坑是Agent-Reach上线后遇到的第一起线上事故印象极其深刻。起因是一个新加入的运营配置小助手代理它发现用户问“我的优惠券为什么不能用”时自己拿不准于是触达了订单代理。订单代理为了确认优惠券对应的订单状态又触达了运营配置代理——因为它认为优惠券规则是运营配置代理维护的。两个代理就这么你来我往每条消息都有合法的唯一ID每次触达都在超时时间内返回从单次看没有任何问题。但半小时后这两条代理通道之间的消息积压了好几万条系统资源直线飙升。排查链路是这样的我先在监控面板上看到这两个代理的RN队列长度异常随后在日志里抓到了它们互相触达的消息轨迹打印出来一看一个完整的循环链条清晰可见。当时第一反应是加“每个请求最大跳数”的限制但这个方案只能止损不能根治——因为循环触达的消息ID每次都是新的跳数限制只能限制链条长度没法阻止它们持续生成新消息。真正的修复是两步。第一步在每个代理的上下文里显式声明**“我当前正在处理的任务上下文”**触达请求必须携带这个上下文ID当接收方发现自己已经在上下文中处理过这条链路时直接返回一个“dependency loop detected”的信号不再继续往下传递。第二步更底层注册中心会根据每个代理过去5分钟内的行为计算“依赖熵”如果发现两个代理之间存在高频双向触达自动在控制面打出告警并降级其中一条触达链路的优先级。这套组合拳上线后再也没出现过循环风暴。4.2 状态不同步导致“幽灵触达”代理以为会话已结束第二个坑和代理的会话状态管理有关问题隐藏得很深花了将近两周才彻底定位清楚。现象是用户在网页端关闭了对话窗口但后台仍然每隔几分钟就有一条触达消息从订单代理发往知识库代理去查那些已经没人关心的文档。第一直觉是前端没有正确发送会话关闭信号但我翻遍了前端代码信号确实发了。接着怀疑是会话状态存到了Redis但没设置过期时间翻代码发现也做了。直到我静下心打印了订单代理的完整日志链路才发现问题出在代理内部的对话循环机制上订单代理处理用户问题时先自己推理了一轮然后触达知识库代理知识库返回之后订单代理又做了一轮推理并输出了回复。用户关闭窗口后订单代理的推理循环并没有被外部信号打断——它认为“这次对话还没完成我的回复用户还没看到”于是继续执行后续的触达计划。说白了触达请求的生命周期绑定错了对象。它应该跟随“用户会话”的生命周期而不是跟随“代理推理循环”的生命周期。修复方案是在Agent-Reach的SDK里增加了一个会话心跳检查接口任何代理在发出触达请求前必须先校验当前会话是否仍然活跃如果会话已关闭则直接中止后续动作并回收上下文资源。同时我在注册中心加了一份“会话活跃表”由业务前端在会话生命周期变化时推送状态。这个修复除了解决幽灵触达还意外地省了一大笔模型上下文费用——之前那些无效触达背后都跟着一轮又一轮的模型请求钱就是这么悄悄流走的。4.3 重试风暴一次瞬时故障引发的连锁雪崩第三个坑就是前面3.3节提到的重试风暴那次事故的完整排查过程值得再细说一遍。那天订单数据库发生了一次40秒的主从切换按理说业务影响不应该太大但Agent-Reach初版的重试策略立刻让事态升级。先是订单代理收到数据库连接失败它自己内部有一个重试机制尝试了3次。与此同时客服主代理、售后代理、库存代理都在自己的推理流程里触达了订单查询接口每个代理都各自重试每轮重试失败之后还触发下一轮的“重新分析”。最多的时候四个代理同时对订单代理发出每秒上百次的查询触达。我在监控面板上看到订单代理的请求量曲线像坐火箭一样往上冲第一时间把业务代理的入口全部切到了降级模式才把请求量按下来。事后复盘时我画了一条请求量时间线系统正常时峰值请求量是每秒30次左右故障期间最高冲到每秒470多次其中真正用户产生的请求只有不到5%剩下全是代理之间的自愈性重试。这次事故之后我把重试机制整个重构了一遍所有代理SDK内置的重试逻辑全部移除统一交给RN层处理。RN层设置每对触达关系独立的重试计数器而不是全局共享一个同时引入故障熔断——如果某个目标代理在10秒内的失败率超过50%RN会直接短路所有指向它的触达请求快速失败返回给上游而不是继续傻等或者傻重试。熔断状态每30秒尝试放行一次探测请求成功一次就慢慢恢复流量。这些机制组合起来之后再做了一次故障演练同样杀订单数据库40秒系统P99响应时间只从1.8秒升到了4.2秒没有出现任何雪崩。5. 权限与安全边界代理能触达的不等于可以执行多代理系统带来的一个隐性问题是触达范围的扩大直接等于攻击面的扩大。在一个单体应用里一个函数能不能调用另一个函数编译器替你管着在多代理系统里每次触达都是一次跨信任边界的调用必须有明确的权限控制。5.1 信任分级与令牌传递Agent-Reach把代理之间的信任关系分成三个级别。L1级是核心代理群包括订单、支付这些涉及到钱的代理它们之间的触达必须使用双向mTLS认证同时每次触达请求都要附带由注册中心签发的短期令牌。L2级是常规业务代理触达时只需要单向认证加令牌。L3级是外部插件代理它们在沙箱环境中运行只允许通过受限的API接口对外触达且所有触达内容都要过内容审计。令牌传递的设计我踩过一次坑最初为了省事令牌的有效期设成了24小时结果某天一个代理的日志泄露到了测试环境攻击者直接拿着有效令牌在内部网络里横向触达了好几个代理。后来我把令牌有效期缩到15分钟同时规定令牌只能由发起触达的代理持有禁止目标代理将令牌转发给第三方。要做级联触达时目标代理必须重新向注册中心申请新的令牌而不是复用上游传来的令牌。这样每个跳转链路都是独立的凭证链单一环泄露不会导致整条链失控。5.2 高危动作的双人复核闸门能力目录里我在声明时加了一个danger_level字段就是为了在触达层做高危操作的拦截。比如“取消订单”“发起退款”“删除工单”这些操作不允许因为某个代理被另一个代理触达就能直接执行。Agent-Reach里实现了一个复核闸门机制当目标代理接收到danger_levelhigh的触达请求时它不会直接执行操作而是向操作审批服务发起一个复核请求。审批服务会在企业IM群里拉起一条审批卡片需要指定的业务负责人点击确认后操作才会继续执行。这个看起来“不AI”的设计实际运行中反而给整个系统带来了更多信任度——业务方终于不再把代理之间的自动触达看成黑盒怪物了。有一件印象很深的事上线复核闸门的第一周某天下午突然有一条“批量发送优惠券”的高危触达请求发起复核审批群里一个主管点了同意事后发现其实是运营误操作触发了一个错误的消息链路。因为有大模型参与的推理过程本身可能有幻觉这种闸门给了人类一个纠错窗口成本和收益相比完全划算。6. 性能实测与调优记录最后用实测数据收个尾这部分直接关系到Agent-Reach到底能不能扛住真实业务流量。6.1 端到端延迟与吞吐量压测压测环境是20个代理节点分布在同一地域的三台服务器上每台服务器跑了一个RN节点和6-7个代理进程。压测场景模拟了客服系统的混合负载60%的简单触达单跳查询30%的链式触达两跳A→B→C10%的广播聚合触达。结果是场景QPSP50延迟P95延迟P99延迟单跳触达50045ms90ms180ms两跳链式触达200220ms500ms920ms广播聚合5个目标501.2s2.4s3.6s注意这里测的只是触达层的延迟不包含代理内部调用大模型的时间。实际生产环境中跨代理调用大模型会把P95拉到十几秒这时候触达层的几百毫秒延迟基本被淹没但它决定了系统的稳定上限——如果触达层都在抖后面的大模型推理就不可能稳定。我个人的判断标准是触达层P95延迟必须控制在代理自身推理时间的十分之一以内这层才算是合格的。6.2 三个最有效的调优点调优过程中有三个改动效果特别明显给后面想做类似系统的人一个参照。第一个是把路由查询从同步阻塞改成异步预取。最初代理发出触达请求时RN会先去注册中心查路由查完再转发整个过程是串行的。后来改成RN本地缓存路由表并周期性预取绝大多数触达请求不需要再实时访问注册中心单跳延迟直接降了60%以上。第二个是使用连接池复用来替代每条消息新建连接。这个是很朴素但收益巨大的一步。RN之间的长连接保持Time wait状态处理得好不好直接影响系统的并发上限。我专门看了一下连接复用之前的TIME_WAIT套接字数量在高峰期能到3000多复用之后降到几十。第三个是压缩广播聚合的冗余回执。广播模式很容易出现5个目标代理都回了内容但最终聚合时只用到2-3个结果的情况。我从两个方向优化一是在广播请求里附加“已有结果摘要”其他目标代理看到摘要后可以决定是否还要认真计算二是聚合阶段做了流式处理先返回的结果先参与判定如果前两个结果已经能形成明确答案就不再等待剩余回执。这两个优化让广播聚合的平均完成时间缩短了大约1倍。Agent-Reach这个项目的核心收获说来说去就是一句话多代理协作的复杂度不以代理数量线性增长而是以触达关系的数量指数级增长。与其把精力花在让每个代理变得更聪明上不如先确保代理之间的每一条通路都可靠、可控、可观测。我现在已经不太担心某个代理的推理能力不够了反而更担心哪条触达链路在无人注意的角落里悄悄累积风险。这个项目后续我会接着做故障注入演练和更精细的链路追踪等有新的进展再写文章分享。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询