第11章 MCP+A2A,助力旅游规划团队协作
11-1 回顾AgentScope旅游规划的整体架构
旅游规划项目整体架构的回顾,涵盖基于AgentScope与Spring Boot框架的模块化设计、主管智能体的任务分解与分发机制、A2A协议下通过Nexus注册中心实现的远程智能体发现与调用,以及工具挂载、PlanLowBook规划能力、Skills专业流程嵌入等核心组件。
目标与架构更新背景
- 回顾旅游规划项目整体架构。
- 因AI新功能“skills”(专业固定流程)推出,新增深度讲解章节,并将skills嵌入项目。
项目基础框架与模块划分
- 基于两个框架:AgentScope 1.0 与 Spring Boot(采用最新版)。
- 采用Docker部署多节点Agent,故进行模块化设计,共四个模块:
- 公共模块(common) :存放公共工具类。
- 主管智能体模块(manager):负责整体旅游规划需求的接收、自主任务分解、执行计划制定与任务分发。
- 路线制定智能体模块(route-planner):专职执行路线规划任务。
- 行程规划智能体模块(itinerary-planner):专职执行行程安排任务。
主管智能体的核心能力
- 自主分解用户提交的复杂Prompt需求。
- 集成 Hook插件机制 :作为可挂载插件,支持日志输出、执行过程可视化等扩展能力。
- 依赖 PlanLowBook对象 :实现任务分解、步骤生成、状态跟踪与动态调整,赋予主管智能体自主规划与决策能力;该对象支持自定义,包括人工确认开关、计划存储方式及状态跟踪策略 。
A2A协议与Nacos注册中心机制
- 主管智能体通过 A2A(Agent-to-Agent)协议 发现并调用远程智能体。
- 采用 Nacos注册中心 作为A2A协议注册点 。Nacos
- 路线制定与行程规划智能体向Nacos注册其 智能体卡片 ,包含所持技能(skills)、专长领域等元数据。
- 主管智能体从Nacos查询并识别可用远程智能体及其能力,据此精准分发任务 。
**任务执行流程与思考范式 **
主管智能体分发任务后,真正执行业务逻辑的是远程智能体)。
各智能体均遵循
思考-执行-反馈自主执行任务模式:
- Think:理解任务目标与上下文;
- Act:执行具体操作;
- Reflect:反思执行结果是否符合预定目标,不一致则自我修正 。
执行过程中将嵌入 skills专业流程 :skills非单一工具,而是一整套标准化、固定化的专业工作流程 。
公共模块工具类详解
- 共三个工具类:
- AgentBuilder工具类 :封装AgentScope框架下智能体的构建逻辑。
- NacosUtil工具类 :用于生成Nacos服务客户端,支撑A2A注册与发现。
- ToolUtils工具类 :封装工具挂载(tool loading)与工具注册逻辑。
**工具挂载(Tool Loading)机制 **
- 智能体通过
builder.toolKey()方法挂载工具包 ; - 工具包内注册MCP等具体工具 ;
- 挂载后智能体即具备对应工具调用能力 ;
- 公共模块已将工具挂载与注册逻辑封装为复用工具类。
**远程智能体调用实现 **
- 主管智能体将获取到的远程智能体作为自身工具进行调用 ;
- 在主管模块的
@Tool方法中实现远程智能体获取逻辑 ; - 通过A2A协议,使主管智能体可识别并调用 ;
- 调用本质是:主管智能体将任务委托给已注册至Nacos的远程智能体执行,后者作为其“可执行工具” 。
**已实现三大核心功能总结 **
- 主管智能体对复杂用户需求的自主分解能力;
- 基于A2A协议与Nacos注册中心的远程智能体发现机制;
- 主管智能体将远程智能体作为工具挂载并调度执行任务的能力。
11-2 测试团队成员基于A2A协议注册到Nacos
旅游规划项目中主管智能体与远程智能体协同机制的实现原理及A2A协议注册中心的测试验证过程,重点解析了AgentScope框架自带注册中心与Nacos注册中心的区别、Bean自动注入机制、依赖管理优化等关键技术点,并通过实际运行演示了行程规划智能体成功注册至Nacos注册中心的全过程。
**项目已实现功能回顾 **
- **主管智能体的任务分解功能 **:将复杂需求自主分割为多个简单子任务。
- 基于Nacos的A2A协议注册中心功能:远程智能体可将自身智能体卡片注册至Nacos注册中心;主管智能体可从中获取卡片并分发任务。
- **主管智能体工具化注册功能 **:主管智能体将获取的远程智能体作为工具注册至自身注册中心,并在任务分发时调用该工具执行子任务。
**核心目标:注册中心功能测试 **
- 测试内容:验证远程智能体能否成功将智能体卡片注册到Nacos注册中心,以及主管智能体能否从中正确获取卡片。
- 测试前提:仅需导入
agentscope-a2a-spHookEventring-boot-starter与agentscope-nacos-spring-boot-starter两个依赖,即可支持A2A协议及Nacos注册中心。
启动报错分析与根本原因
错误现象:行程规划智能体模块启动时报“缺少bean”错误。
**关键知识点一:AgentScope自带注册中心 **:AgentScope框架内置注册中心对象(
agent-code-a2a-server),无需强制使用Nacos。关键知识点二:智能体卡片注册的两种方式
自动注入方式(Spring Boot Bean):通过
@Component声明Bean,由Spring Boot自动注入至指定注册中心。java@Component public class TripPlannerAgent { @Bean public ReActAgent getTripPlannerAgent() { //行程规划Agent Builder ReActAgent.Builder builder = AgentUtils.getReActAgentBuilder("TripPlannerAgent", "擅长处理景点行程规划"); return builder.build(); } }手动写入方式:直接调用注册中心对象的方法,传入自定义智能体卡片。
java//=========== 手动写入注册中心,项目不用种方式 START ==== //行程规划Agent 智能体卡片 ConfigurableAgentCard agentCard = new ConfigurableAgentCard.Builder() .name("TripPlannerAgent") .description("行程规划Agent") .build(); //将智能体卡片写入到AgentScope自带的注册中心 AgentScopeA2aServer.builder(builder) .agentCard(agentCard) .deploymentProperties(new DeploymentProperties("localhost",8080)) .build(); //还需要AgentScopeA2aServer启动 //======== 手动写入注册中心,项目不用种方式 END ====
**手动注册方式详解 **
- 注册中心构建:通过
DeploymentProperties配置注册中心地址与端口:new DeploymentProperties("localhost", 8080)。 - 智能体卡片写入:调用
agentCard()方法传入自定义智能体卡片对象,支持完全自定义卡片字段。 - 注册中心启动要求:手动注册前必须显式启动AgentScope自带注册中心实例。
- 项目适配性说明:因本项目为分布式架构,不采用手动注册方式,故注释相关代码段。
自动注入方式实现
- Bean定义规范
- 添加
@Component注解标识为Spring组件; - 方法返回类型必须为
ReactAgent对象(如withAnAgent); - 返回值即为待注册的智能体实例。
- 添加
- 注册中心定位机制:Spring Boot依据配置文件中
Nacos配置项(如nacos.address=localhost:8080)自动识别目标注册中心。 - 智能体卡片生成逻辑:框架自动从
ReactAgent对象中提取name、description等元信息,构造标准智能体卡片,无需手动编写卡片类。
依赖管理优化
- 问题根源:
agentscope-a2a-spring-boot-starter依赖被置于根pom.xml的<dependencies>中,导致所有模块(含无需注册的主管智能体模块)均强制引入该依赖,但主管模块未提供对应Bean而报错。 - 解决方案: 将依赖移至
<dependencyManagement>标签内,仅在需注册功能的模块(行程规划、路线制定)中显式声明该依赖。 - 效果验证: 避免无关模块因缺失Bean而启动失败,实现按需依赖注入。
Nacos注册中心验证流程
- 前置准备:启动Nacos服务(执行文档中提供的启动命令),访问管理页面确认
Agent列表为空。 - 模块启动与日志观察:启动行程规划模块,日志显示
Spring Boot successfully registered agent card to Nacos。 - 页面验证结果
- 刷新Nacos管理界面
Agent列表,出现行程规划智能体卡片; - 点击详情页,
description字段值为“形成规划智能体”,name字段与代码中设置一致; - 证实智能体卡片信息由框架自动从
ReactAgent对象中抽取并持久化至Nacos。
- 刷新Nacos管理界面
**总结与关键结论 **
- AgentScope框架同时支持内置注册中心与第三方注册中心(如Nacos),二者互不冲突。
- 自动注入方式依赖Spring Boot生命周期与配置驱动,简洁高效;手动方式灵活但需额外维护注册中心实例。
- 分布式项目中应严格遵循“谁需要谁引入”原则,通过
<dependencyManagement>实现精细化依赖管控。 - Nacos注册中心成功接收并展示智能体卡片,标志着A2A协议远程协同能力已具备工程落地基础。
11-3 测试主管Agent基于A2A协议获取团队成员
主管智能体如何通过Nacos注册中心发现并调用远程智能体(路线制定智能体)的完整流程,包括智能体卡片注册一致性、@Tool注解工具方法定义、A2aAgent对象获取与call()阻塞调用、日志验证机制及多模块协同启动实践。
主管智能体调用远程智能体的整体目标
- 内容:课聚焦主管智能体通过Nacos注册中心发现并调用已注册的远程智能体。
- 前置基础确认:行程规划智能体与路线制定智能体均已按相同方式注册至Nacos注册中心,其智能体卡片具备唯一
name属性。
远程智能体注册机制复述
**行程规划智能体注册状态 **:已完成行程规划智能体卡片写入Nacos注册中心。
路线制定智能体注册状态
路线制定智能体(即路线规划智能体)已按相同流程注册至Nacos注册中心。
注册方法与行程规划智能体完全一致,即通过自动注入智能体卡片实现 。
java@Component @Slf4j public class RouteMakingAgent { @Bean public ReActAgent getRouteMakingAgent() { //注入到Nacos return AgentUtils.getReActAgentBuilder("RouteMakingAgent", "擅长处理自驾游路线制定").build(); } }
**注册关键配置 **:仅需将对应组件设为
Bean即可完成注册配置。
**主管智能体模块结构与工具方法定义 **
智能体工具基础规范:主管智能体模块中,智能体工具需使用
@Tool注解修饰。已定义的两个远程智能体获取工具
工具一:获取远程路线制定智能体。
java@Tool(description = "从Nacos注册中心获取路线制定Agent") public void callRouteMakingAgent() throws NacosException { log.info("工具方法:路线制定智能体...正在调用中"); A2aAgent agent = A2aAgent.builder() .name("RouteMakingAgent") .agentCardResolver( //创建 Nacos 的 AgentCardResolver new NacosAgentCardResolver(NacosUtil.getNacosClient())) .build(); log.info("获取到的远程Agent描述:"+agent.getDescription()); //远程Agent运行 agent.call().block(); }工具二:获取远程行程规划智能体 。
java@Tool(description = "从Nacos注册中心获取行程规划Agent") public void callTripPlannerAgent() throws NacosException { A2aAgent agent = A2aAgent.builder() .name("TripPlannerAgent") .agentCardResolver( //创建 Nacos 的 AgentCardResolver new NacosAgentCardResolver(NacosUtil.getNacosClient())) .build(); //远程Agent运行 agent.call().block(); }
工具执行逻辑 : 两工具均通过Nacos注册中心读取对应智能体卡片 ),进而获取远程智能体实例 )。
**功能完成状态说明 **:此前所写功能尚未进行实际测试 。
测试目标与验证路径
- 核心验证目标:测试主管智能体能否成功从注册中心Nacos读取智能体卡片 。
- 延伸验证目标:验证主管智能体能否驱动远程智能体执行具体任务。
远程智能体获取技术实现
- 获取对象类型:通过
A2aAgent对象获取远程智能体。 - 获取参数配置
- 传入Nacos注册中心地址作为方法参数 。
- 依据
name属性匹配注册中心内智能体卡片。 name值必须与注册中心Nacos中卡片的name属性严格一致,方可成功获取。
- **返回对象确认 **:成功获取后返回
A2aAgent对象实例。
**远程智能体调用与运行机制 **
- 调用入口方法:通过
call()方法运行A2aAgent对象 。 - **执行模式特性 **:
call()方法为阻塞式同步执行 。
日志验证方案设计(在@Tool中)
- **日志注解配置 **: 使用
@Slf4j注解启用日志功能 。 - **非空校验逻辑 **: 若
A2aAgent对象不为空,则证明远程智能体获取成功,并打印日志 。 - 描述属性验证逻辑
- 打印
A2aAgent对象的getDescription()返回值 。 - 通过比对描述内容(如“路线制定智能体”)确认获取对象准确性 。
- 打印
主管智能体启动方式与Prompt调整
- 启动类运行模式
- 前期测试采用
main函数直接运行,未启动Spring容器。 - 因当前需依赖Spring上下文注入,后续需切换为Spring容器启动模式 。
- 前期测试采用
- Prompt驱动调用逻辑
- 主管智能体需接收Prompt指令以触发对应
@Tool方法 。 - 当前测试目标为调用路线制定智能体,故Prompt需明确指示“调用路线制定智能体”。
- 方法描述(description)与Prompt语义匹配是触发成功的前提。
- 主管智能体需接收Prompt指令以触发对应
11-4 测试主管Agent调动团队成员执行任务
路线智能体调用MCP服务配置以及主管智能体调用路线智能体的业务流程
路线智能体配置MCP服务,先不实现MCP中具体业务,创建mcp模块,构建MCP服务
@Slf4j
public class BaiduMapMCP {
/**
* description: 创建百度地图MCP客户端
*/
@Tool(description = "百度地图MCP Server")
public void getBaiduMapMCP() {
log.info("==================");
log.info("正在调用百度地图MCP....");
log.info("==================");
}
}将MCP服务工具注册到路线规划智能体中
@Component
@Slf4j
public class RouteMakingAgent {
@Bean
public ReActAgent getRouteMakingAgent() {
ToolUtils toolUtils = new ToolUtils();
Toolkit toolkit = toolUtils.getToolkit(new BaiduMapMCP());
//打印挂载的工具
Set<String> toolNames = toolkit.getToolNames();
toolNames.stream().forEach(value -> log.info("挂载的工具名称:"+value));
//注入到Nacos
return AgentUtils.getReActAgentBuilder("RouteMakingAgent", "擅长处理自驾游路线制定")
//工具包
.toolkit(toolkit)
.build();
}
}再修改主管智能体中的基于A2A协议获取路线制定Agent的工具
@Slf4j
public class RemoteAgentTool {
/**
* description: 基于A2A协议获取路线制定Agent
*/
@Tool(description = "从Nacos注册中心获取路线制定Agent")
public void callRouteMakingAgent() throws NacosException {
log.info("工具方法:路线制定智能体...正在调用中");
A2aAgent agent = A2aAgent.builder()
.name("RouteMakingAgent")
.agentCardResolver(
//创建 Nacos 的 AgentCardResolver
new NacosAgentCardResolver(NacosUtil.getNacosClient()))
.build();
log.info("获取到的远程Agent描述:"+agent.getDescription());
//远程Agent运行
// agent.call().block();
Flux<Event> stream = AgentUtils.streamResponse(agent, "调用百度地图MCP");
stream.doOnNext(msg->System.out.println(msg.getMessage().getTextContent()))
//阻塞直到结束
.blockLast();
}
}11-5 获取百度地图MCP服务端的工具列表
具体实现百度MCP的业务逻辑,通过魔塔社区mcp文档https://modelscope.cn/mcp/servers/@baidu-maps/mcp获取百度地图的mcp获取

@Slf4j
public class BaiduMapMCP {
//MCP 客户端
private McpClientWrapper baiduMapMCP = null;
//MCP 客户端初始化
private boolean mcpInitialized = false;
/**
* description: 创建百度地图MCP客户端
*/
public void getBaiduMapMCP() {
//创建MCP客户端
baiduMapMCP = McpClientBuilder.create("BaiduMap-mcp")
//和MCP Server以SSE方式进行通信
.sseTransport("你的MCP地址")
//请求超时
.timeout(Duration.ofSeconds(120))
//异步请求
.buildAsync()
.block();
}
/**
* description: 初始化百度地图MCP客户端
*/
public McpClientWrapper initBaiduMapMCP() {
//通过Optional判断百度MCP客户端是否为null
Optional<McpClientWrapper> mcpClientWrapper = Optional.ofNullable(baiduMapMCP);
if(mcpClientWrapper.isPresent()) {
log.info("百度MCP客户端已经创建");
if(!mcpInitialized) {
synchronized (this) {
if (!mcpInitialized) {
//MCP客户端初始化
baiduMapMCP.initialize().block();
//获取MCP服务端工具列表
if(baiduMapMCP.isInitialized()) {
log.info("百度地图MCP 客户端初始化成功!");
baiduMapMCP.listTools().block().forEach(tool -> {
log.info("百度地图MCP工具列表:" + tool.name());
});
mcpInitialized=true;
}
}
}
}
}
return baiduMapMCP;
}
}然后在远程的行程智能体的启动类中测试临时调用
public class RouteMakingAgentApplication {
public static void main(String[] args) {
BaiduMapMCP mcp = new BaiduMapMCP();
mcp.getBaiduMapMCP();
mcp.initBaiduMapMCP();
}
}11-6 路线制定专员Agent挂载百度地图MCP
在工具类ToolUtils中添加同mcp获取工具包的方法
public class ToolUtils {
private final Toolkit toolkit ;
public ToolUtils() {
//创建工具包
toolkit = new Toolkit();
}
/**
* description: 获取工具包
*/
public Toolkit getToolkit(McpClientWrapper mcp) {
//把MCP服务端的所有工具添加到工具包
toolkit.registerMcpClient(mcp).block();
return toolkit;
}
}再在路线制定专员Agent上挂载百度地图MCP
@Component
@Slf4j
public class RouteMakingAgent {
@Bean
public ReActAgent getRouteMakingAgent() {
BaiduMapMCP mcp = new BaiduMapMCP();
//创建百度地图MCP客户端
mcp.getBaiduMapMCP();
//初始化百度地图MCP客户端
McpClientWrapper mcpClient = mcp.initBaiduMapMCP();
//Toolkit
ToolUtils toolUtils = new ToolUtils();
Toolkit toolkit = toolUtils.getToolkit(mcpClient);
//打印挂载的工具
Set<String> toolNames = toolkit.getToolNames();
toolNames.stream().forEach(value -> log.info("挂载的工具名称:"+value));
//注入到Nacos
return AgentUtils.getReActAgentBuilder("RouteMakingAgent", "擅长处理自驾游路线制定")
//工具包
.toolkit(toolkit)
.build();
}
}11-7 主管Agent自主分发任务给远程相应的成员

两个远程智能体要对其描述功能:
AgentUtils.getReActAgentBuilder("RouteMakingAgent","擅长处理自驾游路线制定")
AgentUtils.getReActAgentBuilder("TripPlannerAgent", "擅长处理景点行程规划")修改promptm,定义从Nacos注册中心获取远程的Agent
String prompt =
"""
帮我制定2026年元旦,
深圳到惠州3日游自驾游计划,
请包含吃住行,天气,酒店,餐饮美食。
- 从Nacos注册中心获取远程的Agent,
- 把子任务分发给擅长处理相关任务的Agent
- 每个子任务要注明调用的Agent
""";再将工具类Tools中描写修改为从Nacos中获取
@Tool(description = "从Nacos注册中心获取路线制定Agent")
public void callRouteMakingAgent()
@Tool(description = "从Nacos注册中心获取行程规划Agent")
public void callTripPlannerAgent()11-8 人工介入修改主管Agent制定的计划
人工介入的话,需要在自定义 Agent自主分解旅游规划任务中开启
/**
* description: 自定义 Agent自主分解旅游规划任务
*/
public class TripPlan {
/**
* description: 自定义 PlanNotebook 实例
*/
public PlanNotebook getPlan() {
return PlanNotebook.builder()
//计划步骤是否需要用户确认
.needUserConfirm(true)
//分解出来的子任务数量限制
.maxSubtasks(5)
.build();
}
}同时需要一个UserAgent来实现将用户输入的传入agent中,可以在main中测试,通过用户输入看打印
public static void main(String[] args) {
UserAgent user = UserAgent.builder().name("User").build();
Msg userInput = user.call().block();
System.out.println("用户说:"+ userInput.getTextContent());
}在项目中,通过Hook来传入人工介入
@Slf4j
public class PlanHook implements Hook {
//监听用户输入
private final UserAgent user;
//计划步骤
private final PlanNotebook plan;
public PlanHook(PlanNotebook planNotebook) {
this.user = UserAgent.builder().name("User").build();
this.plan = planNotebook;
}
@Override
public <T extends Hook@Slf4j
public class PlanHook implements Hook {
//监听用户输入
private final UserAgent user;
//计划步骤
private final PlanNotebook plan;
public PlanHook(PlanNotebook planNotebook) {
this.user = UserAgent.builder().name("User").build();
this.plan = planNotebook;
}
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {Event> Mono<T> onEvent(T event) {
//匹配不同的事件
if (event instanceof PreReasoningEvent) {
//用户输入事件
PreReasoningEvent e = (PreReasoningEvent) event;
String reason = e.getInputMessages().get(0).getTextContent();
log.info("#### 用户的Prompt:#######");
log.info(reason);
} else if (event instanceof PostReasoningEvent) {
//推理思考事件
PostReasoningEvent e = (PostReasoningEvent) event;
String reason = e.getReasoningMessage().getTextContent();
log.info("#### 思考过程:#######");
log.info(reason);
//当计划列表已生成
Plan currentPlan = plan.getCurrentPlan();
if (currentPlan != null) {
System.out.println("请输入修改意见: ");
user.call().block();
}
} else if (event instanceof PostActingEvent) {
//调用工具事件
PostActingEvent e = (PostActingEvent) event;
String toolName = e.getToolUse().getName();
log.info("##### 调用工具:" + toolName);
} else {
// 其他事件忽略
}
// 返回原事件
return Mono.just(event);
}
}Docker部署nacos 3.x
下载
sudo docker pull nacos/nacos-server:v3.1.0
运行
sudo docker run --rm --name nacos -e MODE=standalone -e NACOS_AUTH_ENABLE=false -e NACOS_AUTH_TOKEN=MjM1ZmU4NjAxMzU1NTQyYWU0MTEyYWU4ZDg3YTZiNGUK -e NACOS_AUTH_IDENTITY_KEY=nacos -e NACOS_AUTH_IDENTITY_VALUE=nacos -p 8848:8848 -p 9848:9848 -p 8088:8080 nacos/nacos-server:v3.1.0
控制台页面:http://127.0.0.1:8088
控制台登录username: nacos
控制台登录password: nacos
说明
8848:HTTP端口
9848:RPC端口
8088:控制台页面端口