前端转 Agent 开发·第五节:Memory 会话记忆
英文标题:前端转 Agent 开发 · 第五节:Memory 会话记忆
大模型调用本身是无状态的,无法自动记住此前对话;LangChain 的 Memory 机制用于按 `session_id` 保存并注入会话历史。文章演示了通过 `RunnableWithMessageHistory` 和 `InMemoryChatMessageHistory` 实现临时内存会话记忆,并将其类比为前端的 HOC、Session 和 React state。针对重启后记忆丢失的问题,文中进一步通过继承 `BaseChatMessageHistory` 实现文件持久化,并列出 Redis、SQL、PostgreSQL 等可选存储方案。需要注意的是,`RunnableWithMessageHistory` 已被 LangChain 标记为废弃,生产环境应关注 LangGraph 的内置持久化能力及相关官方集成包。
17.5 KB
20.3 KB
19.3 KB
6.6 KB
中文
🚀 前端转 Agent 开发 · 第五节:Memory 会话记忆
📅 学习日期:2026-09-09(Day 05) 🧑💻 学习者:一名前端开发工程师 🎯 今日目标:让链“记住”聊过的天——临时记忆(内存)与持久化记忆(文件/数据库)两种方案
📖 写在前面:大模型是个“金鱼记忆”患者 🐠
先泼一盆冷水:大模型本身是无状态的。每次 invoke 对它来说都是初次见面——上一轮你说了什么,它一概不知 😅
是不是很熟悉?从前端视角看:HTTP 协议本身就是无状态的,我们依靠 Cookie / Session / localStorage 让 Web 应用“记住”用户。LangChain 里的 Memory,做的正是同一件事。
| 前端世界 🌍 | LangChain 世界 🦜 |
|---|---|
| HTTP 无状态 | 模型调用无状态 |
| Cookie / Session 机制 | RunnableWithMessageHistory 包装器 |
| 按用户 id 存 session | 按 session_id 存会话历史 |
| JS 变量 / React state(刷新即丢) | InMemoryChatMessageHistory(重启即丢) |
| localStorage / 数据库 | 文件 / Redis / SQL 持久化存储 |
今天的知识地图:
⚡ 一、临时会话记忆(内存版)
🧩 核心组合
- 🎁
RunnableWithMessageHistory:为普通链套上一层“记忆外壳”,创建带历史会话记忆的 Runnable 实例 - 🧠
InMemoryChatMessageHistory:将历史消息存放在内存中(进程重启后会丢失,适合测试)
💻 测试代码:金鱼记忆觉醒实验 🐠
实验设计:告诉 AI 三件事,最后询问总和,观察它能否记住前面的话。
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
key = ""
model = ChatOpenAI(model="qwen-max", api_key=key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
to_Str = StrOutputParser()
history_store = {}
f_p = ChatPromptTemplate.from_messages(
[
("system", "根据会话历史回答问题"),
MessagesPlaceholder("chat_history"),
("human", "请回答以下问题,{q}")
]
)
def get_history(sid):
if sid not in history_store:
history_store[sid] = InMemoryChatMessageHistory()
return history_store[sid]
base_chain = f_p | model | to_Str
new_chain = RunnableWithMessageHistory(
base_chain,
get_history,
input_messages_key="q",
history_messages_key="chat_history"
)
if __name__ == '__main__':
s_config = {"configurable": {"session_id": 'user_001'}}
res = new_chain.invoke({"q": "小明有一只猫"}, s_config)
print('---第一次----', res)
res = new_chain.invoke({"q": "小花有两条狗"}, s_config)
print('---第二次----', res)
res = new_chain.invoke({"q": "小李有两条狗"}, s_config)
print('---第三次----', res)
res = new_chain.invoke({"q": "一共有多少动物"}, s_config)
print('---第四次----', res)
🖼️ 记忆注入的完整流程
🧠 我的理解:它就是一个“记忆 HOC”
从前端视角看,RunnableWithMessageHistory 所做的事情极像 React 的高阶组件(HOC):
普通链(base_chain)原本没有记忆能力,经它包装后——会帮你自动获取历史、注入插槽、保存新消息,链本身一行都不用改 ✨
请记住四个关键参数:
| 参数 | 作用 | 前端类比 |
|---|---|---|
base_chain |
被包装的普通链 | 被 HOC 包裹的组件 |
get_history |
按 session_id 获取历史的函数 | getSession(userId) |
input_messages_key="q" |
告诉包装器:用户输入位于 invoke 字典的哪个 key 中 | props 命名 |
history_messages_key="chat_history" |
历史记录注入模板时使用的插槽 | slot 名称对齐 |
⚠️
session_id要放在s_config的configurable中传入——不同session_id的记忆彼此隔离,这和“每个用户各自拥有购物车”是同一个道理 🛒
🪦 一个重要警告
运行后,控制台会报出:
LangChainDeprecationWarning: RunnableWithMessageHistory is deprecated. Use LangGraph's built-in persistence instead.
这意味着:RunnableWithMessageHistory 已被标记为废弃,官方建议迁移至 LangGraph 内置的持久化存储。在学习阶段使用它理解原理完全没有问题,但生产项目需要关注新方案 📌
💾 二、持久化会话记忆(文件版)
内存版一旦重启就会失忆。若要真正记住用户,就需要落盘——自行继承 BaseChatMessageHistory 抽象类实现读写操作。
🏗️ 抽象类的三个必答题
BaseChatMessageHistory 是一个抽象类,规定了子类必须实现的框架(类似前端中的抽象基类 / 接口约定 📐):
| 方法 | 职责 | 项目常见实现 |
|---|---|---|
add_messages(messages) |
追加消息(先读取旧消息 → 合并 → 写回) | 写文件 / 写 Redis / INSERT |
messages(property) |
读取全部历史 | 读文件 / 读 Redis / SELECT |
clear() |
清空历史 | 删除文件 / DEL / DELETE |
💻 文件存储实现
import json
import os
from typing import Sequence
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import message_to_dict, messages_from_dict, BaseMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
class FileChatMessageHistory(BaseChatMessageHistory):
"""📁 把会话历史持久化到本地文件(每个 session 一个目录)"""
def __init__(self, s_id, s_path):
self.s_id = s_id
self.s_path = s_path
self.file_path = os.path.join(s_id, s_path)
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
def add_messages(self, messages: Sequence[BaseMessage]) -> None:
all_message = list(self.messages)
all_message.extend(messages)
new_messages = [message_to_dict(m) for m in all_message]
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(new_messages, f)
@property
def messages(self) -> list[BaseMessage]:
try:
with open(self.file_path, "r", encoding="utf-8") as f:
message_data = json.load(f)
return messages_from_dict(message_data)
except FileNotFoundError:
print("读取失败:历史文件还不存在")
return []
def clear(self):
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump([], f)
key = ""
model = ChatOpenAI(model="qwen-max", api_key=key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
to_Str = StrOutputParser()
f_p = ChatPromptTemplate.from_messages(
[
("system", "根据会话历史回答问题"),
MessagesPlaceholder("chat_history"),
("human", "请回答以下问题,{q}")
]
)
def get_history(sid):
return FileChatMessageHistory(sid, "chat_history")
base_chain = f_p | model | to_Str
new_chain = RunnableWithMessageHistory(
base_chain,
get_history,
input_messages_key="q",
history_messages_key="chat_history"
)
if __name__ == '__main__':
s_config = {"configurable": {"session_id": 'user_001'}}
res = new_chain.invoke({"q": "一共有多少动物"}, s_config)
print('---第四次----', res)
🖼️ 读写流程
💡 前端类比:这就是一个手写的本地缓存层——读取缓存 → 合并数据 → 写回,和前端“在 localStorage 中 merge 购物车后再 setItem”是同一套逻辑 😎
📦 三、现成方案盘点
在项目中,一般不会手写文件存储,而会结合 Redis、数据库进行处理。此前 langchain-community 社区版集成了许多现成实现:
| 类名 | 存储位置 |
|---|---|
RedisChatMessageHistory |
Redis 数据库 ⚡ |
SQLChatMessageHistory |
SQL 数据库 🗄️ |
FileChatMessageHistory |
本地文件 📁 |
PostgresChatMessageHistory |
PostgreSQL 数据库 🐘 |
DynamoDBChatMessageHistory |
AWS DynamoDB ☁️ |
但是!⚠️ langchain-community 已经停止维护,目前可选方案包括:
- 🛠️ 像第二节一样,继承
BaseChatMessageHistory手动构建(理解原理的必修课) - 📦 引入各细分领域的官方包,例如使用 Redis 时安装
langchain-redis包
🗺️ 明日计划
🎯 今日总结
📌 一句话:模型天生会失忆,Memory 就是为链套上一层“记忆外壳”(RunnableWithMessageHistory)——按 session_id 获取历史、注入插槽、存回新消息;存入内存是临时记忆,继承 BaseChatMessageHistory 自行实现存取就是持久化记忆(文件/Redis/SQL 任你选择)。
🐠 失忆模型 + 🧠 记忆外壳 + 💾 持久化
= 会“记得你”的对话应用 💘
第五天,从“一次性问答”升级到“多轮连续对话” ✅ 下一步,为这套记忆系统换上 LangGraph 的新引擎,再让模型长出双手(Tool Calling)…… 🌱
English / 原文
🚀 前端转 Agent 开发 · 第五节:Memory 会话记忆
📅 学习日期:2026-09-09(Day 05) 🧑💻 学习者:一名前端开发工程师 🎯 今日目标:让链"记住"聊过的天 —— 临时记忆(内存)与持久化记忆(文件/数据库)两种方案
📖 写在前面:大模型是个"金鱼记忆"患者 🐠
先泼一盆冷水:大模型本身是无状态的 。每次 invoke 对它来说都是初次见面 —— 上一轮你说了什么,它一概不知 😅
是不是很熟悉?前端视角:HTTP 协议本身就是无状态的 ,我们靠 Cookie / Session / localStorage 让 Web 应用"记住"用户。LangChain 里的 Memory,干的就是同一件事。
| 前端世界 🌍 | LangChain 世界 🦜 |
|---|---|
| HTTP 无状态 | 模型调用无状态 |
| Cookie / Session 机制 | RunnableWithMessageHistory 包装器 |
| 按用户 id 存 session | 按 session_id 存会话历史 |
| JS 变量 / React state(刷新即丢) | InMemoryChatMessageHistory(重启即丢) |
| localStorage / 数据库 | 文件 / Redis / SQL 持久化存储 |
今天的知识地图:
⚡ 一、临时会话记忆(内存版)
🧩 核心组合
- 🎁
RunnableWithMessageHistory:给普通链套一层"记忆外壳",创建带历史会话记忆的 Runnable 实例 - 🧠
InMemoryChatMessageHistory:把历史消息存在内存 里(进程重启就没了,适合测试)
💻 测试代码:金鱼记忆觉醒实验 🐠
实验设计:告诉 AI 三件事,最后问它总和,看它能不能记住前面的话。
from langchain_core.chat_history import InMemoryChatMessageHistory
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
key = ""
model = ChatOpenAI(model="qwen-max", api_key=key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
to_Str = StrOutputParser()
history_store = {}
f_p = ChatPromptTemplate.from_messages(
[
("system", "根据会话历史回答问题"),
MessagesPlaceholder("chat_history"),
("human", "请回答以下问题,{q}")
]
)
def get_history(sid):
if sid not in history_store:
history_store[sid] = InMemoryChatMessageHistory()
return history_store[sid]
base_chain = f_p | model | to_Str
new_chain = RunnableWithMessageHistory(
base_chain,
get_history,
input_messages_key="q",
history_messages_key="chat_history"
)
if __name__ == '__main__':
s_config = {"configurable": {"session_id": 'user_001'}}
res = new_chain.invoke({"q": "小明有一只猫"}, s_config)
print('---第一次----', res)
res = new_chain.invoke({"q": "小花有两条狗"}, s_config)
print('---第二次----', res)
res = new_chain.invoke({"q": "小李有两条狗"}, s_config)
print('---第三次----', res)
res = new_chain.invoke({"q": "一共有多少动物"}, s_config)
print('---第四次----', res)
🖼️ 记忆注入的完整流程
🧠 我的理解:它就是个"记忆 HOC"
前端视角,RunnableWithMessageHistory 干的事像极了 React 的高阶组件(HOC) :
普通链(base_chain)本来没有记性,用它一包装 —— 帮你自动取历史、注入插槽、存回新消息 ,链本身一行不用改 ✨
四个关键参数记牢:
| 参数 | 作用 | 前端类比 |
|---|---|---|
base_chain |
被包装的普通链 | 被 HOC 包裹的组件 |
get_history |
按 session_id 取历史的函数 | getSession(userId) |
input_messages_key="q" |
告诉包装器:用户输入在 invoke 字典的哪个 key | props 命名 |
history_messages_key="chat_history" |
历史注入模板的哪个插槽 | slot 名字对齐 |
⚠️
session_id放在s_config的configurable里传 —— 不同 session_id 的记忆互相隔离,跟"每个用户自己的购物车"一个道理 🛒
🪦 一个重要警告
运行后控制台会报:
LangChainDeprecationWarning: RunnableWithMessageHistory is deprecated. Use LangGraph's built-in persistence instead.
意思是:RunnableWithMessageHistory 已被标记废弃,官方推荐迁移到 LangGraph 内置的持久化存储 。学习阶段用它理解原理完全 OK,生产项目要留意新方案 📌
💾 二、持久化会话记忆(文件版)
内存版一重启就失忆。要真正记住用户,得落盘 —— 自己继承 BaseChatMessageHistory 抽象类实现存取。
🏗️ 抽象类的三个必答题
BaseChatMessageHistory 是抽象类,规定了子类必须实现的框架(像前端的抽象基类 / 接口约定 📐):
| 方法 | 职责 | 项目常见实现 |
|---|---|---|
add_messages(messages) |
追加消息(先读旧 → 合并 → 写回) | 写文件 / 写 Redis / INSERT |
messages(property) |
读取全部历史 | 读文件 / 读 Redis / SELECT |
clear() |
清空历史 | 删文件 / DEL / DELETE |
💻 文件存储实现
import json
import os
from typing import Sequence
from langchain_core.chat_history import BaseChatMessageHistory
from langchain_core.messages import message_to_dict, messages_from_dict, BaseMessage
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from langchain_core.runnables import RunnableWithMessageHistory
from langchain_openai import ChatOpenAI
class FileChatMessageHistory(BaseChatMessageHistory):
"""📁 把会话历史持久化到本地文件(每个 session 一个目录)"""
def __init__(self, s_id, s_path):
self.s_id = s_id
self.s_path = s_path
self.file_path = os.path.join(s_id, s_path)
os.makedirs(os.path.dirname(self.file_path), exist_ok=True)
def add_messages(self, messages: Sequence[BaseMessage]) -> None:
all_message = list(self.messages)
all_message.extend(messages)
new_messages = [message_to_dict(m) for m in all_message]
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(new_messages, f)
@property
def messages(self) -> list[BaseMessage]:
try:
with open(self.file_path, "r", encoding="utf-8") as f:
message_data = json.load(f)
return messages_from_dict(message_data)
except FileNotFoundError:
print("读取失败:历史文件还不存在")
return []
def clear(self):
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump([], f)
key = ""
model = ChatOpenAI(model="qwen-max", api_key=key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
to_Str = StrOutputParser()
f_p = ChatPromptTemplate.from_messages(
[
("system", "根据会话历史回答问题"),
MessagesPlaceholder("chat_history"),
("human", "请回答以下问题,{q}")
]
)
def get_history(sid):
return FileChatMessageHistory(sid, "chat_history")
base_chain = f_p | model | to_Str
new_chain = RunnableWithMessageHistory(
base_chain,
get_history,
input_messages_key="q",
history_messages_key="chat_history"
)
if __name__ == '__main__':
s_config = {"configurable": {"session_id": 'user_001'}}
res = new_chain.invoke({"q": "一共有多少动物"}, s_config)
print('---第四次----', res)
🖼️ 读写流程
💡 前端类比:这就是个手写的本地缓存层 —— 读缓存 → 合并数据 → 写回,跟前端"localStorage 里 merge 购物车再 setItem"是同一套逻辑 😎
📦 三、现成方案盘点
项目中一般不手写文件存储,而是结合 Redis、数据库来处理。以前 langchain-community 社区版集成了很多现成实现:
| 类名 | 存储位置 |
|---|---|
RedisChatMessageHistory |
Redis 数据库 ⚡ |
SQLChatMessageHistory |
SQL 数据库 🗄️ |
FileChatMessageHistory |
本地文件 📁 |
PostgresChatMessageHistory |
PostgreSQL 数据库 🐘 |
DynamoDBChatMessageHistory |
AWS DynamoDB ☁️ |
但是!⚠️ langchain-community 已经停止维护了,现在的选择:
- 🛠️ 像第二节那样,继承
BaseChatMessageHistory手动构建 (理解原理必备) - 📦 引入各细分官方包,比如用 Redis 就装
langchain-redis包
🗺️ 明日计划
🎯 今日总结
📌 一句话 :模型天生失忆,Memory 就是给链套一层"记忆外壳"(RunnableWithMessageHistory)—— 按 session_id 取历史、注入插槽、存回新消息;存内存是临时记忆,继承 BaseChatMessageHistory 自己实现存取就是持久化记忆(文件/Redis/SQL 随你挑)。
🐠 失忆模型 + 🧠 记忆外壳 + 💾 持久化
= 会"记得你"的对话应用 💘
第五天,从"一次性问答"升级到"多轮连续对话" ✅ 下一步,给这套记忆系统换上 LangGraph 的新引擎,再让模型长出双手(Tool Calling)…… 🌱