在上一章中介绍了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 会:
- 拆解任务:① 查询北京林区近 3 个月火灾数据;② 查询未来 7 天北京天气预报;③ 结合两者生成防控建议;
- 调用工具:① 调用「火灾数据查询 API」;② 调用「天气预报 API」;
- 多轮规划:如果工具返回数据不完整,会自动重试调用,或询问用户补充信息;
- 结果整合:将工具返回的数据整理后,调用大模型生成最终建议。
而你的案例目前只能处理「直接可回答的单一问题」(如 “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. 环境准备
- 安装 JDK 17+
- 安装 Redis(本地运行,无需额外配置,默认端口 6379)
- 替换
application.yml中的dashscope.api-key为你的通义千问 API Key(从阿里云获取)
2. 启动步骤
- 编译项目:
mvn clean package - 运行主启动类
QwenAgentApplication.java - 验证启动:控制台输出启动成功提示,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 可直接部署使用,也可基于此扩展更多功能(如新增工具、优化提示词、集成数据库等)。
3425

被折叠的 条评论
为什么被折叠?



