☰
多智能体协作可达性设计:服务注册与消息路由的工程实践
2026/10/29 9:19:54 网站建设 项目流程

多智能体协作这几年被炒得很热,但真正在项目里跑过几个Agent的人应该都有同感:最磨人的从来不是模型能力不够,而是Agent和Agent之间"互相够不着"。你设计了一个调度Agent,它要去调用数据分析Agent的结果,结果消息发出去了,石沉大海;你配置了一个工具Agent,另一个Agent按文档调用它,却一直超时重试。这些问题说到底都属于同一个范畴——智能体的可达性(Agent Reachability)。

Agent-Reach就是我针对这个问题做的一套实操型方案,它不是一个炫技的AI框架,而是一个聚焦"多智能体如何可靠地互相发现、触达、协作"的项目。它解决的核心痛点很具体:当一个系统里有几十个甚至上百个Agent时,如何让任意一个Agent在正确的时间找到正确的另一个Agent,并在网络抖动、服务重启、能力变更的情况下依然稳定完成调用。这篇文章我会从设计思路、能力注册、消息路由、坑位排查到权限控制完整拆一遍,适合正在做Agent编排、或者打算把团队里的AI服务从单点走向协同的开发者参考。

1. 为什么我会做"Agent-Reach"这个项目

先说清楚背景。我之前在一个偏中台性质的团队,负责把各种AI能力封装成服务给业务线用。一开始是单体Agent,所有工具、模型、记忆逻辑都塞在一个进程里,看起来一切正常。后来业务变多,开始拆成独立的Agent服务,有做意图识别的、有做知识库检索的、有做数据抽取的、有做内容生成的。它们之间需要互相调用,这时候问题就来了。

1.1 从一次失败的Agent协作说起

那次故障我记得很清楚。主调度Agent需要调用一个"行业报告生成Agent",这个Agent本身又依赖一个"数据查询Agent"去拉取指标。我们当时用的方式很简单,就是在配置里写死了对方的HTTP地址和接口路径。上线第一天就翻车了:数据查询Agent因为一次内存问题发生了重启,它的服务端口临时变了,而行业报告生成Agent还对着旧地址狂发请求。调度Agent那边显示"已派发任务",但是下游一直在报连接超时,最后整个任务链卡死,用户等了三分钟拿到一个报错。

这个事儿让我意识到,Agent之间的协作不能靠"大家商量好一个地址"来实现。Agent是动态的,它的状态、位置、能力都会变化,你必须有意识地设计一套机制去解决"它现在在哪、它还能不能干活、它用什么方式沟通"这些问题。这套机制,就是Agent-Reach要做的"可达性保障"。

1.2 三个维度的可达性:发现可达、契约可达、路由可达

我在设计Agent-Reach时,把"可达性"拆成了三个层面,每个层面解决不同的问题。

第一个是发现可达。一个Agent怎么知道另一个Agent存在?它有证书吗?有调用方式吗?在一堆服务里能不能按能力找到对方?这不是网络层面上"通不通"的问题,而是信息层面上"知不知道"的问题。很多协作失败不是网络断了,而是发起方根本不知道要找谁,或者拿到的候选列表是过期的。

第二个是契约可达。就算找到了对方,你知道它的输入输出格式吗?你知道它期望的参数是JSON还是XML?字段是驼峰还是下划线?调用是同步还是异步?如果两边契约不一致,就算网络全通,消息到了对方手里也只是无法理解的一堆字符。我见过太多Agent对接的坑,最后查出来是字段名大小写不一致,这属于典型的"物理可达但语义不可达"。

第三个是路由可达。找到对方、契约也对了,但是调用链路上有中间层——可能是网关、可能是Agent Hub、可能是消息队列。这条链路上的节点是否有足够的权限转发消息?是否有路由表规则把请求送往正确的实例?是否会因为负载均衡策略把请求送到了一个已经不健康的节点?

Agent-Reach把这三个层面分开处理,在注册阶段解决发现,在配置阶段解决契约,在运行阶段解决路由。分开处理有个好处,就是出问题的时候你能快速定位是哪一个层面失效,而不是所有错误都混在一起,看起来都是"调用失败"。

2. Agent-Reach 的核心设计:能力注册与发现机制

这一块是整个项目的地基。没有注册机制,后面一切免谈。Agent-Reach在架构上做了一个轻量的注册中心,每个Agent在启动时必须向注册中心上报自己的基础信息和能力声明。这个设计借鉴了服务注册发现的思路,但同时补充了AI场景下特有的能力描述维度。

2.1 能力注册表:Agent的"名片"

注册中心里维护的是一张能力注册表,每一条记录对应一个Agent实例。字段不只是IP和端口,还包括以下几类关键信息。

第一类是身份信息,比如agent_id、agent_name、版本号、运行模式(同步/异步)。agent_id是全局唯一的,绝对不能复用,不然会出现幽灵实例。第二类是能力声明,也就是这个Agent能干什么。第三类是接入信息,包括通信协议、接口路径、鉴权方式。第四类是运行时状态,由Agent定时上报。

下面是我在项目里实际用的注册数据示例,用JSON格式展示一条注册记录:

{ "agent_id": "agent-report-gen-001", "agent_name": "report_generator", "version": "2.3.1", "mode": "async", "capabilities": [ { "name": "generate_industry_report", "description": "根据指定行业和时间范围生成结构化报告", "input_schema": { "industry": "string", "time_range": "string", "include_charts": "boolean" }, "output_schema": { "report_id": "string", "status": "string", "download_url": "string" }, "qps": 10, "latency_sla_ms": 5000 } ], "endpoints": { "primary": "http://10.2.3.4:8080/api/call", "fallback": "http://10.2.3.5:8081/api/call" }, "auth": { "type": "token", "key_name": "X-Agent-Token" }, "health_check": { "path": "/healthz", "interval_sec": 15, "timeout_sec": 3 } }

这里有个细节值得强调:capabilities里我专门写了input_schema和output_schema。这就是前面说到的"契约可达"的落地方式。只有把请求和响应的结构显式声明出来,别的Agent在对接时才不需要靠猜。你写一个Python脚本去调别人的接口,还得先读人家的文档呢,Agent之间也一样,只不过它读的是机器可解析的schema。

2.2 能力描述文件怎么写

直接用JSON往注册中心POST当然可以,但在Agent-Reach里我推荐把能力描述做成独立的配置文件,随Agent一起发布。原因很简单:配置文件可以进Git仓库做版本管理,发生人员变动时也不至于把注册逻辑搞丢。

能力描述文件我用的是YAML格式,因为可读性更好。核心是name、description、input_schema、output_schema、tags这几个字段。tags特别有用,它用于能力检索。比如一个Agent需要"查天气",它不一定要知道具体哪个Agent提供这个能力,只要向注册中心发起一个基于标签的查询,注册中心返回所有打了weather标签的Agent即可。这就是从"指定调用"到"按能力调用"的转变。

tags的粒度需要把控一下,太粗了会匹配到一堆不相关内容,太细了又难以命中。我一般建议tags控制在5到10个,使用小写字母加连字符的命名风格,比如data-query、report-generate、text-summarize。同一类能力的不同实现可以用version或者vendor字段区分,而不是在tags里造一堆近义词。

2.3 健康检查与心跳机制

注册表不能只进不出,必须有机制把死掉的Agent清出去,否则调用方会不断尝试触碰那些早已不存在的实例。这是Agent-Reach里生命周期管理的基础。在Agent-Reach里,健康检查采用了主动探测和被动心跳相结合的方式。

主动探测是指注册中心定期向Agent的health_check.path发起HTTP请求,看返回的状态码。被动心跳是指Agent自己每隔一段时间上报一个心跳包,里面带上当前的负载、最近一次任务完成时间、内存使用率等指标。两条腿走路的好处是,主动探测能发现"进程活着但响应异常"的情况,心跳能反映"进程正常但正忙于大量任务"的情况。

心跳间隔和超时阈值直接决定了故障发现的时效。我这里用过一个经验公式:健康检查间隔不要超过业务SLA中允许的最大故障感知时间的一半。举个例子,如果你的业务要求一个Agent不可用最迟30秒内必须被感知到,那把健康检查间隔设为15秒,连续两次失败才判定下线,这样最坏情况是30秒出头,基本踩线。如果要求再严格,就缩短间隔并加入TCP层的快速探测。

3. 消息路由与调用链:从"找到"到"用上"

注册环节解决的是"我知道你存在"的问题,接下来要解决的是"我怎样把消息成功送到你手里并拿到结果"。Agent-Reach在调用链上设计了路由表和一套调用策略,同时把超时、重试、熔断这些稳定性措施内置进去了。

3.1 路由表与优先级

路由表本质上是"消息应该往哪走"的决策依据。Agent-Reach的每个Agent都维护一份本地路由缓存,这份缓存是从注册中心拉取后生成的,包含目标Agent所有可用实例、协议类型、优先级权重。

路由选择不是简单随机挑一个实例,而是按照优先级顺序尝试。优先级由几个因素决定:实例的最近健康状态(健康权重最高)、响应延迟(越低越优先)、负载情况(负载越低越优先)、以及发布版本(灰度期间新版本可能降低优先级)。我用一个加权公式计算每个实例的综合得分,然后按得分从高到低排序。

def score_instance(instance, current_time): health_score = instance.health_factor latency_score = max(0, 1 - instance.avg_latency_ms / instance.latency_sla_ms) load_score = 1 - instance.current_load / instance.max_load version_score = 1.0 if instance.is_stable else 0.7 return (0.4 * health_score + 0.3 * latency_score + 0.2 * load_score + 0.1 * version_score)

这几个权重的比例不是拍脑袋定的,它反映的是我对Agent协作稳定性的优先级排序:健康状态最重要,因为调用一个不健康实例大概率会失败;延迟第二,因为Agent协作往往是链式调用,每一跳延迟都会乘上去;负载第三,避免把消息打到一个已经很忙的Agent;版本因素权重最低,只在灰度发布场景下起作用。

3.2 超时与重试参数的经验值

调用超时设置是Agent协作里最讲究的环节。设太短,慢一点的Agent永远被误判为不可用;设太长,链路一多,整体响应时间就失控了。

我在Agent-Reach里默认把单次Agent调用的超时设为目标Agent所声明latency_sla_ms的1.5倍。为什么是1.5倍而不是正好1倍?因为SLA一般是理想状态下的P95值,实际运行中网络波动、GC暂停、队列排队都会增加耗时,留出50%的缓冲比较合理。重试策略上,我的经验是:只对可重试的失败类型做重试。连接超时、网络抖动、返回503这类错误可以重试;而业务逻辑错误(比如参数校验失败、数据格式错误)绝对不能重试,重试只会放大问题。

重试的次数和退避策略也有讲究。我用的是指数退避加抖动(exponential backoff with jitter),基础退避时间200毫秒,每次翻倍,最多重试3次。加抖动的目的是防止多个Agent同时重试形成共振,把下游打崩。下面的伪代码展示了Agent-Reach里的重试逻辑:

def call_with_retry(endpoint, payload, max_retries=3): delay = 0.2 for attempt in range(max_retries + 1): try: return invoke_agent(endpoint, payload) except NetworkRetryableError as e: if attempt == max_retries: raise sleep(delay * (2 ** attempt) + random.uniform(0, 0.1)) return None

这套参数在我的生产环境里跑了半年多,调用成功率稳定在99.5%以上。要注意的是,不同业务的容忍度不同,如果是实时交互场景,这个超时设置可能太长,需要压缩到几百毫秒级别;如果是离线任务型场景,甚至可以放宽到几十秒。

3.3 熔断和降级:不要让一个Agent拖垮全局

Agent协作最怕一个慢节点拖垮整条链。如果A调用B超时5秒,B调用C又超时5秒,两层下来就是10秒起步。更麻烦的是,超时并不代表B停止等待了,B可能在后台还在继续执行,然后C的结果回来了,B接着往下跑,后面的调用又把任务执行了一遍,代价不可控。

为了避免这种情况,Agent-Reach在每个调用节点上加了熔断器。核心逻辑是监控最近一段时间内的调用错误率,一旦错误率超过阈值,熔断器打开,后续请求不再发往下游,而是直接快速失败或者走降级逻辑。降级有几种方式:返回缓存中的历史结果、调用一个功能相似的备用Agent、或者返回一个默认的包含错误信息的响应体,由上层决定如何处理。

熔断器的参数我一般这样设置:滑动窗口时间10秒,窗口内最少请求数20次,错误率阈值50%。这些值不是普遍适用,如果你的Agent调用频率很低(比如每分钟几次),20次最少请求数可能等很久都无法触发熔断,需要相应调低;如果调用频率很高,又需要提高最小请求数以避免偶发抖动误触发。原则是:熔断必须基于足够的统计样本,不能因为一两次偶发错误就断掉整条链路。

4. 落地过程中的坑与排查链路

再好的设计也要经过真实的故障检验。这一章节我完整记录一次Agent-Reach上线后遇到的典型故障,从现象、排查链到最终根因,整个过程走一遍。你会发现,很多问题的实质和表象完全不是一回事。

4.1 现象:协作任务一直卡在"等待响应"

那次故障是凌晨2点开始出现的。线上一个"舆情分析Agent"调"数据采集Agent"的接口,任务状态一直显示"等待响应"。从调用方的日志看,请求确实发出去了,但迟迟没有收到响应。最诡异的是,以前偶发的超时都是秒级的,这次直接卡死,没有触发任何超时或者重试机制。

我第一反应是网络问题,直接ping数据采集Agent所在节点,通了;telnet它暴露的端口,也通。那问题可能出在应用层。打开数据采集Agent的日志,发现它确实收到了请求,也进入了处理函数,但处理函数卡在一个内部阻塞调用上。这个调用不是HTTP请求,而是它向一个外部数据库发起的连接。

4.2 排查链路:从路由日志到健康检查

我按模块逐层排查。先看路由日志,Agent-Reach的路由层会记录每一次转发决策,包括选择了哪个实例、得分是多少、是否触发了重试。日志显示,数据采集Agent当时只有一个可用实例,实例健康状态正常,路由层没有做任何特殊处理,选择了主实例。

然后看健康检查记录。注册中心的健康检查显示,数据采集Agent的/probe接口在故障前一直是200,故障发生后依然有几次200——也就是说,注册中心始终认为它是健康的。这就说明问题不在Agent-Reach的发现和路由层,而是在数据采集Agent自身的业务逻辑内部。

我又回去看它的进程线程状态。jstack打印出来的线程栈显示,有一个线程阻塞在数据库连接获取环节,正处于"连接池等待"状态。原因是数据库连接池的maximumPoolSize设置为了5,而前一个晚上有一个批量任务同时启动了10个采集子任务,每个子任务都要占用一个数据库连接。连接池被打满,后来的请求只能排队等着空闲连接,而前5个子任务又因为数据量特别大,处理时间被拉得很长,队列里的请求就一直在等。

4.3 根因与修复

根因找到了:不是网络、不是路由、不是Agent-Reach的问题,而是Agent内部依赖资源不足导致的线程阻塞。这类问题最坑的地方在于,Agent的存活状态是正常的,HTTP端口也能响应,健康检查返回200,但实际上它已经无法处理业务请求了。健康检查只探活不探"业务可用性",这是很多Agent系统的一个盲区。

修复方案分两步。第一步是临时救火,把数据库连接池的maximumPoolSize调大到15,同时把阻塞的线程解除,任务重新跑起来。第二步是长期优化,在Agent的health check里增加业务健康判断——只有连接池使用率低于80%、线程池活跃度低于90%时才返回200,否则返回503。这样注册中心就能及时发现"半死不活"的Agent,把它临时标记为不健康,路由层自然会把请求切换到其他可用实例。

4.4 类似问题的快速定位清单

经过这次故障,我在Agent-Reach的运维文档里加了一份排查清单。当你遇到"Agent调用异常"时,按照这个顺序逐层排查,可以节省大量时间。

第一层检查注册中心,确认目标Agent是否还存在、状态是否为健康。如果状态异常,直接分析下线原因,这一步可以排除一大部分问题。第二层检查路由日志,确认请求实际发往了哪个实例、用了哪个协议、走的哪个端口。这里能发现路由决策不符合预期的情况,比如规则没加载、权重配置被修改过。第三层检查网络连通性,确认实例的IP和端口从调用方所在节点是否可达。第四层检查目标Agent的应用日志,看请求是否到达、处理过程有没有报错或者卡住。第五层检查目标Agent依赖的外部资源,比如数据库连接池、缓存集群、外部API的调用状态。

最后一层才考虑Agent-Reach自身的问题,比如是不是注册中心有bug、路由表是不是没刷新、熔断器是不是误打开了。之所以把它放最后,是因为从我遇到的故障比例来看,绝大多数问题出在被调Agent的自身逻辑或者它依赖的资源上,Agent-Reach这类框架层反而相对稳定。

5. 安全边界与权限控制

Agent之间可以互相调用了,随之而来的安全问题就不能忽视。在企业内部环境里,Agent持有的数据和能力可能涉及敏感信息,如果不加控制,任何一个被攻破的Agent都可能变成横向攻击的跳板。Agent-Reach在安全设计上做了几层防护。

5.1 Agent身份与Token

Agent之间通信必须有身份凭证。Agent-Reach为每个Agent颁发一对Client ID和Client Secret,启动时用它们换取短期访问Token。Token的有效期一般设为2小时,过期后自动刷新。关键点是:Agent之间的调用必须携带Token,注册中心不做隐式信任,即使请求来自内网IP。

在实际部署中,我见过很多团队图省事,Agent之间的调用直接走内网裸HTTP,觉得"反正防火墙挡着呢"。但现代微服务和Agent架构里,攻击面非常大,一个低权限Agent的日志泄露都可能被利用。Token化之后,至少能保证消息来源可识别、调用可追溯。

5.2 能力分级与授权

不同Agent的能力灵敏度是不一样的。一个"生成摘要"的Agent相对低风险,一个"查询员工薪资"的Agent就是高敏感。Agent-Reach引入了能力分级授权:每个Agent在注册时声明自己的安全等级,调用方必须拥有相匹配或更高等级的授权才能调用。

我在项目里将等级分为L1到L4。L1是公开通用能力,比如文本转写、通用搜索;L2是内部业务能力,比如生成销售报告;L3是包含个人信息的能力,比如查询某用户的订单记录;L4是核心资产能力,比如批量导出客户库里所有联系方式。路由层在转发请求前会校验调用方Agent的授权范围,如果等级不够,直接拒绝并记录审计日志。

这套分级的实现并不复杂,本质上是在能力注册表的每条能力里加一个security_level字段,在调用请求的Token里附带调用方的授权等级,路由层做一个简单的数值比较。难的是让各个Agent团队接受这个约束,并正确为每个能力定级。我的建议是宁可初定偏高,再用实际使用情况放宽,也不要一开始就漏成筛子,后期收紧时各个业务线会很不适应。

5.3 审计与可观测性

Agent-Reach从设计之初就把审计日志当作一等公民。每一次跨Agent调用,都会记录下调用方Agent ID、被调方Agent ID、调用时间、路由决策、成功失败状态、耗时等字段。这些日志统一汇集到可观测性平台,既用于排障,也用于安全审计。发生了"某个Agent异常调用另外一个Agent"的告警时,你可以快速回放当时的调用链。

可观测性这块我用了三个维度的监控指标:调用成功率、P90/P99延迟、以及错误类型分布。错误类型分布尤其重要,它把"连接失败""超时""业务拒绝""熔断拒绝"分开统计,能直接反映系统健康状态的组成。如果某天熔断拒绝的比例突然升高,虽然调用成功率还是99%,实际上网络通信已经出问题了,只是熔断器在兜底。没有这个维度,你很难及时发现这些隐患。

监控之外,规则引擎也值得配置一套告警。我最常用的一条告警规则是:在1分钟内,同一个调用方Agent对同一个被调方Agent的重试次数超过10次,立即触发告警。这种模式通常说明被调方存在持续性问题,继续重试没有意义,需要人工介入。另外,新注册的Agent上线后前24小时内额外增加监控频次,因为新上线Agent的配置和依赖往往不完善,容易出幺蛾子。

最后再说一点

整个Agent-Reach项目给我最大的体会是:做多Agent系统,别把重心全放在模型怎么聪明、提示词怎么花哨上,先把"Agent之间怎么稳定地互相够到"这件事做扎实。你设计的智能体网络里,任何一个节点不可达、契约对不齐、权限挡路,业务都会立刻感受到,而这些都跟模型智商无关。

如果你准备在项目里搭建Agent协作体系,我建议从最小可用版本开始:一个注册中心、一套能力声明、一份路由规则、一批健康检查。先让两个Agent稳定互调,再逐步扩张。不要一上来就搞几十个Agent互相协作,那样排障成本会吃掉你所有开发时间。Agent-Reach在我这边已经稳定运行了大半年,中间迭代了超过六版配置规范,每一次迭代都源于实际故障的教训。希望这套思路和细节能帮你在Agent协作的路上少踩几个坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询