前两天在我们那个 agent 技术交流群里,有人问了一个特别实际的问题:“我现在手里有一个负责库存查询的 agent,还有一个负责订单流转的 agent,怎么让它们之间直接对话?”大家七嘴八舌,有人推荐 MCP,有人贴了自己封装的 HTTP 调用,有人提议上消息队列。讨论到最后,其实谁都没给出根治的办法,因为大家遇到的不是“协议不够多”的问题,而是 agent 之间的发现路径和触达路径太乱了。Agent-Reach 这个项目,就是冲这个来的。
Agent-Reach 是一个开源的 agent 间通信中间件,核心功能只有三块:能力注册、路径发现、消息路由。它相当于在各类 agent 之上铺了一层轻量的“寻址网络”,让运行在不同框架、不同进程、甚至不同机器上的 agent,可以通过同一套注册与路由机制识别彼此、交换消息。无论你是做多 agent 协作类产品,还是做企业内部复杂流程自动化,凡遇到“我的 agent 需要调用另一个 agent”的场景,这套东西都能用得上。
这篇文章我会把 Agent-Reach 从设计背景、核心协议到部署实操、踩坑记录一次讲透。内容更适合已经上手过至少一个 agent 框架、准备把 agent 放进真实业务系统的开发者。如果你还在纠结单个 agent 的 prompt 怎么写,可以先收藏,等做到多 agent 协作阶段再回来看,会更对味。
1. Agent 变成了一座座孤岛,这是 Agent-Reach 要解决的根本问题
先说清楚背景。2024 到 2025 年这波 agent 浪潮里,最热闹的不是推理能力,而是“怎么把多个 agent 串起来”。LangGraph、AutoGen、CrewAI、Semantic Kernel,各家的多 agent 编排各有各的江湖套路,但本质上都还是“一个主控拉着多个子 agent 跑”的集中式玩法。这套玩法在小规模演示里问题不大,一旦 agent 数量上到几十个、归属到不同团队、部署到不同机器,问题就全暴露了。
这里有个关键概念要区分:编排(orchestration)和互联(interconnection)不是一回事。编排是“谁来调度谁”的控制逻辑,互联是“agent 之间怎么找到对方、怎么把消息送过去”的基础能力。现在大多数人只做了编排,互联基本靠硬编码。我在一个真实项目中见过最极端的场景:A 团队用 LangGraph 写了三个法律检索 agent,B 团队用 AutoGen 写了两个合同审查 agent,两边要协作,于是程序员手工把对方的 service URL 写死在自己的代码里,再约定 JSON 格式。第一次联调花了两天,之后每次谁改一下接口都得互相通知。这不是技术问题,这是架构退化。
1.1 “每个 agent 一套协议”的碎片化局面
碎片化首先体现在协议层。有的 agent 通过 HTTP JSON-RPC 暴露能力,有的走 WebSocket,有的用 gRPC,有的干脆只在内存里调用。五花八门本身没问题,互联网本来也是各协议并存,但 agent 通信缺的不是传输协议,而是对“能力描述”的统一理解。
举个例子:一个“天气查询 agent”对外暴露了一个接口,参数是城市名,返回体里带温度、湿度、风力。但这三个字段缺省代表什么?城市名支持哪些写法?接口报错时的错误码体系是什么?限流是多少?如果这些信息只存在于函数签名里,另一个 agent 想要调用它,只能把文档读一遍,再写一堆适配代码。Agent-Reach 的做法是把“你能干什么、怎么调用你、你的状态怎么样”抽象成标准的能力描述,统一注册、统一发现。这样一来,调用方只需要查一次注册表就知道该调用谁、用什么样的请求结构。
1.2 传统服务发现方案为什么在 agent 场景失灵
有人说,这不就是微服务里的服务发现吗?我拿 etcd 或者 Consul 注册一下不就行了?我在早期也这么想过,但做下来发现两者差距很大。传统服务发现解决的是“哪个 IP 提供哪个服务”的问题,其核心粒度是服务实例,一个服务通常只有少数几个接口,接口签名相对稳定。agent 的粒度是什么呢?是能力。同一个 agent 可能同时具备“总结文档”“提取实体”“判断舆情”三个能力,而这三个能力可能在同一个进程内,也可能分别由不同进程承担。
更麻烦的是,agent 的能力具有上下文相关性。同一个“客服 agent”,在高峰期可能只接受简单问答请求,拒绝深度分析请求;在低峰期则放开全部能力。传统服务发现框架的健康检查只能回答“你活着吗”,回答不了“你现在能干吗”。Agent-Reach 在设计时,把健康检查扩展成了“能力可达性评估”,每个 agent 不仅要上报存活状态,还要上报当前能承接的负载、可用的能力子集、期望的调用频率。这个信息最终会变成路由决策的依据。
我用一个类比帮助理解:传统服务发现像电话黄页,你查到一个公司电话打过去就行;Agent-Reach 更像一个带实时状态更新的外卖平台,每个商家不但告诉你“营业中”,还告诉你“现在单多,配送可能慢”。前者是静态关系,后者是动态关系。agent 之间通信恰恰需要这种动态性。
2. 架构设计核心思路:从“注册中心”到“可达性网络”
Agent-Reach 的整体架构不复杂,但有几个设计取舍我需要重点讲。它没有尝试做一套全新的传输协议,而是把重心放在两件事上:标准化的能力描述和动态路由决策。传输层可以用 HTTP、WebSocket、gRPC,甚至可以用本地内存通道,Agent-Reach 只关注“消息从 agent A 到 agent B 的路径怎么确定”。
2.1 能力注册表:agent 的“活电话簿”
整个系统的第一块基石是能力注册表。每个接入 Agent-Reach 的 agent,启动时必须向注册节点提交一份能力声明,内容包括:agent 的唯一标识、负责的能力列表、每个能力的输入输出 schema、当前负载状态、连接地址偏好、心跳周期。注册表不是一个静态数据库,而是一个持续更新的分布式视图。
我在早期版本里直接把注册信息存在 Redis 里,key 是 agent_id,value 是能力 JSON,加个 TTL 当心跳超时。这套做法在 10 个 agent 以内跑得很欢,但后来测试到 100 个 agent 同时注册、频繁上下线时,问题就来了:首先是 Redis 的 key 过期扫描有延迟,agent 已下线但注册信息还在路由表里留存了几十秒,调用方会一直往黑洞里发消息;其次是能力查询需要支持“按能力名反查 agent”,我在 Redis 里不得不维护大量二级索引,数据一致性开始变得棘手。
现在的实现改用了一种混合拓扑:每个接入点维护一份局部注册表,记录直接挂在自己下面的 agent;接入点之间通过 gossip 协议交换摘要信息,定期同步可达性状态。gossip 协议这个词听起来高级,其实就是“每隔几秒把自己知道的信息告诉邻居,邻居再告诉他们的邻居”,跟村里大妈传播消息一个原理。这种做法的好处是不会有单点故障,即使某个接入点挂了,其他接入点仍然能从邻居那里拿到大部分路由信息。
2.2 可达性评估:让“谁能找到谁”动态化
第二个核心设计是可达性评估(Reachability Scoring),这也是项目名字 Agent-Reach 的由来。一个 agent“可被调用”并不等于“应该被调用”。Agent-Reach 会给每一条 agent 到 agent 的候选路径计算一个综合分数,路由时优先选分数高的路径。分数由四个因子组成:
- 最近成功率:过去 5 分钟内调用成功的比例。如果有一次超时或返回错误,分数立刻往下走。
- 响应延迟:最近 10 次调用的平均响应时间,延迟越短分数越高。
- 负载状态:agent 自己上报的当前任务队列长度或 CPU 占用百分比。负载超过 70% 的 agent,分数打五折。
- 网络距离:在同一台机器上的 agent 分数最高,同一局域网次之,跨公网最低。
这四个因子加权求和,默认权重是 4:3:2:1。你可以通过配置文件调整。为什么要做动态打分,而不是像负载均衡那样简单轮询?因为 agent 之间的调用往往不是等概率的。一个 agent 可能长时间空闲,突然来一批任务就积压了。轮询会把新请求继续发给已经积压的 agent,而基于可达性分数的路由,会自动把流量引导到更空闲的副本上。这个过程是自适应的,不需要人工干预。
2.3 路由与转发:不重建传输,只优化路径
Agent-Reach 的第三层是路由转发层。它的职责是:收到一个 agent 发来的消息,解析目标能力名,查询可用 agent 列表,按可达性分数排序,然后把消息转发给最优目标。如果最优目标不可达,自动退回到第二优目标,整个过程对调用方透明。
这里有一个容易混淆的点:Agent-Reach不是一个消息队列。它不做消息持久化,不保证消息的堆积能力。它的定位是同步请求-响应的路由层,就像一个快递柜,只管把包裹放到正确的格口,不管包裹在格口里放多久。如果业务需要异步解耦,建议在 Agent-Reach 后面再接消息队列,两者是互补关系。
关于转发方式,我做了两种模式。第一种是代理模式,调用方把消息发给 Agent-Reach 节点,由节点转发给目标 agent,再把响应原路返回。这种模式对调用方最简单,只需要知道 Agent-Reach 节点的地址。第二种是直连模式,Agent-Reach 只负责返回目标 agent 的连接信息,调用方自己直接连目标。直连模式省掉一跳,延迟更低,但调用方需要额外处理连接建立和鉴权。我的建议:对内网环境用代理模式就够了,跨公网且对延迟敏感时再考虑直连。
3. 核心协议与实现细节:注册表、封包、鉴权怎么落地
第 2 节讲的是设计思路,这一节看代码和协议层面的具体实现。Agent-Reach 的协议设计遵循一个原则:消息的结构要稳定,消息的内容要灵活。能抽象成公共字段的,都放进信封(envelope);业务相关的数据,全部塞进 payload。
3.1 能力描述的数据模型
先看最基础的能力描述结构。每个 agent 注册时提交的数据遵守下面的格式,我简化掉了一些可选的扩展字段,保留核心部分:
{ "agent_id": "legal-review-agent-01", "display_name": "合同审查助手", "capabilities": [ { "name": "contract.clause_check", "version": "2.1.0", "input_schema": { "type": "object", "properties": { "contract_text": {"type": "string"}, "focus_areas": {"type": "array", "items": {"type": "string"}} }, "required": ["contract_text"] }, "output_schema": { "type": "object", "properties": { "issues": {"type": "array"}, "risk_level": {"type": "string"} } } }, { "name": "contract.summarize", "version": "1.0.0", "input_schema": { "type": "object", "properties": { "contract_url": {"type": "string"} }, "required": ["contract_url"] }, "output_schema": { "type": "object", "properties": { "summary": {"type": "string"} } } } ], "endpoint": { "type": "http", "url": "http://10.0.3.21:8080/rpc", "prefer_proxy": true }, "heartbeat_interval_sec": 30, "max_concurrent_requests": 10, "current_load": 3 }注意几个容易被忽略的细节。第一,capabilities 是一个数组,一个 agent 可以上报多个能力,路由时按能力名精确匹配。第二,input_schema 和 output_schema 用的是 JSON Schema 标准,没有自己发明格式,这样任何语言的 agent 都能解析。第三,current_load 是一个动态字段,agent 可以在心跳消息里不断更新它。第四,endpoint 里的 prefer_proxy 字段告诉路由节点:如果这个 agent 不方便被外部直连,就请走代理转发模式。
为什么要用 JSON Schema 而不是简单写个字段列表?因为 agent 的输入输出往往嵌套很深,简单的字段列表表达不了层级关系,而且在路由时,Agent-Reach 可以通过 JSON Schema 做前置校验。也就是说,如果调用方发来的 payload 不符合目标 agent 声明的 input_schema,路由节点直接拒绝转发,连目标 agent 都不用打扰。这个机制能挡掉一大半低级错误。
3.2 消息封包与路由标识
Agent-Reach 的消息信封按下面的结构设计:
envelope: version: "3.0" msg_id: "8f2e9c1a4b7d" trace_id: "trace-20240605-001" msg_type: "request" # request | response | heartbeat | broadcast source_agent: "inventory-checker" target_capability: "contract.clause_check" # 路由依据 target_agent: "" # 可选,指定具体 agent persistent: false # 是否需要可靠投递 ttl_sec: 60 priority: 5 # 1~9,数字越小优先级越高 timestamp_utc: "2025-06-05T06:12:33Z" payload: # 业务数据,遵循目标能力声明的 input_schema路由节点拿到消息后,处理顺序是:校验 msg_id 是否重复(通过布隆过滤器去重)、解析 target_capability、查询注册表得到候选 agent 列表、按可达性分数排序、转发。如果 target_agent 字段非空,则跳过能力查询,直接指定目标。ttl_sec 字段是防呆机制:消息在路由节点队列里呆超过这个时间就直接丢弃,返回超时错误,避免死循环。
这里有个设计细节值得展开:为什么要区分 target_capability 和 target_agent。在微服务时代,你调用一个服务基本是“指名道姓”的,因为每个服务就一组能力。但 agent 时代,能力可能分散在多个 agent 上,也可能多个 agent 共享同一种能力。比如你有三个 agent 都实现了聊天气象解析。调用方关心的是“我要天气解析能力”,不关心具体是哪一个 agent 提供服务。所以 Agent-Reach 默认按能力寻址,只在特殊情况下按 agent 寻址。前一种方式是“按需路由”,后一种方式是“点对点路由”,两种都要支持。
3.3 身份认证与权限控制
Agent-Reach 的身份体系借鉴了零信任的思路:每个 agent 首次接入时生成一对密钥,私钥保存在 agent 本地,公钥注册到注册节点。所有消息在传输时都要带签名,路由节点验签通过才转发。这么做的好处是:即使注册表数据被泄露,攻击者也无法冒充一个合法 agent 去发起调用,因为私钥不在注册表里。
权限控制采用“能力白名单”模型。每个 agent 在注册时不仅要声明自己能提供什么能力,还要声明它有权调用哪些能力。这个调用白名单由注册节点的管理员统一维护。我建议在权限配置上宁可先收紧,再逐步放开。你可以在配置中心维护一个 per_agent_call_permission 表,最细粒度精确到“agent A 能不能调用 agent B 的 contract.summarize 能力”。
还有一个容易被忽视的点:容量自保护。单个 agent 的 max_concurrent_requests 要跟实际处理能力匹配。如果调用方疯狂并发调用,agent 来不及处理,反而会拖垮整个链路。Agent-Reach 里每个注册的 agent 都可以设置并发上限,路由节点转发前先检查目标当前并发数,若已满则等待或退避。
4. 实操记录:从零搭建一套多 agent 互通环境
思路和协议都说完了,下面进入实操。我用一个最常见的业务场景演示 Agent-Reach 的落地过程:一个“库存查询 agent”要和“订单处理 agent”互通,顺便接入一个“合同审查 agent”。整个环境我放在同一台服务器上模拟,用 Docker Compose 一键拉起。
4.1 环境准备与架构拓扑
我做实验时用的服务器配置很一般:4 核 8G 内存的云主机,Ubuntu 22.04,Docker 24,Docker Compose v2。Agent-Reach 本身对资源要求很低,控制面节点内存占用大概 150MB,路由节点 100MB,剩下的都是 agent 业务进程。
拓扑是这样的:三个 agent 分别用 Python 写了独立进程,模拟“三个不同团队维护的 agent”。它们之间不直接知道对方的地址,所有通信都通过 Agent-Reach 路由节点完成。另一个节点作为注册中心,负责接收心跳、维护能力注册表。这个拓扑里注册中心只有单节点,生产环境建议至少跑三个节点做 gossip 组,我为了演示简洁就单机单节点了。
4.2 配置文件初始化
Agent-Reach 的初始化非常轻量,下载发行包后只需要两个文件:一个 config.yaml 描述节点角色和网络参数,一个 bootstrap.json 描述初始集群成员。我的 config.yaml 关键部分长这样:
node: role: router_and_registry # 演示环境合并角色,生产建议拆开 listen_addr: "0.0.0.0:7981" gossip_peers: - "192.168.1.10:7981" - "192.168.1.11:7981" registry: heartbeat_timeout_sec: 90 capability_ttl_sec: 180 prune_interval_sec: 30 routing: score_weights: [4, 3, 2, 1] # 成功率、延迟、负载、网络距离 retry_on_failure: true max_retry_count: 2 connection_pool_size: 64 auth: public_key_store: "./keys/public_keys.json" require_signature: true logging: level: "info" access_log_enabled: true路由里的 score_weights 我特意标出来了。默认权重 4:3:2:1,如果你们的场景对延迟极其敏感,可以把延迟的权重调高,比如 [2, 5, 2, 1];如果最怕调用失败,就加大成功率权重,比如 [6, 1, 2, 1]。调参没有绝对最优,主要看你线上真实的失败原因比例。
启动节点一条命令:
$ ./agent-reach --role router_and_registry --config config.yaml看到日志出现 "gossip sync complete, peers=2, registered_agents=0" 就说明节点起来了。
4.3 用 Python SDK 注册第一个 agent
我写了一个最小的 Python agent,它提供一个“stock.query”能力。关键部分如下:
from agent_reach import AgentReachAgent # 1. 初始化 agent,加载本地私钥 agent = AgentReachAgent( agent_id="inventory-query-agent", private_key_path="./keys/inventory_agent_ed25519.pem", capability=[ { "name": "stock.query", "version": "1.0.0", "input_schema": { "type": "object", "properties": { "sku": {"type": "string"} }, "required": ["sku"] }, "output_schema": { "type": "object", "properties": { "sku": {"type": "string"}, "quantity": {"type": "integer"} } } } ], endpoint={"type": "http", "url": "http://127.0.0.1:9101/rpc"}, heartbeat_interval_sec=30, ) # 2. 启动 RPC 服务,监听来自路由节点的调用 @agent.rpc_handler("stock.query") def handle_stock_query(request: dict) -> dict: sku = request["sku"] # 这里原来是查数据库,我 mock 一下 return {"sku": sku, "quantity": 42} # 3. 注册并保持心跳 agent.start()运行这个脚本后,日志里会出现一行:agent registered, id=inventory-query-agent, capability_count=1。此时你去注册中心查询,会看到这个 agent 的状态是 available,score 是默认值 10.0。
一个小细节:heartbeat_interval_sec 是 30,注册中心的 heartbeat_timeout_sec 是 90。也就是说,如果 agent 连续三次心跳没有上报,注册中心才会把它标记为 unavailable。三次的冗余是用来容忍网络抖动的。如果你需要更快的故障切换,可以把 timeout 调到 45 秒,但要注意这也会增加误判频率。
4.4 用注册表发现并调用能力
第二个 agent,也就是“订单处理 agent”,我现在注册它去调用库存查询。配置好私钥和调用白名单后,它的主动调用代码是这样的:
from agent_reach import AgentReachAgent client_agent = AgentReachAgent( agent_id="order-processing-agent", private_key_path="./keys/order_agent_ed25519.pem", capabilities=[], # 这个 agent 不对外提供服务 ) def check_stock(sku: str): request = { "msg_type": "request", "source_agent": "order-processing-agent", "target_capability": "stock.query", "payload": {"sku": sku}, "ttl_sec": 30, } response = client_agent.call(request) return response if __name__ == "__main__": result = check_stock("SKU-88213") print("库存查询结果:", result)调用方的代码比我想象中短很多,因为握手、寻址、签名、重试这些都是 SDK 内部处理的。实测从发出请求到拿到响应,内网环境大约 8~15ms,其中路由开销占 2~4ms,剩下是业务处理时间。
这里我想说一个调用方最常见的认知误区:调用方不需要关心目标 agent 的 URL。你只声明“我要调用哪个能力”,路由层帮你找到最合适的 agent。如果你的业务里存在多个提供“stock.query”的 agent,比如一个查主仓库存、一个查地区仓库存,你可以不指定 agent,让路由层按分数选;也可以强制 target_agent 指定某个 agent,精确调用。
4.5 验证路由容错
实操有一个必测场景:把一个 agent 手动停掉,观察调用方能否自动切换到其他可用 agent。我在实验里起了两个“stock.query”能力相同的 agent,一个开在 9101 端口,一个开在 9102 端口。杀掉 9101 端口的 agent 后,连续调用了 20 次:
- 前 3 次调用返回成功,因为注册中心还没摘除旧节点,消息发给了 9101,但连接失败后路由层自动重试,第二次就转到了 9102。
- 第 4 次之后的调用完全走 9102,注册中心的记录里已经看不到 9101 的 agent 了。
- 整个过程调用方无感知,响应时间从平均 10ms 变成了平均 12ms,多出来的 2ms 是路由层重试退避的影响。
这说明 retry_on_failure 机制在工作。但我要提醒:默认的 max_retry_count 是 2,如果两次重试都失败,调用方会收到一个路由错误响应。在生产环境,重试次数不宜设得太高,否则在目标 agent 大面积故障时,所有请求都会卡在重试队列里,造成响应雪崩。
5. 常见问题与排查技巧实录
Agent-Reach 虽然设计上尽量简洁,但我在实际部署和维护中还是踩了不少坑。这一节我把高频问题按现象分类,每个问题都给出排查思路和定位方法。这部分应该是最值钱的,因为这些经验都是线上跑出来的,不是文档里能查到的。
5.1 注册后查不到或状态一直为 pending
现象描述:agent 进程启动后,日志显示注册成功,但注册中心查询结果里始终是 pending,或者直接查不到。
我遇到这类问题时的排查顺序是:
- 先看心跳。在 agent 日志里搜索
heartbeat sent字样,确认心跳线程在跑。如果心跳没有发送,最常见原因是 agent 的时钟偏差过大,或注册中心地址配错了。 - 再看注册中心的
heartbeat_timeout_sec和 agent 的heartbeat_interval_sec比值。如果 timeout 设置得太小,比如低于心跳间隔的两倍,就可能出现“上报完了刚过期又来不及续”的情况。 - 看注册中心是否有冲突的 agent_id。两个不同 agent 用了同一个 id 注册,后面的会把前面的挤掉,也可能造成状态轮换。
最坑的一次是 agent 的机器时钟比服务器快了 3 分钟,心跳消息里的时间戳一直比注册中心的当前时间还晚,被当作异常消息丢弃。排查了半天才发现是 NTP 没同步。现在我的部署检查清单里,第一项就是校验时钟同步。
5.2 调用超时但目标 agent 明明在运行
现象描述:路由日志显示消息已转发,目标 agent 的日志也显示已收到请求并处理完成,但调用方就是超时。
这种问题十有八九出在响应回传链路。一个常见原因是响应消息的 target_agent 字段没有正确填充。某些 SDK 版本在处理异步回调时,会把响应消息的 source 和 target 弄反,导致路由节点不知道把响应送给谁。我做过一次抓包分析:请求消息的记录是全的,但响应消息的 trace_id 对不上,消息被路由节点的去重机制误判为重复消息丢弃了。
排查方法很简单:用 trace_id 串联日志。我在生产环境强制要求所有 agent 的日志输出带 trace_id,这样一条请求从调用方到路由节点、再到目标 agent、再返回,整个链路可以按 trace_id 一次性查询。如果中间哪一环日志断了,直接定位那一环。
还有一个容易忽略的地方:响应消息的 ttl_sec。有些 agent 处理业务特别慢,比如要调一次大模型推理,耗时会超过默认 ttl。如果 ttl 设得太短,响应还没发回来,路由节点已经把它丢弃了。我的建议是:对响应消息,ttl 至少要留目标 agent 业务处理平均耗时的 3 倍。
5.3 路由总是选错 agent,或者流量不均匀
现象描述:有多个 agent 提供同一能力,但路由层总是把请求发给同一个 agent,另一个几乎不收到流量。
这种情况是可达性分数做出来的结果:那一个 agent 的成功率、延迟、负载各项表现都更好,所以分数一直最高,路由层自然一直选它。这不是 bug,是机制。但这会导致一个隐患——如果这个最优 agent 真的过载了,它的负载因子会拉低分数,路由最终还是会切换。只是负载因子的权重默认是 2,占比不高,切换效果没那么灵敏。
想让流量更分散,有两个调整思路:
- 把 score_weights 的负载权重调高,比如改成 [4, 3, 4, 1],让负载对分数的下拉作用更明显。
- 在路由层开启随机化:对分数排名前 N 的候选做加权随机,而不是永远选第一名。加权随机在我自己的项目里效果很好,让流量均匀了,整体吞吐不降反升。
这个改动在 config.yaml 里加一个字段即可:
routing: select_mode: "weighted_random" select_top_n: 35.4 重试导致的重复调用问题
现象描述:目标 agent 处理请求成功了,但响应在回传路上丢了,路由层触发重试,导致目标 agent 收到两条相同的请求。如果该能力不是幂等的,就会造成重复扣款、重复发消息这类严重事故。
这是所有分布式系统都要面对的问题,Agent-Reach 也不例外。Agent-Reach 的门面解法是 msg_id 去重:路由节点会记录最近处理过的 msg_id,用布隆过滤器保存约十万条记录,重复的请求直接丢弃。但注意,去重只发生在路由节点,如果调用方和路由节点之间的网络出了问题,调用方自己发起重试,又生成了新的 msg_id,那去重就失效了。
我的经验是两条:第一,幂等性必须在业务 agent 内部做,agent 的每个能力都应该接收业务幂等键,比如订单号、request_id,在业务数据层去重;第二,调用方的 SDK 重试逻辑要配置“只在连接失败时重试,响应超时不重试”。连接失败说明消息一定没到对面,可以放心重试;响应超时则不确定对面是否处理了,盲目重试有风险。这两个原则写在我团队每个 agent 开发规范的第一页。
5.5 可观测性:不要等到线上故障才开始补日志
我在 Agent-Reach 里内置了 access log 和 metrics 接口。metrics 暴露三个核心指标:每秒消息吞吐量、路由队列积压数量、注册表里 available 的 agent 数量。这三个指标属于“看到数字不对劲再看其他”的入门指标。
我推荐每个接入 Agent-Reach 的 agent 至少打三行日志:收到请求时打一行(带 trace_id 和 payload 摘要)、开始处理时打一行(记录耗时起点)、返回响应时打一行(记录结果)。这三行日志的成本极低,但排查问题时价值极高。很多 agent 开发同学喜欢在代码里写“debug 日志”,线上默认级别是 info,导致这些日志全被滤掉,一排查就只能加日志重新发版,这是最笨的排查方式。
还有一个技巧:刚开始做多 agent 系统时,给每个 agent 起一个容易读的名字,比如“inventory-query-agent”而不是“svc-a1”,差之毫厘,排查时却能省半小时。
6. 几点实战感悟与后续可以扩展的方向
Agent-Reach 这个项目做下来,我最大的感受是:多 agent 通信的瓶颈往往不在技术上,而在“有没有人愿意定义统一的能力描述”。只要每个 agent 愿意把自己的能力、输入输出 schema、负载状态交到一个标准中心,后面的路由和调度就是水到渠成的事。难的是让不同团队、不同技术栈的 agent 开发者接受同一套规则,这是组织协同问题,比技术问题难解得多。
我个人在实际使用中的体会是,这类中间件的核心价值不在于它有多强,而在于它能不能让你在新增一个 agent 时不需要改任何调用方的代码。做到这一点,你的多 agent 系统才算是有了“生长能力”。我现在新增 agent 的流程基本就是:写一个脚本、注册能力、在权限白名单里加一行,完事。这种体验一旦习惯了,就再也回不去那种硬编码服务地址的苦日子了。
如果后续继续演进,我觉得有三个方向值得投入:一是把可达性分数做得更智能,引入历史故障预测和流控反馈,让路由不再只靠本地统计;二是做跨集群的联邦注册,让多个企业内部的 Agent-Reach 网络可以相互授权、局部分享能力,而不是全部集中到一个大节点;三是跟主流 agent 框架深度集成,让 LangGraph、AutoGen 的 agent 通过一个 SDK 装饰器就能接入,不需要改框架内部逻辑。这些方向技术上都是可行的,难的是场景足够多、用户足够广。希望这篇文章能给你一些启发,如果你也在做类似的多 agent 互联,欢迎一起聊聊实际踩过的坑。