手敲重构学透 Multi-Agent 代码(下):从 AgentScope 1.0.8 升级到 2.0,API 变了什么、踩了哪些坑
上篇把 AgentScope 1.0.8 的代码逐行手敲重构了一遍,每一行都搞懂了。这一篇接着记录升级到 2.0 的全过程——先是借助 AI 工具(Qoder、OpenCode)把 2.0 跑了一遍,踩了一堆坑;再手动重构,搞懂每一个 API 变化。哪些 API 变了、哪些坑踩了、虎头蛇尾又怎么收的场,都记在这里了。
1.0.8 版本全部搞懂之后,开始升级到 AgentScope 2.0。2.0 相比 1.x 有不少变化,这里先列一个总览,后面逐个展开:
| 变化点 | 1.0.8 写法 | 2.0 写法 |
|---|---|---|
| 任务分解 | PlanNotebook + .enablePlan() | .enableTaskList() |
| Skills 加载 | JarSkillRepositoryAdapter | ClasspathSkillRepository |
| DashScope 模型包路径 | io.agentscope.core.model | io.agentscope.extensions.model |
| 结构化输出 | 需 StructuredOutputReminder | call(msg, Class) 原生支持 |
| 核心依赖 | agentscope-spring-boot-starter | agentscope + 独立扩展包 |
| Nacos A2A | 打包在 starter 中 | 拆为 agentscope-extensions-nacos-a2a |
看起来变化不少,但实际改起来,大部分是「删掉旧写法、换成新方法名」的体力活。真正费时间的是那些文档没写清楚、只能靠报错一步步摸索的细节。
不过在手动重构之前,其实已经借助 AI 工具把 2.0 版本先跑了一遍。这个过程一波三折——先用 Qoder,免费额度用完了;改用 OpenCode,又踩了一堆坑。但正是这个「先让 AI 跑一遍」的过程,给后面的手动重构打下了基础:至少知道了 2.0 大概要改哪些东西,哪些地方是坑。
一、借助 AI 初探 2.0:从 Qoder 到 OpenCode
1.1 Qoder 试水
前面用 Qoder 学习代码的效果不错,这次升级 2.0 也先让它试试:
在AiTripPlan-V2中,将前面108的功能升级到2.0。
这些功能,文档都可以提取在readme.md中
Qoder 确实能做,但免费版本用不了——额度又用完了。
1.2 改用 OpenCode 迁移
Qoder 免费额度用完之后,改用 OpenCode 来做迁移。OpenCode 的优势在于免费,虽然能力上比 Qoder 差了不少,但免费就是最大的优势。
AiTripPlan-v108 -> 多agent功能已完成
将同样的功能迁移到AiTripPlan-V2
目录已创建,同样的功能都要迁移,且生成文档放在AiTripPlan-V2中的doc中。
ai-trip-plan-frontend 是v108的前端,要保证v2的所有接口都一样,直接访问,端口也一样吧。
1.3 运行踩坑:logback 冲突与 Nacos 连接
代码迁移完之后,启动就报错了。
logback 版本冲突
运行 ManagerAgentApplication 时,直接启动失败——logback-classic 1.4.14 和 logback-core 1.5.34 版本不兼容,LoggerContext.initCollisionMaps() 方法在 1.4.x 中不存在:
Logging system failed to initialize using configuration from 'null'
java.lang.NoSuchMethodError: 'void ch.qos.logback.classic.LoggerContext.initCollisionMaps()'
at ch.qos.logback.classic.LoggerContext.reset(LoggerContext.java:362)
at ch.qos.logback.classic.LoggerContext.stop(LoggerContext.java:343)
at org.springframework.boot.logging.logback.LogbackLoggingSystem.stopAndReset(LogbackLoggingSystem.java:309)
...
17:52:09.510 [main] ERROR org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter --
***************************
APPLICATION FAILED TO START
***************************
Description:
An attempt was made to call a method that does not exist. The attempt was made from the following location:
ch.qos.logback.classic.LoggerContext.reset(LoggerContext.java:362)
The following method did not exist:
'void ch.qos.logback.classic.LoggerContext.initCollisionMaps()'
The calling method's class, ch.qos.logback.classic.LoggerContext, was loaded from the following locations:
jar:file:/F:/maven/repository/ch/qos/logback/logback-classic/1.4.14/logback-classic-1.4.14.jar
The called method's class hierarchy was loaded from the following locations:
ch.qos.logback.classic.LoggerContext: logback-classic-1.4.14.jar
ch.qos.logback.core.ContextBase: logback-core-1.5.34.jar
Action:
Correct the classpath of your application so that it contains a single, compatible version of ch.qos.logback.classic.LoggerContext
问题根因:AgentScope 2.0 的依赖树拉入了 logback-core 1.5.34,但 Spring Boot 管理的 logback-classic 还停留在 1.4.14,两个版本跨大版本不兼容。这也是为什么后面 ai-core 的 pom 里显式指定了 logback-classic 的版本——统一版本才能解决冲突。
Nacos 连不上
logback 解决之后,又遇到了 Nacos 连接问题。OpenCode 生成的配置里,Agent Card 的 URL 写成了本地 IP,而不是走 Nacos 注册中心:
a2a:
server:
enabled: true
card:
name: RouteMakingAgent
url: ${ROUTE_AGENT_CARD_URL:http://192.168.55.1:8120}
description: 路线规划专家,提供驾车/铁路/飞机路线、距离、耗时、交通方式对比 为什么不是192.168.55.140:8848
没看到连接到nacos啊
太慢了,取消了前面的回答,重新提问:
自己注册到nacos也不用自己配置啊,这多笨啊。 108版本不是有注册到nacos吗?你有没有看代码 。
OpenCode 又开始「试一下那个试一下」了,不得不提醒它:
我不希望你是这样试一下那样试一下,而是要就这种方法,一定要搞好,不然就会同一堆bug不停的出现,不停的出现。 一次只改一个地方,一个bug,不要动不动就换思路,只要是开始认为的思路就一直走下去。将这个写进opencode目录下的skills里面去。还有java,maven,这种常用的工具地址也写进skills里面去,不要每次查找下载。
1.4 关于 AI 工具的体会
真正来说,OpenCode 的优势在于免费,Qoder 比 OpenCode 强的不是一点点。但 OpenCode 免费啊,这也算是大优势了。少说一句都不行,还自作主张,说是没调通 Nacos,所以就自己注册了——妈呀,这都不是幻觉了。
更多的时候,我不希望 AI 工具这样试一下那样试一下,而是要就这种方法,一定要搞好,不然就会同一堆 bug 不停地出现、不停地出现。
正是有了这次 AI 辅助的初探,才让后面的手动重构更有方向感——至少知道了 2.0 的坑大概在哪些地方:logback 版本冲突、Nacos 连接配置、依赖包结构变化。下面就开始逐模块手动重构。
二、初始环境准备
为了避免和 1.0.8 的模块混淆,将 ai-common 改名为 ai-core。名字虽然只改了几个字母,但意义不同:ai-common 是 1.0.8 的公共模块,ai-core 是 2.0 的公共模块,两边依赖的 AgentScope 版本不同,混在一起容易出问题。新开一个项目 AITripPlan-V2,结构上和 1.0.8 保持一致:ai-core(公共模块)、manager-agent、routeMaking-agent、tripPlanner-agent。
粗糙地将 2.0 版本运行了出来,其他代码变化不大,主要是配置和 pom 依赖的调整。
2.1 ai-core
Properties 只是做了常规调整,包名从 vip.wayhua.ivy.ai.common.conf 改为 vip.wayhua.ivy.ai.conf。字段和 1.0.8 一致,没有什么新增。
package vip.wayhua.ivy.ai.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;
}
}
AgentCallLogHook 改动不大,主要是包名调整。
package vip.wayhua.ivy.ai.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);
}
}
修改比较多的是 pom。2.0 版本的依赖结构有变化,agentscope-spring-boot-starter 换成了核心包 agentscope,Nacos A2A 相关功能拆成了独立扩展包。这个变化其实反映了 2.0 的设计思路:核心功能和扩展功能分离,你只引入你需要的部分,而不是一股脑全塞进来。好处是依赖更轻量,坏处是你得搞清楚自己到底需要哪些包——以前一个 starter 搞定的事,现在要手动拼。
<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>AITripPlan-V2</artifactId>
<version>1.0.1</version>
</parent>
<artifactId>ai-core</artifactId>
<packaging>jar</packaging>
<name>ai-core</name>
<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>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- 实现slf4j接口,不然日志打印不出来 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-nacos-a2a</artifactId>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-extensions-a2a-client</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</dependency>
</dependencies>
</project>
1.0.8 的大部分功能还是能用的,主要是修改一些配置和 pom 依赖。2.0 把一些功能拆成了独立的扩展包,比如 agentscope-extensions-nacos-a2a(Nacos A2A 扩展)和 agentscope-extensions-a2a-client(A2A 客户端扩展),需要单独引入。
AgentUtils 的核心逻辑没变,但 DashScopeChatModel 的包路径从 io.agentscope.core.model 移到了 io.agentscope.extensions.model。这个变化说明 DashScope 支持从核心包剥离成了扩展——毕竟不是所有人都用阿里云的模型,放到 extensions 里更合理。
package vip.wayhua.ivy.ai.utils;
import io.agentscope.core.ReActAgent;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import io.agentscope.extensions.model.dashscope.DashScopeChatModel;
import vip.wayhua.ivy.ai.conf.Properties;
import vip.wayhua.ivy.ai.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();
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))
;
}
}
三、manager-agent 模块
3.1 ManagerAgent
2.0 版本的 ManagerAgent 主要变化在任务分解和结构化输出:PlanNotebook 被移除,改用 .enableTaskList();StructuredOutputReminder 不再需要,call(msg, Class) 原生支持结构化输出。
说具体一点:
- 任务分解:1.0.8 里要手动创建
PlanNotebook,通过.enablePlan()和.planNotebook(planNotebook)来开启任务分解。2.0 简化成一个链式调用.enableTaskList(),Agent 能自主分解复杂任务、生成执行步骤、跟踪状态并动态调整。代码量少了,但功能反而更强了。 - 结构化输出:1.0.8 里需要额外传一个
StructuredOutputReminder来告诉 Agent「请按这个 schema 返回」。2.0 直接在call()方法里传 Class 参数就行,框架自动处理序列化。少了一层间接性,代码更直观。
代码中的注释块详细记录了这些 API 变化,方便对照。
package vip.wayhua.ivy.ai.manager.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.event.AgentEventType;
import io.agentscope.core.event.ThinkingBlockDeltaEvent;
import io.agentscope.core.message.Msg;
import io.agentscope.core.nacos.a2a.discovery.NacosAgentCardResolver;
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.data.ResponseSchema;
import vip.wayhua.ivy.ai.manager.hook.PlanHook;
import vip.wayhua.ivy.ai.manager.tool.RemoteAgentTool;
import vip.wayhua.ivy.ai.utils.AgentUtils;
import vip.wayhua.ivy.ai.utils.NacosUtils;
import vip.wayhua.ivy.ai.utils.PromptUtils;
import vip.wayhua.ivy.ai.utils.ToolUtils;
import java.util.HashMap;
import java.util.Map;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 16:42
* @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;
//Toolkit
ToolUtils toolUtils = new ToolUtils();
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();
//将远程Agent封装为工具的封装注册到工具包
Toolkit toolkit = toolUtils.getToolkit(new RemoteAgentTool(routeMakingAgent,tripPlannerAgent));
agent = agentUtils.getReActAgentBuilder(
"ManagerAgent",
"主管Agent"
)
/* **********************
*
* AgentScope 2.0 的任务分解能力:
*
* 1.x 版本通过 PlanNotebook (PlanAct + ReAct) 实现任务分解与步骤执行:
* .enablePlan() / .planNotebook(planNotebook)
*
* 2.0 版本 PlanNotebook 已移除,改为 .enableTaskList():
* 开启后 ReActAgent 能自主分解复杂任务、生成执行步骤、状态跟踪、动态调整。
*
* *********************/
.enableTaskList()
.hook(new PlanHook())
//工具包
.toolkit(toolkit)
//结构化输出:2.0 由 call(msg, Class) 原生支持,无需 StructuredOutputReminder
.build();
}
public ResponseSchema run(String prompt) {
Msg response = agent.call(PromptUtils.getUserMsg(prompt)).block();
ResponseSchema data = new ResponseSchema();
data.response = response != null ? response.getTextContent() : "";
return data;
}
public Flux<String> stream(String prompt) {
return agent.stream(PromptUtils.getUserMsg(prompt))
// .filter(event -> {
// // 2.0 枚举:AgentStreamEvent.Type.REASONING
// boolean isReasoning = AgentStreamEvent.Type.REASONING.equals(event.type());
// if (isReasoning) {
// log.debug("已过滤 REASONING 事件,不推送到前端");
// }
// return !isReasoning;
// })
// .filter(event -> {
// // 过滤思考推理事件,不推前端
//// boolean isReasoning = event instanceof ThinkingBlockDeltaEvent;
// boolean isReasoning = AgentEventType.R.REASONING.equals(event.getType());
// if (isReasoning) {
// log.debug("已过滤 REASONING(ThinkingBlockDeltaEvent) 事件,不推送到前端");
// }
// 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}"));
//
// .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序列化失败\"}";
}
}
}
3.2 RemoteAgentTool
RemoteAgentTool 和 1.0.8 版本基本一致,主要是包名调整。流式调用的逻辑没有变化——还是通过 A2aAgent.stream(userMsg) 发起流式请求,用 reduce 拼接结果,加上超时和错误处理。
值得一提的是,2.0 里 A2aAgent 的构建方式没变,还是通过 NacosAgentCardResolver 从 Nacos 注册中心获取 Agent Card。上篇里花了不少时间把 Trae 硬编码的 127.0.0.1:8120 改成 Nacos 服务发现,这次升级到 2.0 这部分代码倒是省心了,直接搬过来就行。
package vip.wayhua.ivy.ai.manager.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.tool.Tool;
import io.agentscope.core.tool.ToolParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import vip.wayhua.ivy.ai.utils.PromptUtils;
import java.time.Duration;
/**
*
* @Author:黄卫华(wayhua@126.com)
* @Description:
* @date: 2026-08-06 20:24
* @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();
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());
});
}
}
四、routeMaking-agent 模块
4.1 pom.xml
routeMaking-agent 模块引入 ai-core 作为公共依赖,加上 A2A 和 Nacos 的 Spring Boot Starter。
<?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>AITripPlan-V2</artifactId>
<version>1.0.1</version>
</parent>
<artifactId>routeMaking-agent</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>
<dependency>
<groupId>vip.wayhua.ivy.ai</groupId>
<artifactId>ai-core</artifactId>
<version>1.0.1</version>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-a2a-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.agentscope</groupId>
<artifactId>agentscope-nacos-spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
4.2 RouteMakingAgent
先将百度 MCP 去掉了,因为要用户确认,不知道是因为用量用完了还是什么原因,暂时注释掉。MCP(Model Context Protocol)是 Anthropic 提出的标准协议,让大模型能调用外部工具。百度地图的 MCP Server 提供了路线查询、POI 搜索、地理编码等能力,但在实际使用中发现每次调用都要用户确认,体验不太顺畅。这个问题先搁置,后面再研究是 MCP 客户端的配置问题还是百度地图的限制。
其余代码和 1.0.8 基本一致,只是注释掉了 MCP 相关的部分。
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.conf.Properties;
import vip.wayhua.ivy.ai.route.mcp.BaiduMapMcp;
import vip.wayhua.ivy.ai.utils.AgentUtils;
import vip.wayhua.ivy.ai.utils.ToolUtils;
import java.util.Set;
@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();
// //将百度地图MCP的工具注册到工具包
// 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();
}
}
4.3 特别注意:Nacos 连接配置
一直连不上 Nacos,升级到 3.2.0 也连不上,查了资料才发现要修改配置——多个 enabled 属性需要显式设置为 true。这个问题在前面借助 OpenCode 升级时也遇到过(见 1.3 节),当时 OpenCode 没有搞清楚原因,直接硬编码了 Agent Card 的 URL 来绕过问题。手动重构时终于找到了根因:2.0 把功能拆成了扩展包,配置项也跟着多了。到底是哪一个生效我也还是没有完全搞明白,可能是 agentscope.a2a.nacos.enabled,这些细节还要在不断使用中去探索、积累。
这个问题挺典型的:2.0 把功能拆成了扩展包,灵活度上来了,但配置项也多了。1.0.8 里一个 starter 搞定的事,现在要手动开三个开关:agentscope.a2a.enabled、agentscope.a2a.server.enabled、agentscope.a2a.nacos.enabled。文档里没写清楚哪些是必须的,只能靠报错反推。这种「不报错就不知道少了什么」的体验,是升级过程中最耗时间的部分。
server:
port: 9991
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]
agentscope:
a2a:
enabled : true
server:
enabled: true
nacos:
enabled: true
server-addr: 192.168.55.140:8848
username: nacos
password: nacos
五、TripPlanner-agent
5.1 SuggestSightAgent
2.0 版本最大的改动在于加载 Skills 的方式:JarSkillRepositoryAdapter 改成了 ClasspathSkillRepository,从 classpath 路径读取 Skill 定义文件。
这个变化的影响其实不小。1.0.8 用 JarSkillRepositoryAdapter 加载 Skills,需要把 Skill 文件打包成 jar,然后通过 jar 路径加载——开发时改一次 Skill 文件就得重新打包,调试体验很差。2.0 改成 ClasspathSkillRepository,直接从 classpath 读取,开发时放在 resources/skills/ 目录下就行,改完刷新就生效。一个小改动,开发体验好了不少。
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.repository.ClasspathSkillRepository;
import io.agentscope.core.tool.Toolkit;
import vip.wayhua.ivy.ai.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
ClasspathSkillRepository repo = null;
try {
repo = new ClasspathSkillRepository("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();
}
}
5.2 TripPlannerAgent
TripPlannerAgent 的代码和 1.0.8 基本一致,只是从构造函数初始化改成了 @Bean 方式注入。它通过 toolkit.registration().subAgent() 把 SuggestSightAgent 注册为子 Agent 工具,实现 Agent 的嵌套调度。
这个 subAgent 的设计思路值得多说一句:TripPlannerAgent 是行程规划专家,但它不一定擅长景点推荐。与其把景点推荐的逻辑塞进 TripPlannerAgent 里,不如把 SuggestSightAgent 作为一个「工具」挂上去,需要的时候调用。这其实就是 Multi-Agent 的核心价值——每个 Agent 只做自己擅长的事,通过工具调用的方式组合起来。1.0.8 和 2.0 在这一点上没有变化,说明这个设计是经得住考验的。
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.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;
// ReActAgent agent;
//
// public TripPlannerAgent(AgentUtils agentUtils) {
// 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);
//
// agent = builder.build();
// }
//
// public ReActAgent getTripPlannerAgent() {
// return this.agent;
// }
@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();
}
}
5.3 pom.xml
配置文件和 routeMaking-agent 是一样的,这个就不用多介绍了。
六、虎头蛇尾
总感觉升级到 2.0 有点虎头蛇尾,有点将就。怎么说呢?
仔细想想,「虎头」在于前面手动重构 1.0.8 时下了大功夫,每个模块逐行拆解,每个 API 都查了文档,信心满满地开始升级 2.0。结果到了 2.0,发现大部分代码直接搬过来就能用,真正要改的就是那几个 API 名字和 pom 依赖。心理预期是「大干一场」,实际是「搬砖+改名字」,落差感自然就来了。
「蛇尾」还体现在一些功能暂时搁置了。百度地图 MCP 因为用户确认的问题注释掉了,Nacos 连接的 enabled 配置到底哪个生效也没完全搞明白。这些问题不是不能解决,而是时间有限,先把主干跑通,细节后面再补。
但我认为这也不算什么大问题——代码最重要的是先运行起来,再在能运行的基础上不断探索、不断改进。能跑了之后,再研究文档、其他用法。如果连基础框架都搭建不起来,所有优化、高性能都是扯淡。先有再优,这是工程上的基本逻辑。
七、测试结果
2.0 版本调通后,发送「帮我制定10号从合肥到岳西的6日行程」的请求,下面是 Agent 返回的完整行程规划:
为您定制了这份10号合肥出发前往岳西的3日深度游攻略。这份行程结合了岳西的特色自然风光(明堂山/鹞落坪)与大别山红色文化底蕴,兼顾了休闲康养。
🗓️ 概览:10号-12日 · 大别山清凉避暑之旅
主题:高山康养 + 峡谷徒步 + 红色寻踪
最佳方式:自驾(最灵活,约2.5小时直达)
核心亮点:明堂山玻璃栈道、鹞落坪原始森林、禅溪温泉、岳西吊锅
🚗 第一部分:交通出行指南(合肥 ↔ 岳西)
出行方式 路线/班次详情 耗时参考 费用参考 💡 专家建议
🚙 自驾
(强烈推荐) 路线:合肥 → G4212合六叶高速 → S32铜黄高速 → 岳西出口
路况:全程高速,出高速后走X098县道至县城,路况良好。 单程约2~2.5小时 油费+过路费约60-80元/车 ✅ 首选。岳西景点分散(如明堂山、鹞落坪距县城有一段距离),自驾最方便携带行李和土特产。
🚌 城际大巴 站点:合肥客运南站 → 岳西汽车站
频次:每日约8-10班(首班06:30,末班17:00) 约2.5~3小时 票价约 55-65 元/人 ⚠️ 若需乘车,请提前在“皖美出行”小程序查票。到达后有公交或打车去景区。
🚄 高铁+接驳 步骤:合肥南→安庆站(40min) → 换乘汽车 → 岳西 全程约3.5小时 高铁70元 + 大巴60元 ❌ 不推荐。需要在安庆中转,折腾且总耗时较长,除非无车且想体验高铁。
🗺️ 第二部分:3日详细行程安排
▶ Day 1 (10号):抵达岳西 · 温泉洗尘
上午:轻松启程
建议 07:30 前 从合肥出发,避开出城早高峰。
约 10:00 抵达岳西县城,办理入住或寄存行李。
中午:舌尖上的岳西
前往老地方土菜馆或当地口碑好的农家菜馆。
🔥 必点菜单:岳西吊锅(灵魂菜品,集多种食材于一锅煮制)、黑猪汤、豆腐圆子、清炒高山野菜。
下午:入住与休整
方案A(度假风):入住县城的禅溪温泉度假村,稍作休息后直接在酒店泡温泉,消除路途疲劳。
方案B(山林风):入住明堂山景区附近民宿,感受山区清凉空气。
晚上:星空与夜色
如果不住景区,可在县城散步,品尝凉皮米豆腐、烤饼等宵夜。岳西光污染少,适合观星。
▶ Day 2 (11号):探秘大别山 · 核心自然景观
此日有两个热门选项,请根据同行人员体力二选一:
✅ 选项A:明堂山(震撼景观,适合大多数游客)
08:30 抵达明堂山风景区,乘坐索道上山(省时省力)。
10:00-13:00 游览玻璃栈道(刺激打卡)、天梯(登高望远)、探幽谷。全程游览约3-4小时。
14:00 下山后返回县城或住宿地午餐。
15:30 如果喜欢户外,可去周边的天堂湖散步;或者回酒店做SPA按摩。
🌿 选项B:鹞落坪(原始秘境,适合摄影/喜静/轻徒步者)
09:00 前往鹞落坪自然保护区(车程约1小时)。
10:00-13:00 漫步原始次生林步道,这里是华东地区保存最完好的天然氧吧之一。主要看点是竹林、溪流和珍稀植物。
特色:这里更原生态,没有太多人工开发,适合深呼吸吸氧。中午可在保护区附近的农家乐吃地道的“笋宴”。
▶ Day 3 (12号):红色记忆 · 满载而归
上午:文化与怀古
睡个懒觉,09:30 起床。
参观刘邓大军千里跃进大别山指挥部旧址(位于省立中学旧址内),免费参观,接受爱国主义教育,了解那段峥嵘岁月。
或者前往程长庚故居(徽剧奠基人故里),欣赏徽派建筑风格。
中午:伴手礼采购
在县城集市购买岳西翠兰茶叶(绿茶名品)、高山云雾茶、野生蜂蜜、干笋干、腊肉。
享用最后一顿午餐:荞面糊糊、石耳炖鸡。
下午:愉快返程
14:30 启程返回合肥。
预计 17:00前 抵达合肥,结束愉快的假期。
🎒 第三部分:行前贴士 & 避坑指南
👕 穿衣指南:
山区气温比县城低3-5度,昼夜温差大。即使是夏季,也建议带一件薄冲锋衣或长袖外套。
鞋子必须是防滑运动鞋,严禁穿高跟鞋爬山。
☔ 天气应对:
山区气候多变,建议随身携带雨衣(登山时雨伞不便)和驱蚊水。
如果遇到雨天,Day 2 的自然景点可能会因安全原因临时关闭,此时可改为“茶文化体验(河图镇龙井冲茶园)+ 游泳/水上项目”。
⚠️ 驾驶注意:
进入岳西境内后,国道和省道弯道较多,特别是雨雾天气,务必控制车速,鸣笛示警。
💰 消费预警:
景区内餐饮较贵,建议自备一些干粮和水。
购买茶叶时,不懂行的建议在正规专卖店购买,不要轻信路边拉客推销的散装茶。
祝您10号的旅程一路顺风,在大别山享受美好的时光!
八、1.0.8 vs 2.0 升级要点回顾
整个升级过程,真正要改的地方其实不多。下面按模块汇总一下,方便后面再升级时参考:
依赖变化
| 依赖项 | 1.0.8 | 2.0 |
|---|---|---|
| 核心包 | agentscope-spring-boot-starter | agentscope |
| Nacos A2A | 打包在 starter 中 | agentscope-extensions-nacos-a2a |
| A2A 客户端 | 打包在 starter 中 | agentscope-extensions-a2a-client |
| Spring Web | spring-boot-starter-web | spring-boot-starter-webflux |
| Nacos 客户端 | 通过 starter 间接引入 | 显式引入 nacos-client |
API 变化
| API | 1.0.8 | 2.0 |
|---|---|---|
| 任务分解 | .enablePlan() + PlanNotebook | .enableTaskList() |
| Skills 加载 | JarSkillRepositoryAdapter | ClasspathSkillRepository |
| DashScope 模型 | io.agentscope.core.model | io.agentscope.extensions.model |
| 结构化输出 | StructuredOutputReminder | call(msg, Class) 原生支持 |
| 流式输出 | call().block() 或 stream() | stream()(推荐) |
配置变化
# 1.0.8 — 不需要显式开启
agentscope:
a2a:
server-addr: 192.168.55.140:8848
# 2.0 — 三个 enabled 都要手动设为 true
agentscope:
a2a:
enabled: true
server:
enabled: true
nacos:
enabled: true
server-addr: 192.168.55.140:8848
没变的部分
好消息是,以下内容在 1.0.8 和 2.0 之间完全兼容,直接搬过来就行:
ReActAgent.builder()链式构建方式A2aAgent+NacosAgentCardResolver的远程 Agent 调用Toolkit+@Tool+@ToolParam的工具注册toolkit.registration().subAgent()的子 Agent 嵌套SkillBox+registerSkill()的技能加载Hook的事件监听机制Msg消息对象和PromptUtils工具类
小结
升级 2.0 的感受
升级 2.0 的过程,比预想的要顺利,也比预想的要平淡。1.0.8 手动重构时那种「每行代码都要想半天」的紧张感没有了,大部分时间花在改包名、改依赖、改 API 名字上。真正需要动脑子的,只有 Nacos 连接配置那三个 enabled 开关。
这也说明 1.0.8 的手动重构没有白费。正因为每一行代码都搞懂了,升级时才能快速判断「这个要不要改」「那个改了会不会影响别的」。如果跳过手动重构直接升级,大概率会被各种报错淹没——你不知道哪个报错是新版本的问题,哪个是你原来就没理解的。
关于 AgentScope 框架的演进
从 1.0.8 到 2.0,能看出 AgentScope 的演进方向:核心更精简,扩展更灵活。核心包只保留 ReActAgent、Toolkit、SkillBox、Hook 这些最基础的能力,DashScope 模型、Nacos A2A、A2A 客户端都拆成了独立扩展包。这种思路和 Spring Boot 的 starter 生态很像——你只引入你需要的,而不是一个庞大的全家桶。
PlanNotebook 到 enableTaskList() 的简化,JarSkillRepositoryAdapter 到 ClasspathSkillRepository 的改进,StructuredOutputReminder 的移除——这些变化的方向是一致的:让 API 更简洁、让开发体验更好。框架在往「少写配置、多写业务」的方向走,这是好事。
关于 AI 辅助编程的边界
这次升级 2.0 经历了两个阶段:先借助 AI 工具(Qoder → OpenCode)跑通,再手动重构搞懂。两个阶段的对比很能说明问题。
Qoder 能力强,但免费额度用完了;OpenCode 免费,但能力差一截,还自作主张——说是没调通 Nacos,就直接硬编码了 Agent Card 的 URL 来绕过。这不叫幻觉,这叫「偷懒式解决」:任务确实完成了,但问题没解决,只是绕过去了。127.0.0.1:8120 硬编码能跑通,但换一台机器就废了。
AI 能帮你跑通代码,但跑通≠正确。它能解决眼前的问题,但不会考虑架构的合理性、代码的可维护性。这就是 AI 辅助编程的边界:它是很好的执行者,但不是好的架构师。你需要自己判断什么是对的,什么是将就的。
先有再优
升级 2.0 有点虎头蛇尾,但我不觉得这是什么大问题。百度 MCP 暂时注释掉了,Nacos 的 enabled 配置还没完全搞明白,流式输出的 filter 逻辑还有一些注释掉的试验代码——这些都不完美,但代码跑起来了,Agent 能正常协作了,行程规划能正常返回了。先有再优,在能运行的基础上不断打磨,比追求一步到位然后卡在环境配置上强得多。
一句话总结:手动重构 1.0.8 让我搞懂了每一行代码,升级 2.0 让我看到了框架的演进方向。先有再优,代码能跑起来,才有资格谈优化。
本文是 AiTripPlan 系列的第三篇(下篇)。上篇记录了 AgentScope 1.0.8 的手动重构过程,本篇记录了从 1.0.8 升级到 2.0 的完整过程——先是借助 Qoder 和 OpenCode 跑通 2.0,再手动重构逐行搞懂。从「抄出来了」到「跑通了」再到「真懂了」,再到「升级了」,这段教程代码才算真正变成了自己的。如果你也有「能跑但没懂」的教程项目,不妨试试这个学法。感谢阅读!