YAOTU INSIGHTS

多智能体系统可靠触达:Agent-Reach 的注册、路由与回执设计

多智能体系统可靠触达:Agent-Reach 的注册、路由与回执设计
聊一个我最近大半年一直在打磨的内部项目Agent-Reach。名字听着抽象其实核心就一句话——在多智能体系统里把“任务能不能送达到位、结果能不能拿得回来”这件事做成一套可量化、可治理的流程。最早触发这个项目是因为我负责的一套自动化系统里接了六个不同类型的智能体有负责日志分析的有负责告警处理的也有对外对接工单的。表面上每个智能体都有 API测试时也都能单独跑通。可真到并行任务一多问题就全冒出来了消息发出去没有回执任务执行到一半进程重启重试几次后出现重复动作下游 Agent 被瞬时流量冲垮。那段时间我每天都在跟“触达”搏斗而不是跟“智能”搏斗。所以我决定把触达问题单独拿出来做一套基础设施这就是 Agent-Reach。它不负责教 Agent 怎么思考也不负责具体的业务编排只专注一件事把一个任务可靠地投递给正确的 Agent并且把处理结果完整地拿回来。无论你是做智能客服、自动化运维还是做营销触达只要你的系统里有多个 Agent 在协作Agent-Reach 的思路都能直接迁移。1. Agent-Reach到底解决什么问题1.1 智能体触达的“最后一公里”困境很多团队在做多智能体系统时习惯把精力放在模型选型、提示词工程和业务编排上却低估了“消息怎么可靠地送到 Agent 手里”这个基础问题。我见过太多类似场景调度中心把任务塞进 HTTP 请求等待返回结果 Agent 那边业务处理很重几秒钟都没有响应HTTP 客户端先超时了调用方以为失败了于是重试Agent 其实已经收到并开始执行重试又带进来一条一模一样的任务。最后数据库里同一个工单被创建了两遍线上投诉直接找到研发。这就是典型的触达链路不可控。Agent-Reach 要解决的是这个链条上的三个核心问题任务有没有送达到目标 AgentAgent 有没有真正接受并开始处理Agent 处理结束后结果能不能可靠地送回给调用方。这三个问题如果单靠每个 Agent 自己实现最后一定各自为政。有的用 Redis 队列有的用 HTTP 回调有的直接丢进日志里算“成功”。出问题的时候根本没有一个统一的地方能说清楚“这条消息现在到底在哪”。Agent-Reach 就是把这一层收敛成公共基础设施。换个生活化的类比这就像外卖平台。你下单后平台不关心商家怎么做菜但它必须管住几个节点商家有没有接单、骑手有没有取餐、餐有没有送到。Agent-Reach 在智能体系统里扮演的正是这个“履约平台”的角色。1.2 从“通知触达”到“协作触达”边界要划清楚做这个项目之前我强烈建议先把概念边界划清楚你需要的到底是一个通知系统还是一个协作触达系统。通知触达是 fire-and-forget发出去就算结束。短信、Push、邮件都属于这类。它们的特点是发送方不关心接收方是否真的“处理”只要协议上送达任务就结束了。这类系统简单但无法支撑多智能体协作。协作触达则完全不同。它要求全链路确认消息被接收、被受理、被执行、被执行完并拿到结果。每一步都要有明确的回执和记录。Agent 之间不只是“知会一声”而是真正交付一项任务交付的结果直接影响下一步决策。Agent-Reach 定位在后者但它同时也兼容前者。比如某些场景只要求把事件广播给多个 Agent不需要它们回传结果那 Agent-Reach 可以走 one-way 模式不去等待业务完成回执。这个边界一旦划清楚后面的设计才不会跑偏。因为如果你把“通知”和“协作用触达”混在一起很容易做出一个什么都像、什么都不精的系统。我的经验是先把协作触达作为主航道因为它是多智能体系统里最难、也最值得做扎实的部分。2. Agent-Reach 的核心设计注册、路由与回执2.1 接入层先把 Agent 的行为描述清楚Agent-Reach 的第一层是接入层也就是 Agent 注册。任何一个 Agent 想接入系统必须先到注册中心登记自己的元信息和能力否则调度器根本不知道这个 Agent 能做什么、怎么调用。我最初犯过一个错误以为注册中心只要保存一个地址就够了。结果 Agent 一多地址是会变的容器重启后 IP 也变。而且不同 Agent 的能力差异极大有的能处理日志分析有的只能发通知有的必须串行处理任务。如果不对这些信息建模路由就是盲猜。所以 Agent-Reach 的注册表至少包含这些字段agent_id全局唯一标识capabilities能力标签列表比如log_analysis、incident_handlerprotocolhttp、grpc、mq 等endpoint具体调用地址concurrency该 Agent 支持的最大并发健康检查方式主动探活还是被动心跳。我通常用一段 YAML 来定义一个 Agent例如agent: id: analyzer-01 capabilities: [log_analysis] protocol: http endpoint: http://agent-analyzer:8081/reach concurrency: 20 retry_policy: max_attempts: 3 backoff: exponential注册表同时维护每个 Agent 的动态状态最近心跳时间、当前在途任务数、最近 ack 成功率。静态信息和动态信息分开存储这样既方便人工配置又能支持运行时的自动调度。有一点很重要不要把业务逻辑塞进注册表。比如“某个 Agent 只在晚上处理任务”这种逻辑应该放在路由策略里而不是写在注册表配置里。否则每次改业务策略都要动元数据容易引发连锁事故。2.2 路由策略可达性优先而不是负载优先传统负载均衡最常用的是轮询、随机或者最少连接数。这些策略在 Agent 状态相对稳定时没问题但放到多智能体环境下Agent 可能会出现短时间的卡顿、半死不活、或者恢复后还在慢启动阶段。这时候如果只看 p99 延迟或者连接数去路由照样把任务发给一个刚好在 Full GC 的 Agent。Agent-Reach 引入了“可达性评分”作为路由的首选依据。每个 Agent 会得到一个动态分数计算公式可以设计成score w1 * health w2 * (1 - current_load / declared_concurrency) w3 * recent_ack_rate其中health 来自最近的心跳或探活结果current_load 是在途任务数declared_concurrency 是 Agent 注册时声明的最大并发recent_ack_rate 是最近一个时间窗口内返回 ack 的成功比例w1、w2、w3 是权重。举个例子假设 w10.5、w20.3、w30.2一个 Agent 健康分值 1.0当前在途 10 个任务声明并发 20最近 ack 率 0.9那么它的得分是0.5 * 1.0 0.3 * (1 - 10/20) 0.2 * 0.9 0.5 0.15 0.18 0.83另一个 Agent 健康分值 1.0当前在途 2 个声明并发 20但最近 ack 率只有 0.5得分是0.5 0.3 * 0.9 0.2 * 0.5 0.5 0.27 0.1 0.87虽然第二个 Agent 的空闲度更高但因为 ack 率低得分并没有压倒性优势。如果 ack 率继续下降它的得分会被快速反超调度器就自动把流量引到更可靠的 Agent 上。做这套路由的时候我踩过一个大坑不能只在发生错误的时候立刻摘除 Agent。因为瞬时抖动和真正故障之间有一个灰度带太敏感会导致流量反复抖动。建议加入“连续 N 次失败才降权”和“冷却时间”机制。2.3 回执协议三个 ack 撑起触达的可观测性Agent-Reach 最核心的抽象是三层回执模型。很多人理解的“回执”就是最终调用成功但 Agent-Reach 把回执拆成三个层级回执类型含义触发时机received消息已被 Agent 进程接收到网络层和序列化层成功后立即触发acceptedAgent 已受理准备开始业务处理业务逻辑入口处鉴权、校验通过后触发doneAgent 已完成处理带上结果数据业务逻辑成功或失败出口处received 解决的是“任务到底有没有到对方手里”的问题。很多时候调用方以为超时是等于没送到但实际上 Agent 已经收到只是卡在处理流程里。有了 received 回执就可以把网络问题与业务处理问题区分开。accepted 解决的是“对方愿不愿意做、能不能做”的问题。比如 Agent 正在降级保护它可以在收到消息后直接返回 rejected表示“现在无法接受”。调度器看到 rejected 会立刻换另一个 Agent而不是傻等超时。done 则是最终交付它会携带业务结果例如工单号、执行状态、产物地址。只有 done 到达后Agent-Reach 才会从在途任务中把这条消息移除。用外卖类比就是received 等于骑手拿到了订单accepted 等于商家说可以做done 等于餐已经送到你手上。三者缺一个说明链路里一定有值得查的地方。这套回执模型带来一个很直接的好处监控粒度变细了。以前只能看到“成功率”一个指标现在可以分别看“送达率”“受理率”“完成率”。到底是网络丢包、Agent 拒单还是业务执行慢一眼就能定位。3. 实操过程队列、超时、重试和幂等怎么落地3.1 队列模型不要用同步 RPC 承载所有任务一开始我以为 Agent-Reach 只需要做一层封装底层直接调 Agent 的 HTTP API 就行。但实际跑起来发现同步 RPC 在任务一多、Agent 处理一慢的情况下调用方会因为等待线程池耗尽而雪崩。所以 Agent-Reach 在触达层前面加了一个持久化队列。所有任务先入队再由派发器异步投递。这一步看起来很基础但解决了三个大问题调用方不需要长时间阻塞等待响应速度变快任务落盘后即使派发器重启也不会丢可以对流量做削峰填谷防止冲垮 Agent。队列选型上我更推荐带 ack 机制、支持消费者组和死信队列的消息中间件。比如 RabbitMQ、Pulsar或者 Redis Stream 在低并发场景下也够用。反而直接拿 MySQL 当队列容易出性能问题不建议硬扛高吞吐。队列里每个消息都要带上全局唯一的 message_id这是后面做幂等和链路追踪的基础。另外消费端处理完业务逻辑后再 ack避免“消息刚被拉出来进程一崩就丢”的情况。3.2 生命周期和状态机消息一定要有明确身份Agent-Reach 中每条消息从产生到结束会经历一串状态。我把状态机定义成下面这张表状态含义触发条件PENDING任务已入队等待派发调用方投递成功后DELIVERED消息已投递给 Agent派发器把消息发送到 Agent 后ACCEPTEDAgent 已受理Agent 返回 accepted 回执EXECUTINGAgent 正在处理业务进入业务处理入口DONE已成功完成Agent 返回 done 回执FAILED业务失败进入重试Agent 返回错误DLQ超过最大重试次数进入死信重试耗尽消息状态变化时Agent-Reach 会记录触发时间、Agent id、回执内容并且全程携带同一个 trace_id。有了这套状态机排查问题时不是“到处翻日志碰运气”而是直接把一条消息的完整旅程拉出来看。我强烈建议把这个状态机做成只追加的事件日志不要只保存当前状态。因为“当前状态”只能告诉你最后到哪了追加上下文才能告诉你中间发生了什么、花了多久、在哪一步最慢。这相当于给每条消息写了传记。3.3 重试策略与退避参数既要快又不能添乱失败重试是触达系统里最容易出问题的地方。不重试任务丢了用户感知很糟糕重试太勤又可能把已经卡住的 Agent 打得更死。Agent-Reach 默认使用指数退避加随机抖动。基础公式是sleep min(base * 2^n jitter, max_interval)其中 n 是当前重试次数jitter 是一个随机增量。单纯指数退避有个经典问题如果大量消息同时失败它们会按相同的退避序列在同一时刻重试造成“惊群效应”。加 jitter 就是为了把重试时间打散。参数怎么定我一般会根据业务可接受的最晚完成时间来倒推。假设一个任务要求 30 秒内必须出结果单次调用超时设置 3 秒基础退避 1 秒那重试次数大概解这个公式3 * N (2^N - 1) 30N4 时总耗时约12 15 27秒可以N5 时总耗时约15 31 46秒超了。所以重试次数设为 4 最合适。还要设计最大重试次数防止无限重试拖垮系统。重试耗尽后消息进死信队列同时触发告警。这个 DLQ 非常重要千万别只做重试不做死信。没有死信的话一批任务在最深处彻底消失你甚至不知道它们没了。3.4 幂等控制至少一次投递下的安全网消息中间件在最坏情况下是“至少一次投递”即消费者可能收到重复消息。Agent-Reach 不奢望底层帮我们消灭重复而是在业务接入层强制要求幂等控制。最常用的方案是幂等表加唯一索引。每条消息都带一个业务幂等键 biz_keyAgent 在处理前先尝试往幂等表插入一条记录键就是 biz_key。如果插入成功说明这条消息是第一次来正常处理如果插入冲突说明之前已经处理过直接返回前一次的结果。这里有个细节必须注意不能只在第一次处理时做幂等在最后更新业务状态和返回结果时也要做。我遇到过 Agent 处理了两次两次都通过了前置幂等判断因为第一次处理最终提交状态时失败了没有留下终态记录第二次拿到的还是初始状态于是又跑了一遍。正确的做法是让幂等记录和业务状态更新在同一个事务里完成或者给业务表增加版本号用乐观锁防止重复覆盖。另外不要让业务方自己实现幂等。Agent-Reach 的 SDK 里会内置一个本地幂等处理器同时支持接入外部 Redis 或数据库。如果每个 Agent 各自写一套迟早有人会漏掉这个逻辑。4. 常见问题与排查实录4.1 消息发出却“石沉大海”问题可能不在网络我在真实故障中最常见的一个现象是Agent-Reach 后台显示消息已经 DELIVERED但 Agent 始终没有返回 received。第一反应肯定是查网络但查来查去网络正常真正原因往往藏在细节里。有一次是 Agent 用的 HTTP 框架在反序列化请求时抛了异常异常发生在框架层业务代码根本不知道有这条消息。由于框架没有把异常转换成一个合法的 received 响应Agent-Reach 一直等不到回执最后超时重试。还有一次是 Agent 的线程池配置有问题。消息进来后放入线程池队列但核心线程全被阻塞队列满了后新的请求被丢弃客户端只看到超时。这类问题在日志里不一定有 trace因为请求根本没进业务层。所以排查建议是先看 Agent 的接入层日志它有没有收到请求如果收到了再看线程池和反序列化如果确实收到了但没回执检查业务代码里有没有把 ack 放在 try 块之后一行一旦前面抛异常 ack 就不会执行。最佳实践是把 received 回执放在框架拦截器里和业务代码解耦。4.2 重试导致重复执行先查幂等表再查路由还有一次生产事故用户反馈同一个告警工单被创建了两次。当时我们第一反应是“重试投递造成重复”但看了消息日志发现两条消息的 message_id 不一样说明这不是简单的消息重复而是同一个业务请求被重复发起了。继续追才发现是调用方在 Agent-Reach 的 SDK 里设置了超时但 SDK 内部没有在超时后取消原请求也没有把同一个 biz_key 传透下来。后面的重试相当于一次全新请求Agent 自然认为是两个不同工单。这个教训让我意识到重试必须携带业务幂等键。Agent-Reach 的调用方在首次投递时就要生成一个 biz_key所有重试都必须复用同一个 biz_key。Agent 侧的幂等表也要对相同 biz_key 返回首次执行结果而不是重新执行。如果你发现幂等表没生效优先检查这两点唯一索引是否真的建上去了查询和插入是否在同一个事务里。很多人以为只要代码里先 query 再判断不存在就 insert 是安全的但并发情况下两个请求同时 query 到不存在又同时 insert假如唯一索引没建成功重复数据就进去了。4.3 Agent 重启后的流量风暴限流必须前置Agent 重启恢复后队列里可能会积压几十万条消息。如果所有 worker 同时开始消费下游数据库和 API 会被瞬间打满。我们第一次遇到时数据库直接 CPU 100%整个业务链路由此拖垮。Agent-Reach 的解决办法是双重限流。第一层在服务端每个 Agent 的消费速率按声明并发控制每次批量拉取不超过concurrency / 10条例如并发 20一次最多拉 2 条。第二层在 Agent 侧SDK 内置令牌桶令牌容量等于配置的并发每秒补充速率按队列积压量动态调整。更有效的一个策略是“慢启动放量”。Agent 重启后前 30 秒只放 5% 的流量观察错误率和延迟如果稳定再逐步提高放量比例。这样一来即使 Agent 刚注册完还没完全热起来也不会被积压消息淹死。还有一个容易忽略的点重试退避的 jitter 一定要设。如果不设一堆消息在同一个时间点重试就会出现周期性脉冲。设了 jitter 后重试流量在时间上摊开整体会平滑很多。5. 落地场景与扩展形态5.1 在智能客服和工单协作里的用法客服系统是我最早验证 Agent-Reach 的场景。一个用户请求进线后可能先由意图识别 Agent 判断分类再由业务处理 Agent 查询订单最后由生成 Agent 起草回复。每一步都是一个 Agent 任务任何一个 Agent 挂了工单就会卡住。用 Agent-Reach 之后每个工单从创建开始就是一个触达任务。派发给意图识别 Agent它返回 accepted 表示受理返回 done 时附带分类结果。调度器再用这个结果触发下一个 Agent。如果某个 Agent 连续重试失败工单会自动转人工处理池不会丢。这里最有价值的是“半路失败”可以被发现。以前工单在某个 Agent 里阻塞只能等用户催单。现在 Agent-Reach 直接监控到 EXECUTING 状态超时主动把工单升级大大降低了用户无感知的等待。5.2 在自动化运维告警触达里的用法运维场景比客服更看重“必须被执行”。以前告警系统发出一条通知发完就结束了有没有人处理完全不可控。Agent-Reach 把告警恢复动作也纳入触达管理告警触发后由恢复 Agent 去执行检查或自愈脚本完成后必须回传 done。如果恢复 Agent 执行失败Agent-Reach 会根据策略把任务升级到更高一级的 on-call 人员触达渠道同时也保留完整的升级轨迹。这样审计“某个告警到底处理到哪一步”变得非常容易。在运维场景中我对 Agent-Reach 的推荐是优先接那些有明确自动化恢复动作的 Agent不要一开始就接全量告警。让触达链路在实际恢复任务中先跑稳再扩大范围。5.3 下一步从内部项目到通用化Agent-Reach 目前还是一件内部武器但我已经在考虑把它抽成一个通用框架对外提供一套标准 API 和 SDK。未来它可以支持更多传输协议比如 WebSocket、gRPC也希望能做一个可视化拓扑让你一眼看到不同 Agent 之间的触达关系。从业务维度看Agent-Reach 还可以和流程编排引擎结合让触达不再只是单个任务的投递而是一整个工作流在不同 Agent 之间可靠流转。这一步做起来会比较复杂但方向是清晰的。如果你也在做多智能体相关的东西我的建议是别一上来就堆模型能力。先把触达这件事做扎实。我在实际搭建 Agent-Reach 时最大的体会是很多东西看着简单真正跑起来才发现“已送达”和“已被人看见并动手处理”是两个完全不同的承诺。Agent-Reach 把这件事拆成了可量化、可测试的流程你可以从最小闭环开始一步步把可靠性做起来。最后再分享一个小技巧刚开始只接两个 Agent不要接太多因为触达链路的问题往往在规模上来之后才暴露。等重试、幂等、可视化都跑顺了再扩大接入面会少踩很多坑。