我们都知道,大模型本身是无状态、无记忆的。默认情况下,我们对大模型的每次提问,在其内部都会被视为一次全新的调用。尽管诸如 ChatGPT 等聊天应用内置了部分记忆功能,可以记录用户最近几轮的聊天信息,但它仍然存在上下文长度限制,对话历史超过一定长度后,就会强制开启新一轮对话。
为了解决这个问题,很多 AIGC 应用都需要独立开发记忆系统,特别是像 AI 聊天陪伴、RAG、智能客服等应用,记忆系统的质量会直接影响到用户体验和产品口碑。目前,业内典型的 Memery 实现方案如下:
- 全量缓冲记忆 ======
1.1 实现思路
最基础的记忆模式,将所有 Human/AI 生成的消息全部存储起来,每次需要使用时将保存的所有聊天消息列表全部传递到 Prompt 中,通过往用户的输入中添加历史对话信息/记忆,可以让 LLM 能理解之前的对话内容,而且这种记忆方式在上下文窗口限制内是无损的。
1.2 优点
- 在上下文窗口内可以实现无损记忆,可以记忆用户输入的全部内容。
- 实现方式简单,兼容性最好,所有大模型都支持。
1.3 缺点
-
将全部的历史上下文都传递给 LLM,输入中会包含大量的 Token,会导致响应时间变慢和成本增加。
-
LLM 的上下文有最大的 token 限制,无法记忆太长的对话。
-
记忆内容不是无限的,对于上下文长度较小的模型来说,记忆内容会变得极短。
-
缓冲窗口记忆 ======
2.1 实现思路
在缓冲记忆的基础上,增加上下文窗口限制,即只保留固定轮次的历史对话,"遗忘"掉过于久远的记忆。
2.2 优点
- 在窗口内可以实现无损记忆。
- 对小模型也比较友好,在不提问较为久远的内容时效果最佳。
- 实现方式简单,性能优异,所有大模型都支持。
2.3 缺点
-
无法保留长期的记忆,会遗忘之前的互动。
-
如果窗口内部分对话的内容较长,也容易超过 LLM 的上下文限制。
-
token 缓冲记忆 ==========
3.1 实现思路
同样是基于缓冲记忆的思想,只保留 max_tokens 长度的历史上下文,超过长度限制的历史记忆会被遗忘。
3.2 优点
- 可以基于特定模型的上下文长度限制来分配记忆长度。
- 对小模型也比较友好,如果不提问比较远的关联内容,一般效果最佳。
- 实现方式简单,性能优异,所有大模型都支持。
3.3 缺点
-
无法保留长期的记忆,会遗忘之前的互动。
-
摘要总结记忆 ======
4.1 实现思路
将每轮对话的输入输出,生成总结摘要,作为记忆保存起来,并在下一轮对话时传递给 LLM。
4.2 优点
- 可以同时支持长期记忆和短期记忆。
- 基于摘要功能,可以有效减少长对话中使用的token 数量,能记忆更多轮的对话信息。特别是在长对话时效果更加明显。
4.3 缺点
-
因为记忆是基于生成的摘要,因此无论是长期记忆还是短期记忆,都是模糊记忆,会丢失对话的细节。
-
对于较短的对话,可能会增加 token 使用量(短对话时,生成的摘要可能会比原始对话更长)。
-
记忆功能完全依赖于摘要 LLM 的能力,并且需要为摘要 LLM 额外分配 Token,会增加使用成本。
-
摘要+缓冲 混合记忆(缓冲短期记忆+摘要长期记忆) ===============================
5.1 实现思路
摘要+缓冲混合记忆,是目前业内采用较为广泛的记忆方案,它结合了缓冲窗口记忆和摘要总结记忆两种模式:
- 对于窗口限制内的对话,保留原始内容,作为短期记忆。
- 对于超过窗口限制的历史对话,生成摘要后保存,作为长期记忆。
- 将短期记忆与长期记忆合并,作为记忆保存。
例如,针对最大 token 长度为16k 的 LLM,可以设置记忆窗口 max_token = 12k,并将超过 12k 的历史对话生成摘要。
5.2 实现流程
5.3 优点
- 可以同时实现长期记忆和短期记忆,长期为模糊记忆,短期为精准记忆。
- 通过摘要功能,可以有效减少长对话中使用的 token 数量,能记忆更多轮的对话信息。
5.4 缺点
-
久远的历史对话为模糊记忆,会丢失部分细节。
-
长期记忆部分依赖于摘要 LLM 的能力,需要为摘要 LLM 额外分配 token,会增加使用成本。
-
向量数据库记忆 =======
6.1 实现思路
将全量记忆数据存储在向量存储中,每次搜索记忆时,基于向量检索,获取前 K 个最匹配的语料。
6.2 优点
- 基于向量数据库的横向扩展能力,理论上可以支持无限长度的记忆。
- 在记忆的细节上,可以比摘要总结处理得更好。
- token 的消耗相对可控。
6.3 缺点
-
需要向量数据库支持,增加使用成本。
-
用户的每次对话,都需要经过 Embedding 过程,性能有一定损耗。
-
记忆效果受 Embedding 检索结果的影响,效果不稳定。
下面,我们使用智谱AI的GLM-4-Long超长上下文模型,并借助LangChain的Memory模块,快速实现一个具有记忆功能的聊天机器人。
首先,基于LangChain,封装一个带有记忆功能的 chat_memory_chain:
(完整代码参考:https://gitee.com/zhangshenao/llm-ops-backend/blob/master/internal/handler/chat\_memory\_chain.py)
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 = 50# 保存历史对话的轮数
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,
return_messages=True,
chat_memory=chat_history
)
retriever = vector_store_service.as_retriever()
| vector_store_service.join_document_page_contents
chain = RunnablePassthrough.assign(
context=itemgetter(INPUT_KEY) | retriever,
history=RunnableLambda(_load_memory_variables_from_config) | itemgetter(HISTORY_KEY)
) | prompt_template | llm | parser
memory_chain =
(chain.with_config(configurable={MEMORY_CONFIG_KEY: memory})
.with_listeners(on_end=_save_chat_history))
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.save_context(run_obj.inputs, run_obj.outputs)
启动应用,开启聊天。我们先告诉大模型关于个人的一些信息:
接下来,我们让大模型生成一些较长的内容,便于后面测试记忆能力:
最后,我们来测试一下记忆功能,看看 LLM 是否还记得最开始的聊天信息:
可以看到,即使中间经历了多轮对话和长文本生成,GLM-4-Long
仍然能够准确记忆历史的聊天信息。
最后,简单介绍一下智谱AI的 GLM-4-Long 这个大模型。
GLM-4-Long 是支持100万上下文的⻓⽂本模型,它专为处理超⻓⽂本和
记忆型任务设计,支持⼤约相当2本红楼梦或者125篇科研论⽂的⻓度。
GLM-4-Long 极⼤的提⾼了模型的上下⽂理解能⼒,
丰富了⼤模型的应用落地能力。