我们都知道,大模型本身是无状态、无记忆的。默认情况下,我们向大模型发起的每次提问,在其内部都会被视为一次全新的调用。尽管诸如 ChatGPT 等聊天应用内置了部分记忆功能,可以记录用户最近几轮的聊天信息,但它仍然存在上下文长度限制,对话历史超过一定长度后,就会强制开启新一轮对话。
为了解决这个问题,很多 AIGC 应用都需要独立开发记忆系统,特别是像 AI 聊天陪伴、RAG、智能客服等应用,记忆系统的质量决定了产品是否有能力维持长期的用户对话,这会直接影响到用户体验和产品口碑。
针对 LLM 应用的记忆系统,业界已经探索出了一些成熟的解决方案,常见的实现方式包括以下几种:
这是最基础的记忆模式,将所有人类提问和 AI 生成的消息全部缓存起来,每次需要使用时将保存的所有聊天消息列表全部传递到 Prompt 中,通过往用户的输入中添加历史对话信息/记忆,可以让 LLM 能理解之前的对话内容,而且这种记忆方式在上下文窗口限制内是无损的。
在缓冲记忆的基础上,增加上下文窗口限制,即只保留固定轮次的历史对话,“遗忘”掉过于久远的记忆。
同样是基于缓冲记忆的思想,只保留 max_tokens 长度的历史上下文,超过长度限制的历史记忆会被遗忘。
将每轮对话的输入输出,生成总结摘要,作为记忆保存起来,并在下一轮对话时传递给 LLM。
摘要+缓冲混合记忆,结合了缓冲窗口记忆和摘要总结记忆两种模式,是目前业内采用较多的一种方案:
将全量记忆数据存储在向量存储中,每次搜索记忆时,基于向量检索,获取前 K 个最匹配的语料,整体的思路类似于 RAG 系统。
讲了这么多理论,下面我们动手自己实现一个简单的记忆系统。我们选择 LangChain 作为开发框架,因为 LangChain 对于 Memory 模块已经有了比较完善的封装,内置了很多开箱即用的 ChatMemory 组件,大大提升了开发效率。上面我们提到的几种记忆方案,在 LangChain 中基本都可以找到对应的实现。
下面是具体的实现代码:
# -*- coding: utf-8 -*-
"""
@Time : 2024/7/11 11:53
@Author : ZhangShenao
@File : chat_memory_chain.py
@Desc : 聊天记忆链
把聊天历史的记忆功能单独封装成一个Chain
"""
from operator import itemgetter
from typing import Dict
from langchain.memory import ConversationBufferWindowMemory
from langchain.memory.chat_memory import BaseChatMemory
from langchain_community.chat_message_histories import FileChatMessageHistory
from langchain_core.language_models import BaseChatModel
from langchain_core.output_parsers import BaseTransformOutputParser
from langchain_core.prompts import BaseChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough, RunnableLambda, RunnableConfig
from langchain_core.runnables.utils import Output
from langchain_core.tracers import Run
from typing_extensions import Any
from internal.service import VectorStoreService
CHAT_HISTORY_FILE_PATH = '../../storage/memory/chat_history.json' # 聊天历史文件路径
HISTORY_KEY = 'history' # 聊天历史Key
CONTEXT_KEY = 'context' # 上下文信息Key
INPUT_KEY = 'input' # 聊天输入Key
OUTPUT_KEY = 'output' # 聊天输出Key
MEMORY_CONFIG_KEY = 'memory' # 记忆配置Key
SAVE_CONVERSATION_ROUNDS = 5 # 保存历史对话的轮数
def invoke_chain_with_chat_memory(input: Dict[str, Any],
prompt_template: BaseChatPromptTemplate,
llm: BaseChatModel,
parser: BaseTransformOutputParser[str],
vector_store_service: VectorStoreService) -> Output:
"""
将传入的Runnable组件编排成Chain,并在此基础上封装聊天记忆功能,返回最终调用结果
:param input: 调用输入参数
:param prompt_template: 提示词模板
:param llm: LLM聊天模型
:param parser: 输出解析器
:return: Chain调用结果
"""
# 使用本地文件保存聊天历史
chat_history = FileChatMessageHistory(file_path=CHAT_HISTORY_FILE_PATH)
# 创建聊天记忆组件,使用缓冲窗口记忆方式
memory = ConversationBufferWindowMemory(
input_key=INPUT_KEY,
output_key=OUTPUT_KEY,
memory_key=HISTORY_KEY,
k=SAVE_CONVERSATION_ROUNDS, # 保留最近5轮的聊天历史,即10条消息
return_messages=True, # 结果返回聊天消息列表,而不是字符串
chat_memory=chat_history # 设置MessageHistory组件,用于持久化历史聊天记录
)
# 构建Retriever Chain,执行向量相似度检索
retriever = vector_store_service.as_retriever() | vector_store_service.join_document_page_contents
# 构造Chain执行链,用于编排组件的执行流程
chain = RunnablePassthrough.assign(
context=itemgetter(INPUT_KEY) | retriever,
history=RunnableLambda(_load_memory_variables_from_config) | itemgetter(HISTORY_KEY)
) | prompt_template | llm | parser
# 封装chain,在运行配置中传入记忆信息,并且注册on_end监听回调,在回调函数中保存聊天历史
memory_chain = (chain.with_config(configurable={MEMORY_CONFIG_KEY: memory})
.with_listeners(on_end=_save_chat_history))
# 调用memory_chain,返回结果
output = memory_chain.invoke(input)
return output
def _load_memory_variables_from_config(input: Dict[str, Any], config: RunnableConfig) -> Dict[str, Any]:
"""
从运行配置中,加载记忆变量
:param input: 运行调用输入
:param config: 运行配置
:return: 记忆变量字典
"""
# 获取运行时配置,从配置读取记忆信息
conf = config.get('configurable', {})
memory = conf.get(MEMORY_CONFIG_KEY, None)
if memory is not None and isinstance(memory, BaseChatMemory):
return memory.load_memory_variables(input)
# 空记忆信息
return {}
def _save_chat_history(run_obj: Run, config: RunnableConfig) -> None:
"""
保存聊天历史
:param run_obj: 运行时对象,包含了所有运行时的相关信息
:param config: 运行时配置信息
"""
# 获取运行时配置,从配置读取记忆信息
conf = config.get('configurable', {})
memory = conf.get(MEMORY_CONFIG_KEY, None)
if memory is not None and isinstance(memory, BaseChatMemory):
# 将当前聊天的输入输出保存到Memory中
memory.save_context(run_obj.inputs, run_obj.outputs)
详细介绍一下实现思路:
FileChatMessageHistory
组件,将聊天历史保存在本地文件系统中。MessageHistory
也是 LangChain 封装的消息历史组件,下面有多种具体的实现,如 RedisChatMessageHistory
可以将聊天消息保存到 Redis 中、MongoDBChatMessageHistory
则是保存到 MongoDB 中等等。这里我们为了方便演示,则是直接采用本地文件的方式。ConversationBufferWindowMemory
组件,并设置 k = 5,它对应了上面的 Buffer Window Memory——缓冲窗口记忆模式,保留最近的5轮对话。 如果想选择其他记忆模式,仅需将组件替换成 ConversationSummaryMemory
、ConversationTokenBufferMemory
等等即可。with_listeners
监听器机制,设置了 on_end
回调,即在每轮对话完成后,执行 _save_chat_history
函数,将本轮对话保存到 Memory 组件中。通过以上的方式,即可实现一个具有缓冲窗口记忆功能的聊天应用,是不是非常简单?项目的完整代码可以参考:https://gitee.com/zhangshenao/llm-ops-backend/blob/master/internal/handler/chat_memory_chain.py
具备了记忆能力之后,我们就可以在本地简单测试一下聊天功能,看看大模型是否能记住之前的历史对话。 首先,我们先做个简单的自我介绍,让大模型记录相关信息:
接下来,我们让大模型生成一些较长的内容,便于后面测试记忆能力:
到这里,我们已经让大模型生成了很长的文本,肯定超过了单次上下文长度限制。 最后,我们来测试一下记忆功能,看看 LLM 是否还记得最开始的聊天信息:
可以看到,即使中间经历了多轮对话和长文本生成,大模型仍然能够准确记忆历史的聊天信息。这说明,大模型已经具备了初步的记忆能力。
本篇文章首先介绍了记忆系统对于 LLM 应用的重要性,接下来介绍了业界主流的记忆系统实现方案,之后我们利用 LangChain 框架为 LLM 应用添加上记忆功能,最后简单演示了下整体效果。 如果我们把大模型比作人类的大脑,那么记忆则是大脑非常重要的一项原生能力。可惜的是,即使是目前最先进的大模型,其记忆能力还是完全无法媲美人类,已有的记忆方案更像是打补丁,并没有将记忆内化到大模型内部。 一方面,我们在通过各种工程化的方式,如 GraphRAG 来优化记忆功能;另一方面,大模型自身参数、算力和上下文长度等维度的提升,也有助于记忆能力的扩展。希望在未来的某一天,大模型可以具备堪比人类的记忆能力,那时候才有可能真正实现 AGI。