记忆+对话历史+Redis

解决问题:

  • 上下文连续性:让模型知道上一轮说过什么。
  • 会话个性化:让模型记住用户在当前会话里提供的信息,如名字、偏好、任务背景。
  • 多步任务承接:让模型把前一步结果作为后一步输入,而不是每轮都重新从零开始。

记忆的实现原理

  1. 读历史
  2. 把历史拼进当前提示
  3. 调用模型
  4. 把本轮输入和输出写回历史

工程化表达

  第一步:根据会话标识找到对应历史。
  比如通过 session_id="user-001" 找到这位用户当前会话的消息列表。

  第二步:把历史消息插入 Prompt。
  这一步通常会和 第 13 章 里的 MessagesPlaceholder("history") 配合使用。

  第三步:把“历史 + 当前问题”一起交给模型。
  这样模型看到的就不再只是当前一句话,而是完整上下文。

  第四步:把本轮消息写回历史存储。
  也就是把:用户这一轮输入、模型这一轮回复,都追加进去,供下一轮再读取。

Session_id

如何知道哪份历史属于哪位用户、哪次会话?最常见的做法就是使用 session_id,它就是“当前会话的编号”

RunnableWithMessageHistory(更合适的写法)

作用: 给一条已有的 Runnable / Chain 包上一层历史管理能力。

它并不替代你的 Prompt、Model、Parser,而是站在它们外面,统一负责历史的读取、注入和写回

BaseChatMessageHistory(历史存储接口)

作用:历史到底存在哪里、怎么存。它可以看作“聊天消息历史的统一抽象接口”

  • RunnableWithMessageHistory:控制历史何时读、何时写
  • BaseChatMessageHistory 及其实现类:控制历史存到哪里

常见实现类

组件名称 存储方式 适合场景
InMemoryChatMessageHistory 进程内内存 本地学习、单进程演示、临时会话
FileChatMessageHistory 本地文件 轻量持久化、小型脚本
RedisChatMessageHistory Redis 持久化、跨进程、多实例共享
其他后端实现 ES、数据库等 和现有技术栈集成

案例代码:

 1.内存版

from dotenv import load_dotenv
load_dotenv(encoding="utf-8")

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory, RunnableConfig
from langchain.chat_models import init_chat_model
from langchain_core.chat_history import InMemoryChatMessageHistory
from loguru import logger
import os

llm = init_chat_model(
    model="qwen-plus",
    model_provider="openai",
    api_key=os.getenv("aliQwen-api"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# 提示模板:history 占位符用于注入历史消息,input 为当前用户输入
prompt = ChatPromptTemplate.from_messages(
    [
        MessagesPlaceholder(variable_name="history"),
        ("human","{input}"),
    ]
)

parser = StrOutputParser()
chain = prompt | llm | parser
# 记忆组件:内存实现,进程内有效,重启后丢失
history = InMemoryChatMessageHistory()
# 包装链为「带历史」版本:本例固定返回同一个 history,重点先放在“自动读写历史”
runnable = RunnableWithMessageHistory(
    chain,
    get_session_history = lambda session_id: history,
    input_messages_key= "input",
    history_messages_key= "history",
)
history.clear()
# 保留 session_id 配置,是为了让调用方式和 V2 / Redis 版保持一致
config = RunnableConfig(configurable={"session_id": "user-001"})

# 第一轮:写入「我叫张三,我爱好学习。」,模型回复后会自动写回 history
logger.info(runnable.invoke({"input": "我叫张三,我爱好学习。"}, config))
# 第二轮:history 中已有上一轮,模型能回答「叫什么、爱好是什么」
logger.info(runnable.invoke({"input": "我叫什么?我的爱好是什么?"}, config))
  • MessagesPlaceholder("history") 负责接收历史
  • RunnableWithMessageHistory 负责包住整条链
  • session_id 负责告诉系统“当前该读哪份历史”

2.多session写法,按session_id维护多份历史


from dotenv import load_dotenv

load_dotenv(encoding="utf-8")

from langchain.chat_models import init_chat_model
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.runnables.history import RunnableWithMessageHistory
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.output_parsers import StrOutputParser
import os

llm = init_chat_model(
    model="qwen-plus",
    model_provider="openai",
    api_key=os.getenv("aliQwen-api"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
# 按 session_id 保存多份历史,便于多用户/多会话;生产可改为 Redis 等
store = {}

def get_session_history(session_id: str):
    """
    根据 session_id 获取对应的历史消息对象。
    如果不存在则创建一个新的 InMemoryChatMessageHistory。
    """
    if session_id not in store:
        store[session_id] = InMemoryChatMessageHistory()
    return store[session_id]

# 定义 Prompt 模板
#     - system: 给模型设定角色
#     - MessagesPlaceholder: 历史消息将注入这里
#     - human: 当前用户输入
prompt = ChatPromptTemplate.from_messages(
    [
        ("system","你是一个友好的中文助理,会根据上下文回答问题。"),
        MessagesPlaceholder("history"),
        ("human","{question}"),
    ]
)
# 构建基本链:Prompt → LLM → 输出解析
memory_chain = prompt | llm | StrOutputParser()
# 包装为带历史链:get_session_history 决定「当前 session 用哪份 history」
with_history = RunnableWithMessageHistory(
    memory_chain,
    get_session_history,
    input_messages_key="question",
    history_messages_key="history",
)
cfg_user_001 = {"configurable": {"session_id": "user-001"}}
cfg_user_002 = {"configurable": {"session_id": "user-002"}}
print("用户A(user-001):我叫张三。")
print("AI:", with_history.invoke({"question": "我叫张三。"}, cfg_user_001))

print("\n用户B(user-002):我叫李四。")
print("AI:", with_history.invoke({"question": "我叫李四。"}, cfg_user_002))

print("\n用户A(user-001):我叫什么?")
print("AI:", with_history.invoke({"question": "我叫什么?"}, cfg_user_001))

print("\n用户B(user-002):我叫什么?")
print("AI:", with_history.invoke({"question": "我叫什么?"}, cfg_user_002))

# ---------- 查看当前存储了哪些历史数据 ----------
# store 的 key 为 session_id,value 为该会话的 InMemoryChatMessageHistory
# 每个 history 的 .messages 为 List[BaseMessage],即该会话至今的全部消息(HumanMessage、AIMessage 等)
print("\n--- 当前 store 中的历史数据 ---")
for sid, history in store.items():
    print(f"[session_id={sid}] 共 {len(history.messages)} 条消息:")
    for i, msg in enumerate(history.messages):
        # msg 有 .type(如 human/ai)、.content(文本内容)
        content = str(msg.content)
        content_preview = (content[:50] + "…") if len(content) > 50 else content
        print(f"  {i+1}. [{msg.type}] {content_preview}")
print("--- 以上 ---\n")

3. 直接操作InMemoryChatMessagesHistory

# 直接使用 InMemoryChatMessageHistory 的 API:add_message、messages,手动拼历史后调用模型
from dotenv import load_dotenv

load_dotenv(encoding="utf-8")

from langchain.chat_models import init_chat_model
from langchain_core.chat_history import InMemoryChatMessageHistory
from loguru import logger
import os

llm = init_chat_model(
    model="qwen-plus",
    model_provider="openai",
    api_key=os.getenv("aliQwen-api"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)

# 创建内存版历史实例(BaseChatMessageHistory 的实现)
history = InMemoryChatMessageHistory()

# 手动添加用户消息并调用模型;模型输入为当前全部 messages
history.add_user_message("我叫张三,我的爱好是学习")
ai__messages = llm.invoke(history.messages)
logger.info(f"第一次回答\n{ai_message.content}")
# 手动把 AI 回复写回 history;否则下一轮只会看到用户消息,达不到“多轮记忆”的效果
history.add_message(ai_message)

# 再追加一轮:用户问「我叫什么?我的爱好是什么?」;此时 history.messages 已含上一轮
history.add_user_message("我叫什么?我的爱好是什么?")
ai_message2 = llm.invoke(history.messages)
logger.info(f"第二次回答\n{ai_message2.content}")
# 这一轮的 AI 回复也同样需要手动写回
history.add_message(ai_message2)

# 遍历当前会话全部消息;可以直观看到 history.messages 本质上就是一组 BaseMessage
for index, message in enumerate(history.messages, start=1):
    logger.info(f"第{index}条[{message.type}] {message.content}")
  • 想快速搭多轮链:优先用 RunnableWithMessageHistory
  • 想完全掌控读写时机:可以直接操作 InMemoryChatMessageHistory

Redis存储

环境监测:


import os

try:
    import redis
except ModuleNotFoundError:
    print("❌ 未找到 redis 包,请先执行:pip install -r requirements.txt")
    raise SystemExit(1)

REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")

print("✅ redis 包导入成功!")
print(f"✅ redis 包版本:{redis.__version__}")
print(f"✅ REDIS_URL 环境变量:{REDIS_URL}")
print(f"正在连接 Redis:{REDIS_URL}")

client = None
try:
    client = redis.Redis.from_url(REDIS_URL, decode_responses=True)
    print(f"✅ Redis 连接成功,PING -> {client.ping()}")
except (redis.ConnectionError, redis.TimeoutError, redis.ResponseError) as e:
    print("❌ Redis 连接失败")
    print(f"   REDIS_URL = {REDIS_URL}")
    print(f"   错误信息 = {e}")
    print("   如果你使用的是 Redis Stack 的 Docker 端口映射,可尝试:")
    print("   export REDIS_URL=redis://localhost:26379")
    raise SystemExit(1)
except Exception as e:
    print(f"❌ Redis 环境校验异常:{e}")
    raise SystemExit(1)
finally:
    if client is not None:
        client.close()
Redis 持久化对话历史
# 用户输入问题 → LangChain 自动从 Redis 读取这个用户之前的聊天历史 → 
# 把历史和新问题一起发给大模型 → 模型回答 → LangChain 再把本轮问答写回 Redis
# 核心组合是:
# RunnableWithMessageHistory + RedisChatMessageHistory


from dotenv import load_dotenv

load_dotenv(encoding="utf-8")
#    初始化聊天模型
from langchain.chat_models import init_chat_model
#    聊天历史的基类,表示“这是一个可以存取聊天消息的对象”
from langchain_core.chat_history import BaseChatMessageHistory
#    给普通 chain 加上历史记忆能力
from langchain_core.runnables.history import RunnableWithMessageHistory
#     构造 prompt 模板     在 prompt 中预留一个位置,用来放历史消息
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
#    给 chain 传运行配置,比如当前是哪一个用户的 session。
from langchain_core.runnables import RunnableConfig
import os
import redis
from loguru import logger

#     兼容两种 RedisChatMessageHistory 来源
try:
    #     新版推荐使用:
    from langchain_redis import RedisChatMessageHistory
    USE_LANGCHAIN_REDIS = True
except ModuleNotFoundError:
    #    有些老环境可能没有安装 langchain-redis
    from langchain_community.chat_message_histories import RedisChatMessageHistory
    USE_LANGCHAIN_REDIS = False
    
    
# 支持环境变量 REDIS_URL;未设置时默认 localhost:6379(标准 Redis),教程 Docker 可能用 26379
REDIS_URL = os.getenv("REDIS_URL", "redis://localhost:6379")
FORCE_SAVE = os.getenv("REDIS_FORCE_SAVE", "0") == "1"

def _check_redis():
    """启动时检查 Redis 是否可达,不可达时给出明确提示后退出。"""
    try:
        r = redis.Redis.from_url(REDIS_URL, decode_responses=True)
        r.ping()
        r.close()
    except (redis.ConnectionError, redis.ResponseError) as e:
        logger.error(
            "Redis 连接失败({})。请先启动 Redis,例如:\n"
            "  docker run -d -p 6379:6379 redis\n"
            "若使用其他端口,可设置环境变量:REDIS_URL=redis://localhost:端口",
            REDIS_URL,
        )
        raise SystemExit(1) from e
_check_redis()


# 原生 Redis 客户端,decode_responses=True 使返回值为 str 而非 bytes
redis_client = redis.Redis.from_url(REDIS_URL, decode_responses=True)

logger.info(
    "Redis 历史实现:{} | REDIS_URL={}",
    "langchain-redis" if USE_LANGCHAIN_REDIS else "langchain-community(兼容回退)",
    REDIS_URL,
)

llm = init_chat_model(
    model="qwen-plus",
    model_provider="openai",
    api_key=os.getenv("aliQwen-api"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)


prompt = ChatPromptTemplate.from_messages(
    [MessagesPlaceholder("history"), ("human", "{question}")]
)

#    根据 session_id 获取聊天历史
def get_session_history(session_id: str) -> BaseChatMessageHistory:
    """为每个 session_id 创建/返回对应的 Redis 历史实例,实现持久化存储。"""
    if USE_LANGCHAIN_REDIS:
        return RedisChatMessageHistory(
            session_id=session_id,
            redis_url=REDIS_URL,
        )
    return RedisChatMessageHistory(
        session_id=session_id,
        url=REDIS_URL,
    )


chain = RunnableWithMessageHistory(
    prompt | llm,
    get_session_history,
    input_messages_key="question",
    history_messages_key="history",
)
#    配置当前会话 ID
config = RunnableConfig(configurable={"session_id": "user-001"})

print("开始对话(输入 'quit' 退出)")
while True:
    question = input("\n输入问题:")
    if question.lower() in ["quit", "exit", "q"]:
        break
    response = chain.invoke({"question": question}, config)
    logger.info(f"AI回答:{response.content}")
    # 可选:把 Redis 当前内存快照刷到磁盘,方便演示“Redis 重启后仍能恢复”。
    # 这不是多轮记忆生效的必要条件,真实项目也不建议在每轮对话后都手动 SAVE。
    if FORCE_SAVE:
        redis_client.save()

 

posted @ 2026-05-06 14:16  幻影之舞  阅读(26)  评论(0)    收藏  举报