Sping AI接入deepseek
Sping AI 概述
在经历了八个里程碑式的版本之后(M1~M8),Spring AI 1.0 正式版本,终于在 2025 年 5 月 20 日正式发布了,这是另一个新高度的里程碑式的版本,标志着 Spring 生态系统正式全面拥抱人工智能技术,并且意味着 Spring AI 将会给企业带来稳定 API 支持。
Spring AI 是 Spring 官方推出的一个人工智能集成框架,旨在简化 AI 功能在 Spring 应用中的整合。它提供了一套标准化的 API 和工具,让开发者能够轻松接入多种主流 AI 服务(如 OpenAI、Azure AI、Hugging Face 等),同时保持 Spring 生态的简洁性和灵活性。
1、spring AI 是一个框架,这个框架是spring家族中的一员,是一个抽象层,用来继承大语言模型
2、只需要写一份代码即可,修改配置文件,切换大语言模型
核心特性
- 统一 API 设计
- 通过抽象层屏蔽不同 AI 提供商(如 OpenAI、Gemini、Ollama 等)的接口差异,开发者只需使用 Spring AI 的通用接口(如 ChatModel),即可切换底层 AI 服务。
- 开箱即用的功能支持
- 支持对话模型(Chat)类的大模型:与 ChatGPT 类似的交互。
- 支持嵌入模型(Embedding)类的大模型:嵌入模型是将文本、图像或其他数据转换为数值向量(即嵌入向量)的技术。
- 支持图像生成(Image Generation)类的大模型:图像生成是指AI根据文本描述或其他输入创建新图像的能力。
- 支持函数调用(Function Calling):函数调用功能使AI模型能够与外部API和服务交互。也就是说,你写一个函数,AI 也能调用你写的函数。
- 与 Spring 生态无缝集成
- 支持 Spring Boot 自动配置、依赖注入、Actuator 监控等,与 Spring Security、Spring Data 等组件协同工作。
- Prompt(提示) 工程支持
- 提供
PromptTemplate
等工具,方便动态生成提示词(Prompts),支持上下文管理。(根据会话记录,来回答问题 )
- 提供
- 模块化设计
- 开发者可以根据项目需求选择特定的AI服务模块
- 例如只需OpenAI功能就只引入 spring-ai-openai
- 如需Google Vertex AI则引入spring-ai-vertexai
- 每个AI供应商/服务有独立的Spring Starter模块
适用场景
- 快速构建 AI 驱动的应用(如智能客服、内容生成工具)。
- 需要灵活切换 AI 后端(如从本地模型切换到云服务)。
- 结合 Spring 生态实现企业级 AI 功能(如权限控制、数据持久化)。
官网与资源
AI 提供商与模型类型
在SpringAI中,模型类型和AI提供商是两个不同维度的概念,但它们又相互关联。让我用更清晰的方式帮你区分和理解:
模型类型(Model Type)
指的是AI模型的功能类别,即它能完成什么任务。
特点:与具体厂商无关,是通用的能力分类。
常见模型类型:
模型类型 | 功能说明 | 典型应用场景 |
---|---|---|
Chat(对话型) | 对话交互(如ChatGPT) | 客服机器人、聊天助手 |
Embedding(嵌入型) | 将文本,视频,图片,声音转换为向量(数值数组) | 语义搜索、RAG + 传统搜索基于关键词匹配(如Google早期的搜索),而语义搜索通过理解查询的语义(含义)来返回更相关的结果。 + 它利用深度学习模型(如BERT、Embedding模型)将文本转换为向量(vector),通过向量相似度匹配内容,即使查询词和文档没有直接的关键词重叠。 |
Image(文生图型) | 生成/处理图像(如Stable Diffusion) | 设计辅助、内容生成 |
Text-to-Speech(文转语音型) | 将文本转为语音 | 语音助手、有声内容 |
Function Calling(函数回调型) | 让AI调用外部函数/API | 实时数据查询、工作流自动化 |
AI提供商(Provider)
提供具体AI模型服务的公司或平台。
特点: 同一提供商可能支持多种模型类型
常见提供商:
提供商 | 支持的模型类型 | 具体模型示例 |
---|---|---|
OpenAI | Chat, Embedding, Image | GPT-4o、text-embedding-3、DALL-E |
Google Vertex AI | Chat, Embedding, Image | PaLM 2、Imagen |
Azure OpenAI | Chat, Embedding | 微软托管的OpenAI服务 |
Hugging Face | Chat, Embedding, Image | 开源模型(如BLOOM、Stable Diffusion) |
Stability AI | Image | Stable Diffusion系列 |
DeepSeek | Chat,代码专用模型、Embedding、数学专用模型 | DeepSeek-V3、DeepSeek-Coder、DeepSeek-Embedding、DeepSeek-Math |
两者的关系
- 一个提供商支持多种模型类型
例如:OpenAI同时提供Chat模型(GPT-4)、Embedding模型(text-embedding-3)、Image模型(DALL-E)。 - 一种模型类型可由多个提供商实现
例如:Chat模型既可以用OpenAI的GPT-4,也可以用Google的PaLM 2。
Spring AI 框架支持哪些 AI 提供商的哪些模型
Spring AI 框架支持主流的 AI 提供商的主流模型,并且会随着 Spring AI 版本的升级而变化。
以下是截至 2024年6月,Spring AI 框架官方及社区支持的 AI 提供商及其对应的 模型类型的详细列表,包含国内和国外主流厂商。
AI 提供商 | 支持的模型类型 | 具体模型示例 | 是否国内厂商 | Spring AI 模块名 |
---|---|---|---|---|
OpenAI | Chat, Embedding, Image Generation, Function Calling | GPT-4, GPT-3.5, text-embedding-3, DALL-E 3 | ❌ | spring-ai-openai |
Azure OpenAI | Chat, Embedding, Image Generation | GPT-4, GPT-3.5, text-embedding-ada-002 | ❌ | spring-ai-azure-openai |
Google Vertex AI | Chat, Embedding, Code Generation | Gemini 1.5, PaLM 2, textembedding-gecko | ❌ | spring-ai-vertexai |
Hugging Face | Chat, Embedding, Text Generation, Image Generation | BLOOM, Llama 2, Stable Diffusion, BERT | ❌ | spring-ai-huggingface |
Stability AI | Image Generation | Stable Diffusion XL, Stable Diffusion 3 | ❌ | spring-ai-stabilityai |
Anthropic | Chat | Claude 3, Claude 2 | ❌ | spring-ai-anthropic (社区支持) |
Ollama | Chat, Embedding (本地运行开源模型) | Llama 3, Mistral, Gemma | ❌ | spring-ai-ollama |
DeepSeek | ❌ (尚未官方支持,但未来可能集成) | DeepSeek-V3, DeepSeek-Coder | ✅ | 暂无,虽然没有给 deepseek 提供专门的 starter,但是由于 deepseek API 接口规范与 OpenAI 保持一致,因此也可以使用 openai 的 starter。 |
百度文心大模型 | ❌ (尚未官方支持) | ERNIE-Bot 4.0, ERNIE-Embedding | ✅ | 暂无 |
阿里云通义千问 | ❌ (尚未官方支持) | Qwen-72B, Qwen-Embedding | ✅ | 暂无 |
智谱AI (GLM) | ❌ (尚未官方支持) | ChatGLM3, GLM-Embedding | ✅ | 暂无 |
讯飞星火 | ❌ (尚未官方支持) | SparkDesk 3.0 | ✅ | 暂无 |
MiniMax | ❌ (尚未官方支持) | ABAB 5.5 | ✅ | 暂无 |
spring-ChatModel
什么是chatModel
chat Model 即"聊天模型”,它是 spring AI 中处理对话的核心组件,负责将用户的输入(如文本、图像、语音等多模态数据)转换为AI模型的指令,并返回结构化的响应。
工作原理:
【源码】:
public interface ChatModel extends Model<Prompt, ChatResponse>, StreamingChatModel {
// 简化的AI模型交互入口,无需掌握Prompt和chatResponse的细节即可立马上手
//1.message参数被封装为Prompt
//2.返回的是chatResponse的"文本"部分
default String call(String message) {
Prompt prompt = new Prompt(new UserMessage(message));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getText() : "";
}
default String call(Message... messages) {
Prompt prompt = new Prompt(Arrays.asList(messages));
Generation generation = call(prompt).getResult();
return (generation != null) ? generation.getOutput().getText() : "";
}
//标准的AI交互入口
@Override
ChatResponse call(Prompt prompt);
default ChatOptions getDefaultOptions() {
return ChatOptions.builder().build();
}
default Flux<ChatResponse> stream(Prompt prompt) {
throw new UnsupportedOperationException("streaming is not supported");
}
}
streamingchatmodel 接口提供了"流式响应"的功能,使模型的输出内容展现出更优效果
什么是流式响应的效果?
答:输出的效果呈现打字机的形式,一个一个字出来。
import java.util.Arrays;
import reactor.core.publisher.Flux;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.model.StreamingModel;
@FunctionalInterface
public interface StreamingChatModel extends StreamingModel<Prompt, ChatResponse> {
default Flux<String> stream(String message) {
Prompt prompt = new Prompt(message);
return stream(prompt).map(response -> (response.getResult() == null || response.getResult().getOutput() == null
|| response.getResult().getOutput().getText() == null) ? ""
: response.getResult().getOutput().getText());
}
default Flux<String> stream(Message... messages) {
Prompt prompt = new Prompt(Arrays.asList(messages));
return stream(prompt).map(response -> (response.getResult() == null || response.getResult().getOutput() == null
|| response.getResult().getOutput().getText() == null) ? ""
: response.getResult().getOutput().getText());
}
//标准的AI交互入口
@Override
Flux<ChatResponse> stream(Prompt prompt);
}
ChatClient接囗
chatclient 接口提供了高层封装(如:Fluent API 流式API,提供链式调用)、简化了开发,但是底层仍调用 chatmodel。
public interface ChatClient {
//1.手工模式创建指定模型的chatclient实例
static ChatClient create(ChatModel chatModel) {
return create(chatModel, ObservationRegistry.NOOP);
}
//2.建造者模式创建系统默认的chatclient实例
interface Builder {
ChatClient build();
}
}
注入实例
下面有整体的实例,这个只是拓展另一种方式,底层也是使用chatmodel。单击:将下面的接入deepseek跑通再来看,详细介绍功能点。
package com.gj.config;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author GJ
* @date 2025/6/24 21:36
*/
@Configuration
public class SpringAIConfig {
/**
* 有两种法式创建大模型客户端,任选其一
* @param builder
* @return
*/
// 1、创建基于默认的大模型客户端
@Bean
public ChatClient openAiChatClient(ChatClient.Builder builder) {
return builder.build();
}
// 2、创建基于模型的客户端,参数传不同的大模型,可以创建不同的客户端
@Bean
public ChatClient openAiChatClient(OpenAiChatModel openAiChatModel) {
return ChatClient.create(openAiChatModel);
}
}
在controller层进行调用:
package com.gj.controller;
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
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;
/**
* @author GJ
* @date 2025/6/24 21:41
*/
@RestController
@RequestMapping("/api")
public class ChatModelController {
@Resource
private ChatClient chatClient;
@GetMapping("/chat")
public String chat(@RequestParam(value = "msg",defaultValue = "deepseek") String msg) {
return chatClient
.prompt() //设置请求上下文(如角色和内容)
.user(msg)//设置用户输入消息
.call()//发送请求并获取模型生成的响应
.content(); //获取响应内容
}
}
springAI-Prompt
什么是Prompt
Prompt
即“提示词",也可叫做"引导词"。它是引导 AI生成特定输出的输入指令,直接影响模型响应质量。
注意:本文从开发者角度讨讹Prompt而不是使用者角度
使用者角度给到的提示词越来越细致。
我们将提示词转换成对应的编码,这个就是我们要解决的问题?怎么来解决这个问题呢!
我们这时需要用到prompt
得到prompt 通过
1、SystemMessage 系统角色、系统消息
2、UserMessage 用户输入的信息
3、ChatOptions 聊天模型的参数信息
4、 将前三步的信息进行组合最终得到prompt
import jakarta.annotation.Resource;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
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 java.util.List;
/**
* @author GJ
* @date 2025/6/24 21:41
*/
@RestController
@RequestMapping("/api")
public class ChatModelController {
@Resource
private ChatClient chatClient;
@GetMapping("/chat")
public String chat(@RequestParam(value = "msg",defaultValue = "deepseek") String msg) {
// 1、系统角色
SystemMessage systemMessage = new SystemMessage("你是项目经理");
// 2、用户消息
UserMessage userMessage = new UserMessage(msg);
// 3、模型参数
ChatOptions chatOptions = ChatOptions.builder()
.temperature(0.5) //多样化系数
.maxTokens(500) //限制token用量,防止模型输出过长,这也导致可能输出内容的缺失,没有特殊要求一般注释
.build();
//4、进行组合
Prompt prompt = new Prompt(List.of(systemMessage, userMessage), chatOptions);
return chatClient
.prompt(prompt) //设置请求上下文(如角色和内容)
.call()//发送请求并获取模型生成的响应
.content(); //获取响应内容
}
}
简化写法
@GetMapping("/simpleChat")
public String simpleChat(@RequestParam(value = "msg",defaultValue = "deepseek") String msg) {
// 3、模型参数
ChatOptions chatOptions = ChatOptions.builder()
.temperature(0.5) //多样化系数
// .maxTokens(500) //限制token用量,防止模型输出过长,这也导致可能输出内容的缺失
.build();
return chatClient
.prompt() //设置请求上下文(如角色和内容)
.system("你是项目经理")//设置系统消息
.user(msg)//设置用户输入消息
.options(chatOptions)//设置模型参数
.call()//发送请求并获取模型生成的响应
.content(); //获取响应内容
}
}
全局配置
“模型参数"等每次对话中相对固定的部分作为全局配置,如果需要使用可将"系统角色"、全局配置时可不再额外指定。
如果系统角色也想设置为全局:
// 1、创建基于默认的大模型客户端
@Bean
public ChatClient openAiChatClient(ChatClient.Builder builder) {
// return builder.build();
return builder.defaultSystem("你是项目经理")
.build();
}
角色定义
Prompt通过结构化消息角色(SYSTEM/USER/ASSISTANT)明确交互意图
1.SYSTEM:定义系统角色(给AI立个人设,如"你是项目经理")
2. USER: 定义用户请求(接收用户输入)
3.ASSISTANT:定义助手角色(存储和传递聊天内容")
@GetMapping("/assistan/chat")
public String assistanChat(@RequestParam(value = "msg",defaultValue = "deepseek") String msg) {
// 1、系统角色
SystemMessage systemMessage = new SystemMessage("你是项目经理");
// 2、用户消息
UserMessage userMessage = new UserMessage(msg);
// 助手角色
AssistantMessage assistantMessage = new AssistantMessage("用户曾经将时间标注上去");
// 3、模型参数
ChatOptions chatOptions = ChatOptions.builder()
.temperature(0.5) //多样化系数
// .maxTokens(500) //限制token用量,防止模型输出过长,这也导致可能输出内容的缺失
.build();
//4、进行组合,注意顺序一定要一致,否则会失效
Prompt prompt = new Prompt(List.of(systemMessage,assistantMessage, userMessage), chatOptions);
return chatClient
.prompt(prompt) //设置请求上下文(如角色和内容)
.call()//发送请求并获取模型生成的响应
.content(); //获取响应内容
}
}
运行结果:
SpringAl-PromptTemplate
1.什么是PromptTemplate
PormptTemplate 是生成 Prompt 的预定义模版类。通过"占位符"动态注入变量值生成提示词,可降低代码冗余,提升可维护性。
@GetMapping("/promptTemplate/chat")
public String promptTemplateChat(@RequestParam(value = "software",defaultValue = "deepseek") String software) {
// 1、提供模版
String template = "推荐{software}最火的事件";
PromptTemplate promptTemplate = new PromptTemplate(template);
promptTemplate.add("software",software);
Prompt prompt = promptTemplate.create();
return chatClient
.prompt(prompt) //设置请求上下文(如角色和内容)
.call()//发送请求并获取模型生成的响应
.content(); //获取响应内容
}
结果:
多角色模版
使用 PormptTemplate 可构建不同角色的消息,有助于模型分析上下文、明确角色之间交互的意图。
父类PromptTemplate
子类:
-SystemPromptTemplate //系统角色提示模板
-AssistantPromptTemplate //助手角色提示模板
-FunctionPromptTempliite // 函数&工具调用角色提示模板
@GetMapping("/multiple/PromptTemplate/chat")
public String multiplePromptTemplateChat(@RequestParam(value = "msg",defaultValue = "deepseek") String msg) {
// 1、提供模版
String template = "推荐{msg}最火的事件";
PromptTemplate systemPromptTemplate = new SystemPromptTemplate(template);
String role="你是项目经理";
Message message = systemPromptTemplate.createMessage(Map.of("role", role));
//用户角色
UserMessage userMessage = new UserMessage(msg);
Prompt prompt = new Prompt(List.of(message, userMessage));
return chatClient
.prompt(prompt) //设置请求上下文(如角色和内容)
.call()//发送请求并获取模型生成的响应
.content(); //获取响应内容
}
加载外部资源
可以将提示数据放在文件中(如:system-message.st),然后将该资源直接传递给 PromptTemplate
@Value("classpath:/prompts/system-message.st")
private org.springframework.core.io.Resource systemMessages;
@GetMapping("/multiple/PromptTemplate/chat")
public String multiplePromptTemplateChat(@RequestParam(value = "msg",defaultValue = "deepseek") String msg) {
// 1、提供模版
// String template = "推荐{msg}最火的事件";
System.out.println(systemMessages);
PromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemMessages);
String role="你是项目经理";
Message message = systemPromptTemplate.createMessage(Map.of("role", role));
//用户角色
UserMessage userMessage = new UserMessage(msg);
Prompt prompt = new Prompt(List.of(message, userMessage));
return chatClient
.prompt(prompt) //设置请求上下文(如角色和内容)
.call()//发送请求并获取模型生成的响应
.content(); //获取响应内容
}
}
结果:
自定义模板
提示模板支持自定义,例如:使用带有’‘和’>'分隔符的自定义渲染器模板
SpringAl-基于内存的多轮对话实现
1.什么是对话记忆
对话记忆是指大模型能够在一段时间内记住用户提供的信息。
1.用户信息持久化:使用内存或数据库保存用户的背景与偏好
2.上下文跟踪:在多轮对话中保持上下文一致性
3.较高的回答质量:用户询问或者追问之前提到的内容时可"回忆追溯"
总结:对话记忆有助于提供更加个性化和精准的回复!
对话记忆需要区分不同的会话ID(conversationrd),以便于隔离不同用户的会话
ChatMemory接囗
chatMemory 接口用于存储和管理"对话记忆"
基于内存的多轮对话实现
- AI模型接收Prompt输入,使用chatResponse输出(响应)
- 系统消息仅需首次添加至对话记忆;用户消息、助手消息则每次都要添加至对话记忆
依赖引入:
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId>
</dependency>
配置文件
@Bean
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository()) //对话记忆使用内存存储,程序关闭记录消失
.maxMessages(10) //对话记忆窗口大小,{记录条数}
.build();
}
访问:
@Resource
private ChatMemory chatMemory;
@GetMapping("/memeory/PromptTemplate/chat")
public String memeoryPromptTemplateChat(String question, HttpSession session) {
// 1、生成会话ID
String id = session.getId();
String template = "你是项目经理";
PromptTemplate systemPromptTemplate = new SystemPromptTemplate(template);
Message message = systemPromptTemplate.createMessage();
// 2、初始化系统消息(首次)
if (chatMemory.get(id).isEmpty()) {
//将系统消息存入会话中
chatMemory.add(id, message);
}
// 3、获取历史信息
List<Message> historyMessage = chatMemory.get(id);
System.out.println("对话记忆长度:"+historyMessage);
//4、用户信息
UserMessage userMessage1 = new UserMessage(question);
//5、新建集合避免污染历史记录
ArrayList<Message> messages = new ArrayList<>(historyMessage);
//6|将用户信息存入会话中
messages.add(userMessage1);
Prompt prompt = new Prompt(messages);
String content = chatClient
.prompt(prompt) //设置请求上下文(如角色和内容)
.call()//发送请求并获取模型生成的响应
.content();//获取响应内容
// 7、将生成的内容存入会话中,将本次AI相应添加至对话记忆中(助手角色)
chatMemory.add(id, new AssistantMessage(content));
return content;
}
记录信息:
第一次访问:你是谁
第二次访问:我是张三
第三次访问:我是谁
限制历史记录
MessagewindowchatMemory 是 chatMemory 接囗的实现类,用于限制消息窗口大小(保留最近 N 条消息)。
持续更新中…
SpringAl-基于JDBC的多轮对话实现
1.基于JDBC的原因
对话记忆 的存储库默认使用内存(InMemorychatMemoryRepository),数据容易丢失。
public ChatMemory chatMemory() {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(new InMemoryChatMemoryRepository()) //对话记忆使用内存存储,程序关闭记录消失
.maxMessages(10) //对话记忆窗口大小,{记录条数}
.build();
}
ChatMemoryRepository
chatmemoryRepository 接口是对话记忆存储的抽象。支持多种存储方式,例如:内存、JDBC、
Redis 等等,每种存储方式都有特定的实现类。
jdbcChatMemoryRepository
JdbcchatMemoryRepository 使用 JDBC 方式存储对话记忆的实现类。它包含在spring-ai-starter-mode1-chat-memory-repository-jdbc 模块当中。
依赖引入:
<!-- Spring AI jdbc存储 -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-chat-memory-repository-jdbc</artifactId>
</dependency>
<!-- mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
配置改成jdbc:
@Bean
public ChatMemory chatMemory(JdbcChatMemoryRepository jdbcChatMemoryRepository) {
return MessageWindowChatMemory.builder()
.chatMemoryRepository(jdbcChatMemoryRepository) //对话记忆使用jdbc存储
.maxMessages(10) //对话记忆窗口大小,{记录条数}
.build();
}
配置文件:
# Spring AI OpenAI/DeepSeek 配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/jdbc-ai?useSSL=false&serverTimezone=UTC
username: root
password: 123456
ai:
chat:
memory:
repository:
jdbc:
schema: classpath:/schema/springai-jdbc.sql
# 是否初始化表结构,可选值包括:
# 1.embedded - 仅针对嵌入式数据库自动创建表。默认值
#2.always - 任意数据库都强制建表。首次执行推荐
# 3.never - 禁用自动建表。首次执行后推荐
initialize-schema: always
数据库表名必须是:spring_ai_chat_memory
没有mysql,但是有别的数据库,可以更具仿写:
CREATE TABLE `spring_ai_chat_memory` (
`conversation_id` VARCHAR(36) NOT NULL COMMENT '会话id',
`content` TEXT NOT NULL COMMENT '对话内容',
`type` VARCHAR(10) NOT NULL COMMENT '角色信息',
`timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '时间戳'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Spring AI对话记忆表';
@Resource
private ChatMemory chatMemory;
@GetMapping("/memeory/PromptTemplate/chat")
public String memeoryPromptTemplateChat(String question, HttpSession session) {
// 1、生成会话ID
String id = session.getId();
String template = "你是项目经理";
PromptTemplate systemPromptTemplate = new SystemPromptTemplate(template);
Message message = systemPromptTemplate.createMessage();
// 2、初始化系统消息(首次)
if (chatMemory.get(id).isEmpty()) {
//将系统消息存入会话中
chatMemory.add(id, message);
}
// 3、获取历史信息
List<Message> historyMessage = chatMemory.get(id);
System.out.println("对话记忆》》》》:"+historyMessage);
//4、用户信息
UserMessage userMessage1 = new UserMessage(question);
//5、新建集合避免污染历史记录
ArrayList<Message> messages = new ArrayList<>(historyMessage);
//6|将用户信息存入会话中
messages.add(userMessage1);
Prompt prompt = new Prompt(messages);
String content = chatClient
.prompt(prompt) //设置请求上下文(如角色和内容)
.call()//发送请求并获取模型生成的响应
.content();//获取响应内容
// 7、将生成的内容存入会话中,将本次AI相应添加至对话记忆中(助手角色)
chatMemory.add(id, new AssistantMessage(content));
return content;
}
当时当关闭程序,重启之后,访问结果并没有更具jdbc中存储的记录两种情况:
1、配置yml中是否初始化表。
2、访问中的会话id是否一致(可以通过前端页面 访问时传送过来)。
Spring AI 接入 DeepSeek
DeepSeek 的 API 设计兼容 OpenAI:DeepSeek 的 API 接口规范(如请求/响应格式、鉴权方式)与 OpenAI 保持一致,因此可以直接使用 OpenAI 的客户端库调用 DeepSeek。
spring ai会根据配置文件application. yml 中配置的大模型自动创建ChatModel对象
jdk>=17
springboot:3.3.9(注意:springboot版本要求为3.2.x和3.3.x)
引入依赖
<!-- Spring MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--
DeepSeek 的 API 设计兼容 OpenAI:
DeepSeek 的 API 接口规范(如请求/响应格式、鉴权方式)与 OpenAI 保持一致,
因此可以直接使用 OpenAI 的客户端库调用 DeepSeek。
-->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
配置文件
server:
port: 8888
# Spring AI OpenAI/DeepSeek 配置
spring:
ai:
openai:
api-key: sk-b7d2d672216b46209dd6e40exxxx # 设置 DeepSeek API 的访问密钥,永远不要将密钥提交到代码仓库,建议通过环境变量注入。-收费
base-url: https://api.deepseek.com # 指定 DeepSeek API 的基础地址,格式与OpenAI相同。
chat:
options:
model: deepseek-chat # 选择要调用的 DeepSeek 模型名称,必须与 DeepSeek 支持的模型列表匹配(如 deepseek-chat、deepseek-coder 等),不同模型可能有不同的计费标准和能力。
temperature: 1.3 # temperature 值越高,AI 回答越随机和创意;值越低,回答越确定和保守。1.3 属于高值,适合需要发散性输出的场景,但可能牺牲准确性。
service层
package com.gj.service;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
import org.springframework.stereotype.Service;
import java.util.Map;
//这里会自动生成一个带有ChatModel参数的构造方法,显然这里使用的是棉造注入。
@RequiredArgsConstructor
@Service
public class AiService {
//springai会自动根据application. yml配置文件自动创建ChatModel对象。并目纳入IoC方法来和大语言模型通信。
//ChatModel是Spring AI的核接口,通过这个接口中的call方法来和大语言模型通信
private final ChatModel chatModel;
// 简单的直接调用一次性返回结果
public String generate(String message) {
return chatModel.call(message);
}
// 简单的直接调用,一个一个字将结果返回
// public Flux<String>generate(String message) {
// return chatModel.stream(message);
//}
// 使用系统提示模板
public String generateWithSystemPrompt(String userMessage) {
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate("""
你是一个资深{domain}专家,回答需满足以下要求:
1. 语言风格:{tone}
2. 回答长度:{length}
3. 用户问题:{userMessage}
""");
Prompt prompt = new Prompt(
systemPromptTemplate.createMessage(Map.of(
"domain", "科技",
"tone", "幽默",
"length", "不超过100字",
"userMessage", userMessage
))
);
return chatModel.call(prompt).getResult().getOutput().getText();
}
}
controller 层
package com.gj.controller;
import com.gj.service.AiService;
import lombok.RequiredArgsConstructor;
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;
@RestController
@RequestMapping("/api/ai")
@RequiredArgsConstructor
public class AiController {
private final AiService aiService;
//普通聊天
@GetMapping("/chat")
public String chat(@RequestParam String message) {
System.out.println("收到消息:" + message);
return aiService.generate(message);
}
//带有提示的聊天
@GetMapping("/chat-with-prompt")
public String chatWithPrompt(@RequestParam String message) {
return aiService.generateWithSystemPrompt(message);
}
}
前端页面使用deepseek生成
后端地址是http://localhost:8888/api/ai/chat?message=xxx
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeepSeek AI - 智能助手</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
}
body {
background: linear-gradient(135deg, #0c0f1d 0%, #1a1e2e 50%, #1c1124 100%);
color: #e0e0ff;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
overflow-x: hidden;
}
.container {
width: 100%;
max-width: 900px;
background: rgba(15, 18, 32, 0.85);
border-radius: 20px;
overflow: hidden;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(92, 107, 192, 0.3);
backdrop-filter: blur(10px);
position: relative;
}
/* 顶部霓虹灯效果 */
.header {
background: linear-gradient(90deg, #0a0e1a, #1c1f3a);
padding: 25px 30px;
text-align: center;
border-bottom: 1px solid rgba(92, 107, 192, 0.3);
position: relative;
overflow: hidden;
}
.header::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 200%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(92, 107, 192, 0.2),
transparent);
animation: shine 3s infinite;
}
@keyframes shine {
0% { left: -100%; }
100% { left: 100%; }
}
.logo {
display: flex;
align-items: center;
justify-content: center;
gap: 15px;
}
.logo-icon {
font-size: 2.5rem;
color: #6c7bff;
text-shadow: 0 0 15px rgba(108, 123, 255, 0.7);
}
.logo-text {
font-size: 2.2rem;
font-weight: 700;
background: linear-gradient(90deg, #6c7bff, #a66cff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
text-shadow: 0 0 20px rgba(108, 123, 255, 0.4);
}
.tagline {
margin-top: 10px;
font-size: 1.1rem;
color: #a0a8ff;
letter-spacing: 1px;
}
/* 聊天区域 */
.chat-container {
height: 500px;
padding: 20px;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 25px;
}
/* 自定义滚动条 */
.chat-container::-webkit-scrollbar {
width: 8px;
}
.chat-container::-webkit-scrollbar-track {
background: rgba(20, 23, 42, 0.5);
border-radius: 4px;
}
.chat-container::-webkit-scrollbar-thumb {
background: linear-gradient(#6c7bff, #a66cff);
border-radius: 4px;
}
.message {
max-width: 80%;
padding: 18px 22px;
border-radius: 18px;
line-height: 1.6;
position: relative;
animation: fadeIn 0.4s ease-out;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.user-message {
background: linear-gradient(135deg, #3a3f8f, #4a2f7a);
align-self: flex-end;
border-bottom-right-radius: 5px;
}
.ai-message {
background: linear-gradient(135deg, #25294a, #1e223d);
align-self: flex-start;
border-bottom-left-radius: 5px;
border: 1px solid rgba(92, 107, 192, 0.3);
}
.message-header {
display: flex;
align-items: center;
margin-bottom: 10px;
font-weight: 600;
}
.user-message .message-header {
color: #b9c0ff;
}
.ai-message .message-header {
color: #6c7bff;
}
.message-header i {
margin-right: 10px;
font-size: 1.2rem;
}
.message-content {
font-size: 1.05rem;
}
.message-content p {
margin: 8px 0;
}
/* Markdown样式增强 */
.message-content pre {
background: rgba(15, 20, 40, 0.8);
border-radius: 8px;
padding: 15px;
overflow-x: auto;
margin: 15px 0;
border: 1px solid rgba(92, 107, 192, 0.2);
}
.message-content code {
font-family: 'Fira Code', monospace;
background: rgba(15, 20, 40, 0.5);
padding: 2px 6px;
border-radius: 4px;
font-size: 0.95em;
}
.message-content blockquote {
border-left: 4px solid #6c7bff;
padding: 5px 15px;
margin: 10px 0;
background: rgba(92, 107, 192, 0.1);
border-radius: 0 8px 8px 0;
}
.message-content table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: rgba(20, 25, 45, 0.5);
}
.message-content th, .message-content td {
padding: 10px;
border: 1px solid rgba(92, 107, 192, 0.2);
text-align: left;
}
.message-content th {
background: rgba(92, 107, 192, 0.2);
}
/* 输入区域 */
.input-container {
padding: 20px;
background: rgba(18, 21, 36, 0.8);
border-top: 1px solid rgba(92, 107, 192, 0.3);
display: flex;
gap: 15px;
}
.input-box {
flex: 1;
padding: 18px 25px;
border-radius: 50px;
border: none;
background: rgba(25, 29, 50, 0.8);
color: #e0e0ff;
font-size: 1.1rem;
border: 2px solid rgba(92, 107, 192, 0.2);
outline: none;
transition: all 0.3s;
}
.input-box:focus {
border-color: rgba(108, 123, 255, 0.5);
box-shadow: 0 0 15px rgba(108, 123, 255, 0.3);
}
.input-box::placeholder {
color: #6a75b0;
}
.send-btn {
width: 60px;
height: 60px;
border-radius: 50%;
background: linear-gradient(135deg, #6c7bff, #a66cff);
border: none;
color: white;
font-size: 1.4rem;
cursor: pointer;
transition: all 0.3s;
display: flex;
justify-content: center;
align-items: center;
box-shadow: 0 5px 15px rgba(108, 123, 255, 0.4);
}
.send-btn:hover {
transform: translateY(-3px);
box-shadow: 0 8px 20px rgba(108, 123, 255, 0.6);
}
.send-btn:active {
transform: translateY(0);
}
.send-btn.loading {
background: linear-gradient(135deg, #4a5080, #6a4f8f);
cursor: not-allowed;
}
.send-btn.loading i {
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 加载动画 */
.typing-indicator {
display: flex;
align-items: center;
padding: 15px 25px;
background: rgba(25, 29, 50, 0.8);
border-radius: 30px;
width: fit-content;
margin-top: 10px;
align-self: flex-start;
animation: fadeIn 0.3s ease-out;
}
.typing-text {
color: #a0a8ff;
font-size: 1rem;
margin-right: 10px;
}
.dots {
display: flex;
gap: 5px;
}
.dot {
width: 10px;
height: 10px;
background: #6c7bff;
border-radius: 50%;
animation: pulse 1.5s infinite;
}
.dot:nth-child(2) {
animation-delay: 0.2s;
}
.dot:nth-child(3) {
animation-delay: 0.4s;
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 0.7; }
50% { transform: scale(1.2); opacity: 1; }
}
/* 错误提示 */
.error-message {
background: rgba(180, 50, 70, 0.2);
border: 1px solid rgba(220, 80, 100, 0.5);
color: #ffa0a8;
padding: 10px 20px;
border-radius: 10px;
margin-top: 10px;
align-self: flex-start;
}
/* 底部信息 */
.footer {
text-align: center;
padding: 20px;
font-size: 0.9rem;
color: #6a75b0;
border-top: 1px solid rgba(92, 107, 192, 0.2);
}
.status-indicator {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
margin-right: 8px;
}
.status-connected {
background: #4caf50;
box-shadow: 0 0 8px #4caf50;
}
.status-disconnected {
background: #f44336;
box-shadow: 0 0 8px #f44336;
}
/* 响应式设计 */
@media (max-width: 768px) {
.chat-container {
height: 400px;
padding: 15px;
}
.message {
max-width: 90%;
padding: 15px;
}
.header {
padding: 20px;
}
.logo-text {
font-size: 1.8rem;
}
.input-box {
padding: 15px 20px;
}
.send-btn {
width: 50px;
height: 50px;
}
}
@media (max-width: 480px) {
.chat-container {
height: 350px;
}
.logo {
flex-direction: column;
gap: 8px;
}
.logo-text {
font-size: 1.5rem;
}
.tagline {
font-size: 0.9rem;
}
.input-container {
padding: 15px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="logo">
<div class="logo-icon">
<i class="fas fa-brain"></i>
</div>
<div class="logo-text">DeepSeek AI</div>
</div>
<div class="tagline">下一代人工智能助手 | 提供深度洞察与智能解决方案</div>
</div>
<div class="chat-container" id="chatContainer">
<div class="message ai-message">
<div class="message-header">
<i class="fas fa-robot"></i>
DeepSeek 助手
</div>
<div class="message-content">
<p>👋 您好!我是DeepSeek AI助手,有什么可以帮您的吗?</p>
<p>我可以帮您解答问题、撰写内容、分析数据、编程辅助以及提供各种创意方案。</p>
<p>请直接在下方输入您的问题,我会尽力为您提供帮助!</p>
</div>
</div>
<div class="typing-indicator" id="typingIndicator" style="display: none;">
<div class="typing-text">DeepSeek正在思考</div>
<div class="dots">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</div>
</div>
<div class="input-container">
<input type="text" class="input-box" id="userInput" placeholder="向DeepSeek提问...">
<button class="send-btn" id="sendBtn">
<i class="fas fa-paper-plane"></i>
</button>
</div>
<div class="footer">
<span class="status-indicator status-connected" id="statusIndicator"></span>
<span id="statusText">已连接到后端API服务</span> |
DeepSeek AI 智能助手 | 支持Markdown渲染 | 修复JSON解析问题
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const chatContainer = document.getElementById('chatContainer');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const typingIndicator = document.getElementById('typingIndicator');
const statusIndicator = document.getElementById('statusIndicator');
const statusText = document.getElementById('statusText');
// 配置
const API_URL = 'http://localhost:8888/api/ai/chat';
// 初始滚动到底部
chatContainer.scrollTop = chatContainer.scrollHeight;
// 发送消息到后端API(修复JSON解析问题)
async function sendMessageToAPI(message) {
try {
// 显示加载状态
sendBtn.classList.add('loading');
const response = await fetch(`${API_URL}?message=${encodeURIComponent(message)}`);
if (!response.ok) {
throw new Error(`API请求失败: ${response.status}`);
}
// 修复:后端返回的是纯文本,而不是JSON
const responseText = await response.text();
// 更新状态指示器
statusIndicator.className = 'status-indicator status-connected';
statusText.textContent = '后端API响应成功';
return responseText;
} catch (error) {
console.error('API请求错误:', error);
// 更新状态指示器
statusIndicator.className = 'status-indicator status-disconnected';
statusText.textContent = '后端API连接失败';
// 添加错误消息
addErrorMessage(`服务暂时不可用: ${error.message}`);
return null;
} finally {
sendBtn.classList.remove('loading');
}
}
// 添加用户消息
function addUserMessage(content) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', 'user-message');
const headerDiv = document.createElement('div');
headerDiv.classList.add('message-header');
headerDiv.innerHTML = '<i class="fas fa-user"></i> 用户';
const contentDiv = document.createElement('div');
contentDiv.classList.add('message-content');
contentDiv.innerHTML = `<p>${content}</p>`;
messageDiv.appendChild(headerDiv);
messageDiv.appendChild(contentDiv);
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// 添加AI消息(Markdown渲染)
function addAIMessage(content) {
const messageDiv = document.createElement('div');
messageDiv.classList.add('message', 'ai-message');
const headerDiv = document.createElement('div');
headerDiv.classList.add('message-header');
headerDiv.innerHTML = '<i class="fas fa-robot"></i> DeepSeek 助手';
const contentDiv = document.createElement('div');
contentDiv.classList.add('message-content');
// 使用Marked.js渲染Markdown并安全插入
const renderedMarkdown = DOMPurify.sanitize(marked.parse(content));
contentDiv.innerHTML = renderedMarkdown;
messageDiv.appendChild(headerDiv);
messageDiv.appendChild(contentDiv);
chatContainer.appendChild(messageDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// 添加错误消息
function addErrorMessage(message) {
const errorDiv = document.createElement('div');
errorDiv.classList.add('error-message');
errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${message}`;
chatContainer.appendChild(errorDiv);
chatContainer.scrollTop = chatContainer.scrollHeight;
}
// 发送消息处理
async function sendMessage() {
const message = userInput.value.trim();
if (message === '') return;
// 添加用户消息
addUserMessage(message);
userInput.value = '';
// 显示"正在输入"指示器
typingIndicator.style.display = 'flex';
chatContainer.scrollTop = chatContainer.scrollHeight;
try {
// 发送到API并获取响应
const aiResponse = await sendMessageToAPI(message);
if (aiResponse) {
// 隐藏指示器并添加AI回复
typingIndicator.style.display = 'none';
addAIMessage(aiResponse);
}
} catch (error) {
console.error('处理消息时出错:', error);
typingIndicator.style.display = 'none';
addErrorMessage('处理您的请求时出错,请稍后再试');
}
}
// 事件监听
sendBtn.addEventListener('click', sendMessage);
userInput.addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});
// 初始焦点
userInput.focus();
// 添加示例消息
setTimeout(() => {
addUserMessage("你好,请帮我写一个Python函数计算斐波那契数列");
typingIndicator.style.display = 'flex';
setTimeout(() => {
typingIndicator.style.display = 'none';
addAIMessage("当然可以!以下是计算斐波那契数列的Python函数:\n\n```python\ndef fibonacci(n):\n \"\"\"\n 计算斐波那契数列的第n项\n \"\"\"\n if n <= 0:\n return 0\n elif n == 1:\n return 1\n else:\n a, b = 0, 1\n for _ in range(2, n+1):\n a, b = b, a + b\n return b\n\n# 示例:计算前10项\nfor i in range(1, 11):\n print(fibonacci(i))\n```\n\n这个函数高效地计算斐波那契数列,避免了递归的低效问题。");
}, 1500);
}, 1000);
});
</script>
</body>
</html>
问题
需要充值即可:
充值之后进行访问