短期、高频、会话级的对话历史 → 存 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 对话历史管理,你只需几行代码即可接入,并支持与 ConversationBufferMemoryRunnableWithMessageHistory 等高级组件无缝协作。

核心优势(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。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐