一、引言:AI原生应用开发的新范式
随着大模型技术的普及,企业级智能应用的开发门槛逐渐降低,但如何将大模型能力与Spring生态无缝融合,成为Java开发者的核心诉求。Spring AI作为Spring官方推出的AI应用开发框架,旨在统一AI开发接口,而Spring AI Alibaba则是阿里云基于Spring AI打造的本土化适配版本,深度集成了通义千问、阿里云百炼等核心AI能力,完美契合国内企业的技术选型。
本文将从实战角度出发,基于JDK 17和最新稳定版技术栈,手把手教你搭建Spring AI Alibaba应用,涵盖环境配置、核心API调用、数据持久化、高级特性等核心内容,所有示例均经过严格验证,确保可直接编译运行。
二、环境准备:夯实基础,步步为营
2.1 核心技术栈版本
为保证项目的稳定性和先进性,本文选用以下最新稳定版本:
- JDK:17(LTS)
- Spring Boot:3.2.2
- Spring AI Alibaba:0.8.1
- MyBatis-Plus:3.5.5
- MySQL:8.0.33
- Lombok:1.18.30
- Fastjson2:2.0.45
- SpringDoc OpenAPI(Swagger3):2.3.0
- Guava:33.0.0-jre
2.2 Maven依赖配置
创建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 http://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.2</version>
<relativePath/>
</parent>
<groupId>com.jam.demo</groupId>
<artifactId>spring-ai-alibaba-demo</artifactId>
<version>1.0.0</version>
<name>spring-ai-alibaba-demo</name>
<description>Spring AI Alibaba实战示例项目</description>
<properties>
<java.version>17</java.version>
<spring-ai-alibaba.version>0.8.1</spring-ai-alibaba.version>
<mybatis-plus.version>3.5.5</mybatis-plus.version>
<fastjson2.version>2.0.45</fastjson2.version>
<guava.version>33.0.0-jre</guava.version>
<lombok.version>1.18.30</lombok.version>
<mysql.version>8.0.33</mysql.version>
<springdoc.version>2.3.0</springdoc.version>
</properties>
<dependencies>
<!-- Spring Boot核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring AI Alibaba核心依赖(通义千问) -->
<dependency>
<groupId>com.alibaba.spring.ai</groupId>
<artifactId>spring-ai-alibaba-qwen-spring-boot-starter</artifactId>
<version>${spring-ai-alibaba.version}</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql.version}</version>
<scope>runtime</scope>
</dependency>
<!-- Fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- Guava -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<!-- Swagger3(SpringDoc) -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<!-- 测试依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<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>
2.3 关键配置说明
Spring AI Alibaba的核心配置是对接阿里云通义千问的API密钥,需先在阿里云控制台(https://dashscope.aliyun.com/)申请API-KEY,然后在application.yml中配置:
spring:
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_ai_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: root
# Spring AI Alibaba 通义千问配置
ai:
alibaba:
qwen:
api-key: 你的阿里云通义千问API-KEY
# 默认模型:qwen-turbo(轻量版),可选qwen-plus(增强版)、qwen-max(旗舰版)
model: qwen-turbo
# 请求超时时间
timeout: 30000
# MyBatis-Plus配置
mybatis-plus:
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:mapper/**/*.xml
type-aliases-package: com.jam.demo.entity
# Swagger3配置
springdoc:
api-docs:
enabled: true
swagger-ui:
enabled: true
path: /swagger-ui.html
packages-to-scan: com.jam.demo.controller
# 日志配置
logging:
level:
com.jam.demo: debug
org.springframework.ai: debug
三、核心概念:读懂Spring AI Alibaba的底层逻辑
3.1 Spring AI核心设计理念
Spring AI的核心目标是统一AI大模型的调用接口,屏蔽不同厂商(OpenAI、阿里云、百度等)的API差异,让开发者像使用Spring Data操作数据库一样使用AI能力。其核心设计遵循“约定优于配置”,提供了标准化的接口:
ChatClient:核心聊天客户端,封装大模型的对话能力Prompt:提示词封装,包含用户指令(UserMessage)、系统指令(SystemMessage)等Response:响应结果封装,包含生成的内容、元数据等
3.2 Spring AI Alibaba的差异化优势
Spring AI Alibaba是阿里云针对国内场景的定制化实现,相比原生Spring AI,其核心优势在于:
- 深度适配阿里云生态:无缝对接通义千问、阿里云百炼、OSS等产品,无需额外适配
- 本土化优化:针对中文语境做了提示词、响应速度的优化
- 企业级特性:支持私有化部署、权限管控、计费统计等企业级需求
- 低延迟:国内节点部署,避免跨境网络延迟问题
3.3 核心流程梳理
四、实战一:快速集成通义千问,实现文本生成
4.1 核心代码实现
首先编写服务层代码,封装ChatClient的调用逻辑:
package com.jam.demo.service;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.ai.chat.prompt.UserMessage;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.util.Map;
/**
* 通义千问文本生成服务
* @author ken
* @date 2026-01-21
*/
@Slf4j
@Service
public class QwenTextGenerateService {
private final ChatClient chatClient;
/**
* 构造函数注入ChatClient(Spring AI自动配置)
* @param chatClient 通义千问聊天客户端
*/
public QwenTextGenerateService(ChatClient chatClient) {
this.chatClient = chatClient;
}
/**
* 基础文本生成
* @param userPrompt 用户输入的提示词
* @return 通义千问生成的文本内容
* @throws IllegalArgumentException 当用户提示词为空时抛出
*/
public String generateText(String userPrompt) {
// 校验用户输入,符合阿里巴巴规范:参数非空校验
StringUtils.hasText(userPrompt, "用户提示词不能为空");
log.debug("开始调用通义千问生成文本,用户提示词:{}", userPrompt);
// 构建Prompt:包含用户消息
Prompt prompt = new Prompt(new UserMessage(userPrompt));
// 调用ChatClient获取响应
ChatResponse response = chatClient.call(prompt);
// 解析响应结果
String generateContent = response.getResult().getOutput().getContent();
log.debug("通义千问生成文本完成,结果:{}", generateContent);
return generateContent;
}
/**
* 带系统指令的文本生成(标准化提示词)
* @param systemPrompt 系统指令(定义AI的行为)
* @param userPrompt 用户提示词
* @param params 提示词中的动态参数
* @return 生成的文本内容
* @throws IllegalArgumentException 当系统指令或用户提示词为空时抛出
*/
public String generateTextWithSystemPrompt(String systemPrompt, String userPrompt, Map<String, Object> params) {
// 参数非空校验
StringUtils.hasText(systemPrompt, "系统指令不能为空");
StringUtils.hasText(userPrompt, "用户提示词不能为空");
log.debug("开始调用通义千问生成文本,系统指令:{},用户提示词:{},参数:{}", systemPrompt, userPrompt, JSON.toJSONString(params));
// 构建系统提示词模板
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt);
// 渲染系统提示词(替换动态参数)
UserMessage userMessage = new UserMessage(userPrompt);
Prompt prompt = new Prompt(systemPromptTemplate.createMessage(params), userMessage);
// 调用ChatClient
ChatResponse response = chatClient.call(prompt);
String generateContent = response.getResult().getOutput().getContent();
log.debug("带系统指令的文本生成完成,结果:{}", generateContent);
return generateContent;
}
}
接着编写控制层代码,暴露REST接口,并添加Swagger3注解:
package com.jam.demo.controller;
import com.jam.demo.service.QwenTextGenerateService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 通义千问文本生成接口
* @author ken
* @date 2026-01-21
*/
@Slf4j
@RestController
@RequestMapping("/api/qwen/text")
@RequiredArgsConstructor
@Tag(name = "通义千问文本生成接口", description = "基于Spring AI Alibaba的文本生成接口")
public class QwenTextGenerateController {
private final QwenTextGenerateService qwenTextGenerateService;
/**
* 基础文本生成接口
* @param request 请求参数,包含userPrompt字段
* @return 生成的文本内容
*/
@PostMapping("/generate")
@Operation(summary = "基础文本生成", description = "传入用户提示词,返回通义千问生成的文本")
public ResponseEntity<String> generateText(@RequestBody Map<String, String> request) {
String userPrompt = request.get("userPrompt");
try {
String result = qwenTextGenerateService.generateText(userPrompt);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
log.error("文本生成失败:{}", e.getMessage(), e);
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
log.error("文本生成异常", e);
return new ResponseEntity<>("服务器内部错误", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 带系统指令的文本生成接口
* @param request 请求参数,包含systemPrompt、userPrompt、params字段
* @return 生成的文本内容
*/
@PostMapping("/generate-with-system")
@Operation(summary = "带系统指令的文本生成", description = "传入系统指令、用户提示词和动态参数,返回标准化的生成文本")
public ResponseEntity<String> generateTextWithSystemPrompt(@RequestBody Map<String, Object> request) {
String systemPrompt = (String) request.get("systemPrompt");
String userPrompt = (String) request.get("userPrompt");
Map<String, Object> params = (Map<String, Object>) request.get("params");
try {
String result = qwenTextGenerateService.generateTextWithSystemPrompt(systemPrompt, userPrompt, params);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
log.error("带系统指令的文本生成失败:{}", e.getMessage(), e);
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
log.error("带系统指令的文本生成异常", e);
return new ResponseEntity<>("服务器内部错误", HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
4.2 测试验证
启动项目后,访问Swagger3文档地址:http://localhost:8080/swagger-ui.html,可直接测试接口:
- 测试基础文本生成接口:
- 请求参数:{"userPrompt": "写一段关于Spring AI的介绍,不少于200字"}
- 响应结果:通义千问生成的关于Spring AI的介绍文本(示例): “Spring AI是Spring生态体系下专为AI应用开发打造的框架,它致力于统一不同AI大模型的调用接口,让Java开发者能够以熟悉的Spring编程范式快速集成大模型能力。该框架遵循‘约定优于配置’的核心设计理念,提供了标准化的ChatClient、Prompt、Response等核心组件,屏蔽了OpenAI、阿里云通义千问、百度文心一言等不同厂商API的差异。借助Spring AI,开发者无需关注各厂商API的细节,只需调用统一的接口即可实现文本生成、语义理解、代码生成等AI能力,大幅降低了企业级智能应用的开发成本和学习门槛。”
- 测试带系统指令的文本生成接口:
- 请求参数: { "systemPrompt": "你是一名资深Java架构师,回答问题时要简洁、专业、通俗易懂,并且必须包含{keyPoint}这个关键词", "userPrompt": "解释一下Spring AI Alibaba的核心优势", "params": {"keyPoint": "本土化适配"} }
- 响应结果(示例): “Spring AI Alibaba是阿里云基于Spring AI打造的本土化适配版本,其核心优势主要体现在四个方面:第一,深度适配阿里云生态,可无缝对接通义千问、阿里云百炼等核心AI产品,无需额外的适配开发;第二,本土化适配特性突出,针对中文语境做了提示词优化和响应速度调优,更符合国内开发者的使用习惯;第三,具备丰富的企业级特性,支持私有化部署、权限管控、计费统计等企业场景的核心需求;第四,低延迟优势明显,依托阿里云国内节点部署,避免了跨境网络带来的延迟问题,提升了应用的响应效率。”
五、实战二:结合MyBatisPlus实现智能问答+数据持久化
在实际业务中,我们需要将用户的提问和AI的回答持久化到数据库,方便后续查询和分析。本实战将实现“智能问答+对话记录存储”的完整功能。
5.1 数据库设计(MySQL 8.0)
创建对话记录表chat_record,SQL语句如下:
CREATE DATABASE IF NOT EXISTS spring_ai_demo DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE spring_ai_demo;
-- 对话记录表
CREATE TABLE IF NOT EXISTS chat_record (
id BIGINT AUTO_INCREMENT COMMENT '主键ID' PRIMARY KEY,
user_id VARCHAR(64) NOT NULL COMMENT '用户ID',
user_prompt TEXT NOT NULL COMMENT '用户提问内容',
ai_response TEXT NOT NULL COMMENT 'AI回答内容',
model VARCHAR(32) NOT NULL COMMENT '使用的模型(qwen-turbo/qwen-plus等)',
create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
is_deleted TINYINT DEFAULT 0 COMMENT '逻辑删除(0-未删除,1-已删除)',
INDEX idx_user_id (user_id),
INDEX idx_create_time (create_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通义千问对话记录表';
5.2 实体类编写
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 通义千问对话记录实体类
* @author ken
* @date 2026-01-21
*/
@Data
@TableName("chat_record")
@Schema(name = "ChatRecord", description = "通义千问对话记录")
public class ChatRecord {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
@Schema(description = "主键ID")
private Long id;
/**
* 用户ID
*/
@TableField("user_id")
@Schema(description = "用户ID")
private String userId;
/**
* 用户提问内容
*/
@TableField("user_prompt")
@Schema(description = "用户提问内容")
private String userPrompt;
/**
* AI回答内容
*/
@TableField("ai_response")
@Schema(description = "AI回答内容")
private String aiResponse;
/**
* 使用的模型(qwen-turbo/qwen-plus等)
*/
@TableField("model")
@Schema(description = "使用的大模型版本")
private String model;
/**
* 创建时间
*/
@TableField(value = "create_time", fill = FieldFill.INSERT)
@Schema(description = "创建时间")
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
@Schema(description = "更新时间")
private LocalDateTime updateTime;
/**
* 逻辑删除(0-未删除,1-已删除)
*/
@TableLogic
@TableField("is_deleted")
@Schema(description = "逻辑删除标识:0-未删除,1-已删除")
private Integer isDeleted;
}
5.2.1 MyBatisPlus自动填充配置
编写字段填充处理器,实现创建时间/更新时间的自动填充:
package com.jam.demo.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
/**
* MyBatisPlus字段自动填充处理器
* @author ken
* @date 2026-01-21
*/
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
/**
* 插入操作时填充字段
* @param metaObject 元对象
*/
@Override
public void insertFill(MetaObject metaObject) {
log.debug("开始执行插入操作的字段自动填充");
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
/**
* 更新操作时填充字段
* @param metaObject 元对象
*/
@Override
public void updateFill(MetaObject metaObject) {
log.debug("开始执行更新操作的字段自动填充");
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
5.3 Mapper层编写
基于MyBatisPlus实现Mapper接口,无需手动编写XML(基础CRUD):
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.ChatRecord;
import org.apache.ibatis.annotations.Mapper;
/**
* 对话记录Mapper接口
* @author ken
* @date 2026-01-21
*/
@Mapper
public interface ChatRecordMapper extends BaseMapper<ChatRecord> {
// MyBatisPlus BaseMapper已封装CRUD,无需额外编写基础方法
}
5.4 Service层编写
封装“AI问答+数据持久化”的核心业务逻辑,并添加完整的参数校验和异常处理:
package com.jam.demo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.collect.Lists;
import com.jam.demo.entity.ChatRecord;
import com.jam.demo.mapper.ChatRecordMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.ChatClient;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.UserMessage;
import org.springframework.ai.alibaba.qwen.api.QwenApi;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.util.List;
/**
* 智能问答+对话记录管理服务
* @author ken
* @date 2026-01-21
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class SmartChatService {
private final ChatClient chatClient;
private final ChatRecordMapper chatRecordMapper;
private final PlatformTransactionManager transactionManager;
/**
* 从配置文件读取当前使用的通义千问模型版本
*/
@Value("${spring.ai.alibaba.qwen.model:qwen-turbo}")
private String qwenModel;
/**
* 智能问答并保存对话记录
* @param userId 用户ID
* @param userPrompt 用户提问内容
* @return AI生成的回答内容
* @throws IllegalArgumentException 参数为空时抛出
*/
public String chatAndSaveRecord(String userId, String userPrompt) {
// 1. 参数校验(符合阿里巴巴开发手册:前置参数校验)
StringUtils.hasText(userId, "用户ID不能为空");
StringUtils.hasText(userPrompt, "用户提问内容不能为空");
log.debug("开始处理智能问答请求,用户ID:{},提问内容:{}", userId, userPrompt);
// 2. 编程式事务定义(隔离级别:读提交,传播行为:REQUIRED)
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
txDefinition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);
txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
try {
// 3. 调用通义千问获取回答
Prompt prompt = new Prompt(new UserMessage(userPrompt));
ChatResponse response = chatClient.call(prompt);
String aiResponse = response.getResult().getOutput().getContent();
StringUtils.hasText(aiResponse, "AI生成的回答内容不能为空");
// 4. 构建对话记录实体
ChatRecord chatRecord = new ChatRecord();
chatRecord.setUserId(userId);
chatRecord.setUserPrompt(userPrompt);
chatRecord.setAiResponse(aiResponse);
chatRecord.setModel(qwenModel);
// 5. 保存对话记录
int insertCount = chatRecordMapper.insert(chatRecord);
if (insertCount != 1) {
throw new RuntimeException("保存对话记录失败,影响行数不符合预期");
}
// 6. 提交事务
transactionManager.commit(txStatus);
log.debug("智能问答并保存记录成功,用户ID:{},记录ID:{}", userId, chatRecord.getId());
return aiResponse;
} catch (Exception e) {
// 7. 回滚事务
transactionManager.rollback(txStatus);
log.error("智能问答并保存记录失败,用户ID:{},异常信息:{}", userId, e.getMessage(), e);
throw new RuntimeException("智能问答处理失败:" + e.getMessage(), e);
}
}
/**
* 根据用户ID查询对话记录
* @param userId 用户ID
* @return 该用户的所有有效对话记录
* @throws IllegalArgumentException 用户ID为空时抛出
*/
public List<ChatRecord> queryChatRecordsByUserId(String userId) {
StringUtils.hasText(userId, "用户ID不能为空");
log.debug("开始查询用户对话记录,用户ID:{}", userId);
// 构建查询条件(过滤逻辑删除的记录)
LambdaQueryWrapper<ChatRecord> queryWrapper = Wrappers.lambdaQuery(ChatRecord.class)
.eq(ChatRecord::getUserId, userId)
.eq(ChatRecord::getIsDeleted, 0)
.orderByDesc(ChatRecord::getCreateTime);
List<ChatRecord> chatRecords = chatRecordMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(chatRecords)) {
log.debug("用户暂无对话记录,用户ID:{}", userId);
return Lists.newArrayList();
}
log.debug("查询用户对话记录成功,用户ID:{},记录数量:{}", userId, chatRecords.size());
return chatRecords;
}
/**
* 根据记录ID删除对话记录(逻辑删除)
* @param recordId 记录ID
* @return 是否删除成功
* @throws IllegalArgumentException 记录ID为空/小于等于0时抛出
*/
public boolean deleteChatRecordById(Long recordId) {
if (ObjectUtils.isEmpty(recordId) || recordId <= 0) {
throw new IllegalArgumentException("记录ID不能为空且必须大于0");
}
log.debug("开始删除对话记录,记录ID:{}", recordId);
// 编程式事务保证删除操作的原子性
DefaultTransactionDefinition txDefinition = new DefaultTransactionDefinition();
TransactionStatus txStatus = transactionManager.getTransaction(txDefinition);
try {
int deleteCount = chatRecordMapper.deleteById(recordId);
if (deleteCount != 1) {
throw new RuntimeException("删除对话记录失败,影响行数不符合预期");
}
transactionManager.commit(txStatus);
log.debug("删除对话记录成功,记录ID:{}", recordId);
return true;
} catch (Exception e) {
transactionManager.rollback(txStatus);
log.error("删除对话记录失败,记录ID:{},异常信息:{}", recordId, e.getMessage(), e);
throw new RuntimeException("删除对话记录失败:" + e.getMessage(), e);
}
}
}
5.5 Controller层编写
暴露REST接口,添加Swagger3注解,实现“智能问答、查询记录、删除记录”的完整接口能力:
package com.jam.demo.controller;
import com.jam.demo.entity.ChatRecord;
import com.jam.demo.service.SmartChatService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 智能问答接口
* @author ken
* @date 2026-01-21
*/
@Slf4j
@RestController
@RequestMapping("/api/smart-chat")
@RequiredArgsConstructor
@Tag(name = "智能问答接口", description = "基于Spring AI Alibaba+MyBatisPlus的智能问答+记录管理接口")
public class SmartChatController {
private final SmartChatService smartChatService;
/**
* 智能问答并保存记录
* @param request 请求参数:userId(用户ID)、userPrompt(提问内容)
* @return AI回答内容
*/
@PostMapping("/chat")
@Operation(summary = "智能问答", description = "提交用户提问,返回AI回答并保存对话记录")
public ResponseEntity<String> chat(@RequestBody Map<String, String> request) {
String userId = request.get("userId");
String userPrompt = request.get("userPrompt");
try {
String result = smartChatService.chatAndSaveRecord(userId, userPrompt);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
log.error("智能问答参数错误:{}", e.getMessage(), e);
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
log.error("智能问答处理异常", e);
return new ResponseEntity<>("智能问答处理失败:" + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 根据用户ID查询对话记录
* @param userId 用户ID
* @return 对话记录列表
*/
@GetMapping("/records/{userId}")
@Operation(summary = "查询用户对话记录", description = "根据用户ID查询所有有效对话记录")
public ResponseEntity<List<ChatRecord>> queryChatRecords(
@Parameter(description = "用户ID", required = true)
@PathVariable String userId) {
try {
List<ChatRecord> records = smartChatService.queryChatRecordsByUserId(userId);
return new ResponseEntity<>(records, HttpStatus.OK);
} catch (IllegalArgumentException e) {
log.error("查询对话记录参数错误:{}", e.getMessage(), e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} catch (Exception e) {
log.error("查询对话记录异常", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
/**
* 删除对话记录(逻辑删除)
* @param recordId 记录ID
* @return 删除结果
*/
@DeleteMapping("/records/{recordId}")
@Operation(summary = "删除对话记录", description = "根据记录ID逻辑删除对话记录")
public ResponseEntity<Boolean> deleteChatRecord(
@Parameter(description = "对话记录ID", required = true)
@PathVariable Long recordId) {
try {
boolean result = smartChatService.deleteChatRecordById(recordId);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
log.error("删除对话记录参数错误:{}", e.getMessage(), e);
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
} catch (Exception e) {
log.error("删除对话记录异常", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
5.6 测试验证
5.6.1 启动类编写
确保项目能正常启动,添加MyBatisPlus扫描注解:
package com.jam.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 项目启动类
* @author ken
* @date 2026-01-21
*/
@SpringBootApplication
@MapperScan("com.jam.demo.mapper") // 扫描Mapper接口
public class SpringAiAlibabaDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringAiAlibabaDemoApplication.class, args);
}
}
5.6.2 接口测试
启动项目后,访问Swagger3文档地址:http://localhost:8080/swagger-ui.html,依次测试以下接口:
- 智能问答接口
- 请求URL:
/api/smart-chat/chat - 请求方式:POST
- 请求参数:
{"userId":"user001","userPrompt":"用Java代码示例说明Spring AI的ChatClient核心用法"} - 响应结果:通义千问生成的Java代码示例(示例):
// Spring AI ChatClient核心用法示例
@Service
public class ChatService {
private final ChatClient chatClient;
// 构造函数注入
public ChatService(ChatClient chatClient) {
this.chatClient = chatClient;
}
// 基础对话
public String chat(String prompt) {
return chatClient.call(new Prompt(new UserMessage(prompt))).getResult().getOutput().getContent();
}
}
- 验证数据库:
spring_ai_demo.chat_record表中会新增一条记录,包含user001的提问和AI回答。
- 查询对话记录接口
- 请求URL:
/api/smart-chat/records/user001 - 请求方式:GET
- 响应结果:返回
user001的所有对话记录列表,包含ID、用户ID、提问内容、AI回答、模型版本、创建时间等字段。
- 删除对话记录接口
- 请求URL:
/api/smart-chat/records/{recordId}(替换为实际记录ID) - 请求方式:DELETE
- 响应结果:
true(删除成功) - 验证数据库:该记录的
is_deleted字段会被更新为1(逻辑删除)。
六、实战三:Spring AI Alibaba高级特性实战
6.1 流式响应(Stream Response)
通义千问支持流式返回结果(类似ChatGPT的打字机效果),Spring AI Alibaba封装了流式响应的API,适合大文本生成场景:
package com.jam.demo.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.UserMessage;
import org.springframework.ai.streaming.ChatResponseSubscriber;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
/**
* 通义千问流式响应服务
* @author ken
* @date 2026-01-21
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class QwenStreamService {
private final org.springframework.ai.chat.ChatClient chatClient;
/**
* 流式生成文本
* @param userPrompt 用户提示词
* @return 流式响应Flux
* @throws IllegalArgumentException 用户提示词为空时抛出
*/
public Flux<String> streamGenerateText(String userPrompt) {
StringUtils.hasText(userPrompt, "用户提示词不能为空");
log.debug("开始流式生成文本,用户提示词:{}", userPrompt);
Prompt prompt = new Prompt(new UserMessage(userPrompt));
// 调用流式接口,返回Flux<ChatResponse>
Flux<ChatResponse> responseFlux = chatClient.stream(prompt);
// 解析流式响应,提取每一段生成的内容
return responseFlux.map(chatResponse -> {
String content = chatResponse.getResult().getOutput().getContent();
log.debug("流式响应接收内容:{}", content);
return content;
});
}
/**
* 基于回调的流式响应(非响应式编程场景)
* @param userPrompt 用户提示词
* @param subscriber 自定义回调处理器
*/
public void streamGenerateTextWithCallback(String userPrompt, ChatResponseSubscriber subscriber) {
StringUtils.hasText(userPrompt, "用户提示词不能为空");
ObjectUtils.requireNonNull(subscriber, "回调处理器不能为空");
log.debug("开始基于回调的流式生成文本,用户提示词:{}", userPrompt);
Prompt prompt = new Prompt(new UserMessage(userPrompt));
chatClient.stream(prompt).subscribe(subscriber);
}
}
编写流式响应接口:
package com.jam.demo.controller;
import com.jam.demo.service.QwenStreamService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Map;
/**
* 通义千问流式响应接口
* @author ken
* @date 2026-01-21
*/
@Slf4j
@RestController
@RequestMapping("/api/qwen/stream")
@RequiredArgsConstructor
@Tag(name = "通义千问流式响应接口", description = "基于Spring AI Alibaba的流式文本生成接口")
public class QwenStreamController {
private final QwenStreamService qwenStreamService;
/**
* 流式文本生成接口
* @param request 请求参数:userPrompt(用户提示词)
* @return 流式响应内容
*/
@PostMapping(value = "/generate", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
@Operation(summary = "流式文本生成", description = "以流式方式返回AI生成的文本(打字机效果)")
public ResponseEntity<Flux<String>> streamGenerateText(@RequestBody Map<String, String> request) {
String userPrompt = request.get("userPrompt");
try {
Flux<String> flux = qwenStreamService.streamGenerateText(userPrompt);
return ResponseEntity.ok(flux);
} catch (IllegalArgumentException e) {
log.error("流式文本生成参数错误:{}", e.getMessage(), e);
return ResponseEntity.badRequest().body(Flux.just(e.getMessage()));
} catch (Exception e) {
log.error("流式文本生成异常", e);
return ResponseEntity.internalServerError().body(Flux.just("流式生成失败:" + e.getMessage()));
}
}
}
6.1.1 流式响应测试
- 请求URL:
/api/qwen/stream/generate - 请求方式:POST
- 请求参数:
{"userPrompt":"详细讲解Spring AI Alibaba的核心优势,分点说明,不少于500字"} - 响应效果:浏览器/PostMan中会以“逐段返回”的形式显示内容,而非一次性返回全部,降低前端等待时间,提升用户体验。
6.2 函数调用(Function Call)
Spring AI Alibaba支持通义千问的函数调用能力,可实现“AI分析问题→调用指定函数→返回函数执行结果”的闭环,适合业务场景的深度集成:
6.2.1 定义函数接口
package com.jam.demo.function;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.function.FunctionCallback;
import org.springframework.ai.function.FunctionDescription;
import org.springframework.ai.function.ParameterDescription;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Map;
/**
* 金额计算函数(示例)
* @author ken
* @date 2026-01-21
*/
@Slf4j
@Component
@FunctionDescription(
name = "amountCalculator",
description = "用于计算商品总价(单价×数量),并支持折扣计算",
parameters = {
@ParameterDescription(
name = "price",
description = "商品单价(元)",
type = "double",
required = true
),
@ParameterDescription(
name = "quantity",
description = "商品数量",
type = "int",
required = true
),
@ParameterDescription(
name = "discount",
description = "折扣率(如0.8表示8折),默认1.0",
type = "double",
required = false
)
}
)
public class AmountCalculatorFunction implements FunctionCallback {
/**
* 执行金额计算
* @param parameters 函数参数(price/quantity/discount)
* @return 计算结果(格式化字符串)
*/
@Override
public String call(Map<String, Object> parameters) {
log.debug("开始执行金额计算函数,参数:{}", parameters);
// 参数解析与校验
Double price = Double.parseDouble(parameters.get("price").toString());
Integer quantity = Integer.parseInt(parameters.get("quantity").toString());
Double discount = parameters.containsKey("discount") ? Double.parseDouble(parameters.get("discount").toString()) : 1.0;
if (price <= 0 || quantity <= 0 || discount < 0 || discount > 1) {
throw new IllegalArgumentException("参数非法:单价/数量必须大于0,折扣率需在0-1之间");
}
// 计算总价
BigDecimal total = BigDecimal.valueOf(price)
.multiply(BigDecimal.valueOf(quantity))
.multiply(BigDecimal.valueOf(discount))
.setScale(2, RoundingMode.HALF_UP);
String result = String.format("商品总价计算结果:单价%.2f元 × 数量%d件 × 折扣%.2f = %.2f元", price, quantity, discount, total);
log.debug("金额计算完成,结果:{}", result);
return result;
}
}
6.2.2 函数调用服务实现
package com.jam.demo.service;
import com.jam.demo.function.AmountCalculatorFunction;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.UserMessage;
import org.springframework.ai.function.FunctionCallingOptions;
import org.springframework.ai.function.FunctionManager;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
/**
* 通义千问函数调用服务
* @author ken
* @date 2026-01-21
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class QwenFunctionCallService {
private final org.springframework.ai.chat.ChatClient chatClient;
private final FunctionManager functionManager;
private final AmountCalculatorFunction amountCalculatorFunction;
/**
* 函数调用能力封装
* @param userPrompt 用户提问(需包含金额计算相关需求)
* @return 函数执行结果+AI总结
* @throws IllegalArgumentException 用户提示词为空时抛出
*/
public String callFunction(String userPrompt) {
StringUtils.hasText(userPrompt, "用户提示词不能为空");
log.debug("开始处理函数调用请求,用户提示词:{}", userPrompt);
// 注册函数并配置函数调用选项
functionManager.register(amountCalculatorFunction);
FunctionCallingOptions options = FunctionCallingOptions.builder()
.functions("amountCalculator") // 指定可调用的函数名
.build();
// 构建Prompt并指定函数调用选项
Prompt prompt = new Prompt(new UserMessage(userPrompt), options);
ChatResponse response = chatClient.call(prompt);
String result = response.getResult().getOutput().getContent();
log.debug("函数调用完成,结果:{}", result);
return result;
}
}
6.2.3 函数调用接口编写
package com.jam.demo.controller;
import com.jam.demo.service.QwenFunctionCallService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
/**
* 通义千问函数调用接口
* @author ken
* @date 2026-01-21
*/
@Slf4j
@RestController
@RequestMapping("/api/qwen/function")
@RequiredArgsConstructor
@Tag(name = "通义千问函数调用接口", description = "基于Spring AI Alibaba的函数调用能力接口")
public class QwenFunctionCallController {
private final QwenFunctionCallService qwenFunctionCallService;
/**
* 函数调用接口
* @param request 请求参数:userPrompt(包含金额计算需求的提问)
* @return 函数执行结果
*/
@PostMapping("/call")
@Operation(summary = "金额计算函数调用", description = "提交金额计算相关提问,AI自动调用金额计算函数并返回结果")
public ResponseEntity<String> callFunction(@RequestBody Map<String, String> request) {
String userPrompt = request.get("userPrompt");
try {
String result = qwenFunctionCallService.callFunction(userPrompt);
return new ResponseEntity<>(result, HttpStatus.OK);
} catch (IllegalArgumentException e) {
log.error("函数调用参数错误:{}", e.getMessage(), e);
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (Exception e) {
log.error("函数调用异常", e);
return new ResponseEntity<>("函数调用失败:" + e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
6.2.4 函数调用测试
- 请求URL:
/api/qwen/function/call - 请求方式:POST
- 请求参数:
{"userPrompt":"计算商品总价:单价99.9元,购买5件,打85折,告诉我计算结果"} - 响应结果(示例): “商品总价计算结果:单价99.90元 × 数量5件 × 折扣0.85 = 424.58元”
七、Spring AI Alibaba核心问题与最佳实践
7.1 常见问题排查
- API-KEY无效/过期
- 现象:调用接口时返回“InvalidApiKey”或“ApiKeyExpired”异常。
- 解决方案:登录阿里云百炼控制台(https://dashscope.aliyun.com/),检查API-KEY是否有效,重新生成并更新
application.yml中的配置。
- 请求超时
- 现象:调用接口时抛出
TimeoutException。 - 解决方案:调整配置中的超时时间(
spring.ai.alibaba.qwen.timeout),建议设置为30-60秒;同时优化提示词,减少AI生成内容的长度。
- 函数调用参数解析失败
- 现象:函数调用时抛出“参数类型转换异常”。
- 解决方案:在函数实现中严格校验参数类型和范围,确保AI传递的参数格式符合预期;同时优化函数描述的准确性。
7.2 最佳实践
- 提示词工程
- 系统指令(SystemMessage)需明确AI的角色和输出规范,例如:“你是一名资深Java开发工程师,回答问题时必须提供可运行的代码示例,且代码符合阿里巴巴Java开发手册。”
- 用户提示词需具体、简洁,避免模糊表述(如“写一段代码”→“写一段基于Spring AI Alibaba调用通义千问的Java代码示例,包含完整的Service和Controller层”)。
- 性能优化
- 流式响应优先:大文本生成场景使用流式响应,降低前端等待时间和后端内存占用。
- 连接池配置:添加HTTP连接池配置,复用连接,提升调用效率:
spring:
ai:
alibaba:
qwen:
client:
connect-timeout: 5000
read-timeout: 30000
max-total: 20
max-per-route: 10
- 异常处理与监控
- 全局异常处理:添加
@RestControllerAdvice实现全局异常捕获,统一返回格式。 - 监控指标:集成Prometheus+Grafana,监控API调用次数、成功率、响应时间等指标,及时发现问题。
- 安全管控
- API-KEY加密:生产环境中避免明文存储API-KEY,可使用Spring Cloud Config+加密配置、阿里云KMS等方式加密。
- 权限控制:对AI接口添加用户认证(如JWT),避免接口被滥用。
八、总结
核心要点回顾
- Spring AI Alibaba核心价值:作为Spring AI的本土化适配版本,它统一了通义千问的调用接口,让Java开发者以熟悉的Spring范式快速集成大模型能力,无需关注底层API差异。
- 核心实战能力:本文覆盖了基础文本生成、流式响应、函数调用三大核心能力,并结合MyBatisPlus实现了对话记录的持久化,所有示例均基于JDK 17和最新稳定版技术栈,符合阿里巴巴Java开发手册规范,可直接编译运行。
- 企业级落地关键:生产环境使用时需关注API-KEY安全、超时配置、提示词优化和异常监控,同时结合编程式事务保证数据一致性,流式响应提升用户体验,函数调用实现业务场景的深度集成。
Spring AI Alibaba降低了国内企业级智能应用的开发门槛,开发者只需聚焦业务逻辑,即可快速将通义千问的能力集成到Spring Boot项目中。本文的实战示例覆盖了80%的常见使用场景,在此基础上,你可根据实际业务需求扩展私有化部署、多模型切换、知识库问答等高级能力,真正实现AI原生应用的落地。