【langchain4j系列教程-05】一文读懂:人工智能如何实现会话记忆
文章目录
- 引言
- 依赖引入
- 没有会话记忆示例
- 手动添加会话记忆示例
- langchain4j实现自动会话记忆
- 全局会话记忆
- 会话记忆隔离
- 总结
引言
会话记忆是每个大模型都具备的能力,那么他是怎么实现的呢?你是否思考过大模型是如何实现知晓之前的聊天内容的。这篇文章将会由浅入深带你窥探大模型会话记忆的秘密!
依赖引入
本文涉及到的所有依赖统一放在这里
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.2.3</version>
</dependency>
<!-- 引入langchain4j依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- 引入OpenAI依赖。由于deepseek跟open ai共用一套标准所以deepseek也用这个依赖-->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-open-ai</artifactId>
<version>${langchain4j.version}</version>
</dependency>
<!-- SpringBoot整合百炼/千问依赖 -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-dashscope-spring-boot-starter</artifactId>
</dependency>
<!-- spring webflux -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</dependencies>
没有会话记忆示例
首先,请大家看下方代码,代码中与大模型进行了两次聊天,第一次告诉大模型我是谁,第二次问大模型我是谁。
public class DemoErrorTest {
public static void main(String[] args) {
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.apiKey("demo")
.modelName("gpt-4o-mini")
.build();
String result = chatModel.chat("我是Jayden。");
System.out.println(result);
String result2 = chatModel.chat("我是谁?");
System.out.println(result2);
}
}
输出如下:观察输出可以清晰看到,此时的大模型是没有记忆的
手动添加会话记忆示例
接下来将演示手动添加会话记忆的能力,这个demo其实在官网里也有提到,目的是让大家更了解原理,首先看代码,原理就是在下一次聊天的时候放入上一次聊天的对话与答案
public class DemoSuccessTest {
public static void main(String[] args) {
OpenAiChatModel chatModel = OpenAiChatModel.builder()
.apiKey("demo")
.modelName("gpt-4o-mini")
.build();
UserMessage userMessage1 = UserMessage.userMessage("我是Jayden。");
ChatResponse result1 = chatModel.chat(userMessage1);
AiMessage response1 = result1.aiMessage();
System.out.println(response1);
UserMessage userMessage2 = UserMessage.userMessage("我是谁?");
ChatResponse result2 = chatModel.chat(userMessage1,response1,userMessage2);
AiMessage response2 = result2.aiMessage();
System.out.println(response2);
}
}
我们来看执行的结果,这次大模型通过上下文知道了我是谁。那么问题来了,如果一个会话有很多对话,那这个代码是不是有问题?解决问题的方式往下看。
langchain4j实现自动会话记忆
抛开langchain4j不谈,在web2的世界里,数据都是要存储的,无非就是怎么存的问题。在会话这种场景下很容易就想到K-V结构的存储,K是会话的id,V是对应的聊天列表。
接下来来看下langchain4j里实现上述原理是怎么做的
全局会话记忆
首先要定一个对话助手Assistant
,然后在SpringBoot中实例化这个bean。
@Configuration
public class ChatMemoryAiConfig {
/**
* 对话助手
*/
public interface Assistant {
String chat(String prompt);
TokenStream streamChat(String prompt);
}
@Bean
public Assistant assistant(ChatLanguageModel chatLanguageModel, StreamingChatLanguageModel streamingChatLanguageModel) {
MessageWindowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(33);
// 原理是对话助手增加动态代理
/**
* 第一次:chat
* ==》 对话内容存储到内存(ChatMemory)
* ==》 取出来历史对话内容
* ==》 放到当前对话内容中
*/
return AiServices.builder(Assistant.class)
.chatLanguageModel(chatLanguageModel)
.streamingChatLanguageModel(streamingChatLanguageModel)
.chatMemory(chatMemory)
.build();
}
}
这里的关键代码是MessageWindowChatMemory chatMemory = MessageWindowChatMemory.withMaxMessages(33);
这就是langchain4j提供的会话记忆的类,在通过AiServices
构造Assistant
时,将指定存储上下文的MessageWindowChatMemory
设置到对话助手上即可实现会话记忆。参数里的33代表最多存储多少条会话内容。
我们来看下MessageWindowChatMemory
源码,看看会话内容到底怎么存的?
public class MessageWindowChatMemory implements ChatMemory {
private final Object id;
private final Integer maxMessages;
private final ChatMemoryStore store;
// 省略。。。
}
可以看到会话记忆应该是存在ChatMemoryStore
里的,我们打开源码查看,可以看到langchain4j是存在了内存里的,用了一个线程安全的Map,这与我们的猜想是一致的。
public class InMemoryChatMemoryStore implements ChatMemoryStore {
private final Map<Object, List<ChatMessage>> messagesByMemoryId
= new ConcurrentHashMap<>();
// 省略。。。
}
定义好会话助手Bean后就可以通过接口来测试了,测试接口代码
@RestController
@RequestMapping("/chat/memory/ai")
public class ChatMemoryAIController {
@Autowired
private ChatMemoryAiConfig.Assistant assistant;
@RequestMapping("/chat")
public String chat(@RequestBody String prompt) {
return assistant.chat(prompt);
}
}
大家可以通过这个接口进行测试,是带会话记忆的能力的。这里需要补充一点,Assistant
是一个接口,那么为什么可以直接调用呢?这里实际是使用了动态代理。
会话记忆隔离
上一部分的会话意义我定位为全局的会话记忆,原因是所有人共用会话记忆。实际使用过DeepSeek等其他ai产品的都知道,会话记忆一般都是通过会话id来进行隔离的,所以才会有会话列表。为了解决这个问题,所以有会话隔离。
首先我们定义一个会话助手,这里需要注意的是@MemberId
注解,这个注解名很容易理解,就是记忆id,他可以是任何具有唯一属性的字段,具体看业务。比如它可以是userId,也可以是会话id等等。
首先要定一个对话助手MemoryAssistant
,然后在SpringBoot中实例化这个bean。
import com.jayden.ai.memory.store.ChatMemoryStorePersistent;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import dev.langchain4j.model.chat.ChatLanguageModel;
import dev.langchain4j.model.chat.StreamingChatLanguageModel;
import dev.langchain4j.service.AiServices;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.TokenStream;
import dev.langchain4j.service.UserMessage;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ChatMemoryAiConfig {
public interface MemoryAssistant{
String chat(@MemoryId Integer memoryId, @UserMessage String prompt);
TokenStream streamChat(@MemoryId Integer memoryId, @UserMessage String prompt);
}
/**
* 带有记忆的对话助手 多轮对话的内容是存在内存中的
* @param chatLanguageModel
* @param streamingChatLanguageModel
* @return
*/
@Bean
public MemoryAssistant memoryAssistant(ChatLanguageModel chatLanguageModel, StreamingChatLanguageModel streamingChatLanguageModel) {
return AiServices.builder(MemoryAssistant.class)
.chatLanguageModel(chatLanguageModel)
.streamingChatLanguageModel(streamingChatLanguageModel)
.chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder().maxMessages(33).id(memoryId).build())
.build();
}
}
测试接口
import com.jayden.ai.memory.config.ChatMemoryAiConfig;
import dev.langchain4j.service.TokenStream;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;
@RestController
@RequestMapping("/chat/memory/ai")
public class ChatMemoryAIController {
@Autowired
private ChatMemoryAiConfig.MemoryAssistant memoryAssistant;
@GetMapping("/memory/chat")
public String memoryChat(@RequestParam("prompt") String prompt, @RequestParam("chatId") Integer chatId){
return memoryAssistant.chat(chatId, prompt);
}
}
可以通过接口进行测试,可以发现会话之间实现了隔离。
总结
本文从基础案例一步步到会话隔离,深入浅出的介绍了会话记忆,但这里的会话记忆是存在内存的,内存的特点是断电即失,所以数据需要做持久化,如果觉得文章有帮助,可以三连走起来,专栏里有整套langchain4j的教程,欢迎关注!