
凌晨两点我盯着屏幕上乱成一锅粥的日志第七次手动重启了一个失联的子 Agent。这不是个例——过去两个月我一直在维护一个多智能体协作系统调度的 Agent 数量从 3 个涨到 15 个。数量一上来问题全暴露了Agent 之间用硬编码的 IP 地址互相调用地址一变更就全线崩溃健康状况靠调用方自己去猜一个 Agent 假死就能拖慢整条任务链路更别提每次扩容都要人工改配置改完还要追着所有人同步。后来我痛下决心把这块东西重写了。我给它起名叫 Agent-Reach——一句话概括它是一套专门为 AI Agent 协作场景设计的“触达层”基础设施。核心功能就四个Agent 的注册与发现、健康检查与状态同步、请求路由与连接复用、全链路可观测。有了它Agent 之间不再需要互相知道对方的地址只需要知道对方名字系统自动帮你找到可用实例、建立连接、转发请求并实时监控每一次触达的质量。这篇文章我想把 Agent-Reach 从设计、选型到落地的完整过程讲清楚。如果你也在维护三个以上的 Agent 协作系统或者正打算给多智能体应用搭一套可靠的服务间通信底座这篇文章应该能帮你少走不少弯路。我会把架构思路、核心机制的取舍理由、可复用的代码骨架、以及我踩过的那些坑都放出来保证是可落地的东西。1. 先想清楚要解决什么问题1.1 多 Agent 协作的真正痛点在哪很多人在搭智能体系统时第一反应是“让 Agent 直接互相调用”。单机、两三个 Agent 的时候这个方案简单直接确实没问题。但系统一复杂你会发现 Agent 之间的通信根本不是“转发一个 HTTP 请求”这么简单而是需要解决可用性和确定性的问题。我总结下来痛点集中在四个层面。第一是寻址问题。Agent A 要调用 Agent B它得知道 B 的网络地址。在容器化环境里地址是动态分配的重启一次就变了。不是你手写一次配置就一劳永逸的事。第二是健康状态无人管理。Agent B 进程活着不代表它能正常处理请求。可能线程池满了可能依赖的下游数据库连接断了可能模型接口超时。调用方如果只是“发个请求然后等结果”大概率会把几十秒的重试时间浪费在一个已经没救的实例上。第三是调用关系混乱。Agent 数量多了以后谁在什么时候调谁根本没有一张清晰的图。出了问题只能靠翻日志猜。更麻烦的是有些 Agent 之间其实存在循环调用一旦 A 等 B、B 等 A整个系统就卡死。这种问题在无中心化的全互联模式下极难发现。第四是扩展性差。你想加一个 Agent 实例来分摊压力结果要改的是所有调用方的配置还要挑一个低峰期小心翼翼地上线。这在“随时扩容”的云原生理念下是完全不可接受的。Agent-Reach 解决的就是这一整坨问题。它的核心思路很简单把“Agent 之间如何找到对方”这件事从业务代码里抽出来收编到一个统一协调层里去管。1.2 Agent-Reach 的定位与边界在设计之初我就给 Agent-Reach 划了一条明确的边界它只做触达不做编排。也就是说Agent 之间的调用关系、业务流程怎么走、谁先谁后这些属于编排层比如 LangGraph、自研的 workflow engine的职责。Agent-Reach 只保证一件事当你需要触达某一个 Agent 时你能稳定、快速、可观测地触达它。这个边界非常重要。如果不做这个划分很容易把 Agent-Reach 做成一个“什么都能干”的大杂烩既管理流程、又管理状态、还要管理通信最后每个模块都浅尝辄止。Agent-Reach 的组件职责是四个Agent Registry注册中心维护所有 Agent 的实例列表、元数据、健康状态。Reach Gateway触达网关接收调用方的请求按策略分发给目标 Agent 的可用实例。Agent SidecarAgent 伴侣进程部署在 Agent 旁边负责向注册中心上报心跳、拉取配置、启动本地代理端口。Control Console控制台用于查看拓扑、状态、调用链路和各项指标。一句话总结定位Agent-Reach 是整个多 Agent 系统的“通信总线”。所有 Agent 接入总线通过总线互相触达而不是直接私聊。2. 整体设计与技术选型的取舍2.1 架构分层的逻辑Agent-Reach 在设计上分了控制面和数据面两层这和大多数成熟的基础设施是一样的思路。控制面负责“知道谁是谁”。它包含注册中心、健康检查服务、配置管理和控制台。控制面不需要很高的吞吐但对一致性和实时性有要求。数据面负责“把请求送过去”。它包含网关节点的路由逻辑、连接池管理、负载均衡和重试策略。数据面必须高吞吐、低延迟、高可用。为什么要分离因为这两层的扩缩容维度完全不同。控制面可能只需要三个节点组成一个小集群就能服务整个系统但数据面需要跟着 Agent 的接入规模走。如果混在一起要么控制面过度投入浪费资源要么数据面撑不住被聊爆。分离之后两边可以各自独立扩缩容故障也能隔离。实际部署里Agent-Reach 的中心组件Registry Gateway Console是三个独立进程可以部署在同一台机器上用于开发环境也可以分布式部署用于生产环境。每个 Agent 旁边部署 SidecarSidecar 通过本地端口与 Agent 进程通信对外连接全部由 Sidecar 代理。这种部署模型带来的最大好处是Agent 业务代码里不需要引入任何 Agent-Reach 的 SDK只需要把请求地址指向本地的 Sidecar 端口即可。2.2 通信协议选型为什么不用 REST这是我在设计过程中纠结最久的一个点。Agent 之间的通信采用什么协议REST/HTTP 肯定是兼容性最好的几乎任何语言都能实现。但实际压测之后我放弃了“全 REST”的方案。原因是 Agent 协作场景的流量模式和传统 Web API 有所不同。一次 Agent 触达本质上是一个长时间运行的异步任务。比如 Agent A 让 Agent B 做一段文本分析B 可能需要调用大模型耗时十几秒甚至几十秒。这时候如果用普通的 HTTP 请求会发生两种情况一是连接长时间占用导致 TCP 连接池耗尽二是请求超时设定的时间很难拿捏设短了误杀慢任务设长了故障检测形同虚设。Agent-Reach 最终采用了gRPC 双向流 HTTP 兜底的双协议方案。gRPC 双向流负责“长任务型”的 Agent 调用。调用方发起一个请求后可以持续从流中获取阶段性结果——比如 Agent B 正在分析先回传一个“已接收”再回传“正在调用大模型”最后回传“分析完成结果如下”。同时像心跳上报、状态变更推送这种高频小包消息也走 gRPC 长连接省去反复建立连接的开销。HTTP 兜底主要面向两类场景一是 Agent 需要回调外部 Webhook二是开发者需要快速做原型集成不想处理 gRPC 的序列化定义。这两种场景的请求频率都不高HTTP 足够用了。顺带一提Sidecar 本地端口用的是 gRPC所以 Agent 进程需要有一个能发起 gRPC 调用的客户端库。目前主流的语言都有成熟的 gRPC 支持接入成本很低。提示如果 Agent 是 Python 写的推荐用grpc.aio写异步客户端如果是 Node.jsgrpc/grpc-js是官方维护的选择。gRPC 的 proto 定义文件全系统共享建议单独抽一个api/proto仓库来管理。2.3 注册中心选型Etcd 还是自研注册中心是整个 Agent-Reach 的最核心组件它的选型直接决定了系统的一致性模型和运维复杂度。第一版我用了 Etcd。Etcd 的强一致性、lease 机制、watch 机制几乎是为服务发现量身定做的Agent 启动时往 Etcd 写入一个带 lease 的 key然后定时续约其他组件 watch 这个 key 的变更事件就能实时感知实例上下线。生产环境配套 Etcd 集群也很成熟三个节点一套稳如老狗。但后来我发现直接用 Etcd 做注册中心开发速度快但在功能上有几个明显的短板Etcd 没有内置“健康状态分级”的概念。一个 key 存在只代表这个 Agent 的 lease 还活着不代表它真的能处理请求。Etcd 的 watch 是事件驱动的但客户端拿到事件后怎么做负载均衡、怎么做权重调整这些逻辑还是要自己写。Etcd 的扁平 key-value 模型表达力有限。当想查询“所有类型为 embedding 的 Agent”或者“所有在某个命名空间下的 Agent”时要么依赖 key 前缀设计要么在客户端自己过滤。最终Agent-Reach 的第二版把注册中心的“存储底座”仍保留为 Etcd但在底座之上封装了一层注册服务Registration Service专门负责元数据管理、健康状态标记和查询接口。注册服务向 Gateway 和 Sidecar 暴露的是语义明确的 API比如RegisterAgent()、DiscoverAgent()、Heartbeat()、ReportHealth()底层再翻译成 Etcd 的读写。这样既享受了 Etcd 的可靠性又把业务语义和控制逻辑收口在一个服务里。如果你的项目规模较小、Agent 数量不超过 50 个也可以直接用 Redis 做注册中心配合定时写 key 和 TTL 过期机制实现更简单。但请注意Redis 模式下失去的是 watch 的准实时性故障感知会慢半拍需要设计兜底轮询机制。3. 核心机制拆解与关键参数详解3.1 注册与发现不是“上线了”就完事注册与发现是 Agent-Reach 最基础的能力但它并不像我最早想的那么简单。每个 Agent 在启动时需要调用RegisterAgent接口提交自己的实例信息Agent 名称、实例 ID、协议类型gRPC/HTTP、端口、命名空间、标签列表。注册服务收到请求后会先检查这个 Agent 类型是否已经注册过——整个注册过程是一个两阶段流程预注册和确认注册。预注册阶段注册服务把实例状态标记为STARTING然后把信息写入 Etcd。此刻这个实例还不会接收任何真实流量。等 Sidecar 与 Agent 进程完成本地握手、确认 Agent 进程中的业务初始化已经就绪后Agent 再调用ConfirmRegistration接口把状态置为READY此时流量才会放进来。为什么一定要有这个“两阶段”我踩过坑。第一版没有STARTING状态Agent 进程一启动立即注册但此时大模型客户端还没初始化完成线程池还没预热结果 Gateway 在实例刚注册的瞬间就把流量打过来了然后一堆超时。后来我加了确认机制问题才消失。在发现侧Gateway 不会每次请求都去查一次注册中心而是本地维护一份 Agent 实例缓存通过订阅注册服务的变更事件来更新缓存。这样单次触达请求的路径上没有额外的网络往返去看注册中心延迟可以控制在纯转发级别。3.2 心跳与健康检查多维度健康状态机心跳机制解决的是“这个 Agent 还活着吗”的问题但 Agent 协作场景里“活着”要用四个维度来定义进程维度Sidecar 能通过本地网络端口探活 Agent 进程。依赖维度Agent 进程上报自身的依赖健康度比如数据库连接、Redis、大模型 API 的连通性。队列维度Agent 当前正在处理的请求数是否超过阈值。调用维度最近一段时间内Agent 对外调用的错误率是否异常升高。我把这四个维度合并成一个健康状态机分为三档HEALTHY、DEGRADED、UNHEALTHY。四个维度全部正常状态是HEALTHY流量全量发送。任意一个维度异常但进程存活状态是DEGRADED只发送只读类请求或低优先级请求。进程失联或者依赖维度全面崩溃状态是UNHEALTHY立即从路由表中剔除。关键参数可以参考如下参数推荐值说明心跳上报周期5 秒Sidecar 上报本地指标的间隔心跳超时阈值15 秒超过 15 秒无心跳判定失联健康检查周期10 秒注册服务定期访问 Agent 的 health 接口降级触发阈值错误率 10%最近 1 分钟错误率超过 10%状态降级心跳包的默认上报周期我试过 1 秒太频繁了Agent 数量稍多时控制面压力陡增试过 30 秒故障感知又太慢。5 秒上报、15 秒超时是一个比较均衡的配置能保证在 20 秒左右感知到一个 Agent 的异常对绝大多数任务链路的容忍度来说都是够用的。3.3 路由与负载均衡有时候要“惩罚”慢节点Gateway 的路由逻辑是整个系统的“临门一脚”。请求已经来了该发给哪个实例如果同一类 Agent 有三个实例负载均衡算法选谁Agent-Reach 默认采用的是加权最少请求数Weighted Least Request算法。这个算法与经典的 Round-Robin 或随机算法相比最大的优势是它能持续观察每个实例当前正在处理的请求数优先把请求发给“正在忙的请求数最少”的实例。因为 Agent 之间很多调用都是长耗时任务一个实例可能同时挂着好几个大模型请求如果只看轮询不看实际负载很容易把任务塞给已经忙到冒烟的实例。在这个基础上我还加了一个慢节点惩罚机制EWMA-based Penalty。每次触达结束后Gateway 会记录这次调用的耗时。如果某个实例的近期平均响应时间超过全类实例平均值的 1.5 倍它的权重就会被打折连续低质量响应超过 5 次会暂时摘除流量 30 秒等待恢复。这个机制在实践经验里极其有用——Agent 的响应时间方差大偶尔一次慢可能只是模型抽风但稳定变慢十有八九是该实例下游出问题了。3.4 连接复用与背压防止雪崩的关键Agent 协作链路与普通 API 网关的另一个显著区别是一次业务请求会引发 Agent 之间的多级级联调用。比如用户输入到主 Agent主 Agent 调用了 BB 又调用了 C。如果最底层的 C 慢了整个链路的请求会逐级堆积。这个问题的标准解法就是背压Backpressure。在 Agent-Reach 的实现里每个连接都有一个并发窗口concurrency window默认 32。发送方在窗口满时不允许继续发送新请求必须等待响应或者超时。这个设计迫使请求的堆积发生在业务层而不是传输层让 Agent 有机会根据自身负载做拒绝或降级而不是被瞬时流量挤爆。连接池的复用策略也值得一提Agent-Reach 的 Gateway 倾向于复用与侧边车之间的长连接而不是每次请求都新建连接。我们的压测数据是复用连接的 P99 延迟比新建连接低了近 37%而且 CPU 占用显著下降。长连接的空闲保活时间默认 1 小时超过 1 小时无流量的连接会被回收避免过多空闲连接挨个慢慢打补丁。4. 从零搭建一个最小可用 Agent-Reach4.1 环境准备与总体依赖这部分我直接分享一套实验环境可复用的配置。我用的版本号可能后续会变但思路是通用的。准备如下环境Linux 或 macOS 均可Docker 与 Docker ComposeEtcd 镜像3.5 以上Python 3.10用于编写 Agent 原型以及 Node.js 18用于编写 Gateway 原型因为 Node 的异步模型天然适合做这类代理转发。第一步拉一个最小化的 etcd 服务# docker-compose.yml services: etcd: image: quay.io/coreos/etcd:v3.5.16 command: - /usr/local/bin/etcd - --nameetcd0 - --data-dir/etcd-data - --advertise-client-urlshttp://0.0.0.0:2379 - --listen-client-urlshttp://0.0.0.0:2379 ports: - 2379:2379 volumes: - etcd_data:/etcd-data volumes: etcd_data:启动它docker compose up -d etcd我的建议是开发环境直接用 Docker 起的单节点 etcd生产环境至少三节点。其实哪怕是演示项目我也推荐先用标准配置起一个因为后面的注册服务封装中用到的 watch、lease 这些 API 和正式集群是一致的不会有“开发环境能用生产环境用法完全不同”的落差。4.2 注册服务的核心实现注册服务是整个系统的控制面中枢。它负责接收 Agent 的注册请求、写 Etcd、对外提供发现接口。我写一个 Python 版本的核心骨架。# registry_service.py import asyncio import etcd3 import uuid from datetime import datetime, timezone from typing import Optional class AgentRegistry: def __init__(self, etcd_endpoint: str localhost:2379): self.etcd etcd3.client(hostlocalhost, port2379) self._lease_id: Optional[int] None async def start(self): # 创建一个全局 lease用于注册记录的统一过期时间 lease self.etcd.lease(ttl30) self._lease_id lease.id return self async def register_agent(self, name: str, instance_id: str, meta: dict): # 两阶段注册先写 STARTING 状态 key f/agents/{name}/{instance_id} value { status: STARTING, name: name, instance_id: instance_id, registered_at: datetime.now(timezone.utc).isoformat(), **meta } self.etcd.put(key, json.dumps(value), leaseself._lease_id) return instance_id async def confirm_registration(self, name: str, instance_id: str): key f/agents/{name}/{instance_id} existing self.etcd.get(key) if not existing[0]: raise RuntimeError(finstance {instance_id} not in STARTING state) value json.loads(existing[0][0]) value[status] READY self.etcd.put(key, json.dumps(value), leaseself._lease_id) async def update_health(self, name: str, instance_id: str, health: dict): key f/agents/{name}/{instance_id} existing self.etcd.get(key) if not existing[0]: return value json.loads(existing[0][0]) value[health] health self.etcd.put(key, json.dumps(value), leaseself._lease_id) async def list_agents(self, name: Optional[str] None): if name: agents self.etcd.get_prefix(f/agents/{name}/) else: agents self.etcd.get_prefix(/agents/) results [] for value, meta in agents: item json.loads(value) if item.get(status) READY: results.append(item) return results注意几个关键点。lease_ttl是 30 秒也就是说注册服务持有的租约是 30 秒所有注册信息都挂在同一个租约上注册服务进程挂了30 秒后全部注册数据自动过期。但实际的 Agent 实例并不依赖这个租约存活性每个 Agent 的 Sidecar 会以自己的节奏定期调用update_health可以看作是一种“租约内的二次确认”。用这种设计只要注册服务进程活着就能保证数据的活性先底层刷新它死了所有状态自动清空。这看起来比较符合一个中间件的故障模型控制面不可用时数据面保留最后的正确路由表继续工作但不再有新注册和新变化可见。4.3 Gateway 的数据平面实现Gateway 是接受 Agent 调用请求、按策略分发到目标 Agent 的组件。它必须高性能因此建议用 Node.js 或 Go。我给一个 Go 的 gRPC 转发的简化示例重点展示路由逻辑和并发控制。// gateway.go 核心片段 type Gateway struct { registry *AgentRegistryClient connPool *sync.Map // map[string][]*grpc.ClientConn inflight *sync.Map // map[string]int32 (每个实例的正在处理的请求数) } func (g *Gateway) forward(ctx context.Context, targetAgent string, payload []byte) (*Response, error) { // 1. 从本地缓存获取可用实例列表 instances, ok : g.localCache.Get(targetAgent) if !ok || len(instances) 0 { return nil, ErrAgentNotFound } // 2. 用加权最少请求数选择实例 var picked *Instance var pickedScore int32 math.MaxInt32 for _, inst : range instances { if !inst.Available() { continue } inflightCount : g.inflightCount(inst.ID()) weightedScore : inflightCount / inst.Weight() if weightedScore pickedScore { pickedScore weightedScore picked inst } } // 3. 增加该实例的 in-flight 计数执行 RPC完成后恢复计数 g.inflightInc(picked.ID()) defer g.inflightDec(picked.ID()) conn : g.connectionFor(picked) client : NewAgentClient(conn) stream, err : client.Invoke(ctx, InvokeRequest{Payload: payload}) if err ! nil { g.reportFailure(picked) // 速率与错误率统计 return nil, err } // ... 从 stream 中读取最终结果返回 }在 Agent 的 Sidecar 侧它需要实现Invoke的 gRPC 接口。Sidecar 收到请求后将请求通过本地地址转发给 Agent 的业务进程。这里有一个方便的实现技巧Sidecar 可以从注册信息中读取 Agent 本地监听的端口如果 Agent 本身是 HTTP 服务Sidecar 就把 gRPC 请求翻译成 HTTP 转发如果 Agent 本身就是 gRPC 服务Sidecar 就直接变成一层透明代理。// agent_reach.proto syntax proto3; package agentreach; service AgentSidecar { rpc Invoke(InvokeRequest) returns (stream InvokeResponse) {} rpc Ping(PingRequest) returns (PingResponse) {} } message InvokeRequest { string agent_name 1; string message_id 2; bytes payload 3; mapstring, string metadata 4; } message InvokeResponse { string message_id 1; enum Stage { ACCEPTED 0; RUNNING 1; SUCCEEDED 2; FAILED 3; } Stage stage 2; string progress 3; bytes result 4; }为什么要用stream InvokeResponse而不是普通的InvokeResponse前面提过Agent 协作的长任务特性让“阶段性进度反馈”变得很有价值。上游 Agent 可以实时知道下游任务进展一方面可以用来排查“卡在哪”另一方面可以提前做超时策略决策比如某个阶段明显卡住可以提前取消或降级。4.4 接入一个最小 Agent 起 Demo现在我们把整个流程走通。我的演示 Agent 是一个“文本增强 Agent”它对外提供一句话的改写服务内部去调用本地的一个 mock HTTP 服务模拟大模型延迟。Agent Sidecar 的启动流程Sidecar 启动后尝试与本地 Agent 进程端口建立探活连接。调用注册服务的register_agent接口提交实例信息。等待 Agent 进程的 readiness 检查通过。调用confirm_registration状态置为 READY。建立到 Gateway 的长连接开始接受流量。下面是一个 Python 写的 Sidecar 启动脚本的骨架# sidecar.py import grpc import agent_reach_pb2 as pb2 import agent_reach_pb2_grpc as pb2_grpc class SidecarService(pb2_grpc.AgentSidecarServicer): def __init__(self, agent_port: int): self.agent_port agent_port self.http_client httpx.AsyncClient() async def Invoke(self, request, context): # 阶段1已接收 yield pb2.InvokeResponse( message_idrequest.message_id, stagepb2.InvokeResponse.ACCEPTED, progressrequest accepted ) # 调用本地 Agent 的 HTTP 服务 async with self.http_client.stream( POST, fhttp://127.0.0.1:{self.agent_port}/infer, contentrequest.payload ) as response: # 阶段2运行中 yield pb2.InvokeResponse( message_idrequest.message_id, stagepb2.InvokeResponse.RUNNING, progressinference started ) result await response.aread() # 阶段3完成 yield pb2.InvokeResponse( message_idrequest.message_id, stagepb2.InvokeResponse.SUCCEEDED, resultresult )这里一个容易踩坑的点gRPC 的 server 默认最大发送消息大小是 4MB。如果你传输的 payload 中带着大段文本、图片 embedding 甚至二进制数据迟早会撞到这条限制。在构造 Sidecar server 时务必要把options[(grpc.max_send_message_length, 100 * 1024 * 1024)]这类参数加上。跑通之后的效果调用方只需要知道“文本增强 Agent”这个名字不用关心它在哪个 IP、哪个端口、有几个实例。Gateway 会从注册中心拉到可用的实例集合选中一个把请求送过去侧边车再带着 Agent 进程把脏活干完结果原路返回。整个过程对调用方暴露的只是一个 gRPC 流式接口。5. 运维中的坑与排查心得5.1 Agent 假死健康检查的盲区即使有了心跳机制和健康状态机Agent-Reach 在真实运行中仍然遇到过一个让我头疼很久的问题——Agent 明明心跳正常但请求全部超时。排查后发现这类 Agent 实例的进程还活着心肺还在跳但内部依赖的数据库线程池已经卡死。传统心跳只检查进程存活发现不了这个。后来上面的多维健康检查方案落地上线后这个坑被填上了但具体的处理还是要在业务侧配合。我现在的做法是Agent 网络中每个 Agent 实例都要暴露一个/healthHTTP 端点里面不仅要返回“OK”还要返回关键依赖的连接状态。健康检查返回体建议如下{ status: degraded, detail: { database: ok, redis: timeout, model_api: ok, inflight: 18, max_inflight: 32 } }注册服务拿到这个返回体后会更新该实例的健康状态。如果status是degradedGateway 会立刻减少发往该实例的流量权重并通过一段时间观察它是否能自行恢复。5.2 连接池耗尽引发的级联超时有一次压测时我模拟了 10 个 Agent 同时触发文本增强服务。因为测试脚本一次性发出大量请求Gateway 的并发窗口瞬间被占满后续请求全部阻塞在连接池的等待队列里。由于阻塞调用方的超时时间先到于是主动断开但这在 Gateway 侧会留下一个“半死”的请求上下文——虽然客户端不等待了但 Agent 还在继续处理白费资源。这个问题背后是一个普遍的设计缺陷在级联调用链路中超时时间必须逐级递减而不是统一设置相同的超时时间。比如调用方超时 60 秒Gateway 转发超时应该是 50 秒Sidecar 本地转发应该是 45 秒Agent 内部业务逻辑的超时应该是 40 秒。留出逐级缓冲才能避免“上层等下层下层等更下层”集体超时的情况。现在 Agent-Reach 的所有组件统一从配置中心读取超时参数形成一份超时配置表层级超时时间说明调用方应用60 秒用户可感知的最长等待Gateway 转发50 秒包含排队和网络传输Sidecar 探活45 秒本地请求允许的最长时间Agent 业务逻辑40 秒内部处理、依赖调用等5.3 注册数据的 cache stampede当 Gateway 本地缓存过期后如果同时涌入大量请求要触发回源刷新注册服务会瞬间被高并发的读请求打满。这就是典型的缓存击穿Cache Stampede。我的解法是给 Gateway 的本地缓存加的“singleflight”逻辑同一时间、同一个 Agent 名称的缓存刷新只有一个请求会真实打到注册服务其他请求直接等待这个请求返回并共享结果。这样即使有 100 个并发请求同时发现缓存过期注册服务也只会收到一个查询请求。另外缓存过期时间也做了差异化设计正常注册数据的本地缓存 TTL 是 30 秒但健康状态数据 TTL 是 5 秒。也就是说路由表允许拉取稍旧的但健康状态必须更敏感这是“可用性优于一致性”的取舍。5.4 Agent 重启时的流量短暂黑洞Agent 实例在重启过程中注册中心里的记录还在租约没到期但实际端口已经不听使唤。在这个窗口期Gateway 依然会把请求分给这台重启中的 Agent。要解决这个问题最可靠的办法是让 Agent 进程在退出前主动发一条DeregisterAgent请求。但如果是被 kill -9 杀死这条请求根本没机会发出去。更保险的兜底方案是依赖健康状态机。Sidecar 在 Agent 进程停止的瞬间本地 TCP 连接会立刻断开因为端口监听没了Sidecar 会探测到这一事件立即把本实例的健康状态改为UNHEALTHY并上报。这样 Gateway 最快可以在 1 秒内把这个实例从路由列表中摘除。为了稳妥起见今年每次发布版本我都强制要求 Agent 使用这个优雅退出流程先注销、再停止端口监听、最后退出进程这个流程应该写进团队的部署规范里。6. 最后再聊点有用的Agent-Reach 做到现在最大的感受是多 Agent 系统里通信的问题永远比模型能力的问题更早暴露。如果你准备做一个多智能体产品不要等 Agent 数量多了再补基础设施。哪怕只有两三个 Agent我建议你也按 Agent-Reach 的模式跑——把注册发现、健康检查、链路观测这几个能力预留出来。因为一旦系统进入生产环境流量真实起来再去补这套能力牵涉到的就不只是技术重构还有各种历史包袱代码里的硬编码地址、约定好的内部 API、部署脚本里的 IP 白名单……改起来会非常痛苦。两阶段注册、多维健康检查、逐级超时配置、local cache 的 singleflight——这四个设计是我在实际运行中被逼出来的方案也都经过了压测和线上验证。在我的场景里Agent 协作的异常恢复时间从最初的十几分钟缩小到了几十秒。现在每次扩容新增一个实例注册中心自动发现Gateways 自动开始分发流量整个过程不需要人为修改任何调用方配置。下一步我还有几个可以继续做的事把 Routing 策略升级成基于响应延迟的动态权重、把链路追踪从转发日志升级成原生的 OpenTelemetry 集成、以及补充一个跨数据中心的注册中心同步机制。这些方向都可以在 Agent-Reach 这套骨架上继续长出来。如果你也在做 Agent 间通信的基建欢迎从这套设计里挑几个思路去试。至少在我看来这套路子没有走歪。