
我最近在做一个叫 Agent-Reach 的小项目。名字拆开就是 Agent Reach智能体到底能“够到”多少工具、多少服务、多少别的智能体。这个项目解决的是 AI Agent 之间的发现、连接与安全触达问题你可以把它理解成一张专门给 Agent 用的“通讯录 路由表”。如果你手头有一堆 Agent每个都能单独干活却互相不知道对方能做什么、也不知道该怎么安全地调用那这篇笔记就是给你准备的。先说结论Agent-Reach 不是一个新模型也不是什么复杂框架它更像一层很薄的“智能体连接层”。我把它做成统一入口让 Agent 能注册自己、声明能力、被其他 Agent 发现并且通过一套打分机制决定“谁最适合处理这次请求”。下面我把整个项目的来龙去脉、架构取舍、实操步骤和踩坑记录都拆开讲清楚。1. Agent-Reach 到底在解决什么问题如果你也维护过两三个以上的 Agent一定遇到过这种尴尬局面A Agent 会查天气B Agent 会写周报C Agent 能操作内部系统但它们之间没有任何联系。你想让 A 把天气数据塞进 B 的周报里只能自己写胶水代码或者手工拷数据。这就是典型的“Agent 孤岛”。Agent-Reach 想解决的就是三个非常具体的问题。第一是发现。一个 Agent 怎么知道另一个 Agent 存在它能提供什么能力能力入口在哪里我见过很多人把 Agent API 地址硬编码在配置里换一台机器、改一个端口整个链路就断了。Agent-Reach 在项目里塞了一个注册中心的概念每个 Agent 上线时把自己能力和地址登记上去其他 Agent 通过查询就能知道“找谁干活”。第二是路由。当多个 Agent 都能完成同一个请求时到底该选哪个这里不能只靠“谁先注册谁上”还要看响应延迟、历史成功率、调用成本。Agent-Reach 会给每个候选 Agent 算一个可达分按从高到低排序再把请求转给最合适的那个。第三是安全。Agent 之间互相调用最容易出问题的就是权限失控。如果每个 Agent 都有对方管理后台的完全权限那整个系统就是裸奔。Agent-Reach 在调用链上加了能力级别的最小授权一个 Agent 只能调用对方显式声明过的能力拿不到无关接口。有人可能会说这不是和 MCPModel Context Protocol这类东西重复了吗我的理解不太一样。MCP 解决的是“模型如何标准化调用外部工具”它更像是给模型接上鼠标键盘Agent-Reach 解决的是“一个完整的 Agent 如何接上另一个完整的 Agent”它更像是给每个 Agent 装一张名片和一部电话。两者可以配合用Agent-Reach 底层调用某个能力时完全可以把 MCP 当作执行通道。2. 整体设计我为什么把它做成三层Agent-Reach 的核心不是一个单体服务而是一套约定加一个轻量级服务端。我把整个系统拆成三层注册层、路由层、执行层。每一层只干一件事这样调试的时候就能很快定位问题。注册层负责“身份和能力声明”。每个 Agent 启动以后带着自己的 manifest 文件来注册。manifest 里写清楚 agent_id、版本号、能力列表、输入参数格式、回调地址。注册层把这些信息存进一个轻量的存储里同时定期做健康检查发现某个 Agent 连续几次心跳超时就把它标记为离线避免请求打到一个已经挂掉的服务上。路由层负责“请求匹配和打分”。我收到一个请求时会先根据能力名过滤出一批候选 Agent然后对每个候选算一个可达分。这个分数不是拍脑袋定的它由三部分构成历史成功率、最近五次调用的平均延迟、以及调用成本权重。算分公式我用得非常简单reach_score success_rate * 0.5 (1 - normalized_latency) * 0.3 cost_weight * 0.2其中 normalized_latency 是把延迟压到 0 到 1 之间的值我用的方法是最小最大归一化(latency - min_latency) / (max_latency - min_latency)。如果某个 Agent 延迟特别高它的 latency 接近 1那1 - normalized_latency就接近 0得分自然被拉低。成本权重更直白按每次调用消耗的 token 或者内部计价来填便宜又快的 Agent 容易排到前面。执行层负责“最后的调用落地”。它把外部请求统一翻译成目标 Agent 能理解的格式带上身份凭证发起 HTTP 调用接收结果并且写一条审计日志。这一层还做了超时控制和熔断如果某个 Agent 连续三次超时执行层会把它暂时移出候选列表避免整套系统被一个慢节点拖垮。三层之间的关系非常清晰请求先打到路由层路由层去注册层拿候选选出最优后交给执行层执行层再去找目标 Agent。我把服务端做成了单机可跑的 FastAPI 应用存储先用 SQLite等流量大了再换 PostgreSQL。为什么这么做因为初期这种工具最怕过度设计单机版够用才能把精力放在能力声明和路由逻辑这类核心问题上。这里有一个很重要的设计取舍Agent-Reach 不对 Agent 的内部实现做任何约束。目标 Agent 可以是用 LangChain 写的、用 AutoGen 写的甚至是一段简单的 Python 脚本。只要它暴露一个标准 HTTP 接口并且能读懂 Agent-Reach 传过来的请求格式就能接入。这样我那些历史项目不用重写只需要加一层薄薄的适配就进去了。3. 实操从零搭一个 Agent-Reach 节点我搭这个项目时把“能跑通”当作第一优先级。整个最小版本只用了不到三百行代码但每一步都有讲究。下面我把完整流程写下来你照着走一遍就能复现。3.1 先准备一份 Agent 能力声明文件Agent-Reach 的一切都从 manifest 开始。我给自己的“天气助手”写了一份声明{ agent_id: weather-cn, display_name: 天气查询助手, version: 2.1.0, capabilities: [ { name: weather.query, description: 查询指定城市未来几天的天气, input_schema: { city: string, date: string }, cost_weight: 0.3 } ], endpoint: http://127.0.0.1:39001/invoke, auth_token: Bearer wthr-token-2024, heartbeat_interval: 30 }这份文件看起来简单里面有几个容易被忽视的细节。capabilities[].name必须全局唯一而且最好用“领域.动作”的命名方式比如weather.query不要用什么get_weather_info这种含糊的名字。因为路由层是按能力名精确匹配的命名不对系统就会认为这个 Agent 不提供这个能力。input_schema也很重要它不只是给别人看的文档路由层会用它做参数校验避免一个 Agent 收到自己根本不认识的字段。auth_token是目标 Agent 调用时要用到的凭证Agent-Reach 会把它安全地保存在服务端不会明文出现在日志里。3.2 注册到 Agent-Reach 服务端注册动作本身非常直接curl -X POST http://localhost:8800/agents \ -H Content-Type: application/json \ -d weather-manifest.json服务端收到之后会做三件事解析并校验 manifest 格式、生成一个唯一的注册 ID、启动后台心跳任务。如果校验失败服务端会返回具体的错误字段这一点比很多“出错只给个 500”的接口要友好得多。我第一次测试时漏了capabilities字段返回信息直接标出了缺失路径省了不少排查时间。注册接口设计成幂等的也就是同一个 agent_id 多次注册不会重复创建记录。我特意这样处理是因为 Agent 重启必然会导致重新注册如果每次都新建一条存储里很快就是一堆废弃数据。现在的逻辑是发现相同 agent_id 就做更新操作只保留最新一次注册信息。3.3 用一次“触达查询”测试路由能力注册完事不代表通你还需要确认 Agent-Reach 确实能找到这个 Agent。我准备了一个/reach接口来干这个事curl -X POST http://localhost:8800/reach \ -H Content-Type: application/json \ -d { query: 北京明天天气, capability: weather.query, params: { city: 北京, date: 2025-03-21 } }服务端收到请求后会先把能力名weather.query和当前所有可用 Agent 的能力列表做匹配然后按 reach_score 排序返回候选。初期只有一个 Agent匹配结果肯定只有它一个但这个接口的价值在于当你以后挂了十几个 Agent它能一眼告诉你“谁会处理这个请求以及为什么是它”。我在返回结果里还加了一个debug_reason字段直接写出每个候选的分数构成。这样即使选错了也能看到是成功率拖了后腿还是延迟太高。3.4 把路由结果接入真实调用路由结果拿到了最后一步就是调用。我写了个简单的 Python 客户端来验证整条链路import httpx async def reach_and_invoke(agent_reach_url, capability, params): async with httpx.AsyncClient(timeout10) as client: # 第一步从 Agent-Reach 获取最优候选 route_resp await client.post( f{agent_reach_url}/reach, json{capability: capability, params: params} ) route_resp.raise_for_status() candidates route_resp.json()[candidates] if not candidates: raise RuntimeError(没有可用 Agent 能处理这个能力) target candidates[0] # 已经按分数排好序 invoke_url target[endpoint] headers {Authorization: target[auth_token]} # 第二步直接调用目标 Agent invoke_resp await client.post( invoke_url, jsonparams, headersheaders ) invoke_resp.raise_for_status() return invoke_resp.json()这里有一个我特别坚持的细节请求参数在从路由层交给执行层时必须做一次 schema 校验。比如目标 Agent 声明需要city和date那传过去的请求里就不能多带location这种原字段。多传字段看似无害实际上很容易让目标 Agent 的解析逻辑产生歧义尤其是那些用自然语言 prompt 解析入参的 Agent字段一多模型就容易乱。我最初就踩过这个坑多传了一个city_code结果天气助手把 city 和 city_code 当成两个条件反倒判断不了城市的归属度最后只能重写解析逻辑。4. 常见问题与排查技巧实录项目跑了三个月我把最常遇到的问题整理成了一张速查表每一类都是真实踩过的坑。现象可能原因解决办法Agent 注册成功但/reach查不到能力名不匹配或 Agent 被判离线检查 manifest 中能力名是否完全一致查看心跳时间戳如果超过 TTL 需要改短 heartbeat_interval调用返回 401auth_token 无效或 scope 不足检查 token 是否对应目标 Agent 声明的能力在 Agent-Reach 后端查看审计日志里的 token 前缀偶尔出现超时但 Agent 本身没问题路由层对延迟打分不够敏感把 normalized_latency 的平滑窗口调小我用的是最近 5 次调用同时检查是否缺了连接池复用两个 Agent 互相调用导致死循环缺失层级控制在请求头里加x-reach-hop-limit: 5并且每次转发时把该值减一归零则拒绝转发熔断后恢复太慢熔断窗口设置过短我选用的是“连续 3 次失败进入半开状态下一次成功则关闭熔断”避免抖动期反复抖动排查这类问题时最有效的工具不是日志而是“触达追踪 ID”。我在 Agent-Reach 的每个入口请求都会生成一个reach_id从入站到出站全程透传。目标 Agent 只要在响应头里带上这个 ID我就能把所有日志串成一条完整的链路。你如果没有做这个设计排查的时候就只能靠时间戳猜非常痛苦。还有一个经验要特别提一下不要轻易在生产环境里开着“全部自动路由”。我测试时发现当两个 Agent 能力相似但返回格式不一样时自动路由很有可能会把请求发给格式不兼容的那个。后来我加了路由策略开关简单需求用“分数最高”复杂需求用“手动指定 Agent”并在 manifest 里新增了一个preferred_for字段声明该 Agent 更适合处理什么场景。比如“日报生成助手”虽然也会写周报但我在它的 manifest 里标注了“优先处理日维度数据”这样路由层在遇到周维度请求时就会自觉避开它。5. 个人经验几个值得一试的扩展方向Agent-Reach 这个项目我目前还在持续迭代如果你也想做一个类似的连接层我建议先从这几个方向延展。第一个方向是“能力集市”。注册层里已经存了每个 Agent 的能力声明不如把它做成一个可浏览的 UI 页面。我加了一个简单的/agents前端页面把每个 Agent 的能力、延迟、成功率都展示出来团队成员一眼就能看出哪些 Agent 空闲、哪些 Agent 负载高。很多人不重视可视化但在实际协同里能看得到“谁在线”比“调通接口”更能提高效率。第二个方向是“多环境隔离”。我现在用namespace字段区分开发、测试、生产三个环境。同一个weather.query能力开发环境里可能指向本地 mock 服务生产环境指向真实数据服务。路由层在匹配时不跨 namespace避免测试请求打到生产 Agent也避免生产请求被测试 Agent 接收。这个设计非常简单却是我被坑过两次以后才加上的。第三个方向是“回退链”。有些 Agent 支持天气查询但它依赖的外部数据源偶尔会断。我在执行层加了回退逻辑如果第一候选失败并且失败原因不是参数错误就自动尝试第二候选。回退之前会先判断两个 Agent 的返回结构是否兼容不兼容就直接放弃避免把错误格式的结果返回给调用方。这个逻辑让我在数据源故障时依然能保证核心服务可用。最后说点个人体会。Agent-Reach 这类项目最大的难点不在代码而在怎么约束 Agent 的行为。AI Agent 不像传统 API它的输入输出天然带着不确定性。所以我在实践中越来越依赖“能力声明”这个看似笨重的东西。没有能力声明系统就是一堆黑盒互相乱调有了能力声明哪怕 Agent 内部再狂野对外表现也是稳定可控的。我个人建议你在接入第一个 Agent 时别急着写路由和打分先用一个完整、明确的 manifest 文件跑通链路。声明写得越细致后面踩的坑就越少。