SpringAi简介
Spring AI是一个面向AI工程的应用框架,旨在将Spring生态系统的设计原则(如可移植性和模块化)扩展至人工智能领域。Spring AI支持多种主流大模型平台,如OpenAI、Microsoft、Amazon、Google和Huggingface,并提供灵活的Prompt Template8和模型输出解析能力,支持多模态生成式AI能力,如对话、文生图、文生语音等12。
主要功能
- 多模态生成:支持对话、文生图、文生语音等多种模态的生成式AI能力。
- 大模型服务对接:提供多种大模型服务对接能力,包括业界大多数主流大模型服务。
- 灵活的Prompt Template和模型输出解析:支持灵活的Prompt Template和模型输出解析Output Parsing能力。
- 多模态生成式AI:支持同步和流式调用,同时也支持传递特定模型的定制参数。
- RAG能力:支持RAG(Retrieval-Augmented Generation)能力的基础组件,包括DocumentLoader、TextSpillter、EmobeddingClient、VectorStore等。
- AI Spring Boot Starter:支持AI Spring Boot Starter实现配置自动装配。
SpringAi官网:https://spring.io/projects/spring-ai
Spring集成OpenAi实现对话功能
在集成OpenAi需要设置key来源,可以从网站中注册获取
网站:https://api.xty.app/register?aff=PuZD




三个配置在Spring配置文件中如下
#配置的地址
spring.ai.openai.base-url=https://api.xty.app
#访问的key也就是秘钥
spring.ai.openai.api-key=sk-1xN8TRjXsEMC4iAceYJHUdeXvc83rYUa1ifNjmsBMVHiDc2V
#选择的jpt
spring.ai.openai.chat.options.model=gpt-3.5-turbo
导入依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-openai-spring-boot-starter</artifactId>
</dependency>
进行简单测试
@RestController
@CrossOrigin
public class OpenAiController {
private final OpenAiChatModel chatModel;
@Autowired
public OpenAiController(OpenAiChatModel openAiChatModel) {
this.chatModel = openAiChatModel;
}
@GetMapping("/ai/generate")
public Map<String,String> generate(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
return Map.of("generation", this.chatModel.call(message));
}
@GetMapping(value = "/ai/generateStream",produces = MediaType.TEXT_EVENT_STREAM_VALUE )
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "讲个笑话") String message) {
// Prompt prompt = new Prompt(new UserMessage(message));
Flux<String> stream = chatModel.stream(message);
return stream;
}
}
测试结果

前端代码
<template>
<div class="common-layout">
<el-container>
<el-header>
<div style="text-align: center;">
<h3> CHAT_JPT接入</h3>
</div>
</el-header>
<el-main>
<div style="text-align: center;">
<span v-for="(val,i) in res" :key="i">{{val}}</span>
</div>
</el-main>
<el-footer>
<div style="text-align: center;">
<el-input v-model="msg" style="width: 240px" :autosize="{ minRows: 2, maxRows: 4 }" type="textarea" placeholder="Please input"/>
</div>
<div style="text-align: center;margin-top: 10px;">
<el-button type="success" @click="sendMsg">提问</el-button>
</div>
</el-footer>
</el-container>
</div>
</template>
<script setup lang="ts">
import { nextTick, reactive, ref } from "vue";
import { CircleCloseFilled } from '@element-plus/icons-vue'
import type { FormInstance } from 'element-plus'
import { ElMessageBox, ElMessage, ElNotification } from 'element-plus'
import http from "@/http";
const msg = ref('')
const res = ref([])
res.value = []
let count=0;
const sendMsg = () => {
//接收后台的事件流
let source= new EventSource(`http://localhost:8084/ai/generateStream?message=${msg.value}`)
source.onmessage=(event)=>{
//由于EventSource不知道这个后台数据是否已经传输完成,回反复传输数据,但是会给你末尾两个空串
if(event.data===null||event.data===''){
count++;
}
//如果执行了两次关闭即可
if(count==2){
source.close();
count=0;
}
res.value.push(event.data)
}
}
</script>
<style scoped>
</style>
Spring集成OpenAi实现图片生成功能
#都可以不写
#开启AI图片功能,默认是开启的
spring.ai.openai.image.enabled=true
#生图用的模型,有默认模型
spring.ai.openai.image.options.model=
controller代码
@RestController
@CrossOrigin
public class OpenAiimagsController {
@Autowired
private OpenAiImageModel aiImageModel;
@GetMapping(value = "/ai/image",produces = "text/html")
public String getImage(@RequestParam String image){
//请求deepSeek模型
ImageResponse response = aiImageModel.call(new ImagePrompt(image));
//返回图片
Image output = response.getResult().getOutput();
//获取图片的地址
String url = output.getUrl();
//获取图片的base64编码
return "<img src='"+url+"'>";
}
}
浏览器请求
ollama本地模型
安装ollama模型
官网:https://ollama.com/download/mac
github:https://gitcode.com/gh_mirrors/oll/ollama?utm_source=csdn_github_accelerator

安装
1.选择ollama安装路径

2.在该路径打开cmd命令输入OllamaSetup.exe /DIR=加上指定安装路径

3.安装成功输入ollama -v查看是否安装成功

4.由于ollama修改了位置,但是模型安装位置并没有修改还是安装在C盘,则需要配置环境变量,新建 OLLAMA_MODELS 环境变量,建议配置在系统变量中
5,以管理员方式打开CMD运行 setx OLLAMA_MODELS D:\ruanjian\OllamaModel :注意这里需要重启一下ollama,下载路径才生效
6.下载deepseek本地模型:ollama pull deepseek-r1:1.5b 正常下载到路径
7.模型正常启动 ollama pull deepseek-r1:1.5b 这个可以不pull可以直接run没有会直接下载
8.安装 Chatbox 可视化界面
下载之后直接双击选择安装路径即可
9.配置自己的本地模型即可
ollama 常用命令
ollama serve # 启动服务端:启动 Ollama 后台服务(API 接口)
ollama create # 创建模型:通过 Modelfile 自定义创建一个新模型
ollama show # 查看信息:显示某个模型的详情、配置、系统提示词
ollama run # 运行模型:直接启动聊天对话(最常用)
ollama stop # 停止模型:停止正在后台运行的模型
ollama pull # 拉取模型:从仓库下载模型到本地
ollama push # 推送模型:把本地模型上传到 Ollama 仓库
ollama list # 列出模型:查看本地已下载的所有模型
ollama ps # 查看进程:查看当前正在运行中的模型
ollama cp # 复制模型:复制一个模型并改名
ollama rm # 删除模型:从本地删除模型(释放空间)
ollama help # 帮助命令:查看所有命令说明
java连接本地模型
依赖
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>1.0.0-M6</version>
</dependency>
配置
spring:
ai:
ollama:
base-url: http://localhost:11434
chat:
model: deepseek-r1:1.5b # 与本地模型名称对应
@RestController
@RequestMapping("/ai")
public class ChatController {
@Autowired
private OllamaChatModel chatClient;
@GetMapping("/chat")
public String chat(@RequestParam(value = "message") String message) {
// 调用 ChatClient 生成响应,并以 Flux<String>(响应流)形式返回
return chatClient.call(message);
}
}
大模型规范接口
目前大多数大模型都遵循OpenAI的接口规范,是基于Http协议的接口。因此请求路径、参数、返回值信息都是类似的,可能会有一些小的差别。具体需要查看大模型的官方API文档。
我们以DeepSeek官方给出的文档为例:
curl -X POST https://api.deepseek.com/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <DeepSeek API Key>" \
-d '{
"model": "deepseek-chat",
"messages": [
{
"role": "system",
"content": "You are a helpful assistant."
},
{
"role": "user",
"content": "Hello!"
}
],
"stream": false
}'
接口说明
- 请求方式:通常是POST,因为要传递JSON风格的参数
- 请求URL:与平台有关
- DeepSeek官方平台:https://api.deepseek.com/chat/completions
- 阿里云百炼平台:https://dashscope.aliyuncs.com/compatible-mode/v1
- 本地ollama部署的模型:http://localhost:11434
- 请求头:开放平台都需要提供API_KEY来校验权限,本地ollama则不需要
- Content-Type: application/json,请求参数的格式,必须是application/json,稍后解释
- Authorization: Bearer ,上一节创建的API_KEY
- 请求参数:JSON格式:
-
{ "model": "deepseek-chat", "messages": [ {"role": "system", "content": "You are a helpful assistant."}, {"role": "user", "content": "Hello!"} ], "stream": false } -
model:模型名称,DeepSeek支持deepseek-reasoner和deepseek-chat两者模型 -
messages:发送给大模型的消息,[]是数组的意思,里面可以有多条消息。消息结构:content:是消息的内容role:消息的角色,有system、user、assisant三种角色system:是给大模型设定一个角色,比如你让她扮演你的奶奶,让她哄你睡觉user:就是用户提问的问题assisant:是大模型的回答
-
stream:true,代表响应结果流式返回;false,代表响应结果一次性返回,但需要等待
-
注意,这里请求参数中的messages是一个消息数组,而且其中的消息要包含两个属性:
- role:消息对应的角色
- content:消息内容
其中System和User消息的内容,也被称为提示词(Prompt),也就是用户发送给大模型的指令。
- System提示词,是系统指令,给大模型设定一个角色,比如你让她扮演你的奶奶,让她哄你睡觉
- User提示词,是用户指令,也就是用户向大模型的提问或命令
提示词角色
通常消息的角色有三种:
| 角色 | 描述 | 示例 |
|---|---|---|
| system | 优先于user指令之前的指令,也就是给大模型设定角色和任务背景的系统指令 | 你是一个乐于助人的编程助手,你以小团团的风格来回答用户的问题。 |
| user | 终端用户输入的指令(类似于你在ChatGPT聊天框输入的内容) | 写一首关于Java编程的诗 |
| assistant | 由大模型生成的消息,可能是上一轮对话生成的结果 | 注意,用户可能与模型产生多轮对话,每轮对话模型都会生成不同结果。 |
其中System类型的消息非常重要!影响了后续AI会话的行为模式。
会话记忆问题
大模型的API接口是"无状态"的,服务端不会记录用户请求的上下文。因此我们调用API接口与大模型对话时,每一次对话信息都不会保留,多次对话之间都是独立的,没有关联的。
因此大模型并不知道之前的聊天历史,也就是说大模型是没有记忆的。
测试,我询问AI一个问题:12个苹果分给3个人,每人能分几个?

AI的答案是:每人可以分到4个苹果。
我们接着问:如果是分给4个人呢?,由于AI没有记忆,它不知道我是接着上一题问的,因此不知道要分的是12个苹果,答案就有问题:

可以看到,AI完全不知道我们聊天的背景是上一次的分12个苹果。
那么,如何才能让AI具备记忆呢?
要想让大模型有记忆,必须在每次请求时,将之前所有对话的历史拼接好,传递给对话API接口。
要想让AI具备记忆,就必须把对话历史都添加到请求体中的messages数组中,像这样:
{
"model": "deepseek-chat",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "12个苹果分给3个人,每人能分几个?直接告诉我答案"},
{"role": "assistant", "content": "每人可以分到4个苹果。"},
{"role": "user", "content": "如果是分给4个人呢?"}
],
"stream": false
}
结果

AI网址
准备工作:
问小白:https://www.wenxiaobai.com/chat/200006
sealos:https://hzh.sealos.run/signin
cursor:https://www.cursor.com/cn
Spring AI Alibaba
SpringAIAlibaba基本介绍
Spring AI Alibaba 是阿里云基于Spring AI 官方标准深度打造的企业级 AI 应用开发框架,核心价值是:一套标准 API,屏蔽不同大模型的底层差异,让 Java 开发者像写 Spring Boot 业务代码一样开发 AI 应用。
底层兼容 Spring AI 全量 API,无缝对接 Spring Boot、Spring Cloud 微服务生态,Java 开发者零学习成本迁移 深度整合阿里云灵积 DashScope 平台,原生支持通义千问全系列模型、DeepSeek 等国产主流大模型,国内访问稳定、合规性拉满 2026 最新版本核心升级:Agent Skills 技能体系、Multi-agent 多智能体并行执行、Graph 工作流并行条件边、异步工具执行、流式全链路优化,是国内 Java 生态最成熟的 Agent 开发框架 云原生友好:完美适配 Spring Boot Actuator、Micrometer 监控、Resilience4j 熔断降级,完全符合大厂生产级开发规范
SpringAIAlibaba基本使用
1.导入依赖
<!-- Spring AI Alibaba 核心依赖 -->
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-agent-framework</artifactId>
<version>1.1.2.0</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId>
<version>1.1.2.1</version>
</dependency>
<!-- Spring Web 必备,提供HTTP接口能力 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
LangChain4j
LangChain4j介绍
LangChain4j 的目标是简化将大语言模型(LLM - Large Language Model)集成到 Java 应用程序中的过程。
官网:https://docs.langchain4j.dev
与大型语言模型和向量数据库的便捷交互
通过统一的应用程序编程接口(API),可以轻松访问所有主要的商业和开源大型语言模型以及向量数据库,使你能够构建聊天机器人、智能助手等应用。
专为 Java 打造
借助Spring Boot 集成,能够将大模型集成到ava 应用程序中。大型语言模型与 Java 之间实现了双向集成:你可以从 Java 中调用大型语言模型,同时也允许大型语言模型反过来调用你的 Java 代码
智能代理、工具、检索增强生成(RAG)
为常见的大语言模型操作提供了广泛的工具,涵盖从底层的提示词模板创建、聊天记忆管理和输出解析,到智能代理和检索增强生成等高级模式。
LangChain4j基本使用
配置
langchain4j: #是框架的根配置节点,表明后续配置属于相关设置。
open-ai: #子节点,指定配置的是 OpenAI 兼容的模型服务
chat-model: #表示配置的是聊天模型相关参数
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
#聊天模型的 API 基础地址
api-key: sk-2b2747e2966c41718405c76318be157a
#访问该 API 服务的身份验证密钥
model-name: qwen-plus
log-requests: true
log-responses: true
logging:
level:
dev.langchain4j: debug
依赖
<!-- 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.0-beta3</version>
</dependency>
<!-- 覆盖logback的依赖配置,使用兼容 SLF4J 2.x 的版本 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
测试代码
//初始化模型
OpenAiChatModel model = OpenAiChatModel.builder()
//LangChain4j提供的代理服务器,该代理服务器会将演示密钥替换成真实密钥, 再将请求转发给OpenAI API
.baseUrl("http://langchain4j.dev/demo/openai/v1") //设置模型api地址(如果apiKey="demo",则可省略baseUrl的配置)
.apiKey("demo") //设置模型apiKey //这里可以获取环境变量就是 直接配置在环境变量即可 .apiKey(System.getenv("API_KEY"));
.modelName("gpt-4o-mini") //设置模型名称
.logRequests(true) //打印日志
.logResponses(true) //打印日志
.build();
//向模型提问
String answer = model.chat("你好");
//输出结果
System.out.println(answer);
Ai Service基本使用
依赖
<!-- <!– 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 –>-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
<!--Aiservice工具类-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
config类
@Configuration
public class CommonConfig {
@Autowired
private OpenAiChatModel modesl;
@Bean
public ConsultantService consultantService(){
ConsultantService consultantService = AiServices.builder(ConsultantService.class)
.chatModel(modesl)
.build();
return consultantService;
}
}
Service类
package com.yuan.langchain4j.aiservice;
public interface ConsultantService {
//用于聊天的方法
public String chat(String message);
}
Contorller类
@RestController
public class ChatController {
@Autowired
private ConsultantService consultantService;
@RequestMapping("/chat")
public String chat(String message){
String result = consultantService.chat(message);
return result;
}
}
测试

声明式使用方式不需要在CommonConfig类中写这么麻烦,直接在接口@AiService就可以了
打开CommonConfig类,把consultantService方法注释掉
在ConsultantService接口中,类名的上方添加如下代码
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
chatModel = "openAiChatModel",//指定模型
)
也可以直接写@AiService 不用带括号里的东西。直接写这个@AiService注解是默认装配,可以不用多余的去写 wiringMode = AiServiceWiringMode.EXPLICIT
但是现在为了教学懂得其中原理,要一步一步人人真真的写过程,方便理解,不能简略。
@AiService
public interface ConsultantService {
//用于聊天的方法
public String chat(String message);
}
LangChain4j流式调用
流式调用的方法
首先添加如下依赖,点击maven刷新依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.0.1-beta6</version>
</dependency>
配置流式配置
langchain4j:
open-ai:
chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${API-KEY}
model-name: qwen-plus
log-requests: true
log-responses: true
#配置流式配置如下
streaming-chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${API-KEY}
model-name: qwen-plus
log-requests: true
log-responses: true
修改Service配置
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
chatModel = "openAiChatModel",//指定模型
streamingChatModel = "openAiStreamingChatModel" //流式调用
)
//@AiService
public interface ConsultantService {
//用于聊天的方法
// public String chat(String message);
public Flux<String> chat(String message);
}
修改Conterller
@RestController
public class ChatController {
@Autowired
private ConsultantService consultantService;
@RequestMapping(value = "/chat",produces = "text/html;charset=utf-8")
public Flux<String> chat( String message){
Flux<String> result = consultantService.chat(message);
return result;
}
}
LangChain4j消息注解
@SystemMessage通常用于标记系统级别的消息,这类消息一般由系统自身发出,用于提供背景信息、设定对话规则、定义 AI 的行为模式
@UserMessage用于标记用户发送的消息,即对话中用户输入的内容,是 AI 需要响应或处理的核心信息。例如用户提问 “如何设置快捷键”,这条内容会被标记为@UserMessage。
@UserMessage("你是东哥的助手小月月,温柔貌美又多金。{{it}}")
{{it}}是个占位符,比如实际传入的数据是 “今天天气真好” 那么最终生成的消息会是:
"你是东哥的助手小月月,温柔貌美又多金。今天天气真好"
如果想把{{it}}改成其他的,可以这样写 @V("msg")是一个自定义注解
public Flux<String> chat(@V("msg")String message);
把system.txt复制到resources包下
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
chatModel = "openAiChatModel",//指定模型
streamingChatModel = "openAiStreamingChatModel" //流式调用
)
//@AiService
public interface ConsultantService {
//用于聊天的方法
// public String chat(String message);
@SystemMessage(fromResource = "system.txt")
public Flux<String> chat(String message);
}

LangChain4j会话记忆
CommonConfig类中加入如下代码 用于让代码记住历史会话
//构建回答记忆对象
@Bean
public ChatMemory chatMemory(){//用于管理聊天对话的上下文记忆。
//采用窗口机制来管理对话历史,通过构建器模式(Builder Pattern)来进行实例化
MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
.maxMessages(20)//记住最多20条对话
.build();
return memory;
}
ConsultantService接口中的@AiService()中加入如下代码,记得前面多个逗号
chatMemory = "chatMemory"//指定记忆对象
会话记忆隔离
在ConsultantService接口中的@AiService()中最后一行chatMemory
注释掉,添加如下一行
chatMemoryProvider = "chatMemoryProvider"//配置会话记忆提供者对象
ConsultantService接口中把chat方法修改为如下,注意@UserMessage只能在一个类中出现一次,否会报错
public Flux<String> chat(/*@V("msg")*/@MemoryId String memoryId,@UserMessage String message);
Flux:方法的返回类型。用于处理流式数据。这个方法可能先返回 “正在思考...”,过一会儿又返回 “你的问题是...”,最后返回 “答案是...”—— 这就是多个字符串按顺序异步过来的过程。
@MemoryId*:标记该参数用于标识会话记忆,框架会使用这个ID来识别和维护不同用户的会话上下文。*
String memoryId:用于传入会话ID,与@MemoryId注解配合使用。
CommonConfig类中加入如下方法
@Bean
public ChatMemoryProvider chatMemoryProvider(){
ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
@Override
public ChatMemory get(Object memoryId) {
return MessageWindowChatMemory.builder()//使用构建器模式创建并返回一个MessageWindowChatMemory对象。
.id(memoryId)//设置唯一标识符为传入的memoryId,这样可以区分不同的对话会话
.maxMessages(20)//设置消息窗口的最大容量为20条,限制对话历史的长度
.build();//调用构建器的build()方法,完成MessageWindowChatMemory对象的创建
}
};
return chatMemoryProvider;//结束匿名内部类的定义,并返回创建好的ChatMemoryProvider对象
}
这段代码的主要作用是创建一个能够根据会话ID提供相应聊天记忆的服务提供者,具体有以下几个目的:
会话隔离:通过memoryId参数,为不同的用户或会话提供独立的对话记忆,确保对话上下文不会混淆。
记忆持久化:记忆对象使用Redis存储,能够将对话历史持久化到Redis数据库中,避免应用重启后对话历史丢失。
支持多用户场景:在多用户或多会话的场景下,能够为每个会话提供独立且持久的对话记忆,使AI服务能够记住与每个用户的对话历史。
在ChatController类中添加一个memoryId
Flux<String> result = consultantService.chat(memoryId,message);
pom中添加redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis连接信息,在application.yml中添加
spring:
data:
redis:
host: localhost
port: 6379
password:123456
根据视频提示创建repository包,包下创建RedisChatMemoryStore类,实现ChatMemoryStore接口,按下Alt+Insert实现3个方法。嫌麻烦的直接复制如下代码
@Repository
public class RedisChatMemoryStore implements ChatMemoryStore {
//注入RedisTemplate
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
//获取会话消息
String json = redisTemplate.opsForValue().get(memoryId);
//把json字符串转化成List<ChatMessage>
List<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(json);
return list;
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> list) {
//更新会话消息
//1.把list转换成json数据
String json = ChatMessageSerializer.messagesToJson(list);
//2.把json数据存储到redis中
redisTemplate.opsForValue().set(memoryId.toString(),json, Duration.ofDays(1));
}
@Override
public void deleteMessages(Object memoryId) {
redisTemplate.delete(memoryId.toString());
}
}
在CommonConfig类中添加Redis存储对象
@Autowired
private ChatMemoryStore redisChatMemoryStore;
.maxMessages(20)下方插入这行代码
.chatMemoryStore(redisChatMemoryStore)//设置聊天内存存储库为RedisChatMemoryStore,用于存储和检索对话历史
@Repository注解的作用
主要用于标识数据访问层(DAO 层)负责与数据库进行交互,执行 CRUD(创建、读取、更新、删除)操作。当 Spring 容器进行组件扫描时,会检测带有@Repository注解的类,并将其注册为 SpringBean
LangChain4j RAG知识库
2.1.1****什么是RAG知识库?
RAG 是让 AI 查 “知识库” 回答的技术,不用重新训练 AI。它先把要参考的资料拆成小块存起来,方便快速找。用户提问时,先从存的资料里找出相关内容。把找到的资料和问题一起给 AI,让 AI 结合资料回答。这样 AI 回答更准,还能引用具体来源,避免瞎编。
2.1.2****什么是向量数据库?
**向量距离越小,相似度越大。**向量数据库就是专门存“向量”这种特殊数据的库。向量是把文字、图片这些东西转成的一串数字,能代表它们的特征。它不像普通数据库比文字、数字,而是算向量间的相似度,比如找和“猫”的向量最像的“橘猫”向量。查的时候快,还能精准匹配相关数据,常用来给AI找资料、做图片搜索这些场景。

先拿文档Document ,用文本分割器Text Splitter把它拆成小段Segments。这些小段送到嵌入模型Model Embedding,模型会把每段转成叫“嵌入”Embeddings的数字向量。最后,小段本身和对应的嵌入向量一起存到嵌入存储Embedding Store里,这样之后找相关内容时,就能通过向量快速匹配啦。
RAG知识库基本使用
引入依赖,刷新maven
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.0.1-beta6</version>
</dependency>
加载知识数据文档,resources下创建content文件夹,文件夹下粘贴文件

CommonConfig类中添加如下代码
//构建向量数据库操作对象
@Bean
public EmbeddingStore store(){//embeddingStore的对象, 这个对象的名字不能重复,所以这里使用store
//1.加载文档进内存,从类路径下的 "content" 目录加载所有文档 将加载的文档存储在 documents 列表中
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
//2.创建一个内存中的向量存储实例 提供存储向量数据的容器
InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(store)//为构建器设置之前创建的向量存储对象 告诉处理器要将向量存储到哪里
.build();//创建 EmbeddingStoreIngestor 实例
ingestor.ingest(documents);//完成从原始文档到向量数据的转换和存储
return store;//返回配置好的向量存储对象 使其成为 Spring 容器中的 Bean
}
//构建向量数据库检索对象
@Bean
//定义一个返回 ContentRetriever 的方法,方法参数中注入了之前创建的EmbeddingStore对象 创建用于检索向量数据的对象
public ContentRetriever contentRetriever(EmbeddingStore store){
return EmbeddingStoreContentRetriever.builder()//创建检索器的构建器
.embeddingStore(store)//设置检索器要使用的向量存储对象,告诉检索器从哪里获取数据
.minScore(0.5)//设置检索 最小相似度 0.5
.maxResults(3)//设置最多返回 3 条检索结果。控制返回结果的数量
.build();//完成构建,返回配置好的 ContentRetriever 对象
}
在ConsultantService接口中的@AiService()中最后一行添加如下一行
contentRetriever = "contentRetriever"//配置向量数据库检索对象
RAG数据库核心
文档加载器
Document Loader,用于把磁盘或者网络中的数据加载进程序
- FileSystemDocumentLoader,根据本地磁盘绝对路径加载
List<Document> documents = FileSystemDocumentLoader.loadDocuments("C:\\ideaProjects\\consultant\\src\\main\\resources\\content");
- classPathDocumentLoader,相对于类路径加载
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
- UrlDocumentLoader,根据url路径加载
文档解析器/文本分割器
Document Parser,用于解析文档加载器加载进内存的内容,把数据转化成纯文本
TextDocumentParser,解析纯文本格式的文件
ApachePdfBoxDocumentParser,解析PDF格式文件
ApachePoiDocumentParser,解析微软的office文件,例如DoC、PPT、XLS
ApacheTikaDocumentParser(默认),几乎可以解析所有格式的文件
解析pdf格式文件
<!--pdf解析器依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
<version>1.0.1-beta6</version>
</dependency>
把pdf文件复制到content包下
注意这个new ApachePdfBoxDocumentParser()只解析PDF格式文件
运行项目,前端测试pdf内容是否被大模型识别
文本分割器
DocuemntByParagraphSplitter,按照段落分割文本
DocumentByLineSplitter,按照行分割文本
DocumentBySentenceSplitter,按照句子分割文本
DocumentByWordSplitter,按照词分割文本
DocumentByCharacterSplitter,按照固定数量的字符分割文本
DocumentByRegexSplitter,按照正则表达式分割文本
DocumentSplitters.recursive()(默认),递归分割器,优先段落分割 再按照行分割,再按照句子分割,再按照词分割
在CommonConfig类的构建向量数据库操作对象的方法中添加如下代码
DocumentSplitter ds = DocumentSplitters.recursive( 500, 100);//创建一个递归文档分割器 用于将文档切分成较小的块
.documentSplitter(ds)//设置文档分割器 用于将文档切分成较小的块
向量模型
所谓向量是指这段语言的方向。比如“这个菜不好吃”和“我以后不想点这个菜”虽然表达不同 但是表达的意思是相似的。向量模型可以对文本片段闻闻尝尝知道这句话的感情色彩 真正意指。向量模型就是把文字变成一长串有意义的数字,让电脑能“算”出文字之间的相似度。苹果”和“橙子”的坐标很接近(都是水果,甜度酸度差不多),所以它们意思是相似的。
配置向量模型步骤
配置向量模型
text-embedding-v3
embedding-model: #配置向量模型
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: sk-2b2747e2ae6c41718405c763108e157a
model-name: text-embedding-v3 #向量模型名,通用文本向量v3
log-requests: true
log-responses: true
max-segments-per-batch: 10
CommonConfig类中插入如下代码
@Autowired
private EmbeddingModel embeddingModel;//向量模型
构建向量库操作对象和构建检索对象的方法种插入如下代码
.embeddingModel(embeddingModel)//设置嵌入模型 用于将文本转换为向量表示
向量化数据存到RedisSearch
为什么要把向量化数据存到*RedisSearch
RedisSearch 可实现毫秒级向量检索,能满足实时推荐、问答系统等低延迟场景需求,不用额外搭复杂向量数据库;相比专门的向量数据库按向量数量或查询次数收费,RedisSearch 可部署在自有服务器,长期使用能大幅降低付费成本,尤其数据量大时更明显;Redis支持把向量数据和元数据持久化到磁盘
步骤
打开Docker桌面端后打开CMD
停止原有的redis镜像: cmd运行
docker stop redis
删除原有的redis镜像:
docker rm redis
删除有的redis容器
docker rm -f redis-vector
安装带有向量化功能的redis: ,不确定是否安装成功可以再运行一次,耐心等待5min
docker run --name redis-vector -d -p 6379:6379 redislabs/redisearch
在pom.xml中引入依赖
<!--引入langchain4j对于redis向量数据库的支持-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-redis-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
application.yml中插入如下内容。注意,是在langchain4j的子节点,也就是和open-ai: 同级的节点
community:
redis:
host: localhost
port: 6379
在CommoConfig类下插入如下
@Autowired
private RedisEmbeddingStore redisEmbeddingStore;
这是以前的EmbeddingStore,只能操作内存版本的向量数据库。需要注释掉
//InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
现在我们要操作的是research,把后面的store都改成redisEmbeddingStore
并且把ContentRetriever方法参数注入的store对象注释掉
现在有个问题,百炼平台一次性吃进去的文档有限制,不可能所有pdf一次性喂进去。需要限制数量。在application.yml中添加如下内容
max-segments-per-batch: 10 # 每个批次最多处理的文本段数
还有个问题,每次启动项目,并没有新的志愿填报文件加进来。但是文本早就处理好了。每次启动一遍再处理一次没有意义。事实上不是每次启动项目,每次都会把处理好的文本段再次处理,那性能上太浪费了。事实上配置好Redis后,处理好的数据就存在这里。所以可以把@Bean注释掉。避免 Spring 容器在启动时自动执行该方法,从而防止重复处理已存入 Redis 的文本数据
@Bean注解用于告诉 Spring:“这个方法返回的对象需要被纳入容器管理,并且在容器启动时执行该方法来创建对象”。
LangChain4j Tools工具
说白话就是和AI对话提供预约,将数据存储到数据库中
SQL
create database if not exists volunteer;
use volunteer;
create table if not exists reservation
(
id bigint primary key auto_increment not null comment '主键ID',
name varchar(50) not null comment '考生姓名',
gender varchar(2) not null comment '考生性别',
phone varchar(20) not null comment '考生手机号',
communication_time datetime not null comment '沟通时间',
province varchar(32) not null comment '考生所处的省份',
estimated_score int not null comment '考生预估分数'
)
pom.xml引入如下依赖
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
如下内容粘贴到application.yml中。注意datasource:
是在spring节点下,mybatis:是在根节点
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver #mysql驱动类
url: jdbc:mysql://localhost:3307/volunteer?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: 1234
mybatis:
configuration:
map-underscore-to-camel-case: true #开启驼峰-→下划线 命名映射
新建pojo包,包下建Reservation类,作为预约信息的实体类
代码如下
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Reservation {
private Long id;
private String name;
private String gender;
private String phone;
private LocalDateTime communicationTime;
private String province;
private Integer estimatedScore;
}
新建mapper包,包下建ReservationMapper类,作用是sql语句操作数据库。类名上面加个@Mapper。 代码如下
//1.添加预约信息
@Insert("insert into reservation(name,gender,phone,communication_time,province,estimated_score) values(#{name},#{gender},#{phone},#{communicationTime},#{province},#{estimatedScore})")
void insert(Reservation reservation);
//2.根据手机号查询预约信息
@Select("select * from reservation where phone=#{phone}")
Reservation findByPhone(String phone);
新建service包,包下建ReservationService类,作用是处理预约信息的业务逻辑
类名上面加个注解@Service,让Spring理解是个服务类
类中代码如下
@Autowired
private ReservationMapper reservationMapper;
//1.添加预约信息的方法
public void insert(Reservation reservation) {
reservationMapper.insert(reservation);
}
//2.查询预约信息的方法(根据手机号查询)
public Reservation findByPhone(String phone) {
return reservationMapper.findByPhone(phone);
}
Tools工具 P22
ConsultantService接口上的@AiService()中添加如下一行,记得带逗号
tools = "reservationTool"//配置工具对象
新建tools包,包下建ReservationTool类。
大模型会从用户输入中提取对应信息,自动填充参数,调用方法时创建 Reservation 对象,再通过 reservationService 保存到存储中,实现自动处理预约的功能。
@Autowired
private ReservationService reservationService;
//1.工具方法: 添加预约信息
@Tool("预约志愿填报服务")
public void addReservation(
@P("考生姓名") String name,
@P("考生性别") String gender,
@P("考生手机号") String phone,
@P("预约沟通时间,格式为: yyyy-MM-dd'T'HH:mm") String communicationTime,
@P("考生所在省份") String province,
@P("考生预估分数") Integer estimatedScore
) {Reservation reservation = new Reservation(null, name, gender, phone, LocalDateTime.parse(communicationTime), province, estimatedScore);
reservationService.insert(reservation);
}
//2.工具方法: 查询预约信息
//@Tool 注解标明这是查询工具及功能,@P 注解说明参数需要考生手机号,大模型会从用户输入提取手机号,自动传入方法,调用 reservationService 的查询方法获取并返回预约单,实现自动查询功能。
@Tool("根据考生手机号查询预约单")
public Reservation findReservation(@P("考生手机号") String phone) {return reservationService.findByPhone(phone);}
@Tool("预约志愿填报服务")这个注解能让大模型能认出这个方法是可用工具,调用时会按指定名称来。目的是让大模型知道哪些方法能调用,方便在处理任务时,自动选用合适工具完成操作,不用手动写调用逻辑,让集成工具更方便。
@P("考生姓名") String name,这个注解能让大模型明白这个 String name 参数需要啥值,会从输入里提取 "考生姓名" 相关信息填进去。能让大模型自动识别并填充方法参数,不用手动处理参数映射
测试


LangChain4j完整代码

后端代码
依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- <!– 基于open-ai的langchain4j接口:ChatGPT、deepseek都是open-ai标准下的大模型 –>-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
<!--Aiservice工具类-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
<!--langchain4j流式调用依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-reactor</artifactId>
<version>1.0.1-beta6</version>
</dependency>
<!-- 覆盖logback的依赖配置,使用兼容 SLF4J 2.x 的版本 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- <!–引入langchain4j对于redis向量数据库的支持–>-->
<!-- <dependency>-->
<!-- <groupId>dev.langchain4j</groupId>-->
<!-- <artifactId>langchain4j-community-redis-spring-boot-starter</artifactId>-->
<!-- <version>1.0.1-beta6</version>-->
<!-- </dependency>-->
<!--引入知识库rag-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-easy-rag</artifactId>
<version>1.0.1-beta6</version>
</dependency>
<!--pdf解析器依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-document-parser-apache-pdfbox</artifactId>
<version>1.0.1-beta6</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
</dependency>
</dependencies>
aiservice
package com.yuan.langchain4j.aiservice;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
import reactor.core.publisher.Flux;
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT,//手动装配
chatModel = "openAiChatModel",//指定模型
streamingChatModel = "openAiStreamingChatModel" ,//流式调用
// chatMemory = "chatMemory",//指定记忆对象
chatMemoryProvider = "chatMemoryProvider",//配置会话记忆提供者对象
contentRetriever = "contentRetriever",//配置向量数据库检索对象
tools = "reservationTool"//配置工具对象
)
//@AiService
public interface ConsultantService {
//用于聊天的方法
// public String chat(String message);
@SystemMessage(fromResource = "system.txt")
public Flux<String> chat(/*@V("msg")*/@MemoryId String memoryId, @UserMessage String message);
}
配置类
package com.yuan.langchain4j.config;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DocumentSplitter;
import dev.langchain4j.data.document.loader.ClassPathDocumentLoader;
import dev.langchain4j.data.document.parser.apache.pdfbox.ApachePdfBoxDocumentParser;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
import dev.langchain4j.memory.ChatMemory;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.rag.content.retriever.ContentRetriever;
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
import dev.langchain4j.store.embedding.EmbeddingStore;
import dev.langchain4j.store.embedding.EmbeddingStoreIngestor;
import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
@Configuration
public class CommonConfig {
/* @Autowired
private OpenAiChatModel modesl;
@Bean
public ConsultantService consultantService(){
ConsultantService consultantService = AiServices.builder(ConsultantService.class)
.chatModel(modesl)
.build();
return consultantService;
}*/
//构建回答记忆对象
// @Bean
// public ChatMemory chatMemory(){//用于管理聊天对话的上下文记忆。
// //采用窗口机制来管理对话历史,通过构建器模式(Builder Pattern)来进行实例化
// MessageWindowChatMemory memory = MessageWindowChatMemory.builder()
// .maxMessages(20)//记住最多20条对话
// .build();
// return memory;
// }
@Autowired
private ChatMemoryStore redisChatMemoryStore;
// @Autowired
// private EmbeddingModel embeddingModel;//向量模型
// 向量化数据存到RedisSearch
// @Autowired
// private RedisEmbeddingStore redisEmbeddingStore;
@Bean
public ChatMemoryProvider chatMemoryProvider(){
ChatMemoryProvider chatMemoryProvider = new ChatMemoryProvider() {
@Override
public ChatMemory get(Object memoryId) {
return MessageWindowChatMemory.builder()//使用构建器模式创建并返回一个MessageWindowChatMemory对象。
.id(memoryId)//设置唯一标识符为传入的memoryId,这样可以区分不同的对话会话
.maxMessages(20)//设置消息窗口的最大容量为20条,限制对话历史的长度
.chatMemoryStore(redisChatMemoryStore)//设置聊天内存存储库为RedisChatMemoryStore,用于存储和检索对话历史
.build();//调用构建器的build()方法,完成MessageWindowChatMemory对象的创建
}
};
return chatMemoryProvider;//结束匿名内部类的定义,并返回创建好的ChatMemoryProvider对象
}
//构建向量数据库操作对象
@Bean
public EmbeddingStore store(){//embeddingStore的对象, 这个对象的名字不能重复,所以这里使用store
//1.加载文档进内存,从类路径下的 "content" 目录加载所有文档 将加载的文档存储在 documents 列表中
// List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
// Document Loader,用于把磁盘或者网络中的数据加载进程序FileSystemDocumentLoader,根据本地磁盘绝对路径加载
// List<Document> documents = FileSystemDocumentLoader.loadDocuments("E:\\work\\Learningmaterials\\Learningmaterials\\扩展组件接入\\接入代码\\springbootqt\\LangChain4j\\src\\main\\resources\\content");
//从类路径的 content 目录加载文件,用 PDF 解析器处理,结果存到 documents 列表里。
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content",new ApachePdfBoxDocumentParser());
//2.创建一个内存中的向量存储实例 提供存储向量数据的容器
InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
//创建一个递归文档分割器 用于将文档切分成较小的块 最多500个字段重复字段为100
DocumentSplitter ds = DocumentSplitters.recursive( 500, 100);
//3.构建一个EmbeddingStoreIngestor对象,完成文本数据切割,向量化, 存储
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(store)//为构建器设置之前创建的向量存储对象 告诉处理器要将向量存储到哪里
// . embeddingStore(redisEmbeddingStore)
.documentSplitter(ds)//设置文档分割器
.build();//创建 EmbeddingStoreIngestor 实例
ingestor.ingest(documents);//完成从原始文档到向量数据的转换和存储
// return redisEmbeddingStore;//返回配置好的向量存储对象 使其成为 Spring 容器中的 Bean
return store;
}
//构建向量数据库检索对象
@Bean
//定义一个返回 ContentRetriever 的方法,方法参数中注入了之前创建的EmbeddingStore对象 创建用于检索向量数据的对象
public ContentRetriever contentRetriever(EmbeddingStore store){
return EmbeddingStoreContentRetriever.builder()//创建检索器的构建器
.embeddingStore(store)//设置检索器要使用的向量存储对象,告诉检索器从哪里获取数据
.minScore(0.5)//设置检索 最小相似度 0.5
.maxResults(3)//设置最多返回 3 条检索结果。控制返回结果的数量
// .embeddingModel(embeddingModel)//设置嵌入模型 用于将文本转换为向量表示
.build();//完成构建,返回配置好的 ContentRetriever 对象
}
}
Controller
package com.yuan.langchain4j.controller;
import com.yuan.langchain4j.aiservice.ConsultantService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
@RestController
public class ChatController {
@Autowired
private ConsultantService consultantService;
@RequestMapping(value = "/chat",produces = "text/html;charset=utf-8")
public Flux<String> chat(String memoryId, String message){
Flux<String> result = consultantService.chat(memoryId,message);
return result;
}
}
mapper
package com.yuan.langchain4j.mapper;
import com.yuan.langchain4j.pojo.Reservation;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ReservationMapper {
//1.添加预约信息
@Insert("insert into reservation(name,gender,phone,communication_time,province,estimated_score) values(#{name},#{gender},#{phone},#{communicationTime},#{province},#{estimatedScore})")
void insert(Reservation reservation);
//2.根据手机号查询预约信息
@Select("select * from reservation where phone=#{phone}")
Reservation findByPhone(String phone);
}
pojo
package com.yuan.langchain4j.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Reservation {
private Long id;
private String name;
private String gender;
private String phone;
private LocalDateTime communicationTime;
private String province;
private Integer estimatedScore;
}
Repository
package com.yuan.langchain4j.repository;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.ChatMessageDeserializer;
import dev.langchain4j.data.message.ChatMessageSerializer;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Duration;
import java.util.List;
/**
* @Repository注解的作用
* 主要用于标识数据访问层(DAO 层)负责与数据库进行交互,
* 执行 CRUD(创建、读取、更新、删除)操作。当 Spring 容器进行组件扫描时,
* 会检测带有@Repository注解的类,并将其注册为 SpringBean
*/
@Repository
public class RedisChatMemoryStore implements ChatMemoryStore {
//注入RedisTemplate
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
//获取会话消息
String json = redisTemplate.opsForValue().get(memoryId);
//把json字符串转化成List<ChatMessage>
List<ChatMessage> list = ChatMessageDeserializer.messagesFromJson(json);
return list;
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> list) {
//更新会话消息
//1.把list转换成json数据
String json = ChatMessageSerializer.messagesToJson(list);
//2.把json数据存储到redis中 ,存储时间为1天
redisTemplate.opsForValue().set(memoryId.toString(),json, Duration.ofDays(1));
}
@Override
public void deleteMessages(Object memoryId) {
redisTemplate.delete(memoryId.toString());
}
}
service
package com.yuan.langchain4j.service;
import com.yuan.langchain4j.mapper.ReservationMapper;
import com.yuan.langchain4j.pojo.Reservation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ReservationService {
@Autowired
private ReservationMapper reservationMapper;
//1.添加预约信息的方法
public void insert(Reservation reservation) {
reservationMapper.insert(reservation);
}
//2.查询预约信息的方法(根据手机号查询)
public Reservation findByPhone(String phone) {
return reservationMapper.findByPhone(phone);
}
}
toos类
package com.yuan.langchain4j.tools;
import com.yuan.langchain4j.pojo.Reservation;
import com.yuan.langchain4j.service.ReservationService;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class ReservationTool {
@Autowired
private ReservationService reservationService;
//1.工具方法: 添加预约信息
@Tool("预约志愿填报服务")
public void addReservation(
@P("考生姓名") String name,
@P("考生性别") String gender,
@P("考生手机号") String phone,
@P("预约沟通时间,格式为: yyyy-MM-dd'T'HH:mm") String communicationTime,
@P("考生所在省份") String province,
@P("考生预估分数") Integer estimatedScore
){
Reservation reservation = new Reservation(null,name,gender,phone, LocalDateTime.parse(communicationTime),province,estimatedScore);
reservationService.insert(reservation);
}
//2.工具方法: 查询预约信息
@Tool("根据考生手机号查询预约单")
public Reservation findReservation(@P("考生手机号") String phone){
return reservationService.findByPhone(phone);
}
}
yml
langchain4j:
#ai配置
open-ai:
chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: sk-6c70387ce0c9401483bc2bd2cfd2779b
model-name: qwen-plus
log-requests: true
log-responses: true
#流式调用
streaming-chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: sk-6c70387ce0c9401483bc2bd2cfd2779b
model-name: qwen-plus
log-requests: true
log-responses: true
#配置向量模型
embedding-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: sk-6c70387ce0c9401483bc2bd2cfd2779b
model-name: text-embedding-v3 #向量模型名,通用文本向量v3
log-requests: true
log-responses: true
max-segments-per-batch: 10 #每个批次的最大段数为10个
logging:
level:
dev.langchain4j: debug
#redis配置
spring:
data:
redis:
host: localhost
port: 6379
password:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/volunteer?serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true
username: root
password: root
前端代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AI志愿填报顾问</title>
<link href="min.css" rel="stylesheet">
<link rel="stylesheet" href="all_min.css">
<script src="min.js"></script>
<style>
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 输入框自适应高度 */
textarea {
min-height: 44px;
max-height: 200px;
transition: height 0.2s;
}
/* 加载动画 */
@keyframes pulse {
0%, 100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
.animate-pulse {
animation: pulse 1.5s infinite;
}
.delay-100 {
animation-delay: 0.1s;
}
.delay-200 {
animation-delay: 0.2s;
}
/* 打字机效果 */
.typing-cursor::after {
content: "|";
animation: blink 1s step-end infinite;
}
@keyframes blink {
from, to {
opacity: 1;
}
50% {
opacity: 0;
}
}
</style>
</head>
<body>
<div id="app" class="flex flex-col h-screen bg-gray-50">
<!-- 顶部导航栏 -->
<header class="bg-white shadow-sm py-3 px-4 flex items-center justify-between">
<div class="flex items-center">
<div class="text-xl font-bold text-blue-600">AI志愿填报顾问</div>
</div>
<div class="flex items-center space-x-3">
<button
@click="startNewConversation"
class="ml-2 p-3 rounded-lg bg-green-500 hover:bg-green-600 text-white" style="width: 50px">
<i class="fas fa-plus"></i>
</button>
<button @click="toggleDarkMode" class="p-2 rounded-full hover:bg-gray-100">
<i :class="darkMode ? 'fas fa-moon text-gray-600' : 'fas fa-sun text-gray-600'"></i>
</button>
</div>
</header>
<!-- 聊天内容区域 -->
<main class="flex-1 overflow-y-auto p-4 space-y-6" ref="chatContainer" :class="{ 'bg-gray-800': darkMode }">
<div v-for="(message, index) in messages" :key="index" class="max-w-3xl mx-auto">
<div :class="['flex', message.role === 'user' ? 'justify-end' : 'justify-start']">
<div :class="['flex items-start space-x-3', message.role === 'user' ? 'flex-row-reverse space-x-reverse' : '']">
<div :class="['w-8 h-8 rounded-full flex items-center justify-center',
message.role === 'user' ? 'bg-blue-100 text-blue-600' : 'bg-green-100 text-green-600',
darkMode && message.role === 'assistant' ? 'bg-gray-700 text-green-400' : '']">
<i :class="message.role === 'user' ? 'fas fa-user' : 'fas fa-robot'"></i>
</div>
<div :class="['p-3 rounded-lg max-w-lg',
message.role === 'user'
? 'bg-blue-500 text-white'
: darkMode
? 'bg-gray-700 text-gray-100 border-gray-600'
: 'bg-white shadow border border-gray-100']">
<div v-if="message.role === 'assistant' && message.isLoading" class="flex space-x-2">
<div :class="['w-2 h-2 rounded-full', darkMode ? 'bg-gray-400' : 'bg-gray-300', 'animate-pulse']"></div>
<div :class="['w-2 h-2 rounded-full', darkMode ? 'bg-gray-400' : 'bg-gray-300', 'animate-pulse delay-100']"></div>
<div :class="['w-2 h-2 rounded-full', darkMode ? 'bg-gray-400' : 'bg-gray-300', 'animate-pulse delay-200']"></div>
</div>
<div v-else class="whitespace-pre-wrap">
<span v-for="(char, charIndex) in message.content" :key="charIndex"
:class="{'opacity-0': charIndex >= message.visibleChars, 'fade-in': charIndex < message.visibleChars}">
{{ char }}
</span>
<span v-if="message.isStreaming" class="typing-cursor"></span>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- 输入框区域 -->
<footer :class="['border-t p-4', darkMode ? 'bg-gray-800 border-gray-700' : 'bg-white border-gray-200']">
<div class="max-w-3xl mx-auto relative">
<div class="flex items-center">
<textarea
v-model="userInput"
@keydown.enter.exact.prevent="sendMessage"
@keydown.ctrl.enter.exact.prevent="sendMessage"
@keydown.esc.exact="stopResponse"
placeholder="输入您的问题..."
:class="['flex-1 border rounded-lg py-3 px-4 pr-12 focus:outline-none focus:ring-2 resize-none',
darkMode
? 'bg-gray-700 border-gray-600 text-white focus:ring-blue-400 placeholder-gray-400'
: 'border-gray-300 focus:ring-blue-500 focus:border-transparent']"
rows="1"
ref="textarea"
@input="adjustTextareaHeight"
></textarea>
<!-- 新建会话按钮 -->
<button
@click="isLoading ? stopResponse() : sendMessage()"
:disabled="!userInput.trim() && !isLoading"
:class="['ml-2 p-3 rounded-lg',
isLoading
? 'bg-red-500 hover:bg-red-600 text-white'
: 'bg-blue-500 hover:bg-blue-600 text-white',
'disabled:opacity-50 disabled:cursor-not-allowed']"
>
<i :class="isLoading ? 'fas fa-stop' : 'fas fa-paper-plane'"></i>
</button>
</div>
</div>
</footer>
</div>
<script>
const {createApp, ref, nextTick, onMounted, watch} = Vue;
createApp({
setup() {
const messages = ref([]);
const userInput = ref('');
const isLoading = ref(false);
const chatContainer = ref(null);
const textarea = ref(null);
const darkMode = ref(false);
const memoeryId = ref(Date.now().toString());
let controller = null;
let typingInterval = null;
let currentTypingIndex = 0;
// 调整文本区域高度
const adjustTextareaHeight = () => {
const textareaEl = textarea.value;
textareaEl.style.height = 'auto';
textareaEl.style.height = `${Math.min(textareaEl.scrollHeight, 200)}px`;
};
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
if (chatContainer.value) {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
}
});
};
// 切换暗黑模式
const toggleDarkMode = () => {
darkMode.value = !darkMode.value;
localStorage.setItem('darkMode', darkMode.value);
};
// 新建会话
const startNewConversation = () => {
// 清空聊天记录
messages.value = [];
// 生成新的 memoryId
memoeryId.value = Date.now().toString();
// 添加欢迎消息
messages.value.push({
role: 'assistant',
content: '你好!我是传智教育提供的AI志愿填报顾问,请问有什么能帮到您?',
isLoading: false,
visibleChars: 0,
isStreaming: false
});
// 确保欢迎消息完全可见
messages.value[0].visibleChars = messages.value[0].content.length;
// 滚动到底部
scrollToBottom();
// 聚焦输入框
nextTick(() => {
textarea.value.focus();
});
};
// 模拟逐字打印效果
const startTypingEffect = (messageIndex) => {
const message = messages.value[messageIndex];
if (!message || message.visibleChars >= message.content.length) {
clearInterval(typingInterval);
typingInterval = null;
messages.value[messageIndex].isStreaming = false;
return;
}
messages.value[messageIndex].visibleChars++;
scrollToBottom();
};
// 发送消息
const sendMessage = async () => {
if (!userInput.value.trim() || isLoading.value) return;
// 中止之前的请求
if (controller) {
controller.abort();
}
controller = new AbortController();
const userMessage = {
role: 'user',
content: userInput.value.trim(),
isLoading: false,
visibleChars: userInput.value.trim().length,
isStreaming: false
};
messages.value.push(userMessage);
const assistantMessage = {
role: 'assistant',
content: '',
isLoading: true,
visibleChars: 0,
isStreaming: true
};
messages.value.push(assistantMessage);
userInput.value = '';
adjustTextareaHeight();
scrollToBottom();
isLoading.value = true;
try {
const response = await fetch(`/chat?message=${encodeURIComponent(userMessage.content)}&memoryId=${memoeryId.value}`, {
signal: controller.signal
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
let messageIndex = messages.value.length - 1;
// 先清除之前的打字效果
if (typingInterval) {
clearInterval(typingInterval);
typingInterval = null;
}
// 开始流式处理
while (true) {
const {done, value} = await reader.read();
if (done) break;
const chunk = decoder.decode(value, {stream: true});
buffer += chunk;
// 直接更新内容
messages.value[messageIndex].content = buffer;
messages.value[messageIndex].isLoading = false;
// 启动打字效果
if (!typingInterval) {
typingInterval = setInterval(() => {
startTypingEffect(messageIndex);
}, 20); // 调整这个值可以改变打字速度
}
scrollToBottom();
}
} catch (error) {
if (error.name === 'AbortError') {
console.log('请求被用户中止');
} else {
console.error('请求出错:', error);
const lastMessage = messages.value[messages.value.length - 1];
lastMessage.content = '抱歉,请求过程中出现错误: ' + error.message;
lastMessage.visibleChars = lastMessage.content.length;
}
} finally {
const lastMessage = messages.value[messages.value.length - 1];
lastMessage.isLoading = false;
lastMessage.isStreaming = false;
// 确保所有字符都可见
if (lastMessage.visibleChars < lastMessage.content.length) {
lastMessage.visibleChars = lastMessage.content.length;
}
isLoading.value = false;
controller = null;
if (typingInterval) {
clearInterval(typingInterval);
typingInterval = null;
}
scrollToBottom();
}
};
// 停止响应
const stopResponse = () => {
if (controller) {
controller.abort();
const lastMessage = messages.value[messages.value.length - 1];
lastMessage.isLoading = false;
lastMessage.isStreaming = false;
if (lastMessage.visibleChars < lastMessage.content.length) {
lastMessage.visibleChars = lastMessage.content.length;
}
isLoading.value = false;
controller = null;
if (typingInterval) {
clearInterval(typingInterval);
typingInterval = null;
}
}
};
// 初始化
onMounted(() => {
// 检查暗黑模式偏好
darkMode.value = localStorage.getItem('darkMode') === 'true' ||
(window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches);
// 添加欢迎消息
messages.value.push({
role: 'assistant',
content: '你好!我是传智教育提供的AI志愿填报顾问,请问有什么能帮到您?',
isLoading: false,
visibleChars: 0,
isStreaming: false
});
// 确保欢迎消息完全可见
messages.value[0].visibleChars = messages.value[0].content.length;
scrollToBottom();
// 聚焦输入框
nextTick(() => {
textarea.value.focus();
});
});
// 监听消息变化自动滚动
watch(messages, scrollToBottom, {deep: true});
return {
messages,
userInput,
isLoading,
darkMode,
chatContainer,
textarea,
sendMessage,
stopResponse,
toggleDarkMode,
adjustTextareaHeight,
startNewConversation
};
}
}).mount('#app');
</script>
</body>
</html>