【高速缓存】RedisVL管理 LLM 对话消息历史指南
引言
大语言模型(LLM)本质上是无状态的 —— 它不会记住之前对话中的任何内容。这在多轮对话中会带来挑战,因为后续的问题往往依赖前文提供的上下文。为了让 LLM “记住” 之前聊过什么,我们必须在每次调用时,把整个对话历史重新传递给模型。
但如果每次都传递全部历史,随着对话变长,token 数量会迅速膨胀,导致延迟增加、成本飙升。我们需要一种聪明的方法来存储、检索和管理对话历史,既能保证上下文完整,又能控制开销。
Redis 凭借其高性能、灵活的数据结构,非常适合承担这一角色。本指南将带你全面了解 RedisVL 提供的MessageHistory和SemanticMessageHistory工具,它们能帮你轻松管理 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)这种方法虽然简单,但存在两个严重问题:
- Token 膨胀:每次请求携带的历史消息越长,消耗的 token 越多,直接推高 API 费用。
- 延迟增加:模型处理长输入需要更长时间,影响用户体验。
一种常见的优化是截断历史,只保留最近 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 有时会“幻觉”(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()了解对话规模。