手敲重构学透 Multi-Agent 代码(上):逐行拆解 AgentScope 1.0.8,从「跑通了但没懂」到真懂了
上一篇用 Trae 把《多 Agent+Skills+SpringAI 构建自主决策智能体》的代码调通了。代码确实跑起来了,但说实话------跑通和搞懂之间,还隔着一整条街。
代码跑通,只表示功能完成了,并不代表这些逻辑真的进了脑子。AI 帮你把代码调通了,但每一行为什么这么写、Agent 之间怎么协作、工具调用链怎么走的------这些问题不自己动手过一遍,永远是「别人的代码」。
所以这一次,我没有再让 AI 帮我改代码,而是按照之前学习和调通过程中积累的理解,自己从头手敲了一遍。先从 AgentScope 1.0.8 版本开始,逐行拆解、全部搞懂,为后面升级 2.0 打好基础。过程中踩的坑、犯的错、想明白的事,都记在这里了。
本文分为上下两篇,本篇是上篇,聚焦 1.0.8 版本的手动重构。下篇将记录从 1.0.8 升级到 AgentScope 2.0 的完整过程。
AiTripPlan 是一个智能旅游规划系统,核心架构是 Multi-Agent 协作:一个 ManagerAgent(主管)调度 RouteMakingAgent(路线规划专家)、TripPlannerAgent(行程规划专家)和 SuggestSightAgent(景点推荐)等子 Agent,协同完成出行规划。各 Agent 以独立 Spring Boot 微服务的形式部署,通过 Nacos 注册中心实现服务发现,通过 A2A(Agent-to-Agent)协议进行通信。
这套系统基于 AgentScope 框架构建------AgentScope 是阿里开源的 Agent 框架,提供了 ReActAgent(推理-行动循环 Agent)、Toolkit(工具包)、SkillBox(技能盒)、Hook(钩子)等核心组件。1.0.8 是我学习的起始版本。
1.1 项目准备
1.1.1 创建项目
创建 XAiTripPlan-V108 项目,采用 Maven 多模块结构:ai-common(公共模块)、manager-agent(主管)、routeMaking-agent(路线规划)、tripPlanner-agent(行程规划)。

1.1.2 搭建空项目

1.1.3 pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>vip.wayhua.ivy.ai</groupId>
<artifactId>XAiTripPlan-V108</artifactId>
<version>1.0.1</version>
<packaging>pom</packaging>
<modules>
<module>ai-common</module>
<module>manager-agent</module>
<module>routeMaking-agent</module>
<module>tripPlanner-agent</module>
</modules>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring-boot.version>4.0.2</spring-boot.version>
<AgentScope.version>1.0.8</AgentScope.version>
<logback.version>1.5.25</logback.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- 以 SpringBoot 方式添加 A2A 依赖 -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-a2a-spring-boot-starter</artifactId>
<version>${AgentScope.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
前面说过,manager 是总管 Agent,routeMaking 是路线规划专家,tripPlanner 是行程规划专家。各 Agent 之间要通过 Nacos 注册和自动发现。既然所有模块都要引用 ai-common,所以一些关键的公共依赖和工具类就直接放在 ai-common 中,避免重复。
1.2 ai-common
ai-common 是公共模块,集中管理所有 Agent 共用的依赖、配置、工具类和 Hook。这样做的目的是让各 Agent 模块只关注自己的业务逻辑,公共部分不用反复拷贝。
1.2.1 pom.xml
添加所有引用
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>vip.wayhua.ivy.ai</groupId>
<artifactId>XAiTripPlan-V108</artifactId>
<version>1.0.1</version>
</parent>
<artifactId>ai-common</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- AgentScope 和 SpringBoot 的集成 -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-spring-boot-starter</artifactId>
<version>${AgentScope.version}</version>
</dependency>
<!-- 实现slf4j接口,不然日志打印不出来 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 额外添加 Nacos Spring Boot starter 的依赖 -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-nacos-spring-boot-starter</artifactId>
<version>${AgentScope.version}</version>
</dependency>
</dependencies>
</project>
1.2.2 配置信息
在项目根目录创建 .env 文件,用于保存一些核心变量,如 MODEL_NAME(大模型名称)、DASHCOPE_KEY(阿里云 DashScope API 密钥)、DASHSCOPE_OPENAI_BASE_URL(DashScope 兼容 OpenAI 接口的地址)等。这些敏感信息不放代码里,通过环境变量注入。
properties
DASHSCOPE_OPENAI_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
DASHCOPE_KEY=sk-b956cd85b6******
MODEL_NAME=qwen3.7-flash
BAIDU_MAP_ADDR=https://mcp.api-inference.modelscope.net/6ba95***/sse
这是一个学习过程,也是一个整理过程。尽可能把时间花在高效的地方------不光是学完,学完后代码还能直接拿来用,这样才不浪费。能优化的地方尽可能一次性优化好。
在ai-common中创建appliction.yml
yaml
ivy:
agent:
model_name: ${MODEL_NAME}
dashscope_key: ${DASHCOPE_KEY}
dashscope_openai_base_url: ${DASHSCOPE_OPENAI_BASE_URL}
mcp:
baidu_map_addr: ${BAIDU_MAP_ADDR}
这里要说明一下:尽可能将配置文件放在一个根目录下,不然到处都是,找起来比较累,也容易遗忘。据我对 Spring Boot 的了解,在 ai-common 中添加了配置,后面其他项目应该会自动带过来。后面测试过了,发现不可以------但这里还是要放,让别人一眼就知道需要配置哪些项。
Properties代码
java
package vip.wayhua.ivy.ai.common.conf;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 08:58
* @modifiedBy:
* @version: 1.0
*/
@Configuration
public class Properties {
// 大模型名称
@Value("${ivy.agent.model_name}")
private String modelName;
// 阿里云DashScope Key
@Value("${ivy.agent.dashscope_key}")
private String dashscopeKey;
@Value("${ivy.agent.dashscope_openai_base_url}")
private String dashscopeOpenAiBaseUrl;
// 百度地图MCP Key
@Value("${ivy.mcp.baidu_map_addr}")
private String baiduMapAddr;
public String getModelName() {
return modelName;
}
public void setModelName(String modelName) {
this.modelName = modelName;
}
public String getDashscopeKey() {
return dashscopeKey;
}
public void setDashscopeKey(String dashscopeKey) {
this.dashscopeKey = dashscopeKey;
}
public String getDashscopeOpenAiBaseUrl() {
return dashscopeOpenAiBaseUrl;
}
public void setDashscopeOpenAiBaseUrl(String dashscopeOpenAiBaseUrl) {
this.dashscopeOpenAiBaseUrl = dashscopeOpenAiBaseUrl;
}
public String getBaiduMapAddr() {
return baiduMapAddr;
}
public void setBaiduMapAddr(String baiduMapAddr) {
this.baiduMapAddr = baiduMapAddr;
}
}
这里有一点要说明:这些配置项是和实际项目相关的,如果要添加是可以的,目前没有做成通用的。这一点后续可以考虑优化。
1.2.3 日志 Hook
这是我让 Trae 添加上来的功能。AgentScope 的 Hook 机制允许你在 Agent 生命周期的事件节点上插入自定义逻辑------类似 Spring 的 Interceptor。主要是我当时调试时,不知道到底有没有调用子 Agent,要添加上日志,这样我就知道有没有具体调用。
这里实现了三个关键事件的监听:Agent 收到请求开始推理(PreReasoningEvent)、推理完成(PostReasoningEvent)、工具调用完成(PostActingEvent)。
java
package vip.wayhua.ivy.ai.common.hook;
import io.agentscope.core.hook.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
/**
* Agent 调用日志 Hook
* <p>
* 监听 Agent 的推理和工具调用事件,在 Agent 被调用时输出明确的日志,
* 用于确认 Agent 确实被调用过。
* <p>
* 事件说明:
* - PreReasoningEvent: Agent 收到请求,开始推理(证明 Agent 被调用)
* - PostReasoningEvent: Agent 推理完成
* - PostActingEvent: Agent 工具调用完成
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 09:18
* @modifiedBy:
* @version: 1.0
*/
public class AgentCallLogHook implements Hook {
private static final Logger log = LoggerFactory.getLogger(AgentCallLogHook.class);
private final String agentName;
public AgentCallLogHook(String agentName) {
this.agentName = agentName;
}
@Override
public <T extends HookEvent> Mono<T> onEvent(T event) {
if (event instanceof PreReasoningEvent e) {
// Agent 收到请求,开始推理 ------ 这证明 Agent 确实被调用了
String input = "";
if (e.getInputMessages() != null && !e.getInputMessages().isEmpty()) {
input = e.getInputMessages().get(0).getTextContent();
if (input != null && input.length() > 300) {
input = input.substring(0, 300) + "...";
}
}
log.info("======================================================");
log.info(">>> [{}] Agent 被调用! 开始处理请求", agentName);
log.info(">>> [{}] 输入内容: {}", agentName, input);
log.info("======================================================");
} else if (event instanceof PostReasoningEvent e) {
// Agent 推理完成
String reasoning = e.getReasoningMessage() != null
? e.getReasoningMessage().getTextContent()
: "";
int length = reasoning != null ? reasoning.length() : 0;
log.info(">>> [{}] 推理完成 (思考内容长度: {} 字符)", agentName, length);
} else if (event instanceof PostActingEvent e) {
// Agent 工具调用完成
String toolName = e.getToolUse() != null ? e.getToolUse().getName() : "unknown";
log.info(">>> [{}] 工具调用完成: {}", agentName, toolName);
}
// 返回原事件,不修改
return Mono.just(event);
}
}
1.2.4 AgentUtils
AgentUtils 是一个工具类,封装了 ReActAgent 的构建过程。ReActAgent 是 AgentScope 的核心 Agent 类型,采用 ReAct(Reasoning + Acting)模式:先推理决定下一步做什么,再执行行动(调用工具),然后根据结果继续推理,循环往复直到完成任务。这里统一配置大模型、日志 Hook,各 Agent 模块调用时只需传入名称和描述即可。
java
package vip.wayhua.ivy.ai.common.utils;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.model.DashScopeChatModel;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import vip.wayhua.ivy.ai.common.conf.Properties;
import vip.wayhua.ivy.ai.common.hook.AgentCallLogHook;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 09:24
* @modifiedBy:
* @version: 1.0
*/
@Component
public class AgentUtils {
private static final Logger log = LoggerFactory.getLogger(AgentUtils.class);
@Resource
Properties properties;
public ReActAgent.Builder getReActAgentBuilder(String name, String description) {
String host = properties.getDashscopeOpenAiBaseUrl();
log.error("==========================");
log.error("openAiBaseUrl:" + host);
log.error("==========================");
String aliApiKey = properties.getDashscopeKey();
log.error("==========================");
log.error("apiKey:" + aliApiKey);
log.error("==========================");
String modelName = properties.getModelName();
log.error("==========================");
log.error("主管Agent使用的大模型:" + modelName);
log.error("==========================");
return ReActAgent.builder()
.name(name)
.description(description)
.model(DashScopeChatModel.builder()
//请求语言大模型的apikey
.apiKey(aliApiKey)
//所使用的语言大模型
.modelName(modelName)
.stream(true)
//开启思考模式
.enableThinking(true)
.build())
//直接在这添加全局日志
.hook(new AgentCallLogHook(name))
;
}
}
暂时在这里没有使用到 baseUrl,等调试的时候再确认一下。因为这是重新学习、重新整理,有些基础的东西就没有详细说明了。
1.2.5 PromptUtils
PromptUtils 封装了用户消息(User Message)的构建。AgentScope 中消息以 Msg 对象表示,包含角色(Role)和内容块(Content Block)。这里把文本提示词包装成标准的 USER 角色消息,方便各处统一调用。
java
package vip.wayhua.ivy.ai.common.utils;
import io.agentscope.core.message.Msg;
import io.agentscope.core.message.MsgRole;
import io.agentscope.core.message.TextBlock;
import java.util.List;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 09:33
* @modifiedBy:
* @version: 1.0
*/
public class PromptUtils {
public static Msg getUserMsg(String prompt) {
Msg userPrompt = Msg.builder()
.role(MsgRole.USER)
.content(List.of(
TextBlock.builder()
.text(prompt)
.build()
))
.build();
return userPrompt;
}
}
1.2.6 ToolUtils
ToolUtils 封装了 Toolkit(工具包)的构建。AgentScope 中 Agent 通过 Toolkit 调用外部工具------既可以是标注了 @Tool 注解的普通 Java 方法,也可以是 MCP(Model Context Protocol)服务端提供的远程工具。这里提供了两个重载方法:一个注册本地工具,一个注册 MCP 客户端工具。
java
package vip.wayhua.ivy.ai.common.utils;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.mcp.McpClientWrapper;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 09:34
* @modifiedBy:
* @version: 1.0
*/
public class ToolUtils {
private final Toolkit toolkit;
public ToolUtils() {
//创建工具包
toolkit = new Toolkit();
}
public Toolkit getToolkit(Object Tool) {
//把工具添加到工具包,能自动扫描@Tool所注释的方法,作为Agent的工具
toolkit.registerTool(Tool);
return toolkit;
}
//获取工具包
public Toolkit getToolkit(McpClientWrapper mcp) {
//把MCP服务端的所有工具添加到工具包
toolkit.registerMcpClient(mcp).block();
return toolkit;
}
}
这种尝试重写代码的原则是:尽可能少地添加代码,除非完全理解。不理解的可以先不添加------这样的好处是,后面编译肯定通不过,再查看为什么通不过、缺什么,然后回头添加上,反而更有助于理解。
1.2.7 StringUtils
这是我的一点小趣味------用 ASCII 佛像图案提示所有加载完成。本来 StringUtils 中有很多内容,这里只迁移最少的部分。
java
package vip.wayhua.ivy.ai.common.utils;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 09:41
* @modifiedBy:
* @version: 1.0
*/
public class StringUtils {
/**
* org.springframework.util.StringUtil.isEmpty() 被抛弃了
* 替代函数为org.springframework.util.StringUtils.hasText(),
* 但只能传入String类型,不支持Object,不方便,所以编写此函数。
*
* @param str
* @return
*/
public static boolean isEmpty(Object str) {
if (str == null)
return true;
boolean hasTexted = org.springframework.util.StringUtils.hasText(str.toString());
return !hasTexted;
}
public static String bannerX(String appName, String startConfig) {
String success = """
*************************************************************
_
_ooOoo_
o8888888o
88" . "88
(| -_- |)
O\\ = /O
____/`---'\\____
.' \\\\| |// `.
/ \\\\||| : |||// \\
/ _||||| -:- |||||_ \\
| | \\\\\\ - /'| | |
| \\_| `\\`---'// |_/ |
\\ .-\\__ `-. -'__/-. /
___`. .' /--.--\\ `. .'___
."" '< `.___\\_<|>_/___.' _> \\"".
| | : `- \\`. ;`. _/; .'/ / .' ; | Buddha
\\ \\ `-. \\_\\_`. _.'_/_/ -' _.' /
================-.`___`-.__\\ \\___ /__.-'_.'_.-'================
`=--=-' ivy-core
\t%s启动成功!!
\t%s
*************************************************************
""";
String result = String.format(success, appName, startConfig);
return result;
}
public static String banner(String appName, String port) {
return banner(appName, port, false);
}
public static String banner(String appName, String port, boolean hasBff) {
// 地址: http://127.0.0.1:%s
// %s
if (StringUtils.isEmpty(port)) {
port = "8080";
}
String sconfig = "地址: http://127.0.0.1:" + port + "\n";
String bff = "\tDoc地址:\thttp://127.0.0.1:%s/doc.html";
String bffFill = "";
if (hasBff) {
bffFill = String.format(bff, port);
sconfig += bffFill;
}
String result = bannerX(appName, sconfig);
return result;
}
}
会在每个 Spring Boot 启动时用一下,当出现这个图案时表示所有加载完成,如果有 doc.html 也可以直接访问。此次项目不添加 Swagger。
1.3 manager-agent
manager-agent 是整个系统的总调度模块。ManagerAgent 不直接干活,而是通过 A2A 协议远程调用其他子 Agent。它还有任务分解能力------通过 PlanNotebook 把复杂任务拆成子步骤,逐步执行。
先将所有 Nacos 中的配置删除,为了避免重建 Nacos,如果有多台机器可以保留,这样有个参照。删除了后面有问题就缺少参照了。
所有项目都要引用 ai-common,这个后面就不再说明。前面说过在 ai-common 中创建了 application.yml,是不能直接被后面的项目引用的------只是做个标识,告诉别人后面要用到哪些配置。
1.3.1 启动项
java
package vip.wayhua.ivy.ai.manager.agent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import vip.wayhua.ivy.ai.common.utils.StringUtils;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 09:47
* @modifiedBy:
* @version: 1.0
*/
@SpringBootApplication(scanBasePackages = "vip.wayhua.ivy.ai")
public class MgrAgentApplication {
private static final Logger log= LoggerFactory.getLogger(MgrAgentApplication.class);
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MgrAgentApplication.class, args);
ConfigurableEnvironment env = run.getEnvironment();
String appName = "Manager-agent Spring boot服务";
String port = env.getProperty("server.port");
String slog = StringUtils.banner(appName, port);
log.error(slog);
}
}
同时将配置迁移过来。
yaml
server:
port: 8888
ivy:
agent:
model_name: ${MODEL_NAME}
dashscope_key: ${DASHCOPE_KEY}
dashscope_openai_base_url: ${DASHSCOPE_OPENAI_BASE_URL}
mcp:
baidu_map_addr: ${BAIDU_MAP_ADDR}
1.3.2 运行报错

说明 .env 的变量没有引用进来。
要在application.yml中添加上
yaml
spring:
config:
import: optional:file:.env[.properties]
同样在 ai-common 中的 application.yml 也添加上,并编写说明,这样以后使用就不会忘记(这个非常重要)。
yaml
ivy:
agent:
model_name: ${MODEL_NAME}
dashscope_key: ${DASHCOPE_KEY}
dashscope_openai_base_url: ${DASHSCOPE_OPENAI_BASE_URL}
mcp:
baidu_map_addr: ${BAIDU_MAP_ADDR}
# 要使用.env 就必须添加上
spring:
config:
import: optional:file:.env[.properties]
1.3.3 再次运行

这样既可以看到启动端口(还有 doc.html,因为此项目没使用就关闭了),又可以确定启动确实完成了。
1.3.4 Controller
到此准备工作完成,开始编写 Agent。
为了方便测试,直接通过 Controller 的接口测试。为了方便还是将 Swagger 添加上比较好------Knife4j 肯定不行(不适配 SpringBoot 4),只能用 NextDoc4j。添加上来,就不修改前面内容了。
java
package vip.wayhua.ivy.ai.manager.agent.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import vip.wayhua.ivy.ai.common.dto.PromptSchema;
import vip.wayhua.ivy.ai.common.dto.ResponseSchema;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 10:23
* @modifiedBy:
* @version: 1.0
*/
@RestController
@CrossOrigin
@Tag(name = "Manager Agent Controller")
public class ManagerAgentController {
private static final Logger log = LoggerFactory.getLogger(ManagerAgentController.class);
@Operation(summary = "旅行")
@RequestMapping(value = "/trip",
produces = "application/json;charset=UTF-8",
method = RequestMethod.POST)
public ResponseSchema tripPlan(@RequestBody PromptSchema input) {
log.info("输入--->" + input.getPrompt());
ResponseSchema responseSchema = new ResponseSchema();
responseSchema.response =input.getPrompt()+ "成功";
return responseSchema;
// return managerAgent.run(input.getPrompt());
}
}
先编写简单内容,测试一下接口文档是否正常。

这时就可以直接访问http://127.0.0.1:8888/doc.html

后台日志

前期测试就可以通过这个接口进行。
1.3.5 TripPlan
TripPlan 封装了 PlanNotebook 的配置。PlanNotebook 是 AgentScope 1.x 的任务分解组件------ManagerAgent 收到用户请求后,会先通过 create_plan 工具把任务拆解成多个子任务,再逐个执行。这里配置了是否需要用户确认(needUserConfirm)和子任务数量上限(maxSubtasks)。
java
package vip.wayhua.ivy.ai.manager.agent.plan;
import io.agentscope.core.plan.PlanNotebook;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 10:36
* @modifiedBy:
* @version: 1.0
*/
public class TripPlan {
public PlanNotebook getPlan() {
return PlanNotebook.builder()
//计划步骤是否需要用户确认
.needUserConfirm(false)
//分解出来的子任务数量限制
.maxSubtasks(5)
//计划的存储方式
//.storage()
.build();
}
}
1.3.6 NacosUtils
根据官方文档,要配置 PropertyKeyConst.SERVER_ADDR,并返回 AiService。Nacos 在这个系统中承担服务注册与发现的角色------每个子 Agent 启动时把自己的 AgentCard(包含名称、描述、服务地址等信息)注册到 Nacos,ManagerAgent 通过 Nacos 查找子 Agent 的地址再发起调用。这就是 A2A 协议的发现机制。
java
// 设置 Nacos 地址
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, "localhost:8848");
// 创建 Nacos Client
AiService aiService = AiFactory.createAiService(properties);
// 创建 Nacos 的 AgentCardResolver
NacosAgentCardResolver nacosAgentCardResolver = new NacosAgentCardResolver(aiService);
// 创建 A2A Agent
A2aAgent agent = A2aAgent.builder()
.name("remote-agent")
.agentCardResolver(nacosAgentCardResolver)
.build();
NacosUtils 就是做这事的,主要是读取 server-addr 配置并创建 AiService 实例。
java
package vip.wayhua.ivy.ai.common.utils;
import com.alibaba.nacos.api.PropertyKeyConst;
import com.alibaba.nacos.api.ai.AiFactory;
import com.alibaba.nacos.api.ai.AiService;
import com.alibaba.nacos.api.exception.NacosException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Properties;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 10:39
* @modifiedBy:
* @version: 1.0
*/
@Component
public class NacosUtils {
@Value("agentscope.a2a.nacos.server-addr")
String server_addr;
public AiService getNacosClient() throws NacosException {
// 设置 Nacos 地址
Properties properties = new Properties();
properties.put(PropertyKeyConst.SERVER_ADDR, server_addr);
// 创建 Nacos Client
return AiFactory.createAiService(properties);
}
}
1.3.7 RemoteAgentTool
RemoteAgentTool 是 ManagerAgent 的核心工具类。它把远程子 Agent(RouteMakingAgent、TripPlannerAgent)封装成 @Tool 注解的方法,注册到 ManagerAgent 的工具包中。这样 ManagerAgent 在推理过程中,就能像调用普通工具一样,通过 A2A 协议远程调用子 Agent。每个工具方法都有详细的 description,这是告诉大模型什么时候该用这个工具。
java
package vip.wayhua.ivy.ai.manager.agent.tool;
import com.alibaba.nacos.api.exception.NacosException;
import io.agentscope.core.a2a.agent.A2aAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.nacos.a2a.discovery.NacosAgentCardResolver;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import vip.wayhua.ivy.ai.common.utils.AgentUtils;
import vip.wayhua.ivy.ai.common.utils.NacosUtils;
import vip.wayhua.ivy.ai.common.utils.PromptUtils;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 10:37
* @modifiedBy:
* @version: 1.0
*/
public class RemoteAgentTool {
private static final Logger log = LoggerFactory.getLogger(RemoteAgentTool.class);
@Resource
AgentUtils agentUtils;
@Resource
NacosUtils nacosUtils;
// @Tool(description = "从Nacos注册中心获取路线制定Agent")
@Tool(description = "路线规划专家,提供:驾车/铁路/飞机路线、距离、耗时、交通方式对比。必须调用此工具获取任何交通路线信息,严禁自行编造车次和航班或路线")
public String callRouteMakingAgent(
@ToolParam(name = "prompt", description = "路线查询需求,包含起点、终点、出行方式(铁路/自驾/飞机等)")
String prompt) throws NacosException {
if (prompt == null || prompt.isBlank()) {
log.warn("callRouteMakingAgent 收到空 prompt,跳过调用");
return "无法制定路线:未收到有效的起终点信息";
}
log.error("==========================");
log.error("callRouteMakingAgent->call");
log.error("工具方法:路线制定智能体...正在调用中");
log.error("==========================");
A2aAgent agent = A2aAgent.builder()
.name("RouteMakingAgent")
.agentCardResolver(
//创建 Nacos 的 AgentCardResolver
new NacosAgentCardResolver(nacosUtils.getNacosClient()))
.build();
log.error("==========================");
log.error("获取到的远程Agent描述:" + agent.getDescription());
log.error("==========================");
log.error("============");
log.error("这个工具方法传入的参数:" + prompt);
log.error("============");
// Flux<Event> stream = agentUtils.streamResponse(agent, "调用百度地图MCP");
//
// stream
// .doOnNext(msg -> System.out.println(msg.getMessage().getTextContent()))
// //阻塞直到结束
// .blockLast();
//远程Agent运行
// agent.call().block();
Msg userMsg = PromptUtils.getUserMsg(prompt);
//远程Agent运行
Msg remoteAgentResponse = agent.call(userMsg).block();
return remoteAgentResponse.getTextContent();
}
@Tool(description = "行程规划专家,提供:每日景点安排、美食推荐、住宿建议、天气参考。获取路线后必须调用此工具完成行程细节规划")
public String callTripPlannerAgent(
@ToolParam(name = "prompt", description = "行程规划需求,包含目的地、天数、偏好、路线信息等")
String prompt) throws NacosException {
log.info("============");
log.info("工具方法:行程规划智能体...正在调用中");
log.info("============");
if (prompt == null || prompt.isBlank()) {
log.warn("callTripPlannerAgent 收到空 prompt,跳过调用");
return "无法规划行程:未收到有效的行程需求信息";
}
A2aAgent agent = A2aAgent.builder()
.name("TripPlannerAgent")
.agentCardResolver(
//创建 Nacos 的 AgentCardResolver
new NacosAgentCardResolver(nacosUtils.getNacosClient()))
.build();
log.error("==========================");
log.error("获取到的远程Agent描述:" + agent.getDescription());
log.error("==========================");
//远程Agent运行
// agent.call().block();
log.info("============");
log.info("这个工具方法传入的参数:" + prompt);
log.info("============");
Msg userMsg = PromptUtils.getUserMsg(prompt);
//远程Agent运行
Msg remoteAgentResponse = agent.call(userMsg).block();
return remoteAgentResponse.getTextContent();
}
}
1.3.8 ManagerAgent
ManagerAgent 是整个系统的中枢。它在构造函数中完成所有初始化:构建远程 Agent 的 A2A 代理、创建工具包、配置任务分解(PlanNotebook)、挂载日志 Hook,最后构建 ReActAgent 实例。
java
package vip.wayhua.ivy.ai.manager.agent.agent;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.model.StructuredOutputReminder;
import io.agentscope.core.plan.PlanNotebook;
import io.agentscope.core.tool.Toolkit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import vip.wayhua.ivy.ai.common.utils.AgentUtils;
import vip.wayhua.ivy.ai.common.utils.ToolUtils;
import vip.wayhua.ivy.ai.manager.agent.hook.PlanHook;
import vip.wayhua.ivy.ai.manager.agent.plan.TripPlan;
import vip.wayhua.ivy.ai.manager.agent.tool.RemoteAgentTool;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 10:34
* @modifiedBy:
* @version: 1.0
*/
@Component
public class ManagerAgent {
private static final Logger log = LoggerFactory.getLogger(ManagerAgent.class);
private final ObjectMapper objectMapper = new ObjectMapper();
private final ReActAgent agent;
AgentUtils agentUtils;
public ReActAgent getManagerAgent() {
return this.agent;
}
public ManagerAgent(AgentUtils agentUtils) {
this.agentUtils = agentUtils;
TripPlan plan = new TripPlan();
//Toolkit
ToolUtils toolUtils = new ToolUtils();
//将远程Agent封装为工具的封装注册到工具包
Toolkit toolkit = toolUtils.getToolkit(new RemoteAgentTool());
PlanNotebook planNotebook = plan.getPlan();
agent = agentUtils.getReActAgentBuilder(
"ManagerAgent",
"主管Agent"
)
.planNotebook(planNotebook)
.hook(new PlanHook(planNotebook))
//工具包
.toolkit(toolkit)
//结构化输出
.structuredOutputReminder(StructuredOutputReminder.PROMPT)
.build();
}
public ResponseSchema run(String prompt) {
// 发送查询,指定输出类型
Msg response = agent
.call(PromptUtils.getUserMsg(prompt),
ResponseSchema.class)
.block();
// 提取类型化数据
ResponseSchema data = response.getStructuredData(ResponseSchema.class);
return data;
}
}
1.3.9 测试
在controller修改,
java
@Resource
ManagerAgent managerAgent;
@Operation(summary = "旅行")
@RequestMapping(value = "/trip",
produces = "application/json;charset=UTF-8",
method = RequestMethod.POST)
public ResponseSchema tripPlan(@RequestBody PromptSchema input) {
log.info("输入--->" + input.getPrompt());
ResponseSchema responseSchema = managerAgent.run(input.getPrompt());
return responseSchema;
}
会报错的。
1.4 RouteMakingAgent
RouteMakingAgent 是路线规划专家,它通过 MCP(Model Context Protocol)协议连接百度地图服务,获取实时的驾车、铁路、公交等路线信息。MCP 是一种标准化的工具协议,Agent 通过 MCP 客户端调用 MCP 服务端提供的工具。
1.4.1 RouteMakingAgentApplication
java
package vip.wayhua.ivy.ai.route;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import vip.wayhua.ivy.ai.common.utils.StringUtils;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 11:16
* @modifiedBy:
* @version: 1.0
*/
@SpringBootApplication(scanBasePackages = "vip.wayhua.ivy.ai")
public class RouteMakingAgentApplication {
private static final Logger log = LoggerFactory.getLogger(RouteMakingAgentApplication.class);
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(RouteMakingAgentApplication.class, args);
ConfigurableEnvironment env = run.getEnvironment();
String appName = "路线规划专家 Spring boot服务";
String port = env.getProperty("server.port");
String slog = StringUtils.banner(appName, port, false);
log.error(slog);
}
}

1.4.2 BaiduMapMcp
BaiduMapMcp 封装了百度地图 MCP 客户端的创建和初始化。通过 SSE(Server-Sent Events)方式与 MCP Server 通信,支持路线查询、地理编码、POI 搜索等功能。这里用了双重检查锁定(Double-Checked Locking)来保证 MCP 客户端只初始化一次。
java
package vip.wayhua.ivy.ai.route.mcp;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.mcp.McpClientBuilder;
import io.agentscope.core.tool.mcp.McpClientWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import vip.wayhua.ivy.ai.common.conf.Properties;
import java.time.Duration;
import java.util.Optional;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 11:20
* @modifiedBy:
* @version: 1.0
*/
public class BaiduMapMcp {
private static final Logger log = LoggerFactory.getLogger(BaiduMapMcp.class);
//MCP 客户端
private McpClientWrapper baiduMapMCP = null;
//MCP 客户端初始化
private boolean mcpInitialized = false;
private Properties properties;
public BaiduMapMcp(Properties properties) {
this.properties = properties;
}
@Tool(description = "百度地图MCP Server")
public void getBaiduMapMCP() {
log.error("==========================");
log.error("路线制定Agent调用了百度地图MCP Server");
log.error("==========================");
//创建MCP客户端
baiduMapMCP = McpClientBuilder.create("BaiduMap-mcp")
//和MCP Server以SSE方式进行通信
.sseTransport(properties.getBaiduMapAddr())
//请求超时
.timeout(Duration.ofSeconds(120))
//异步请求
.buildAsync()
.block();
}
public McpClientWrapper initBaiduMapMCP() {
//通过Optional判断百度MCP客户端是否为null
Optional<McpClientWrapper> mcpClientWrapper = Optional.ofNullable(baiduMapMCP);
if (mcpClientWrapper.isPresent()) {
log.info("==================");
log.info("百度MCP客户端已经创建");
log.info("==================");
if (!mcpInitialized) {
synchronized (this) {
if (!mcpInitialized) {
//MCP客户端初始化
baiduMapMCP.initialize().block();
//获取MCP服务端工具列表
if (baiduMapMCP.isInitialized()) {
log.info("=============");
log.info("百度地图MCP 客户端初始化成功!");
log.info("=============");
baiduMapMCP.listTools().block().forEach(tool -> {
log.info("==================");
log.info("百度地图MCP工具列表:" + tool.name());
log.info("==================");
});
mcpInitialized = true;
}
}
}
}
}
return baiduMapMCP;
}
}
1.4.3 agent
java
package vip.wayhua.ivy.ai.route.agent;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.tool.Toolkit;
import io.agentscope.core.tool.mcp.McpClientWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import vip.wayhua.ivy.ai.common.conf.Properties;
import vip.wayhua.ivy.ai.common.utils.AgentUtils;
import vip.wayhua.ivy.ai.common.utils.ToolUtils;
import vip.wayhua.ivy.ai.route.mcp.BaiduMapMcp;
import java.util.Set;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 11:24
* @modifiedBy:
* @version: 1.0
*/
@Component
public class RouteMakingAgent {
private static final Logger log = LoggerFactory.getLogger(RouteMakingAgent.class);
AgentUtils agentUtils;
@Bean
public ReActAgent getRouteMakingAgent(AgentUtils agentUtils, Properties properties) {
this.agentUtils = agentUtils;
BaiduMapMcp mcp = new BaiduMapMcp(properties);
mcp.initBaiduMapMCP();
//创建百度地图MCP客户端
mcp.getBaiduMapMCP();
//初始化百度地图MCP客户端
McpClientWrapper mcpClient = mcp.initBaiduMapMCP();
ToolUtils toolUtils = new ToolUtils();
//将远程Agent封装为工具的封装注册到工具包
Toolkit toolkit = toolUtils.getToolkit(mcpClient);
//打印挂载的工具
Set<String> toolNames = toolkit.getToolNames();
log.info("=============");
toolNames.stream().forEach(
value -> log.info("挂载的工具名称:" + value)
);
log.info("=============");
log.info(">>> [RouteMakingAgent] Agent 初始化完成,已挂载 {} 个百度地图MCP工具", toolNames.size());
log.info(">>> [RouteMakingAgent] 已添加调用日志 Hook,被调用时将输出日志");
return agentUtils.getReActAgentBuilder(
"RouteMakingAgent",
"路线规划专家,可使用百度地图工具查询:驾车/铁路/公交/步行/骑行路线、距离耗时、地理编码、POI搜索、天气、IP定位。你有实时地图数据,必须提供具体的路线信息"
)
.toolkit(toolkit)
.build();
}
}
要添加
xml
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-a2a-spring-boot-starter</artifactId>
</dependency>

1.5 TripPlanAgent
TripPlannerAgent 是行程规划专家。它内部还包含一个子 Agent------SuggestSightAgent(景点推荐),通过 toolkit.registration().subAgent() 把子 Agent 注册为工具,实现 Agent 的嵌套调用。此外,TripPlannerAgent 还使用了 AgentScope 的 Skills 机制,通过 SkillBox 加载预定义的技能文件(Skill.md),让 Agent 具备特定领域的能力。
1.5.1 启动
java
package vip.wayhua.ivy.ai.trip;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;
import vip.wayhua.ivy.ai.common.utils.StringUtils;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 11:32
* @modifiedBy:
* @version: 1.0
*/
@SpringBootApplication(scanBasePackages = "vip.wayhua.ivy.ai")
public class TripPlanAgentApplication {
private static final Logger log= LoggerFactory.getLogger(TripPlanAgentApplication.class);
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(TripPlanAgentApplication.class, args);
ConfigurableEnvironment env = run.getEnvironment();
String appName = "行程规划专家 Spring boot服务";
String port = env.getProperty("server.port");
String slog = StringUtils.banner(appName, port,false);
log.error(slog);
}
}
1.5.2 SuggestSightAgent
java
package vip.wayhua.ivy.ai.trip.agent;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.skill.AgentSkill;
import io.agentscope.core.skill.SkillBox;
import io.agentscope.core.skill.util.JarSkillRepositoryAdapter;
import io.agentscope.core.tool.Toolkit;
import vip.wayhua.ivy.ai.common.utils.AgentUtils;
import java.io.IOException;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 11:34
* @modifiedBy:
* @version: 1.0
*/
public class SuggestSightAgent {
AgentUtils agentUtils;
public SuggestSightAgent(AgentUtils agentUtils){
this.agentUtils=agentUtils;
}
//创建景点推荐Agent
public ReActAgent getSuggestSightAgent( ) {
Toolkit toolkit = new Toolkit();
//构建Skill,并将工具包和Skill结合
SkillBox skillBox = new SkillBox(toolkit);
//以文件形式读取Skill.md
JarSkillRepositoryAdapter repo = null;
try {
repo = new JarSkillRepositoryAdapter("skills");
} catch (IOException e) {
throw new RuntimeException(e);
}
//景点推荐技能
AgentSkill SuggestSightsSkill = repo.getSkill("Suggest-Sights");
skillBox.registerSkill(SuggestSightsSkill);
// //注册工具
// skillBox.registration().tool(new Calculate());
ReActAgent.Builder builder = agentUtils.getReActAgentBuilder
("SuggestSightAgent",
"擅长景点推荐"
)
//挂载工具包
.toolkit(toolkit)
//挂载Skills
.skillBox(skillBox);
return builder.build();
}
}
1.5.3 TripPlannerAgent
java
package vip.wayhua.ivy.ai.trip.agent;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.tool.Toolkit;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import vip.wayhua.ivy.ai.common.utils.AgentUtils;
import java.io.IOException;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 11:35
* @modifiedBy:
* @version: 1.0
*/
@Component
public class TripPlannerAgent {
AgentUtils agentUtils;
@Bean
public ReActAgent getTripPlannerAgent(AgentUtils agentUtils) throws IOException {
this.agentUtils = agentUtils;
Toolkit toolkit = new Toolkit();
SuggestSightAgent SuggestSightAgent = new SuggestSightAgent(agentUtils);
//将智能体(子Agent)作为工具
toolkit.registration().subAgent(
() -> SuggestSightAgent.getSuggestSightAgent()
).apply();
//行程规划Agent Builder
ReActAgent.Builder builder = agentUtils.getReActAgentBuilder(
"TripPlannerAgent",
"你是行程规划专家。规划前必须先用 weather_check 脚本查目的地天气,所有数据整理后用 recalc 脚本生成表格。严禁编造天气和景点信息。"
)
.toolkit(toolkit);
return builder.build();
}
}
1.6 测试
启动所有微服务后,通过接口发送「合肥去岳西3日游规划」的请求。下面是完整的运行日志,可以从中看到 ManagerAgent 的完整推理和工具调用过程:
ini
[13:36:36.824] INFO o.a.c.c.C.[.[.[/] - Initializing Spring DispatcherServlet 'dispatcherServlet'
[13:36:36.825] INFO o.s.w.s.DispatcherServlet - Initializing Servlet 'dispatcherServlet'
[13:36:36.826] INFO o.s.w.s.DispatcherServlet - Completed initialization in 1 ms
[13:36:36.961] INFO v.w.i.a.m.a.c.ManagerAgentController - 输入--->合肥去岳西3日游规划
[13:36:37.077] INFO i.a.c.t.Toolkit - Registered tool 'generate_response' in group 'ungrouped'
[13:36:37.088] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:37.088] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:37.088] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:37.089] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:37.089] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:37.089] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:39.827] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:39.827] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:39.827] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:39.930] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: create_plan
[13:36:39.930] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:create_plan
[13:36:39.930] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:39.930] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:39.930] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:39.931] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:39.931] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:39.931] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:41.531] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:41.531] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:41.532] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:41.549] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:41.549] ERROR v.w.i.a.m.a.t.RemoteAgentTool - callRouteMakingAgent->call
[13:36:41.549] ERROR v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:路线制定智能体...正在调用中
[13:36:41.549] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:41.572] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: update_subtask_state
[13:36:41.573] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:update_subtask_state
[13:36:41.573] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callRouteMakingAgent
[13:36:41.574] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callRouteMakingAgent
[13:36:41.574] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:41.574] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:41.575] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:41.575] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:41.575] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:41.575] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:42.506] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:42.507] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:42.507] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:42.511] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: get_subtask_count
[13:36:42.511] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:get_subtask_count
[13:36:42.512] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: view_subtasks
[13:36:42.512] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:view_subtasks
[13:36:42.512] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:42.513] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:42.513] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:42.513] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:42.513] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:42.513] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:43.477] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:43.478] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:43.478] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:43.483] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:43.484] ERROR v.w.i.a.m.a.t.RemoteAgentTool - callRouteMakingAgent->call
[13:36:43.484] ERROR v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:路线制定智能体...正在调用中
[13:36:43.484] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:43.486] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callRouteMakingAgent
[13:36:43.486] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callRouteMakingAgent
[13:36:43.487] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:43.488] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:43.488] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:43.488] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:43.489] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:43.489] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:44.758] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:44.759] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:44.759] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:44.761] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:44.761] ERROR v.w.i.a.m.a.t.RemoteAgentTool - callRouteMakingAgent->call
[13:36:44.761] ERROR v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:路线制定智能体...正在调用中
[13:36:44.761] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:44.762] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callRouteMakingAgent
[13:36:44.762] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callRouteMakingAgent
[13:36:44.763] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:44.763] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:44.763] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:44.763] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:44.764] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:44.764] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:46.348] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:46.349] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:46.349] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:46.354] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:46.354] ERROR v.w.i.a.m.a.t.RemoteAgentTool - callRouteMakingAgent->call
[13:36:46.355] ERROR v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:路线制定智能体...正在调用中
[13:36:46.355] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:46.356] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callRouteMakingAgent
[13:36:46.356] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callRouteMakingAgent
[13:36:46.357] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:46.357] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:46.357] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:46.358] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:46.358] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:46.358] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:48.600] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 38 字符)
[13:36:48.600] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:48.600] INFO v.w.i.a.m.a.h.PlanHook - 路线规划工具遇到了技术问题,让我尝试直接调用行程规划专家来完成整体规划:
[13:36:48.602] INFO v.w.i.a.m.a.t.RemoteAgentTool - ============
[13:36:48.602] INFO v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:行程规划智能体...正在调用中
[13:36:48.602] INFO v.w.i.a.m.a.t.RemoteAgentTool - ============
[13:36:48.602] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callTripPlannerAgent
[13:36:48.602] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callTripPlannerAgent
[13:36:48.603] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:48.603] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:48.603] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:48.603] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:48.603] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:48.603] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:49.456] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:49.456] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:49.456] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:49.458] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:49.458] ERROR v.w.i.a.m.a.t.RemoteAgentTool - callRouteMakingAgent->call
[13:36:49.458] ERROR v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:路线制定智能体...正在调用中
[13:36:49.458] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:49.458] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callRouteMakingAgent
[13:36:49.459] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callRouteMakingAgent
[13:36:49.459] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:49.459] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:49.459] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:49.459] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:49.459] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:49.459] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:50.446] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:50.446] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:50.446] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:50.448] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:50.448] ERROR v.w.i.a.m.a.t.RemoteAgentTool - callRouteMakingAgent->call
[13:36:50.448] ERROR v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:路线制定智能体...正在调用中
[13:36:50.448] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:50.452] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callRouteMakingAgent
[13:36:50.452] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callRouteMakingAgent
[13:36:50.452] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:50.453] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] Agent 被调用! 开始处理请求
[13:36:50.453] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 输入内容: 合肥去岳西3日游规划
[13:36:50.453] INFO v.w.i.a.c.h.AgentCallLogHook - ======================================================
[13:36:50.453] INFO v.w.i.a.m.a.h.PlanHook - #### 用户的Prompt:#######
[13:36:50.453] INFO v.w.i.a.m.a.h.PlanHook - 合肥去岳西3日游规划
[13:36:51.225] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 推理完成 (思考内容长度: 0 字符)
[13:36:51.225] INFO v.w.i.a.m.a.h.PlanHook - #### 思考过程:#######
[13:36:51.225] INFO v.w.i.a.m.a.h.PlanHook -
[13:36:51.229] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:51.229] ERROR v.w.i.a.m.a.t.RemoteAgentTool - callRouteMakingAgent->call
[13:36:51.230] ERROR v.w.i.a.m.a.t.RemoteAgentTool - 工具方法:路线制定智能体...正在调用中
[13:36:51.230] ERROR v.w.i.a.m.a.t.RemoteAgentTool - ==========================
[13:36:51.231] INFO v.w.i.a.c.h.AgentCallLogHook - >>> [ManagerAgent] 工具调用完成: callRouteMakingAgent
[13:36:51.231] INFO v.w.i.a.m.a.h.PlanHook - ##### 调用工具:callRouteMakingAgent
[13:36:58.603] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: No structured output in message metadata. Key '_structured_output' not found.] with root cause
java.lang.IllegalStateException: No structured output in message metadata. Key '_structured_output' not found.
at io.agentscope.core.message.Msg.getStructuredData(Msg.java:276)
at vip.wayhua.ivy.ai.manager.agent.agent.ManagerAgent.run(ManagerAgent.java:76)
at vip.wayhua.ivy.ai.manager.agent.controller.ManagerAgentController.tripPlan(ManagerAgentController.java:37)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:568)
at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:258)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:190)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:117)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:934)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:853)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:86)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:963)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:866)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1003)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:903)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:649)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:874)
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:710)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:128)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:199)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:107)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:165)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:77)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:482)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:113)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:83)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:72)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:397)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:903)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1778)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)
at org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:946)
at org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:480)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:57)
at java.base/java.lang.Thread.run(Thread.java:833)
看日志可以发现几个关键信息:ManagerAgent 首先调用了 create_plan 工具分解任务,然后多次尝试调用 callRouteMakingAgent(路线规划),但似乎一直没拿到有效结果。最后它自行推理出「路线规划工具遇到了技术问题」,转而调用 callTripPlannerAgent(行程规划)。
最终报错 No structured output in message metadata------这是因为 call().block() 是同步阻塞调用,Agent 在多轮工具调用后返回的消息中没有包含结构化输出数据。这个问题的根源在于 block 模式不适合多 Agent 协作场景,需要改成流式。
1.7 改成流式
block 模式(同步阻塞)在 Multi-Agent 场景下有两个问题:一是响应非常慢,用户要等所有 Agent 都跑完才能看到结果;二是容易超时和缓冲区溢出。改成流式(stream)后,结果可以逐步推送到前端,用户体验好很多。
1.7.1 Controller
java
@RequestMapping(value = "/trip/stream",
produces = "application/json;charset=UTF-8",
method = RequestMethod.POST)
public Flux<String> tripPlanStream(@RequestBody PromptSchema input) {
return managerAgent.stream(input.getPrompt());
}
1.7.2 stream
java
public Flux<String> stream(String prompt) {
PromptUtils promptUtils = new PromptUtils();
return agent.stream(promptUtils.getUserMsg(prompt))
// 过滤掉 REASONING 事件,不将 LLM 的推理过程(含 "via ManagerAgent" 等描述)推送到前端
.filter(event -> {
boolean isReasoning = event.getType() == EventType.REASONING;
if (isReasoning) {
log.debug("已过滤 REASONING 事件,不推送到前端");
}
return !isReasoning;
})
.map(this::eventToJson)
.onErrorResume(e -> {
log.warn("Agent 流式执行异常", e);
Map<String, Object> err = new HashMap<>();
err.put("type", "ERROR");
err.put("text", "执行出错:" + e.getMessage());
return Mono.just(toJsonString(err));
})
.concatWith(Mono.just("{\"type\":\"DONE\",\"text\":\"\",\"isLast\":true}"));
}
private String eventToJson(Event event) {
Map<String, Object> map = new HashMap<>();
EventType type = event.getType();
String text = event.getMessage() != null ? event.getMessage().getTextContent() : "";
if (type == EventType.REASONING) {
map.put("type", "REASONING");
map.put("text", text != null ? text : "");
} else if (type == EventType.TOOL_RESULT) {
map.put("type", "TOOL_RESULT");
map.put("text", text != null ? text : "");
} else {
map.put("type", "TEXT");
map.put("text", text != null ? text : "");
}
map.put("isLast", event.isLast());
return toJsonString(map);
}
private String toJsonString(Map<String, Object> map) {
try {
return objectMapper.writeValueAsString(map);
} catch (Exception e) {
return "{\"type\":\"ERROR\",\"text\":\"JSON序列化失败\"}";
}
}
1.7.3 远程 tools 改流式
RemoteAgentTool 也做了大改:不再在工具方法内部 new A2aAgent,而是由 ManagerAgent 构造时传入已经构建好的实例。工具方法的返回类型从 String 改为 Mono<String>,使用 stream() 流式调用远程 Agent,通过 reduce 拼接结果。这样可以避免 call().block() 导致的 SSE 缓冲区溢出问题。
java
package vip.wayhua.ivy.ai.manager.agent.tool;
import com.alibaba.nacos.api.exception.NacosException;
import io.agentscope.core.a2a.agent.A2aAgent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.nacos.a2a.discovery.NacosAgentCardResolver;
import io.agentscope.core.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import vip.wayhua.ivy.ai.common.utils.AgentUtils;
import vip.wayhua.ivy.ai.common.utils.NacosUtils;
import vip.wayhua.ivy.ai.common.utils.PromptUtils;
import java.time.Duration;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 10:37
* @modifiedBy:
* @version: 1.0
*/
public class RemoteAgentTool {
private static final Logger log = LoggerFactory.getLogger(RemoteAgentTool.class);
// 流式调用超时时间
private static final Duration STREAM_TIMEOUT = Duration.ofSeconds(180);
// ========= 不在这里注入,外部把A2aAgent实例传入 =========
private final A2aAgent routeMakingAgent;
private final A2aAgent tripPlannerAgent;
/**
* 构造函数:由业务侧(ManagerAgent)传入已经构建好的A2aAgent实例
* @param routeMakingAgent 路线agent
* @param tripPlannerAgent 行程agent
*/
public RemoteAgentTool(A2aAgent routeMakingAgent, A2aAgent tripPlannerAgent) {
this.routeMakingAgent = routeMakingAgent;
this.tripPlannerAgent = tripPlannerAgent;
}
// @Tool(description = "从Nacos注册中心获取路线制定Agent")
@Tool(description = "路线规划专家,提供:驾车/铁路/飞机路线、距离、耗时、交通方式对比。必须调用此工具获取任何交通路线信息,严禁自行编造车次和航班或路线")
public Mono<String> callRouteMakingAgent(
@ToolParam(name = "prompt", description = "路线查询需求,包含起点、终点、出行方式(铁路/自驾/飞机等)")
String prompt) throws NacosException {
final String PREFIX = "【callRouteMakingAgent 返回】\n";
if (prompt == null || prompt.isBlank()) {
log.warn("callRouteMakingAgent 收到空 prompt,跳过调用");
return Mono.just(PREFIX + "无法制定路线:未收到有效的起终点信息");
}
log.info("callRouteMakingAgent -> 正在通过流式调用路线规划Agent");
log.info("参数:" + prompt);
// A2aAgent agent = A2aAgent.builder()
// .name("RouteMakingAgent")
// .agentCardResolver(
// //创建 Nacos 的 AgentCardResolver
// new NacosAgentCardResolver(nacosUtils.getNacosClient()))
// .build();
log.error("==========================");
log.error("获取到的远程Agent描述:" + routeMakingAgent.getDescription());
log.error("==========================");
log.error("============");
log.error("这个工具方法传入的参数:" + prompt);
log.error("============");
// Flux<Event> stream = agentUtils.streamResponse(agent, "调用百度地图MCP");
//
// stream
// .doOnNext(msg -> System.out.println(msg.getMessage().getTextContent()))
// //阻塞直到结束
// .blockLast();
//远程Agent运行
// agent.call().block();
Msg userMsg = PromptUtils.getUserMsg(prompt);
//远程Agent运行
// 使用 stream() 流式调用,避免 call().block() 导致的 SSE 缓冲区溢出
return routeMakingAgent.stream(userMsg)
.doOnNext(event -> log.debug("[RouteMakingAgent] 收到流式事件: type={}, isLast={}",
event.getType(), event.isLast()))
.map(event -> {
if (event.getMessage() != null) {
String text = event.getMessage().getTextContent();
log.info("callRouteMakingAgent---->>" + text);
return text != null ? text : "";
}
return "";
})
.filter(text -> !text.isEmpty())
.reduce("", String::concat)
.map(result -> result.isEmpty() ? PREFIX + "远程Agent未返回内容" : PREFIX + result)
.timeout(STREAM_TIMEOUT)
.doOnSuccess(result -> log.info("callRouteMakingAgent 流式调用完成, 结果长度: {} 字符",
result != null ? result.length() : 0))
.onErrorResume(e -> {
log.error("callRouteMakingAgent 流式调用失败", e);
return Mono.just(PREFIX + "路线规划Agent调用失败: " + e.getMessage());
});
}
@Tool(description = "行程规划专家,提供:每日景点安排、美食推荐、住宿建议、天气参考。获取路线后必须调用此工具完成行程细节规划")
public Mono<String> callTripPlannerAgent(
@ToolParam(name = "prompt", description = "行程规划需求,包含目的地、天数、偏好、路线信息等")
String prompt) throws NacosException {
// 工具名前缀,用于前端识别工具归属(与 mock 数据格式一致)
final String PREFIX = "【callTripPlannerAgent 返回】\n";
if (prompt == null || prompt.isBlank()) {
log.warn("callTripPlannerAgent 收到空 prompt,跳过调用");
return Mono.just(PREFIX + "无法规划行程:未收到有效的行程需求信息");
}
log.info("callTripPlannerAgent -> 正在通过流式调用行程规划Agent");
log.info("参数:" + prompt);
// A2aAgent agent = A2aAgent.builder()
// .name("TripPlannerAgent")
// .agentCardResolver(
// //创建 Nacos 的 AgentCardResolver
// new NacosAgentCardResolver(nacosUtils.getNacosClient()))
// .build();
log.error("==========================");
log.error("获取到的远程Agent描述:" + tripPlannerAgent.getDescription());
log.error("==========================");
//远程Agent运行
// agent.call().block();
log.info("============");
log.info("这个工具方法传入的参数:" + prompt);
log.info("============");
Msg userMsg = PromptUtils.getUserMsg(prompt);
// 使用 stream() 流式调用,避免 call().block() 导致的 SSE 缓冲区溢出
return tripPlannerAgent.stream(userMsg)
.doOnNext(event -> log.debug("[TripPlannerAgent] 收到流式事件: type={}, isLast={}",
event.getType(), event.isLast()))
.map(event -> {
if (event.getMessage() != null) {
String text = event.getMessage().getTextContent();
log.info("callTripPlannerAgent---->>" + text);
return text != null ? text : "";
}
return "";
})
.filter(text -> !text.isEmpty())
.reduce("", String::concat)
.map(result -> result.isEmpty() ? PREFIX + "远程Agent未返回内容" : PREFIX + result)
.timeout(STREAM_TIMEOUT)
.doOnSuccess(result -> log.info("callTripPlannerAgent 流式调用完成, 结果长度: {} 字符",
result != null ? result.length() : 0))
.onErrorResume(e -> {
log.error("callTripPlannerAgent 流式调用失败", e);
return Mono.just(PREFIX + "行程规划Agent调用失败: " + e.getMessage());
});
}
}
1.7.4 新改的 agent
ManagerAgent 也做了相应调整:构造函数中提前构建好两个远程 Agent 的 A2A 代理实例,传给 RemoteAgentTool。同时新增了 stream() 方法,用 Reactor 的 Flux 实现流式输出,过滤掉 REASONING 事件(不把推理过程推给前端),最终拼上 DONE 标记。
java
package vip.wayhua.ivy.ai.manager.agent.agent;
import com.alibaba.nacos.api.ai.AiService;
import com.alibaba.nacos.api.exception.NacosException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.agentscope.core.ReActAgent;
import io.agentscope.core.a2a.agent.A2aAgent;
import io.agentscope.core.agent.Event;
import io.agentscope.core.agent.EventType;
import io.agentscope.core.message.Msg;
import io.agentscope.core.model.StructuredOutputReminder;
import io.agentscope.core.nacos.a2a.discovery.NacosAgentCardResolver;
import io.agentscope.core.plan.PlanNotebook;
import io.agentscope.core.tool.Toolkit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import vip.wayhua.ivy.ai.common.dto.ResponseSchema;
import vip.wayhua.ivy.ai.common.utils.AgentUtils;
import vip.wayhua.ivy.ai.common.utils.NacosUtils;
import vip.wayhua.ivy.ai.common.utils.PromptUtils;
import vip.wayhua.ivy.ai.common.utils.ToolUtils;
import vip.wayhua.ivy.ai.manager.agent.hook.PlanHook;
import vip.wayhua.ivy.ai.manager.agent.plan.TripPlan;
import vip.wayhua.ivy.ai.manager.agent.tool.RemoteAgentTool;
import java.util.HashMap;
import java.util.Map;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 10:34
* @modifiedBy:
* @version: 1.0
*/
@Component
public class ManagerAgent {
private static final Logger log = LoggerFactory.getLogger(ManagerAgent.class);
private final ObjectMapper objectMapper = new ObjectMapper();
private final ReActAgent agent;
AgentUtils agentUtils;
public ReActAgent getManagerAgent() {
return this.agent;
}
public ManagerAgent(AgentUtils agentUtils, NacosUtils nacosUtils) throws NacosException {
this.agentUtils = agentUtils;
log.info("--------------------------");
AiService nacosClient = nacosUtils.getNacosClient();
log.info("--------------------------");
A2aAgent routeMakingAgent = A2aAgent.builder()
.name("RouteMakingAgent")
.agentCardResolver(
//创建 Nacos 的 AgentCardResolver
new NacosAgentCardResolver(nacosClient))
.build();
A2aAgent tripPlannerAgent = A2aAgent.builder()
.name("TripPlannerAgent")
.agentCardResolver(
//创建 Nacos 的 AgentCardResolver
new NacosAgentCardResolver(nacosClient))
.build();
TripPlan plan = new TripPlan();
//Toolkit
ToolUtils toolUtils = new ToolUtils();
//将远程Agent封装为工具的封装注册到工具包
Toolkit toolkit = toolUtils.getToolkit(new RemoteAgentTool(routeMakingAgent,tripPlannerAgent));
PlanNotebook planNotebook = plan.getPlan();
agent = agentUtils.getReActAgentBuilder(
"ManagerAgent",
"主管Agent"
)
.planNotebook(planNotebook)
.hook(new PlanHook(planNotebook))
//工具包
.toolkit(toolkit)
//结构化输出
.structuredOutputReminder(StructuredOutputReminder.PROMPT)
.build();
}
public ResponseSchema run(String prompt) {
// 发送查询,指定输出类型
Msg response = agent
.call(PromptUtils.getUserMsg(prompt),
ResponseSchema.class)
.block();
// 提取类型化数据
ResponseSchema data = response.getStructuredData(ResponseSchema.class);
return data;
}
public Flux<String> stream(String prompt) {
PromptUtils promptUtils = new PromptUtils();
return agent.stream(promptUtils.getUserMsg(prompt))
// 过滤掉 REASONING 事件,不将 LLM 的推理过程(含 "via ManagerAgent" 等描述)推送到前端
.filter(event -> {
boolean isReasoning = event.getType() == EventType.REASONING;
if (isReasoning) {
log.debug("已过滤 REASONING 事件,不推送到前端");
}
return !isReasoning;
})
.map(this::eventToJson)
.onErrorResume(e -> {
log.warn("Agent 流式执行异常", e);
Map<String, Object> err = new HashMap<>();
err.put("type", "ERROR");
err.put("text", "执行出错:" + e.getMessage());
return Mono.just(toJsonString(err));
})
.concatWith(Mono.just("{\"type\":\"DONE\",\"text\":\"\",\"isLast\":true}"));
}
private String eventToJson(Event event) {
Map<String, Object> map = new HashMap<>();
EventType type = event.getType();
String text = event.getMessage() != null ? event.getMessage().getTextContent() : "";
if (type == EventType.REASONING) {
map.put("type", "REASONING");
map.put("text", text != null ? text : "");
} else if (type == EventType.TOOL_RESULT) {
map.put("type", "TOOL_RESULT");
map.put("text", text != null ? text : "");
} else {
map.put("type", "TEXT");
map.put("text", text != null ? text : "");
}
map.put("isLast", event.isLast());
return toJsonString(map);
}
private String toJsonString(Map<String, Object> map) {
try {
return objectMapper.writeValueAsString(map);
} catch (Exception e) {
return "{\"type\":\"ERROR\",\"text\":\"JSON序列化失败\"}";
}
}
}
1.8 测试
改成流式后重新测试,发现一个低级但很典型的问题------@Value 注解的写法。
java
@Value("agentscope.a2a.nacos.server-addr")
String server_addr;
这个错误本来不应该犯------@Value 注解的属性占位符必须用 ${} 包裹,少了 $ 和 { 就变成了直接按字面量注入。这种低级错误,看来用 AI 用多了,人真的会变傻。基本语法反而生疏了。
java
@Value("${agentscope.a2a.nacos.server-addr}")
String server_addr;

现在这个是对的。
代码调用基本上没问题了,但是还是会死------子微服务重启导致 Agent 有的死了,但 ManagerAgent 还在调用。日志中出现了 overflow buffer is full 的异常,这是 A2A 流式传输时缓冲区溢出的问题。
less
======================================================
[15:07:50.851] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] threw exception
java.lang.IllegalStateException: The following item cannot be propagated because there is no demand and the overflow buffer is full: io.a2a.spec.SendStreamingMessageResponse@253427b
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:27)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.transport.jsonrpc.handler.JSONRPCHandler$1.onNext(JSONRPCHandler.java:264)
at io.a2a.transport.jsonrpc.handler.JSONRPCHandler$1.onNext(JSONRPCHandler.java:254)
at io.a2a.server.requesthandlers.DefaultRequestHandler$1.onNext(DefaultRequestHandler.java:445)
at io.a2a.server.requesthandlers.DefaultRequestHandler$1.onNext(DefaultRequestHandler.java:418)
at mutiny.zero.operators.Transform$Processor.onNext(Transform.java:48)
at mutiny.zero.operators.Transform$Processor.onNext(Transform.java:48)
at mutiny.zero.internal.TubeBase.drainLoop(TubeBase.java:188)
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:25)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.server.util.async.AsyncUtils$AbstractSubscriber.onNext(AsyncUtils.java:130)
at mutiny.zero.internal.TubeBase.drainLoop(TubeBase.java:188)
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:25)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.server.util.async.AsyncUtils$AbstractSubscriber.onNext(AsyncUtils.java:130)
at mutiny.zero.internal.TubeBase.drainLoop(TubeBase.java:188)
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:25)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.server.events.EventConsumer.lambda$consumeAll$0(EventConsumer.java:92)
at mutiny.zero.internal.TubePublisher.subscribe(TubePublisher.java:50)
at io.a2a.server.util.async.AsyncUtils.lambda$processor$1(AsyncUtils.java:79)
at mutiny.zero.internal.TubePublisher.subscribe(TubePublisher.java:50)
at io.a2a.server.util.async.AsyncUtils.lambda$processor$1(AsyncUtils.java:79)
at mutiny.zero.internal.TubePublisher.subscribe(TubePublisher.java:50)
at mutiny.zero.operators.Transform.subscribe(Transform.java:35)
at mutiny.zero.operators.Transform.subscribe(Transform.java:35)
at io.a2a.server.requesthandlers.DefaultRequestHandler.lambda$onMessageSendStream$5(DefaultRequestHandler.java:418)
at io.a2a.transport.jsonrpc.handler.JSONRPCHandler.lambda$convertToSendStreamingMessageResponse$0(JSONRPCHandler.java:254)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1804)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
[15:07:50.852] ERROR o.a.c.c.C.[.[.[.[dispatcherServlet] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: The following item cannot be propagated because there is no demand and the overflow buffer is full: io.a2a.spec.SendStreamingMessageResponse@253427b] with root cause
java.lang.IllegalStateException: The following item cannot be propagated because there is no demand and the overflow buffer is full: io.a2a.spec.SendStreamingMessageResponse@253427b
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:27)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.transport.jsonrpc.handler.JSONRPCHandler$1.onNext(JSONRPCHandler.java:264)
at io.a2a.transport.jsonrpc.handler.JSONRPCHandler$1.onNext(JSONRPCHandler.java:254)
at io.a2a.server.requesthandlers.DefaultRequestHandler$1.onNext(DefaultRequestHandler.java:445)
at io.a2a.server.requesthandlers.DefaultRequestHandler$1.onNext(DefaultRequestHandler.java:418)
at mutiny.zero.operators.Transform$Processor.onNext(Transform.java:48)
at mutiny.zero.operators.Transform$Processor.onNext(Transform.java:48)
at mutiny.zero.internal.TubeBase.drainLoop(TubeBase.java:188)
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:25)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.server.util.async.AsyncUtils$AbstractSubscriber.onNext(AsyncUtils.java:130)
at mutiny.zero.internal.TubeBase.drainLoop(TubeBase.java:188)
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:25)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.server.util.async.AsyncUtils$AbstractSubscriber.onNext(AsyncUtils.java:130)
at mutiny.zero.internal.TubeBase.drainLoop(TubeBase.java:188)
at mutiny.zero.internal.BufferingTube.handleItem(BufferingTube.java:25)
at mutiny.zero.internal.TubeBase.send(TubeBase.java:80)
at io.a2a.server.events.EventConsumer.lambda$consumeAll$0(EventConsumer.java:92)
at mutiny.zero.internal.TubePublisher.subscribe(TubePublisher.java:50)
at io.a2a.server.util.async.AsyncUtils.lambda$processor$1(AsyncUtils.java:79)
at mutiny.zero.internal.TubePublisher.subscribe(TubePublisher.java:50)
at io.a2a.server.util.async.AsyncUtils.lambda$processor$1(AsyncUtils.java:79)
at mutiny.zero.internal.TubePublisher.subscribe(TubePublisher.java:50)
at mutiny.zero.operators.Transform.subscribe(Transform.java:35)
at mutiny.zero.operators.Transform.subscribe(Transform.java:35)
at io.a2a.server.requesthandlers.DefaultRequestHandler.lambda$onMessageSendStream$5(DefaultRequestHandler.java:418)
at io.a2a.transport.jsonrpc.handler.JSONRPCHandler.lambda$convertToSendStreamingMessageResponse$0(JSONRPCHandler.java:254)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1804)
at java.base/java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java)
at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1136)
at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:635)
at java.base/java.lang.Thread.run(Thread.java:833)
这个 overflow buffer is full 异常的根因是 Spring MVC(基于 Servlet)和流式响应不太兼容。解决方案是把 spring-boot-starter-web 换成 spring-boot-starter-webflux(基于 Reactor + Netty),原生支持响应式流。
ai-common的pom.xml
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>vip.wayhua.ivy.ai</groupId>
<artifactId>XAiTripPlan-V108</artifactId>
<version>1.0.1</version>
</parent>
<artifactId>ai-common</artifactId>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- AgentScope 和 SpringBoot 的集成 -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-spring-boot-starter</artifactId>
<version>${AgentScope.version}</version>
</dependency>
<!-- 实现slf4j接口,不然日志打印不出来 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 额外添加 Nacos Spring Boot starter 的依赖 -->
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-nacos-spring-boot-starter</artifactId>
<version>${AgentScope.version}</version>
</dependency>
<!-- <!– NextDoc4j 现代化API文档,适配SpringBoot4,替代Swagger/Knife4j –>-->
<!-- <dependency>-->
<!-- <groupId>top.nextdoc4j</groupId>-->
<!-- <artifactId>nextdoc4j-spring-boot-starter</artifactId>-->
<!-- </dependency>-->
<!-- <dependency>-->
<!-- <groupId>org.springdoc</groupId>-->
<!-- <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>-->
<!-- </dependency>-->
</dependencies>
</project>
还有要将里面涉及 import io.swagger.v3.oas.annotations.media.Schema 的引用注释掉,因为换成 webflux 后不再使用 Swagger。
基本能完成了。发一个真实的行程规划请求测试一下:
帮我制定10号从合肥到岳西的6日行程






小结
手动重构一遍,到底带来了什么
前面让 Trae 帮忙把代码跑起来,我又花了一天半时间,自己从头手敲处理优化了一遍。这一遍下来,感受和之前完全不同。
为什么要手动重构? 因为代码能跑和代码能懂是两回事。Trae 帮你调通了,但每一行为什么这么写,你不一定清楚。手敲一遍,编译报错→查缺什么→补上→理解,这个过程才是真正把知识内化的过程。比如 @Value 那个低级错误------少了 ${},如果是 AI 写的,你根本不会注意;自己写错了,排查一遍,这辈子都忘不了。
AI 的「捷径思维」
为什么要优化呢?这就要说到 AI 幻觉上去了。Trae 发现无法连接 Nacos,可能它收到的命令是「跑通」吧,所以就手动添加了微服务的直接连接地址,如 127.0.0.1:8881。这怎么说呢?任务确实是完成了,但没满足我的心理期待。还有就是我上次写在 skills 里面的------AI 就是一个花心大萝卜,这个试一下,不行就换一个,从来不认真研究代码、深入解决问题。驴粪团子表面光,里面一堆垃圾,用代码说话。
Trae 的写法是这样的:
java
public class RemoteAgentTool {
private static final Logger log = LoggerFactory.getLogger(RemoteAgentTool.class);
// 远程 Agent 的 A2A 服务地址(Well-Known URI 方式,不依赖 Nacos 版本)
private static final String ROUTE_AGENT_URL = "http://localhost:8120";
private static final String TRIP_AGENT_URL = "http://localhost:8130";
//...
// 使用 WellKnownAgentCardResolver 直接通过 HTTP 获取 Agent Card
A2aAgent agent = A2aAgent.builder()
.name("RouteMakingAgent")
.agentCardResolver(
WellKnownAgentCardResolver.builder()
.baseUrl(ROUTE_AGENT_URL)
.build())
.build();
log.info("远程Agent描述:" + agent.getDescription());
而我要的是通过 Nacos 服务发现,下面是我改进后的写法,这段代码写在 manager-agent 里面:
java
A2aAgent routeMakingAgent = A2aAgent.builder()
.name("RouteMakingAgent")
.agentCardResolver(
//创建 Nacos 的 AgentCardResolver
new NacosAgentCardResolver(nacosClient))
.build();
还是一个问题:AI 自作主张,反正结果是任务完成了。但如果要迁移呢?如果是多个微服务呢?这好像不是这次任务的问题------你要下一次提问,才能解决。结论是:AI 是能完成当次任务的,也只限于当次任务。至于有没有问题,那是下一个任务的事。挖坑,放树苗,填土------今天放树苗的人不在,那就挖坑,填土。
关于学习路径的一点感悟
这次的学习路径是:教程跟敲 → Trae 调通 → 手动重构。三个阶段,每个阶段的收获完全不同。跟敲让你知道代码长什么样;Trae 调通让你知道代码哪里会出问题;手动重构才让你真正理解每一行在干什么。如果跳过前两步直接手敲,大概率会卡在环境配置上放弃。如果止步于第二步,代码永远只是「别人的」。
关于 AgentScope 框架,这次手动重构也让我对 Multi-Agent 架构有了更具体的感觉。ReActAgent 的推理-行动循环、Toolkit 的工具注册机制、A2A 协议的 Agent 发现与调用、Hook 的事件监听------这些概念光看文档是记不住的,只有自己写一遍、调一遍、报错一遍,才会真正变成自己的东西。
一句话总结:代码跑通了只是起点,手动重构一遍才是真正的学习。AI 能帮你调通代码,但理解这件事,只能靠自己动手。
本文是 AiTripPlan 系列的第三篇(上篇)。前两篇分别记录了教程跟敲和 Trae 调通过程。本篇聚焦 AgentScope 1.0.8 版本的手动重构,下一篇将记录从 1.0.8 升级到 AgentScope 2.0 的完整过程------API 变了什么、踩了哪些坑、虎头蛇尾又怎么收的场。感谢阅读,下篇见!