在折腾了大半年多智能体协作之后,我越来越觉得“Agent-Reach”这个词本身就点破了分布式 AI 应用里最疼的一个问题:你手里的 Agent 再聪明,如果它触达不了该触达的节点、拿不到该拿的数据,那它跟单机脚本没什么区别。我们今天聊的这套 Agent-Reach 项目,核心就是解决智能体之间的“通讯录”和“快递系统”——让每个 Agent 知道别人在哪、怎么找、消息怎么安全送达。
我搭建这套系统的初衷其实很现实。团队正在做一个多智能体的任务编排平台,早期 Agent 之间用网络请求直连,代码里写死的 IP 和白名单,节点一多直接乱成一锅粥。新 Agent 上线要改配置,旧 Agent 下线了其他节点还在拼命重试,消息偶尔丢失也没人管。Agent-Reach 就是在这个背景下从零手搓的一套轻量级智能体注册与消息触达框架,今天我把完整的设计思路、关键实现、踩过的坑一次讲清楚,希望对正在做类似架构的你有点帮助。
1. 为什么多智能体协作需要一张“通讯录”:从一次线上事故说起
先说一个让我下定决心重构的线上事故。当时我们有三个 Agent 节点协同处理用户查询,A 节点负责意图识别,B 节点负责知识库检索,C 节点负责生成答复。一次发布后 B 节点因为配置错误悄悄挂了,但 A 节点完全不知道,还在按老地址持续给它发请求。结果就是每个请求都在等待超时,前端转圈转满 30 秒,用户投诉电话被打爆。
事后复盘时我们发现,问题的根源不是代码逻辑,而是节点之间缺乏一套“存在性感知”机制。
1.1 原生实现的问题清单
在那个阶段我们用的是最原始的 HTTP 直连方案,问题积累到一定规模后集中爆发:
- 地址硬编码:每个 Agent 的配置里写死其他 Agent 的 IP 和端口,换一台机器就得全部改配置。
- 无健康检查:接收方挂了,发送方完全无感知,只能靠超时重试,每次失败浪费大量时间。
- 无消息确认:发出去了就认为自己任务完成,但对方到底收没收到、处理成功没成功,完全没有反馈。
- 无动态扩缩容:想临时加一个 Agent 节点分担压力,需要手动通知所有现有节点,操作极容易出错。
你可以把 Agent 想象成一家公司的员工。早期公司只有三五个人,你扯着嗓子喊一声就能沟通,不需要什么组织结构。等团队扩到几十上百人,没有通讯录、没有前台转接,你要找财务得挨个办公室敲门问,这种效率迟早完蛋。Agent-Reach 就是给这群 Agent 装一套内部通讯录和中转系统。
1.2 Agent-Reach 的设计边界
在动手写代码之前,我先明确了 Agent-Reach 要解决的问题边界,这个很关键,不然容易越做越臃肿:
- 只管 Agent 之间的“互认”和“互达”:它负责让新 Agent 能注册进来、让其他节点能找到它、让消息能可靠投递。
- 不管 Agent 内部的业务逻辑:Agent 拿到消息之后怎么处理、用不用大模型、选什么模型,这些不在 Agent-Reach 的职责范围内。
- 提供同步和异步两种通信模式:实时性要求高的调用走同步接口,任务型、耗时长的请求走消息队列。两者使用同一个注册发现机制,避免维护两套系统。
明确了边界之后,整体架构就清晰了。Agent-Reach 由三个核心模块构成:注册中心(Registry)、消息交换层(Exchange)、客户端 SDK(Reachlet)。下面的章节我会逐个拆解它们在真实项目中是怎么设计和落地的。
2. 注册中心的实现细节:Agent 状态感知的“心脏”
注册中心是整个 Agent-Reach 系统里最容易做但最难做对的部分。它表面上只是一个存储 Agent 元数据的表,但“元数据的一致性和实时性”直接决定了整个系统触达成功率。
2.1 数据模型与注册协议
我给每个 Agent 定义了一套简洁的注册信息,用 JSON 格式表示,字段如下:
{ "agent_id": "agent-query-01", "agent_type": "intent_recognizer", "host": "192.168.1.105", "port": 9011, "capabilities": ["text_classification", "slot_filling"], "replica_of": "agent-query", "ttl": 15, "rating": 0.85 }这里几个字段是后来实际运行中验证过的“刚需”:
agent_id:全局唯一标识,格式由“业务名-实例序号”组成,方便日志排查时一眼看出是哪个 Agent 的哪个副本。capabilities:能力标签数组。我最初没用它,后来发现有业务方想按“能力”而不是“名称”去路由请求,比如“找一个能做文本分类的 Agent”,这个字段就成了动态路由的依据。ttl:生存时间,单位秒。Agent 必须在这个时间内发送心跳续约,否则注册中心将该节点标记为不健康。这是实现故障感知的核心机制。
注册协议我设计成了三个极简接口,走 HTTP 或 gRPC 都行,我最终选了 gRPC 因为团队内部已经统一使用了:
Register(agent_info):首次启动时调用,带完整元数据。Heartbeat(agent_id, timestamp):周期性续约,只带最小字段。Deregister(agent_id, reason):优雅退出时调用,主动告知大家“我要走了”。
2.2 为什么心跳周期定在 TTL/3
这里有一个非常容易踩坑的参数设计:心跳周期和 TTL 之间应该保持什么比例?
我一开始让 Agent 每 1 秒发一次心跳,TTL 设 3 秒,想着这样故障发现最快。结果注册中心一台 4 核 8G 的机器扛 50 个 Agent 就很吃力了——每秒 50 个心跳请求虽然不算多,但 gRPC 连接保持、状态加锁、超时检查加在一起,CPU 损耗比预想高很多。后来把心跳周期调成 5 秒、TTL 15 秒,性能压力立刻降下来,故障发现也才慢 5 秒,完全在可接受范围内。
我现在的经验公式是心跳周期 = TTL/3,TTL 设为 15 秒时心跳周期 5 秒。这样即使网络抖动导致连续丢 2 个心跳包,节点也不会被误杀,而第 3 个心跳没到就基本可以判定节点真出问题了,可靠性有保证。
2.3 注册中心的存储选型
早期我直接用 Redis 存 Agent 元数据,key 是 agent_id,value 是 JSON 字符串,配合 expire 机制实现 TTL 淘汰。但后来发现一个致命问题:Redis 的过期删除是惰性的,Agent 已经失联了但注册中心返回的状态可能还是健康,因为 Redis 还没执行淘汰。
后来切成了内存注册表 + Raft 日志持久化的方案。每个注册中心节点自己维护一个并发的 map,Agent 元数据存内存,读路径极快;变更操作(注册、心跳、注销)通过 Raft 复制到其他副本,保证高可用场景下的强一致。
这个设计简洁可靠,实测 100 个 Agent 同时注册,注册中心的内存占用不到 20MB,读写 P99 延迟都稳定在微秒级。
3. 消息交换层的触达策略:从同步阻塞到异步可靠
注册中心解决了“Agent 在哪”的问题,但“消息怎么送过去”是另一个维度的问题。Agent-Reach 的消息交换层设计了三种模式,分别对应不同的业务场景。
3.1 三种投递模式怎么选
| 模式 | 适用场景 | 实现方式 | 可靠性 |
|---|---|---|---|
| 同步请求-响应 | 意图识别、在线问答等需要实时结果的调用 | gRPC 双向流,请求发出后等待响应 | 中等,依赖超时重试 |
| 异步任务投递 | 离线计算、批量向量化、文档处理等耗时任务 | 持久化消息队列,消费完成后回执 | 高,at-least-once 投递 |
| 广播订阅 | 事件通知、状态同步、配置刷新 | 发布-订阅模型 | 中高,按消费者确认进度管理 |
同步模式不用多讲,就是普通 RPC 加上注册中心的服务发现,唯一需要注意的是调用链路的超时时间一定要比下游的 TTFB 长。我遇到过客户抱怨“请求总是超时”,查到最后是上游设置了 3 秒超时,但下游 Agent 用的模型推理一次就要 4 秒,这已经不是网络问题而是业务层面的超时配置问题。
异步模式是 Agent-Reach 的重点,尤其适合那些“一生成大段文本、处理大文件”的 Agent 任务。我基于 etcd 的 Watch 特性封装了一个简化版任务队列,结构大致如下:
{ "task_id": "task-8f2a9c", "producer": "agent-orchestrator", "consumer": "agent-summarizer", "payload": { "doc_url": "s3://...", "params": { "max_length": 2000 } }, "status": "pending", "retries": 0, "created_at": "2025-06-01T10:00:00Z" }消费者消费任务后必须写回一个完成状态,我把它叫作“回执”。回执上会记录处理开始时间、结束时间、结果摘要以及异常信息。没有回执的任务会被认为投递失败,进入重试队列。
3.2 消息丢失的兜底方案
就算有队列,消息还是可能丢。我遇到过两种情况:一种是生产端把任务写进 etcd 后进程崩溃,还没等消费者拉取,任务就永远停在 pending 状态;另一种是消费者处理完成后写回执时 etcd 集群抖动,回执没写成功,任务被重复投递。
针对第一种,我设计了“孤儿任务扫描器”,每 5 分钟扫一次所有 pending 超过 10 分钟的任务,重新投递。针对第二种,要求消费者在处理逻辑上做幂等——这个非常重要,尤其涉及“写数据库”“发通知”这类操作,一定要用任务 ID 做去重索引。
老实讲,这套设计最初我认为已经“足够可靠了”,直到现实中发生了一次消息重复投递导致 Agent 把同一篇文档向量化入库了三次、用户搜索时看到三个相同结果的问题。从那以后“幂等优先”成了这个项目的铁律,这不是设计上的锦上添花,而是分布式系统生存的底线。
4. 踩过的坑与对应修复策略:Agent-Reach 实际运行中的三场硬仗
从框架跑通到稳定上线,我至少经历了三轮比较硬核的排错。这些坑的根因并不深,但在没踩过之前,光靠看文档和设计图是根本看不出来的。
4.1 心跳风暴:注册中心为何 CPU 飙到百分百
系统刚上线一周,注册中心所在节点 CPU 连续几天在 Scan 状态下跑满。起初我以为是机器配置不够,准备盲目扩容,但在加机器前先抓了一下 goroutine 和网络连接数,发现一个诡异的现象:连接数一直在涨,但 Agent 总数只有 30 个。
顺着排查发现,某个 Agent 的 SDK 心跳逻辑有 bug——它把Heartbeat调用写在了重连循环里,服务端每次返回失败它就越等越短,最后变成了死循环高频发送。这个心跳包又触发了注册中心的续约逻辑,续约的同时要更新内存表里的last_heartbeat,加锁、对比、刷新,形成了典型的活锁。
修复方案分两层:客户端限制心跳最小间隔为 1 秒,连续失败后指数退避而不是疯狂重试;服务端加了一个滑动窗口限流器,单个 Agent 的单位时间心跳次数超阈值直接丢弃并告警。
这个经历让我意识到:分布式系统的很多故障,根源不在设计图上标出的主链路,而在于各种异常路径和自愈尝试偶然撞在一起。心跳这种看起来最微不足道的机制,反而是故障放大效应最明显的环节。
4.2 节点状态不同步:注册信息的“脑裂”
有一次上线 10 个新 Agent 节点之后,老节点频繁报“目标 Agent 不可用”,但注册中心查询明明显示目标节点是健康的。两边对着日志吵了半天,最后发现是两边各连了一个注册中心节点,而新注册的 Agent 信息只被其中一个注册中心接受了。
原因很简单:注册中心组的高可用用的是 Raft 一主多从模式,但客户端 SDK 里的注册逻辑写错了——它向从节点发起了Register请求,从节点返回了自己的状态信息,而客户端没有重定向到主节点,以为注册已经成功。结果主节点上根本没有这条记录。
修复方法很直接:所有写操作必须经过主节点。SDK 收到NOT_LEADER响应时,要根据响应中的 leader hint 自动重新发起请求。同时注册中心的主节点还要支持把最新注册表定期快照推给从节点,这样新接入的客户端不管连到哪台,都能拿到全量的 Agent 信息。
这里我给所有做注册中心类系统的朋友一个建议:一定不要把“写成功”和“生效”混为一谈,写操作的返回必须明确告知客户端“你的数据现在在哪个节点生效了”。
4.3 消息重复触达:任务队列的幂等保卫战
项目跑了两个月后,一位用户反馈他上传了三遍同一份文档,系统竟然生成了三份摘要并发送了三封邮件。抓日志发现是任务队列的分区再平衡引起的:Consumer 节点扩容时,原本分配给旧节点的任务分区会被重新分配,旧节点在关闭前处理了一部分任务但没来得及写回执,新节点接手后又重新消费这些任务。
那段时间我试过直接清空队列、重启 consumer,但问题总是反复出现。后来是跟同事一起在任务表里加了一个processed_flag,消费时先尝试用task_id去更新这个标记,更新成功才算真正拿到任务,否则跳过。
这不光解决了一次线上事故,也让我想明白了一个道理:任务队列本身能保证的是“至少一次投递”,但业务层必须用自己的手段把“至少一次”变成“恰好一次”。对 Agent 场景来说,最常见的恰好一次手段就是任务 ID 幂等表,因为 LLM 推理这类操作天然不可回滚,所以消费端去重是最省成本的办法。
5. Agent-Reach 的扩展玩法:基于能力标签的动态路由
注册中心落地之后,我们又往上加了一层进阶功能:基于能力标签的动态路由。这东西一开始不在计划里,是业务方被逼出来的需求。
他们有一个场景:同一份文本需要做实体抽取、情感分析、语义相似度计算三个子任务,而这些子任务分别由不同团队的 Agent 提供能力。入口 Agent 不可能在代码里硬编码“情感分析找谁”,因为能力集群是动态变化的,新能力上线、旧能力下线都要自动感知。
Agent-Reach 在注册信息中已经带了capabilities字段,所以动态路由做起来不算难。核心逻辑是三步:
- 调用方声明需要的 capability,例如
task_score = "text_embedding"。 - 注册中心从内存表中筛选所有声明了该 capability 且状态健康的 Agent。
- 如果匹配到多个副本,按
rating加权随机选择一个作为目标。
这里评分rating是我埋的一个“伏笔”。我建议每个对外服务能力的 Agent 在上报元数据时带上自己的最近服务质量指标,例如平均响应时间、任务成功率、被用户点赞的次数。路由层根据评分做平滑加权轮询,既能做到负载均衡,又能自动规避那些服务质量突然崩掉的节点。
实际效果怎么样?比如我们的情感分析能力有 3 个实例在用不同的开源模型提供服务,其中一个小模型的准确率近期明显下降,但响应速度极快。如果单纯轮询,用户就可能随机分配到差模型;用 rating 加权后,质量高的模型分到的流量更多,系统整体体验立刻提升。这种方式远比复杂的人工配置要符合 Agent 世界的真实情况——每个 Agent 都是独立的、动态变化的个体,它们互相之间应该靠机制而不是配置来择优协作。
6. 压测实录:一套可以照抄的触达性能基准
光说架构和排错还不够,一个系统能不能真正投入生产,最终还是要看数据说话。我整理了一套 Agent-Reach 的触达基准测试方法和结果,给你做性能评估时参考。
6.1 测试环境与用例设计
测试环境是三台 8C16G 的云主机,一台跑注册中心,两台跑 Agent 节点。Agent-Reach 客户端 SDK 和模拟业务代码部署在同一批主机上,尽量走内网低延迟链路。压测工具用的是 ghz(gRPC 压测工具),直接对同步触达接口打流量。
测试分三组:
- 组一:10 个 Agent 节点,同步请求-响应调用,消息体 1KB。
- 组二:50 个 Agent 节点,同步请求-响应调用,消息体 10KB。
- 组三:100 个 Agent 节点,异步任务投递,单任务负载 100KB。
6.2 关键指标与结果
| 指标 | 组一(10节点) | 组二(50节点) | 组三(100节点异步) |
|---|---|---|---|
| 每秒完成触达数 | 3200 | 2450 | 1180 |
| 触达延迟 P50 | 8ms | 12ms | 20ms |
| 触达延迟 P99 | 42ms | 68ms | 145ms |
| 失败率 | 0.02% | 0.08% | 0.15% |
| 注册中心 CPU | 12% | 35% | 64% |
注意看组三异步模式的延迟比同步高出不少,是因为消息投递包含了进队列、落盘、消费端回执确认三个环节。这个延迟换来的是极高的可靠性,我专门模拟过注册中心节点宕机,异步任务一条没丢。
调优过程中最有价值的一次改动,是把同步触达接口的 JSON 序列化换成了 protobuf。开始我贪图调试方便,直接用了 JSON over HTTP,压测到 2000 QPS 左右服务端 CPU 就到 80% 了。换成 protobuf 之后,同样配置轻松跑到 3000+ QPS,这个优化在规模上来以后是躲不掉的。
7. 从 Agent-Reach 到更广阔的场景:一些可以继续深挖的方向
Agent-Reach 目前的形态已经足够支撑中小规模的多智能体协作平台,但距离我理想中的“智能体互联网”还有不少路要走。以下几个方向是我觉得最有潜力,也最值得投入精力去深挖的。
单从“触达”视角来看,下一个值得突破的点是内容感知的触达决策。现在的路由还是基于能力标签和评分,但实际场景中,用户的一个模糊意图可能需要多个 Agent 协同才能完成。Agent-Reach 未来如果能在触达层做一点语义分发——比如根据请求内容向量去匹配最合适的 Agent——那系统的智能化程度会上一个台阶。
另外我还在计划把 Agent-Reach 和外部事件源打通。目前它假设所有 Agent 都在一个私有的网络信任域内,但实际的智能体生态一定是跨组织、跨平台的。如果能在注册协议里加入“可信网关”的概念,让外部 Agent 通过安全隧道注册进来,同时保留现有的权限控制,整个系统的触达范围就会被放大。
最后是记忆与上下文的触达。这听起来偏应用层,但我发现触达机制如果能把上下文片段随消息一起传递,会大幅减少接收方 Agent 对用户意图的揣摩成本。比如一套文档问答系统里,A 节点已经识别出用户想查“合同违约条款”,把它作为元数据附在消息里,B 节点拿到后就不用重新解析整段对话。Agent-Reach 的消息格式目前已经有metadata段的预留位,我可以顺着这个方向继续扩展。
踩过这么多坑之后我最大的一个体会是:做智能体系统的人,很多时候把精力都扑在模型效果上,觉得提示词写得妙、模型选得强,系统就成功了一半。但真实跑起来你会发现,模型再好,如果 Agent 之间触达不到、配合不起来,一切都是空中楼阁。Agent-Reach 给我的最大启发并不是某个具体的算法突破,而是让我意识到,智能体的“社会性”和“互操作性”才是这类系统从 demo 走向生产要迈过的第一道坎。布局好这张网,后面的生长空间自然会打开。