Sping AI接入deepseek

发布于:2025-06-26 ⋅ 阅读:(21) ⋅ 点赞:(0)

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、只需要写一份代码即可,修改配置文件,切换大语言模型

核心特性

  1. 统一 API 设计
    • 通过抽象层屏蔽不同 AI 提供商(如 OpenAI、Gemini、Ollama 等)的接口差异,开发者只需使用 Spring AI 的通用接口(如 ChatModel),即可切换底层 AI 服务。
  2. 开箱即用的功能支持
    • 支持对话模型(Chat)类的大模型:与 ChatGPT 类似的交互。
    • 支持嵌入模型(Embedding)类的大模型:嵌入模型是将文本、图像或其他数据转换为数值向量(即嵌入向量)的技术。
    • 支持图像生成(Image Generation)类的大模型:图像生成是指AI根据文本描述或其他输入创建新图像的能力。
    • 支持函数调用(Function Calling):函数调用功能使AI模型能够与外部API和服务交互。也就是说,你写一个函数,AI 也能调用你写的函数。
  3. 与 Spring 生态无缝集成
    • 支持 Spring Boot 自动配置、依赖注入、Actuator 监控等,与 Spring Security、Spring Data 等组件协同工作。
  4. Prompt(提示) 工程支持
    • 提供 PromptTemplate 等工具,方便动态生成提示词(Prompts),支持上下文管理。(根据会话记录,来回答问题 )
  5. 模块化设计
  • 开发者可以根据项目需求选择特定的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 接口用于存储和管理"对话记忆"
在这里插入图片描述

基于内存的多轮对话实现

在这里插入图片描述

  1. AI模型接收Prompt输入,使用chatResponse输出(响应)
  2. 系统消息仅需首次添加至对话记忆;用户消息、助手消息则每次都要添加至对话记忆

依赖引入:

        <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>

问题
在这里插入图片描述
需要充值即可:
在这里插入图片描述

充值之后进行访问

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到