Agent-Reach:轻量级智能体互联网关的设计与实践
每个做智能体Agent的人大概率都遇到过同一个尴尬单机跑得好好的 Agent一旦想让它调用另外一个系统里的 Agent或者让两个不同团队开发的 Agent 互相协作立刻变成一场灾难。地址写死、接口对不上、鉴权各管各、回调抓瞎、状态丢失……说白了Agent 之间缺一个统一的“触达”机制。我当时搞 Agent-Reach 这个项目就是被这种痛折磨够了。它本质上是一个轻量级的智能体互联与触达网关解决三件事让 Agent 可以被发现、让 Agent 能被安全路由调用、让 Agent 之间的会话上下文不断片。如果你也在搭建多 Agent 协作系统、Agent 服务化平台或者手头有一堆异构 Agent 不知道怎么统一暴露出去这篇文章应该能给你一些可以直接落地的思路。1. Agent-Reach 要解决的核心问题1.1 先聊聊单 Agent 和多 Agent 的鸿沟单个 Agent 跑好并不难。难的是跨系统、跨团队的 Agent 协作。我见过不少团队把 Agent 包装成了 HTTP 接口用简单的 service 名加路径去调比如http://agent-a:8080/chat。一开始能跑等到 Agent 数量超过三五个问题就全冒出来了你根本不知道哪个 Agent 还活着、哪个 Agent 能处理什么意图、哪个 Agent 换了地址、哪个接口的鉴权方式是什么。这就回到了微服务时代的老问题——只不过这次的“服务”变成了会对话、有状态的 Agent。所以 Agent-Reach 第一件事就是给每个 Agent 发一个“身份名片”注册到一个统一的目录里。任何调用方不再直接面对具体的 Agent 地址而是只对着 Agent-Reach 说话。这思路跟微服务的注册中心很像但有一点完全不同Agent 的能力描述不能只靠几个 tag它需要语义级的描述比如“这个 Agent 能处理天气查询入参是城市名出参是结构化天气数据”这样路由层才能做真正的意图路由而不是简简单单的地址转发。1.2 异构 Agent 之间的三个基础需求把问题拆开看多 Agent 互联只围绕三个基础需求转。第一个是寻址和发现。A 系统怎么知道 B 系统有一个“可以订会议室”的 Agent不是靠配置文件手写而是需要一个动态目录Agent 上线就注册下线就注销能力变更就更新。第二个是协议适配。不同团队可能用不同的 Agent 框架有的是 MCP 协议有的是自研的 JSON 对话接口有的是纯函数式工具调用。Agent-Reach 必须像一个翻译器统一上下游的调用语义而不是强制大家改一套协议。第三个是安全可控。跨系统调用绝对不能裸奔谁调的、能调哪个 Agent、调用参数长什么样都要有可配置的规则。这三个需求我在 Agent-Reach 里用一套分层结构去承接接入层做协议转换目录层做注册与发现路由层做意图分发通道层做消息可靠性保障最后再加一个治理模块管鉴权、限流和日志。2. Agent-Reach 的整体架构与设计选择2.1 架构核心注册表、路由层、消息通道Agent-Reach 的整体结构并不复杂核心就三块Agent Registry注册表、Reach Router路由层、Message Channel消息通道。注册表负责维护所有 Agent 的元数据我称之为 AgentManifest。它包含 agent_id、名称、描述、能力清单、通信协议、回调地址、认证方式、限流配额。路由层拿到调用方的请求后先去注册表匹配“谁适合处理这个请求”再把请求转发过去。消息通道则负责同步请求响应、异步任务回执、超时重试等传输细节。我用一个生活化的类比来解释这个设计注册表像电话簿路由层像接线员消息通道像电话线路本身。没有电话簿你只能靠背号码没有接线员你根本不知道该转给谁没有可靠线路电话打一半就断线。Agent-Reach 做的就是这三样事。在设计上有个关键取舍要不要把消息队列直接集成进去我最终的结论是不内置重负载的 MQ而是提供一个回执接口让异步型 Agent 通过回调方式把结果送回。原因很简单Agent 调用场景大部分还是短交互真正需要长耗时任务的场景由调用方根据自己的队列系统去接更灵活。Agent-Reach 保证的是链路可追踪、回执不丢而不是强行做一个万能的调度器。2.2 协议抽象为什么不能只认 MCP 或 A2A现在谈到 Agent 互联很多人第一反应就是“上 MCP”或者“上 A2A 协议”。MCP 确实解决了 Agent 与工具之间的标准化问题A2A 也确实定义了一套 Agent 之间的通信语义。但我实际做完以后最大的体会是协议统一是理想协议适配才是现实。你没法要求存量系统为了接入你而全部重写一遍。所以 Agent-Reach 在协议设计上做了一个抽象层调用方发过来的请求统一规范成一套内部的ReachRequest结构包含intent意图、payload参数、session_id会话标识、caller调用方身份、meta链路追踪信息。路由层根据 AgentManifest 声明的协议类型把ReachRequest转换成目标 Agent 能理解的格式。比如目标 Agent 走的是 MCP就把 intent 映射为 tool call走的是自研 HTTP 接口就把 payload 包装成它的 JSON 请求体走的是 REST 回调型则立即返回202 Accepted后续结果通过回执接口异步通知。这种设计有一个额外的好处调用方永远面对一套 API 语义底层 Agent 无论怎么换框架对上游的影响都收敛在注册表的一条记录里。我后续迭代时增加新的协议适配器也不需要动上游任何代码。3. 核心模块拆解与关键参数3.1 AgentManifest智能体的“身份名片”这是 Agent-Reach 里最重要的一个概念也是我强烈建议你在自己的项目里复用的东西。一份合格的 AgentManifest 至少包含以下字段agent_id全局唯一标识推荐用domain.app.agent这种命名空间格式避免冲突。display_name人看的名字用于管理界面展示。description用于路由匹配的语义描述不要写“这是一个天气助手”这种空话要写“能查询中国主要城市未来三天的天气支持气温、降水、风力查询返回结构化字段”描述越具体语义路由才能越准。capabilities能力清单数组形式每个能力带name、input_schema、output_schema。这个字段直接支撑精细化路由。protocol接入协议类型目前我定义了mcp、http-json、async-callback三种。transport具体的接入地址比如https://agent-weather.internal:8443/mcp。auth目标 Agent 认证方式与凭据引用凭据不要直接写在 Manifest 里而是引用安全存储里的 key。limits速率限制与超时配置比如max_qps、timeout_ms。这份 Manifest 用 JSON Schema 校验注册的时候不合法直接拒绝。为什么强调 schema因为我在早期版本吃过亏不同团队写的 Manifest 字段命名五花八门有的用input有的用params路由模块的解析逻辑差点写成一坨 if-else 地狱。定了 schema相当于给所有人的元数据上了一个硬约束省心太多了。3.2 路由策略从关键字匹配到语义匹配Agent-Reach 的路由层支持两种策略组合使用。第一种是结构化路由。调用方显式指定能力名称比如capability: query_weather路由层在注册表里精确匹配。这种方式稳、快、可预期适合内部系统对 Agent 能力非常明确的场景。第二种是语义路由。调用方只写一段自然语言意图比如“帮我看看上海明天适合户外跑步吗”路由层用 embedding 向量匹配和规则过滤结合的方式找到最合适的 Agent。这里有个细节我不能只靠向量相似度必须先做一轮能力过滤比如排除掉不含天气能力的 Agent再做向量排序否则语义相近但能力不符的 Agent 会被误召。生产环境我建议两种方式叠加显式能力优先自然语言兜底。还有一个小技巧——路由结果必须要带一个置信度阈值低于阈值就拒绝而不是硬把一个请求丢给不合适的 Agent。我现在默认阈值是 0.6出问题的时候调日志分析就够了。3.3 鉴权与会话上下文容易被忽视的两个坑跨系统调用鉴权是硬门槛。Agent-Reach 的鉴权分两层接入鉴权和转发鉴权。接入鉴权管的是“谁在调用 Agent-Reach”我用 API Key 加请求签名的方式签名体包含时间戳和请求摘要防止重放攻击。转发鉴权管的是“Agent-Reach 调用目标 Agent 时用什么身份”这一层直接透传或换成目标 Agent 认可的凭证具体由 AgentManifest 里的auth字段决定。会话上下文是我踩坑最多的地方。Agent 是有状态的用户问一句“那明天呢”你必须知道“那”指的是哪一天、什么城市。Agent-Reach 在内部传递session_id并且保存一份轻量的上下文映射记录当前会话最近一次路由到了哪个 Agent、当时用什么参数。但这不代表我默认替调用方存会话历史——上下文数据仍由各 Agent 自持我的会话映射只是为了保证路由连续性。这个边界要划清楚否则你就要变成一个会话存储中间件存储压力会完全失控。4. 从零搭建一个最小可用的 Agent-Reach4.1 环境准备与依赖动手前先明确任务我打算在本地开三个进程一个是 Agent-Reach 网关另外两个是模拟的异构 Agent一个天气 Agent一个日历 Agent。天气 Agent 用http-json协议日历 Agent 用async-callback协议这样等于把两种最常见的模式都覆盖到了。依赖方面Agent-Reach 本身我用 Python FastAPI 实现三个进程也全部用 Python 写方便演示。需要提前安装fastapi、uvicorn、httpx、pydantic。如果你要跑语义路由还需要加一个 embedding 服务但本地演示我先把语义路由降级成规则匹配避免环境依赖太重。启动顺序有讲究先启动两个 Agent再启动注册表最后启动网关。为什么因为注册表初始化的时候会去拉取已有 Agent 的健康状态如果 Agent 没起来注册表会把它标记为离线网关路由时自动跳过。如果顺序反了你会在日志里看到一堆连接拒绝排查起来平白浪费时间。4.2 注册两个模拟智能体天气 Agent 我用 FastAPI 写了一共大概 40 行。它暴露一个POST /weather/query接口接收{city: 上海, days: 3}返回一段 JSON。注意这个接口本身不知道 Agent-Reach 的存在它就是一个普通 HTTP 服务。它的 AgentManifest 长这样{ agent_id: demo.weather.forecast, display_name: 天气预报Agent, description: 查询国内主要城市未来3-7天天气预报支持气温、降水概率、风力风向, capabilities: [ { name: query_weather, input_schema: { city: string, days: integer }, output_schema: { city: string, daily: array } } ], protocol: http-json, transport: { endpoint: http://127.0.0.1:9101/weather/query, method: POST }, auth: { type: none }, limits: { max_qps: 20, timeout_ms: 5000 } }日历 Agent 稍微特殊一点因为它走异步回调。它的接口POST /calendar/create_event收到请求后立即返回{status: accepted, task_id: xxx}然后等 2 秒模拟耗时操作再主动回调 Agent-Reach 提供的回执接口POST /v1/reach/callback把真正的执行结果送回去。它的 Manifest 里protocol字段就是async-callback。把这两个 Manifest 丢给注册表有两种方式一种是调用POST /v1/registry/register另一种是配置目录扫描让注册表自动读取本地文件夹里的 Manifest 文件。我调试时用第一种上线后建议用第二种配合 Git 仓库管理 Manifest 的变更记录。4.3 通过 Agent-Reach 发起跨智能体调用一切就绪后调用方只需要面对 Agent-Reach 的入口接口POST /v1/reach/execute。请求体为{ caller: demo.app1, request_id: req-20250001, capability: query_weather, payload: { city: 杭州, days: 3 }, session_id: sess-001 }Agent-Reach 的处理流程是这样的先对调用方做接入鉴权然后去注册表找支持query_weather能力的候选 Agent 列表接着根据路由策略这里因为capability字段明确直接走结构化路由锁定天气 Agent然后做协议转换把ReachRequest映射成天气 Agent 的 HTTP 请求最后等待响应并原样返回给调用方。我实测下来最顺的情况一个请求走完不到 20ms不包括 Agent 处理时间瓶颈完全在目标 Agent 自身的响应速度上。如果目标是异步 Agent调用方会立刻收到202 Accepted之后通过回执接口通知或者调用方主动轮询GET /v1/reach/tasks/{task_id}拿结果。这里有个值得注意的细节request_id和session_id必须分开。request_id用于单次请求的链路追踪要全局唯一session_id用于会话延续同一次对话的多个请求应该复用同一个值。我见过有人把这两个字段混用一个结果在排查“上一个请求超时影响了下一个请求”的问题时完全没法区分链路只能怪自己没设计好。5. 常见故障排查速查与避坑实录5.1 我踩过的五个典型问题问题一请求 504 超时但目标 Agent 日志显示已经处理完了。排查后确认是响应体太大网关默认的响应缓冲太小大 JSON 传输被掐断。把网关的max_response_size调大就好了。经验是任何网关类组件都要提前验收大响应场景不要让用户在线上环境帮你发现这个。问题二注册表里 Agent 状态显示在线但路由的时候却被跳过。原因出在健康检查。我当时健康检查只查了 Agent 进程是否存活但没查它的依赖比如数据库连接池导致 Agent 进程活着却无法处理实际请求。后来把健康检查改成两层Liveness进程级加 Readiness依赖级Agent 就绪了才允许被路由。问题三异步回调丢消息。日历 Agent 执行完调用回调接口时Agent-Reach 的进程刚好重启了回调落到了一个不存在的通道上。后来我引入了内存重试队列并在回调接口里加了幂等键task_id重复回调不会重复入账。但内存重试队列重启还是会丢所以我给重要任务加了本地 SQLite 持久化重启后从待发送表里恢复。问题四语义路由召回了奇怪的 Agent。有一次用户问“帮我看下孩子几点放学”系统里面的“学校通知 Agent”和“家庭日程 Agent”都被召回了但这两个 Agent 实际上都不能直接回答这个问题因为学校通知 Agent 只接文件上传家庭日程 Agent 只管理成人的日程。问题出在向量匹配的候选集太大且没有做能力预过滤。后来我在语义路由前面强制卡了一道“能力标签必须匹配”的过滤器误召率明显下降。问题五不同环境下的 Agent 地址私网不可互达。开发环境注册的是http://localhost:9101测试环境网关跑在另一台机器上自然连接失败。这个问题的根源是配置管理不统一。重点是AgentManifest 里的transport.endpoint必须按环境注入不要在共享配置里写死 localhost。我后来用{base_url}占位符来做环境适配化学解决了“换环境改一串地址”的老大难。5.2 故障排查的通用思路如果你也打算做一个类似的中控网关建议一开始就把链路追踪的字段设计好。我给每条请求都打上了trace_id从调用方进入 Agent-Reach 开始一直到目标 Agent 处理完返回全链路日志都带这个 ID。出故障的时候用grep trace_id一卷一眼就能定位问题卡在哪一段。如果日志里发现请求根本没出网关优先查路由匹配和鉴权。路由匹配查 Agent 是否在线、能力是否声明正确鉴权查调用方Key 是否过期、签名时间戳是否偏差过大。如果请求已经出了网关但没收到响应优先查目标 Agent 的处理日志和网络策略。总之按“先内后外、先路由后链路”的次序排查效率最高。最后再分享一点我的实际体会Agent-Reach 这套东西做下来我最大的感触是智能体互联的难点真不是模型能力而是工程治理。你把目录、路由、鉴权、会话、追踪这五个基础层做扎实了后面无论是接 MCP 还是接 A2A都是水到渠成的适配问题。如果你目前只是三五个人用的内部 Agent 系统没必要一上来就上 K8s 或者 Service Mesh 那套重武器用 Agent-Reach 这个思路做轻量实现把数字框死Manifest 必须写、路由必须有阈值、鉴权必须两层、链路必须留痕。这四条守住系统就乱不到哪里去。