本文涉及所有源码及笔记分享如下
链接: https://pan.baidu.com/s/1r1CJQxBXs2wFzt7Jr6g2Bg?pwd=eyww 提取码: eyww
大模型部署
本机ollama部署
- 下载Ollama
- 打开models
- 下载qwen3:0.6b
- 命令行运行
阿里云百炼平台
- 打开阿里云百炼平台申请秘钥
- 打开模型广场
- 选择通义千问-Plus
- 找到curl

- 打开apifox进行调用
- 复制url到地址框,在header里面填入Authorization,注意不要忽略Bearer以及后面的一个空格

- 填入json请求数据

大模型调用
常见参数

响应数据
- 什么是token信息:在LLM中,Token是大模型处理文本的基本单位,可以理解为模型看得懂的最小文本片段,用户输入的内容都需要转换为token,才能让大模型更好的处理。文本要转换成token,需要分词器,对于相同的文本片段,不同的分词器转换成的token数量不同
- 英文:1token=4字符
- 中文:1汉字=1~2个token

LangChain4j
会话功能
快速入门


新建project,选择maven项目,选择jdk17,选择quickstart

package org.ljj;
import dev.langchain4j.model.openai.OpenAiChatModel;
public class App {
public static void main(String[] args) {
// 初始化OpenAiChatModel对象
OpenAiChatModel model = OpenAiChatModel.builder()
.baseUrl("https://dashscope.aliyuncs.com/compatible-mode/v1")
.apiKey(System.getenv("DASHSCOPE_API_KEY")) // 配置到系统的环境变量里面
.modelName("qwen-plus")
.logRequests(true) // 详细输出请求信息
.logResponses(true) // 详细输出响应信息
.build();
// 使用模型进行对话
String result = model.chat("你是谁");
System.out.println(result);
}
}
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.ljj</groupId>
<artifactId>langchain4j-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>langchain4j-demo</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
<!--langchain4j依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>1.0.1</version>
</dependency>
<!--打印日志信息-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
</dependencies>
</project>
Spring整合LangChain4j

新建项目

选择Spring Web,因为我们要开发一个web项目访问浏览器

引入maven
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.ljj</groupId>
<artifactId>langchain4j-consultant</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>langchain4j-consultant</name>
<description>langchain4j-consultant</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--langchain4j起步依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
编写chatcontroller
package com.example.langchain4jconsultant.controller;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ChatController {
@Autowired
private OpenAiChatModel model;
@RequestMapping("/chat")
public String chat(String message){
return model.chat(message);
}
}
编写application.yml
langchain4j:
open-ai:
chat-model:
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
api-key: ${DASHSCOPE_API_KEY}
model-name: qwen-plus
log-requests: true # 开启打印详细请求日志
log-responses: true # 开启打印详细响应日志
# 日志配置为DEBUG级别
logging:
level:
dev.langchain4j: DEBUG
启动application应用,浏览器访问,可以看到响应

同时控制台有详细日志打印

AiServices工具类
之前我们调用大模型都是使用model.chat(message)的方式,这个方式不是很方便,因为我们之后在调用工具的时候需要配置很多的参数,但是langchain4j给我们提供了AIService工具类,封装了model对象和其他的一些操作

引入aiservices依赖
<!--langchain4j的AIService相关依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-spring-boot-starter</artifactId>
<version>1.0.1-beta6</version>
</dependency>
创建如图所示的三个类

AIServices工具类也可以声明式使用

修改两个类:1.注释掉Config类中的consultantService方法,2.在ConsultantService类中进行修改
package com.example.langchain4jconsultant.aiservice;
import dev.langchain4j.service.spring.AiService;
import dev.langchain4j.service.spring.AiServiceWiringMode;
// 方式一:手动装配,需要自己指定参数,为方便学习理解,先采用这种方式
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT, // 手动装配,表示将来AiService工具类为我们创建代理对象的时候,装配的时候是手动装配还是自动装配。EXPLICIT表示手动装配
chatModel = "openAiChatModel" // 注入模型,langchain4j会注入名为openAiChatModel的bean作为聊天模型,默认是类名的首字母小写
)
// 方式二:自动装配(两种方式二选一) 直接在接口类上方写一个注释@AiService
//@AiService
public interface ConsultantService {
// 用于聊天的接口
public String chat(String message);
}
流式调用
之前我们在yml文件中写的是chat-model,那么langchain4j就会为容器自动注入一个名为"OpenAiChatModel"的对象,这种类型的对象都是阻塞式调用

但是当我们改为"streaming-chat-model",那么就会注入一个OpenAiStreamChatModel对象,该对象调用的模型都是流式调用


和前端对接,写好前端页面代码之后直接访问http://localhost:8080/index.html
<!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="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
<script src="https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.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>
执行效果:

消息注解
如果我们规定这个顾问只能回答志愿填报相关的问题,而对其他问题不予回答,那么我们就必须采用系统消息的消息注解

langchain4j中提供了两个注解来设置系统消息:一个是SystemMessage、一个是UserMessage
- SystemMessage
第一种方式,直接写到括号里面;第二种方式,当我们的系统消息过长,我们可以把他们写到txt文件中再进行读取。

在ConsultantService的方法上面,进行注解

或者准备一个外部文件

- UserMessage

langchain4j有两种用户消息的定义方式,一种是在括号内部写**{{it}}**,注意字母不能变;如果不想写it,就可以采用第二种方式,写个@V手动起名字,并且保证两处名字一致,就能获取得到。


我们可以看到他的回复并没有之前我们使用SystemMessage时候表现的出色,这是因为我们发送的是User消息

我们使用用户消息的第二种方式


会话记忆
初步实现会话记忆
大模型是不具备记忆能力的,要想让大模型记住之前聊天的内容,唯一的办法就是把之前的聊天内容与新的提示词一起发送给大模型

在之前我们为了演示记忆效果,采取手动拼接消息,现在我们采用langchain框架,可以实现自动记录聊天消息并且发送
整体流程如图所示

浏览器用户输入一条消息,web后端有一个存储容器,这条消息经过web后端被存到存储对象当中,再被发送给大模型,大模型做出响应,比如说“是的”,此时还是需要先经过web后端,存储到存储容器当中,再连并之前的消息一起发送给浏览器,这样循环往复,每一次消息的对话内容都能被双方完整接受到,实现了“记忆”的效果

关键是在配置类中配置的MessageWindowChatMemory会话记忆对象,然后在AiService注解中配置

效果可以看到真正实现了会话记忆,并且从控制台可以看到之前的对话都被当做记忆进行了携带


会话隔离
到此为止仅仅是实现了会话记忆,但是还有一个重要的点就是要实现会话隔离,不然即便在不同的用户端发送消息,所有用户的消息都会被当成一个用户的消息进行传输,如下面两张图片所示:在第二张图中我并没有说是哪个大学,但是他直接获取到之前的记忆进行了回答


出线上面的原因就是因为我们只向IOC容器中注入了一个chatMemory对象,所有会话使用的是同一个记忆对象,因此不同会话之间的记忆并没有做到隔离,通过下面的方式才能真正做到会话隔离

解决办法
定义会话对象提供者chatMemoryProvider,注意不是直接定义会话记忆对象,将来如果langchain4j没有找到指定id1的chatMemory对象,chatMemoryProvider就会调用get方法来获取一个新的chatMemory对象使用,参数就是Object类型的memoryId

在单击前端页面的加号,就会触发startNewConversation,该按钮以当前的时间戳来定义一个新的会话id,如果不单击这个按钮就不会新建会话,还是会沿用之前的id


控制台中信息可以看出成功做了隔离


持久化记忆
我们发现只有后端一重启服务,用户的会话信息就丢失了,在实际开发过程中,后端服务重启是很常见的,不能因为重启后端服务而丢失用户信息,下面是重启丢失的原因:
MessageWindowChatMemory这个对象内部有一个ChatMemoryStore成员变量,所以我们实现message的add方法底层其实是store帮我们实现的,所以我们需要分析这个store是如何帮我们实现的,ChatMemoryStore是一个接口,它里面提供了三个方法分别用于根据id得到、更新、删除会话记录,langchain4j给这个接口提供了两个实现类,一个是InMemoryChatMemoryStore,一个是SingleSlotChatMemoryStore;在我们的MessageWindowChatMemory的store中,默认使用的是SingleSlotChatMemoryStore存储会话记录,在其中维护了一个messages集合来存储会话对象,很明显是服务器内存存储,所以一旦当服务器重启,这些消息必然丢失,我们不能把消息存储在内存中,而是需要把他们存储在redis、mysql等地方

最直观的方法就是不让MessageWindowChatMemory使用SingleSlotChatMemoryStore来存储会话,我们要自己提供一个chatMemoryStore的实现类,在其中我们自己把消息存储到其他地方,然后再把我们自己写的chatMemoryStore的实现类对象交给MessageWindowChatMemory来使用即可
我们使用redis来实现


代码环节:
首先安装docker,之后配置镜像源,其次下载redis镜像
docker run --name redis -d -p 6379:6379 redis
docker ps查看已经启动的镜像
启动之后回到idea
下载依赖
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
# yml文件中补充redis
spring:
data:
redis:
host: localhost
port: 6379
创建一个repository包,在里面创建一个RedisChatMemoryStore.java
package com.example.langchain4jconsultant.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;
// 重写ChatMemoryStore接口,不在使用默认的SingleSlotChatMemoryStore
@Repository
public class RedisChatMemoryStore implements ChatMemoryStore {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
// 根据memoryId从Redis中获取消息
String json = stringRedisTemplate.opsForValue().get(memoryId.toString());
// 把json数据转换成List<ChatMessage>类型,需要用到反序列化
List<ChatMessage> chatMessages = ChatMessageDeserializer.messagesFromJson(json);
return chatMessages;
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> list) {
// 更新会话消息
// 1.把list数据转换成json数据
String json = ChatMessageSerializer.messagesToJson(list);
// 2.使用stringRedisTemplate的opsForValue().set()方法存储到Redis中, 存储时间设置为1天
// memoryId是键,注意:memoryId需要转换成String类型,因为Redis的key是String类型
stringRedisTemplate.opsForValue().set(memoryId.toString(), json, Duration.ofDays(1));
}
@Override
public void deleteMessages(Object memoryId) {
stringRedisTemplate.delete(memoryId.toString());
}
}
然后在commonConfig中进行修改

可以看到成功在redis中存储了会话记录,键名就是memoryId

RAG知识库
原理
大模型训练用的是通用知识库,一般知识仅仅更新到几年前,为了获取最新消息,需要外挂一个知识库



整体原理就是我们将一个文档(document),通过文本分割器(text splitter),把大的文档分成小的片段(segments),然后这些小的文本片段需要用一种专门的模型叫做向量模型(Embedding model 专门擅长文本向量化的模型),把一个个的文本片段转换成向量,然后我们只需要把文本片段和向量一起存储到向量数据库中即可

也就是下面的流程:

用户向模型提问并且获得回答的流程如图所示:
用户输入的问题被向量模型转换为向量,再拿着去向量数据库中的每一条记录通过余弦相似度进行比对,把满足要求的向量筛选出来得到对应的文本片段,最后再把用户的问题以及文本片段共同组成要提交给大模型的消息,让大模型根据消息生成回答即可

快速入门
- 存储(构建向量数据库操作对象,把数据向量化之后存储到向量数据库中)

第一步引入依赖,
第二步将本地存储知识的文档加载到内存当中,每一个文档都会生成一个Document对象用来记录文档的内容,
第三步构建向量数据库操作对象,langchain4j给我们提供了一个操作内存版本的向量数据库的类,这个类叫做InMemoryEmbeddingStore,
第四步把内存中的文本对象切割成一个一个的小文本片段,然后使用向量模型把这些片段存储到向量数据库之后,langchain4j为我们提供了一个类叫做EmbeddingStoreIngestor,它把很多操作细节都封装起来了,可以帮我们快速完成这一步的操做,我们需要构建这个对象,构建的时候需要告诉他我要把向量化的数据存储到哪里,也就是把我们刚才创建的InMemoryEmbeddingStore这个对象设置进去就可以了,然后调用ingest方法,把documents对象传进去,方法内部会使用内置的文本分割器先分隔,然后调用内置的向量模型向量化,把结果存储到向量数据库当中。
- 检索(构建向量数据库检索对象,用于从向量数据库中检索出对于的文本片段)

首先构建向量数据库检索对象,langchain4j提供的检索对象叫做EmbeddingStoreContentRetrieve,对象调用三个方法:
第一个是embeddingstore告诉对象从哪里检索
第二个调用minStore方法设置最小的余弦相似度的值,检索的时候会把用户的问题向量化,然后与数据库中已经存在的向量计算余弦相似度,值越大,相似度越高,使用minStore设置一个值可以保证检索出来的值和用户的问题相关度很高
第三个maxResult是设置最大的检索出来的文本数量,会选取分数(相关性)最高的几个片段,因为如果把所有检索出来的都发送个大模型,那么token消耗很大,并且一些分数很低的片段被发送给大模型会影响大模型最终生成的结果
最后在@AiService注解中配置我们的retriever
代码部分:
- 在Config中进行编辑,两个方法,一个是用于向量存储的EmbeddingStore,一个是用于检索的ContentRetriever

- 在AiService注解中进行retriever的配置

核心API
整体流程:

文档加载器


文档解析器

// 存储:构建EmbeddingStore对象(向量数据库操作对象)
@Bean
public EmbeddingStore store(){
// 1.加载文档进内存
// List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content", new ApachePdfBoxDocumentParser());
// List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:\\java_code\\langchain4j-consultant\\src\\main\\resources\\content");
//2.构建向量数据库操作对象
InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
//3.构建一个EmbeddingStoreIngetor
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
.embeddingStore(store)
.build();
ingestor.ingest(documents);
return store;
}
文本分割器

注意各种文本分割器虽然是按照不同的标准(段落,行,句子等等)对文本进行的分割,但是分割后的结果并不是进行向量化的文本片段,而我们要进行向量化的文本片段是根据这六部分组合而来的,通常情况下,langchian4j是允许我们指定文本片段的字符容量的。
比如我们按照段落分割文本:默认单个文本片段的最大字符数量是300,在组合文本片段的时候,一个文本片段可以放多个原来的段落,但是所有段落的字数综合加起来不能超过300

这里我们针对最后一种DocumentSplitters.recursive(…)进行讨论,他是递归分割器,他组合了段落分割器、行分割器、句子分割器和词分割器,会按照优先级进行分割。比如上图还想再放入第三个段落是放不下的,他就会进一步把这个段落分成行,如果还是放不下就分成句子,还是放不下就分成词语。
如果我们不想设置默认字符数300,我想多设置一些字符,我们可以切换文本分割器。一共有两步骤:1、构建文本分割器对象,2、设置文本分割器对象
设置片段之间的重叠字符的个数是为了保证片段之间的连贯性。

CommonConfig.java

向量模型
使用通义千问为我们提供的向量模型,还是先=需要在yml文件中进行指定

之后把该对象交给EmbeddingStoreIngestor和EmbeddingStoreContentRetriver即可

代码部分:
- yml文件进行补充

向量数据库操作


首先下载镜像

下载依赖之后,修改yml文件:

修改代码
@Autowired
private RedisEmbeddingStore redisEmbeddingStore; // 使用Redis作为向量数据库
// 存储:构建EmbeddingStore对象(向量数据库操作对象)
@Bean
public EmbeddingStore store(){
// 1.加载文档进内存
// List<Document> documents = ClassPathDocumentLoader.loadDocuments("content");
List<Document> documents = ClassPathDocumentLoader.loadDocuments("content", new ApachePdfBoxDocumentParser());
// List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:\\java_code\\langchain4j-consultant\\src\\main\\resources\\content");
// List<Document> documents = FileSystemDocumentLoader.loadDocuments("D:\\java_code\\langchain4j-consultant\\src\\main\\resources\\content", new ApachePdfBoxDocumentParser());
//2.构建向量数据库操作对象,操作的是内存版本的向量数据库
// InMemoryEmbeddingStore store = new InMemoryEmbeddingStore();
// 构建文本分割器对象
DocumentSplitter ds = DocumentSplitters.recursive(300, 100);
//3.构建一个EmbeddingStoreIngetor
EmbeddingStoreIngestor ingestor = EmbeddingStoreIngestor.builder()
// .embeddingStore(store)
.embeddingStore(redisEmbeddingStore) // 向量数据库操作对象
.documentSplitter(ds) // 文本分割器对象
.embeddingModel(embeddingModel) // 向量模型对象
.build();
ingestor.ingest(documents);
return redisEmbeddingStore;
}
// 检索:构建向量数据库检索对象
@Bean
public ContentRetriever contentRetriever(/*EmbeddingStore store*/){
return EmbeddingStoreContentRetriever.builder()
.maxResults(3)
.minScore(0.5)
// .embeddingStore(store)
.embeddingStore(redisEmbeddingStore)
.embeddingModel(embeddingModel)
.build();
}
启动之后,效果如图所示

当我们把所有的pdf文档放进去之后,启动会报错


原因是百炼平台对一次性发送给大模型要向量化处理的文本片段的数量进行了限制,不限制的话处理过程和占用内存很大

那么我们就需要在代码中进行配置max-segment-per-batch,告诉后端服务一次最多给向量模型发送几个片段,一次发不完,多发几次就可以了

可以看到成功存储了很多向量化之后的片段

遗留问题
因为我们给方法标注了@Bean注解,意味着每一次我们启动应用,方法就会重启一次,那么就会重新进行分段、向量化等操作(如CommonConfig中的store、contentRetriever方法),十分浪费资源,因为我们已经把向量化之后的数据存储到外部向量数据库中了(Redis中已经有数据),所以我们就不需要让这个方法每次都执行了,那我们就可以把方法上面的@Bean注释掉,这些方法在重启之后就不会被二次执行了
但是上面这种节约资源的方法还是需要在有@Bean的情况下第一次执行了向量化存储之后才可以采纳(取消@Bean注解)

Tools工具
以前叫做Function Calling,现在叫做Toolings
准备工作

mysql数据库
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文件增加三个依赖

yml文件配置mysql数据库连接信息、驼峰命名自动转换

配置实体类
package com.example.langchain4jconsultant.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data // 生成get/set方法
@NoArgsConstructor // 无参构造函数
@AllArgsConstructor // 全参构造函数
public class Reservastion {
// bigint用Long
private Long id;
private String name;
private String gender;
private String phone;
// datetime用LocalDateTime
private LocalDateTime communicationTime;
// int用Integer
private Integer estimatedScore;
}
配置mapper
package com.example.langchain4jconsultant.mapper;
import com.example.langchain4jconsultant.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);
}
配置service,通常配置service需要配置接口和impl实现类,但是这里比较简单,所以直接写成实现类
package com.example.langchain4jconsultant.service;
import com.example.langchain4jconsultant.mapper.ReservationMapper;
import com.example.langchain4jconsultant.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 addReservation(Reservation reservation) {
reservationMapper.insert(reservation);
}
// 2.根据手机号查询预约信息
public Reservation getReservationByPhone(String phone) {
return reservationMapper.findByPhone(phone);
}
}
测试——单元测试
package com.example.langchain4jconsultant;
import com.example.langchain4jconsultant.pojo.Reservation;
import com.example.langchain4jconsultant.service.ReservationService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
@SpringBootTest
public class ReservationServiceTest {
@Autowired
private ReservationService reservationService;
// 测试添加
@Test
void testAddReservation() {
// 创建一个Reservation对象
Reservation reservation = new Reservation(null, "张三", "男", "12345678901", LocalDateTime.now(), "北京", 5);
// 调用ReservationService的addReservation方法
reservationService.addReservation(reservation);
}
// 测试查询
@Test
void testGetReservationByPhone() {
// 调用ReservationService的getReservationByPhone方法
Reservation reservation = reservationService.getReservationByPhone("12345678901");
// 验证返回的结果是否正确
if (reservation != null) {
System.out.println("预约信息: " + reservation);
} else {
System.out.println("未找到预约信息");
}
}
}
原理
为了实现实时将用户的数据存入数据库,就需要AI能够自动调用函数接口实现数据的存储
当我们加入Tools之后,整个工作流程就会发生一些改变:当用户把问题发送给AI应用,在AI应用的内部需要组织交给大模型的数据,而这些数据里边,需要描述清楚我们AI应用中有哪些函数能够被大模型调用。每一个函数的描述需要包含三个部分,分别是方法的名称、方法的作用以及方法的入参。当AI应用把这些数据发送给大模型了以后,大模型会根据用户的问题以及上下文去拆解任务,从而判断是否需要调用函数。如果有函数需要调用,则需要把调用的函数的名称以及调用时需要用到的参数给它准备好,一并响应给AI应用。AI应用接收到响应了之后,需要执行对应的函数并得到结果。接下来需要把得到的结果和之前的信息一块儿组织好,再发送给大模型应用。需要注意的是,由于在一次任务的处理过程中可能需要根据顺序调用多个函数,所以当大模型接收到AI应用发送的数据之后,它会继续拆解任务,如果发现还要调用其他的函数,那这一块就会重复4.1~4.4这几个步骤,直到无需函数的调用,最终把生成的结果响应给AI应用,然后呢,由AI应用再发送给用户。
这就是增加了function calling或者是tools工具之后整个AI应用的工作流程,好消息是,下面这些工作launchain4j都能够帮我们自动的完成。对于我们来说,只需要按照langchain4j的规则描述清楚有哪些方法可以被大模型调用,方法的名字是什么,方法的作用是什么,以及都需要哪些参数。

实现
@Tool注解用于对方法的作用进行描述
@P注解用于对方法的参数进行描述
还要在tool方法的上面添加@Component以便于被IOC容器识别

package com.example.langchain4jconsultant.tool;
import com.example.langchain4jconsultant.pojo.Reservation;
import com.example.langchain4jconsultant.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;
// 工具方法:添加预约信息
@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.addReservation(reservation);
}
// 工具方法:根据手机号查询预约信息
@Tool("根据考生手机号查询预约信息")
public Reservation getReservationByPhone(
@P("考生手机号") String phone
) {
return reservationService.getReservationByPhone(phone);
}
}
下一步我们需要配置工具方法,我们需要把上述工具方法告诉langchain4j,这样才能自动找到工具方法,根据反射获取这些信息,最后组织数据发送给大模型
@AiService(
wiringMode = AiServiceWiringMode.EXPLICIT, // 手动装配,表示将来AiService工具类为我们创建代理对象的时候,装配的时候是手动装配还是自动装配。EXPLICIT表示手动装配
chatModel = "openAiChatModel", // 注入模型,langchain4j会注入名为openAiChatModel的bean作为聊天模型,默认是类名的首字母小写
streamingChatModel = "openAiStreamingChatModel", // 注入流式模型,langchain4j会注入名为openAiStreamingChatModel的bean作为流式聊天模型,默认是类名的首字母小写
// chatMemory = "chatMemory", // 注入会话记忆,langchain4j会注入名为chatMemory的bean作为会话记忆,默认是类名的首字母小写
chatMemoryProvider = "chatMemoryProvider",
contentRetriever = "contentRetriever", // 注入内容检索器,langchain4j会注入名为contentRetriever的bean作为内容检索器,默认是类名的首字母小写
tools = "reservationTool" // 注入工具,langchain4j会注入名为reservationTool的bean作为工具,默认是类名的首字母小写
)
启动之后发起对话

我们来看后台信息,在控制台输出的信息中我们很明显看到后台调用了工具方法向数据库进行信息的存储
2025-08-07T16:47:41.639+08:00 INFO 32344 --- [hain4j-OpenAI-1] d.l.http.client.log.LoggingHttpClient : HTTP request:
- method: POST
- url: https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions
- headers: [Authorization: Beare...67], [User-Agent: langchain4j-openai], [Content-Type: application/json]
- body: {
"model" : "qwen-plus",
"messages" : [ {
"role" : "system",
"content" : "你是一个专业的AI志愿填报顾问,可以给用户提供如下功能:\r\n1.查询目标院校的院校简介\r\n2.查询目标院校的录取规则\r\n3.查询目标院校的奖学金设置状况\r\n4.查询目标院校的食宿条件\r\n5.查询目标院校招生联系方式\r\n6.查询目标院2024年不同专业录取情况\r\n7.查询热门专业\r\n8.查询天坑专业\r\n9.根据学生提供的分数和不同学校以及学校历年录取分数,推荐合适的学校和专业,每次根据匹配度,按照冲、稳、保的逻辑,罗列出合适的学校以及专业,给用户呈现时需要呈现学校名称、专业名称、历年录取分数以及专业热度\r\n10.高考志愿填报一对一沟通预约服务\r\n11.查询志愿指导服务预约详情\r\n说明:\r\n 1.每次回答完用户问题,最后都加上一句话:<br/>志愿填报需要考虑的因素有很多,如果要得到专业的志愿填报指导,建议您预约一个一对一的指导服务,是否需要预约?\r\n 2.下预约单需要用户提供生姓名、考生性别、考生电话、考生预约沟通时间(日期+时间)、考生所在省份、考生预估分数,当用户表达出需要预约志愿指导服务的意愿后,你不能自己模拟这些数据下预约单,而是需要以委婉的方式引导用户提供考生姓名、考生性别、考生电话、考生预约沟通时间(日期+时间)、考生所在省份、考生预估分数,这些信息必须是用户全部提供,不能有模拟数据,否则不要下预约单\r\n 3.一旦预约成功,最后不要再跟上面第1条指定的话术,而是更改为:恭喜您,一对一志愿指导服务已经预约成功,我们会准时联系您,请注意接听电话!\r\n 4.给用户的回复中,不要提及类似\"根据您提供的信息/根据资料中的信息\"这样的话术\r\n\r\n你是智能志愿填报咨询师,只回答有关高考志愿填报的问题,其它问题不予回答。\r\n"
}, {
"role" : "user",
"content" : "西北大学是211吗\n\nAnswer using the following information:\n475 515 37605 532 -\n西北大学2024年分专业录取情况\n\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n序号 专业 批次 本科线 最低分 位次 平均分 备注\n16 工商管理类 本科一批 488 555 2737 562 -\n17 公共管理类 本科一批 488 553 2974 558 -\n18 广播电视编导 本科一批 488 514 8171 524 -\n19 戏剧影视文学 本科一批 488 515 7923 532 --\n20 档案学 本科一批 488 559 2425 562 --\n序号 专业 批次 本科线 最低分 位次\n\n28 生物医学科学 本科一批 475 569 16380 576 -\n29 药学类 本科一批 475 571 15845 576 -\n30 生态学 本科一批 475 567 17058 568 -\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n序号 专业 批次 本科线 最低分 位次 平均分 备注\n31 电子信息类 本科一批 475 575 14714 591 -\n32 计算机类 本科一批 475 584 12472 598 -\n33 化工与制药类 本科一批 475 568 16686 572 -"
}, {
"role" : "assistant",
"content" : "是的,西北大学是211工程重点建设高校之一,同时也是一所具有较高声誉和综合实力的重点大学。\n\n以下是部分2024年西北大学不同专业的录取情况:\n\n- **工商管理类**:本科一批,最低分555,平均分562\n- **公共管理类**:本科一批,最低分553,平均分558\n- **广播电视编导**:本科一批,最低分514,平均分524\n- **戏剧影视文学**:本科一批,最低分515,平均分532\n- **档案学**:本科一批,最低分559,平均分562\n- **生物医学科学**:本科一批,最低分569,平均分576\n- **药学类**:本科一批,最低分571,平均分576\n- **生态学**:本科一批,最低分567,平均分568\n- **电子信息类**:本科一批,最低分575,平均分591\n- **计算机类**:本科一批,最低分584,平均分598\n- **化工与制药类**:本科一批,最低分568,平均分572\n\n这些专业均在本科一批进行录取,具体录取分数和位次可以作为参考。\n\n志愿填报需要考虑的因素有很多,如果要得到专业的志愿填报指导,建议您预约一个一对一的指导服务,是否需要预约?"
}, {
"role" : "user",
"content" : "我需要预约\n\nAnswer using the following information:\n475 515 37605 532 -\n西北大学2024年分专业录取情况\n\n2024 陕西 普通类 理工 水利类 63 596 574 578 475\n2024 陕西 普通类 理工 葡萄与葡萄酒工程(卓越工程师班) 14 593 573 582 475\n2024 陕西 普通类 理工 智慧水利 12 584 573 576 475\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n西北农林科技大学2024年分专业录取情况\n年份 省份 类别 科类 专业名称 录取人数 最高分 最低分 平均分 控制线\n\n2024 陕西 中外合作 理工 建筑学(中外合作办学) 605 600 595 物理+化学\n2024 陕西 提前文 文史 马克思主义理论 615 600 596 思想政治\n西安交通大学2024年分专业录取情况\n西安交通大学2024年分专业录取情况\n西安交通大学2024年分专业录取情况"
}, {
"role" : "assistant",
"content" : "为了完成您的预约,我需要您提供以下信息:\n\n- 考生姓名\n- 考生性别\n- 考生手机号\n- 考生预约沟通时间(请提供具体日期和时间段)\n- 考生所在省份\n- 考生预估分数\n\n请您提供以上信息,以便我们为您安排一对一志愿指导服务。"
}, {
"role" : "user",
"content" : "zzx,男,18888888888,7.30,山西省,750\n\nAnswer using the following information:\n475 515 37605 532 -\n西北大学2024年分专业录取情况\n\n15 知识产权 本科一批 475 568 16686 575 -\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n西北大学2024年分专业录取情况\n序号 专业 批次 本科线 最低分 位次 平均分 备注\n16 数学与应用数学(熊庆来班) 本科一批 475 598 9394 600 -\n17 金融数学 本科一批 475 574 15024 576 -\n18 数学类 本科一批 475 576 14467 585 -\n19 物理学类 本科一批 475 573 15306 578 -\n20 化学类 本科一批 475 568 16686 574 -\n\n2024 陕西 优师专项 理工\\物理类 英语(国家优师专项) 2 596 596 596 475\n2024 陕西 优师专项 理工\\物理类 生物科学(国家优师专项) 4 606 589 595.8 475\n2024 陕西 优师专项 理工\\物理类 数学与应用数学(国家优师专项) 21 622 588 594.3 475\n2024 陕西 优师专项 理工\\物理类 物理学(国家优师专项) 11 607 586 592.9 475\n2024 陕西 优师专项 理工\\物理类 化学(国家优师专项) 14 611 583 591 475\n年份 省市 类别 科类 专业名称 录取人数 最高分 最低分 平均分 控制线"
}, {
"role" : "assistant",
"tool_calls" : [ {
"id" : "call_3023840324894a33b90d77",
"type" : "function",
"function" : {
"name" : "addReservation",
"arguments" : "{\"communicationTime\": \"2024-07-30T00:00\", \"estimatedScore\": 750, \"gender\": \"男\", \"name\": \"zzx\", \"phone\": \"18888888888\", \"province\": \"山西省\"}"
}
} ]
}, {
"role" : "tool",
"tool_call_id" : "call_3023840324894a33b90d77",
"content" : "Success"
} ],
"stream" : true,
"stream_options" : {
"include_usage" : true
},
"tools" : [ {
"type" : "function",
"function" : {
"name" : "getReservationByPhone",
"description" : "根据考生手机号查询预约信息",
"parameters" : {
"type" : "object",
"properties" : {
"phone" : {
"type" : "string",
"description" : "考生手机号"
}
},
"required" : [ "phone" ]
}
}
}, {
"type" : "function",
"function" : {
"name" : "addReservation",
"description" : "预约志愿填报服务",
"parameters" : {
"type" : "object",
"properties" : {
"name" : {
"type" : "string",
"description" : "考生姓名"
},
"gender" : {
"type" : "string",
"description" : "考生性别"
},
"phone" : {
"type" : "string",
"description" : "考生手机号"
},
"communicationTime" : {
"type" : "string",
"description" : "沟通预约时间,格式为:yyyy-MM-dd'T'HH:mm"
},
"province" : {
"type" : "string",
"description" : "考生所在省份"
},
"estimatedScore" : {
"type" : "integer",
"description" : "考生预估分数"
}
},
"required" : [ "name", "gender", "phone", "communicationTime", "province", "estimatedScore" ]
}
}
} ]
}
数据库中成功存储了信息

自此项目完结!
项目总结
一、用到的大模型相关知识
- 大语言模型(LLM, Large Language Model)
原理:基于Transformer架构,通过大规模语料训练,具备强大的自然语言理解和生成能力。
应用:本项目通过LangChain4j集成了OpenAI(如GPT-3/4)或阿里云通义千问(Qwen)等大模型,实现了智能问答、对话等功能。 - 向量化与Embedding
Embedding模型:将文本(如用户问题、文档内容)转化为高维向量,便于相似度计算和检索。
用途:用于知识检索(RAG)、语义搜索、上下文记忆等。
实现:通过配置embedding-model,如OpenAI的text-embedding-v3或Qwen的embedding接口。 - RAG(Retrieval-Augmented Generation,检索增强生成)
原理:先用Embedding模型将知识库内容向量化,用户提问时将问题向量化后检索最相关的内容,再与大模型结合生成答案。
实现:本项目用EmbeddingStore存储向量,ContentRetriever检索相关内容,结合大模型生成更有知识性的回答。 - 会话记忆(Chat Memory)
原理:保存用户历史对话,实现多轮对话上下文理解。
实现:通过ChatMemory和ChatMemoryStore(如Redis持久化)实现,memoryId用于区分不同会话。 - 流式响应(Streaming)
原理:大模型生成内容时,前端可实时接收token流,提升用户体验。
实现:后端接口返回Flux,前端用SSE(Server-Sent Events)或Fetch流式处理。
二、用到的开发技术
- LangChain4j 框架
作用:Java生态下的LangChain实现,封装了大模型调用、RAG、记忆、服务注入等能力。
用法:
@AiService注解定义AI服务接口
自动装配模型、记忆、检索等Bean
支持多种大模型(OpenAI、Qwen等) - Spring Boot
作用:主流Java后端开发框架,负责依赖注入、配置管理、Web服务等。
用法:
@Configuration、@Bean、@Autowired等注解管理Bean
@RestController暴露HTTP接口
application.yml集中配置 - Redis
作用:高性能键值数据库,用于会话记忆和(可选)向量存储。
用法:
ChatMemoryStore自定义实现,持久化对话历史
可选:Redis Stack支持向量检索(需Redis Search模块) - 前端技术
Vue3:响应式前端框架,负责页面渲染、状态管理、与后端交互。
Tailwind CSS:实用型CSS框架,快速构建美观UI。
Fetch API:与后端流式通信,支持流式响应和打字机效果。 - 其他技术点
文档加载与解析:支持PDF、TXT等格式,使用Apache PDFBox、Tika等解析器。
文本分割:长文档分割为小块,提升检索和大模型处理效率。
依赖管理:Maven管理依赖,集成LangChain4j、Spring Boot、Redis等库。
日志与调试:通过配置文件开启详细请求/响应日志,便于排查问题。
三、项目架构与流程简述
● 前端:用户输入问题,生成/维护memoryId,发送请求到后端。
● 后端:
○ 接收请求,按memoryId检索/保存会话历史
○ 问题向量化,检索知识库相关内容(RAG)
○ 调用大模型生成答案,支持流式返回
○ 会话记忆持久化到Redis
○ 知识库:文档预处理、分割、向量化,存入向量数据库(内存或Redis)。
○ 配置:所有模型、存储、日志等参数集中在application.yml,支持灵活切换和扩展。
四、总结
本项目是一个典型的“RAG+多轮对话”智能问答系统,融合了大模型、知识检索、会话记忆、流式交互等多项AI与现代Web开发技术,具备如下特点:
● 大模型能力:支持主流大模型(OpenAI、Qwen等),可灵活切换
● 知识增强:通过RAG让大模型具备“专属知识库”
● 多轮对话:会话记忆持久化,支持上下文连续对话
● 高性能:Redis加速存储,流式响应提升体验
● 易扩展:基于Spring Boot和LangChain4j,配置灵活,易于二次开发
1790

被折叠的 条评论
为什么被折叠?



