多智能体协作中的可靠触达层:Agent-Reach设计实践
这两年我一直在折腾多智能体协作最大的感受是让一个Agent学会思考不难难的是让它在几十个Agent和一堆外部系统之间准确地把任务递到。Agent-Reach这个名字最早是我们内部给这套触达层起的代号后来用着用着就变成了正式项目名。它解决的不是这个Agent能干什么而是这个Agent怎么触达别人——能力路由、握手协商、超时重试、熔断回退一套链路全包。如果你也在做多智能体系统手底下的Agent数量超过五个或者已经开始让Agent去调用第三方服务、去操作工单系统、去触发自动化流程那么这篇文章应该能帮上忙。我会把Agent-Reach的设计思路、核心链路、部署时真正容易翻车的细节全部摊开讲。1. 我为什么要单独做一层触达1.1 智能体的调用不是普通的HTTP调用很多人一开始做多智能体都是把每个Agent包装成一个HTTP接口然后A调用B、B调用C用FastAPI写几个路由就完事了。早期确实够用Agent数量一多问题立刻暴露。普通HTTP调用的核心假设是对方一定会快速响应而且响应结果可信——但智能体不满足这个假设。一个Agent可能思考好几秒才返回可能返回的是一个任务已受理的异步状态而不是最终结果更麻烦的是它可能因为上下文太长、模型超时、外部工具抖动直接给你一个我做不到的响应。如果你用调普通API的方式去调Agent重试逻辑、超时设置、鉴权方式全都得重写一遍。我见过最惨烈的场景某个Agent调另一个Agent做数据分析后者又去调一个Python执行环境三层嵌套每一层都自己写超时和重试结果某个晚上底层执行环境抖了一下三层一起重试等于把同一个请求放大了九倍直接把下游系统打挂了。这就是典型的每个Agent都会走路但没有统一的路网。1.2 Agent-Reach 的定位管够得着与接得住Agent-Reach的定位非常纯粹它是一个位于所有Agent之上的触达层。任何Agent想要调用另一个Agent、任何外部任务想要进入Agent体系都先走Agent-Reach。它只管三件事第一这次触达该发给谁。不是简单地靠名字匹配而是根据能力描述、负载情况、区域归属去路由保证同一类任务在多个Agent之间尽量均衡分布。第二这次触达怎么安全抵达。建立连接的时候先做握手双方交换能力清单、当前负载、期望响应时间避免一个已经快被挤爆的Agent还在被疯狂派活。第三失败之后怎么兜底。超时没有回退、重试导致雪崩、熔断之后流量往哪里切这些都在触达层统一处理不让业务逻辑里散落着一堆if重试。我习惯用一个类比如果把每个Agent当成一个门店Agent-Reach就是中间那套物流中转和调度系统。门店负责把货摆好但货怎么到你手上、路上坏掉怎么办、司机跑错地方怎么纠正统统是物流系统的事。1.3 与消息队列、RPC框架的分工边界你可能会问这不就是消息队列加RPC框架的事吗为什么还要自己造一个Agent-Reach确实Kafka、RabbitMQ、gRPC都能做一部分事情但有一个关键差异消息队列和RPC框架处理的是确定性的消息而Agent-Reach面对的是一群行为不稳定、结果需要二次确认的智能体。举个例子你用Raft或gRPC做服务发现和负载均衡那是基于实例的IP和端口但Agent的实例可能是一个动态生成的会话同一个Agent在不同会话里能力范围还不一样。RPC框架不知道这个Agent当前能不能处理中文文档总结这种语义级别的信息Agent-Reach恰恰要管理这一层。用一张表说明分工能力点消息队列RPC框架Agent-Reach消息投递是部分是能力语义路由否否是异步状态跟踪弱弱强握手协商负载/能力否否是业务级回退否否是我并不是说有了Agent-Reach就不需要MQ和RPC了。底层传输、持久化、服务发现这些脏活累活Agent-Reach可以复用现成的基础设施它只在这之上增加一层Agent语义的触达管理。2. Agent-Reach 的核心构成三张表加两条链路2.1 能力路由表不写死地址写能力声明Agent-Reach最核心的部分是能力路由表。它不保存Agent A的地址是http://xxx:8001而是保存Agent A具备什么能力接受什么格式的输入期望多长的响应时间。设计成能力声明的好处是Agent的物理地址变了路由表不需要变Agent的能力升级了只需要更新它的声明触达层会自动把新的能力纳入路由范围不再有改地址就要重启服务的痛。一份能力声明大概是这样的结构{ agent_id: agent-doc-summarizer, endpoint: grpc://10.20.1.11:9001, capabilities: [ { name: summarize_document, input_schema: document-reference, max_input_tokens: 8000, max_response_seconds: 30, concurrency_limit: 5 } ], load_report: { current_inflight: 2, queue_depth: 1, healthy: true } }这个声明在满足以下条件时由Agent主动上报启动时、能力变化时、每隔30秒。路由表不会猜测Agent状态全部以Agent自己上报的为准避免触达层单方面判断错误把请求发给一个其实已经不可用的Agent。2.2 握手协议建立连接时先交换能力与负载Agent-Reach不是简单地把请求丢给目标Agent就完事。它在正式投递前有一个握手阶段类似两个人第一次见面先对暗号确认你会这个我现在有空。握手过程很轻量一次请求里有三个字段{ handshake: { protocol_version: v1, task_type: summarize_document, payload_estimate_bytes: 2048, deadline_seconds: 30, require_confirmation: true } }目标Agent收到握手请求后会检查自己的当前负载。如果负载正常返回READY并带上一个确认号如果正在处理大量任务返回BUSY并给出建议等待时间如果彻底不可用返回REJECT。Agent-Reach拿到BUSY之后不会傻等而是根据负载策略决定是排队等待、换一个同类Agent还是直接降级。这个设计帮我挡掉了一个很常见的坑以前Agent调用Agent时目标Agent已经忙到要爆炸但调用方完全不知道只是傻傻地等超时。有了握手协议之后调度决策在投递之前就能做出来。2.3 回退策略超时、熔断、降级的外部表现Agent-Reach里的回退策略不是简单的重试三次而是把失败场景拆开处理场景表现处理方式目标超时超过握手时约定的deadline未返回记录超时尝试同一Agent一次仍超时则切换同类Agent目标重启握手阶段返回REJECT直接路由到其他同类Agent给源Agent发状态通知连续失败同一个Agent在5分钟内失败超过8次触发熔断10分钟内不再分配新任务无同类可换所有候选Agent都不可用降级为队列暂存或返回业务方可感知的忙信号回退策略全部在触达层执行业务方的Agent里不再需要自己写重试。这样做的另一个好处是重试行为可以被统一观测——哪类任务重试率高、哪个Agent是经常拖后腿的那个触达层看一眼监控就清清楚楚。3. 一次触达请求从进入到落地的完整过程3.1 从任务进入触达层到路由命中假设我现在有一个总结第三季度销售报告的任务要派发给文档类Agent。这个任务先进入Agent-Reach的入口触达层会做三件事。首先做能力解析从任务描述中提取出这是一个文档总结任务匹配到summarize_document能力接着做路由过滤把所有声明了该能力且目前健康的Agent列出来最后做负载排序根据每个Agent的current_inflight和queue_depth计算一个综合评分负载低的优先。这里有一个小细节路由过滤时会同时检查Agent是否处于熔断状态列表里。如果一个Agent刚经历连续失败即使它现在上报自己healthy触达层也会在短时间内绕过它直到熔断时间窗口结束。3.2 握手通过后的状态同步路由命中后Agent-Reach不会直接把任务丢给Agent就结束。它会标记这次触达为一个pending状态的任务并开启一个状态跟踪协程持续监听目标Agent回传的状态更新。目标Agent可能返回的状态有四种PENDING已收到排队中、RUNNING正在处理、SUCCEEDED已完成、FAILED失败。Agent-Reach会把这些状态实时同步给发起方并且保留完整的状态变更历史。我在Agent-Reach里给每个触达请求都挂了一个全局唯一的touch_id。发起方拿着这个ID就能随时追问我的任务到底到哪一步了不需要自己维护和下游的多次交互记录。这个设计在多Agent协作里极其重要因为一次用户问题往往被拆成七八个子任务没有统一的状态中心最后根本拼不出来整个执行链路。3.3 失败后的三次重试加一次回退失败处理逻辑我把它写得非常死板因为死板意味着可控。默认策略如下第一次失败重试同一个Agent。如果失败原因是目标在处理途中宕机或超时同一Agent重试一次还有机会因为它恢复之后缓存的中间状态可能还在。第二次失败换一个同类Agent。如果第一个Agent确实有问题第二个Agent从头开始处理虽然可能重复跑一遍但至少比无限等下去强。第三次失败尝试降级方案。如果任务本身允许离线处理就放入队列等整个集群恢复正常如果允许简化处理就调用一个快速版本比如只统计数字不做语义分析都不允许就返回失败并附上失败原因统计让发起方决定下一步。这个策略看上去简单但实际跑起来非常有用。早期我试过让每个Agent自己重试结果重试次数五花八门有的重试五次有的一次就放弃。统一固化之后运维只需要盯一套指标最终成功率、平均触达耗时、回退触发次数。3.4 一次完整状态流转示意把上面整个过程串起来一次触达的状态大致这样变化阶段状态说明任务进入PENDING触达层收到请求进行能力解析路由匹配ROUTED选中目标Agent开始握手握手确认HANDSHAKED目标Agent返回READY等待执行WAITING目标队列排队中执行中RUNNING目标Agent开始处理成功SUCCEEDED结果回传发起方确认收到如果是失败场景中间会多出RETRY_SAME、RETRY_OTHER、DEGRADED、FAILED这些分支。所有状态都带时间戳写入日志排查问题的时候直接按touch_id拉全文链路即可。4. 实际部署中容易翻车的五个细节4.1 握手状态没有收敛任务全堵在协商中第一个让我凌晨爬起来处理的线上问题就是握手状态不收敛。某个Agent在一次发布后一直保持PENDING没有返回READY也没有返回REJECTAgent-Reach只能一直等。结果后续任务全部卡在握手阶段表现为任务进入了但没人执行。排查下来发现是Agent端的一个bug它在处理握手请求时先去加载了一个大模型权重加载了20秒还没完成而握手的超时时间设置的是10秒。触达层等了10秒发现没有响应正常应该重试或换Agent但由于代码逻辑有缺陷重试分支里没有把上一次握手会话关闭导致整个状态机卡死在半开状态。这个坑教会我一件事握手必须有绝对超时到点就关闭会话触发回退。绝对不能无限等待一个永不回复的Agent。修复办法也很粗暴给握手请求加了一个timeout字段目标Agent在超时前必须给出明确答复。同时要求Agent端监听握手请求的Timer如果因为加载模型等耗时操作来不及响应必须先返回一个PENDING_HANDSHAKE而不是让触达层猜。4.2 重试风暴同一批任务在整点集体失败还有一个经典问题重试剂量没有做随机化。有一次我把重试间隔设置成固定的60秒结果某个整点时刻外部数据源开始刷新导致一大批Agent任务集体失败。所有任务的第一次重试都落在整点后60秒第二次重试落在120秒一批任务的重试请求像波浪一样层层叠加直接把下游系统的接口打穿了。监控图上能看到一条一条整齐的尖峰特别吓人。后来我给Agent-Reach的重试间隔加了一个抖动因子jitter每次重试间隔在基础值基础上随机浮动正负20%。比如基础间隔30秒那么实际间隔可能是26秒也可能是34秒。这个改动成本极低但重试请求的峰形直接从尖峰变成了平缓丘陵下游系统再也没有被打穿过。4.3 回调地址配置成了死循环另一个让我记忆犹新的坑出在回调地址上。Agent-Reach允许多级Agent链式调用Agent A调Agent BB处理完通过回调通知A。结果有一次配置中心里把B的回调地址误配成了B自己B处理完任务之后回调又回到了B自己相当于B给自己派了一个新任务新任务处理完又回调B自己。两小时后B已经积压了上千个任务单个任务的执行时间从正常的10秒暴涨到5分钟。这个问题的根因是回调地址没有校验。Agent-Reach里有一层信任边界默认只允许回调地址属于known_agents名单并且回调里携带的touch_id必须已经在触达层登记过。任何一个回调请求如果touch_id查无记录直接丢弃并报警。从那以后死循环回调基本绝迹了。4.4 触达层缓存与Agent重启之间的竞态生产环境里还有一个隐蔽的竞态问题。Agent-Reach会在内存里缓存Agent的负载信息Agent端每30秒上报一次触达层刷新缓存。但如果一个Agent在这30秒内崩了又重启触达层手里拿到的还是旧的健康状态就会像一个假装还活着的僵尸节点继续接收任务。解决思路是双向确认Agent重启后会主动向触达层发送一个AGENT_RESTARTED的广播触达层收到广播后立即清除该Agent的所有缓存状态并标记为不可用直到该Agent重新完成握手和上报。不能只依赖定时上报因为定时上报的时间窗口内足够让整批任务送进黑洞。4.5 长轮询比实时WebSocket省心太多最后一个是性能相关的心得。最初的Agent-Reach用WebSocket做实时状态推送每个触达请求保持一条长连接。Agent数量少的时候体验很好状态秒级更新但Agent数量上了两位数连接数开始飙升Agent端还要维护心跳经常出现心跳超时导致连接假死状态又开始不准。后来我把大部分状态同步改成了长轮询Agent端每隔5秒拉取一次自己的待完成任务和状态变更指令。实时性牺牲了一点但稳定性大幅提升。对于Agent触达这个场景秒级和5秒级的延迟差别用户基本感知不到但连接管理和运维复杂度完全不在一个量级。5. Agent-Reach 还能延伸出来什么5.1 触达追踪把一次触达当成一个可观测的SpanAgent-Reach跑顺之后我开始在它上面叠加观测能力。每个touch_id可以生成一个完整的调用链包含发起方、目标Agent、握手耗时、排队耗时、执行耗时、结果大小。这些信息拼起来就能画出整个系统里谁在调用谁的关系图。我在实际排查问题时体验过一个典型场景某个用户请求特别慢拆开看是Agent A调B的握手只花了50毫秒B执行也只花了1秒但B在排队阶段等了整整40秒。这立刻定位到问题出在B的负载不均衡上跟A和B本身的能力无关。没有触达追踪这种问题只能靠猜。5.2 批量触达与编排另一个有用的扩展是批量触达。比如用户问跟上周比我们的核心指标有什么变化这个请求需要同时触达三个Agent一个拉数据、一个做分析、一个写摘要。Agent-Reach可以在一次编排请求里并行触达三个目标再按依赖关系做汇合比让一个Agent依次调用另外三个快了很多。批量触达还有一个隐藏优势它让触达层有了全局视角。三个任务全部失败触达层可以一次性判断这是系统性问题还是局部问题决定是整体重试还是分别回退而不是让上层Agent面对三个独立的失败信号手忙脚乱。5.3 从触达到共识多Agent并行验证最后我在琢磨的一个方向是把Agent-Reach的触达语义从投递升级成共识。具体来说让多个同类Agent同时处理同一个关键任务然后对它们的结果做交叉验证。比如一个高风险操作同时让三个Agent给出判断如果三个结果一致才认为可信如果分歧很大则标记为需要人工确认。这个玩法正在实验依赖Agent-Reach的能力路由和并行触达能力我只需要在触达层加一个选出共识候选集的配置项让同一个任务可以路由到多个候选Agent然后汇总结果做一致性校验。目前看起来效果不错至少比单个Agent拍脑袋可信多了。如果让我给后来者一条最实用的建议先把触达这层做稳再谈思考能力和模型选型。Agent跑得再聪明触达链路一断什么都传不出去。Agent-Reach这个名字当年起得随意但它指向的这件事——让每个Agent都能可靠地够到彼此——在多智能体系统里永远值得认真对待。