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

在上一章中介绍了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 开发,可直接运行。

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

plaintext 复制代码
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 复制代码
<?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)

yaml 复制代码
# 服务器配置
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(配置属性绑定)
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 初始化配置)
java 复制代码
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 缓存配置)
java 复制代码
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(对话记忆模型)
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 实现)
java 复制代码
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(工具接口)
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(工具调度器)
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(火灾数据查询工具)
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(天气预报查询工具)
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(任务规划器)
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(结果校验器)
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 主类)
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.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 交互接口)
java 复制代码
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 工具类)
java 复制代码
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)

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:简单问答(无需工具)

请求地址:

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

返回结果:

plaintext 复制代码
📢 对话ID:f47ac10b19674b2da635c87681234567

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

请求地址:

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

返回结果:

plaintext 复制代码
📢 对话ID:a1b2c3d4e5f64a5b9c8d7e6f5a4b3c2d

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

六、Agent 核心能力总结

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

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

相关推荐
NAGNIP8 小时前
一文搞懂深度学习中的通用逼近定理!
人工智能·算法·面试
冬奇Lab9 小时前
一天一个开源项目(第36篇):EverMemOS - 跨 LLM 与平台的长时记忆 OS,让 Agent 会记忆更会推理
人工智能·开源·资讯
冬奇Lab9 小时前
OpenClaw 源码深度解析(一):Gateway——为什么需要一个"中枢"
人工智能·开源·源码阅读
AngelPP13 小时前
OpenClaw 架构深度解析:如何把 AI 助手搬到你的个人设备上
人工智能
宅小年13 小时前
Claude Code 换成了Kimi K2.5后,我再也回不去了
人工智能·ai编程·claude
九狼13 小时前
Flutter URL Scheme 跨平台跳转
人工智能·flutter·github
ZFSS13 小时前
Kimi Chat Completion API 申请及使用
前端·人工智能
天翼云开发者社区14 小时前
春节复工福利就位!天翼云息壤2500万Tokens免费送,全品类大模型一键畅玩!
人工智能·算力服务·息壤
知识浅谈14 小时前
教你如何用 Gemini 将课本图片一键转为精美 PPT
人工智能
Ray Liang15 小时前
被低估的量化版模型,小身材也能干大事
人工智能·ai·ai助手·mindx