手敲重构学透 Multi-Agent 代码(上):逐行拆解 AgentScope 1.0.8,从「跑通了但没懂」到真懂了

手敲重构学透 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>
<!--        &lt;!&ndash; NextDoc4j 现代化API文档,适配SpringBoot4,替代Swagger/Knife4j &ndash;&gt;-->
<!--        <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 变了什么、踩了哪些坑、虎头蛇尾又怎么收的场。感谢阅读,下篇见!

相关推荐
ESBK20251 小时前
学术邀约|CMICA 2026 第二届计算方法、智能控制与航空航天国际会议
大数据·人工智能·算法·飞行器·智能控制·航空航天·计算
能源科技集1 小时前
宜昌基地投产释放产业新动能,远景动力(AESC)790Ah超大电芯引领储能产业高质量发展
人工智能
武子康1 小时前
SWE-1.7 的提升究竟来自哪里?接受大量强化学习后训练的 Kimi K2.7 为起点,继续进行大规模 RL
人工智能·llm·agent
一根数据线1 小时前
不用无人机也不用软件,AI也能直接从地图生成3D模型?
人工智能·3d·3d建模·无人机·倾斜摄影·造形家·场地建模
Vec‑Jie1 小时前
MerchantOps-KBQA 实践(十四):本地运行、性能限制与可验证的交付边界
人工智能·后端·python·flask
苏醒的人生1 小时前
2026年支持最多全模态素材的AI视频生成工具推荐:即梦AI Seedance 2.5一次投喂50个素材,精准还原每一步
人工智能·音视频
libokaifa1 小时前
Claude Code 的工程化落地:插件(Plugin)篇
ai编程
cd_949217211 小时前
人形机器人产业深度研报:具身智能闭环成型,软硬一体Tier1龙头构筑数据飞轮终局
大数据·人工智能·机器人