接了个挺奇怪的需求:同事想让他负责资料总结的 AI Agent,自动去调用另一个负责合同审核的 AI Agent 的结果,中间还得经过一个本地部署的模型服务。我听完第一反应不是“这智能程度够不够”,而是“这俩服务到底怎么触达对方”。后来我把这套解决 Agent 之间“触达”问题的机制,做成了一整个小项目,名字就叫 Agent-Reach。
这篇文章不是产品发布会,是我自己在做这个项目时的完整记录。适合那些本地部署了多个 AI 服务、想让它们互相协作,却发现“怎么调起来”比“怎么生成”更头疼的人。我会把核心设计、部署过程、生产环境踩坑,以及最后沉淀下来的取舍,全部摊开讲。
1. 做 Agent 串联时,我先撞上的不是“智能”,而是“触达”
1.1 断头路场景:每个 Agent 都是一座孤岛
客户环境里跑着十几个 AI 服务:有做文档解析的,有做摘要的,有做合同条款抽取的,还有一个本地向量检索服务。每个服务单独用都挺正常,可一旦要让它们协同,问题就全涌出来了:A 服务根本不知道 B 服务存在,不知道 B 能干什么,不知道该用哪个地址调它,更不知道 B 返回的数据长什么样。
我刚开始天真地以为,这事情找个工程师写几段 HTTP 调用代码就完了。可真动手就发现,这不是“写接口对接”的活,而是“连接关系会一直变”的活。今天 A 要调 B,明天 B 要调 C,后天 A 又要调 C 的新版本。接口文档更新不及时,参数对不上,鉴权方式各搞一套,一个联动需求能改两周。
1.2 从“能生成”到“能触达”,是一个大家下意识忽略的层次
大多数 Agent 项目,讨论的焦点都在模型选择、Prompt 编排、RAG 召回效果这些“智能”环节上。可真实跑起来之后你会意识到:模型再聪明,如果它连该调的另一个服务都调不到,那一切智能都白搭。我把这层能力叫“触达”。
触达不只是把 HTTP 请求发出去,它至少包含四件事:
- 发现:一个 Agent 怎么知道另一个 Agent 提供了什么能力;
- 选择:多个 Agent 都能做同一件事时,该选哪一个;
- 调用:按对方要求的鉴权方式、参数格式把请求送进去,并拿到结果;
- 回溯:一次协作跨了三四个服务,出了问题能查出在哪一环断的。
这四个点合起来,就是 Agent-Reach 项目要解决的问题。往大了说,这是智能体之间的连接层;往小了说,这就是一张不断更新的服务地图。
1.3 别把它当成 API 网关,差挺多的
有人可能会说,这不就是 API 网关吗?我开始也这么想,直到画架构图时才意识到两者有本质区别。API 网关面对的是稳定的、由人定义的接口,路由规则写死就行;Agent 场景里,调用方不是人,是模型。模型会根据当前任务动态决定下一步调谁,所以“能力列表”必须是可查询、可变化的,而不是写死在墙上。
这有点像你到了一个园区,不是只看门口贴的店铺名单(那是 API 文档),而是随时能打开手机地图,看哪个店开着、人多不多、要不要排队(能力注册与路由状态)。Agent-Reach 做的,就是这张地图加一套导航机制。
2. 给每个 Agent 一张“可达性地图”
2.1 能力注册:不是接口文档,是能力清单
Agent-Reach 的第一个核心组件,是一个叫能力注册中心的东西。每个 Agent 启动时,都会上报自己“能做什么”,而不是“接口长什么样”。我用的能力描述格式大致是:
agent_id: contract-checker display_name: 合同审核助手 endpoint: http://contract-svc:8080/invoke auth: type: bearer token_env: CONTRACT_SVC_TOKEN capabilities: - name: check_contract_compliance description: 检查合同条款是否符合给定合规策略,返回风险项列表 input_schema: contract_text: string policy: string output_schema: risk_items: array rate_limit: 10这个设计的思路是:把“接口”抽象成“能力”。接口是给程序看的,能力是给模型看的——模型不需要知道 HTTP 动词和 URL,它只要知道“有一项能力叫 check_contract_compliance,输入是什么,输出是什么”。注册中心存储这些能力,同时定期做健康检查,挂了的能力自动从可用列表里摘掉。
2.2 路由决策:直连、中转、广播
地图有了,接下来是导航。Agent-Reach 里我实现了三种路由模式,对应真实场景中的三种情况:
- 直连:调用方明确知道要调哪个 Agent,注册中心只负责返回目标地址和鉴权信息,调用方直接发请求。这种模式延迟最低,适合服务间已建立稳定信任的场景。
- 中转:调用方只知道要找某个能力,不知道具体该调谁。请求先进 Reach 中心,由中心根据能力名、负载、健康状态选一个目标,再转发。这种方式对调用方最透明,缺点是多一跳。
- 广播:有时候不是“找一个服务执行”,而是“通知所有关注这件事的服务”。比如合同审核完成后,需要同步给归档服务、通知服务、风控服务。Reach 支持按主题发布事件,各 Agent 按订阅关系接收。
我实际项目里 80% 的流量走的是“中转”,因为它最适合模型主导的动态调用。只有高频且路径稳定的调用,才切成直连。别一上来就全部直连,会让调用方代码里写死一堆地址,地图就失效了。
2.3 触达链路的可观测性:trace 贯穿全程
多 Agent 协作最怕的是“黑盒”。以前排查问题,靠人工翻各服务日志,时间全靠猜。Agent-Reach 从设计初就强制所有请求带上一个 trace_id,格式是随机 ID 加节点编号。每经过一个 Agent,就在日志里追加一条:
[1a2b3c] summary-agent -> router -> contract-checker [1a2b3c] contract-checker 返回 2 个风险项 [1a2b3c] summary-agent 收到结果,耗时 4.2s这套东西看着简单,但后面排查“为什么合同审核结果没回到总结 Agent”这类问题时,救命级别的好用。没有 trace,你只能对着十几个服务一个个猜。
3. 把 Agent-Reach 跑起来的全过程
3.1 环境准备与初始化
Agent-Reach 本身是一个 Python 服务,我用的技术栈很常规:FastAPI 提供 HTTP 入口,Redis 做注册中心的临时存储,SQLite 存能力描述的历史版本,Docker Compose 统一拉起。你不需要多大的机器,单机 2C4G 就能跑起来,因为真正消耗资源的是下游 Agent,不是连接层。
初始化时有个容易忽略的细节:所有 Agent 实例的时间必须同步。trace 排序、超时计算、健康检查都依赖时间戳,如果各机器时间差超过 1 秒,你会在日志里看到“离奇”的乱序调用记录。我直接用 NTP 协议让所有节点对时,这个问题建议你提前处理,别等出问题再排查。
3.2 用能力描述文件注册第一个 Agent
我把一个现成的“合同审核服务”接进来,它原本是个独立的 Flask 应用,我只改了少量代码。接入方式是给 Agent-Reach SDK 加一段初始化代码:
from agent_reach import Agent agent = Agent( agent_id="contract-checker", config_file="capability.yaml", ) agent.register()SDK 启动后做三件事:读取本地能力描述文件,向注册中心上报 endpoint 与能力列表,然后开启一个心跳协程,每 15 秒上报一次存活状态。
这里我最想提醒的一点是,注册时一定要确认 endpoint 是“别人能访问到的地址”,而不是“自己访问自己的地址”。我第一版配置就写成了 http://localhost:8080/invoke,结果另一台机器上的总结 Agent 拿到这个地址后,请求打到了它自己身上,报错报了半小时。这种问题不是看代码能看出来的,得想清楚网络拓扑。
3.3 发起一次跨进程调用
注册完成后,我在另一个 Agent 里写了一段调用逻辑:总结 Agent 拿到一份合同文档,先生成摘要,然后把摘要原文发给合同审核 Agent,让对方检查合规性。
代码大致是这样的:
from agent_reach.client import ReachClient client = ReachClient() result = client.invoke( capability="check_contract_compliance", payload={ "contract_text": summary_result["text"], "policy": "内部采购合规政策v3", }, timeout=60, )这段代码里没有出现任何目标 IP、URL、端口。调用方只表达“我要什么能力,给什么输入”,至于这个能力目前由谁提供、地址变没变,全部由 Reach 层处理。这跟我前面说的“地图导航”对上了:开车的人不用背路名,导航知道就行。
3.4 实测中三个典型的失败场景
第一次真正跑通之前,我连续失败了好多次,记录一下,免得你重复踩:
- 输入字段不匹配:合同审核 Agent 期望的字段名是 contract_text,我传的是 content,结果对方返回 schema 校验错误。这种情况 Reach 层检查不出来,因为连接层不知道业务字段的语义,必须靠调用方仔细看能力描述。
- 鉴权头丢失:Reach 层做中转时只转发业务 body,没带目标服务要求的 Authorization 头,目标服务直接回 401。后来我在能力描述里显式声明 auth 来源,支持从调用方透传或由 Reach 统一注入,才解决。
- 同步等待过长:合规模块内部还调了一个外部 OCR 服务,最慢跑了 40 多秒,而 SDK 默认超时 10 秒。第一次跑直接超时重试,重试两次,把下游打出来重复任务。
这三个问题都不是“代码写错”,而是“机制没想清楚”。我的结论是:连接层一定要把超时、鉴权、数据契约当成一等公民来设计,不能等用户报错再补救。
4. 生产环境最容易翻车的三个连接问题
4.1 重试风暴:Agent 的固执会把下游打挂
Agent 场景有个特点:模型收到异常后,往往会自己重试。这在用户体验上是好的,但在系统负载上是灾难。有一个晚上我压测,发现上游 Agent 连续报错后不停地重发请求,下游合同审核服务直接被打到 CPU 满载。
问题根源是两层重试叠加了:Reach 层重试一次,Agent 层又重试三次,相当于一次失败产生四次流量。解决方式是我在 Reach 层做了三个措施:
- 指数退避:每次重试间隔翻倍,第一次 1 秒,第二次 2 秒,第三次 4 秒,而不是立即重发;
- 最大重试限制:默认 2 次,特殊场景最多 5 次,不让模型逻辑无限放大流量;
- 熔断:连续失败超过 10 次,暂时把该能力标记为不可用,让它冷却 30 秒再恢复。
这里我建议你把“哪些重试是连接层该做的、哪些是业务层该做的”用文档写清楚。连接层只负责解决瞬时抖动,业务层才处理逻辑失败。两者混在一起,全链路负载就没法算。
4.2 回调地址:你以为的 localhost 不是我的 localhost
这个坑我在 3.2 里提过,但它在生产环境反复出现,值得单独说。Kubernetes 部署时,Agent A 返回一个回调地址,写的是 http://pod-ip:8080/callback。问题是这个 Pod IP 是集群内部的,只有集群内能访问。如果消费者在集群外,回调就打不回来。
Agent-Reach 的解法是:注册时支持两种 endpoint 声明,internal 和 external,Reach 层根据调用方所处网络位置,返回对应地址。如果因为安全策略不能直接暴露地址,就必须走中转模式,由中心转发。你如果自己做类似项目,一定要设计这个“地址可见范围”的概念,不然就是一地 localhost。
4.3 超时设置:慢思考不等于连接失败
AI Agent 的推理速度,跟传统 API 完全不是一个量级。传统接口 200 毫秒没返回就是异常,可一个 Agent 在逐步思考、调用工具、翻阅上下文时,10 秒 30 秒都很正常。我见过太多团队拿默认的 HTTP 超时去调 Agent,五分钟之内全在报错。
我后来的策略是分开设置两组超时:
| 超时类型 | 默认值 | 说明 |
|---|---|---|
| 连接超时 | 5 秒 | 只覆盖 TCP/TLS 握手阶段 |
| 响应超时 | 120 秒 | 覆盖 Agent 完整推理时间 |
| 空闲超时 | 30 秒 | 流式响应中,相邻数据包最大间隔 |
这个表看起来简单,但它解决的问题很关键:连接超时短,是为了快速失败出来,别让调用方傻等一个连不上的地址;响应超时长,是为了给 Agent 足够的思考时间。两个值分开设,就不需要因为“怕超时”而把所有超时都调大。
5. 扩展场景:把它变成人机协作的业务通道
5.1 人工审批节点的“触达人”
Agent 之间的触达跑通后,客户提了一个我没想到的需求:能不能在链路里插入一个人工审批节点?合同审核 Agent 发现了高风险条款,不能直接通过,得让合规专员看一眼。
这就从“触达服务”变成了“触达人”。我在 Agent-Reach 里加了一个 person_target 类型,把通知推送到协作平台的审批流。链路的 trace 和上下文全部保留,人处理完,结果再交给下一个 Agent。这个设计让我意识到:连接层不需要区分对象是人还是 Agent,只要统一“接收者”抽象,就能把人和智能体放在同一条链路上。
5.2 联合上下文:少传点数据,多传点线索
多 Agent 协作时,最原始的做法是把上下文全量塞给每个 Agent。文档是 40 页 PDF,摘要 Agent 读完生成 2000 字摘要,合同审核 Agent 又要重新把 40 页读一遍。浪费算力,还慢。
Agent-Reach 做了一个轻量的上下文索引:每个 Agent 处理完后,把产出的文档片段存到向量库,再把索引进上下文。下一个 Agent 按需召回,而不是全量接收。这一步优化,把一次“总结经合同审核”链路的整体耗时从 3 分钟降到了 45 秒,体感非常明显。
5.3 与向量库、消息队列、模型网关并存
项目后期,Agent-Reach 已经不只是转发 HTTP 请求的连接层了,它开始做更细的事:把结果写进消息队列供异步消费、从向量库检索相似条款、调用统一模型网关做最终判断。这些都可以做成 Agent 能力注册进来。
我给的建议是,不要企图让连接层直接实现业务功能,连接层只做“触达”,具体业务逻辑继续留在 Agent 内部。否则这个项目会从“轻量连接器”膨胀成一个“业务中台”,维护成本完全不可控。我的规则很简单:触达的归触达,业务的归业务。
6. 落地之后,我最后想说的三件事
6.1 连接层要克制,别让“触达”变复杂
Agent-Reach 项目做到了中后期,最容易犯的毛病是加功能:加消息队列,加分布式事务,加各种自定义协议。我最后砍掉了一大半,只保留了能力注册、路由、trace 三条主线。不是因为那些功能没用,而是连接层的核心价值是“简化”,如果连触达本身都变得复杂,Agent 团队就会绕开你自己造轮子。
6.2 从小规模起步,别一上来全家桶
如果你也想做类似的 Agent 连接层,我的血泪建议是:先用单体服务加 Redis 把 2 个 Agent 接起来,跑通端到端,再考虑高可用、分布式、权限分域。我一开始就按“微服务治理平台”的规格做设计,结果光是部署配置就花了一周,真正跟 Agent 相关的东西一行没写。这个项目真正的转折点,是放弃宏大设计,改用最小可用架构跑通第一个调用。
6.3 留下一份检查清单
最后分享一个我在项目结束前写下的落地清单,现在每次接新的 Agent 我都会照着过一遍,省了不少事:
- 该 Agent 有哪些能力,是否都有 input_schema 和 output_schema;
- endpoint 对外部调用方是否真的可达,还是又一个 localhost;
- 鉴权是透传还是注入,是否在能力描述里写清楚;
- 超时配置是否区分了连接超时和响应超时;
- 重试次数和退避策略是否约束过,会不会叠加成风暴;
- 每次协作是否携带 trace_id,链路日志能不能完整拼出来;
- 人机协作节点是否预留了“人等机器”和“机器等人”两种状态。
Agnt-Reach 这个项目谈不上多高深,但它把一个长期被忽略的问题端到了台面上:Agent 之间能不能可靠地触达彼此,决定了 AI 协作的天花板。我自己的体会是,别一上来就研究模型会不会“犯蠢”,先问一句它能不能拿到该拿的数据、调到该调的服务。连接层打通了,后面很多事情才会顺。