手敲重构学透 Multi-Agent 代码(下):从 AgentScope 1.0.8 升级到 2.0,API 变了什么、踩了哪些坑

手敲重构学透 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 差了不少,但免费就是最大的优势。

rust 复制代码
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 中不存在:

vbnet 复制代码
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 注册中心:

yaml 复制代码
  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 一致,没有什么新增。

java 复制代码
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 改动不大,主要是包名调整。

java 复制代码
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 搞定的事,现在要手动拼。

xml 复制代码
<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 里更合理。

java 复制代码
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 变化,方便对照。

java 复制代码
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 这部分代码倒是省心了,直接搬过来就行。

java 复制代码
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。

java 复制代码
<?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 相关的部分。

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.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.enabledagentscope.a2a.server.enabledagentscope.a2a.nacos.enabled。文档里没写清楚哪些是必须的,只能靠报错反推。这种「不报错就不知道少了什么」的体验,是升级过程中最耗时间的部分。

yaml 复制代码
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/ 目录下就行,改完刷新就生效。一个小改动,开发体验好了不少。

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.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 在这一点上没有变化,说明这个设计是经得住考验的。

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.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 返回的完整行程规划:

makefile 复制代码
为您定制了这份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()(推荐)

配置变化

yaml 复制代码
# 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 生态很像------你只引入你需要的,而不是一个庞大的全家桶。

PlanNotebookenableTaskList() 的简化,JarSkillRepositoryAdapterClasspathSkillRepository 的改进,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,再手动重构逐行搞懂。从「抄出来了」到「跑通了」再到「真懂了」,再到「升级了」,这段教程代码才算真正变成了自己的。如果你也有「能跑但没懂」的教程项目,不妨试试这个学法。感谢阅读!

相关推荐
安逸Ai2 小时前
AI 编程助手如何读取、检索和修改大型代码仓库?
人工智能
码云之上2 小时前
Harness Engineering:让 Agent 在受控边界内运行
人工智能·前端框架·前端工程化
AI 小老六2 小时前
Agent Runtime 如何用 Session、Memory、User Profile 和 Skill 实现外部学习
服务器·人工智能·学习·ai·架构·自动化
aqi002 小时前
15天学会AI应用开发(十九)使用LangGraph实现持久记忆功能
人工智能·python·大模型·ai编程·ai应用
又折桃枝换酒钱2 小时前
CycleChart:一个统一的基于一致性学习的双向图表理解与生成框架(翻译与解读)
人工智能·深度学习·学习
像风一样自由20202 小时前
14.时序异常检测入门到实战: 时序大模型:不训练,直接挑异常
人工智能·时序异常检测·时间序列模型
Asize2 小时前
无状态 Stateless:大模型为什么记不住你是谁
javascript·人工智能·架构
Luhui_Dev2 小时前
AI 写论文,你敢用吗?Google 用 CoE 给结论上证据链
人工智能·agent
起个名字好难啊这也被占用了2 小时前
浏览器里跑能操作DOM的智能体
人工智能