01-SpringAI的介绍
Spring AI 是由 VMware / Spring 团队推出的一个 面向 AI 的 Spring 官方项目,目标是让 Java / Spring 开发者 像用 Spring Data、Spring Security 一样,用统一、规范的方式接入大模型(LLM)和 AI 能力。
一句话理解: Spring AI = 给大模型用的 Spring Framework
02-创建项目远程调用deepseek大模型
1 创建项目
Releases - Use Maven Central
<!-- Maven Central is included by default in Maven builds.
You usually don’t need to configure it explicitly,
but it's shown here for clarity. -->
<repositories>
<repository>
<id>central</id>
<url>https://repo.maven.apache.org/maven2</url>
</repository>
</repositories>
Snapshots - Add Snapshot Repositories
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
</repository>
<repository>
<name>Central Portal Snapshots</name>
<id>central-portal-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
deepseek dependency
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId>
</dependency>
ChatController.java
package org.example.springaidemo.controller;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Map;
@RestController
public class ChatController {
private final DeepSeekChatModel chatModel;
@Autowired
public ChatController(DeepSeekChatModel chatModel) {
this.chatModel = chatModel;
}
@GetMapping("/ai/generate")
public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return Map.of("generation", chatModel.call(message));
}
@GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
var prompt = new Prompt(new UserMessage(message));
return chatModel.stream(prompt).map(ChatResponse -> ChatResponse.getResult().getOutput().getText());
}
}
测试接口
http://localhost:8080/ai/generateStream?message=讲个笑话
03-SpringAI调用本地大模型ollama
1 引入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId>
</dependency>
2 配置yml
# In application.yml
server:
port: 8080
error:
include-message: always
include-binding-errors: always
include-stacktrace: on_param
include-exception: false
spring:
ai:
deepseek:
api-key: sk-7fee8ebd8c0744ac9c1d91d9f0d81372
ollama:
chat:
model: deepseek-r1-1.5b
3 chatController.java 调用本地ollama的大模型
package org.example.springaidemo.controller;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.Map;
@RestController
public class ChatController {
@Resource
private OllamaChatModel chatModel;
@GetMapping("/ai/generate")
public Map generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return Map.of("generation", chatModel.call(message));
}
@GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
var prompt = new Prompt(new UserMessage(message));
return chatModel.stream(prompt).map(ChatResponse -> ChatResponse.getResult().getOutput().getText());
}
}
04-SpringAI创建ChatClient示例实现AI人设
package org.example.springaidemo.controller;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ChatController {
@Resource
private final DeepSeekChatModel chatModel;
@Resource
private ChatClient chatClient;
@Autowired
public ChatController(DeepSeekChatModel chatModel) {
this.chatModel = chatModel;
}
@GetMapping("/ai/generate")
public String generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return chatClient.prompt("你是ai助手小黑").user( message).call().content();
}
@GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return chatClient.prompt("你是ai助手小黑").user( message).stream().content();
}
}
测试
05-SpringAI实现记录上下文
1 创建数据库
打开 navicat 进行创建
数据库的名称就叫
20260110-ai
字符集编码选择为 utf8mb4
创建成功
点击 查询的选项
然后把聊天记录表的 sql 语句给复制粘贴进来
CREATE TABLE `sys_history` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '序列号',
`datetime` datetime DEFAULT NULL COMMENT '聊天时间',
`content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci COMMENT '聊天内容',
`role` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT 'user' COMMENT '角色',
`sessionId` bigint DEFAULT NULL COMMENT '会话Id',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=227 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DYNAMIC COMMENT='聊天记录';
2 导入依赖坐标
<!--Mysql驱动 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.3.0</version>
</dependency>
<!-- Mybatis——Plus 插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<!-- Mybatis——Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
3 配置数据源
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/20260110-ai?allowPublicKeyRetrieval=true&useUnicode=true&useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: root
4 配置其他文件夹
config/AiConfig.java
package org.example.springaidemo.entity.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.springaidemo.entity.History;
import org.example.springaidemo.mapper.HistoryMapper;
import org.example.springaidemo.service.HistoryService;
import org.springframework.stereotype.Service;
@Service
public class HistoryServiceImpl extends ServiceImpl<HistoryMapper, History> implements HistoryService {
}
config/MybatisPlusConfig.java
package org.example.springaidemo.config;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("org.example.springaidemo.mapper")
@Slf4j
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
return interceptor;
}
}
entity/History.java
package org.example.springaidemo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 聊天记录实体类
*/
@Data
@Builder(toBuilder = true)
@AllArgsConstructor
@NoArgsConstructor
@TableName("sys_history")
public class History implements Serializable {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@Schema(title = "聊天时间")
@TableField("datetime")
@JsonFormat( pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime datetime;
@Schema(title = "聊天内容")
@TableField("content")
private String content;
@Schema(title = "角色")
@TableField("role")
private String role;
@Schema(title = "会话Id")
@TableField("sessionId")
private Long sessionId;
}
service/HistoryService.java
package org.example.springaidemo.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.example.springaidemo.entity.History;
public interface HistoryService extends IService<History> {
}
service/impl/HistoryServiceImpl.java
package org.example.springaidemo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.example.springaidemo.entity.History;
import org.example.springaidemo.mapper.HistoryMapper;
import org.example.springaidemo.service.HistoryService;
import org.springframework.stereotype.Service;
@Service
public class HistoryServiceImpl extends ServiceImpl<HistoryMapper, History> implements HistoryService {
}
mapper/HistoryMapper.java
package org.example.springaidemo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.example.springaidemo.entity.History;
@Mapper
public interface HistoryMapper extends BaseMapper<History> {
}
更新chatController.java
使用MyBatis-Plus进行数据库操作
Spring AI框架的各种组件(ChatClient、消息类型等)
DeepSeek和Ollama两个AI模型(虽然导入了Ollama但未使用)
响应式编程库Reactor的Flux
package org.example.springaidemo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.example.springaidemo.entity.History;
import org.example.springaidemo.service.HistoryService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.List;
import java.util.stream.Collectors;
@RestController
public class ChatController {
//依赖注入
@Resource
private final DeepSeekChatModel chatModel;
@Resource
private ChatClient chatClient;
@Resource
private HistoryService historyService;
//构造函数注入
@Autowired
public ChatController(DeepSeekChatModel chatModel) {
this.chatModel = chatModel;
}
/*
* HTTP GET端点:/ai/generate
接收用户消息参数,默认值为"Tell me a joke"
返回AI助手"小黑"对用户消息的同步响应内容
* */
@GetMapping("/ai/generate")
public String generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return chatClient.prompt("你是ai助手小黑").user( message).call().content();
}
@GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message,
@RequestParam(value = "sessionId", defaultValue = " 1 ") long sessionId
) {
//用户消息保存
History userhistory = new History();
userhistory.setDatetime(LocalDateTime.now());
userhistory.setRole("user");
userhistory.setContent(message);
userhistory.setSessionId(sessionId);
historyService.save(userhistory); //创建并保存用户发送的消息到数据库
//获取历史对话
//查询指定会话ID的所有历史记录
//排除刚刚保存的当前用户消息(避免重复)
List<History> histories = historyService.list(
new LambdaQueryWrapper<History>().eq(History::getSessionId, sessionId).ne(History::getId , userhistory.getId())
) ;
//转换历史消息格式: 将历史记录转换为AI模型能理解的消息格式
List<Message> messages = histories.stream().map(history ->
"user".equals(history.getRole())?new UserMessage(history.getContent()):new AssistantMessage(history.getContent())).collect(Collectors.toList());
//数组 用来累积AI流式回复内容
//使用数组是为了在lambda表达式中能够修改变量
StringBuilder[] builder = {new StringBuilder()};
//构建包含上下文的AI请求并返回流式响应
Flux<String> stream = chatClient.prompt("你是ai助手小黑").user( message).messages( messages).stream().content();
return stream.doOnNext(s -> builder[0].append(s)) //当AI返回每个片段时,将其追加到builder中进行累积
.doOnComplete(() -> {
History aihistory = new History();
aihistory.setId(null);
aihistory.setDatetime(LocalDateTime.now());
aihistory.setRole("assistant");
aihistory.setContent(builder[0].toString());
aihistory.setSessionId(sessionId);
historyService.save(aihistory);
});
/*
当AI流式回复完成后,执行以下操作:
创建新的历史记录对象
设置时间戳
设置角色为"assistant"
将累积的完整回复内容保存
设置会话ID
保存到数据库
这种方式确保了只有在AI完全生成回复后,才会将其保存到历史记录中,同时保持了流式响应的实时性*/
}
}
06-语义向量化、向量数据库、RAG的概念
1 RAG 检索增强生成 Retrieval Augmented Generated
文本读取 - 文本分割 - 转向量化 - 存向量数据库 用户问题 - 向量化 - 检索相关内容 - 构造prompt - 大模型生成答案
07-SpringAI实现语义向量化
导入的依赖
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-bom</artifactId>
<version>1.0.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencyManagement>
配置示例
ollama:
chat:
model: deepseek-r1:8b
embedding:
enabled: false
dashscope:
api-key:
embedding:
enabled: true
options:
model: text-embedding-v4
示例方法
@GetMapping("/ai/embedding")
public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of(message));
return Map.of("embedding", embeddingResponse);
}
08-SpringAI实现文本分割
因为在日常工作的过程中, 文本都是以文档的形式进行存储的, 常见的文档格式有 txt 文件、markdown 文件、pdf 文件, 需要把这个文档给读出来, 读出来文档以后才能进行分割, SpringAI 给我们定义了文本读取的规范规范接口, 同时给我们提供了很多类型的文本阅读工具 , 例如 JSON 类型的、txt 类型的、MarkDown 类型的、PDF 类型等
PDF 的有两种读取方式
一种是按页读取
另外一种是按章节读取
他们都是按照 SpringAI 的接口进行实现,特别的规范
1 添加依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-pdf-document-reader</artifactId>
</dependency>
2 测试方法
@Test
void testPdf(){
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/阿里巴巴Java开发手册(终极版).pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build())
.withPagesPerDocument(1)
.build());
System.out.println( pdfReader.read().size());
}
3 按Token进行分割
@Test
void testPdf(){
PagePdfDocumentReader pdfReader = new PagePdfDocumentReader("classpath:/阿里巴巴Java开发手册(终极版).pdf",
PdfDocumentReaderConfig.builder()
.withPageTopMargin(0)
.withPageExtractedTextFormatter(ExtractedTextFormatter.builder()
.withNumberOfTopTextLinesToDelete(0)
.build())
.withPagesPerDocument(1)
.build());
TokenTextSplitter splitter = new TokenTextSplitter(1000, 400, 10, 100000, true);
splitter.apply(pdfReader.read()).forEach(document -> {
System.out.printf(Arrays.toString(embeddingModel.embed(document)));
});
}
09-SpringAI实现向量数据库的存储
1 docker 配置
维度选择 1024 保持一致
2 依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-qdrant</artifactId>
</dependency>
spring:
ai:
vectorstore:
qdrant:
host: localhost
port: 6334
collection-name: vector_store
10-SpringAI实现检索增强
package org.example.springaidemo.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import jakarta.annotation.Resource;
import org.example.springaidemo.entity.History;
import org.example.springaidemo.service.HistoryService;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.AssistantMessage;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.document.Document;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.EmbeddingResponse;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@RestController
public class ChatController {
//依赖注入
@Resource
private final DeepSeekChatModel chatModel;
@Resource
private ChatClient chatClient;
@Resource
private HistoryService historyService;
@Resource
private EmbeddingModel embeddingModel;
@Resource
private VectorStore vectorStore;
//构造函数注入
@Autowired
public ChatController(DeepSeekChatModel chatModel) {
this.chatModel = chatModel;
}
/*
* HTTP GET端点:/ai/generate
接收用户消息参数,默认值为"Tell me a joke"
返回AI助手"小黑"对用户消息的同步响应内容
* */
@GetMapping("/ai/generate")
public String generate(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
return chatClient.prompt("你是ai助手小黑").user( message).call().content();
}
@GetMapping(value = "/ai/generateStream", produces = "text/html; charset=UTF-8")
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message,
@RequestParam(value = "sessionId", defaultValue = " 1 ") long sessionId
) {
List<Document> documentList = vectorStore.similaritySearch(message);
StringBuilder contextBuilder = new StringBuilder();
documentList.forEach(document -> contextBuilder.append(document).append("\n"));
String prompt = "你是编程助手小红 , 以下是本次会话传递的信息"+contextBuilder.toString()+
"/n 请根据以上信息,回答用户的问题:"+message;
//用户消息保存
History userhistory = new History();
userhistory.setDatetime(LocalDateTime.now());
userhistory.setRole("user");
userhistory.setContent(message);
userhistory.setSessionId(sessionId);
historyService.save(userhistory); //创建并保存用户发送的消息到数据库
//获取历史对话
//查询指定会话ID的所有历史记录
//排除刚刚保存的当前用户消息(避免重复)
List<History> histories = historyService.list(
new LambdaQueryWrapper<History>().eq(History::getSessionId, sessionId).ne(History::getId , userhistory.getId())
) ;
//转换历史消息格式: 将历史记录转换为AI模型能理解的消息格式
List<Message> messages = histories.stream().map(history ->
"user".equals(history.getRole())?new UserMessage(history.getContent()):new AssistantMessage(history.getContent())).collect(Collectors.toList());
//数组 用来累积AI流式回复内容
//使用数组是为了在lambda表达式中能够修改变量
StringBuilder[] builder = {new StringBuilder()};
//构建包含上下文的AI请求并返回流式响应
Flux<String> stream = chatClient.prompt(prompt).user( message).messages( messages).stream().content();
return stream.doOnNext(s -> builder[0].append(s)) //当AI返回每个片段时,将其追加到builder中进行累积
.doOnComplete(() -> {
History aihistory = new History();
aihistory.setId(null);
aihistory.setDatetime(LocalDateTime.now());
aihistory.setRole("assistant");
aihistory.setContent(builder[0].toString());
aihistory.setSessionId(sessionId);
historyService.save(aihistory);
});
/*
当AI流式回复完成后,执行以下操作:
创建新的历史记录对象
设置时间戳
设置角色为"assistant"
将累积的完整回复内容保存
设置会话ID
保存到数据库
这种方式确保了只有在AI完全生成回复后,才会将其保存到历史记录中,同时保持了流式响应的实时性*/
}
@GetMapping("/ai/embedding")
public Map embed(@RequestParam(value = "message", defaultValue = "Tell me a joke") String message) {
EmbeddingResponse embeddingResponse = this.embeddingModel.embedForResponse(List.of(message));
return Map.of("embedding", embeddingResponse);
}
}