【高速缓存】RedisVL管理 LLM 对话消息历史指南
引言大语言模型LLM本质上是无状态的 —— 它不会记住之前对话中的任何内容。这在多轮对话中会带来挑战因为后续的问题往往依赖前文提供的上下文。为了让 LLM “记住” 之前聊过什么我们必须在每次调用时把整个对话历史重新传递给模型。但如果每次都传递全部历史随着对话变长token 数量会迅速膨胀导致延迟增加、成本飙升。我们需要一种聪明的方法来存储、检索和管理对话历史既能保证上下文完整又能控制开销。Redis 凭借其高性能、灵活的数据结构非常适合承担这一角色。本指南将带你全面了解 RedisVL 提供的MessageHistory和SemanticMessageHistory工具它们能帮你轻松管理 LLM 对话历史并实现基于语义相关性的智能上下文筛选。前置条件已安装 RedisVLpip 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_historyMessageHistory(namestudent 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()可以按时间顺序获取最近的对话消息默认返回所有消息。contextchat_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_tagstudent_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_tagstudent_two)# 只检索该会话的消息formsginchat_history.get_recent(session_tagstudent_two):print(msg)输出将只包含student_two的代数对话不会混入地理对话。问题长上下文的挑战随着对话轮次增多历史消息列表越来越长。按照传统方式每次调用 LLM 时我们都需传递整个历史whileTrue:promptinput(请输入你的下一个问题)contextchat_history.get_recent()# 获取全部历史responsellm_api_call(prompt,context)# 将全部历史传给 LLMchat_history.store(prompt,response)这种方法虽然简单但存在两个严重问题Token 膨胀每次请求携带的历史消息越长消耗的 token 越多直接推高 API 费用。延迟增加模型处理长输入需要更长时间影响用户体验。一种常见的优化是截断历史只保留最近 N 条消息但这可能导致早期的重要上下文丢失。例如用户问“我之前问过的人口问题答案是什么”如果早期那条关于人口的问题已经被截断模型就无从回答。解决方案语义消息历史SemanticMessageHistory更智能的方法不是按时间截断而是按语义相关性筛选上下文。也就是说对于当前问题我们只从整个历史中提取那些与当前问题语义最相关的消息。RedisVL 的SemanticMessageHistory正是为此而生。它在存储消息时会同时生成向量嵌入查询时计算当前问题与历史消息的余弦距离只返回距离小于阈值的消息即最相关的那些。初始化与使用fromredisvl.extensions.message_historyimportSemanticMessageHistory semantic_historySemanticMessageHistory(nametutor)# 将之前的历史消息导入语义历史也可以直接从空开始semantic_history.add_messages(chat_history.get_recent(top_k8))获取相关上下文现在当用户提问时我们不是拿回全部历史而是获取最相关的部分prompt关于英格兰的面积我学到了什么semantic_history.set_distance_threshold(0.35)# 设置相似度阈值越小越严格contextsemantic_history.get_relevant(prompt)formsgincontext:print(msg)输出可能仅包含那条关于英格兰面积的问题因为它与当前问题语义最接近。其他不相关的消息如首都问题、人口问题被过滤掉了。调整阈值阈值决定了“相关性”的严格程度。余弦距离范围是 [0, 2]0.0要求几乎完全相同的语义匹配极其严格2.0包含所有消息相当于无筛选你可以根据场景灵活调整semantic_history.set_distance_threshold(0.7)# 放宽阈值得到更多相关消息larger_contextsemantic_history.get_relevant(prompt)formsginlarger_context:print(msg)此时可能会返回更多消息包括人口问题等因为它们与“英格兰”有一点关联。下面这张流程图清晰地对比了传统全量历史与语义筛选历史的差异语义筛选方式是否用户提问将问题向量化在历史消息向量库中搜索相似项距离 阈值将相关消息纳入上下文忽略将筛选后的上下文 新问题传给 LLM得到回答且 token 更少传统方式用户提问获取全部历史消息将全部历史 新问题传给 LLM得到回答对话控制删除错误消息LLM 有时会“幻觉”hallucinate给出不正确的信息。如果不及时纠正这些错误信息会持续被当作上下文传递污染后续回答。SemanticMessageHistory提供了删除指定消息的功能。# 存储一个错误信息摩纳哥不是欧洲最小国家梵蒂冈才是semantic_history.store(prompt欧洲最小的国家是哪个,response摩纳哥是欧洲最小的国家面积仅0.78平方英里。# 错误)# 获取最近消息的原始数据包含 entry_idcontextsemantic_history.get_recent(top_k1,rawTrue)bad_keycontext[0][entry_id]# 获取该消息的唯一标识符# 删除该消息semantic_history.drop(bad_key)# 再次查看历史错误消息已消失corrected_contextsemantic_history.get_recent()formsgincorrected_context:print(msg)删除后错误消息不再出现在上下文中后续 LLM 调用就不会受到误导。统计消息数量使用count()方法可以获取当前会话中的消息总数可指定session_tag。print(f会话中共有{chat_history.count()}条消息)# 输出会话中共有 7 条消息总结MessageHistory提供基础的消息存储、检索和多会话隔离适合简单的对话管理。长上下文的挑战全量传递历史会导致 token 浪费和成本上升。SemanticMessageHistory基于向量相似度智能筛选与当前问题最相关的历史消息大幅降低 token 消耗同时保留关键信息。对话修复支持按entry_id删除错误消息保持上下文干净。统计与监控使用count()了解对话规模。