跳到主要内容
SpringAI 与 Deepseek 大模型应用开发实战笔记(上) | 极客日志
Java AI java
SpringAI 与 Deepseek 大模型应用开发实战笔记(上) 综述由AI生成 基于 SpringAI 框架,结合 Deepseek 大模型进行应用开发实战。内容包括对话机器人的基础实现(同步/流式调用)、会话记忆与历史功能(内存与数据库存储方案)、智能客服的 Function Calling 实现(工具定义与提示词设计),以及 ChatPDF 的 RAG 原理与向量数据库(Redis/SimpleVectorStore)集成。文章提供了完整的代码示例和配置细节,涵盖从环境搭建到业务逻辑实现的进阶开发流程。
DebugKing 发布于 2026/4/6 更新于 2026/5/22 35 浏览1.对话机器人
1.1对话机器人 - 初步实现
1.1.1引入依赖
SpringBoot 创建项目引入 Ollama/OpenAI 的依赖。
手动写入 Lombok 依赖,自动导入的有 bug。
1.1.2配置模型信息
application.yaml 中配置信息,以 ollama 为例:
spring:
application:
name: ai-demo
ai:
ollama:
base-url: http://localhost:11434
chat:
model: deepseek-r1:7b
options:
temperature: 0.8
1.1.3编写配置类 CommonConfiguration
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient (OllamaChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是一个傲娇的智能助手,身份是我的女友,请以女友的身份和傲娇的语气回答问题" )
.build();
}
}
1.1.4同步调用
同步调用,需要所有响应结果全部返回后才能返回给前端。
启动项目,在浏览器中访问:http://localhost:8080/ai/chat?prompt=你好
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
ChatClient chatClient;
String {
chatClient.prompt()
.user(prompt)
.call()
.content();
}
}
private
final
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public
chat
(String prompt)
return
1.1.5流式调用 SpringAI 中使用了 WebFlux 技术实现流式调用。
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat (String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
}
1.2对话机器人 - 日志功能
1.2.1添加日志 修改 CommonConfiguration,给 ChatClient 添加日志 Advisor。
@Configuration
public class CommonConfiguration {
@Bean
public ChatClient chatClient (OllamaChatModel model) {
return ChatClient.builder(model)
.defaultSystem("你是合肥工业大学的一名资深老学长,十分熟悉校园,请以该身份的语气和性格回答问题" )
.defaultAdvisors(new SimpleLoggerAdvisor ())
.build();
}
}
1.2.2修改日志级别 在 application.yaml 中添加日志配置,更新日志级别:
logging:
level:
org.springframework.ai: debug
com.itheima.ai: debug
1.3对接前端
1.3.1npm 运行(0 代码前端开发,待学)
1.3.2Nginx 运行
start nginx.exe
nginx.exe -s stop
1.3.3解决 CORS 问题 CORS 问题即跨域问题。前后端分离的项目,默认本地端口不一样,前端是 5173,后端是 8080。浏览器会检查响应头中的 CORS 配置是否允许当前域名访问。
SpringBoot 当中解决 CORS 问题的三种方式:
1.针对某一个接口进行配置(加注解)
在接口的方法上添加@CrossOrigin 注解。
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
@CrossOrigin("http://localhost:5173")
public Flux<String> chat (String prompt) {
return chatClient.prompt()
.user(prompt)
.stream()
.content();
}
@Configuration
public class MvcConfiguration implements WebMvcConfigurer {
@Override
public void addCorsMappings (CorsRegistry registry) {
registry.addMapping("/**" )
.allowedOrigins("*" )
.allowedMethods("GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" , "HEAD" )
.allowHeaders(true );
}
}
1.4会话记忆功能
1.4.1实现原理 让 AI 有会话记忆的方式就是把每一次历史对话内容拼接到 Prompt 中,一起发送过去。SpringAI 自带了会话记忆功能,可以帮我们把历史会话保存下来,下一次请求 AI 时会自动拼接。
1.4.2注册 ChatMemory 对象(与视频有变动) public interface ChatMemory {
default void add (String conversationId, Message message) {
this .add(conversationId, List.of(message));
}
void add (String conversationId, List<Message> messages) ;
List<Message> get (String conversationId, int lastN) ;
void clear (String conversationId) ;
}
所有的会话记忆都是与 conversationId有关联的,也就是会话 Id 。现在统一为:MessageWindowChatMemory 。
在 CommonConfiguration 中注册 ChatMemory 对象:
@Bean
public ChatMemory chatMemory () {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository ())
.maxMessages(10 )
.build();
}
@Bean
public ChatMemory chatMemory () {
return MessageWindowChatMemory.builder().build();
}
MessageWindowChatMemory默认使用的存储库就是 InMemory,默认窗口大小是 20。
1.4.3添加会话记忆 Advisor(与视频有变动) 因为使用的是 MessageWindowChatMemory,添加 advisor 的时候需要如下操作:
@Bean
public ChatClient chatClient (OllamaChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem("你是合肥工业大学的一名资深老学长,十分熟悉校园,请以该身份的语气和性格回答问题" )
.defaultAdvisors(new SimpleLoggerAdvisor ())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
在 chatClient 中传入参数 chatMemory,添加 .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())。
1.4.4设置会话 id 当前虽然实现了会话记忆功能,但是不同的人去对话,都会获取会话记忆。因此需要根据 id,区分不同的会话记忆。
前端每次发送会话请求的时候,除了发送提示词 prompt 之外,还会发送一个会话 id chatid。
在接收到 chatId 之后,将会话 id 配置到 chatClient 的 chatMemory 的 CONVERSATION_ID 属性当中。
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat (@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) {
return chatClient.prompt()
.user(prompt)
.advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))
.stream()
.content();
}
Web 界面中测试,开启新对话后,无法获取之前对话的记忆。
1.5会话历史功能 会话历史与会话记忆是两个不同的事情:
会话记忆 :是指让大模型记住每一轮对话的内容,不至于前一句刚问完,下一句就忘了。
会话历史 :是指要记录总共有多少不同的对话。
1.5.1管理会话 id 定义一个 com.hfut.ai.repository包,然后新建一个 ChatHistoryRepository接口。
package com.hfut.ai.repository;
import java.util.List;
public interface ChatHistoryRepository {
void save (String type, String chatId) ;
void delete (String type, String chatId) ;
List<String> getChatIds (String type) ;
}
@Repository
public class InMemoryChatHistoryRepository implements ChatHistoryRepository {
private final Map<String, List<String>> chatHistory = new HashMap <>();
@Override
public void save (String type, String chatId) {
List<String> chatIds = chatHistory.computeIfAbsent(type, k -> new ArrayList <>());
if (chatIds.contains(chatId)) return ;
chatIds.add(chatId);
}
@Override
public List<String> getChatIds (String type) {
return chatHistory.getOrDefault(type, new ArrayList <>());
}
}
CREATE TABLE chat_history (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
type VARCHAR (255 ) NOT NULL ,
chat_id VARCHAR (255 ) NOT NULL
);
添加 sql 和 Mybatis 依赖,配置数据库连接,定义实体类,创建 Mapper。
@Mapper
public interface ChatHistoryMapper {
@Insert("INSERT INTO chat_history (type, chat_id) VALUES (#{type}, #{chatId})")
void insert (ChatHistory chatHistory) ;
@Delete("DELETE FROM chat_history WHERE type = #{type} AND chat_id = #{chatId}")
void delete (@Param("type") String type, @Param("chatId") String chatId) ;
@Select("SELECT chat_id FROM chat_history WHERE type = #{type}")
List<String> selectChatIdsByType (String type) ;
}
编写 InSqlChatHistoryRepository 实现类,将 chatId 保存到数据库中。
1.5.2保存会话 id 修改 ChatController 中的 chat 方法,做到 3 点:
添加一个请求参数:chatId,每次前端请求 AI 时都需要传递 chatId。
每次处理请求时,将 chatId 存储到 ChatRepository。
每次发请求到 AI 大模型时,都传递自定义的 chatId。
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai")
public class ChatController {
private final ChatClient chatClient;
@Autowired
@Qualifier("inMemoryChatHistoryRepository")
private ChatHistoryRepository chatHistoryRepository;
@RequestMapping(value = "/chat", produces = "text/html;charset=utf-8")
public Flux<String> chat (@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) {
chatHistoryRepository.save(ChatType.CHAT.getValue(), chatId);
return chatClient.prompt()
.user(prompt)
.advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))
.stream()
.content();
}
}
1.5.3查询历史会话 历史会话保存在 ChatMemory 当中,通过 conversationId(chatId)获取。
前端代码的要求是返回一个 role 和 content,分别代表发言人 和发言内容 。
编写 entity 类作为返回类型:
@NoArgsConstructor
@Data
public class MessageVO {
private String role;
private String content;
public MessageVO (Message message) {
switch (message.getMessageType()) {
case USER: role = "user" ; break ;
case ASSISTANT: role = "assistant" ; break ;
default : role = "unknown" ; break ;
}
this .content = message.getText();
}
}
@RequiredArgsConstructor
@RestController
@RequestMapping("/ai/history")
public class ChatHistoryController {
private final ChatMemory chatMemory;
@Autowired
@Qualifier("inMemoryChatHistoryRepository")
private ChatHistoryRepository chatHistoryRepository;
@RequestMapping("/{type}")
public List<String> getChatIds (@PathVariable("type") String type) {
return chatHistoryRepository.getChatIds(type);
}
@RequestMapping("/{type}/{chatId}")
public List<MessageVO> getChatHistory (@PathVariable("type") String type, @PathVariable("chatId") String chatId) {
List<Message> messages = chatMemory.get(chatId);
if (messages == null ) return List.of();
return messages.stream().map(MessageVO::new ).toList();
}
}
通过数据库来保存历史会话(难点)
查看 MessageWindowChatMemory 的源码,默认使用的是 InMemoryChatMemoryRepository。
如果想要通过数据库来保存历史会话,需要以下步骤:
创建表 chat_message。
定义实体类 ChatMessage。
编写 Mapper 接口。
自定义 ChatMemory 的实现类 InSqlChatMemory。
@Component
public class InSqlChatMemory implements ChatMemory {
@Autowired
private ChatMessageMapper chatMessageMapper;
@Override
public void add (String conversationId, List<Message> messages) {
Assert.hasText(conversationId, "conversationId cannot be null or empty" );
Assert.notNull(messages, "messages cannot be null" );
for (Message message : messages) {
String role;
switch (message.getMessageType()) {
case USER: role = "user" ; break ;
case ASSISTANT: role = "assistant" ; break ;
default : role = "unknown" ; break ;
}
ChatMessage chatMessage = new ChatMessage ();
chatMessage.setConversationId(conversationId);
chatMessage.setRole(role);
chatMessage.setContent(message.getText());
chatMessageMapper.save(chatMessage);
}
}
@Override
public List<Message> get (String conversationId) {
Assert.hasText(conversationId, "conversationId cannot be null or empty" );
List<ChatMessage> chatMessages = chatMessageMapper.findByConversationId(conversationId);
List<Message> messages = new ArrayList <>();
for (ChatMessage chatMessage : chatMessages) {
switch (chatMessage.getRole()) {
case "user" : messages.add(new UserMessage (chatMessage.getContent())); break ;
case "assistant" : messages.add(new AssistantMessage (chatMessage.getContent())); break ;
default : throw new IllegalArgumentException ("Unknown role: " + chatMessage.getRole());
}
}
return messages;
}
@Override
public void clear (String conversationId) {
chatMessageMapper.deleteByConversationId(conversationId);
}
}
在 CommonConfiguration 里配置下 ChatMemory:
@Bean
public ChatMemory chatMemory () {
return new InSqlChatMemory ();
}
1.6总结 - 对话机器人
1.6.1基本实现 1.1-1.3 属于是基本配置,需要注意的就是解决 CORS 问题。
1.6.2会话记忆实现 再次复盘会话记忆和会话历史的区别。
会话记忆 :是指让大模型记住每一轮对话的内容。
会话历史 :是指要记录总共有多少不同的对话。
会话记忆的实现,根据三步走就可以实现:
配置 ChatMemory。
在 ChatClient 当中通过 Advisor 加入 ChatMemory。
进行会话时设置会话 id。
1.6.3会话历史实现(难点) 会话历史分为两个部分:会话 id 和具体会话内容。
会话内容是保存在 ChatMemory 当中的,需要通过 ChatId(conversationId)去获取。
会话 id 是我们自己设计方式去保存的。
保存会话 id:内存保存或数据库保存。
保存会话内容:内存保存或数据库保存(仿造 MessageWindowChatMemory 写一个 ChatMemory 的实现类)。
2.哄哄模拟器(纯 prompt 开发)
2.1提示词工程 通过优化提示词,让大模型生成出尽可能理想的内容,这一过程就称为提示词工程(Prompt Engineering)。
2.2代码实现
2.2.1配置 OpenAI 参数 spring:
ai:
openai:
base-url: https://dashscope.aliyuncs.com/compatible-mode
api-key: ${OPENAI_API_KEY}
chat:
options:
model: qwen-max-latest
为了防止 api-key 泄露,使用了${OPENAI_API_KEY}来读取环境变量。
2.2.2配置 ChatClient 我们可以配置多个 ChatClient 用于不同的场景。
@Bean
public ChatClient gameChatClient (OpenAiChatModel model, ChatMemory chatMemory) {
return ChatClient.builder(model)
.defaultSystem(SystemConstants.GAME_SYSTEM_PROMPT)
.defaultAdvisors(new SimpleLoggerAdvisor ())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.build();
}
自定义提示词 SystemConstants.GAME_SYSTEM_PROMPT。
2.2.3编写 Controller @RequestMapping("/ai")
@RestController
@RequiredArgsConstructor
public class GameController {
private final ChatClient gameChatClient;
@RequestMapping(value = "/game", produces = "text/html;charset=utf-8")
public Flux<String> chat (@RequestParam("prompt") String prompt, @RequestParam("chatId") String chatId) {
return gameChatClient.prompt()
.user(prompt)
.advisors(a->a.param(ChatMemory.CONVERSATION_ID,chatId))
.stream()
.content();
}
}
使用新的 gameChatClient,修改路径为/game。
2.3总结
提示词工程,也就是 prompt 文案的设计。
代码方面,如果自定义了一个 ChatMemory 的实现类,在 Client 里直接传入即可。
3.智能客服(Function Calling)
3.1实现思路 由于 AI 擅长的是非结构化数据的分析,如果需求中包含严格的逻辑校验或需要读写数据库,纯 Prompt 模式就难以实现了。此时就需要通过 Function Calling 来实现。
Function Calling 的作用是将数据库的操作定义为 Function(Tool),在提示词中告诉大模型什么情况下需要调用什么工具。
简化步骤:
编写基础提示词(不包括 Tool 的定义)。
编写 Tool(Function)。
配置 Advisor(SpringAI 利用 AOP 帮我们拼接 Tool 定义到提示词,完成 Tool 调用动作)。
3.2基础 CRUD
3.2.1数据库表
3.2.2引入依赖(已配置)
3.2.3配置数据库(已配置)
3.2.4基础代码(MyBatisPlus 生成) 实体类、Mapper 接口、Service 层代码由 MyBatisPlus 生成。
3.3定义 Function(与课程有变动)
根据条件筛选和查询课程。
根据校区名称查询当前校区的所有课程。
新增课程预约单。
3.3.1查询条件分析 封装查询条件的类 ElectiveCourseQuery,使用@ToolParam 注解。
3.3.2定义 Function(关键) @Component
public class ElectiveCourseTools {
@Tool(description = "根据条件查询选修课程")
public List<ElectiveCourse> queryElectiveCourse (@ToolParam(required = false, description = "选修课程查询条件") ElectiveCourseQuery query) {
}
}
3.4System 提示词设计
3.4.1安全防范措施
3.4.2调用规则设计(关键) 设计选修课程咨询规则和课程预约规则,引导大模型按流程调用工具。
发现漏洞并优化编写新的 Function,例如:
如果没有找到符合要求的课程,根据年级查询该年级可选的其他课程。
星期几和周几的转换问题,编写工具方法进行解析。
用户所选课程与所选校区不匹配的情况,增加判断方法。
3.4.3完整代码 完整的 Tools 代码和 System 提示词配置。
3.5配置 ChatClient 为智能客服定制一个 ChatClient,添加工具调用的功能。
@Bean
public ChatClient serviceChatClient (OpenAiChatModel model, ChatMemory chatMemory, ElectiveCourseTools electiveCourseTools) {
return ChatClient.builder(model)
.defaultSystem(SystemConstants.SERVICE_SYSTEM_PROMPT)
.defaultAdvisors(new SimpleLoggerAdvisor ())
.defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build())
.defaultTools(electiveCourseTools)
.build();
}
3.6编写 Controller 编写 CustomerServiceController 类,对接前端接口。
3.7存储到数据库(再详谈)
ChatMemory:负责把会话内容存入和取出内存/数据库。
ChatHistoryRepository:负责把会话 id 存入内存/数据库。
ChatHistoryController:负责把会话 id 和会话内容取出。
3.8总结 总结一下 Function Calling 的整体流程:
数据库构造。
Function 定义。
System 提示词设计。
配置 ChatClient 和 Controller。
4.ChatPDF(RAG)
4.1RAG 原理 为了解决大模型的知识限制问题,外挂一个知识库。通过向量模型将文本向量化,利用向量距离来判断文本相似度,从庞大的知识库中找到与用户问题相关的内容。
4.1.1向量模型 向量模型可以将文本转化为坐标,推断两份数据的相似度。阿里云百炼平台提供了 text-embedding-v3 模型。
4.1.2向量模型测试 编写工具类计算向量之间的欧氏距离和余弦距离,测试向量化效果。
4.1.3向量数据库(进阶) 向量数据库的主要作用有两个:存储向量数据、基于相似度检索数据。
SpringAI 支持多种向量数据库,如 Redis Vector Store、SimpleVectorStore 等。
4.1.3.1安装 docker 和 Redis 搭建虚拟机,安装 Docker,配置 MySQL 和 Redis。
4.1.3.2 SimpleVectorStore(原教程)
4.1.3.3 Redis Vector Store
4.1.3.4.VectorStore 接口 VectorStore 操作向量化的基本单位是 Document。
4.1.4文件的读取和转化 SpringAI 提供了各种文档读取的工具 DocumentReader,如 PagePdfDocumentReader。
编写单元测试测试向量库功能。
注意 :Redis Vector Store 如果是按照我的方法自动配置的,那么不需要去修改 CommonConfiguration。如果要使用 SimpleVectorStore,那么就需要去修改 CommonConfiguration。
今天就基本上更新到这里了,测试能过的情况,后续无非就是和前端输入对接了。剩下的内容只剩一点点了,我会加紧更新,马上要完结撒花了!
相关免费在线工具 Keycode 信息 查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
Escape 与 Native 编解码 JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
JavaScript / HTML 格式化 使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
JavaScript 压缩与混淆 Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online