多轮对话历史管理
需求实现方式多用户隔离自动过期ttl=86400参数限制轮数继承LTRIM生产部署用长期记忆额外将问答存入 FAISS/Milvus建议:短期会话用 LangChain + Redis;长期语义记忆用 RAG + 向量库。两者互补!# 截断:只保留最近 max_messages 条。
短期、高频、会话级的对话历史 → 存 Redis
长期、低频、需语义检索的历史记录 → 存 FAISS(或其他向量库)
但更优的做法是:两者结合使用,各司其职。
一、Redis vs FAISS:定位完全不同
| 特性 | Redis | FAISS |
|---|---|---|
| 数据类型 | 键值对(字符串、列表、哈希等) | 向量索引(用于相似性搜索) |
| 用途 | 快速存取结构化会话状态 | 语义检索相关历史片段 |
| 访问方式 | 按 user_id + session_id 精确查找 |
输入新问题,找“语义最相似”的历史问答 |
| 持久性 | 可持久化,但通常用作缓存 | 本身不持久,需配合磁盘/数据库 |
| 成本 | 内存存储,成本较高 | 可存 SSD,支持十亿级向量 |
误区:FAISS 不是用来“存储完整对话历史”的,而是用来“检索相关历史片段”的。
二、正确架构:Redis + FAISS 协同工作
场景:用户第二天继续问昨天的问题
方案设计:

1. Redis:管理“当前会话”上下文
key: "chat_history:{user_id}:{session_id}"
value: [
{"role": "user", "content": "怎么换轮胎?"},
{"role": "assistant", "content": "首先确保车辆停稳..."}
]
- TTL(过期时间):设为 24 小时或 7 天
- 作用:保证同一会话内的多轮连贯性(如追问)
优势:毫秒级读写,支持 LRU 自动淘汰
2. FAISS:实现“跨会话语义记忆”
- 每次问答结束后,将 用户问题 + 助手回答 编码为向量,存入 FAISS
- 向量 ID 可关联元数据(如 user_id, timestamp, session_id)
- 第二天用户提问时:
- 用新问题 query 向量化
- 在 FAISS 中检索 top-k 最相似的历史问答
- 将这些“相关记忆”注入 Prompt
示例 Prompt:
已知你之前问过: Q: “胎压报警怎么办?” A: “先检查轮胎是否漏气,若无异常可手动复位...” 现在你的问题是:“今天又报警了,但轮胎看起来正常。”优势:即使用户换设备、清缓存,也能“记住”关键历史
1. 元数据过滤(Metadata Filtering)
- 在 FAISS 上层加一层 标量过滤(如只查该用户的记录)
- 可用 Milvus / Pinecone / Weaviate 替代纯 FAISS,原生支持
user_id过滤
2. 历史摘要(Memory Summarization)
- 对长对话做摘要(如用 LLM 生成“用户关注轮胎安全”)
- 将摘要向量化后存 FAISS,减少噪声
3. 冷热分离
- 热数据(近7天):Redis + FAISS 内存
- 冷数据(>7天):转存至 S3 + 向量数据库(如 Milvus)
==================================================================================================================================================
Redis 存最近 N 轮对话(带 TTL)
核心设计思路
| 目标 | 实现方式 |
|---|---|
| 按用户/会话隔离 | Key 格式:chat_history:{user_id}:{session_id} |
| 只保留最近 N 轮 | 使用 Redis List 结构 + LPUSH + LTRIM |
| 自动过期(TTL) | 对 key 设置 EXPIRE(如 24 小时) |
| 支持多轮对话上下文 | 每条消息存为 JSON 字符串(含 role/content) |
数据结构选择:为什么用 List?
- List 支持:
LPUSH:从左侧插入新消息(最新在前)LRANGE 0 N-1:获取最近 N 条LTRIM 0 N-1:自动截断,只保留 N 条
- 比 Hash 或 String 更适合有序、可截断的对话流
Python 示例(使用 redis-py)
import redis
import json
from typing import List, Dict
class ChatHistoryManager:
def __init__(self, redis_url="redis://localhost:6379", max_turns=10, ttl_seconds=86400):
self.redis = redis.from_url(redis_url)
self.max_turns = max_turns # 最多保留多少轮(每轮 = user + assistant)
self.ttl = ttl_seconds # 过期时间:默认 24 小时
def add_message(self, user_id: str, session_id: str, message: Dict[str, str]):
"""
添加一条消息(如 {"role": "user", "content": "你好"})
"""
key = f"chat_history:{user_id}:{session_id}"
# 序列化消息
msg_str = json.dumps(message, ensure_ascii=False)
# 从左侧插入(最新消息在前)
self.redis.lpush(key, msg_str)
# 只保留最近 max_turns * 2 条(因为每轮有 user + assistant 两条)
self.redis.ltrim(key, 0, self.max_turns * 2 - 1)
# 设置/刷新 TTL
self.redis.expire(key, self.ttl)
def get_history(self, user_id: str, session_id: str) -> List[Dict]:
"""
获取最近 N 轮对话(按时间顺序:最早 → 最新)
"""
key = f"chat_history:{user_id}:{session_id}"
messages = self.redis.lrange(key, 0, -1) # 从旧到新(Redis List 是反的)
# 反转:因为 LPUSH 导致最新在前,我们需要 oldest → newest
history = []
for msg in reversed(messages):
history.append(json.loads(msg.decode('utf-8')))
return history
def clear_history(self, user_id: str, session_id: str):
"""清除指定会话历史"""
key = f"chat_history:{user_id}:{session_id}"
self.redis.delete(key)
使用示例
# 初始化
history_mgr = ChatHistoryManager(max_turns=5, ttl_seconds=3600) # 保留5轮,1小时过期
user_id = "user_123"
session_id = "sess_abc"
# 用户提问
history_mgr.add_message(user_id, session_id, {"role": "user", "content": "怎么换轮胎?"})
# 助手回答
history_mgr.add_message(user_id, session_id, {"role": "assistant", "content": "首先确保车辆停稳..."})
# 下次请求时获取上下文
context = history_mgr.get_history(user_id, session_id)
# context = [
# {"role": "user", "content": "怎么换轮胎?"},
# {"role": "assistant", "content": "首先确保车辆停稳..."}
# ]
# 直接拼入 LLM Prompt
prompt = "\n".join([f"{m['role']}: {m['content']}" for m in context])
1. Key 设计更健壮
- 如果支持多设备,可加入
device_id:key = f"chat_history:{user_id}:{session_id}:{device_id}" - 防止 key 冲突:对 user_id 做哈希或 base64 编码(如果含特殊字符)
2. TTL 策略优化
- 滑动过期:每次操作都
EXPIRE,实现“活跃会话不被删” - 固定过期:首次创建时设 TTL,适合严格时效场景
3. 内存控制
- Redis 内存有限,可加全局限制:
# redis.conf maxmemory 2gb maxmemory-policy allkeys-lru
4. 与向量库协同
- 每次
add_message后,异步将(user_query, answer)存入 FAISS/Milvus 用于长期记忆
常见问题解答
Q1:为什么 max_turns * 2?
因为一轮对话通常包含 用户消息 + 助手回复 两条。如果你只存用户消息,则用
max_turns。
Q2:能否用 Sorted Set(ZSET)?
可以,用时间戳做 score,但 List 更简单高效,且天然支持 LTRIM 截断。
Q3:会话 ID 从哪来?
- Web:前端生成 UUID 并存 Cookie / localStorage
- App:设备 ID + 时间戳
- 若无需多会话,可省略 session_id,只用
user_id
==================================================================================================================================================
LangChain,它内置了 `RedisChatMessageHistory`,底层就是类似逻辑
使用 LangChain 官方集成的 RedisChatMessageHistory 来实现“按用户/会话隔离、带 TTL 的最近 N 轮对话存储”的完整方案。
LangChain 已经封装了 Redis 对话历史管理,你只需几行代码即可接入,并支持与 ConversationBufferMemory、RunnableWithMessageHistory 等高级组件无缝协作。
核心优势(LangChain 版本)
- 自动序列化/反序列化消息(BaseMessage)
- 原生支持
user_id/session_id隔离 - 可结合
TTL实现自动过期 - 与 LLM Chain、Agent 一键集成
完整代码示例
1. 基础用法:手动管理历史
from langchain_community.chat_message_histories import RedisChatMessageHistory
from langchain_core.messages import HumanMessage, AIMessage
# 初始化 Redis 历史管理器(自动按 session_id 隔离)
history = RedisChatMessageHistory(
session_id="user_123_sess_abc", # 关键:唯一会话 ID
url="redis://localhost:6379",
ttl=3600 # 自动设置 key 过期时间(秒),默认不过期
)
# 添加消息
history.add_user_message("怎么换轮胎?")
history.add_ai_message("首先确保车辆停稳,拉手刹...")
# 获取历史(自动按时间顺序)
messages = history.messages
print(messages)
# [HumanMessage(content='怎么换轮胎?'), AIMessage(content='首先确保车辆停稳...')]
session_id就是你的 “user_id + session_id” 组合,LangChain 不关心格式,只保证隔离。
2. 与 LLM Chain 集成
from langchain_openai import ChatOpenAI
from langchain.chains import ConversationChain
from langchain.memory import ConversationBufferMemory
from langchain_community.chat_message_histories import RedisChatMessageHistory
def get_chat_chain(session_id: str):
# 创建 Redis 历史存储(带 TTL)
message_history = RedisChatMessageHistory(
session_id=session_id,
url="redis://localhost:6379",
ttl=86400 # 24小时过期
)
# 包装为 Memory
memory = ConversationBufferMemory(
chat_memory=message_health,
return_messages=True # 返回 BaseMessage 列表(推荐)
)
llm = ChatOpenAI(model="gpt-4o")
chain = ConversationChain(llm=llm, memory=memory, verbose=True)
return chain
# 使用
chain = get_chat_chain("user_123_sess_abc")
response = chain.invoke("怎么换轮胎?")
print(response["response"])
# 下次调用自动带上上下文
response2 = chain.invoke("需要千斤顶吗?") # LLM 知道你在问换轮胎的事
3. 与 RunnableWithMessageHistory(LangChain 最新范式)
这是 LangChain 推荐的 多用户、多会话并发处理 方式:
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
from langchain_community.chat_message_histories import RedisChatMessageHistory
llm = ChatOpenAI(model="gpt-4o")
# 定义如何获取历史(关键:根据 config['configurable']['session_id'] 动态创建)
def get_session_history(session_id: str):
return RedisChatMessageHistory(session_id, url="redis://localhost:6379", ttl=86400)
# 包装 LLM 为带历史的 Runnable
chain_with_history = RunnableWithMessageHistory(
llm,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
# 调用时传入 session_id
config = {"configurable": {"session_id": "user_123_sess_abc"}}
response = chain_with_history.invoke(
{"input": "怎么换轮胎?"},
config=config
)
response2 = chain_with_history.invoke(
{"input": "需要千斤顶吗?"},
config=config # 自动复用同一会话历史
)
这种方式天然支持 Web/API 并发,每个请求通过
session_id隔离上下文。
关于“只保留最近 N 轮”
注意:LangChain 的
RedisChatMessageHistory默认不会自动截断历史(即不会限制轮数),它会保留所有消息直到 TTL 过期。
如何实现“最多保留 N 轮”?
方案 :继承并重写
from langchain_community.chat_message_histories.redis import RedisChatMessageHistory
import redis
class LimitedRedisChatMessageHistory(RedisChatMessageHistory):
def __init__(self, session_id: str, url: str, ttl: int = 3600, max_messages: int = 10):
super().__init__(session_id, url=url, ttl=ttl)
self.max_messages = max_messages
self.redis_client = redis.from_url(url)
def add_message(self, message):
super().add_message(message)
# 截断:只保留最近 max_messages 条
key = f"message_store:{self.session_id}"
self.redis_client.ltrim(key, 0, self.max_messages - 1)
if self.ttl:
self.redis_client.expire(key, self.ttl)
然后替换上面代码中的 RedisChatMessageHistory 为 LimitedRedisChatMessageHistory。
Redis 中实际存储结构
LangChain 默认 key 格式:
message_store:{session_id}
值是一个 Redis List,每条消息是 JSON 序列化的 BaseMessage:
[
"{\"type\":\"human\",\"content\":\"怎么换轮胎?\"}",
"{\"type\":\"ai\",\"content\":\"首先确保车辆停稳...\"}"
]
总结:LangChain + Redis 最佳实践
| 需求 | 实现方式 |
|---|---|
| 多用户隔离 | session_id = f"{user_id}_{device_id}" |
| 自动过期 | ttl=86400 参数 |
| 限制轮数 | 继承 RedisChatMessageHistory + LTRIM |
| 生产部署 | 用 RunnableWithMessageHistory + FastAPI/Flask |
| 长期记忆 | 额外将问答存入 FAISS/Milvus |
建议:短期会话用 LangChain + Redis;长期语义记忆用 RAG + 向量库。两者互补!
==================================================================================================================================================
FastAPI 示例(包含 session_id 生成、LLM 调用、历史管理)
- FastAPI:Web 框架
- LangChain:LLM 编排 + Redis 历史管理
- Redis:对话缓存(带 TTL + 截断)
- OpenAI API:大模型后端(可替换为本地模型)
自定义 Redis 历史类(支持截断)
# chat_history.py
from langchain_community.chat_message_histories.redis import RedisChatMessageHistory
import redis
class LimitedRedisChatMessageHistory(RedisChatMessageHistory):
def __init__(self, session_id: str, url: str, ttl: int = 3600, max_messages: int = 10):
super().__init__(session_id=session_id, url=url, ttl=ttl)
self.max_messages = max_messages
self.redis_client = redis.from_url(url)
def add_message(self, message):
super().add_message(message)
# 截断:只保留最近 max_messages 条
key = f"message_store:{self.session_id}"
self.redis_client.ltrim(key, 0, self.max_messages - 1)
if self.ttl:
self.redis_client.expire(key, self.ttl)
FastAPI 主应用 main.py
# main.py
import os
import uuid
from typing import Dict, Any
from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.messages import HumanMessage
from chat_history import LimitedRedisChatMessageHistory
# 加载环境变量
load_dotenv()
app = FastAPI(title="AI 车手助手", version="1.0")
# 全局 LLM
llm = ChatOpenAI(
model="gpt-4o-mini",
api_key=os.getenv("OPENAI_API_KEY"),
temperature=0.7
)
def get_session_history(session_id: str):
return LimitedRedisChatMessageHistory(
session_id=session_id,
url=os.getenv("REDIS_URL"),
ttl=86400, # 24小时过期
max_messages=10 # 最多保留10条消息(5轮对话)
)
# 包装带历史的链
chat_with_history = RunnableWithMessageHistory(
llm,
get_session_history,
input_messages_key="input",
history_messages_key="history"
)
# 请求/响应模型
class ChatRequest(BaseModel):
user_id: str # 用户唯一ID(如手机号、UUID)
device_id: str # 设备ID(可选,用于多设备隔离)
message: str # 用户输入
class ChatResponse(BaseModel):
session_id: str
response: str
@app.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
if not request.user_id or not request.message.strip():
raise HTTPException(status_code=400, detail="user_id 和 message 不能为空")
# 生成稳定且唯一的 session_id(同一用户+设备始终相同)
session_id = f"{request.user_id}_{request.device_id}" if request.device_id else request.user_id
try:
# 调用带历史的 LLM
response = await chat_with_history.ainvoke(
{"input": request.message},
config={"configurable": {"session_id": session_id}}
)
return ChatResponse(
session_id=session_id,
response=response.content
)
except Exception as e:
raise HTTPException(status_code=500, detail=f"LLM 调用失败: {str(e)}")
@app.get("/health")
async def health_check():
return {"status": "ok", "redis": os.getenv("REDIS_URL")}
启动服务
uvicorn main:app --reload --port 8000
测试示例(使用 curl 或 Postman)
请求
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_123",
"device_id": "web_browser_abc",
"message": "我的胎压报警灯亮了,怎么办?"
}'
响应
{
"session_id": "user_123_web_browser_abc",
"response": "胎压报警灯亮通常表示轮胎气压异常。请先安全停车,检查四个轮胎是否有明显漏气或扎钉。如果外观正常,可能是气温变化导致气压下降,建议用胎压计测量并充气至标准值(一般在B柱标签上)。若问题持续,建议前往维修点检查胎压传感器。"
}
第二次请求(自动带上上下文)
curl -X POST http://localhost:8000/chat \
-H "Content-Type: application/json" \
-d '{
"user_id": "user_123",
"device_id": "web_browser_abc",
"message": "标准胎压是多少?"
}'
LLM 会知道你在问“胎压报警”相关的问题!
Redis 中实际数据(示例)
Key:
message_store:user_123_web_browser_abc
Value (List):
[
"{\"type\":\"human\",\"content\":\"我的胎压报警灯亮了,怎么办?\"}",
"{\"type\":\"ai\",\"content\":\"胎压报警灯亮通常表示...\"}",
"{\"type\":\"human\",\"content\":\"标准胎压是多少?\"}",
"{\"type\":\"ai\",\"content\":\"一般轿车标准胎压在2.3~2.5 bar...\"}"
]
这个 FastAPI 服务实现了:
- 自动生成
session_id(用户+设备维度) - Redis 存储对话历史(带 TTL + 最多10条)
- LangChain 自动管理上下文
- 异步非阻塞(
ainvoke) - 易扩展(换模型、加 RAG、接向量库)
下一步可集成 RAG:在调用 LLM 前,先从 FAISS/Milvus 检索车辆手册相关内容,拼入 prompt。
更多推荐



所有评论(0)