Agent-Reach:构建多智能体触达层的设计与实践
Agent-Reach 是我最近在团队里搭的一套多智能体触达层组件。它要解决的问题很具体:在多个 AI Agent 并存协作的场景里,A 怎么知道 B 存在、B 能做什么,以及怎样把一次请求安全、可控地送到 B 手里。如果这些全部靠硬编码去约定,系统会变得非常脆,加一个节点要改一堆代码,换一个模型又得重新发布。Agent-Reach 就是把这些事从业务代码里剥出来,做成独立的“触达基础设施”,让 Agent 之间既能互相找到,又不会因为拓扑变化而互相踩踏。
这套东西适合谁?主要面向正在做 AI Agent 平台、工作流编排、客服机器人、工具调用聚合的后端工程师和架构师。如果你现在手头只有两三个 Agent,可能还用不上,但一旦 Agent 数量超过五个,或者你需要让不同团队维护的 Agent 互相协作,就值得往下看。下面我把设计思路、接入方式和踩坑记录都摊开讲一讲。
1. Agent-Reach 是什么,为什么需要专门做一层触达
1.1 一次失控的 Agent 调用
我先讲个真实事故。之前我们在做一个企业知识库助手,内部拆了三个子 Agent:一个负责检索文档,一个负责调用业务系统接口,一个负责生成报告。最开始直接把三个 Agent 的接口地址互相写死,启动顺序也写死。某天检索服务所在机器扩容,IP 变了,报告 Agent 还在按原来的地址调,连续报错,但日志里只有一行“connection refused”。更麻烦的是,因为其中一个 Agent 直接拿着内部 token 去访问另一个 Agent 的数据目录,权限也乱了。那次故障排查了两个多小时,最后发现根本不是模型问题,而是 Agent 之间的“触达”完全失控。
这个案例给我最大的触动是:多智能体系统复杂度的核心不在单个 Agent 的智力,而在 Agent 与 Agent 之间的连接关系。传统微服务有注册中心、网关、熔断器,但到了 Agent 场景,很多人却回到了“写死 API 地址 + 零散脚本”的老路。Agent 不是普通服务,它有自己的模型上下文、工具清单、会话状态,还需要处理“该调用谁”的语义判断。把这些直接写在业务代码里,等于把家里的电器插头全部焊死在墙上,看似稳定,实际一动就崩。
1.2 用“触达层”替代硬编码协作
Agent-Reach 的核心思路很简单:把 Agent 当作动态节点,每个 Agent 启动时向触达层注册自己的能力、地址、状态,调用方不再关心目标在哪台机器、什么端口,只关心“我要触达谁”或“谁有这个能力”。触达层负责寻址、路由、转发、重试、权限校验和审计。
这句话说起来很像服务注册中心,但 Agent 场景有一个明显差异:调用方可能不是一段确定代码,而是另一个 Agent,甚至是一个由大模型驱动的工具选择器。也就是说,路由决策既要看技术指标(谁在线、谁负载低),也要看语义匹配(谁的能力最符合当前任务)。Agent-Reach 把这两者结合在一条链路里:先用注册表确认可达性,再用语义匹配确认适不适合。
另外,触达层还要处理“不可用时的降级”。以前 Agent 之间直接调用,对方挂了就只能报错。现在触达层可以配置多个后备方案:首选目标触达失败后,是否尝试同类型节点,是否返回可执行的降级指令,还是让模型重新规划。这个设计让 Agent 协作从“必须成功”变成“尽力触达、失败可解释”。
1.3 这层到底放在哪里
从架构位置上看,Agent-Reach 位于 Agent 实例与应用编排层之间。它不是一个帮助 Agent 思考的框架,而是所有 Agent 对外交互都要经过的一层通道。我经常用一张表格来说明它和周边系统的边界:
| 层 | 职责 | 要不要管“触达” |
|---|---|---|
| 前端/用户层 | 接收用户输入,展示结果 | 不需要 |
| 编排层 | 拆解任务、决定分支、汇总结果 | 需要知道调了谁,但不用知道具体地址 |
| Agent-Reach 触达层 | 注册、寻址、路由、鉴权、熔断、审计 | 核心 |
| Agent 实例 | 执行具体业务、调用工具、生成内容 | 不暴露内部细节,只注册能力 |
这层的位置很关键。它既不能太靠前,否则会侵入业务语义;也不能太靠后,否则退化成消息队列。Agent-Reach 实际扮演的是“智能交换总机”:每个 Agent 像分机一样挂上来,总机知道分机在哪、能接什么电话,并且能决定一个来电该转给谁、要不要拦下来。
对比来看,如果只是 Agent 内部模块之间的函数调用,根本不需要这一层;如果用普通 HTTP 直连,省事但没有治理能力;如果引入通用消息总线,解决了解耦,却又丢失了“能力发现”和“语义路由”。Agent-Reach 的定位就是把这些能力专门做成一个高内聚组件,而不是让项目里东拼西凑一堆脚本。
2. 核心设计思路:可触达也是一种能力
2.1 注册表:让每个 Agent 说清楚自己会什么
Agent-Reach 的第一个基础设施是注册表。每个 Agent 上线时,必须提交一份能力描述文档。这份文档包含 agent 的唯一 ID、所属命名空间、具体能力列表、输入输出参数、依赖工具、信任级别、健康检查地址等。描述格式直接用 JSON Schema,方便大模型和代码共同解析。
我把一个典型的注册信息简化如下:
{ "agent_id": "order_agent@e_commerce", "namespace": "e_commerce", "version": "1.2.0", "owner_team": "trade-platform", "endpoints": { "http": "http://10.20.1.24:8080/reach", "grpc": "10.20.1.24:9090" }, "capabilities": [ { "name": "create_order", "description": "创建新订单,校验库存和优惠券", "input_schema": { "type": "object", "properties": { "user_id": {"type": "string"}, "sku_id": {"type": "string"}, "quantity": {"type": "integer", "minimum": 1} }, "required": ["user_id", "sku_id", "quantity"] }, "timeout_ms": 8000 } ], "heartbeat_interval": 10, "tags": ["order", "transaction"] }很多人觉得搞这么重没必要,但我实测下来,能力描述越规范,后面路由准确率越高。原因是:大模型判断“该调用谁”的时候,最怕遇到含糊其辞的描述。你写“处理订单”远远不够,必须写明它接收什么参数、会执行什么副作用、超时多久。这个注册表不仅是给程序看,更是给大模型看的地图。
我建议每个 Agent 的注册信息由两部分组成:一部分由开发人员人工填写,比如负责人、命名空间、信任级别;另一部分由 Agent 启动时基于自身工具清单自动生成,比如能力名称、参数结构。人工维护和自动生成相互校验,避免能力描述和实际实现脱节。
2.2 路由策略:不只看名字,还要看上下文
注册表解决了“谁能触达”,路由则要解决“该触达谁”。Agent-Reach 默认的路由策略分成三段。第一段叫精确匹配,调用方如果明确指定了 agent_id,就直接走目标;第二段叫规则匹配,根据命名空间、标签、版本等条件过滤;第三段叫语义匹配,把“用户意图描述”和注册表里的 capability description 做向量相似度计算。
实际使用中,语义匹配最考验设计。一开始我想直接让大模型来选,每个请求都问一次“该调用哪个 Agent”,结果延迟高、费用高,而且大模型偶尔会编造一个不存在的 Agent。后来改成先用 embedding 把用户意图和所有能力描述都转成向量,算一遍余弦相似度,取 Top 5,再让一个轻量级模型做阈值确认。
这里的阈值不能拍脑袋。我最初设定相似度 0.85 以下就不路由,结果真正该触达的时候经常因为表述不一致被拦了下来。后来把同步调用的判定阈值放到 0.78,异步任务放到 0.72,但又发现 0.78 会带来一些误召。最终我们加了一个“候选分歧”逻辑:如果 Top1 和 Top2 的得分差小于 0.03,就把请求丢给模型做最终裁决;否则直接选 Top1。这套方案跑了一个月,未命中率从 9.6% 降到了 1.4%。所以关键参数不是死的,而是要结合你和场景的动态阈值来调。
2.3 权限与隔离:不透明的触达比不触达更安全
Agent 之间触达绝不是“人人平等”。给每个 Agent 一个全局可访问的地址,等于把内网大门敞开。Agent-Reach 在注册表里内置了访问控制列表,每个 Agent 可以声明“我可以被哪些命名空间调用”以及“我可以调用谁”。调用方发起触达时,必须携带自己的 Agent 身份凭证,触达层先校验身份,再校验目标 Agent 的 ACL,两层都通过了才放行。
多租户场景尤其要重视隔离。不同租户的 Agent 哪怕能力完全相同,也不能互相触达。我会在 agent_id 里强制加命名空间前缀,比如tenant_a@order_agent和tenant_b@order_agent。路由时的命名空间隔离是硬规则,不允许通过语义匹配绕过。这个设计让我在排查问题时省了很多麻烦:两个租户的相似请求不会跑到对方的 Agent 上,数据也不会串。
还有一个容易忽略的点:Agent 的凭证会过期。不是生成了密钥就一劳永逸,Agent-Reach 默认 12 小时轮换一次 agent token,token 丢失或者过期后,Agent 需要重新注册。这个机制虽然多了一点运维成本,但能避免拿到一个 token 之后长驱直入。触达不等于无条件信任,权限校验宁可慢几毫秒,也不能省。
3. 实操:Agent-Reach 的关键配置与接入流程
3.1 需要的最小依赖
Agent-Reach 不是个手机 App,而是服务端组件。我在实现时选了最小的技术栈:Python 3.10 以上,FastAPI 提供 HTTP 接口,Redis 保存注册信息与心跳状态,NATS 做异步消息转发,OpenTelemetry 做链路追踪。如果你团队里已经在用 Kafka 或 PostgreSQL,也都行,核心只是一个可持久化的注册表和一份可靠的路由表。
选 Redis 是因为它支持 key 过期和发布订阅,心跳检测特别顺手。注册表里每个 Agent 节点存为一个带 TTL 的 key,TTL 设成心跳间隔的三倍,比如心跳 10 秒,TTL 30 秒。只要 Agent 没续期,触达层自动把它摘除。NATS 则用来处理异步触达任务,比如“让订单 Agent 去同步一下退款状态”,这种请求不需要同步等结果,发到队列里由 Agent 消费更合适。
我用 Docker Compose 搭了一个最小可运行环境,主要服务如下:
services: reach-registry: image: redis:7-alpine ports: - "6379:6379" reach-bus: image: nats:2.10 ports: - "4222:4222" reach-server: build: . ports: - "8080:8080" environment: REACH_REDIS_URL: redis://reach-registry:6379 REACH_NATS_URL: nats://reach-bus:4222这套组合的好处是没有历史包袱,出现问题时每个人都能看懂日志。不建议一开始就上很重的分布式协调服务,Agent-Reach 的核心价值先放在路由和鉴权,而不是强一致。
3.2 注册一个 Agent 的完整流程
Agent 接入 Agent-Reach 的流程,我尽量控制在三步以内。第一步,Agent 启动后读取自己的能力描述文件;第二步,用 SDK 向触达层发起注册请求;第三步,开启心跳线程周期性续约。
以 Python SDK 为例,注册部分长这样:
from agent_reach import ReachClient client = ReachClient(reach_url="http://localhost:8080") client.register( agent_id="order_agent@e_commerce", capability_path="./capability.json", secret="your-initial-secret", ) client.start_heartbeat(interval=10)注册成功后,触达层会返回一个确认消息,里面包含 agent token、下一次心跳期望时间、以及当前所在集群。注意,初始 secret 不能写在代码里,应该由部署平台注入环境变量,否则等于把钥匙放在门口垫子下面。
注册接口本身很轻,但一定要做幂等。同一个 Agent 重复注册不能产生多个冲突节点。我的做法是对agent_id + endpoint + version做哈希,注册时先查重,如果版本没变,直接复用原有节点的状态;只有版本变化才触发全量刷新。这样发布新版本时不会瞬间掉线。
3.3 发起一次触达请求的完整链路
当客户端已经注册好,需要调用另一个 Agent 时,不是直接发 HTTP 请求,而是调用 Agent-Reach 的/reach接口。我把一个真实请求拆解出来:
from agent_reach import ReachClient client = ReachClient(reach_url="http://localhost:8080") resp = client.reach( from_agent="report_agent@office", to_agent="order_agent@e_commerce", # 可选,指定则跳过语义路由 method="create_order", payload={ "user_id": "u_1024", "sku_id": "sku_8899", "quantity": 1, }, timeout_ms=8000, )触达层接收到请求后,会做以下几件事:
- 校验
from_agent是否合法,token 是否有效。 - 根据
to_agent或method找出候选 Agent。 - 过滤掉不在线、ACL 不允许、版本不匹配的节点。
- 按负载策略选出一个目标,转发请求。
- 等待响应,同时记录全链路追踪号。
返回体是一个统一结构,核心字段包括reach_id、target_agent_id、latency_ms、status_code、data。我建议客户端不要直接透传返回体里的data,而是先检查status_code,它有三种语义:REACH_OK表示正常触达;REACH_FALLBACK表示首选目标失败但备用目标完成了;REACH_TIMEOUT表示没有可用的目标或超时。这样调用方可以针对不同语义做不同处理。
4. 触达失败?常见问题与排查思路
4.1 注册成功但调用 404
这个问题我遇到不下五次。Agent 明明显示在线,注册表里也能查到,但触达时就是 404。后来发现绝大多数情况是命名空间对不上。路由规则严格要求to_agent的目标必须在同一个 namespace,或者显式在 ACL 里声明跨命名空间,否则一律拒绝。比如report_agent@office想调order_agent@e_commerce,如果order_agent的 ACL 没写明允许office命名空间调用,就会直接返回无法触达。
排查思路有两个。先用命令行工具看注册状态:
reachctl get agent order_agent@e_commerce重点看status是否在线、namespace是否正确。再用审计日志查这次触达的拒绝原因,Agent-Reach 会把每一次失败原因记录成REJECT_ACL、NOT_FOUND、ROUTING_NO_CANDIDATE。我强烈建议把审计日志当成第一排查入口,而不是抓瞎看网络。
4.2 模型超时与幻觉式路由
语义路由是 Agent-Reach 的亮点,也是最容易出怪的环节。常见问题是:请求明明想“查一下这个订单有没有发货”,结果路由到了“评价管理 Agent”,因为两边能力描述都出现了“订单”这个词。这属于典型的上下文过浅。
解决办法不是换更好的大模型,而是把能力描述里加“排除条件”。比如订单查询 Agent 的能力描述里我加了一句“仅处理订单状态查询,不处理售后评价、不处理优惠券创建”,评价 Agent 也写清楚“不包含订单物流”。排除条件在语义匹配时权重很高,能直接把相似度压下去。同时,在method名称里避免用同义动词堆叠,比如不要用“get_order_info”和“query_order_info”同时存在,否则模型很难区分。
如果排除了命名问题,还是频繁超时,那就要考虑模型路由的开销。Agent-Reach 支持一个开关:当语义匹配候选列表只有一个且相似度大于 0.9 时,直接跳过二次确认模型。这个开关能把 p95 延迟砍掉 400 到 600 毫秒。
4.3 拓扑抖动导致雪崩
多 Agent 系统里,单个 Agent 重启非常常见。最怕的不是重启,而是重启期间所有调用方都在疯狂重试,把触达层和数据库打爆。这里需要两层保护。Agent-Reach 内置了熔断器,每个目标 Agent 在窗口内(默认 30 秒)失败次数超过 5 次后,熔断 10 秒,期间直接返回失败,不再盲目转发。调用方侧还要做指数退避,第一次重试延迟 200ms,第二次 400ms,最多重试三次。
另外,我强烈建议给健康检查设置“预下线”机制。Agent 在要重启之前,主动调用deregister或者把健康状态设为STOPPING,而不是等心跳超时被摘除。主动下线和被动摘除的最大区别是:主动下线可以让触达层提前把流量迁移走,被动摘除至少还有 30 秒的窗口期会有流量继续进来。我们因为漏做了这个,在一次发布窗口内吃到了不少错误告警。
5. 可观测性:怎么证明触达真的“到达”了
5.1 把每次触达都看作一条链路
Agent 触达不是一次简单的网络请求,而是经过注册、路由、鉴权、转发、响应多个环节,中间任意一环出问题都会影响结果。所以 Agent-Reach 从第一天就要求全链路追踪。每一次触达请求生成唯一reach_id,从入口开始注入到日志、NATS 消息头和 HTTP Header 里。
各个 Agent 在处理触达请求时,也要在自己的业务日志中带上这个reach_id。这样当用户问“为什么订单 Agent 好像没有执行”时,我能直接从触达层拉到一条完整链路,看到请求在触达层花了多少毫秒,是否被转发到了正确的agent_id,目标 Agent 内部处理耗时多长,最终返回了什么错误码。
链路记录的字段不需要太花哨,但必须包含:reach_id、from_agent、to_agent、method、status_code、latency_ms、token_count、error_message。如果某个请求走到了备用目标,还要加一个fallback_from字段,方便知道发生了什么降级。
5.2 看板与告警
有了链路数据,下一步就是聚合。我把 Agent-Reach 的指标接入 Prometheus 和 Grafana,核心看三个面板:触达成功率、p95 延迟、路由命中率。成功率不是单纯看 HTTP 200,而是看status_code == REACH_OK或REACH_FALLBACK的占比。延迟要单独看router_latency和target_latency,因为触达层本身可能很快,但目标 Agent 模型推理很慢。
告警阈值我给一个参考值:全局触达成功率低于 98%,告警;单 Agent 成功率低于 95%,告警;p95 延迟超过 2000ms,告警。不同团队的阈值可以放宽,但核心原则是告警必须能对应到“某个 Agent 的触达变差”,而不是模糊的“系统变慢”。
5.3 审计日志:出事能追溯
Agent 系统泄露数据往往是慢性的。某个 Agent 因为权限配置错误,被另一个命名空间错误触达,虽然单看流量不大,但危害很重。Agent-Reach 会把触达层的关键事件全部记录到审计日志,包括注册、注销、权限变更、路由拒绝、失败触达。审计日志按要求做追加写入,任何人不能修改。
这套审计日志的另一个价值是复盘大模型行为。比如用户反馈“我明明要求调用退款 Agent,结果却走到了客服 Agent”,通过审计日志能看到语义路由时的候选列表、相似度分数、最终决策来自规则还是模型。大模型不是不可解释的,只要在触达层把决策依据留下来,每次误判都有据可查。
6. 做完 Agent-Reach 后的一些真实心得
6.1 别一开始就搞完美注册中心
如果你是第一次在 Agent 场景里做触达层,我建议先别追求注册表、语义路由、ACL 全部一步到位。可以先用“写死的地址 + 一个健康检查”搭出最小链路,然后把问题逐个暴露出来。我发现最容易踩的坑是:方案设计得太完美,代码写了一堆,但真正跑业务时发现 Agent 能力描述根本维护不动。不如先跑起来,把自动生成能力描述的流程做好,再扩展其他能力。
6.2 让 Agent 自己描述能力,比让人维护拓扑图靠谱
拓扑图是一种静态视角,它只能表示“谁连谁”。但 Agent 的能力随时会变,加一个工具、换一个模型的 prompt,能力边界就变了。靠人去改拓扑图根本不现实。Agent-Reach 的注册表之所以用机器可读的 Schema,就是为了让 Agent 启动时能根据自身工具列表自动生成能力描述。这比任何人工维护的资产清单都可靠。
6.3 关于 LLM 路由的取舍
纯靠大模型做路由,体验在 demo 阶段很惊艳,生产阶段会让人崩溃。Agent-Reach 的做法是“能不用模型就不用模型”:精确 ID 路由和规则路由占绝大多数,语义匹配对 0.78 到 0.9 的候选做二次确认,只有候选之间分不出胜负时才让大模型参与。这样既保住了语义灵活性,又把成本和延迟控制在合理范围。
最后再分享一个小技巧:触达层的配置一定要支持热更新。我刚开始做的时候,每次调 ACL 或者路由权重都要重启服务,结果线上抖动全是我自己造成的。后来把配置放到注册表的系统命名空间里,触达层监听变更,实时生效。现在调权重、加白名单、切候选顺序,都能在不重启的情况下完成。这个改动看起来不起眼,但每次线上调整的风险都降了一大截。