降SpringAI阿里第13掌-密云不雨-跨域A2A
降SpringAI阿里第13掌-密云不雨-跨域A2A
同进程里叫协作,跨进程后叫契约
[读者] 已跑通第12掌多 Agent 编排的 Spring AI Alibaba 开发者
[痛点] 知识 Agent 一拆成独立服务,协作立刻从调方法变成扛网络
[现在读] 这一掌要把进程内约定升级为跨服务契约
[读完] 能拉起两个独立服务,用 A2A + Nacos 完成一次跨服务协作
[旧方案] 进程内 Handoff:同一 JVM 传对象,失败即抛异常
|
v
[新需求] 知识 Agent 独立部署、独立扩容、独立发版、跨团队维护
|
v
[冲突] 对象调用语义不等于网络调用语义:超时、重试、版本、身份全部浮现
|
v
[后果] 协作链条变脆:一次慢调用拖垮主流程,一次发版打穿兼容
我是老李,还在做那个数字人项目。前面十二掌,我们把它从一行 DashScope 配置,慢慢堆成了一个有 Supervisor、有路由、有 Handoff、有 Skills 的多 Agent 应用。所有 Agent 都住在同一个 Spring Boot 进程里,共享一个 ApplicationContext,共享一份 Memory,共享一条调用栈。那时候,Agent 之间说话不需要协议,因为它们是同桌同学,传个对象就够了。
这一掌的起点是一次很普通的组织变化:知识库团队想把知识检索和知识问答单独维护。他们有自己的数据源、自己的更新节奏、自己的发版窗口,不想再跟着主应用一起上线。于是知识 Agent 被划到了另一个团队,另一个仓库,另一个服务。
物理上一拆,问题就来了。原来一次进程内的 Handoff,现在变成了一次跨网络的请求;原来抛异常就是抛异常,现在可能是超时、可能是半截流、可能是对方重启、可能是三次重试后返回了三个互相矛盾的结果。核心冲突很清楚:我们习惯了用对象调用来表达协作,但跨服务的世界里,没有类型系统替你兜底,也没有同一个进程生命周期替你收尸。
同样是 Agent 之间的协作,为什么在第12掌用 Handoff 就够了,到了跨服务就必须引入协议?
如果必须引入协议,那我们已经在用的 MCP 能不能顺手把这件事办了,为什么还要再学一个 A2A?
就算协议选对了,网络的不确定性不会自己消失,超时、重试、版本、身份这些脏活,谁来收口?
01、故事
现场是这样的:数字人项目有一个 Web 运行页,用户进去以后,选一个数字人形象,浏览器端和 LiveKit 建立实时连接,服务端跑一条 Agent 链路。用户在页面上问一句产品政策,链路要先判断意图,再决定是直接回答、查知识库、还是走工具。
在第12掌结束时,这条链路是进程内的:Supervisor 决定下一个执行者是谁,Handoff 把当前上下文交给知识 Agent,知识 Agent 检索完把结果回填,Supervisor 再组织语言输出。整个过程在一次方法调用栈里完成,日志是连续的,异常是同步的,调试就是打断点。
这次任务是:把知识 Agent 从主应用里搬出去,做成一个独立部署的 Spring Boot 服务,主数字人通过 A2A 协议调用它,服务之间的发现交给 Nacos,同时明确它和已有 MCP 能力的分工。
当前方案看起来足够好:拆分只是把几个 Bean 挪到另一个工程,然后在主应用里把本地调用换成一个 HTTP 调用。真正开始动工才发现不是这么回事。
第一个变化是生命周期。原来知识 Agent 跟着主应用一起启动、一起停止,它不会在你发请求的时候突然消失。拆出去以后,它是一个可以被单独重启、单独扩容、单独回滚的服务。你调用它的时候,它可能正在滚动升级,三个实例里有两个正在下线。
第二个变化是失败语义。进程内调用失败只有一种形态:抛异常。跨服务调用失败有很多形态:连接不上、连上了但写不进去、写进去了但读超时、读到了一半流断了、重试之后成功了但对方已经执行了两次。这些形态如果不在协议层表达清楚,主流程就只能靠猜。
第三个变化是能力描述。进程内我们靠接口和类型知道对方能干什么,跨服务以后,主应用凭什么知道对面这个服务叫什么、支持哪些技能、接受什么格式、版本是多少?这需要一个显式的、可被发现的能力声明,而不是一份写在群公告里的文档。
联调第一天的现象很典型:本地跑通了,一上测试环境,数字人偶尔卡住十几秒,然后输出一句和问题无关的兜底回答。日志里主应用在等,知识 Agent 那边显示同一个问题处理了三遍。那一刻我意识到,这一掌真正要解决的不是"怎么调通",而是"怎么让不确定的远端变成可控的依赖"。
同室可传参
隔网须立契
一拆见风浪
一契定边界
02、问题
旧方案失效的第一个表现,是调用的确定性没了。进程内 Handoff 的正确性来自两个隐含前提:调用方和被调用方共享类型定义,且共享同一个失败边界。拆成服务后,类型定义各自维护,失败边界变成了两根网线之间的空白地带。
业务影响是直接的。数字人是面向终端用户的交互产品,用户感知不到"知识 Agent 在滚动升级",只感知到"这个数字人今天有点傻"。一次跨服务抖动,在用户侧就是一次答非所问;一次重复执行,在用户侧就是一次重复扣费或重复写库。
技术表现上,问题集中在这几个面:
第一,尾延迟传导。主流程为了等知识 Agent 的结果,线程或事件循环被占住,知识 Agent 的 P99 直接变成主服务的 P99。
第二,重试语义缺失。网络抖动触发了自动重试,但知识问答不是天然幂等的操作,重试变成了重复执行,返回了多份结果。
第三,服务发现边界模糊。Nacos 上明明注册了多个实例,流量却总是落到其中一个,负载均衡看起来没生效。
第四,版本兼容靠默契。新版知识 Agent 在能力声明里加了一个字段,老版主应用解析失败,直接导致整条链路不可用。
第五,身份边界缺失。任何人都能直接调知识 Agent 的接口,等于把一个内部能力暴露成了公网能力。
所以本篇要交付的完成标准,必须是可验证的,而不是"感觉能跑了":
一,存在两个独立进程的服务,主服务不依赖知识 Agent 的任何类,只依赖协议;
二,知识 Agent 注册到 Nacos 后,主服务能通过发现拿到实例列表,并且能观察到多实例被分散调用;
三,一次跨服务协作有贯穿的标识,从主服务日志能追到知识 Agent 日志,超时、重试、熔断的行为是确定的,不是随机的;
四,版本不兼容时失败是显式的、快速的、可诊断的,而不是静默返回错答案。
类型一旦散
契约须自证
标准先立好
结果才可验
03、原理
先给一个反直觉判断:A2A 的价值不在于"把方法调用换成 HTTP",而在于"把远端的不确定性接进本地的状态机"。很多人第一次接触 A2A,会以为它只是给 Agent 加了一个远程调用壳子,序列化一下消息就完事。实际上真正的技术含量在失败语义:一个任务什么时候算创建成功、什么时候算执行中、谁有权取消、超时之后对方还在不在跑、重试是新建任务还是续跑原任务。这些问题在本地调用里根本不存在,在跨服务里一个都不能回避。
第二个反直觉判断:MCP 和 A2A 的边界不是"新协议和老协议",也不是"对内和对外",而是能力归属。MCP 解决的是 Agent 如何使用外部能力:工具、资源、提示模板,被调用方是能力提供者,它没有自己的目标,不主动做决策。A2A 解决的是 Agent 如何与另一个 Agent 协作:被调用方是有自主性的主体,它可能自己再调工具、自己再拆任务、自己多轮追问,返回的不只是一个值,而是一个有生命周期的任务。
按这个标准,判断口径就很清楚了。知识库检索作为一个数据源,走 MCP 更合适:它是一个工具,输入查询、输出片段,无状态、无自主。可是"知识 Agent"如果承担的是多轮澄清、判断该查哪张表、决定要不要反问用户、最后组织成一段可讲解的答案,那它就是一个主体,走 A2A 更合适。我们在这一章做的,就是后者。
第三个原理是关于契约的形态。跨服务协作的契约不是方法签名,而是能力声明加任务状态。能力声明告诉对方"我是谁、我会什么、我怎么被访问",任务状态告诉对方"你交给我的那件事现在到哪一步了"。把契约写成这两个东西,才可能做出版本协商、灰度发布和可观测。
还有一个容易被忽略的点:流式不是锦上添花。数字人的交互体验依赖首字延迟,如果跨服务调用只能等最终结果,那么所有下游优化都会卡在这一个阻塞点上。协议层支持流式,本质上是让"部分结果"成为一种合法的、可被消费的中间状态,而不是一个异常。
远端非函数
契约非签名
边界看归属
失败要归位
04、架构
[输入] 用户在数字人 Web 运行页提问
|
v
[模块] 主服务 DH(Supervisor + ReactAgent)+ Nacos + A2A Client
|
v
[数据/状态] AgentCard / 实例列表 / Task 状态(traceId、taskId、阶段)
|
v
[处理] 发现实例 → 选择实例 → 创建 A2A 任务 → 流式消费 → 结果合成
|
v
[输出] 数字人口播回答 / 超时降级回答 / 明确失败
架构边界要先划清楚。主服务负责编排和最终表达,它知道"要问什么",但不知道"怎么查";知识 Agent 负责检索和知识域内的推理,它知道"怎么查",但不负责数字人的语气、形象和实时通道。中间那层是协议与注册中心,它不承载业务语义,只承载"谁能被找到、谁还能用、谁在提供什么能力"。
收益方面,最直接的是生命周期解耦:知识 Agent 可以按自己的节奏发版和扩容,主服务不需要跟着重建。其次是故障隔离,知识域的问题不再天然地等于整个数字人不可用。第三是团队边界,跨团队协作从"读对方的代码"变成"读对方的能力声明"。
代价也很实在。原来一次本地调用的耗时,现在至少多了序列化、网络往返和发现解析三段开销;原来一次异常,现在可能变成一套超时加退避加熔断的组合策略;原来一个断点能解决的事,现在要跨两个服务的日志去拼。更隐蔽的代价是状态:任务有生命周期之后,就有了"谁负责清理悬挂任务"这个新问题。
适用条件需要说清楚。只有当被拆出去的那部分具备独立生命周期、独立团队、独立扩缩容需求时,A2A 的收益才盖得住成本。如果只是想让代码目录看起来整洁,把一个纯函数检索拆成服务,那是把复杂度换了个地方放,并没有消掉。
一图分两侧
注册居其中
状态有归属
链路才可追
05、实战一次
环境与版本:JDK 21 LTS,Spring Boot 3.x,alibaba/spring-ai-alibaba v1.1.2.2,Nacos 作为注册与配置中心,分支 chapter/13-a2a-nacos,提交信息建议 feat(ch13): add A2A and Nacos agent discovery,实践仓库沿用 https://github.com/lifuchun522/springaialibabapractice 。
需要提醒的是,v1.1.2.2 中 A2A 相关 starter 的坐标、自动配置类名与 API 签名,请以官方文档和主仓库 spring-boot-starters 目录的实际内容为准。本节的类名与配置项为结构示意,接入前需按当前官方文档核验:https://github.com/alibaba/spring-ai-alibaba/tree/v1.1.2.2/spring-boot-starters 。
依赖:拆成两个模块,knowledge-agent(服务端)和 dh-web(客户端)。
<!-- knowledge-agent:暴露 A2A Server 能力 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-a2a-server</artifactId>
<version>1.1.2.2</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-nacos-discovery</artifactId>
<version>1.1.2.2</version>
</dependency>
<!-- dh-web:作为 A2A 客户端消费远程 Agent -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-a2a-client</artifactId>
<version>1.1.2.2</version>
</dependency>
配置:知识 Agent 侧声明服务身份与能力入口,主服务侧声明发现方式。
# knowledge-agent / application.yml
server:
port: 8081
spring:
application:
name: knowledge-agent
ai:
alibaba:
a2a:
server:
enabled: true
card:
name: knowledge-agent
description: 企业知识库检索与问答 Agent
version: 1.0.0
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: public
# dh-web / application.yml
server:
port: 8080
spring:
application:
name: dh-web
ai:
alibaba:
a2a:
client:
enabled: true
connect-timeout: 1s
read-timeout: 8s
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
namespace: public
核心实现:知识 Agent 侧把一个技能挂到能力声明上,技能内部就是真实的知识检索逻辑。
@Configuration
public class KnowledgeAgentConfig {
@Bean
public AgentCard knowledgeAgentCard() {
// 类名与 builder 方法以 v1.1.2.2 官方 API 为准
return AgentCard.builder()
.name("knowledge-agent")
.description("企业知识库检索与问答 Agent")
.version("1.0.0")
.skill(Skill.builder()
.id("kb.answer")
.name("知识问答")
.description("基于内部知识库回答产品与政策问题")
.build())
.build();
}
}
主服务侧不引用知识 Agent 的任何类,只通过远程 Agent 抽象调用。
@Service
public class KnowledgeGateway {
private final A2aRemoteAgent knowledgeAgent;
public KnowledgeGateway(A2aRemoteAgent knowledgeAgent) {
this.knowledgeAgent = knowledgeAgent;
}
public String ask(String question) {
// 远程 Agent 的调用签名与返回值以官方 API 为准,
// 生产环境需要在调用处显式叠加超时、重试策略与降级逻辑
return knowledgeAgent.call(question);
}
}
启动与请求:先起 Nacos,再起知识 Agent,最后起主服务。
java -jar knowledge-agent/target/knowledge-agent-1.0.0.jar --server.port=8081
java -jar dh-web/target/dh-web-1.0.0.jar --server.port=8080
curl -s "http://127.0.0.1:8080/api/dh/ask?q=退款政策是什么"
验证(未在当前环境实测,以下为预期结果):
# 示例输出
dh-web INFO [dh-web,7f3a9c1e] discovered 1 instance of knowledge-agent
knowledge-agent INFO [knowledge-agent,7f3a9c1e] task received, skill=kb.answer
dh-web INFO [dh-web,7f3a9c1e] answer assembled, cost=...ms
验证清单:Nacos 控制台能看到 knowledge-agent 实例;两个服务的日志里出现同一个追踪标识;把 knowledge-agent 停掉后,主服务返回明确失败或降级答案,而不是无限挂起。做到这一步,V1 就算跑通了,但它同时暴露了后面要处理的一堆问题。
先通再谈稳
一通方见坑
契约立得住
拆服务才值
06、排查
诊断链一:同一个问题被处理了三遍。
现象:测试环境里用户问一次,知识 Agent 日志出现三条相同的检索记录,主服务最终输出的答案却只用了其中一条。
怀疑:重试机制把一次调用放大成了多次执行。
检查:核对客户端重试配置、网关超时、以及知识 Agent 侧对同一请求的处理记录,把 taskId 和 traceId 对齐看。
证据:三条记录的请求体完全一致,但 taskId 各不相同;主服务侧的日志显示第一次调用在读超时后触发重试,第二、三次才拿到响应。
根因:读超时被设置得小于知识 Agent 的 P99 处理时间,超时触发重试,重试又没有携带幂等键,于是每次重试都是一次全新的任务创建。重试次数乘以单次超时,又超过了上游网关的超时,最终用户看到的是兜底回答。
修复:把超时按连接、读、任务总时长三层拆分,读超时要大于业务 P99;对可重试的调用携带幂等键,让重复请求能被识别;限制重试次数并加入退避,禁止无界重试。
错误尝试: 第一反应是把重试次数从 2 次提到 5 次,理由是"多试几次总能成功"。这个尝试是错的,因为根因不是网络抖动,而是尾延迟导致的超时误判。重试次数越多,知识 Agent 侧的重复计算越多,负载越高,P99 越差,超时概率反而上升,形成一个自我强化的负面循环。重试是给偶发失败用的,不是给慢用的。
诊断链二:Nacos 上三个实例,流量只打到一个。
现象:知识 Agent 扩到三个实例,Nacos 控制台三个都显示健康,但主服务的请求几乎全部落到 instance-a。
怀疑:负载均衡策略没生效,或者客户端缓存了旧的实例列表。
检查:确认三个实例的元数据是否一致、健康检查端点是否真的可访问、主服务侧是否开启了本地缓存以及缓存刷新周期。
证据:instance-b 和 instance-c 的健康检查路径返回 404,被注册中心标记为不健康,只有 instance-a 真正在候选列表里;同时主服务侧存在实例列表本地缓存,在扩容后没有及时刷新。
根因:注册元数据不统一,健康检查地址配置错误导致新实例从未进入可用集合;客户端缓存刷新滞后,进一步放大了"只有一个实例"的观感。
修复:统一实例元数据模板,把健康检查地址作为标准字段纳入模板;缩短客户端实例列表缓存刷新周期;把"可用实例数"作为一项监控指标暴露出来,而不是等出问题才去控制台看。
诊断链三:发版后主服务直接报错。
现象:知识 Agent 升级到新版本后,主服务在协作开始前就抛出解析失败。
怀疑:能力声明的字段发生了变化。
检查:对比新旧版本的 AgentCard 内容,重点看新增字段是否被标记为必填。
证据:新版在能力声明中新增了一个必填字段,老版主服务的解析器不认识它,直接判定为非法。
根因:把能力声明当成了普通 DTO,用"加字段就是改契约"的方式演进,没有给字段留可选性和默认值,也没有做版本协商。
修复:能力声明只做向后兼容的演进,新增字段一律可选并带默认值;主服务在协作前先比对版本区间,超出支持范围就快速失败并输出可诊断的错误,而不是静默错答。
三病同一理
皆因界不清
先证后动手
改错不改正
07、优化
基于第06章的三条证据,V2 不再改动业务逻辑,只做边界收口。
第一,超时分层。 根因是单层超时无法区分"连不上"和"处理慢"。修改为连接超时、读超时、任务总时长三层,其中读超时由知识域的业务 P99 反推,任务总时长由主流程可接受的首字延迟反推。原因在于不同层的超时代表不同含义,混在一起就只能靠加大数值硬扛。新行为是连接问题快速失败、慢任务被正确等待、超长任务被明确终止。验证方式是人为注入延迟,观察三层超时分别触发时的日志是否可区分。
第二,幂等重试。 根因是重试与任务创建绑死。修改为调用时携带幂等键,服务端对相同键的重复请求返回同一任务状态,不做重复计算;重试只对连接失败和明确的临时错误生效,采用指数退避加抖动,并设置次数上限。原因在于重试的前提是操作可重复,而知识问答的"执行"本身有成本。新行为是抖动时最多创建一次任务。验证方式是抓包确认同一幂等键只对应一个 taskId。
第三,熔断与降级。 根因是知识 Agent 不可用时主流程没有止损点。修改为按实例维度统计失败率,超过阈值后快速失败,并对该实例做短时熔断,同时主服务返回一个明确标注来源的降级回答。原因在于跨服务依赖必须假设对方会挂,挂的时候本地要能给出确定行为。新行为是从"卡十几秒再兜底"变成"立刻兜底并报警"。验证方式是把知识 Agent 直接下线,测量主流程的响应时间与返回内容是否稳定。
第四,服务身份。 根因是内部能力被无差别调用。修改为在服务之间引入身份凭证,注册与调用都要求鉴权,能力入口不对外开放。原因在于一旦服务被发现,就等于被发现者可见。新行为是未携带合法凭证的调用在协议层被拒绝,而不是进入业务逻辑。
第五,版本协商。 根因是契约靠默契演进。修改为能力声明中带版本区间,主服务在协作前协商,落在支持区间内才继续。原因在于跨团队发布节奏不可能永远同步。新行为是版本不兼容时快速失败,错误信息明确指出两端版本,而不是等业务出错再回溯。
需要说明的是,以上都是行为层面的改造方案,具体数值阈值必须由真实业务分布决定,不存在一组可以照抄的通用参数。
慢要分三层
错分可重来
有病先止损
无序先协商
08、演进
[同一输入] 用户提问:我们产品的退款政策是什么
|
+--[V1] 进程内 Handoff 直调
| 行为:一次调用栈内完成,快
| 代价:知识域与主流程同生共死,无法独立发版
|
+--[V2] A2A + Nacos 跨服务协作
| 行为:发现实例、创建任务、流式消费、边界收口
| 代价:多一次网络与状态成本,需要治理能力
|
[Trade-off]
得到:独立生命周期、故障隔离、跨团队边界清晰
失去:本地调用的确定性、最低延迟和单栈调试的便利
适用边界:被拆方需要独立发版、独立扩缩容、独立团队维护
正确性上,V1 依赖共享类型,编译器能替我们挡住大部分契约错误;V2 依赖显式能力声明与版本协商,正确性从"编译期保障"变成了"运行期校验",代价是必须写测试和校验逻辑,收益是两端可以独立演进。
稳定性上,V1 的故障是同步的、可预期的;V2 的故障是多形态的,但一旦把超时、重试、熔断、身份都收口,系统的整体稳定性反而更高,因为故障被限制在了局部,不会再出现"一个知识检索把整个数字人拖死"的局面。
复杂度上,V2 明显更高。多了一个注册中心,多了一层协议,多了一套治理策略,多了跨服务日志追踪的需求。这些复杂度不是免费的,它换回来的是组织上的可并行。如果团队只有两个人、只有一个发版节奏,这笔交易很可能是亏的。
成本上,V2 的固定成本是基础设施与治理代码,可变成本是每次调用多出的网络与序列化开销。固定成本可以摊薄,可变成本无法消除,只能通过减少不必要的跨服务调用、把高频低价值调用留在本地来优化。
适用范围上,V2 适合边界稳定、调用频率中等、单次任务语义完整的 Agent 协作。反过来,如果被调用方只是查一张表返回一段文本,那用 MCP 暴露成一个工具更轻,不必强行为它造一个任务生命周期。
遗留问题也要说清楚:任务的悬挂清理、跨服务的上下文传递边界、以及多实例下的会话粘连,这三件事在 V2 里只是有了对策,还没有到可以不再想的地步。
一拆两世界
一契连两端
得处即失处
取舍在边界
09、洞见
9.1 拆服务不是拆代码,是拆失败域
反直觉判断:把知识 Agent 拆成独立服务,最大的变化不是部署方式,而是失败的影响范围发生了重划。拆之前,知识检索失败和数字人回答失败是同一件事;拆之后,它们是两件事,只要主服务愿意,它可以带着降级答案继续服务。很多团队拆完以后仍然把两个服务绑在一起发布、绑在一起监控、绑在一起回滚,等于只拿到了成本,没拿到收益。判断拆分是否真的完成,看的不是仓库数量,而是故障是否能被限制在边界内。
9.2 MCP 管能力,A2A 管主体
反直觉判断:选 MCP 还是 A2A,跟"内部还是外部"无关,跟"被调用方有没有自主性"有关。一个只会按输入返回输出的检索接口,即使用公网暴露,它本质上还是工具;一个会追问、会拆解、会自己决定查哪里的 Agent,即使是内网部署,它也是需要 A2A 对待的主体。这条判断标准能省下大量架构争论:先问对方是否有自己的任务生命周期,再谈协议选型。
9.3 跨服务协作的第一个接口是错误码,不是方法签名
在进程内,我们定义接口时先写方法签名;跨服务时,接口的第一版应该先写失败分类。哪些错误可以重试、哪些必须立刻失败、哪些要降级、哪些要告警,这些决定了调用方怎么写代码。如果失败分类没定清楚,方法签名定得再漂亮,一旦线上抖动,调用方依然只能靠兜底。
9.4 服务发现真正的难点是下线,不是上线
反直觉判断:注册发现看起来是"让服务被找到",实际工程中最容易出事的是"让服务不再被找到"。新实例注册失败很显眼,老实例下线不干净却很隐蔽:请求打到正在关闭的实例上,表现为偶发的、难以复现的超时。滚动升级、缩容、发布回滚都依赖下线语义的正确性,这一块的测试优先级应该高于扩容。
9.5 版本兼容靠协商,不靠约定
跨团队的发布节奏不可能永远对齐,把兼容性寄托在"发版前打个招呼"上,是把工程问题当组织问题解。能力声明里带版本区间、协作前做一次协商,成本很低,但它把兼容性从人的记忆转移到了代码的执行路径上。凡是能写进协议的东西,都不应该留在聊天记录里。
边界按主体
失败先分类
下线路更险
兼容靠协商
10、系统落地
原来有的:一个数字人主服务,内部装着 Supervisor、路由、Handoff 和知识 Agent,靠 MCP 接工具与数据源,靠 Memory 维持会话,靠 Graph Runtime 编排流程,所有 Agent 共享一个进程和一条调用栈。
本篇新增的:AgentCard 形式的能力声明、基于 A2A 的远程 Agent 调用、基于 Nacos 的注册发现与实例选择、以及围绕调用边界的超时分层、幂等重试、熔断降级、服务身份和版本协商。知识 Agent 从主服务的内部件,变成了一个能被发现、能被独立发版、也能被单独熔断的远程主体。
现在能做的:数字人主服务可以在不重新打包、不感知实现的前提下,调用属于另一个团队、运行在另一组实例上的知识 Agent;知识 Agent 可以独立扩容、独立发布、独立回滚;跨服务链路有统一的追踪标识,能从主流程一路追到知识域;知识域不可用时,主流程有确定的降级行为,而不是随机卡顿。
还缺的:跨服务的上下文一致性还没有统一方案,多轮会话中主服务和知识 Agent 各自持有的上下文可能出现分叉;任务的悬挂清理只有策略没有机制;评测层面,跨服务协作的质量还没有被纳入自动化评测,仍然靠人工回归。
下一步的演进方向:把这些跨服务协作链路纳入统一的观测与评测体系,让"知识 Agent 变慢了"这件事在用户感知之前就被发现;同时把身份与配置从静态文件推进到动态下发,让超时、熔断阈值这类参数可以按环境调整,而不必重新发版。
旧件成远亲
新契连两端
能拆亦能断
方算真解耦
11、小结
Q1 → 进程内协作靠共享类型和同进程生命周期兜底,跨服务这两样都没了,必须用能力声明加任务状态把协作显式表达出来
Q2 → MCP 面向 Agent 调工具与资源,A2A 面向 Agent 与远程 Agent 协作,判断口径是被调用方有没有自主任务生命周期
Q3 → 用能力声明做版本协商、用 Nacos 做注册发现、用三层超时加幂等重试加熔断降级加服务身份,把网络不确定性收口在调用边界
状态 → 两个独立 Spring Boot 服务可以通过 A2A 完成一次跨服务协作,并具备发现、超时、重试、熔断与版本基线
一问为何拆
二问谁归谁
三问谁兜底
一答定全章
12、作业
12.1 理解题:MCP 与 A2A 的边界应该按什么标准划分?
参考答案:按被调用方的自主性划分,而不是按内网外网或新老协议划分。如果被调用方只是被动执行、输入输出确定、没有自己的任务生命周期,它更适合作为 MCP 的工具或资源暴露;如果被调用方有自主决策、可能多轮澄清、需要独立的任务状态,就应该用 A2A 作为主体协作。实践中还有一个辅助判据:被调用方是否需要独立发版、独立扩缩容和独立团队,这三条同时成立时,A2A 的收益才盖得住成本。
12.2 实战题:把知识 Agent 拆成独立服务,并用 A2A 完成一次调用。
参考答案:新建 knowledge-agent 模块,引入 A2A Server 与 Nacos 注册依赖,声明能力声明并暴露一个知识问答技能,端口与主服务区分;主服务引入 A2A Client 依赖,通过远程 Agent 抽象调用,不引用知识 Agent 的任何类。启动顺序为 Nacos、知识 Agent、主服务。验证点:注册中心能看到实例;两服务日志出现同一追踪标识;停掉知识 Agent 后主服务有确定的失败或降级行为。注意 starter 坐标与 API 签名需按 v1.1.2.2 官方文档核验。
12.3 排障题:跨服务调用偶发超时,日志显示同一问题被处理多次,怎么定位?
参考答案:先对齐两边的追踪标识,确认是同一逻辑请求对应多个执行记录。再看超时配置:如果读超时小于下游业务 P99,超时会被误判为失败并触发重试。第三看重试是否携带幂等键,没有幂等键的重试等价于重复创建任务。第四看重试次数乘以单次超时是否超过了上游网关超时。修复顺序是先把超时分层并放大读超时,再补幂等键,最后限制重试次数并加退避。切忌直接加大重试次数。
12.4 架构判断题:知识 Agent 拆分后,是否应该和主服务一起发布、一起回滚?
参考答案:不应该。拆分的核心收益就是生命周期解耦和故障隔离,如果仍然绑定发布与回滚,等于承担了分布式成本却没有拿到分布式收益。正确的做法是让知识 Agent 独立发布,通过能力声明的版本协商保证兼容,通过熔断和降级保证主服务在知识域不可用时仍然可用。只有在两者共享同一份强一致状态、无法接受短暂不一致时,才需要重新评估是否值得拆分。
题问边界处
答在证据中
能答亦能证
方为真掌握
13、思考
这一掌的核心冲突不是"怎么把一个服务调通",而是"当协作跨越进程边界之后,我们靠什么维持确定性"。进程内的确定性来自语言和运行时:类型、异常、同一个生命周期。跨服务之后,这些都没了,剩下的确定性只能靠我们自己显式地造出来——用能力声明造出可知,用版本协商造出兼容,用超时分层造出可预期,用幂等和熔断造出可恢复。
也就是说,跨服务 Agent 协作的工程量,绝大部分不在业务逻辑里,而在边界的定义与收口里。把 MCP 和 A2A 分清楚,是为了让每条调用都落到正确的责任主体上;把身份、超时、重试、版本、熔断补齐,是为了让每一次失败都有确定的归宿。一个能被拆开、也能被单独断开的系统,才谈得上真正解耦。
界不清则乱
契不立则散
败不归则险
定其界方安

浙公网安备 33010602011771号