Agent-Reach:智能体触达的注册、发现与路由方案解析 “Agent-Reach”这个名字放在半年前拿出来可能要解释半天放到现在做智能体的人一眼就能读出重心在 Reach 上。Agent 现在不缺缺的是触达一个智能体怎么让别人发现自己、怎么调用自己能力范围之外的服务、怎么在一张不断变大的网络里保持存在感。Agent-Reach 就是我围绕“智能体触达”这个命题整理出来的一套轻量方案核心解决三件事找得到、连得上、调得动。这套方案不是一个重平台也没有打算重新发明一套 Agent 框架。它的做法很朴素给每个 Agent 一份描述自身能力与约束的触达清单用一个极简注册中心做发现再用一层薄薄的路由器做请求转发。收到请求的一方按照清单里声明的协议来响应。适合手里已经有两个以上 Agent、想让它们互相协作的开发者也适合正在搭 Agent 平台的架构师拿来做底层互联的设计参考。我不想把它包装成“又一个新标准”更希望把注册、发现、路由、安全这几件小事讲透。下面我从设计思路、最小闭环、安全边界、常见坑、经验教训五个方向展开最后补一些我自己踩出来的体会。1. Agent-Reach 到底在解决什么问题1.1 智能体的“孤岛效应”先说我看到的很普遍的现象。很多团队把 Agent 做成独立服务本地调试时一切正常一放到生产环境每个 Agent 就变成了一个孤岛。客服 Agent 在处理用户诉求时需要查实时库存可是库存 Agent 是另外一个服务接口、鉴权方式、数据模型全都不一样两边也没有一个约定的触达方式。结果就是每次联调都要靠人肉在中间传话或者干脆把两个 Agent 硬塞进同一个进程里。这种孤岛效应拖慢的不是某一次集成而是整个 Agent 生态的演进节奏。Agent 的价值本来就来自分工协作一个能查天气的 Agent、一个能管日程的 Agent、一个能做审批的 Agent各自独立部署、独立演进然后通过某种方式互相调用整体的能力边界才能超越单个 Agent。可一旦触达机制缺失能力边界就被钉死在服务边界上谁也没法往外多走一步。1.2 三个核心目标可发现、可触达、可控我给自己定的目标很清楚Agent-Reach 只负责三件事。第一可发现。一个 Agent 上线之后其他 Agent 或人能不能通过某种方式知道它的存在、知道它能做什么。这就需要注册和查询机制让能力变成可检索的清单而不是藏在团队文档里无人问津。第二可触达。知道了不代表能调用还得有一条标准的请求路径能够把调用方的意图可靠地送到目标 Agent 手上并拿到结构化的响应。这里包括路由、超时、重试、协议适配。第三可控。触达能力放开之后安全边界也要同步建立。谁可以调用、每次调用的频率上限是多少、整个链路的调用轨迹能不能回溯这些东西在 Agent 数量少的时候无所谓一旦超过十几个没有治理机制就会很快失控。1.3 这个方案适合谁说实在的Agent-Reach 不是给研究大模型的人准备的它的听众大概有三类。第一类是集成工程师。他们手里已经有两个以上的 Agent正准备打通它们之间的调用需要一套不引入太重框架的接法。第二类是 Agent 产品负责人。他们负责一个 Agent 矩阵需要让各个 Agent 的能力能被统一管理、统一调度但又不想被某个云厂商绑死。第三类是平台架构师。他们正在设计 Agent 平台Agent-Reach 的注册、发现、路由模型可以直接拿来作为平台底层互联模块的设计参考。2. 核心架构与方案选型为什么是“注册-发现-路由”2.1 协议优先于平台讨论 Agent 互联时最常见的方案是直接上一站式 Agent 平台把 Agent 全部托管进去平台内部天然互通。这个方案体验确实好上手成本低但代价是 Agent 一旦进了这个平台往外扩展就会很别扭。你可以把它类比成物业统一装修的公寓住着方便但你想在承重墙上开一扇门就得先问物业同不同意所有配合都必须在它允许的框架内完成。Agent-Reach 选的是另一条路协议优先平台弱化。先约定一套所有 Agent 都能遵守的触达协议再只保留一个轻量注册中心来维护存在感。说白了协议是契约平台是商品契约一旦被广泛接受具体跑在哪台服务器上反而不重要。这样注册中心本身也可以被替换、被联邦化不会被任何一个厂商锁定。2.2 触达描述文件Agent 的“身价牌”协议优先不只是说要有统一的 HTTP 调用格式更要把 Agent 的身份与能力描述标准化。我给这个描述文件起名叫触达描述文件本质上是一份 JSON列清楚这个 Agent 是谁、在哪、能做什么、有什么限制。下面是一份完整示例{ agent_id: agent-warehouse-01, name: 库存查询助手, version: 1.2.0, protocol: jsonrpc, endpoint: http://192.168.1.20:8096/reach, capabilities: [ { name: inventory.query, schema_version: v1, timeout_ms: 3000, parameters: [ {name: sku, type: string, required: true}, {name: warehouse, type: string, required: false} ] }, { name: inventory.update, schema_version: v2, timeout_ms: 5000 } ], constraints: { max_payload_kb: 128, ratelimit_qps: 10, auth_required: true }, ttl_seconds: 3600 }为什么要花这么大力气定义描述文件因为 Agent 触达时最容易踩的坑不在网络上而在语义层两边都以为自己在说同一件事实际上一个传的是旧版 SKU 字段另一个期待的是新版 warehouse 结构。把能力名、参数结构、schema 版本全部写进描述文件触达双方在发起请求之前就能对齐预期省掉大量来回试探。2.3 路由层的三个关键决策注册中心负责回答“谁能做这件事”路由器负责回答“这次请求该发给谁”。路由层做决策时我反复想过三个问题直接说结论。第一先按能力匹配再按可用性过滤。路由器拿到请求里的 capability 名称先去注册表筛出所有声明支持该能力的 Agent再根据健康状态、当前负载、约束条件过滤最后按策略挑一个。能力匹配放在最前面能过滤掉大部分无效请求。第二默认不跨协议转换。最初我也想过做通用协议转换网关让 HTTP、gRPC、消息队列随便互转看上去很酷实际上维护成本极高。后来只保留一层薄薄的 JSONRPC 适配复杂协议转换丢给调用方自己处理。原因是协议转换一旦发生在链路中段出了问题最难跟踪。第三绝不无限重试。触达失败后的重试看起来很自然但在 Agent 拓扑里一个下游 Agent 抖动会引发上游所有调用方同时重试形成重试风暴。我把默认重试次数压到 2 次并且强制使用指数退避间隔宁可让一次请求失败也不要让整个拓扑瘫痪。2.4 与 MCP、Function Calling 的关系聊到这里可能有人会问这和 MCP、Function Calling 有什么区别简单地说它们处在不同的层。Function Calling 解决的是模型怎么决定调哪个函数的问题。MCP 解决的是模型客户端怎么用统一方式调用外部工具的问题它定义了工具列表、调用入口和返回格式。Agent-Reach 解决的是更靠上的一层在一个多 Agent 网络里谁能被发现、路由怎么选、调用链怎么治理。你可以把 MCP 当成 Agent-Reach 的 payload 协议来用A Agent 通过注册发现 B Agent 的触达入口之后两者之间的调用可以走 MCP 的消息格式。这样不需要重新发明一套格式Agent-Reach 专心做拓扑和治理层。我的实际建议是不要在触达协议层再做一次标准化能复用就复用把精力留给连接关系本身。3. 从零跑通一个 Agent-Reach 最小闭环3.1 准备环境我本地实测的环境是 Python 3.10 加 FastAPI。选 FastAPI 是因为注册中心逻辑本身简单这时候不需要追求高并发性能更重要的是模型清晰、能快速改。如果你团队的主栈是 Node.js用 Express 也是等价的核心逻辑就几十行。依赖装一下pip install fastapi uvicorn httpx pydantic然后准备两个模拟 Agent。为了让过程更真实一个叫 warehouse-agent监听 8096另一个叫 notify-agent监听 8097。它们不一定要用同一个框架实现甚至可以一个用 Python、一个用 Node这也是 Agent-Reach 的初衷只通过协议互通不限定实现语言。3.2 写一个极简注册中心注册中心只需要两个接口注册和发现。核心代码大致如下# registry.py from fastapi import FastAPI, HTTPException from pydantic import BaseModel from typing import Optional, List import time app FastAPI() class ReachManifest(BaseModel): agent_id: str name: str version: str 1.0.0 protocol: str jsonrpc endpoint: str capabilities: List[dict] ttl_seconds: int 3600 agents {} app.post(/register) def register(manifest: ReachManifest): 注册 Agent同一 agent_id 重复注册会覆盖旧记录。 agents[manifest.agent_id] { **manifest.model_dump(), registered_at: int(time.time()) } return {status: ok, agent_id: manifest.agent_id} app.get(/discover) def discover(capability: Optional[str] None, limit: int 10): 按能力名搜索 Agent能力名完全匹配。 result [] for agent in agents.values(): if capability: names [c[name] for c in agent[capabilities]] if capability not in names: continue result.append({ agent_id: agent[agent_id], name: agent[name], endpoint: agent[endpoint], protocol: agent[protocol] }) if len(result) limit: break return {result: result}注意一个细节注册接口故意不区分 create 和 update同一 agent_id 再次注册就是更新。这样 Agent 重启后重新注册时不用先删旧记录少一个步骤也少一种失败场景。提示上面这段代码只是为了把逻辑说明白生产环境至少还要加 TTL 过期检查和鉴权第四部分会展开讲。3.3 把两个 Agent 注册进去注册中心跑在 8000 端口启动之后把两个 Agent 注册进去。用 curl 就能完成curl -X POST http://127.0.0.1:8000/register \ -H Content-Type: application/json \ -d { agent_id: warehouse-01, name: 库存查询助手, protocol: jsonrpc, endpoint: http://127.0.0.1:8096/reach, capabilities: [{name: inventory.query, schema_version: v1, timeout_ms: 3000}], ttl_seconds: 3600 }第二个 Agent 同理把 capability 换成 notify.sendendpoint 指向 8097。注册完成后调用发现接口验证curl http://127.0.0.1:8000/discover?capabilityinventory.query你会得到类似这样的结果{ result: [ { agent_id: warehouse-01, name: 库存查询助手, endpoint: http://127.0.0.1:8096/reach, protocol: jsonrpc } ] }到这里第一个目标“找得到”已经成立。3.4 发起一次真实的跨 Agent 触达请求发现之后就是触达。这里我刻意不把路由器做得太重它的职责只是转发把调用方发来的请求包装成目标 Agent 能理解的格式然后透传给目标 endpoint。路由转发的核心逻辑可以简化成下面这样它复用了注册中心的查表函数# router.py 片段按 capability 路由并转发 from fastapi import Request, HTTPException import httpx app.post(/route) async def route(req: Request): body await req.json() capability body[capability] targets find_by_capability(capability) # 复用注册表的查询逻辑 if not targets: raise HTTPException(status_code404, detailno_agent_found) target targets[0] async with httpx.AsyncClient(timeout3.0) as client: resp await client.post(target[endpoint], jsonbody) return resp.json()调用方发出来的请求体长这样{ capability: inventory.query, args: {sku: SKU-2024-01}, caller_id: agent-cs-01, trace_id: tr-001 }路由器收到后先查注册表拿到 warehouse-01 的 endpoint然后把请求转发过去。目标 Agent 处理完成原样返回结果。这个过程中有两个状态值得记录一是路由耗时即从请求进入路由器到目标 Agent 返回响应的总时长二是目标端点是否健康。把这些信息打进结构化日志之后整个链路才算真正闭环。实战里我发现真正能暴露问题的往往不是注册和发现而是路由这一跳。很多 Agent 的实现对请求格式非常敏感缺一个字段、多一个嵌套都会导致解析失败。所以路由器的第一版一定要保留“透传原样”的能力先不要过度加工请求体等格式稳定了再考虑适配层。3.5 结构化日志与 trace_id这个系统看起来简单但 Agent 之间的调用链可能跨好几个服务没有日志出问题时几乎无从下手。我在每次触达请求上强制带一个 trace_id由调用方生成并透传路由器、目标 Agent 都把它记进日志。日志字段固定为七项时间、trace_id、caller_id、capability、target_agent_id、耗时、结果。格式统一的好处是后续接 Elasticsearch 还是 Loki 都能直接按 trace_id 拉出一条完整调用链。别小看这个习惯没有 trace_id 的时候一次跨 Agent 报错可能要三个团队互相甩锅查一整天有了它十分钟就能定位到具体是哪一跳出了问题。4. 安全与治理触达范围越大风险边界越要清晰4.1 认证与授权的最小实践Agent 触达不是浏览器访问网页调用方是另一个程序所以不能用验证码这种交互式手段。最直接的方案是分发 API Token调用方在请求头里带上自己的凭据路由器验证通过后转发。请求头大致是这样X-Agent-Token: caller token X-Agent-Caller: agent-cs-01 X-Trace-Id: tr-001授权层面我强烈建议给每个 Token 绑定 scope而不是给它全部权限。比如客服 Agent 的 scope 只能调inventory.query不能调inventory.update。这个设计可以在路由器上做一层极薄的越权校验请求里声明要调的 capability必须落在调用方 scope 清单里否则直接拒绝。注意别把安全寄托在“反正我们是内网”这种假设上。Agent 网络一旦开放给协作方边界就不再是局域网防火墙能覆盖的了。每一跳都要有身份和权限检查这是我用实际事故换来的教训。4.2 限流与熔断参数建议自治网络里每个 Agent 都可能成为瓶颈。我给每个注册的 Agent 在描述文件里声明自己的速率上限路由器在转发前先做一次本地令牌桶检查超出限制就返回 429让调用方自己退避。参数上我用的默认值是单 Agent 10 QPS超时 3 秒连续失败 5 次触发熔断熔断持续 1 分钟。这个数值可以根据业务调整但原则是不要让下游 Agent 被上游调用冲垮。有人可能会问为什么不让调用方自己做限流因为在多 Agent 拓扑里调用方数量很多各自做限流很难统一约束。集中到路由器上做限流策略才能一致出问题也更好查。4.3 审计与追踪最后一项看起来不起眼但生产环境出事故时最救命的就是审计日志。每次触达都要记录谁调用、在什么时候、请求了哪个能力、目标 Agent 是谁、响应结果如何、耗时多少。审计日志我建议独立存储不要和业务日志混在一起。原因是审计日志不仅要给开发看还要给安全团队、合规团队看查询模式和分析工具都不一样。成本其实不高每条记录几百字节而已换来的是问题回溯能力和安全事件调查的完整证据链。5. 常见问题排查实录5.1 Agent 注册成功但发现不到这是我在项目里遇到最多的问题。表象是注册接口返回了 ok可发现接口就是查不到。排查顺序如下。先看 TTL。注册记录带了过期时间很多 Agent 只注册一次就不管了TTL 一到就被自动清理自然查不到。解决方案是给 Agent 侧加一个注册续约的定时任务每过 TTL 的一半时间就重新注册一次。再看 capability 匹配方式。我上面写的示例用的是全字匹配很多 Agent 写的是inventory.query.v1实际查询传的是inventory.query对不上就查不出来。建议在描述文件里给 capability 加别名或者统一命名规范这个事要在一开始就定死。最后看注册是否分写失败。如果注册中心有多个节点客户端写到了 A 节点查询打到 B 节点也会出现“注册成功但查不到”。最小闭环可以用单节点绕过生产环境要尽快引入一致性的处理方案。5.2 路由超时与重试风暴另外一类典型事故某个 Agent 实例进程还活着但卡在数据库锁上每次响应都超过 5 秒。调用方默认超时 3 秒于是一个失败多个调用方同时发起重试瞬间把下游请求队列堆满。这台 Agent 本来不慢硬是被重试流量拖垮了。我的处理方式分两层。第一层注册中心加健康检查连续 3 次检查失败就视为不可用从发现结果里摘除而不是等到调用超时才暴露。第二层重试要做全局收敛同一个 trace_id 最多重试 2 次且重试间隔必须成倍递增比如第一次等 500ms第二次等 2 秒。如果再严重就在路由器层面开启单目标 Agent 的并发限制超出并发直接丢弃新请求返回 503。5.3 响应格式不匹配这是一个非常隐蔽的坑。两个 Agent 都叫库存查询助手也都实现了inventory.query但一个返回{inventory: 100}另一个返回{items: [{sku: ..., qty: 100}]}。调用方如果预设了某一种格式另一个 Agent 的响应就会解析失败。解决办法是把 schema 版本写死在触达描述文件里。调用方在发现阶段拿到 schema_version请求时声明自己要的是哪个版本目标 Agent 收到后如果不支持调用方声明的版本直接返回协议错误。这样从一开始就避免了解析错位。实际做的时候我还建议每个 schema 版本都准备一份样例响应挂在 Agent 的接口文档下调试成本能再低一半以上。5.4 调用链环路Agent 网络一旦允许互相调用环路就会出现A 调用 BB 调用 CC 又调用 A。单看每一跳都合理但整个链路变成了无限循环逐渐压垮集群。我做两件事来兜底。一是请求头携带X-Max-Hop: 5每经过一跳递减归零就拒绝。二是每个路由器维护最近 10 分钟内出现过的 trace_id 集合重复出现直接拦截。这两个措施一攻一守实际效果很好部署之后基本没有再见过环路事故。5.5 排查顺序速查表把以上问题归成一张表方便对照现象优先排查项根治手段注册成功但发现不到TTL 是否过期、capability 是否匹配、是否写读到不同节点注册续约、能力命名规范、写读同节点路由超时目标 Agent 是否健康、超时配置是否合理健康检查摘除、熔断重试风暴重试次数是否过多、重试间隔是否太短、是否全局收敛指数退避、trace 级重试限制、并发限制响应格式不匹配两个实现是否声明了同一个 schema_versionschema 版本协商、提供样例响应调用链环路是否存在循环依赖、trace 是否被重复消费跳数上限、trace 去重这张表是我踩过很多次之后整理的实际排障时先按第一列定位现象再对第二列逐一排查大多问题都能在几分钟内找到根因。6. 落地经验与后续扩展方向6.1 先跑通最小闭环再谈生态这个项目给我最深的体会是Agent 互联的复杂度藏在细节里而不是在顶层设计里。一开始不需要设计十几种协议、联邦网络、跨集群路由先把一个注册中心、两个 Agent、一条请求链路跑通把“注册-发现-路由-返回”这四个环节全部打通再往上面加东西。我见过不少团队第一步就想把 Agent 平台铺得很大结果半年过去了还在做注册中心的市场调研。Agent 协作的价值在于现有 Agent 真正开始互相调用而不是停在概念规划。6.2 接下来值得做的三件事第一件事是联邦注册。多个注册中心之间互相同步发现结果让不同团队、不同网络的 Agent 也能互相触达同时各自保留自治权。这个方向需要同步机制设计不能简单地把所有注册记录搬到一张表里。第二件事是动态 schema 协商。当前每个 Agent 在描述文件里声明 schema 版本但那只是静态声明。更好的做法是让 Agent 之间在运行时协商一个双方都支持的版本并支持回退。第三件事是基于能力的 Agent 自动组合。当上层业务需要一次多 Agent 协作时由路由器根据能力依赖图自动编排调用顺序完成从“单一触达”到“链路编排”的进化。这一步做出来的话Agent-Reach 就不再只是基础设施而会变成多智能体协作的调度引擎。6.3 个人踩坑总结最后写几条我觉得最有价值的经验。第一描述文件先于代码。每新建一个 Agent先写触达描述文件确认能力和参数再开始写实现。这个习惯帮我省掉了一大半联调时间。第二别在路由器里做翻译。协议适配尽量放到 Agent 侧或者调用方侧让路由器保持薄。路由器一旦复杂起来出了问题根本分不清是转发坏了还是格式转换坏了。第三日志里必须有 trace_id。没有 trace_id 的多 Agent 网络排障就像在夜里关灯找钥匙。这一个字段的成本换来的排查效率提升是数量级的。第四限流参数要从生产数据里调不要拍脑袋。10 QPS 只是起点不同 Agent 的瓶颈完全不同要通过压测拿到真实水位再定。第五给发现结果做缓存。很多调用方会在每次请求前都调用发现接口注册中心的压力会随着 Agent 数量线性上涨。让调用方本地缓存几十秒就能大幅降低注册中心的压力。我做 Agent-Reach 这段时间最大的感受是智能体本身的能力提升当然重要但真正决定一个 Agent 网络能跑多远、跑多稳的反而是触达机制这些看上去不够性感的部分。协议、描述、路由、限流、审计每一块单独拿出来都很朴素组合在一起却决定了整个协作网络的边界和上限。最后分享一个小技巧把注册中心的 /discover 接口接进团队的调试面板。每次新增或者升级一个 Agent 之后手动调一次 discover看看它的能力清单是否按预期展示比写一百行集成测试都管用。Agent 网络的健康往往就藏在这些一分钟就能做完的常规检查里。