系列文章目录
第一章 「Java AI实战」LangChain4J - 接入Xinference本地大模型
第二章 「Java AI实战」LangChain4J - ChatAPI 及常用配置
第三章 「Java AI实战」LangChain4J - 向量数据库接入与语义检索
第四章 「Java AI实战」LangChain4J - Agent智能体开发
第五章 「Java AI实战」LangChain4J - 记忆缓存
第六章 「Java AI实战」LangChain4J - 文本分类器实践
第七章 「Java AI实战」LangChain4J - 多 Agent 智能体协作
文章目录
前言:为什么要在 Java 项目里上“记忆”?
很多团队在做「大模型接入」的时候,第一步都是一样的:
前端给个输入框,后端用 Java 调一下 OpenAI / DeepSeek / 本地大模型,然后把回答原样返回给前端——这类「纯问答」模式上线很快,但用一段时间你会发现几个明显的痛点:
- 每次请求都是“短期记忆”:模型只看到这一条 prompt,看不到之前聊过什么。
- 无法真正多轮对话:用户刚刚介绍过自己的背景、上下文,下一轮又要重复一遍。
- 做不到个性化:不知道用户是谁、之前问过什么,回答永远是“第一次见面”的状态。
要让 AI 真正“像个助手”而不是“一个高配搜索框”,就离不开:记忆 。
1. 记忆缓存:让对话串起来
所谓“记忆”,本质上就是给每个会话维护一段上下文,把历史对话按一定策略保存下来。它能带来的能力非常直接:
- 多轮对话:上下文能串起来,AI 可以基于上一轮甚至多轮的内容继续回答。
- 用户画像 / 会话状态:记住用户的偏好、最近聊过的主题,在回答时自动“对口味”。
- 分布式部署可用:配合 Redis 做分布式会话记忆,多实例部署时也能共享同一段对话历史。
在 Java 项目里,我们完全可以利用 LangChain4j 的 ChatMemory 体系,加上 Redis 等基础设施,把“记忆”做成一个 可配置、可扩展的中间层能力,而不是写死在某个 Controller 里。
本文会基于 LangChain4j + Spring Boot,从零实现非常贴近实战的能力: 一个带“记忆”的对话 AI
- 支持内存会话记忆(开发调试方便)
- 支持基于 Redis 及 Mysql 的分布式会话记忆(适合生产部署)
一、效果预览
1. 多轮对话效果:AI 真的能“记住你”
先来看最直观的——带记忆的多轮对话。
第一步:用户先介绍自己信息
用户第一次进来时,可以先做一个自我介绍,比如:
我是在农商银行做数据治理的 Java 工程师,平时主要写 Spring Boot 和大数据相关的东西。
这时候,后台会把这段话作为“当前会话”的一部分,写入 LangChain4j 的 ChatMemory。如果配置了 Redis,则会同步存到 Redis 里,对应当前的 sessionId。
第二步:不重复输入,AI 还能记住并复述
接下来,用户只问一句很短的问题:
那你觉得我适合用 LangChain4j 做哪些东西?
在这一步里,前端没有再重复任何“我是谁、在哪工作、做什么”的信息,但是 AI 在回答时会自动带上之前的上下文,例如:
结合你现在在农商行做数据治理、用 Spring Boot 的场景,其实很适合用 LangChain4j 做两块:
1)面向数据工程师的 SQL 助手;
2)基于你们数据标准的字段智能对标……
这说明模型已经利用了记忆里的用户画像,而不是把你当“陌生人”。
第三步:重启浏览器 / 换窗口,对话还能从 Redis 记忆接着聊(可选)
如果你在项目中给每个用户绑定了一个稳定的 sessionId(比如登录用户 ID + 浏览器端生成的 UUID),并且记忆使用的是 Redis 持久化存储,那么:
- 即使你重启浏览器、换个页面,只要
sessionId还在,对话就能无缝接上; - 后端可以在任意一台实例上拿到这段会话的历史记录,做到真正的分布式会话记忆。
二、开发环境与项目初始化
1. 技术栈与版本选择
- JDK:17+
- Spring Boot:3.x
- 构建工具:Maven
- LangChain4j
- Redis
2. 模型选型:本地模型优先
本文选用 本地模型 作为主角,通过 OpenAI 协议接入 LangChain4J:本地部署继续选用:
Xinference。其使用见本系列第一篇文章:
https://blog.youkuaiyun.com/OFFTime_we/article/details/147198108?spm=1001.2014.3001.5501
三、给对话 AI 加上「记忆缓存」
这一节我们从 0 到 1,把一个“只会单轮回答”的 Chat 模型,升级成带记忆的多轮对话 AI,并且支持 InMemory + Redis 两种记忆实现。
3.1 基础 ChatLanguageModel 配置
先把最核心的 Chat 模型跑起来,这一步先不考虑记忆,就是一个「无记忆版本」的 AI 助手。
3.1.1 创建 ChatLanguageModelConfig
示例基于 Spring Boot + LangChain4j,使用 OpenAI 协议模型;
package com.example.ai.config;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
@Configuration
public class ChatLanguageModelConfig {
@Bean
public ChatLanguageModel chatLanguageModel() {
return OpenAiChatModel.builder()
.apiKey(System.getenv("LANGCHAIN4J_API_KEY"))
.baseUrl("http://本地ip:9997/v1") // 比如 DeepSeek / 自建 OpenAI 网关
.modelName("deepseek-chat") // 替换成你实际使用的模型名
.temperature(0.3) // 稳定一点的回复
.timeout(Duration.ofSeconds(15)) // 超时时间
.logRequests(true) // 开启日志,便于排查问题
.logResponses(true)
.build();
}
}
baseUrl / modelName 按你实际接入的Xinference模型调整即可。
3.1.2 封装 AiService 接口(无记忆版本)
LangChain4j 推荐通过 AiServices 把模型封装成一个接口,像调普通 Service 一样去用。
package com.example.ai.service;
import dev.langchain4j.service.UserMessage;
public interface ChatAssistant {
String chat(@UserMessage String message); // 先做一个无记忆的简单版本
}
对应的配置:
package com.example.ai.config;
import com.example.ai.service.ChatAssistant;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.service.AiServices;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatAssistantConfig {
@Bean
public ChatAssistant chatAssistant(ChatLanguageModel chatLanguageModel) {
return AiServices.create(ChatAssistant.class, chatLanguageModel);
}
}
到这一步,就已经有了一个最基础的 Java AI 助手,可以在任意 Service / Controller 中注入 ChatAssistant 来完成简单问答。接下来开始“上记忆”。
3.2 接入内存记忆:MessageWindowChatMemory
先用 InMemory 的方式,在单机环境跑通“多轮对话 + 记忆”,之后再切到 Redis。
3.2.1 AiService + ChatLanguageModelConfig
LangChain4j 内置了一个“窗口记忆”的实现:MessageWindowChatMemory,只保存最近 N 条消息,防止上下文过长导致 token 暴涨。
在实际业务里,我们更常见的是每个用户 / 每个会话有自己的记忆。这时可以用 @MemoryId + chatMemoryProvider.
package com.example.ai.service;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
public interface ChatAssistant {
String chat(@MemoryId String sessionId,
@UserMessage String message);
}
对应配置
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.example.service.AiAssistant;
import org.example.service.ChatAssistant;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ChatLanguageModelConfig {
@Bean
public ChatLanguageModel chatLanguageModel() {
return OpenAiChatModel.builder()
.apiKey(System.getenv("LANGCHAIN4J_API_KEY"))
.baseUrl("http://本地ip:9997/v1")
.modelName("deepseek-chat") // 替换成你实际使用的模型名
.temperature(0.3) // 稳定一点的回复
.timeout(Duration.ofSeconds(15)) // 超时时间
.logRequests(true) // 开启日志,便于排查问题
.logResponses(true)
.build();
}
@Bean
public AiAssistant aiAssistant(ChatLanguageModel chatLanguageModel) {
return AiServices.builder(AiAssistant.class)
.chatLanguageModel(chatLanguageModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(100))
.build();
}
}
这里的 memoryId 一般就是 sessionId,比如用户登录 ID + 浏览器端 UUID 等。
3.2.2 controller 类
import cn.hutool.core.date.DateUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.example.service.AiAssistant;
import org.example.service.ChatAssistant;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class ChatLanguageModelController {
@Resource
private ChatAssistant chatAssistant;
@Resource
private AiAssistant aiAssistant;
@GetMapping(value = "/chatmemory/test1")
public String testWithOutChatMemory() {
String chat = chatAssistant.chat("你好,我的名字叫张三");
System.out.println("chat返回结果:"+chat);
String chat2 = chatAssistant.chat("我的名字是什么");
System.out.println("chat2返回结果:"+chat2);
return "success : "+ DateUtil.now();
}
@GetMapping(value = "/chatmemory/test2")
public String testWithChatMemory() {
aiAssistant.chat(1L, "你好!我的名字是Java.");
aiAssistant.chat(3L, "你好!我的名字是C++");
String chat = aiAssistant.chat(1L, "我的名字是什么");
System.out.println(chat);
chat = aiAssistant.chat(3L, "我的名字是什么");
System.out.println(chat);
return "testWithChatMemory success : "+ DateUtil.now();
}
}
3.3 接入 Redis 记忆:RedisChatMemoryStore
InMemory 记忆只适合单机 / 本地开发,一旦你有多实例部署 / 容器重启 / 横向扩容,记忆就会丢失。这时就需要 Redis 上场。
3.3.1 为什么要引入 Redis?
- 多实例部署 / 会话共享:
用户从网关随机打到不同应用实例,只有 Redis 能保证所有实例看到的对话历史是一致的。 - 防止 JVM 重启导致记忆丢失:
应用重启之后,从 Redis 里把历史对话重新拿出来,对用户而言是“无感”的。
3.3.2 RedisChatMemoryStore
LangChain4j 提供了基于 Redis 的 ChatMemoryStore 实现
package org.example.config;
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 jakarta.annotation.Resource;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RedisChatMemoryStore implements ChatMemoryStore {
public static final String CHAT_MEMORY_PREFIX = "CHAT_MEMORY:";
@Resource
private RedisTemplate<String,String> redisTemplate;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
String retValue = redisTemplate.opsForValue().get(CHAT_MEMORY_PREFIX + memoryId);
return ChatMessageDeserializer.messagesFromJson(retValue);
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
redisTemplate.opsForValue().set(CHAT_MEMORY_PREFIX + memoryId, ChatMessageSerializer.messagesToJson(messages));
}
@Override
public void deleteMessages(Object memoryId) {
redisTemplate.delete(CHAT_MEMORY_PREFIX + memoryId);
}
}
3.3.3 redis配置
package org.example.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactor) {
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactor);
//设置key序列化方式string
redisTemplate.setKeySerializer(new StringRedisSerializer());
//设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
3.3.4 LLMConfig
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.example.service.AiAssistant;
import org.example.service.ChatAssistant;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ChatLanguageModelConfig {
@Resource
private RedisChatMemoryStore redisChatMemoryStore;
@Bean
public ChatLanguageModel chatLanguageModel() {
return OpenAiChatModel.builder()
.apiKey(System.getenv("LANGCHAIN4J_API_KEY"))
.baseUrl("http://本地ip:9997/v1") // 比如 DeepSeek / 自建 OpenAI 网关
.modelName("deepseek-chat") // 替换成你实际使用的模型名
.temperature(0.3) // 稳定一点的回复
.timeout(Duration.ofSeconds(15)) // 超时时间
.logRequests(true) // 开启日志,便于排查问题
.logResponses(true)
.build();
}
@Bean
public ChatMemoryAssistantRedis chatAssistantWithRedis(ChatLanguageModel chatLanguageModel) {
return AiServices.builder(ChatMemoryAssistantRedis.class)
.chatLanguageModel(chatLanguageModel)
// 根据 memoryId(sessionId) + Redis 做持久化记忆
.chatMemoryProvider(memoryId ->
MessageWindowChatMemory.withMaxMessages(20, memoryId, redisChatMemoryStore))
.build();
}
}
重点是:MessageWindowChatMemory.withMaxMessages(20, memoryId, chatMemoryStore),把 Redis 作为底层持久化实现,这样每个 sessionId 的消息都能存入 Redis。
3.3.5 ChatMemoryAssistantRedis
package com.example.ai.service;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.UserMessage;
public interface ChatMemoryAssistantRedis{
String chat(@MemoryId String sessionId,
@UserMessage String message);
}
3.3.6 controller 类
import cn.hutool.core.date.DateUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.example.service.ChatMemoryAssistant;
import org.example.service.ChatMemoryAssistantMySQL;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Date 2025-05-04 15:55
* @Description: TODO
*/
@RestController
@Slf4j
public class ChatMemoryController {
@Resource
private ChatMemoryAssistant chatMemoryAssistant;
@Resource
private ChatMemoryAssistantMySQL chatMemoryAssistantMySQL;
@GetMapping(value = "/chatmemory/redis")
public String testWithChatMemoryStore() {
chatMemoryAssistant.chat(1L, "你好!我的名字是redis");
chatMemoryAssistant.chat(3L, "你好!我的名字是nacos");
String chat = chatMemoryAssistant.chat(1L, "我的名字是什么");
System.out.println(chat);
chat = chatMemoryAssistant.chat(3L, "我的名字是什么");
System.out.println(chat);
return "testWithChatMemoryStore success : "+ DateUtil.now();
}
}
3.4 接入Mysql记忆:MysqlChatMemoryStore
此处只是展示如何使用关系型数据库存储记忆。此处展示多缓存并存的进阶使用。
3.4.1 MysqlChatMemoryStore
package org.example.config;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import jakarta.annotation.Resource;
import org.example.entities.ChatMessageDO;
import org.example.mapper.ChatMessageDOMapper;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Date 2025-05-05 17:46
* @Description: TODO
*/
@Component
public class MysqlChatMemoryStore implements ChatMemoryStore {
@Resource
private ChatMessageDOMapper chatMessageDOMapper;
@Override
public List<ChatMessage> getMessages(Object memoryId) {
LambdaQueryWrapper<ChatMessageDO> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ChatMessageDO::getConversationId,memoryId);
return chatMessageDOMapper.selectList(lambdaQueryWrapper).stream().map(e ->
new UserMessage(e.getContext())).collect(Collectors.toList());
}
@Override
public void updateMessages(Object memoryId, List<ChatMessage> messages) {
this.deleteMessages(memoryId);
List<ChatMessageDO> messageDOList = new ArrayList<>();
messages.forEach(chatMessage -> {
ChatMessageDO chatMessageDO = new ChatMessageDO();
chatMessageDO.setConversationId(memoryId.toString());
chatMessageDO.setRole(chatMessage.type().name());
chatMessageDO.setContext(chatMessage.toString());
messageDOList.add(chatMessageDO);
});
chatMessageDOMapper.insert(messageDOList);
}
@Override
public void deleteMessages(Object memoryId) {
LambdaQueryWrapper<ChatMessageDO> lambdaQueryWrapper = new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(ChatMessageDO::getConversationId,memoryId);
chatMessageDOMapper.delete(lambdaQueryWrapper);
}
}
3.4.2 Mapper
public interface ChatMessageDOMapper extends BaseMapper<ChatMessageDO> {
}
3.4.2 ChatMessageDO
package org.example.entities;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
@TableName(value ="chat_message")
@Data
public class ChatMessageDO implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
*
*/
private String conversationId;
/**
*
*/
private String role;
/**
*
*/
private String context;
/**
*
*/
private Date createdAt;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
}
3.4.2 LLMConfig
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.openai.OpenAiChatModel;
import dev.langchain4j.service.AiServices;
import org.example.service.AiAssistant;
import org.example.service.ChatAssistant;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
public class ChatLanguageModelConfig {
@Resource
private MysqlChatMemoryStore mysqlChatMemoryStore;
@Bean
public ChatLanguageModel chatLanguageModel() {
return OpenAiChatModel.builder()
.apiKey(System.getenv("LANGCHAIN4J_API_KEY"))
.baseUrl("http://本地ip:9997/v1") // 比如 DeepSeek / 自建 OpenAI 网关
.modelName("deepseek-chat") // 替换成你实际使用的模型名
.temperature(0.3) // 稳定一点的回复
.timeout(Duration.ofSeconds(15)) // 超时时间
.logRequests(true) // 开启日志,便于排查问题
.logResponses(true)
.build();
}
@Bean
public ChatMemoryAssistantMySQL chatAssistantWithRedis(ChatLanguageModel chatLanguageModel) {
return AiServices.builder(ChatMemoryAssistantMySQL.class)
.chatLanguageModel(chatLanguageModel)
// 根据 memoryId(sessionId) + mysql做持久化记忆
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.withMaxMessages(20, memoryId, mysqlChatMemoryStore))
.build();
}
}
3.4.3 ChatMemoryAssistantMySQL
public interface ChatMemoryAssistantMySQL {
/**
* 聊天
*
* @param userId 用户 ID
* @param message 消息
* @return {@link String }
*/
String chat(@MemoryId Long userId, @UserMessage String message);
}
3.4.4 controller类
import cn.hutool.core.date.DateUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.example.service.ChatMemoryAssistant;
import org.example.service.ChatMemoryAssistantMySQL;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Date 2025-05-04 15:55
* @Description: TODO
*/
@RestController
@Slf4j
public class ChatMemoryController {
@Resource
private ChatMemoryAssistant chatMemoryAssistant;
@Resource
private ChatMemoryAssistantMySQL chatMemoryAssistantMySQL;
@GetMapping(value = "/chatmemory/mysql")
public String testWithChatMemoryStoreMysql() {
chatMemoryAssistantMySQL.chat(1L, "你好!我的名字是redis");
chatMemoryAssistantMySQL.chat(3L, "你好!我的名字是nacos");
String chat = chatMemoryAssistantMySQL.chat(1L, "我的名字是什么");
System.out.println(chat);
chat = chatMemoryAssistantMySQL.chat(3L, "我的名字是什么");
System.out.println(chat);
return "testWithChatMemoryStoreMysql success : "+ DateUtil.now();
}
}
总结
本篇核心是一件事:让 Java 大模型「有记忆」。我们从最简单的 ChatLanguageModel 出发,接入 MessageWindowChatMemory,实现基于 sessionId 的多轮对话,再扩展到 Redis / MySQL,把 LangChain4j 的记忆能力做成可插拔组件。下一篇将展开「LangChain4j 文本向量化分类与字段智能对标」以及 Agent 工具调用,把记忆 真正落到业务场景中,让这套能力不仅能聊,还能帮你做事。
3607



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



