【高速缓存】RedisVL管理 LLM 对话消息历史指南

📅 2026/7/22 23:22:47 👁️ 阅读次数 📝 编程学习
【高速缓存】RedisVL管理 LLM 对话消息历史指南

引言

大语言模型(LLM)本质上是无状态的 —— 它不会记住之前对话中的任何内容。这在多轮对话中会带来挑战,因为后续的问题往往依赖前文提供的上下文。为了让 LLM “记住” 之前聊过什么,我们必须在每次调用时,把整个对话历史重新传递给模型。

但如果每次都传递全部历史,随着对话变长,token 数量会迅速膨胀,导致延迟增加、成本飙升。我们需要一种聪明的方法来存储、检索和管理对话历史,既能保证上下文完整,又能控制开销。

Redis 凭借其高性能、灵活的数据结构,非常适合承担这一角色。本指南将带你全面了解 RedisVL 提供的MessageHistorySemanticMessageHistory工具,它们能帮你轻松管理 LLM 对话历史,并实现基于语义相关性的智能上下文筛选。


前置条件

  • 已安装 RedisVL:pip install redisvl
  • 一个运行中的 Redis 实例(推荐 Redis 8+ 或 Redis Cloud)

概要

  • 使用MessageHistory存储和检索对话消息
  • 通过session_tag管理多用户、多会话
  • 使用SemanticMessageHistory基于语义相似度获取相关上下文,降低 token 消耗
  • 从历史中删除错误或不合适的消息,保持上下文干净

基本概念与初始化

RedisVL 的MessageHistory类为对话历史提供了一个简单的键值存储接口。每条消息都由role(角色)和content(内容)组成,这与主流 LLM API(如 OpenAI)的消息格式一致。支持的角色有:

  • system:系统提示,设定对话背景或行为规则
  • user:用户输入
  • llm:模型的回复
fromredisvl.extensions.message_historyimportMessageHistory# 初始化一个名为 'student tutor' 的对话历史chat_history=MessageHistory(name='student tutor')

存储消息

你可以单条添加,也可以批量添加。

# 添加一条系统消息chat_history.add_message({"role":"system","content":"你是一个乐于助人的地理老师,用简洁的短句回答关于欧洲国家的问题。"})# 批量添加多条消息(用户提问 + 模型回答)chat_history.add_messages([{"role":"user","content":"法国的首都是哪里?"},{"role":"llm","content":"首都是巴黎。"},{"role":"user","content":"那西班牙的首都呢?"},{"role":"llm","content":"首都是马德里。"},{"role":"user","content":"英国的人口是多少?"},{"role":"llm","content":"截至2023年,英国人口约为6700万。"},])

如果你习惯使用“一问一答”的方式,MessageHistory还提供了store()便捷方法,一条指令即可同时存储用户提示和模型响应:

prompt="与葡萄牙相比,英格兰的面积有多大?"response="英格兰的陆地面积比葡萄牙大约大15000平方英里。"chat_history.store(prompt,response)

检索消息

使用get_recent()可以按时间顺序获取最近的对话消息(默认返回所有消息)。

context=chat_history.get_recent()formessageincontext:print(message)

输出示例(注意顺序由旧到新):

{'role': 'user', 'content': '那西班牙的首都呢?'} {'role': 'llm', 'content': '首都是马德里。'} {'role': 'user', 'content': '英国的人口是多少?'} {'role': 'llm', 'content': '截至2023年,英国人口约为6700万。'} {'role': 'user', 'content': '与葡萄牙相比,英格兰的面积有多大?'} {'role': 'llm', 'content': '英格兰的陆地面积比葡萄牙大约大15000平方英里。'}

注意:系统消息(system)默认不会在get_recent()中返回,除非你通过参数显式指定。这符合常见做法——系统消息通常作为静态上下文,不在对话列表中重复展示。


管理多用户和多会话

当应用程序需要同时服务多个用户(或同一用户的多个独立对话)时,使用session_tag可以为每条消息打上标签,从而实现隔离。

# 为学生二创建代数辅导会话chat_history.add_message({"role":"system","content":"你是一个乐于助人的代数老师,用简单的语言解答数学问题。"},session_tag='student_two')chat_history.add_messages([{"role":"user","content":"方程 2x + 3 = 7 中 x 的值是多少?"},{"role":"llm","content":"x 的值是 2。"},{"role":"user","content":"方程 3y - 5 = 7 中 y 的值是多少?"},{"role":"llm","content":"y 的值是 4。"}],session_tag='student_two')# 只检索该会话的消息formsginchat_history.get_recent(session_tag='student_two'):print(msg)

输出将只包含student_two的代数对话,不会混入地理对话。


问题:长上下文的挑战

随着对话轮次增多,历史消息列表越来越长。按照传统方式,每次调用 LLM 时,我们都需传递整个历史:

whileTrue:prompt=input("请输入你的下一个问题:")context=chat_history.get_recent()# 获取全部历史response=llm_api_call(prompt,context)# 将全部历史传给 LLMchat_history.store(prompt,response)

这种方法虽然简单,但存在两个严重问题:

  1. Token 膨胀:每次请求携带的历史消息越长,消耗的 token 越多,直接推高 API 费用。
  2. 延迟增加:模型处理长输入需要更长时间,影响用户体验。

一种常见的优化是截断历史,只保留最近 N 条消息,但这可能导致早期的重要上下文丢失。例如,用户问“我之前问过的人口问题答案是什么?”,如果早期那条关于人口的问题已经被截断,模型就无从回答。


解决方案:语义消息历史(SemanticMessageHistory)

更智能的方法不是按时间截断,而是按语义相关性筛选上下文。也就是说,对于当前问题,我们只从整个历史中提取那些与当前问题语义最相关的消息。

RedisVL 的SemanticMessageHistory正是为此而生。它在存储消息时会同时生成向量嵌入,查询时计算当前问题与历史消息的余弦距离,只返回距离小于阈值的消息(即最相关的那些)。

初始化与使用

fromredisvl.extensions.message_historyimportSemanticMessageHistory semantic_history=SemanticMessageHistory(name='tutor')# 将之前的历史消息导入语义历史(也可以直接从空开始)semantic_history.add_messages(chat_history.get_recent(top_k=8))

获取相关上下文

现在,当用户提问时,我们不是拿回全部历史,而是获取最相关的部分:

prompt="关于英格兰的面积,我学到了什么?"semantic_history.set_distance_threshold(0.35)# 设置相似度阈值,越小越严格context=semantic_history.get_relevant(prompt)formsgincontext:print(msg)

输出可能仅包含那条关于英格兰面积的问题,因为它与当前问题语义最接近。其他不相关的消息(如首都问题、人口问题)被过滤掉了。

调整阈值

阈值决定了“相关性”的严格程度。余弦距离范围是 [0, 2]:

  • 0.0:要求几乎完全相同的语义匹配(极其严格)
  • 2.0:包含所有消息(相当于无筛选)

你可以根据场景灵活调整:

semantic_history.set_distance_threshold(0.7)# 放宽阈值,得到更多相关消息larger_context=semantic_history.get_relevant(prompt)formsginlarger_context:print(msg)

此时可能会返回更多消息,包括人口问题等,因为它们与“英格兰”有一点关联。

下面这张流程图清晰地对比了传统全量历史与语义筛选历史的差异:

语义筛选方式

用户提问

将问题向量化

在历史消息向量库中搜索相似项

距离 < 阈值?

将相关消息纳入上下文

忽略

将筛选后的上下文 + 新问题传给 LLM

得到回答,且 token 更少

传统方式

用户提问

获取全部历史消息

将全部历史 + 新问题传给 LLM

得到回答


对话控制:删除错误消息

LLM 有时会“幻觉”(hallucinate),给出不正确的信息。如果不及时纠正,这些错误信息会持续被当作上下文传递,污染后续回答。

SemanticMessageHistory提供了删除指定消息的功能。

# 存储一个错误信息(摩纳哥不是欧洲最小国家,梵蒂冈才是)semantic_history.store(prompt="欧洲最小的国家是哪个?",response="摩纳哥是欧洲最小的国家,面积仅0.78平方英里。"# 错误)# 获取最近消息的原始数据(包含 entry_id)context=semantic_history.get_recent(top_k=1,raw=True)bad_key=context[0]['entry_id']# 获取该消息的唯一标识符# 删除该消息semantic_history.drop(bad_key)# 再次查看历史,错误消息已消失corrected_context=semantic_history.get_recent()formsgincorrected_context:print(msg)

删除后,错误消息不再出现在上下文中,后续 LLM 调用就不会受到误导。


统计消息数量

使用count()方法可以获取当前会话中的消息总数(可指定session_tag)。

print(f"会话中共有{chat_history.count()}条消息")# 输出:会话中共有 7 条消息

总结

  • MessageHistory:提供基础的消息存储、检索和多会话隔离,适合简单的对话管理。
  • 长上下文的挑战:全量传递历史会导致 token 浪费和成本上升。
  • SemanticMessageHistory:基于向量相似度,智能筛选与当前问题最相关的历史消息,大幅降低 token 消耗,同时保留关键信息。
  • 对话修复:支持按entry_id删除错误消息,保持上下文干净。
  • 统计与监控:使用count()了解对话规模。