接触过Agent开发的人应该都有同感单个Agent的能力现在越来越强但一旦想让多个Agent协作起来问题就全变了。你会发现Agent之间的触达——也就是它们怎么找到彼此、怎么把任务和结果可靠地送过去——才是真正决定协作效率的瓶颈。这也是我做Agent-Reach这个项目的起因。简单说Agent-Reach是一套面向多Agent场景的触达与编排基础设施它把Agent之间的消息路由、通道管理、稳定性治理做成了一层统一底座。这篇文章就把我在这个项目里的完整思路、踩过的坑、以及最终沉淀下来的方案和参数从头到尾讲一遍供正在做类似方向的朋友参考。1. 为什么我会做Agent-Reach多Agent协作里最被低估的触达问题1.1 从一次翻车现场说起事情起因是团队内部的一次多Agent任务调度demo。当时我们有三个Agent一个负责意图识别一个负责数据检索一个负责生成答复。单看每个Agent都很正常但把它们串起来跑的时候问题一个接一个A Agent处理完了结果B Agent压根没收到消息B收到了但延迟了40秒C Agent因为等不到结果直接超时了最崩溃的是重试机制没做好一条消息被重复执行了三遍给业务方生成了三份重复数据。那段时间我们每天干的最多的事就是在日志里捞消息确认到底这条消息发出去没有Agent B是不是真的收到了。这种状态显然没法上生产。后来我意识到问题不在任何一个Agent内部而在它们之间的触达链路上没有统一的连接管理、没有清晰的消息路由规则、没有可靠的送达确认机制。市面上能找到的编排框架大多关注工作流怎么定义和任务怎么拆解但很少有专门把Agent之间怎么可靠触达当成核心问题去解决的。这正是Agent-Reach诞生的原因。1.2 Agent-Reach到底覆盖什么边界在动手之前我先把Agent-Reach的边界划清楚。它不做LLM调用不做Prompt编排也不做Agent内部的状态机——这些应该由Agent框架去解决。Agent-Reach只做三件事通道管理Agent怎么连上来、怎么保持连接、怎么收发消息、消息路由一条消息该去哪个Agent、按什么规则匹配、可靠性治理消息不丢、不重、不堵。这个边界很重要。很多团队在做类似系统时容易把饼画太大最后什么都做什么都做不扎实。Agent-Reach的定位就是一个哑管道但是一个可靠的哑管道。消息进来之后它负责根据规则投递到正确的Agent并且跟你确认Agent确实收到了。至于Agent收到之后怎么处理那是Agent自己的事。提示做这类基础设施边界感比功能多少更重要。先让送达这件事做到极致再去想怎么让Agent协作得更聪明。2. Agent-Reach的核心模型把Agent触达拆成通信、路由、策略三层2.1 为什么不能把所有逻辑揉在一起一开始我也想过用一个直连方案Agent A直接把消息用HTTP POST发给Agent B的地址。看起来简单但实际跑起来就发现根本不可行你得让每个Agent都记录其他所有Agent的地址某个Agent扩容了新实例其他所有Agent的配置都要跟着改一个Agent挂了所有依赖它的Agent全都在反复重试。这本质上是把分布式系统中的服务发现、负载均衡、失败重试全部手工实现了一遍而且要重复实现N遍。Agent-Reach的做法是把触达能力拆成三层每层只解决一类问题。这个思想来自我在微服务网关项目里的经验但针对Agent场景做了大幅简化通信层只负责连接路由层只负责找人策略层只负责保护整套系统的稳定性。2.2 三层模型的具体职责划分通信层的核心职责是维护Agent与Agent-Reach之间的连接通道。我用的是Agent主动连入的模式每个Agent启动时向Agent-Reach注册自己的身份和能力标签然后维持一条长连接。这样Agent-Reach天然变成星型拓扑的中心任何两个Agent之间要通信都走中心转发谁都不需要知道对方的网络地址。路由层做的事情是找人收到一条消息后根据消息头和Agent注册时上报的能力信息匹配出目标Agent。这里我设计了一套轻量级的规则引擎后面会专门讲。路由层不关心消息内容怎么解析只关心该发给谁。策略层就是各种可靠性治理手段超时控制、限流、熔断、幂等、优先级调度。我见过太多项目在最开始把消息发出去就以为完事了结果生产环境一跑各种莫名其妙的问题全冒出来。策略层的作用就是让你在业务代码里不用关心这些脏活累活Agent只需要关注自己收到消息、处理消息、返回结果。2.3 一个任务从发起到送达的完整链路用一个例子来说明这三层是怎么协作的。假设Agent A发起一个任务需要Agent C来处理某种数据检索。整个流程是Agent A把消息封装成统一格式通过自己的长连接发给Agent-Reach的通信层通信层做完基础校验后把消息转给路由层路由层读取消息头里的target_role字段发现有标签retriever去注册表里查当前哪些Agent上报了retriever能力如果匹配到3个实例策略层介入按负载均衡策略选择一个同时检查这条消息有没有触发限流、目标Agent有没有熔断选好实例后通信层把消息推送给Agent C并等待一个ACK确认Agent C处理完成后再通过同样的链路把结果路由回Agent A这套流程看起来不复杂但每一步都有很多细节坑。光是一个等等Agent C没确认怎么办就够你折腾几个通宵。后面我会把每一步的细节展开讲。3. 通信层演进实录从HTTP轮询到WebSocket长连接折腾两个月踩过的坑3.1 为什么最初选的HTTP轮询被推翻Agent-Reach最早一版的通信层是HTTP轮询每个Agent每3秒来拉一次消息队列。当时觉得这个方案最简单Agent-Server两边都是标准HTTP接口排查问题也方便。但实际用了两周就发现这个模型有结构性缺陷。首先轮询间隔和消息延迟存在天然矛盾。你把轮询间隔调到1秒延迟倒是下来了但1000个Agent每秒钟就是1000次空轮询大部分请求都在返回没有新消息。你调到5秒延迟又上去了。而且并发高峰期轮询会带来很大的无效QPS压力白白消耗服务器资源。更关键的是轮询模式下服务器主动通知变得很别扭你想让Agent立刻知道有新任务只能等它下次来拉这种反向等待在多Agent编排里非常致命——任务依赖链中一个环节要等几秒整条链路就会拖得很长。3.2 切到WebSocket长连接后解决的核心问题后来我才痛下决心把通信层全部重写成WebSocket长连接。每一个Agent启动时通过一个注册接口拿到WebSocket接入点然后建立长连接之后所有消息都在这条通道上双向传输。这个改动带来的收益很快就体现出来了服务端要推送消息的时候直接通过已建立的连接发出去毫秒级触达不再依赖轮询间隔连接建立时有身份认证和元数据上报服务端可以实时感知到每个Agent的在线状态、最后活跃时间长连接天然支持双向通信Agent给Server发消息、Server给Agent推消息用的都是同一通道但WebSocket也不是切过来就完事了。生产环境里它有一堆特有的坑。连接断开的探测是最典型的Agent的进程可能还在跑但网络闪断导致连接已经死了服务端这边如果不做主动探测就会一直往一条死连接上推消息。我的经验是设置心跳机制Agent每45秒发一次心跳帧服务端如果在3个心跳周期内即135秒没收到任何消息就判定这条连接不可用把对应的Agent标记为离线同时触发重连流程。3.3 连接恢复与消息补偿的一次实战长连接最怕的就是断了之后消息怎么办。WebSocket断线重连期间如果服务端往这条连接上推了消息消息就丢了。这个问题我花了很大力气才处理明白。最终方案是给每条消息附加一个递增的序号Agent在重连成功后会主动向服务端请求从序号N开始把N之后的消息补给我。服务端把每个Agent的最近消息在内存里保留15分钟配合持久化存储做兜底基本做到了不丢消息。注意WebSocket本身只是通道它不保证消息可靠性。可靠性是你自己设计出来的——心跳、序号、补偿拉取这些机制合在一起才构成可靠触达。说句实话当时切WebSocket的时候Team里也有不同意见觉得为了个触达搞这么重是不是过度设计。但后面线上压测时消息延迟从轮询模式的500ms级别直接降到了50ms以内调度链路一整个顺畅了这个争议才算平息。4. ReachRule路由规则让消息自己找到该去的Agent4.1 路由信息模型与Agent注册Agent-Reach的路由层依赖于一套Agent元数据模型。每个Agent启动注册时上报三个维度的信息身份agent_id全局唯一格式建议team.role.uuid比如data.retriever.8f3a2c能力capabilities一组标签比如[retriever, langchain, pinecone]指向target用于描述这个Agent适合处理哪类任务比如task_typeretrieval路由的核心逻辑其实就是一个匹配函数给一条消息算出它该去哪个Agent。一开始我想用规则引擎框架比如Drools但后来觉得太重了Agent场景下的匹配规则并不复杂用一套DSL配置就够了。于是就有了ReachRule。4.2 ReachRule的语法设计ReachRule就是一种声明式的路由规则文件用YAML写成Agent-Reach启动时加载并编译成匹配树。初期版本的规则就三种类型rules: - name: route_to_retriever when: message_type: task_request target_role: retriever action: dispatch: random priority: high - name: route_to_aggregator when: message_type: task_result source_role: retriever action: dispatch: sticky sticky_by: sequence_id - name: fallback_to_human_review when: message_type: task_request target_role: retriever action: dispatch: random priority: normal fallback: if_no_available_agent: notify_admin解释一下三个核心字段when消息匹配条件。我预留了message_type、target_role、source_role、custom_tags这几个常用字段项目里大概95%的路由需求都能覆盖action匹配后怎么派发。random表示从候选实例里随机选一个适合无状态Agentsticky表示同一个sequence_id的消息始终派发到同一个实例适合有状态会话比如多轮对话的Agentfallback找不到合适Agent时的兜底行为。notify_admin是给运维发告警queue_task是把任务挂起等待后续重新尝试4.3 实际项目里路由规则怎么收敛规则引擎初期我最大的教训是规则表别一开始就铺得很大。那阵子我各种设想Agent协作场景一口气写了40多条规则上线后发现大量规则互相覆盖查一条消息为什么路由错了要花很久。后来做了一次规则减肥砍到现在常用的11条反而更稳定。这背后的逻辑是Agent数量少的时候规则的价值更多体现在清楚地知道谁该干什么而不是穷举所有可能的协作路径。等Agent数量涨上去之后再根据真实消息流量去补齐规则比一开始就过度设计靠谱得多。另外路由匹配这个环节建议做好观测。我在Agent-Reach里给每条消息加了reach_trace_id从消息进来到最终到达哪个Agent全链路都有日志。排查问题的时候只要拿着这个trace id一查整条路由链路一目了然。这个功能在做系统的人看来可能平平无奇但实际救了我无数次。5. 稳定性治理超时、限流、熔断、幂等这些生产环境绕不开的硬仗5.1 超时重试指数退避不是配个参数那么简单Agent场景和普通API调用的一个很大区别是Agent处理一次任务可能很慢比如检索类任务动辄5~10秒生成类任务可能要20秒以上。这就让超时时间变成了一个很微妙的东西调短了Agent还在干活就被误杀调长了下游Agent等待过久直接堆消息。Agent-Reach对每种消息类型设置了独立的超时配置。比如task_request类默认超时30秒health_check类默认5秒。超时后的重试策略采用指数退避加抖动第一次重试等待1秒第二次2秒第三次4秒最大等待32秒同时每次等待时间加一个0~20%的随机抖动避免重试风暴把服务端打挂。这里提醒一下不要所有消息一超时就重试。我在项目里把消息分成了两类——可重试的和不可重试的。像数据写入类的Agent任务重试可能导致重复写入必须靠幂等机制兜底而纯查询类任务重试一次问题不大。这个分类判断一定要在消息源头做掉不能等到重试那一刻才想这个能不能重试。5.2 限流与熔断Agent多了照样把通道挤爆当Agent从10个涨到100个、再到几百个的时候消息量不是线性增长而是任务之间的组合关系带来的爆发式增长。去年有一场流量测试瞬间有200个Agent同时提交任务消息队列秒级积压了几万条服务端的CPU和内存直接被干到告警线。这让我把限流和熔断提上了优先级。Agent-Reach的限流方案分两层租户级限流每个Agent注册后都会分配到名下的消息配额。比如默认每个Agent每秒最多提交100条消息超过的返回429 Too Many Requests。这个配额可以在Agent-Reach的管理台动态调整关键Agent比如负责最终交付的可以给到1000 QPS目标级限流类似接口限流对每个Agent的收件能力做了保护。目标Agent处理能力有限如果消息把目标Agent的连接缓冲区撑爆整个通道都会出问题。所以Agent-Reach对每个目标Agent维护了一个并发处理上限超过限制的消息先在服务端排队熔断的逻辑是针对目标Agent的持续失败做的。比如路由层发现发给某个Agent的消息连续失败率超过50%在60秒时间窗口内就把该目标Agent标记为熔断状态接下来的2分钟内不再给它派发新消息。这个做法很像剁掉一只手指保住整个手掌——宁可让一部分任务暂时失败也不能让一个坏的Agent拖垮整条触达链。5.3 幂等多Agent场景下最容易被忽略的魔鬼幂等是我在这个项目里吃过最大亏的地方。事情是这样的Agent C处理一条任务时因为网络抖动超时了Agent-Reach自动重试了一次。结果Agent C其实已经把任务处理完了只是结果回传的时候延迟了于是重试导致它重复处理了一遍。如果任务本身是更新数据重复执行可能只是多点消耗但如果是发送消息创建资源那就是实打实的事故。最终方案是在Agent-Reach里加了一层幂等控制。每条消息带一个power_id生成规则是{agent_id}:{task_id}:{timestamp_ms}。服务端在处理消息落地时基于这个power_id做去重同一power_id的消息只投递一次同时把投递结果缓存起来后续重复到达直接返回之前的处理结果。缓存我用的Redis设置了24小时的过期时间——这个窗口够长了超过24小时重复到达的概率非常低。部署这个之后重复处理相关的告警基本清零了。5.4 优先级队列让关键任务插队Agent之间的任务不是平等的。有些是我系统内部的常规任务比如定期数据同步有些是用户实时发起的关键请求比如用户正在用的客服Agent的意图分析。如果都进同一个队列关键请求排在一堆低优先级任务后面等待时间谁都受不了。我在Agent-Reach里设计了一个三级优先级的调度机制优先级适用场景队列资源占比high用户实时请求、任务链路末尾的汇聚Agent60%normal常规任务编排、Agent间协同计算30%low数据同步、后台预计算10%消息进队时打上优先级标签出队时优先从high队列取high队列空了我才去看normal和low。这个比例不是拍脑袋拍的是根据线上真实流量里各类消息的占比和延迟要求调出来的。一开始high给到80%太多导致低优先级任务饿死调到60%之后高优先级任务的P99延迟控制在80ms内低优先级任务虽然慢一点但至少不会被永久搁置相对均衡。6. 实测效果与Agent-Reach上线前后的对比复盘6.1 压测环境和数据Agent-Reach的压测我是在三台4核8G的云主机上做的一台跑Agent-Reach服务端两台各跑20个模拟Agent Worker。存储用的Redis 6.2消息持久化用Kafka。压测工具是自己写的模拟客户端模拟Agent按照真实业务的消息模式发起任务和回传结果。最终压测数据如下指标数值单节点并发长连接数12,000消息吞吐8,500 TPSP99消息触达延迟局域网38ms消息丢失率0压测期间重试成功率99.2%这组数据放到生产环境里是稳稳够用的。顺带一提切到长连接之后对比之前的轮询模式延迟从几百毫秒降到了38ms这个优化幅度连我自己都惊讶——通信模式的选择对整体系统体验影响太大了。6.2 上线前我做的三件收尾事这里有个经验无论你项目功能多全上线前有几件事必须做完否则生产环境一定会给你颜色看。一是错误码规范化。Agent-Reach的所有异常场景都定义了一套统一的错误码比如1001连接超时、1002连接断开、2001路由找不到目标Agent、3001目标Agent熔断。没有这套错误码你根本没法在告警平台里做聚合分析只能天天翻原始日志。二是配置项全量梳理。我把Agent-Reach里每个可调参数都整理了一份说明文档包括默认值、建议值、调整后可能影响什么。比如前面说的心跳间隔45秒、熔断冷却2分钟、幂等缓存24小时这些参数如果你不记录下来三个月后回来改的时候绝对一脸懵。三是降级演练。我专门做了两次故障演练一次是让Redis挂掉验证消息在降级模式下能不能走本地缓存不丢一次是让一个Agent Worker实例突然退出验证剩余实例能不能自动接管消息派发。这两场演练各查出两个隐藏的bug都在正式上线前修掉了。6.3 上线后的真实运行表现Agent-Reach上线到现在已经跑了4个月线上环境有300多个Agent实例在跑。这期间出过三次告警但每次定位时间都在10分钟内——基本上都是靠trace id一查就知道是某个Agent实例网络抖动、连接断开后重连没跟上。每次出这种问题我都会跟团队说如果这三个月没有Agent-Reach我们可能要把一整天花在排查消息到底去哪了上。稳定性和开发效率是Agent基础设施最核心的两个产出。Agent-Reach上线后团队里新接入一个Agent的时间从原来的一天缩短到半天——Agent开发不需要关心怎么连、怎么路由、怎么重试只要注册一下、声明能力标签、收发消息就完事了。注意Agent基础设施本质是在帮你把不可控的Agent协作问题变成可控的管道问题。只要你把每一类消息经过的路径都看清楚、控制住Agent之间哪怕各自逻辑再复杂整体协作仍然可以做到可靠。收尾的一点个人体会Agent-Reach这个项目走到今天我最深的体会是Agent协作的技术难度并不在于某个Agent的算法复杂度而在于很多个Agent同时乱跑带来的复杂性问题。工程上解决问题靠的往往不是特别高深的技术而是一堆基础手段的组合——超时、重试、限流、熔断、幂等、规则路由这些东西在传统后端都是老生常谈但把它们真正做成一套Agent场景下的统一触达底座你遇到的问题还是会有很多新坑。如果你也在做Agent相关的技术基建建议先别急着上各种复杂的调度编排框架把Agent之间怎么可靠地找到彼此、送达消息这件事先啃下来。这个底座打得越牢后面在上层做任何编排都会顺手得多。