
1. 项目概览Agent-Reach 要解决什么问题Agent-Reach 是我最近大半年一直在打磨的一个多智能体调度与触达基础设施项目。在做这个项目之前我踩了不少多 Agent 编排的坑比如智能体之间互相找不到、任务消息投递了但没人消费、扩容之后路由规则一片混乱。这些问题的根源其实很一致大家把精力都放在单个 Agent 的模型能力和工具调用上却忽略了一个最基础的需求——智能体之间到底怎么稳定、高效地“找到对方、叫得动对方”。Agent-Reach 这个名字拆开看就是 Agent Reach核心做的是“智能体触达”。它不是一个具体的业务 Agent而是位于所有智能体之下的一层通信与调度基座。简单来说它负责三件事智能体注册与发现、意图驱动的路由分发、以及全链路的可观测追踪。你问我为什么需要这么一层东西因为当你的系统里只有三五个 Agent 时直接写死调用关系完全没问题但一旦到了几十个 Agent、各自有不同的能力域、部署在不同服务甚至不同机器上硬编码的调用关系就会变成一团乱麻。这个项目适合谁参考两类人。一类是正在做 AI Agent 平台、想把多个智能体串成工作流的技术负责人另一类是对分布式系统里的服务发现、消息路由、任务调度有兴趣的后端开发者。即使你现在只维护两三个 Agent这套设计思路里的注册机制和路由策略也能帮你提前规避不少扩展期的痛苦。我在设计 Agent-Reach 时有一个明确的原则Agent 之间不直接感知对方的存在。所有通信都通过 Reach 层进行间接路由这样任何一个 Agent 的上下线、升级、迁移都不会影响其他 Agent。这个决策在后来的实际运行中被证明是值得的后面我会详细讲为什么。2. 整体设计思路为什么 Agent 之间不能“直连”2.1 从“点对点调用”到“间接路由”的转变早期我搭建多 Agent 系统时采用过最直接的方式Agent A 要调用 Agent B就在代码里写死 B 的服务地址通过 HTTP 接口互相调用。刚开始很爽因为链路短、问题排查快。但 Agent 数量一多问题接踵而至——每新增一个 Agent就要手动修改所有需要调用它的上游代码某个 Agent 换了部署节点地址变了所有调用方都要跟着改某个 Agent 负载高了你想给它多加两个实例却发现上游代码里配置的是单点地址。后来我意识到这个问题在传统的微服务架构里早就有了成熟解法服务注册与发现。那为什么 Agent 系统不能复用这套思路理论上完全可以但 Agent 之间的调用比普通服务调用多了一层语义匹配的需求。普通微服务调用客户端明确知道我要调用“订单服务”只需要找到订单服务的实例地址即可但 Agent 场景下调用方往往只知道“我要完成一个图像描述任务”不知道也不关心具体由哪个 Agent 来执行。所以 Agent-Reach 在传统服务发现之上加了一个“意图路由”的语义层。调用方只需要声明自己的意图Reach 层根据每个 Agent 注册时声明的能力描述和当前负载状态动态决定把任务交给哪个 Agent。这有点像生活中的电话总机你不需要知道对方部门里具体哪个同事在工位上只需要说“帮我转接售后退款”总机会根据各人的忙闲状态帮你接通合适的人。2.2 核心模块划分注册中心、路由引擎、消息总线、追踪系统Agent-Reach 的整体架构可以拆成四个核心模块每个模块各司其职注册中心维护所有 Agent 的动态信息包括能力标签、服务地址、版本号、当前健康状态、负载指标。每个 Agent 启动时向注册中心上报自己的元数据运行期间定时发送心跳。路由引擎接收调用方的意图请求结合注册中心的实时信息通过评分策略选择最合适的 Agent 实例。路由结果可以是同步响应也可以是异步任务的投递目标。消息总线负责任务消息的可靠传递。同步请求走 RPC 通道异步任务走消息队列保证消息不丢、不重复、不乱序。追踪系统为每一次跨 Agent 调用生成唯一的 Trace ID记录调用链路上每个环节的耗时、入参出参摘要和错误信息。这四个模块的解耦在设计上很关键。我见过一些团队把路由逻辑直接塞进消息总线的代码里看起来省事但实际调试时非常痛苦——你很难分清一个任务没被执行到底是路由问题还是消息投递问题。Agent-Reach 把每个模块当作独立服务部署模块之间只通过标准协议通信这样任何一环出问题都能快速定位。2.3 设计取舍一致性和可用性的权衡在注册中心的实现上我遇到了一个经典的分布式系统取舍问题保证强一致性还是高可用。如果为了保证所有节点看到的 Agent 列表完全一致我可以引入 ZooKeeper 或 etcd 这类强一致性组件但如果注册中心短暂不可用整个 Agent 系统就全瘫了代价有点大。最终我选择了“最终一致 本地缓存兜底”的折中方案。注册中心用 etcd 存储元数据但每个接入 Agent-Reach 的业务方本地都会缓存一份 Agent 列表并订阅变更事件。即便注册中心短暂抖动本地缓存仍然可以支撑路由决策只是无法感知最新的上下线变化。这个设计在线下压测时没觉得有多重要直到有一次 etcd 集群因磁盘问题短暂不可用业务流量却完全没有感知我才认识到这个兜底设计是真正的救命稻草。3. 路由引擎的详细设计与匹配机制3.1 能力标签体系Agent 如何描述自己要让路由引擎做出正确决策前提是每个 Agent 能准确描述自己“会什么”。我设计了一套轻量级的能力标签体系每个 Agent 注册时可以声明一到多个能力标签每个标签由三部分组成领域domain所属业务域比如图像、文本、语音、代码。动作action具体能力比如图像生成、文本摘要、情感分析、代码审查。属性attribute额外限制或特征比如支持的模型类型、最大并发数、GPU 需求。举一个实际例子某公司内部有一个 OCR 识别 Agent它的注册信息可能是这样{ agent_id: ocr_worker_01, tags: [ { domain: image, action: ocr, attribute: chinese }, { domain: image, action: ocr, attribute: english } ], endpoint: 10.20.30.40:8080, healthy: true, load: 0.4 }调用方的意图请求不需要指定具体 Agent ID只需要声明自己的需求{ request_id: req_12345, intent: { domain: image, action: ocr, attribute: chinese, priority: high } }路由引擎拿到意图后先在本地缓存里根据标签做粗筛找到所有支持该意图的 Agent再进入下一步的智能评分环节。3.2 评分策略不只看“谁能做”还要看“谁最合适”粗筛之后可能同时有好几个 Agent 都满足调用方的意图。这时候不能随便挑一个而是要根据多维度的评分来选最优解。我在 Agent-Reach 里实现了四个评分因子每个因子都有一个权重默认权重可以通过配置文件调整亲和度AffinityAgent 的历史成功率和调用方或任务类型的历史匹配度。某个 Agent 总是能很好完成这类任务亲和度就高。负载度LoadAgent 当前正在处理的请求数占总容量的比例。负载越低得分越高。资源匹配ResourceAgent 的硬件资源是否满足任务的特殊要求。比如任务标注了“需要 GPU”那支持 GPU 的 Agent 加分。网络距离Latency调用方和 Agent 实例之间的网络往返时延。距离越近得分越高。最终的得分是这几个因子的加权和。我曾经尝试把更多的因子加进来比如 Agent 的模型版本新旧、团队归属等但发现因子太多会导致路由行为难以预测调试时根本说不清楚为什么任务被分配给了某个 Agent。所以建议你们在实际落地时控制因子数量3 到 5 个即可关键是让每个因子的计算逻辑可解释、可观测。路由引擎里有一个很重要的机制就是负载上报的时效性。Agent 并不是每次都实时上报自己的负载而是每隔几秒上报一次。这就会导致路由引擎看到的负载信息有一定延迟两个并发请求可能同时被路由到了同一个 Agent 上。为了解决这个问题路由引擎在本地会做一个“预估负载”的增减——每次把任务路由给某个 Agent就本地记录一个 pending count真实上报的负载到达后再校正。3.3 路由结果缓存与动态配置刷新路由决策本身也是有成本的尤其是当评分因子多、Agent 数量大的时候。虽然单个决策可能只要几毫秒但高并发下积少成多性能就会受影响。所以我在 Agent-Reach 里加了一层路由结果缓存对于相同意图、相同属性组合的请求在缓存有效期内直接复用上一次的路由决策。注意这里的缓存不是永久有效否则 Agent 挂了流量依然会打过去。我默认把缓存时间设为 10 秒并且每次收到 Agent 心跳或下线通知时主动失效相关缓存。动态配置刷新这部分也很关键Reach 的配置中心支持热更新比如调整某个意图的默认路由权重、灰度比例、超时时间等不需要重启服务。我把这些配置项存储在 etcd 中各节点监听变更事件变更后拉取最新配置并加载。灰度发布的时候我会动态调整某个新版本 Agent 的路由权重先放 5% 的流量进去观察错误率和耗时稳定后再逐步放大到 100%。4. 实操过程消息总线的可靠投递与链路追踪实现4.1 消息协议设计同步 RPC 与异步任务的混合策略Agent 之间的调用有些场景需要同步等待结果比如用户问了一个问题需要召回 Agent 返回答案有些场景则适合异步处理比如批量图片审核提交任务后不需要立刻拿到所有结果。Agent-Reach 同时支持这两种模式底层协议使用 gRPC 和消息队列组合。同步 RPC 部分我使用的是 gRPC 双向流。为什么不用普通的 unary 调用因为在一些长时间运行的任务中客户端希望能实时收到中间状态更新比如“任务正在处理中当前进度 45%”unary 调用只能等全部完成才能返回体验很差。双向流可以让服务端主动推送进度事件客户端随时可以感知任务状态。异步任务部分我选择 Redis Stream 作为消息队列的底层存储。选 Redis Stream 而不是 Kafka、RabbitMQ主要是出于对部署复杂度和延迟的考虑。Kafka 适合大规模吞吐的场景但多了一个 Broker 集群要运维Redis Stream 在大多数 Agent 场景下吞吐量已经足够而且支持消费者组、消息确认、死信队列这些关键特性。如果你的集群规模特别大日请求量超过千万级别再考虑换成 Kafka 也不迟。4.2 核心代码实践路由引擎的实现要点路由引擎的核心逻辑其实不复杂难的是把边界情况处理好。我贴一段简化版的意图匹配和打分代码方便你们参考整体思路class RouteEngine: def __init__(self, registry, config): self.registry registry # 注册中心客户端 self.config config # 动态配置 def route(self, intent): # 1. 本地缓存查找命中直接返回 cache_key self._build_cache_key(intent) cached self.cache.get(cache_key) if cached: return cached # 2. 粗筛根据意图标签过滤候选 Agent candidates self.registry.match_tags(intent.domain, intent.action, intent.attribute) # 3. 细筛打分排序 best_agent None best_score float(-inf) for agent in candidates: if not agent.healthy: continue score self._score(agent, intent) if score best_score: best_score score best_agent agent if best_agent is None: raise NoAvailableAgentError(intent) # 4. 写入缓存并返回 self.cache.set(cache_key, best_agent, ttlself.config.cache_ttl) return best_agent def _score(self, agent, intent): # 亲和度、负载、资源匹配、网络距离四个因子加权求和 affinity self.affinity_store.get(agent.agent_id, intent.domain) load_score 1.0 - agent.load resource_score 1.0 if self._resource_match(agent, intent) else 0.0 latency_score 1.0 / (1.0 agent.latency_ms / 100.0) return ( self.config.weight_affinity * affinity self.config.weight_load * load_score self.config.weight_resource * resource_score self.config.weight_latency * latency_score )代码逻辑很直白但有几个细节我要特别提醒缓存命中后的负载修正如果直接使用缓存返回的 Agent不修正它的预估负载流量可能在缓存有效期内全部打到同一个 Agent 上。因此每次缓存命中也要调用一个incr_pending(agent_id)来调整该 Agent 的预估负载值。亲和度信息的冷启动新上线的 Agent 没有历史数据亲和度默认给一个中间值 0.5避免分数过高导致流量突增也避免分数过低导致长时间无流量。评分因子的权重校准不要试图一次性把所有权重都调到完美。我的做法是先所有因子等权0.25运行一周后看路由分布是否符合预期再针对性调整。比如发现某个 Agent 老是空闲而其他 Agent 忙不过来就适当增大负载因子的权重。4.3 链路追踪一次任务请求的全链路视图没有链路追踪的多 Agent 系统排查问题是非常煎熬的。之前试过在 A Agent 的日志里看到“调用 B 失败”但 B 的日志里又找不到任何记录——因为 B 可能根本没收到请求或者收到的请求 ID 对不上。Agent-Reach 的追踪系统从根本上解决了这个痛点。每一次请求进入 Reach 层时都会生成一个全局唯一的 Request ID后续所有内部调用都会携带这个 ID。我把链路追踪的数据模型设计为一个 Trace 由多个 Span 组成每个 Span 对应一次具体的跨 Agent 调用或内部处理步骤。每个 Span 记录以下关键信息Span ID、Parent Span ID用来还原调用层级调用的 Agent ID、方法名、入参摘要开始时间、结束时间、耗时状态码和错误信息在实际存储上我没有引入专用的分布式追踪系统比如 Jaeger 或 Zipkin因为这些系统对 Agent 场景下的自定义字段支持不够灵活。我选择把 Span 数据直接写入 ClickHouse因为 Agent 场景的调用量远不如互联网核心链路那么夸张ClickHouse 的存储和查询性能完全能扛住而且按 Request ID 查询全链路非常快。如果你不想额外引入 ClickHouse用 Elasticsearch 也可以只是写入吞吐要弱一些。追踪数据的可视化界面我是自己写了一个简易的查询页面输入 Request ID 后可以展示出瀑布图式的调用链路每层调用的耗时和状态一目了然。有一次线上排查任务超时问题就是通过这个页面发现某个 Agent 调用外部模型 API 的耗时占了总耗时的 85%而之前没有追踪系统时我们只会盯着 Agent 本身的代码排查在错误方向上浪费时间。5. 常见问题与排查技巧实录5.1 问题一Agent 心跳正常但请求超时如何定位我遇到过好几次现象诡异的问题注册中心显示 Agent 状态健康、心跳正常但路由过去的请求大部分超时。刚开始很困惑后来排查发现原因出在 Agent 的线程池队列被占满健康检查接口用的是独立线程池所以心跳正常但实际业务线程早就没资源处理新请求了。这个问题暴露了“健康检查”和“业务可用性”不是一回事。解决方案是在健康检查接口里加入对核心线程池队列使用率的探测如果队列积压超过 80%那么该 Agent 向注册中心上报的状态就不是 healthy而是 busy。路由引擎在粗筛阶段看到 busy 状态就会自动跳过这个实例把流量分给其他可用实例。5.2 问题二意图匹配误命中任务被路由到了不合适的 Agent能力标签体系如果设计得不好会出现语义相近的两个标签被错误匹配的情况。比如一个 Agent 注册了“image.ocr.chinese”另一个 Agent 注册了“image.ocr.english”调用方意图是“image.ocr.chinese”但如果匹配逻辑只比较 domain 和 action、忽略了 attribute就会把请求随机分到两个 Agent 上一半任务的处理结果完全不对。这类问题我建议做两层防护。第一层匹配逻辑严格要求标签全字段匹配domain、action、attribute 都要一致才算命中。第二层在 Agent 端做能力校验收到任务后先检查消息里的意图声明是否在自己的支持列表内不匹配就直接拒绝并返回错误码而不是硬着头皮处理。这样即使路由层出了 bug也不会产生错误的业务结果只会产生一条清晰的错误日志。5.3 问题三消息重复投递导致业务重复执行异步任务模式下消息队列的“至少一次投递”语义会导致重复消息的出现。比如消费者处理完任务后还没来得及提交确认进程崩溃了队列会把消息再次投递给其他消费者。Agent 任务和普通业务消息不太一样很多任务是幂等的比如“生成一张图片”“执行一次文本分类”重复执行虽然浪费资源但不会产生脏数据。但有些任务不是幂等的比如“给用户 Wallet 增加积分”重复执行会导致用户余额错误。我在 Agent-Reach 里增加了一个任务去重模块基于 Redis 的 SETNX 指令实现每个任务在处理前先尝试写入任务的 Message ID如果写入成功说明是第一次处理正常执行如果写入失败说明已经处理过丢弃这条消息。内存中的去重记录会设置过期时间比如 24 小时避免 Redis 内存无限增长。5.4 常见问题速查表为了方便排查我把遇到过的典型问题整理成一个速查表问题现象可能原因排查方法解决方案请求频繁超时心跳却正常业务线程池队列积压查看 Agent 实际处理耗时和线程池活跃度健康检查增加队列使用率探测路由结果偏向某个实例负载上报延迟导致预估不准对比注册中心负载和实际请求分布增加缓存命中时的 pending 计数意图匹配命中错误 Agent标签匹配条件过宽松查看路由日志中的匹配标签详情严格全字段匹配并增加端侧校验异步任务重复执行消息队列至少一次语义查看消费者日志的 Message ID引入基于 Redis 的任务去重注册中心短暂不可用后任务失败本地缓存缺失检查路由引擎是否报 NoAvailableAgent配置本地缓存兜底并订阅变更事件新 Agent 上线后长时间无流量亲和度冷启动分数低查看路由分数明细设置冷启动默认中间分值并逐渐升温5.5 调试利器把路由决策“摊开”看最后分享一个我强烈推荐的调试习惯——把路由决策过程完整地记录下来。Agent-Reach 里有一个“决策解释模式”开启后每次路由都会输出一条详细日志包括候选 Agent 有哪些、每个候选的四个评分因子分别是多少、最终选择的 Agent 和原因。这就像黑盒测试时打开了白盒开关路由逻辑不再是个无法解释的黑盒。有一次我调优权重参数就是因为开了决策解释模式才发现“资源匹配”因子的权重设置得不合理——几乎所有任务都带 GPU 属性导致不支持 GPU 的 Agent 在打分时永远处于劣势但有些任务其实根本不需要 GPU。我根据日志把任务属性和实际需求收紧后集群的整体资源利用率提升了不少。6. 写在最后的个人体会Agent-Reach 这半年的演进让我对“多智能体系统”有了更务实的理解。很多人讲到 Agent第一反应是模型能力和工具调用但一个真正能上生产环境的 Agent 系统通信基座的稳定性和可观测性才是成败的关键。模型能力再强如果 Agent 之间连基本的“找到对方、可靠地把任务传过去、出了问题能快速定位”都做不到一切都是空中楼阁。在实现过程中我最大的体会是技术的难点往往不在技术本身而在设计决策的权衡。比如最终一致和强一致的取舍、路由因子数量和可解释性的平衡、直接用现成消息队列还是造轮子……这些决策背后没有一个绝对正确的答案只有最适合你当前业务场景的方案。我建议你也不必照搬 Agent-Reach 的全部设计先挑一个最痛的场景切入比如如果你的 Agent 已经常常因为找不到彼此而失败就先落地注册与发现机制把链路追踪留到下一阶段。从我的实践来看先把基础通信层做扎实收益是肉眼可见的Agent 扩容不再需要改代码、故障定位从小时级缩短到分钟级、新 Agent 接入从半天变成半小时。希望这篇文章能给你一些可落地的参考后续如果你在具体实现中遇到什么问题也欢迎多交流碰撞。