Agent-Reach:为多Agent协作打造动态注册、能力路由与通信基础设施

发布时间:2026/10/7 6:45:26
Agent-Reach:为多Agent协作打造动态注册、能力路由与通信基础设施 搞AI Agent智能体这段时间我遇到一个很实际的问题当你有好几个Agent需要互相协作时怎么让它们找到彼此一开始我靠手动配置把对方的IP和端口写死在代码里结果地址一换、实例一迁移所有调用链全断。后来我琢磨出一套专门面向Agent的注册、发现、寻址与触达方案给它起名叫Agent-Reach。这套方案做的事很简单让Agent之间能动态互相发现、按能力路由、安全地建立连接。如果你也在做多Agent编排、想给自己的LLM应用加一层协作基础设施或者单纯想看看Agent之间怎么通信比较靠谱这篇分享应该对你有用。1. 为什么我决定自己写一套Agent寻址方案1.1 没有Agent-Reach之前的协作痛苦最早做Agent协作的时候我天真地以为两个Agent之间互相调用和两个微服务之间调用没什么区别。服务A调用服务B把B的地址配到A的环境变量里然后HTTP调用就完事了。然而真正跑起来才发现问题一个接一个。首先是地址会变。我的翻译Agent跑在本地Docker里文档读取Agent跑在云主机上今天调试时用的内网IP明天重启后就换了。改配置容易但改完之后所有依赖它的Agent都要跟着改这已经是一场噩梦。其次是能力识别不清晰。同一个Agent可能同时具备翻译、摘要、情感分析三种能力调用方里只看到一长串接口文档根本不知道哪个Agent能处理把中文翻译成英文这个意图。我一度在调用方硬编码了一堆if-else判断代码丑到没法看。第三是网络拓扑复杂。有的Agent在NAT后面有的在防火墙内侧有的只有出网权限没有入网端口想让它们之间直接互联根本不可能。1.2 从服务发现到Agent发现缺的不只是名字传统的服务治理方案我也考虑过Nacos、Consul、ZooKeeper都摸了一遍。它们解决的服务发现问题本质上是把服务名映射到实例地址。这套机制非常成熟但它解决的是机器与机器之间的寻址到了Agent场景反而不够用。原因很简单。第一Agent的实例是会漂移的同一个Agent可以今天在本地跑明天被调度到另一台机器甚至同一个逻辑Agent同时存在多个副本。第二Agent之间协作更看重的是能力而不是名字调用方想找的是会做翻译的Agent而不是叫做translator的那个进程。第三Agent是有会话状态的两个Agent之间的连接一旦建立通常要维持一段时间期间还会有流式消息推来推去这比普通HTTP请求-响应要复杂。传统注册中心里存的是一个服务名对应一组IP而Agent场景里需要的是意图参数对应一组能力实例还要考虑每个实例的负载、路由权重、认证方式。名字只是其中很小的一部分。1.3 Agent-Reach的设计目标注册、发现、寻址、触达想清楚痛点之后我给Agent-Reach定了四个核心目标。注册Register每个Agent上线时主动向注册中心登记自己的身份、能力列表、可用地址、协议类型和认证方式。不是我手动写配置是Agent自己报上来。发现Discover调用方描述自己想要的能力由路由层去匹配对应的Agent实例列表。匹配的不只是名字还包括能力参数、语言、协议、地理位置等属性。寻址Address在多实例、多网络环境的情况下找到一条让调用方真正能连通候选Agent的路径。直连不通就中转中转不通就换副本这一步是决定Agent之间能不能真正互相对话的关键。触达Reach建立连接后能维持稳定的通信通道支持流式数据、心跳保活、断线重连直到任务完成。这四步合起来才算是一个完整的Agent间通信闭环。前面说的传统注册中心只覆盖了注册发现的一部分寻址和触达都得自己补。2. Agent-Reach的核心机制一个类似DNS但比DNS聪明得多的寻址层2.1 从名字到地址再到连接Agent-Reach的三层架构Agent-Reach内部拆成三个模块注册中心Registry、路由引擎Router、触达网关Reach Gateway。它们各管一段串起来就是完整链条。注册中心负责持久化所有Agent的元数据包括Agent ID、能力描述、当前状态、路由权重、认证信息。这个模块本质上是一个带TTL的键值存储我基于SQLite加内存缓存实现单机可以扛几千个Agent节点注册不需要专门引一套分布式存储进来。路由引擎负责匹配意图。调用方把我想翻译一段中文到英文这个请求丢给RouterRouter去Registry里查所有声明了翻译能力且支持zh到en方向的Agent再根据负载和网络可达性做排序最终返回一两个候选地址。触达网关负责解决连不上的问题。如果某个Agent只在内网里外部调用方的数据包根本进不去那么Reach Gateway会作为中转站把调用方的请求转发给这个内网Agent。这个网关我设计成无状态服务可以水平扩展只要请求量上来多起几个实例就好。2.2 能力描述与元数据格式给Agent写自我介绍Agent-Reach最关键的一个设计是能力描述格式。我参考了OpenAPI和JSON Schema的思路定义了一套JSON元数据格式。每个Agent注册时都要提交一份自我介绍里面声明自己是谁、能干什么、怎么联系它。{ agent_id: agent-translator-v3, name: 通用翻译Agent, version: 3.2.1, capabilities: [ { type: translate, languages: [zh, en], direction: [zh-en, en-zh, zh-ja], latency: low, concurrency: 10, params: { style: [formal, casual, technical] } }, { type: summarize, language: zh, max_input_length: 8000 } ], reach: { protocol: grpc, endpoint: agent-translator-v3.internal:50051, auth: jwt, use_tls: true, weight: 3 }, liveness: { ttl_seconds: 30, last_seen: 2025-01-15T12:00:00Z } }字段之间是环环相扣的。capabilities是给Router做匹配用的它描述能力类型、参数、约束Router会把调用方的请求意图和这些字段做比对。reach字段是给Reach Gateway做连接用的协议、地址、认证方式都在这里决定怎么把数据包送过去。liveness是给注册中心做清理用的超过TTL没心跳的Agent直接标记为下线。这套格式我调整过好几轮最大的变化是加了weight字段。一开始我以为只要能力匹配就能选出Agent后来发现翻译Agent有多个实例时必须考虑负载分配否则所有请求都打到一个实例上直接把那个Agent打挂。2.3 心跳与生命周期管理为什么用短TTL而不是显式注销Agent的上下线不能完全依赖Agent进程主动发我要下线了这个消息原因很现实进程可能被kill -9可能是断电可能是网络分区总之消息根本没发出去。所以Agent-Reach采用租约lease机制类似于Etcd的TTL方案。每个Agent注册后默认获得一个30秒的租约之后Agent必须每10秒发送一次心跳续租。注册中心只要在30秒内没收到心跳就直接把这个Agent标记为offline状态不再参与路由匹配。这套机制有好处也有代价。好处是健壮Agent突然崩溃也不会留下僵尸节点。代价是心跳频率和TTL的设置需要仔细调调大了反应慢调小了网络抖动就误杀。关于这个我后面专门写一节踩坑过程这里先卖个关子。对比一下Agent-Reach和传统注册中心的做法Nacos有临时实例健康检查通常是服务端主动探测客户端Consul是客户端主动发心跳服务和Agent-Reach更接近。但Agent-Reach额外做了能力级别的健康检查不只是检查进程活着还会检查Agent的某个能力是否还能正常工作。比如翻译Agent进程活着但翻译能力模块挂了我在心跳响应里会标注当前可用的能力列表Router就能避开异常能力。2.4 一次完整的寻址和调用过程把上面的概念串起来一次调用大概是这样的流程调用方Agent把请求发给Agent-Reach的SDKSDK内部生成一个RouteRequest携带意图描述。Router收到后解析意图比如translate然后提取参数zh-en、formal。Router去Registry查询所有声明了translate能力的Agent元数据先过滤掉不在线实例。Router对候选Agent做打分打分依据包括能力匹配度、当前负载、网络拓扑距离、历史调用成功率。Router返回排序后的候选列表正常情况下返回第一个。调用方SDK拿到目标Agent的reach信息如果是直连模式直接用gRPC发起调用如果直连不通走Reach Gateway中转。调用完成后SDK向Agent-Reach上报本次调用的延迟、成功率、错误码这些数据会回流到Router的打分模型里。我实测下来这整套流程单次额外延迟不到5毫秒因为Router本质上是内存索引查询不涉及分布式事务。相比它解决的问题这点开销完全可以接受。3. 从零搭起一套最小可用的Agent-Reach实例3.1 环境准备与启动注册中心Agent-Reach我打包成了单二进制文件加上一个可选的管理Web界面部署非常简单一个进程就是一套完整的控制面。当前最新版本是0.9.2支持Linux和macOSWindows下面WSL也能跑。# 下载并解压 wget https://github.com/agent-reach/releases/download/v0.9.2/agent-reach-linux-amd64.tar.gz tar -xzf agent-reach-linux-amd64.tar.gz cd agent-reach # 初始化配置 mkdir -p /etc/agent-reach cp config.example.yaml /etc/agent-reach/config.yaml # 启动注册中心 ./agent-reach server --config /etc/agent-reach/config.yaml配置文件里有几个关键项需要调整。bind_addr是API监听地址我建议固定成本机内网IP而不是0.0.0.0安全一点。heartbeat_ttl对应前面说的租约时间我默认设30秒如果网络环境比较恶劣可以放宽到60秒。data_dir是元数据持久化目录生产环境务必挂到独立磁盘上别和系统盘混一起。server: bind_addr: 0.0.0.0:7080 heartbeat_ttl: 30 data_dir: /data/agent-reach enable_admin_ui: true registry: default_namespace: default enforce_auth: false reach_gateway: enabled: true listen_addr: 0.0.0.0:7081 max_connection_idle: 120启动之后看日志里有registry ready字样就说明成功了。管理界面默认跑在7080端口的/ui路径上浏览器打开就能看到Agent列表和心跳状态。3.2 注册你的第一个AgentAgent接入Agent-Reach需要用一个很小的SDK。我写了Python、Go、TypeScript三个版本的客户端库这里用Python演示。SDK做的事情其实就三件构造元数据、启动心跳协程、暴露一个接收回调的入口。from agent_reach_python import AgentReachClient, Capability client AgentReachClient( registry_urlhttp://127.0.0.1:7080, agent_idagent-reader-v1, namespacedefault, ttl30, ) client.add_capability(Capability( typefile_read, params{max_file_size_mb: 50, formats: [pdf, docx, txt]} )) client.add_capability(Capability( typeextract_table, params{from_formats: [pdf]} )) client.register()register执行后SDK会和注册中心建立一条持久连接然后每10秒发一次心跳。心跳包里会带上当前Agent的能力快照这样即使进程运行过程中能力发生了变化注册中心也能及时感知。注册完成后去管理界面就能看到这个Agent显示为online状态能力列表完整展示。这里有个小细节同一个Agent可以注册多个命名空间比如开发环境的Agent注册到dev命名空间生产环境的注册到prod命名空间两边互不干扰。后面我建议用环境变量区分而不是写死在代码里。export AGENT_REACH_NAMESPACEdev ./my_agent3.3 通过Agent-Reach发起一次跨Agent调用注册好两个Agent之后比如A能读取文件B能做摘要我现在想让它们协作完成一个任务读一个PDF文件然后生成摘要。调用方代码里不需要知道B的地址只需要描述我要一个能做摘要的Agent处理这段文本。from agent_reach_python import AgentReachClient client AgentReachClient(registry_urlhttp://127.0.0.1:7080) # 按能力发起调用不需要指定IP response client.invoke( capabilitysummarize, payload{text: extracted_pdf_text}, params{language: zh, max_length: 200}, timeout30.0, ) print(response.result)invoke方法内部会先走Router做能力匹配拿到候选Agent后建立连接。底层的负载均衡默认是加权轮询如果某个Agent连续上报失败Router会自动降低它的权重这个机制对不可靠的Agent节点非常友好。第一次跑通整个流程的时候说实话我心里是有成就感的两个Agent之间完全通过能力描述完成协作中间没有任何硬编码地址逻辑健康了很多。3.4 为什么通信层选用gRPC而不是HTTPAgent-Reach的Agent间通信协议我选了gRPC后来也支持了WebSocket和HTTP/1.1但默认推荐还是gRPC。选它的原因有三条都踩在Agent场景的痛点上。第一双向流非常必要。Agent协作过程中最长遇到的就是长任务A让B做深度分析B可能要跑几十秒甚至几分钟中间还有进度回报。gRPC的server-streaming和bidirectional-streaming天生支持这种模式HTTP轮询做起来很别扭。第二连接复用效率高。gRPC基于HTTP/2一个TCP连接上可以并发多个请求流这对Agent之间高频低频混合调用的场景很合适省掉了反复建连的开销。第三protobuf的schema约束很适合能力对接。Agent的能力就是一个接口契约用.protobuf文件定义后双方Server和Client由同一套代码生成天然避免了字段名拼错、类型对不上这类低级错误。我也遇到有人说gRPC调试不方便主要是网络抓包看不懂二进制。这个确实存在我的做法是Agent-Reach在debug模式会自动开一个HTTP/1.1的兼容端口同样的API也可以用curl调试开发体验拉满。4. 我踩过的四个典型坑完整的排查链路4.1 NAT穿透Agent注册的地址外部根本不可达第一个大坑是Agent在两台内网机器上互相发现并调用时的路由问题。现象很蹊跷Agent-A和Agent-B都能在注册中心上看到对方是online状态但A调用B时总是超时。排查过程我是这么走的。先在A所在的机器上telnet B注册的地址端口结果显示connection timed out。这就证明问题出在网络层而不是应用层。然后我去B的机器上看网络配置发现B的endpoint写的是10.17.x.x这个内网地址而A在不同的网段数据包根本到不了B。这个问题的本质是Agent注册的地址是从本机网卡探测出来的在内网环境里这个地址只有同一网段的机器能访问跨越网段就失效了。解决方案是给Agent-Reach加上了Reach Gateway中转能力。具体做法是内网Agent启动时主动向Reach Gateway建立一条反向的长连接用WebSocket或QUICGateway记录这条隧道的ID和对应Agent ID。外部调用方要访问这个内网Agent时先查路由判断直连是否可达不可达就交给GatewayGateway把请求通过隧道转发给内网Agent再把响应原路返回。这套机制跑通之后我的Agent网络一下子从仅限同一局域网扩展到了只要能连上Gateway任何地方都能触达。代价是多一跳网络延迟实测本地环境大约增加2-4ms可接受。4.2 心跳参数设置不当引发的大规模路由抖动第二个坑比较隐蔽发生在一次网络波动之后。当时我把heartbeat_ttl设成90秒心跳间隔设成30秒。本来想着放宽参数免得Agent被误杀结果某天机房网络抖动了几分钟恢复之后整个路由表错乱了大量Agent被反复标记为offline再onelineRouter不断地重算路由调用成功率暴跌。我一开始以为是注册中心出Bug了翻了一天代码也没找到问题。后来把Agent日志和注册中心日志对上时间线才发现是我的心跳参数组合不合理。对TTL机制来说最怕的不是TTL太长而是心跳间隔和TTL的比例失衡。30秒心跳、90秒TTL意味着只要连续3次心跳丢失就会被判死。网络抖动那几分钟里Agent的心跳包全都超时但TTL还没到于是恢复后的第一批心跳一到达又发现Agent还活着立刻恢复online。要命的是几个Agent恢复时间不一致路由表就在这一分钟内反复抖动。后来我把参数改成心跳10秒、TTL30秒抖动的概率大大降低。但真正根治是靠心跳抖动保护逻辑如果Agent的心跳断断续续但从未超过2倍TTL注册中心只会降低该Agent的路由权重不直接判offline。权重降到0的Agent不会接收新请求但不会被移出路由表一旦心跳恢复就自动恢复权重。参数参考表正常情况下心跳10秒、TTL30秒公网环境链路质量差时改成心跳15秒、TTL45秒极端弱网环境心跳30秒、TTL120秒。关键是TTL不要小于心跳间隔的3倍否则网络稍微波动就是连锁反应。4.3 能力描述版本不一致导致的路由错配第三个坑和Agent升级有关。我的翻译Agent从v2升级到v3能力从只支持中英互译扩展到了还支持中日互译。我把新代码部署之后所有调用方都还是通过旧的能力元数据在路由导致大量我想把中文翻译成日文的请求被分发到旧实例上然后得到不支持该语言的错误。定位过程费了点劲。我先确认新Agent已经成功注册管理界面上能看到新语言能力。再看路由结果发现Router还是返回旧实例。再查注册中心里的元数据发现确实还是v2版本。问题的根源在于我的心跳包只发送Agent ID和状态没有传送最新的元数据快照。Agent能力升级后元数据没有同步到注册中心旧数据被一直沿用。Agent-Reach后来加了一个能力版本协商机制API元数据里带schema_version字段Agent升级后主动触发一次数据变更通知把最新的能力描述推到注册中心Router端对同一Agent的新旧能力版本做diff发现有变化就立即刷新路由缓存而不是等下一次心跳自然刷新。同时调用方SDK也可以选择带上最小能力版本号Router会把版本低于预期的Agent过滤掉防止把请求打到能力太旧的实例上。4.4 连接泄漏与优雅退出Agent被杀但连接没断最后这个坑是长时间压测时发现的。我起了一台压测机用Agent-Reach连续调用文件读取Agent几百次跑了一段时间后发现Reach Gateway的进程内存一直涨文件描述符数也在涨。查了一下很多到Agent的连接处于CLOSE_WAIT状态一直没有关闭。原因有两个。一是Agent进程被测试脚本用kill -9强杀后没有机会给服务端发FIN包TCP连接就一直挂在CLOSE_WAIT。二是gRPC的keepalive参数没有配好服务端默认真空闲120秒才主动探测压测期间Agent频繁崩溃堆积几个小时后连接数就很可观了。解决措施分三层。第一层在Agent侧注册了一个优雅退出的信号处理器捕获SIGTERM后先注销Agent再发送GOAWAY帧告诉对端我要下线了然后关闭连接。第二层在Reach Gateway侧加上了对半开连接和CLOSE_WAIT连接的主动扫描凡是空闲超过90秒的连接直接关闭。第三层在SDK侧gRPC的keepalive_time设成20秒keepalive_timeout设成5秒每20秒探测一次对端是否存活连续探测失败就立刻断开连接。这三层加完压测时连接数稳定在一个合理区间内再也没有内存和FD的异常增长。5. 从能用变可靠安全加固和高可用经验5.1 默认裸奔状态下的风险Agent-Reach最初我只在乎功能没怎么考虑安全默认配置下所有API都是明文通信、无鉴权、任何进程都能注册任意Agent。这在实验室环境没问题放到生产环境就是裸奔。想象一下如果某个内网服务被攻破攻击者可以直接往注册中心伪造一个Agent把自己的元数据注册进去然后所有调用方都会把敏感数据发到这个伪造节点上。用一句话描述这个风险Agent之间的通信很多都携带着业务数据甚至用户隐私一旦链路不可信整个Agent网络都在裸奔。5.2 第一层加固给注册中心加Token鉴权最紧急的是给注册和发现流程加鉴权。Agent-Reach支持JWT模式注册中心启动时生成一个master tokenAgent注册时要用这个token换自己的专属token。换到的token里带着Agent ID和命名空间信息后续所有心跳、更新操作都必须附带这个token注册中心校验通过才处理。# 在Agent启动时用master token换取专属token export AGENT_REACH_MASTER_TOKENxxxxxxxx export AGENT_REACH_AGENT_IDagent-translator-v3 ./agent-reach client换成专属token之后没有token的进程就无法注册这个Agent也无法冒充其他Agent的心跳。加上之后路由表和元数据就干净了很多至少能在日志里看到谁在什么时候做了什么操作。5.3 第二层加固用双向TLS加密Agent间通信鉴权解决的是谁能注册TLS解决的是通信内容不能被偷看和篡改。Agent-Reach支持mTLS即调用方和目标Agent各自持有一份证书握手时互相验证身份之后的数据全部加密传输。证书签发我这里直接用内部CA每个Agent一个证书证书里带有Agent ID。这样即便某个Agent被攻破其他Agent也可以拒绝和它通信因为是mTLS下证书身份不匹配。配置mTLS时有个小坑证书有效期管理。Agent会频繁重启、扩容每次生成新证书意味着要更新证书文件。后来我搞了个自动续期Agent启动时检测证书剩余有效期如果少于7天就自动向CA申请新证书。这套流程跑了一段时间没出过问题。5.4 多注册中心架构下的数据一致性取舍Agent网络规模到几百个节点以上时单注册中心就成了瓶颈和单点。Agent-Reach支持多注册中心部署但这里需要做取舍。我试过强一致方案数据同步用类似Raft的协议效果稳定但是复杂度和延迟都上来了。后来我评估了实际场景发现Agent元数据属于典型的读多写少、容忍短暂不一致的数据。路由表差几秒更新不会有致命问题但注册中心宕机10分钟不能接受。所以我最终选择双写双读的主备模式主注册中心接收所有变更异步同步到备中心备中心平时也提供服务一旦主中心失联备中心自动提升为主。这套方案没有Raft那么严格但实际可用性非常高切换时间控制在3秒内对Agent路由的影响几乎感知不到。Agent-Reach在这里的安全策略我做成了三种模式可以根据场景选public模式完全开放适合本地开发token模式做基础鉴权适合内网生产mtls模式做双向验证适合跨公网部署。模式之间切换只需改配置不用改代码建议直接上mtls。6. 这个项目还能往哪里延伸6.1 命名空间与多租户隔离我在Agent-Reach里引入了命名空间本质上是把元数据仓库按逻辑环境隔离。比如一个团队可以有dev、staging、prod三个命名空间每个命名空间里都有翻译Agent但三者互不感知。这个设计在做多租户隔离时很重要如果你们公司有多个业务线每个业务线都有一堆Agent不放Namespace的话它们会聚在一个池子里互相发现出现大乱斗。有了命名空间路由规则也变成命名空间内优先、跨命名空间需要显式授权控制力强很多。6.2 从按名字路由到按意图路由Agent-Reach现在的路由核心是基于能力字段匹配已经在往意图路由靠了。下一步我想做的是真正的语义路由不是匹配能力类型translate而是让Router理解一段自然语言表达比如把这篇文章总结成三个要点发给运营Router会自动选择调用摘要Agent再根据运营这个上下文选一个发送渠道Agent。这个方向要接一个轻量级的意图理解模型把用户请求映射到Agent能力图上。听起来炫酷但难度不在模型而在路由结果的置信度判断和失败兜底。我现在的思路是先做成意图路由人工确认的半自动模式跑一段时间积累数据后再逐步提高自动决策比例。6.3 为存量Agent生成连接适配器Agent-Reach目前要求Agent通过SDK接入但很多存量Agent根本没法改代码比如某些老系统只暴露HTTP接口或者用别的语言写的没法装SDK。我后来给Agent-Reach加了一个适配器代理功能在外部启动一个Agent-Reach代理进程代理负责和注册中心通信、维持心跳然后把收到的gRPC请求转成HTTP请求转发给存量Agent。这个代理只用改配置文件就能挂载任意存量HTTP服务不用改一行Java代码。我在真实项目中用这个方式接入了一个老版本OCR服务跑得很稳算是Agent-Reach里性价比最高的功能之一。如果你也打算给Agent之间搭一张互相找得到、够得着的网络Agent-Reach这套思路可以直接拿来用。先跑通最小实例再逐步加上鉴权、TLS和中转网关一套Agent协作基础设施就成型了。