LLM 模型本身是一个无状态的模型,没有临时记忆的能力。当发生如下场景时,就会产生错误回答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @RestController @RequestMapping("/ai") public class AIController {
private final ChatClient chatClient;
public AIController(ChatClient.Builder builder) { this.chatClient = builder.build(); }
@RequestMapping("/chat/{msg}") public String chat(@PathVariable String msg) {
return this.chatClient.prompt(new Prompt(msg)).call().chatResponse().getResult().getOutput().getContent(); }
}
|
启动项目,访问接口时会出现:
1 2 3 4 5 6 7
| # 1 input:你好,我的名字叫牧生 output:你好,牧生!很高兴认识你。我叫通义千问,是阿里云开发的超大规模语言模型。我可以帮助你解答问题、提供信息或进行各种话题的讨论。有什么我可以帮到你的吗?
# 2 input:我叫什么名字 output:您没有告诉我您的名字,所以我无法直接回答。如果您愿意分享,可以告诉我您的名字是什么。
|
第二次调用 llm 发送 prompt,大模型无法记住第一轮的上下文,所以无法给出正确的答案。
要实现一个可以让大模型具有聊天记忆能力,根据之前的聊天信息进行回答,应该如何如何实现呢?
手动实现一个 Chat Memory
在实现之前,先了解下 Spring AI 的 Message 类型,确保塞到正确的 Message 类型中,避免出现错误。
其中:
- UserMessage:用户消息,指用户输入的 prompt;
- SystemMessage:系统限制性消息,通常用于指定 LLM 角色,一般只会设置一次;
- AssistantMessage:LLM 输出;
- FunctionMessage:函数调用时的消息。
为此,可以写出如下的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| @RestController @RequestMapping("/ai") public class AIController {
List<Message> historyMessage = new ArrayList<>();
private final ChatClient chatClient;
public AIController(ChatClient.Builder builder) { this.chatClient = builder.build(); }
@RequestMapping("/chat/{msg}") public String chat(@PathVariable String msg) {
historyMessage.add(new UserMessage(msg));
AssistantMessage output = this.chatClient.prompt(new Prompt(historyMessage)).call().chatResponse().getResult().getOutput();
historyMessage.add(output);
return output.getContent(); }
}
|
1 2 3 4 5 6 7
| # 1 input:你好,我的名字叫牧生 output:你好,牧生!很高兴认识你。我叫通义千问,是阿里云研发的超大规模语言模型。我可以帮助你解答问题、提供信息或者进行聊天。有什么我可以帮到你的吗?
# 2 input:我叫什么名字 output:你刚才提到你的名字叫牧生。如果你有其他问题或需要进一步的帮助,请告诉我!
|
至此,已经实现了一个简单的 chat memory 功能。然后,我们来分析一波这种写法存在的问题:
historyMessage 的大小是无限的吗?
在 java 代码中无限,但是 llm 的输入 Token 有限,例如 GPT,通义等模型的 Token 限制:
文本内容:在传递给 llm 的内容中,可能存在无关的文本,影响 llm 的输出,使其产生幻觉或者占用大量 token。
- 没有和对话关联,直接从 list 中获取,正常应该是每次会话一个历史消息。
- 数据不持久,在服务器关机重启之后数据丢失。
- 没有相关策略合理组织 Message。
之后,来看下 Spring AI 的 chat Memory 实现
Spring AI Chat Memory
ChatMemory 接口
Spring AI 中提供的对 Message 存取的接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public interface ChatMemory { default void add(String conversationId, Message message) { this.add(conversationId, List.of(message)); }
void add(String conversationId, List<Message> messages);
List<Message> get(String conversationId, int lastN);
void clear(String conversationId); }
|
InMemoryChatMemory
ChatMemory 的实现,表示为聊天对话历史记录提供内存存储;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class InMemoryChatMemory implements ChatMemory { Map<String, List<Message>> conversationHistory = new ConcurrentHashMap();
public InMemoryChatMemory() { }
public void add(String conversationId, List<Message> messages) { this.conversationHistory.putIfAbsent(conversationId, new ArrayList()); ((List)this.conversationHistory.get(conversationId)).addAll(messages); }
public List<Message> get(String conversationId, int lastN) { List<Message> all = (List)this.conversationHistory.get(conversationId); return all != null ? all.stream().skip((long)Math.max(0, all.size() - lastN)).toList() : List.of(); }
public void clear(String conversationId) { this.conversationHistory.remove(conversationId); } }
|
使用方式
Spring AI 框架提供了三种Advisor来使用ChatMeomry。
- MessageChatMemoryAdvisor:查询对象会话ID的历史消息添加到提示词文本中,核心代码如下;
- PromptChatMemoryAdvisor:检索到的内存中的历史消息将添加到提示的系统文本中;
- VectorStoreChatMemoryAdvisor:检索向量数据库中的历史消息将添加到提示的系统文本中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| @RestController @RequestMapping("/ai") public class AIController {
private final ChatClient chatClient;
private final ChatModel chatModel;
public AIController(ChatClient.Builder builder, ChatModel chatModel) {
this.chatModel = chatModel; this.chatClient = ChatClient.builder(chatModel) .defaultAdvisors( new MessageChatMemoryAdvisor(new InMemoryChatMemory()) ).build(); }
@GetMapping("/chatWithChatMemory") public String chatWithChatMemory(String chatId, String prompt) {
return chatClient.prompt() .user(prompt) .advisors( a -> a .param(CHAT_MEMORY_CONVERSATION_ID_KEY, chatId) .param(CHAT_MEMORY_RETRIEVE_SIZE_KEY, 100) ).call().chatResponse().getResult().getOutput().getContent(); }
}
|
访问:
1 2 3 4 5 6 7 8 9 10 11
| # 1 input: http://localhost:8080/ai/chatWithChatMemory?chatId=10001&prompt=你好,我是牧生 output:你好,牧生!很高兴认识你。有什么我可以帮到你的吗?
# 2 input:http://localhost:8080/ai/chatWithChatMemory?chatId=10001&prompt=我是谁 output:您是这次对话的发起者,您之前提到您的名字叫牧生。如果您有其他身份或角色想要分享,或者有任何问题和需要帮助的地方,都欢迎告诉我!
# 当切换 chatId 时 input:http://localhost:8080/ai/chatWithChatMemory?chatId=10002&prompt=我叫什么名字 output:您好!您刚才没有提到您的名字,所以我无法直接回答您的问题。如果您愿意分享,可以告诉我您的名字,或者如果您是在某种特定情境下问这个问题,也可以提供更多背景信息,这样我可能能更好地帮助您。如果这是一个私人问题,您也可以选择不回答。我在这里是为了支持您,有任何问题都可以问我。
|
其中参数chatId
表示会话 ID,实现上下文与会话绑定。CHAT_MEMORY_RETRIEVE_SIZE_KEY
表示历史会话最多100条发给AI。
问题
通过两种方式都可以实现 chat memory,相比 spring ai 更加完善。
但是也存在以下问题:
数据不持久,在服务器关机重启之后数据丢失;
没有相关策略合理组织 Message。
Spring AI Alibaba
Tips: Spring AI Alibaba 的 Chat Memory 功能正在开发中……