Agent-Reach:轻量级Agent注册中心与路由网关实践

发布时间:2026/10/28 14:38:26
Agent-Reach:轻量级Agent注册中心与路由网关实践 1. Agent-Reach 是什么从“Agent 各自为战”到“全局可达”1.1 我为什么折腾这么个东西前几个月我们团队在做一套多智能体系统业务场景是让不同类型的大模型 Agent 协作处理工单一个负责语义理解一个负责查询库存一个负责调用外部 API还有一个负责生成回复。一开始大家各写各的每个 Agent 都对外暴露一个 HTTP 接口互相之间硬编码调用关系。加了三个 Agent 之后调用链已经乱成一团。新增一个节点要改四五个地方某个 Agent 偶尔抽风还得靠人肉盯着日志去查。我当时的想法很简单能不能有一种方式让所有 Agent 都能被“找到”、被“调用”同时还能看清现在哪些 Agent 在线、各自能干什么。后来我花了大概两个周末写了一个非常小的框架内部代号就叫 Agent-Reach。Reach 这个词取的是“触达、覆盖、可达范围”的意思。我不想要什么宏大概念只想解决一个现实问题让 Agent 之间能互相发现、按需路由、稳定调用同时让整个网络的状态对运维人员透明可见。最开始我只把它当玩具。直到有一天同事问我“这个节点怎么接入”我发现我只需要告诉他一个注册地址、一个能力描述文件他改三行配置就能跑起来。那一刻我才意识到这东西的价值不在代码量而在模式本身。1.2 Agent-Reach 解决的核心矛盾现在市面上的 Agent 框架很多LangChain、AutoGen、CrewAI 都很好用。但如果你真正要在生产环境跑超过三个 Agent你一定会撞上四个脏活服务发现A 怎么知道 B 在哪儿全靠配置文件写死 IP还是靠环境变量猜能力描述A 知道 B 存在但 B 能干什么、参数长什么样、调用后返回什么格式如果这些全靠口头沟通代码里一定到处是硬编码。动态路由同一种能力有两个 Agent 都能做今天其中一个挂了请求能不能自动切到另一个流量能不能按权重分摊状态可见某个 Agent 已经死循环半小时了你是靠监控报警知道的还是靠用户投诉才知道的Agent-Reach 的出发点就是把这四个问题抽象成一套极简协议。它不绑定任何大模型平台也不限制 Agent 的实现语言只要你能提供一个 HTTP 服务并且遵循一个统一的注册规范就能加入这个网络。本质上它做的是一个轻量级的“Agent 注册中心 能力路由网关”。我要说明白这绝对不是一个替代 LangChain / AutoGen 的东西。它解决的是这些框架不会替你解决的底层问题。你完全可以在 LangChain 里写业务逻辑然后通过 Agent-Reach 把 Agent 暴露出去、接进来。1.3 适合谁用不适合谁用先说适合的人。你自己有多个 Agent 在跑比如一个对话 Agent、一个工具调用 Agent、一个 RAG Agent你想让它们互相配合但不想引入 Kafka、ZooKeeper 这种重家伙。或者你是一个小团队想搞一个内部 AI 服务总线让不同成员开发的 Agent 能平滑接入。又或者你只是好奇“Agent 网络”怎么搭想找个最小实现开脑洞。这些场景都很合适。不适合的人如果你只有一个 Agent或者你的 Agent 们都在同一个进程里通过函数调用相互协作那真的没必要用 Agent-Reach。杀鸡用牛刀还多了一层网络开销。另外如果你的系统对延迟极度敏感所有 Agent 都必须在同机同进程内通信那也请绕道。Agent-Reach 的定位是跨服务、跨节点、跨团队协作不是进程内通信工具。2. 整体设计思路与核心机制2.1 注册中心 路由分发 的灵感来源有微服务经验的朋友看到 Agent-Reach 一定会想到 Eureka、Consul、Nacos 那一套。坦白讲思路确实是从服务发现那一层借鉴来的。但我没有直接照搬注册中心的常规定义原因是 Agent 和普通微服务有几个本质区别。普通微服务的能力接口相对固定比如 UserService 就是增删改查不会有太多“动态解释”。Agent 不一样同一个 Agent 可能同时具备“翻译、摘要、情感分析”三种能力而且每种能力在调用时还可能需要一段动态的指令模板。所以 Agent-Reach 在注册信息里必须包含能力描述capability而不只是一个服务名和地址。另一个区别是 Agent 的调用往往不是简单的一次请求响应。有些 Agent 会流式输出有些 Agent 会先返回一个任务 ID后台跑几十分钟再回调结果。Agent-Reach 的协议里必须同时支持同步、流式、异步三种模式。我一开始只做了同步后来加流式再到后来才补上异步任务模式。过程很痛苦但也逼我把协议抽象得足够通用。路由分发逻辑则参考了 API 网关的思路。每个进入 Agent-Reach 的请求会先经过一个匹配器根据请求里声明的“目标能力”和“参数约束”找出所有可用的 Agent 实例然后按权重或健康度选中一个再把请求转发过去。这跟 Nginx 的 upstream 很像但匹配维度从 URL 变成了“能力”。2.2 Reach 的抽象服务发现、能力描述、动态编排Agent-Reach 把整个网络分成三个角色节点Node、注册中心Registry、路由网关Router。节点就是每一个具体的 Agent 实例。它启动后会带着自己的身份信息、能力描述、健康检查地址向注册中心发起注册。注册中心保存一份“当前全网 Agent 状态表”并且通过心跳定期检查每个节点是否还活着。路由网关接收外部请求根据状态表决定把请求转给谁。能力描述是整个协议的核心。我设计的能力描述是一个 JSON Schema。举个例子{ agent: english-translator, version: 0.2.1, capabilities: [ { name: translate, description: 将任意文本翻译为英文, input: { type: object, properties: { text: { type: string, minLength: 1 }, tone: { type: string, enum: [formal, casual] } }, required: [text] }, output: { type: object, properties: { result: { type: string } } } } ] }这个描述文件决定了路由网关怎么把请求匹配到这个 Agent。在注册时节点把这份 JSON 发给注册中心注册中心会解析并建立倒排索引从能力名到节点列表的映射。这样当路由网关收到调用translate的请求时就能很快找到所有支持该能力的节点。动态编排则是基于能力的组合。Agent-Reach 不内置 DAG 或者工作流引擎但协议支持chained call一个 Agent 的返回结果可以直接作为另一个 Agent 的输入。这个实现很简单就是允许路由网关在收到某个节点返回的结果后按照预置的“链路策略”继续调用下一个节点。真正的工作流编排还是交给业务层去做Agent-Reach 只提供底层的“能力一跳直达”。2.3 为什么不用现成的消息队列/微服务框架一开始有人建议我直接用 Kafka 或者 RabbitMQ 做解耦还有人说直接用 Nacos 加 Feign 不就行了。我认真调研过最后拒绝的原因很简单消息队列是异步管道它本身并不知道消息该给谁、谁有能力处理。如果你有一个 task 进来要通过 MQ 发给多个 Agent 中的某一个你还需要自己写 router而且 MQ 的重试、死信、顺序问题会让你淹没在中间件运维里。Agent 场景更多是“按需选择、动态切换”用消息队列有点像用货车送外卖能到但时效和灵活性都不对。微服务框架则太重了。Nacos 加 Spring Cloud 那一套光依赖体积就能吓跑很多 Python Agent 项目。Agent-Reach 的注册中心是一个单文件二进制路由网关也是一个单体服务底层通信只走 HTTP JSON任何语言都能方便接入。我明确放弃强类型通信协议因为 Agent 的输入输出本身就不是强契约用 Protobuf 只会让各自定义 Agent 的人更痛苦。所以我用了一个很直白的策略注册中心只做状态管理不做业务转发路由网关只做转发不做业务处理业务数据全部以 JSON 形式透传。这样每一层都是独立的你可以把路由网关换掉只保留注册中心做服务发现也可以不要网关直接让 Agent 之间通过注册中心拿地址后互调。各取所需。3. 实操从零搭建一个 Agent-Reach 节点3.1 环境准备与依赖这里我以 Python 为例因为 Python 写 Agent 最常见。Agent-Reach 本身不依赖任何大模型 SDK你只需要两个库requests用于发送心跳和调用的 HTTP 请求FastAPI或 Flask用于把 Agent 的自身能力暴露成 HTTP 接口。当然你不用 Agent-Reach 提供的 Python SDK 也可以只要你手工实现几个回调端点即可。不过我建议先直接用官方 SDK省去很多细节。我当时的测试环境是两台 Linux 服务器一台跑注册中心和路由网关一台跑三个 Agent 节点。实际上你在本地一台机器上用 Docker 也行。Agent-Reach 官方提供了一套 docker-compose里面包含注册中心、路由网关、示例 Agent 节点可以一键起一套完整网络。起步安装很简单pip install agent-reach这条命令会同时装好agent-reach-registry注册中心命令行、agent-reach-router路由网关命令行和 Python SDK。3.2 定义一个可被 Reach 的 Agent假设你已经有一个 Agent函数叫english_translator(text, toneformal)。你需要做两件事一是用 FastAPI 把它包装成一个本地 HTTP 服务二是提供两个特殊端点/reach/health和/reach/register。/reach/health用来告诉注册中心“我还活着”。它返回 200 和 JSON{status: ok}即可。/reach/register返回上节提到的那个能力描述 JSON。SDK 里可以直接写from fastapi import FastAPI from agent_reach import ReachAgent app FastAPI() agent ReachAgent( agent_nameenglish-translator, version0.2.1, capabilities[ { name: translate, input: { type: object, properties: { text: {type: string}, tone: {type: string, enum: [formal, casual]} }, required: [text] } } ], health_urlhttp://localhost:8000/reach/health, register_urlhttp://localhost:8000/reach/register, # 这是真正干活的入口SDK 会把它暴露为 /reach/invoke invoke_handlerenglish_translator )然后调用agent.start()。SDK 会自动帮你挂上/reach/health、/reach/register、/reach/invoke三个端点并在后台启动一个线程每隔 5 秒向配置好的注册中心发送心跳。这里我要强调一个关键点invoke_handler的名字必须和capabilities里的某个能力名对应否则路由网关调用进来之后不知道如何分发。我最初犯过错误能力叫translate_en函数叫translate结果调用时报“unknown capability”。SDK 底层其实是在/reach/invoke接收请求然后根据请求体里的capability字段去执行对应的处理函数。3.3 注册与发现核心配置与参数解析启动一个 Agent 节点之前你需要告诉它注册中心的地址。我的建议是通过环境变量传入而不是写死在代码里这样可以随时切换调试环境export REACH_REGISTRY_URLhttp://192.168.1.10:8610 export REACH_AGENT_NAMEenglish-translator export REACH_HEARTBEAT_INTERVAL5 export REACH_HEARTBEAT_TIMEOUT15在 SDK 里这些环境变量会自动被读取。REACH_HEARTBEAT_INTERVAL是心跳间隔默认 5 秒REACH_HEARTBEAT_TIMEOUT是注册中心判断节点失联的时间阈值默认 15 秒。这两个参数要联动设置如果心跳间隔太长注册中心会在 Agent 明明活着的时候误判为离线如果太短会给注册中心造成无谓的压力。注册成功后注册中心会返回一个令牌 token。这个 token 是后续该节点做变更、注销、更新能力描述时需要带上的身份凭证。SDK 会把它保存到本地.reach_token文件里下次重启时自动带上不需要你手动处理。注册中心本身启动更简单agent-reach-registry --port 8610 --data-dir /var/lib/agent-reach--data-dir用来持久化已注册节点的信息防止注册中心重启后所有 Agent 需要重新注册。虽然 Agent 也会自动重连但保留状态总归更稳。3.4 跨节点调用一次完整请求的路径Agent-Reach 的路由网关默认端口是 8620。网关对外提供一个通用的调用接口以下是一个实际调用示例curl -X POST http://192.168.1.10:8620/invoke \ -H Content-Type: application/json \ -d { capability: translate, agent: english-translator, input: { text: 你好世界, tone: formal } }这个请求进来后网关会做四步解析目标根据capability和可选的agent参数确定要找哪个能力、优先哪个具体 Agent。查询注册中心拿到当前在线且支持该能力的节点列表。注意这一步是从注册中心的内存状态表直接查的不会每次请求走后端数据库所以很快。注册中心和网关之间的同步是通过长连接和增量更新做的。健康校验如果节点列表里存在已经失联但还没被注册中心摘除的节点这个窗口期可能存在网关会先做一次轻量试探确保节点真的能处理请求。转发与回传网关把请求体原封不动 POST 到节点的/reach/invoke端点然后把节点返回的内容作为响应返回给客户端。整个链路非常“傻”好处就是每一跳都可以被独立日志记录。我在网关里额外记录了一个reach_trace_id每次调用开始时生成传到节点后节点返回时带上这样排查问题时一查 ID 就能看到完整链路。当你调用成功返回结果大概是这样{ trace_id: 8f3e87e2-5d9a-4c6b-9bae-6d19f2d3a1d4, agent: english-translator, capability: translate, output: { result: Hello World } }这个输出格式也是 Agent-Reach 协议的一部分output字段永远代表 Agent 执行后的有效结果其他如耗时、命中节点、错误码则放在meta里。这样无论客户端还是下游链式调用都可以直接取output而不用关心附加信息。4. 核心细节与坑点实录4.1 超时与重试别让你的 Agent 在深夜“假死”我踩过最深的坑就是 Agent 挂了自己不知道网关也无能为力。最开始我的 Agent 因为外部模型 API 不稳定偶尔会卡住 60 秒以上而网关的全局超时只有 30 秒。结果每过一阵子就有用户说“怎么调用没反应”一看日志网关返回了timeout error但 Agent 自己还在吭哧吭哧跑。等到 Agent 终于算完了把结果 POST 给网关网关早就把这次请求丢掉了。要解决这个问题你要想清楚Agent 慢是常态还是异常。如果你调用的模型 API 本身就可能有 100 秒延迟那网关超时必须调大比如 120 秒。但网关无法无限等待所以更合理的方案是把长时间任务设计成异步模式。Agent-Reach 支持你直接返回一个任务 ID像这样{ status: async, task_id: baf8c07e-8a31-4a9e-9a2d-2bd09d2e1b8a, callback_url: http://router:8620/notify?subscriberhttp://client/callback }网关收到这种响应后会先把 202 返回给客户端然后在这个节点完成计算后把结果通过callback_url推送给客户端。我在一开始的设计里就硬编码了“同步/异步”两种模式到最后发现这个设计救了我好多次。现在我们的生产环境里凡是可能超过 30 秒的任务一律要求 Agent 返回异步标志。还有一个细节是重试策略。不要无脑重试因为你重试的不是网络请求而是一个可能已经产生副作用或消耗了 API 费用的 Agent 调用。Agent-Reach 默认不自动重试只透传错误码。如果你想做幂等重试需要在调用请求里带上idempotency_key节点端记录这个键如果重复执行就返回第一次的结果。这个特性我强烈建议开启尤其是在做链式调用的时候中间任何一环断了如果下游有幂等保护你就可以安全地整条链路重启。4.2 能力描述规范写不好路由就白搭能力描述是整个 Agent-Reach 交互的地基。地基没打牢上层再稳也没用。我在内网 GitHub 上开源了能力描述的老版本后来根据自己在实际使用中的经验把规范改成现在这种核心原则只有一条能力描述必须写清“什么输入能让我工作”以及“什么输出是能消费的”。很多人会犯一个错误把能力描述写成自然语言比如“我可以翻译多种语言到目标语言”。这种描述只能给人看机器没法路由。Agent-Reach 的匹配器会解析input里的properties和required如果调用请求里缺失了required里的字段网关会在路由前就返回参数校验错误根本不会把请求转发给你。这样能提前拦住很多低级错误省得你 Agent 内部再去写一堆 if 判断。另一个建议是不要把一个 Agent 的能力描述写得过大。比如一个 Agent 既做文本翻译又做语音识别又做图像生成能力描述会非常臃肿而且在路由时你还得处理多种 schema 冲突。我更推荐一个 Agent 只对外暴露一到两个核心能力剩下的内部逻辑尽量做成私有方法不要暴露成能力。这样路由清晰负载均衡也容易配。我们团队后来强制约定每个 Agent 的能力数不超过 3 个。超过 3 个就觉得它不是一个 Agent而是一个应用——应用不应该注册进 Agent-Reach而应该自己开发 API。4.3 安全性内网穿透不是唯一解权限模型要前置很多人用 Agent-Reach 之后第一反应是要让它能被外部访问。如果你直接把路由网关暴露到公网那确实省事但风险极大。我调试的时候就遇到过有人扫到我服务器的端口用 curl 随便调用我的 Agent 来免费蹭大模型 API。还好我没在网关做任何鉴权那一阵子账单直接爆炸。用 Agent-Reach 生产化之前至少要解决两层安全传输安全和调用鉴权。传输安全可以用 Nginx 把网关包一层 HTTPS这个大家都会。调用鉴权则要你自己在网关前面加一个 API 网关比如 Kong、APISIX或者在 Agent-Reach 里开启内置 token 校验。Agent-Reach 内置的 token 校验很简单启动网关时设置REACH_API_TOKENS这是一个逗号分隔的 token 列表客户端调用/invoke时必须带Authorization: Bearer token否则 401。这个机制不复杂但很多小团队就是懒得加。我的经验是从一开始就加上省得后面为了一次安全事故悔青肠子。还有一点注册中心本身尽量不要暴露到外网。因为注册中心知道了所有 Agent 的地址、能力、版本属于内部元数据。一旦泄露攻击者能非常迅速地画出整个系统的拓扑图。最好的部署方式是把注册中心和网关放在同一内网网关通过公网入口暴露在 DMZ 区两者之间用防火墙限制只允许特定端口互访。5. 常见问题排查表5.1 节点注册不上的三类原因我们内部用了几个月后总结出节点注册失败基本逃不出三类第一类是地址配置错误。注册中心那里看到节点从未出现过而你注册中心日志里也没任何来自节点请求的记录。这大概率是节点的REACH_REGISTRY_URL配错了。注意我这里用的是http://如果改成了https://而注册中心没开 TLS会握手失败被静默丢弃。一旦遇到这种问题先 tcpdump 看一下节点到底有没有发出 SYN 包。第二类是 token 不一致。如果节点在注册时因为非法 token 被拒注册中心日志里会有unauthorized字样。这种情况多发生在你把某个节点的数据目录迁移到另一台机器后token 文件没跟着过去。解决方法是删除节点本地的.reach_token重新注册或者从注册中心的管理接口重新颁发 token。第三类是能力描述非法。注册中心会对能力描述做 schema 校验如果你的name为空、input没有type都会注册失败并返回带detail的错误信息。这种问题在开发期最容易出现因为你的能力描述来自某个动态生成的 dict字段名写错了。当你看到 400 错误时直接去解析错误返回的detail字段它会具体到是哪个字段不合规。5.2 请求超时但你总以为是代码问题有一种非常隐蔽的“网关超时”问题Agent 节点自己处理很快但网关那边仍然报超时。很多人会怀疑是 GC 暂停或者线程阻塞其实问题往往出在HTTP 连接池耗尽。Agent-Reach 的节点和网关之间复用长连接如果你的节点使用 Pythonrequests库且没有正确用完session每次调用都会创建新的 TCP 连接导致节点端出现大量TIME_WAIT状态而网关和节点之间的连接数被内核参数限制得很小新请求无法建立新连接最终等待超时。解决这个问题的办法很简单在节点端使用requests.Session()并将连接池大小调大比如session.mount(http://, HTTPAdapter(pool_connections50, pool_maxsize50))。另外如果你用的是 Flask 自带的单进程开发服务器每次只能处理一个请求那么当并发上来时第二个请求会因为排队而超时。这不是 Agent-Reach 的问题而是你的服务器不支持并发。用 gunicorn 或者 uvicorn 跑节点至少开两个 worker。这个坑真的很多人踩过包括我自己。5.3 路由选错 / 负载不均的排查思路有时候你会遇到明明有两个 Agent 都支持同一能力请求却全打到其中一个上面。这有两个原因。一是因为你在调用时传了agent参数网关会优先选择你指定的 agent于是另一个 Agent 永远没有流量。二是因为健康状态表里另一个 Agent 的 last_seen 时间戳太久没更新网关认为它不健康自动从候选列表里剔除了。排查的第一步调一下注册中心的/nodes接口看所有节点的在线状态和心跳时间。如果某个节点确实健康但没有及时心跳检查心跳间隔和防火墙是否拦了心跳端口。第二步在网关日志里看每次调用的candidates字段它会列出所有命中的节点。如果candidates长度大于 1 但实际选择的却不是你想要的那个说明调度权重有问题。默认的策略是“最低负载优先”如果你没有上报 CPU 或内存状态负载会退化成随机轮询这通常不是 bug。负载不均还有一种情况能力名相似但不完全相同比如一个节点注册的能力是translate另一个节点注册的是translate-en你以为两组都能处理中文到英文但网关只认translate所以另一个节点永远接收不到请求。这种问题根因还是能力描述的命名不规范。所以命名前一定要先定好一份团队内部的“能力字典”。5.4 版本兼容Agent-Reach 0.x 升级注意事项Agent-Reach 目前还处于 0.x 阶段所有接口都可能变动。升级时最容易忽略的是能力描述格式。在 0.3 版本之前input字段允许直接写 JSON Schema 片段0.4 版本开始强制要求最外层有type: object。如果你从 0.3 直接跳到 0.4注册中心会直接拒绝 0.3 格式的注册请求。升级步骤我建议是先升级注册中心和网关再逐个升级节点。因为在混合版本期间旧节点发出的能力描述可以被新注册中心兼容接收如果只是加字段的话。但一旦注册中心先升级成新版本旧节点的描述如果缺字段就会大量注册失败。所以升级顺序非常重要最好是先在测试环境做一次全量升级确认兼容后再动生产环境。另一个容易踩的坑是异步回调的格式变化。在 0.5 版本后异步通知从原来的notify参数改成了统一的callback_url。如果你在旧版本里用了异步功能请先查看官方 changelog确认字段名没变再升级否则回调推不出去你的业务流程会莫名其妙停掉。6. 从个人玩具到小团队基础设施我的体会写 Agent-Reach 之前我觉得难的是让每个 Agent 具备聪明的能力。写了之后才发现真正阻碍多个 Agent 协同工作的永远是那些不起眼的基础设施细节谁在线、谁能处理什么、状态好不好、调用了一个慢任务怎么办。这些东西听起来不酷但它们决定了一个系统能不能稳定跑在用户面前。我个人的建议是不管你能不能做出完整的 Agent-Reach都一定要在动手写多 Agent 系统之前先想清楚服务发现和能力描述这两个问题。不需要一开始就用框架你可以用一张表格记录所有 Agent 的能力、地址、负责人、限流阈值然后让代码从这个表格里读取路由信息。当表格超过一百行、修改频繁导致 Git 冲突时你就知道为什么需要一个动态的注册中心了。最后分享一个小技巧如果你打算长时间跑 Agent-Reach一定记得给注册中心和网关加上持久化存储和日志归档。注册中心最好跑在独立磁盘上因为它的状态表一旦丢失所有 Agent 都要重新注册启动风暴会瞬间把网关卡到不可用。网关的日志至少保留 30 天尤其是trace_id相关的日志不要轻易裁剪。我踩过“日志自动清理后无法排查三个月前事故”的坑到最后只能靠猜那滋味真的不好受。Agent-Reach 现在还谈不上成熟但它已经帮我们解决了三个生产事故级别的协作问题。有时候你会觉得协议和框架都是小事但真正跑起来、被人骂“你这什么破调用链”的时候你才会理解那些小事恰好是最重要的基石。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询