记忆+对话历史+Redis
解决问题:
- 上下文连续性:让模型知道上一轮说过什么。
- 会话个性化:让模型记住用户在当前会话里提供的信息,如名字、偏好、任务背景。
- 多步任务承接:让模型把前一步结果作为后一步输入,而不是每轮都重新从零开始。
记忆的实现原理
- 读历史
- 把历史拼进当前提示
- 调用模型
- 把本轮输入和输出写回历史
工程化表达
第一步:根据会话标识找到对应历史。
比如通过 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()

浙公网安备 33010602011771号