【人工智能应用技术】-基础实战-环境搭建(基于springAI+通义千问)(二)

「鸿蒙心迹」“2025・领航者闯关记“主题征文活动 10w+人浏览 323人参与

在上一章中介绍了SpringAI 整合 通义千问 实现大模型环境接入和基本的问答实现
今天核心目标是实现一个简单的Agent应用,熟悉Agent开发的思路。

上一章内容核心逻辑是「单一轮次的 Prompt + 大模型调用」,缺少 Agent 最关键的「自主决策、工具调用、多轮规划」能力。

一、真正的 AI Agent 必须具备的核心特征

AI Agent 的核心是「自主完成复杂任务」,而不仅仅是回答问题。它需要具备以下能力(你的案例目前缺失):

Agent 相关概念参考
https://zhuanlan.zhihu.com/p/1962475257895052209

二、真正的 AI Agent 必须具备的核心特征

AI Agent 的核心是「自主完成复杂任务」,而不仅仅是回答问题。它需要具备以下能力(你的案例目前缺失):

核心能力说明你的案例现状
1. 目标拆解与规划能将用户的「复杂需求」拆解为「可执行的子任务」,并规划执行步骤❌ 仅能处理「单一问题」,无法拆解复杂需求(如 “分析今年林区火灾数据,生成防控方案并导出 Excel”)
2. 工具调用能力能自主调用外部工具(如数据库查询、文件生成、API 调用等)完成任务❌ 仅能调用大模型本身,无法集成外部工具(如查询林区实时温度、调用火灾预警 API 等)
3. 多轮交互与记忆能记住多轮对话中的上下文信息,根据用户反馈调整执行策略❌ 仅支持「单轮问答」,无法记住历史对话(如用户追问 “刚才说的方案里,洒水车的部署密度是多少”,系统无法关联上一轮回答)
4. 结果校验与重试能验证任务执行结果是否满足需求,失败时自动重试或调整方案❌ 大模型返回结果后直接返回给用户,无结果校验(如大模型回答错误 / 不完整,系统无法识别和修正)
5. 自主决策无需用户干预,自主选择执行步骤、工具、参数❌ 所有执行逻辑(模型参数、提示词模板)都是硬编码的,无自主决策空间

举个例子:如果用户问「请分析近 3 个月北京林区的火灾发生频率,结合未来 7 天的天气预报,给出针对性的防控建议」,真正的 Agent 会:

  1. 拆解任务:① 查询北京林区近 3 个月火灾数据;② 查询未来 7 天北京天气预报;③ 结合两者生成防控建议;
  2. 调用工具:① 调用「火灾数据查询 API」;② 调用「天气预报 API」;
  3. 多轮规划:如果工具返回数据不完整,会自动重试调用,或询问用户补充信息;
  4. 结果整合:将工具返回的数据整理后,调用大模型生成最终建议。

而你的案例目前只能处理「直接可回答的单一问题」(如 “35℃的林区如何防控火灾”),无法完成上述复杂任务。

三、如何将你的案例升级为真正的 AI Agent?

基于你现有的代码架构,推荐分「三步升级」,逐步具备 Agent 核心能力:

第一步:增加「多轮对话记忆」能力(基础)

让系统能记住历史对话,支持上下文关联。

核心改造:引入「对话记忆存储」(如本地缓存、Redis),在提示词中注入历史对话信息。

第二步:增加「工具调用」能力(核心)

让 Agent 能自主调用外部工具(如查询数据、生成文件),处理大模型无法直接完成的任务。核心改造:引入「工具注册与调度」机制,让大模型根据需求选择工具。

第三步:增加「任务规划与结果校验」能力(进阶)

让 Agent 能拆解复杂任务、校验执行结果,具备自主优化能力。核心改造:引入「任务规划器」和「结果校验器」,支持多步骤任务执行。

完整的 AI Agent 改造代码,包含规范目录结构、全套依赖配置、核心模块实现(多轮记忆、工具调用、任务规划),基于 Spring Boot 3.2.5 + 通义千问 SDK 2.10.0 开发,可直接运行。

在这里插入图片描述

一、最终目录结构(规范分层)

src/
├── main/
│   ├── java/
│   │   └── com/
│   │       └── example/
│   │           └── qwenagent/
│   │               ├── QwenAgentApplication.java  // 主启动类
│   │               ├── config/  // 配置模块
│   │               │   ├── DashScopeConfig.java  // 通义千问SDK配置
│   │               │   ├── DashScopeProperties.java  // 配置属性绑定
│   │               │   └── RedisConfig.java  // Redis缓存配置(对话记忆用)
│   │               ├── core/  // Agent核心模块
│   │               │   ├── agent/
│   │               │   │   ├── FireProtectionAgent.java  // 森林防火Agent主类
│   │               │   │   ├── TaskPlanner.java  // 任务规划器
│   │               │   │   └── ResultValidator.java  // 结果校验器
│   │               │   ├── memory/
│   │               │   │   ├── ConversationMemory.java  // 对话记忆模型
│   │               │   │   └── ConversationMemoryManager.java  // 记忆管理(Redis实现)
│   │               │   └── tool/
│   │               │       ├── Tool.java  // 工具接口
│   │               │       ├── ToolDispatcher.java  // 工具调度器
│   │               │       ├── FireDataQueryTool.java  // 火灾数据查询工具
│   │               │       └── WeatherQueryTool.java  // 天气预报查询工具
│   │               ├── controller/  // 接口层
│   │               │   └── AgentController.java  // Agent交互接口
│   │               └── util/  // 工具类
│   │                   └── JsonUtil.java  // JSON序列化工具
│   └── resources/
│       └── application.yml  // 配置文件
└── pom.xml  // Maven依赖

二、全套依赖配置(pom.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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.2.5</version>
		<relativePath/>
	</parent>
	<groupId>com.example</groupId>
	<artifactId>qwen-agent-demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>通义千问森林防火Agent</name>
	<description>基于Spring Boot 3.x + 通义千问SDK的AI Agent案例(森林防火领域)</description>

	<properties>
		<java.version>17</java.version>
		<spring-ai.version>1.0.0-M6</spring-ai.version>
		<dashscope-sdk.version>2.10.0</dashscope-sdk.version>
		<gson.version>2.10.1</gson.version>
		<lombok.version>1.18.32</lombok.version>
		<fastjson2.version>2.0.48</fastjson2.version>
		<spring-boot-starter-redis.version>3.2.5</spring-boot-starter-redis.version>
	</properties>

	<dependencies>
		<!-- Spring Web 核心(HTTP接口) -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<!-- Spring Validation(参数校验) -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-validation</artifactId>
		</dependency>

		<!-- Redis(对话记忆存储) -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
			<version>${spring-boot-starter-redis.version}</version>
		</dependency>

		<!-- Spring AI 核心 -->
		<dependency>
			<groupId>org.springframework.ai</groupId>
			<artifactId>spring-ai-core</artifactId>
			<version>${spring-ai.version}</version>
		</dependency>

		<!-- 通义千问SDK -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>dashscope-sdk-java</artifactId>
			<version>${dashscope-sdk.version}</version>
			<exclusions>
				<exclusion>
					<groupId>org.slf4j</groupId>
					<artifactId>slf4j-simple</artifactId>
				</exclusion>
			</exclusions>
		</dependency>

		<!-- Gson(SDK序列化) -->
		<dependency>
			<groupId>com.google.code.gson</groupId>
			<artifactId>gson</artifactId>
			<version>${gson.version}</version>
		</dependency>

		<!-- FastJSON2(JSON处理) -->
		<dependency>
			<groupId>com.alibaba.fastjson2</groupId>
			<artifactId>fastjson2</artifactId>
			<version>${fastjson2.version}</version>
		</dependency>

		<!-- Lombok(简化代码) -->
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>${lombok.version}</version>
			<optional>true</optional>
		</dependency>

		<!-- Spring Boot 测试 -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<!-- 支持 Java 8 日期时间类型(LocalDateTime、LocalDate 等)序列化 -->
		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
		</dependency>
	</dependencies>

	<!-- 仓库配置(国内加速) -->
	<repositories>
		<repository>
			<id>maven-central</id>
			<url>https://repo1.maven.org/maven2/</url>
			<releases><enabled>true</enabled></releases>
			<snapshots><enabled>false</enabled></snapshots>
		</repository>
		<repository>
			<id>aliyun-maven</id>
			<url>https://maven.aliyun.com/repository/public</url>
			<releases><enabled>true</enabled></releases>
			<snapshots><enabled>true</enabled></snapshots>
		</repository>
	</repositories>

	<pluginRepositories>
		<pluginRepository>
			<id>aliyun-maven</id>
			<url>https://maven.aliyun.com/repository/public</url>
		</pluginRepository>
	</pluginRepositories>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

三、核心配置文件(application.yml)

# 服务器配置
server:
  port: 8080
  servlet:
    context-path: /agent

# 通义千问配置
dashscope:
  api-key: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx  # 替换为你的API Key
  model: qwen-turbo  # 模型:qwen-turbo/qwen-plus/qwen-max
  http-base-url: https://dashscope.aliyuncs.com/api/v1
  websocket-base-url: wss://dashscope.aliyuncs.com/api-ws/v1/inference/
  temperature: 0.3  # 工具选择/任务规划时降低随机性
  max-tokens: 2000  # 最大生成Token数
  task-plan-temperature: 0.2  # 任务规划专用温度(更稳定)
  result-validate-temperature: 0.2  # 结果校验专用温度

# Redis配置(对话记忆存储)
spring:
  data:
    redis:
      host: localhost  # 本地Redis(生产环境替换为实际地址)
      port: 6379
      password:  # 无密码留空
      database: 0
      timeout: 3000ms
      lettuce:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 2

# 日志配置
logging:
  level:
    root: INFO
    com.example.qwenagent: DEBUG
    com.alibaba.dashscope: WARN
    org.springframework.data.redis: WARN
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n"

四、核心代码实现

1. 配置模块(config)

DashScopeProperties.java(配置属性绑定)
package com.example.qwenagent.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.PositiveOrZero;

@Data
@Validated
@Component
@ConfigurationProperties(prefix = "dashscope")
public class DashScopeProperties {

    @NotBlank(message = "通义千问API Key不能为空")
    private String apiKey;

    @NotBlank(message = "模型名称不能为空")
    private String model;

    @NotBlank(message = "HTTP端点地址不能为空")
    private String httpBaseUrl;

    @NotBlank(message = "WebSocket端点地址不能为空")
    private String websocketBaseUrl;

    @PositiveOrZero(message = "temperature必须大于等于0")
    private Float temperature = 0.3f;

    @Positive(message = "max-tokens必须大于0")
    private Integer maxTokens = 2000;

    @PositiveOrZero(message = "task-plan-temperature必须大于等于0")
    private Float taskPlanTemperature = 0.2f;

    @PositiveOrZero(message = "result-validate-temperature必须大于等于0")
    private Float resultValidateTemperature = 0.2f;
}
DashScopeConfig.java(SDK 初始化配置)
package com.example.qwenagent.config;

import com.alibaba.dashscope.utils.Constants;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;

@Slf4j
@Configuration
@RequiredArgsConstructor
public class DashScopeConfig {

    private final DashScopeProperties dashScopeProperties;

    @PostConstruct
    public void initDashScope() {
        // 校验核心配置
        Assert.hasText(dashScopeProperties.getApiKey(), "API Key未配置");
        Assert.hasText(dashScopeProperties.getHttpBaseUrl(), "HTTP端点未配置");

        // 初始化SDK全局配置
        Constants.apiKey = dashScopeProperties.getApiKey();
        Constants.baseHttpApiUrl = dashScopeProperties.getHttpBaseUrl();
        Constants.baseWebsocketApiUrl = dashScopeProperties.getWebsocketBaseUrl();

        // 超时配置
        Constants.CONNECT_TIMEOUT = 15000; // 15秒
        Constants.SOCKET_TIMEOUT = 60000; // 60秒

        // 日志输出(脱敏API Key)
        log.info("通义千问SDK初始化成功!模型:{},API Key:{}",
                dashScopeProperties.getModel(),
                maskApiKey(dashScopeProperties.getApiKey()));
    }

    // API Key脱敏(前6后4)
    private String maskApiKey(String apiKey) {
        if (apiKey.length() < 10) return apiKey;
        return apiKey.substring(0, 6) + "******" + apiKey.substring(apiKey.length() - 4);
    }
}
RedisConfig.java(Redis 缓存配置)
package com.example.qwenagent.config;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 1. 配置 Jackson 序列化器(支持 Java 8 日期时间)
        ObjectMapper objectMapper = new ObjectMapper();
        // 注册 JSR310 模块(关键:支持 LocalDateTime、LocalDate 等)
        objectMapper.registerModule(new JavaTimeModule());
        // 关闭日期时间序列化的时间戳模式(可选:按 ISO 格式序列化,如 "2025-12-08T10:00:00")
        objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
        // 忽略未知字段(避免反序列化时因字段不一致报错)
        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        // 2. 配置 Redis 序列化器
        GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);

        // Key 序列化:String 类型(必须)
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());

        // Value 序列化:JSON 类型(支持对象+日期时间)
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);

        template.afterPropertiesSet();
        return template;
    }
}

2. 核心模块(core)

记忆模块(memory)
ConversationMemory.java(对话记忆模型)
package com.example.qwenagent.core.memory;

import lombok.Data;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Data
public class ConversationMemory {
    private String conversationId; // 对话唯一ID
    private List<Message> messages = new ArrayList<>(); // 历史消息
    private LocalDateTime createTime; // 创建时间
    private LocalDateTime updateTime; // 最后更新时间

    public ConversationMemory(String conversationId) {
        this.conversationId = conversationId;
        this.createTime = LocalDateTime.now();
        this.updateTime = LocalDateTime.now();
    }

    // 添加消息
    public void addMessage(String role, String content) {
        this.messages.add(new Message(role, content));
        this.updateTime = LocalDateTime.now();
    }

    // 生成历史对话Prompt
    public String getHistoryPrompt() {
        StringBuilder sb = new StringBuilder();
        sb.append("历史对话记录:\n");
        for (Message msg : messages) {
            sb.append(String.format("[%s]:%s\n", msg.getRole(), msg.getContent()));
        }
        return sb.toString();
    }

    // 消息内部类(role:user/assistant/system)
    @Data
    public static class Message {
        private String role;
        private String content;
        private LocalDateTime timestamp;

        public Message(String role, String content) {
            this.role = role;
            this.content = content;
            this.timestamp = LocalDateTime.now();
        }
    }
}
ConversationMemoryManager.java(记忆管理,Redis 实现)
package com.example.qwenagent.core.memory;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.time.Duration;

@Slf4j
@Component
@RequiredArgsConstructor
public class ConversationMemoryManager {

    private final RedisTemplate<String, Object> redisTemplate;
    private static final String REDIS_KEY_PREFIX = "qwen:agent:conversation:";
    private static final Duration EXPIRATION = Duration.ofHours(24); // 对话记忆24小时过期

    // 获取或创建对话记忆
    public ConversationMemory getOrCreateMemory(String conversationId) {
        String redisKey = getRedisKey(conversationId);
        ConversationMemory memory = (ConversationMemory) redisTemplate.opsForValue().get(redisKey);

        if (memory == null) {
            memory = new ConversationMemory(conversationId);
            saveMemory(memory);
            log.debug("创建新对话记忆,ID:{}", conversationId);
        } else {
            log.debug("加载已有对话记忆,ID:{},消息数:{}", conversationId, memory.getMessages().size());
        }
        return memory;
    }

    // 保存对话记忆(更新过期时间)
    public void saveMemory(ConversationMemory memory) {
        String redisKey = getRedisKey(memory.getConversationId());
        redisTemplate.opsForValue().set(redisKey, memory, EXPIRATION);
        log.debug("保存对话记忆,ID:{}", memory.getConversationId());
    }

    // 删除对话记忆
    public void deleteMemory(String conversationId) {
        String redisKey = getRedisKey(conversationId);
        redisTemplate.delete(redisKey);
        log.debug("删除对话记忆,ID:{}", conversationId);
    }

    // 构建Redis Key
    private String getRedisKey(String conversationId) {
        return REDIS_KEY_PREFIX + conversationId;
    }
}
工具模块(tool)
Tool.java(工具接口)
package com.example.qwenagent.core.tool;

/**
 * 工具接口:所有外部工具需实现此接口
 */
public interface Tool {
    /** 工具唯一名称(供Agent识别) */
    String getName();

    /** 工具描述(供Agent判断是否使用) */
    String getDescription();

    /** 工具参数说明(JSON格式示例) */
    String getParamExample();

    /** 执行工具调用 */
    String execute(String params);
}
ToolDispatcher.java(工具调度器)
package com.example.qwenagent.core.tool;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Component
@RequiredArgsConstructor
public class ToolDispatcher {

    // 存储所有工具(key:工具名称)
    private final Map<String, Tool> toolMap = new ConcurrentHashMap<>();

    // 构造器注入所有Tool实现类(Spring自动扫描)
    public ToolDispatcher(List<Tool> toolList) {
        for (Tool tool : toolList) {
            toolMap.put(tool.getName(), tool);
            log.info("注册工具:{},描述:{}", tool.getName(), tool.getDescription());
        }
    }

    // 获取所有工具的说明(供Agent选择)
    public String getToolsInstruction() {
        StringBuilder sb = new StringBuilder();
        sb.append("===== 可用工具列表 =====\n");
        for (Tool tool : toolMap.values()) {
            sb.append(String.format("工具名称:%s\n", tool.getName()));
            sb.append(String.format("功能描述:%s\n", tool.getDescription()));
            sb.append(String.format("参数示例:%s\n\n", tool.getParamExample()));
        }
        sb.append("===== 调用规则 =====\n");
        sb.append("1. 若需要使用工具,返回JSON格式:{\"toolName\":\"工具名称\",\"params\":\"参数JSON字符串\"}\n");
        sb.append("2. 若无需工具,直接返回回答内容\n");
        sb.append("3. 参数必须严格匹配示例格式,否则工具调用失败\n");
        return sb.toString();
    }

    // 执行工具调用
    public String dispatchTool(String toolName, String params) {
        Tool tool = toolMap.get(toolName);
        if (tool == null) {
            String errorMsg = "工具调用失败:不存在名称为【" + toolName + "】的工具";
            log.error(errorMsg);
            return errorMsg;
        }

        try {
            log.debug("调用工具:{},参数:{}", toolName, params);
            return tool.execute(params);
        } catch (Exception e) {
            String errorMsg = String.format("工具【%s】调用异常:%s", toolName, e.getMessage());
            log.error(errorMsg, e);
            return errorMsg;
        }
    }
}
FireDataQueryTool.java(火灾数据查询工具)
package com.example.qwenagent.core.tool;

import com.alibaba.fastjson2.JSONObject;
import org.springframework.stereotype.Component;

/**
 * 模拟:林区火灾数据查询工具(实际可对接真实数据库/API)
 */
@Component
public class FireDataQueryTool implements Tool {

    @Override
    public String getName() {
        return "fire_data_query";
    }

    @Override
    public String getDescription() {
        return "查询指定地区、指定时间范围的林区火灾发生数据,包括火灾次数、原因、影响范围";
    }

    @Override
    public String getParamExample() {
        return "{\"region\":\"北京\",\"startTime\":\"2025-01-01\",\"endTime\":\"2025-03-31\"}";
    }

    @Override
    public String execute(String params) {
        // 解析参数
        JSONObject paramJson = JSONObject.parseObject(params);
        String region = paramJson.getString("region");
        String startTime = paramJson.getString("startTime");
        String endTime = paramJson.getString("endTime");

        // 模拟查询结果(实际场景替换为真实数据查询)
        return String.format("===== 林区火灾数据查询结果 =====\n" +
                        "查询地区:%s\n" +
                        "时间范围:%s 至 %s\n" +
                        "火灾发生次数:3次\n" +
                        "主要原因:高温干旱(2次)、人为用火(1次)\n" +
                        "影响范围:累计影响林区面积8.2公顷\n" +
                        "处置结果:均在2小时内扑灭,无人员伤亡",
                region, startTime, endTime);
    }
}
WeatherQueryTool.java(天气预报查询工具)
package com.example.qwenagent.core.tool;

import com.alibaba.fastjson2.JSONObject;
import org.springframework.stereotype.Component;

/**
 * 模拟:天气预报查询工具(实际可对接气象局API)
 */
@Component
public class WeatherQueryTool implements Tool {

    @Override
    public String getName() {
        return "weather_query";
    }

    @Override
    public String getDescription() {
        return "查询指定地区未来7天的天气预报,包括气温、降水概率、风力,用于火灾风险评估";
    }

    @Override
    public String getParamExample() {
        return "{\"region\":\"北京\"}";
    }

    @Override
    public String execute(String params) {
        JSONObject paramJson = JSONObject.parseObject(params);
        String region = paramJson.getString("region");

        // 模拟天气预报结果
        return String.format("===== 未来7天天气预报 =====\n" +
                        "查询地区:%s\n" +
                        "日期范围:2025-04-01 至 2025-04-07\n" +
                        "气温范围:18℃~32℃(4月3日最高温32℃)\n" +
                        "降水概率:均低于10%(全周无有效降雨)\n" +
                        "风力:2-3级西北风\n" +
                        "火灾风险:高(高温、干旱、低湿度)",
                region);
    }
}
Agent 核心(agent)
TaskPlanner.java(任务规划器)
package com.example.qwenagent.core.agent;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.exception.ApiException;
import com.example.qwenagent.config.DashScopeProperties;
import com.example.qwenagent.core.tool.ToolDispatcher;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson2.JSONArray;

import java.util.List;

/**
 * 任务规划器:将用户复杂任务拆解为可执行的子步骤
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TaskPlanner {

    private final DashScopeProperties dashScopeProperties;
    private final ToolDispatcher toolDispatcher;
    private final Generation generationClient = new Generation();

    /**
     * 拆解复杂任务
     * @param task 用户原始任务(如"分析北京近3个月火灾数据+未来7天天气,给防控建议")
     * @return 子步骤列表(如["调用火灾数据查询工具","调用天气预报工具","生成防控建议"])
     */
    public List<String> planTask(String task) {
        String prompt = buildPlanPrompt(task);

        try {
            GenerationParam param = GenerationParam.builder()
                    .model(dashScopeProperties.getModel())
                    .prompt(prompt)
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .temperature(dashScopeProperties.getTaskPlanTemperature())
                    .maxTokens(1000)
                    .build();

            GenerationResult result = generationClient.call(param);
            String planJson = result.getOutput().getChoices().get(0).getMessage().getContent().trim();
            log.debug("任务规划结果JSON:{}", planJson);

            // 解析JSON为子步骤列表
            return JSONArray.parseArray(planJson, String.class);
        } catch (ApiException e) {
            log.error("任务规划失败:{}", e.getMessage(), e);
            return List.of("直接回答用户问题:" + task);
        } catch (Exception e) {
            log.error("任务规划异常:{}", e.getMessage(), e);
            return List.of("直接回答用户问题:" + task);
        }
    }

    // 构建任务规划Prompt
    private String buildPlanPrompt(String task) {
        return String.format("""
                你是专业的森林防火任务规划专家,需要将用户的复杂任务拆解为可执行的子步骤。
                核心要求:
                1. 子步骤必须具体、可落地,每个步骤只能是"直接回答"或"调用某个工具";
                2. 若需要调用工具,必须使用提供的工具列表,步骤描述格式:"调用工具【工具名称】,参数:参数示例";
                3. 步骤顺序合理,前一步的结果为后一步的输入;
                4. 仅返回JSON格式的步骤列表,无需额外说明,格式:["步骤1","步骤2",...];
                5. 不需要的步骤坚决不添加,避免冗余。

                用户复杂任务:%s
                %s
                """, task, toolDispatcher.getToolsInstruction());
    }
}
ResultValidator.java(结果校验器)
package com.example.qwenagent.core.agent;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.exception.ApiException;
import com.example.qwenagent.config.DashScopeProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

/**
 * 结果校验器:验证Agent最终回答是否满足用户需求
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class ResultValidator {

    private final DashScopeProperties dashScopeProperties;
    private final Generation generationClient = new Generation();

    /**
     * 校验结果是否满足需求
     * @param task 用户原始任务
     * @param result Agent最终回答
     * @return true:满足;false:不满足
     */
    public boolean validate(String task, String result) {
        String prompt = buildValidatePrompt(task, result);

        try {
            GenerationParam param = GenerationParam.builder()
                    .model(dashScopeProperties.getModel())
                    .prompt(prompt)
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .temperature(dashScopeProperties.getResultValidateTemperature())
                    .maxTokens(500)
                    .build();

            GenerationResult validateResult = generationClient.call(param);
            String validateContent = validateResult.getOutput().getChoices().get(0).getMessage().getContent().trim();
            log.debug("结果校验反馈:{}", validateContent);

            // 只要包含"满足"则认为校验通过
            return validateContent.contains("满足");
        } catch (ApiException e) {
            log.error("结果校验失败:{}", e.getMessage(), e);
            return false;
        } catch (Exception e) {
            log.error("结果校验异常:{}", e.getMessage(), e);
            return false;
        }
    }

    // 构建结果校验Prompt
    private String buildValidatePrompt(String task, String result) {
        return String.format("""
                你是结果校验专家,负责判断AI的回答是否满足用户的任务需求。
                校验标准:
                1. 完整性:回答是否完整覆盖用户任务的所有要求;
                2. 准确性:回答内容是否专业、准确,无错误信息;
                3. 实用性:回答是否具备实际操作价值,而非空泛理论。

                用户任务:%s
                AI回答:%s

                输出要求:
                1. 先明确返回"满足"或"不满足";
                2. 后简要说明原因(不超过50字);
                3. 无需其他额外内容。
                """, task, result);
    }
}
FireProtectionAgent.java(Agent 主类)
package com.example.qwenagent.core.agent;

import com.alibaba.dashscope.aigc.generation.Generation;
import com.alibaba.dashscope.aigc.generation.GenerationParam;
import com.alibaba.dashscope.aigc.generation.GenerationResult;
import com.alibaba.dashscope.exception.ApiException;
import com.example.qwenagent.config.DashScopeProperties;
import com.example.qwenagent.core.memory.ConversationMemory;
import com.example.qwenagent.core.memory.ConversationMemoryManager;
import com.example.qwenagent.core.tool.ToolDispatcher;
import com.alibaba.fastjson2.JSONObject;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.UUID;

/**
 * 森林防火AI Agent:整合记忆、工具、规划、校验能力
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class FireProtectionAgent {

    private final DashScopeProperties dashScopeProperties;
    private final ConversationMemoryManager memoryManager;
    private final ToolDispatcher toolDispatcher;
    private final TaskPlanner taskPlanner;
    private final ResultValidator resultValidator;
    private final Generation generationClient = new Generation();

    /**
     * Agent核心入口:处理用户请求(支持单轮/多轮、简单/复杂任务)
     * @param conversationId 对话ID(为空则自动生成)
     * @param userInput 用户输入(问题/任务)
     * @return Agent最终回答
     */
    public String handleUserRequest(String conversationId, String userInput) {
        // 1. 初始化对话ID和记忆
        if (conversationId == null || conversationId.isBlank()) {
            conversationId = UUID.randomUUID().toString().replace("-", "");
            log.debug("生成新对话ID:{}", conversationId);
        }
        ConversationMemory memory = memoryManager.getOrCreateMemory(conversationId);
        memory.addMessage("user", userInput);

        try {
            // 2. 判断任务类型:简单问题(直接回答)或复杂任务(需要规划)
            if (isSimpleQuestion(userInput)) {
                // 简单问题:直接调用大模型回答(带历史记忆)
                String answer = directAnswer(memory);
                memory.addMessage("assistant", answer);
                memoryManager.saveMemory(memory);
                return wrapResult(conversationId, answer);
            } else {
                // 复杂任务:规划→执行→校验→反馈
                return handleComplexTask(conversationId, memory, userInput);
            }
        } catch (Exception e) {
            String errorMsg = "Agent处理请求异常:" + e.getMessage();
            log.error(errorMsg, e);
            memory.addMessage("system", errorMsg);
            memoryManager.saveMemory(memory);
            return wrapResult(conversationId, errorMsg);
        }
    }

    /**
     * 判断是否为简单问题(无需工具/规划,直接回答)
     */
    private boolean isSimpleQuestion(String userInput) {
        String prompt = String.format("""
                判断用户输入是否为简单问题(无需调用工具、无需拆解步骤,可直接回答)。
                简单问题示例:"35℃林区如何防控火灾"、"火灾逃生技巧";
                复杂任务示例:"分析北京近3个月火灾数据+未来7天天气,给防控建议"。
                输出要求:仅返回"是"或"否",无需其他内容。
                用户输入:%s
                """, userInput);

        try {
            GenerationParam param = GenerationParam.builder()
                    .model(dashScopeProperties.getModel())
                    .prompt(prompt)
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .temperature(0.1f)
                    .maxTokens(10)
                    .build();

            GenerationResult result = generationClient.call(param);
            String decision = result.getOutput().getChoices().get(0).getMessage().getContent().trim();
            return "是".equals(decision);
        } catch (Exception e) {
            log.error("判断任务类型异常,默认按复杂任务处理", e);
            return false;
        }
    }

    /**
     * 直接回答简单问题(带历史对话记忆)
     */
    private String directAnswer(ConversationMemory memory) {
        String prompt = buildDirectAnswerPrompt(memory);

        GenerationParam param = GenerationParam.builder()
                .model(dashScopeProperties.getModel())
                .prompt(prompt)
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .temperature(dashScopeProperties.getTemperature())
                .maxTokens(dashScopeProperties.getMaxTokens())
                .build();

        try {
            GenerationResult result = generationClient.call(param);
            return result.getOutput().getChoices().get(0).getMessage().getContent().trim();
        } catch (ApiException e) {
            return "大模型调用失败:" + e.getMessage();
        }
    }

    /**
     * 处理复杂任务(规划→执行→校验)
     */
    private String handleComplexTask(String conversationId, ConversationMemory memory, String task) {
        // 1. 任务规划:拆解为子步骤
        List<String> subTasks = taskPlanner.planTask(task);
        log.info("复杂任务拆解结果,对话ID:{},子步骤:{}", conversationId, subTasks);
        memory.addMessage("system", "任务拆解为:" + subTasks);

        // 2. 执行子步骤(调用工具/直接回答)
        for (String subTask : subTasks) {
            String subResult = executeSubTask(subTask, memory);
            memory.addMessage("system", String.format("子步骤【%s】执行结果:%s", subTask, subResult));
        }

        // 3. 生成最终回答
        String finalAnswer = generateFinalAnswer(memory);
        log.debug("复杂任务最终回答,对话ID:{},内容:{}", conversationId, finalAnswer);

        // 4. 结果校验
        boolean isValid = resultValidator.validate(task, finalAnswer);
        if (isValid) {
            memory.addMessage("assistant", finalAnswer);
            memoryManager.saveMemory(memory);
            return wrapResult(conversationId, finalAnswer);
        } else {
            String feedback = "⚠️  当前回答未完全满足你的需求,建议补充以下信息:\n1. 具体地区\n2. 时间范围\n3. 其他特殊要求";
            memory.addMessage("assistant", feedback);
            memoryManager.saveMemory(memory);
            return wrapResult(conversationId, feedback);
        }
    }

    /**
     * 执行单个子步骤
     */
    private String executeSubTask(String subTask, ConversationMemory memory) {
        // 判断子步骤是否需要调用工具
        if (subTask.contains("调用工具")) {
            // 让大模型生成工具调用参数
            String toolCallPrompt = buildToolCallPrompt(subTask, memory);
            GenerationParam param = GenerationParam.builder()
                    .model(dashScopeProperties.getModel())
                    .prompt(toolCallPrompt)
                    .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                    .temperature(0.1f)
                    .maxTokens(500)
                    .build();

            try {
                GenerationResult result = generationClient.call(param);
                String toolCallJson = result.getOutput().getChoices().get(0).getMessage().getContent().trim();
                JSONObject toolJson = JSONObject.parseObject(toolCallJson);
                String toolName = toolJson.getString("toolName");
                String params = toolJson.getString("params");
                // 调用工具
                return toolDispatcher.dispatchTool(toolName, params);
            } catch (Exception e) {
                return "子步骤执行失败:" + e.getMessage();
            }
        } else {
            // 无需工具,直接生成子步骤结果
            return directAnswer(memory);
        }
    }

    /**
     * 生成复杂任务的最终回答
     */
    private String generateFinalAnswer(ConversationMemory memory) {
        String prompt = buildFinalAnswerPrompt(memory);

        GenerationParam param = GenerationParam.builder()
                .model(dashScopeProperties.getModel())
                .prompt(prompt)
                .resultFormat(GenerationParam.ResultFormat.MESSAGE)
                .temperature(dashScopeProperties.getTemperature())
                .maxTokens(dashScopeProperties.getMaxTokens())
                .build();

        try {
            GenerationResult result = generationClient.call(param);
            return result.getOutput().getChoices().get(0).getMessage().getContent().trim();
        } catch (ApiException e) {
            return "生成最终回答失败:" + e.getMessage();
        }
    }

    /**
     * 构建简单问题回答Prompt
     */
    private String buildDirectAnswerPrompt(ConversationMemory memory) {
        return String.format("""
                你是资深森林防火专家,严格遵循以下要求回答:
                1. 专业性:基于行业规范和科学知识,拒绝不专业内容;
                2. 简洁性:控制在300字以内,直击要点;
                3. 实用性:给出可操作建议,优先强调人员安全;
                4. 连贯性:结合历史对话上下文。

                %s
                请回答用户最新问题:%s
                """, memory.getHistoryPrompt(), memory.getMessages().get(memory.getMessages().size() - 1).getContent());
    }

    /**
     * 构建工具调用Prompt
     */
    private String buildToolCallPrompt(String subTask, ConversationMemory memory) {
        return String.format("""
                请根据子步骤和历史对话,生成工具调用JSON(严格按要求格式)。
                %s
                当前子步骤:%s
                %s
                """, memory.getHistoryPrompt(), subTask, toolDispatcher.getToolsInstruction());
    }

    /**
     * 构建复杂任务最终回答Prompt
     */
    private String buildFinalAnswerPrompt(ConversationMemory memory) {
        return String.format("""
                你是森林防火专家,需要基于以下信息生成最终回答:
                1. 整合所有子步骤执行结果;
                2. 结构清晰,分点说明(最多3点);
                3. 重点突出防控建议的可操作性;
                4. 语言专业、简洁,控制在500字以内。

                %s
                请生成最终的森林防火建议:
                """, memory.getHistoryPrompt());
    }

    /**
     * 包装返回结果(包含对话ID,用于多轮对话)
     */
    private String wrapResult(String conversationId, String content) {
        return String.format("📢 对话ID:%s\n\n%s", conversationId, content);
    }
}

3. 接口层(controller)

AgentController.java(Agent 交互接口)
package com.example.qwenagent.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import com.example.qwenagent.core.agent.FireProtectionAgent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import jakarta.validation.constraints.NotBlank;

@Slf4j
@RestController
@RequestMapping("/v1")
@RequiredArgsConstructor
@Tag(name = "森林防火AI Agent", description = "支持简单问答、复杂任务规划的AI Agent接口")
public class AgentController {

    private final FireProtectionAgent fireProtectionAgent;

    /**
     * Agent交互接口(支持GET请求,便于测试)
     * 访问示例:http://localhost:8080/agent/v1/chat?userInput=分析北京近3个月火灾数据和未来7天天气,给防控建议
     */
    @GetMapping("/chat")
    @Operation(
            summary = "Agent交互接口",
            description = "输入问题或复杂任务,Agent自动处理(支持多轮对话,需传递conversationId)",
            parameters = {
                    @Parameter(name = "conversationId", description = "对话ID(首次调用可空,自动生成)", required = false),
                    @Parameter(name = "userInput", description = "用户输入(问题/任务)", required = true)
            }
    )
    public ResponseEntity<String> chat(
            @RequestParam(required = false) String conversationId,
            @RequestParam(required = true) @NotBlank(message = "用户输入不能为空") String userInput) {
        log.info("收到Agent请求,对话ID:{},用户输入:{}", conversationId, userInput);
        try {
            String result = fireProtectionAgent.handleUserRequest(conversationId, userInput);
            return ResponseEntity.ok(result);
        } catch (IllegalArgumentException e) {
            log.warn("参数错误:{}", e.getMessage());
            return ResponseEntity.badRequest().body("❌ " + e.getMessage());
        } catch (Exception e) {
            log.error("Agent接口异常", e);
            return ResponseEntity.internalServerError().body("❌ 服务暂时不可用,请稍后重试");
        }
    }
}

4. 工具类(util)

JsonUtil.java(JSON 工具类)
package com.example.qwenagent.util;

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;

@Slf4j
public class JsonUtil {

    /**
     * 对象转JSON字符串
     */
    public static String toJson(Object obj) {
        try {
            return JSON.toJSONString(obj);
        } catch (Exception e) {
            log.error("对象转JSON失败", e);
            return "";
        }
    }

    /**
     * JSON字符串转对象
     */
    public static <T> T fromJson(String json, Class<T> clazz) {
        try {
            return JSON.parseObject(json, clazz);
        } catch (Exception e) {
            log.error("JSON转对象失败,JSON:{}", json, e);
            return null;
        }
    }

    /**
     * 解析JSON字符串获取字段值
     */
    public static String getField(String json, String fieldName) {
        try {
            JSONObject jsonObject = JSON.parseObject(json);
            return jsonObject.getString(fieldName);
        } catch (Exception e) {
            log.error("解析JSON字段失败,JSON:{},字段:{}", json, fieldName, e);
            return null;
        }
    }
}

5. 主启动类(QwenAgentApplication.java)

package com.example.qwenagent;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class QwenAgentApplication {

    public static void main(String[] args) {
        SpringApplication.run(QwenAgentApplication.class, args);
        System.out.println("""
                ==============================================
                通义千问森林防火AI Agent 启动成功!
                访问地址:http://localhost:8080/agent/v1/chat
                示例请求:http://localhost:8080/agent/v1/chat?userInput=35℃林区如何防控火灾
                ==============================================
                """);
    }
}

五、运行说明

1. 环境准备

  1. 安装 JDK 17+
  2. 安装 Redis(本地运行,无需额外配置,默认端口 6379)
  3. 替换 application.yml 中的 dashscope.api-key 为你的通义千问 API Key(从阿里云获取)

2. 启动步骤

  1. 编译项目:mvn clean package
  2. 运行主启动类 QwenAgentApplication.java
  3. 验证启动:控制台输出启动成功提示,Redis 中生成对话记忆 Key

3. 测试示例

示例 1:简单问答(无需工具)

请求地址:

http://localhost:8080/agent/v1/chat?userInput=35℃的林区如何防控火灾

返回结果:

📢 对话ID:f47ac10b19674b2da635c87681234567

1. 加强巡查:增加日间高温时段(10:00-16:00)巡查频次,重点排查违规用火;
2. 水分补给:对林区边缘植被洒水保湿,降低易燃性;
3. 预警宣传:通过广播、警示牌提醒进入林区人员禁止吸烟、野炊。

在这里插入图片描述

示例 2:复杂任务(调用工具 + 规划)

请求地址:

http://localhost:8080/agent/v1/chat?userInput=分析北京近3个月火灾数据和未来7天天气,给出针对性的防控建议

返回结果:

📢 对话ID:a1b2c3d4e5f64a5b9c8d7e6f5a4b3c2d

===== 北京林区森林防火专项建议 =====
1. 重点时段防控:针对未来7天32℃高温、无降雨的天气,10:00-16:00实行"每2小时巡查"制度,配置无人机空中巡检;
2. 火源管控:近3个月3起火灾中2起因高温干旱,1起为人为用火,需在林区入口增设火源检查点,没收火种;
3. 应急准备:在火灾高发区域(累计影响8.2公顷的区域)提前部署洒水车和灭火队伍,确保2小时内响应。

在这里插入图片描述

六、Agent 核心能力总结

核心能力实现说明
多轮对话记忆基于 Redis 存储对话历史,支持上下文关联
任务规划自动拆解复杂任务为可执行子步骤
工具调用支持多工具注册与自动调度(火灾数据、天气预报)
结果校验验证回答是否满足需求,不满足则提示补充信息
异常处理全链路异常兜底,返回友好提示
配置化模型参数、工具、缓存等可通过配置文件调整

该 Agent 可直接部署使用,也可基于此扩展更多功能(如新增工具、优化提示词、集成数据库等)。

要使用 Spring AI 和 OpenAI 接入阿里云百炼大模型通义,由于百炼的通义模型遵循了 OpenAI 规范,可通过引入 OpenAI 的依赖来接入。不过,若要接入阿里云百炼模型,更推荐使用 Spring AI Alibaba,它是 Spring AI 的阿里云增强版,是通义、百炼平台与 Spring Cloud Alibaba 深度融合的产物,能让 Java 开发者像写 REST API 一样轻松集成 AI 能力,但如果非要用 Spring AI 接入,可采用引入 OpenAI 依赖的方式接入百炼大模型 [^1][^2]。 以下是一个简单的示例思路(实际代码需要根据具体情况调整): ```java // 假设这是一个 Java 项目,首先需要在项目中添加 OpenAI 相关依赖 // 以下是 Maven 依赖示例 <dependency> <groupId>com.theokanning.openai-gpt3-java</groupId> <artifactId>service</artifactId> <version>0.11.0</version> </dependency> import com.theokanning.openai.OpenAiService; import com.theokanning.openai.completion.CompletionRequest; import com.theokanning.openai.completion.CompletionResult; public class SpringAIOpenAIIntegration { public static void main(String[] args) { // 设置 API 密钥 String apiKey = "your-api-key"; OpenAiService service = new OpenAiService(apiKey); // 创建完成请求 CompletionRequest completionRequest = CompletionRequest.builder() .prompt("一些提示信息") .model("合适的模型名称") .maxTokens(100) .build(); // 发起请求并获取结果 CompletionResult completionResult = service.createCompletion(completionRequest); System.out.println(completionResult.getChoices().get(0).getText()); } } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Coder_Boy_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值