🚀 前端转 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 三件事,最后问它总和,看它能不能记住前面的话。
python
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 = "" # 🔑 api-key(建议走环境变量)
model = ChatOpenAI(model="qwen-max", api_key=key, # 模型名以官方文档为准
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
to_Str = StrOutputParser()
# 🗃️ 会话历史仓库:dict 按 session_id 存各会话的历史
history_store = {}
# 💬 提示词三段式:人设 + 历史插槽(slot!) + 当前问题
f_p = ChatPromptTemplate.from_messages(
[
("system", "根据会话历史回答问题"),
MessagesPlaceholder("chat_history"), # 🕳️ Day 03 讲过的插槽,历史从这里注入
("human", "请回答以下问题,{q}")
]
)
# 🔑 按 session_id 取历史,没有就建一个新的
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", # 📥 用户输入存在哪个 key
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) # ✅ 能答出"一共3只动物",说明它记住了前面三轮!
🖼️ 记忆注入的完整流程

🧠 我的理解:它就是个"记忆 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 |
💻 文件存储实现
python
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
# 🧰 两个序列化工具(框架自带):
# message_to_dict(m) 单个消息对象(BaseMessage) -> 字典
# messages_from_dict(list) [字典, 字典...] -> [消息, 消息...]
class FileChatMessageHistory(BaseChatMessageHistory):
"""📁 把会话历史持久化到本地文件(每个 session 一个目录)"""
def __init__(self, s_id, s_path):
self.s_id = s_id
self.s_path = s_path
# 建议路径直接拼接,避免 ./ 干扰,例如 user_001/chat_history
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) # 1️⃣ 读旧历史
all_message.extend(messages) # 2️⃣ 追加新消息
new_messages = [message_to_dict(m) for m in all_message] # 3️⃣ 序列化
with open(self.file_path, "w", encoding="utf-8") as f:
json.dump(new_messages, f) # 4️⃣ 全量写回文件
@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 = "" # 🔑 api-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}")
]
)
# 🔑 唯一的变化:get_history 返回文件版历史
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包
🗺️ 明日计划
- 🔄 实践 LangGraph 的内置持久化(deprecated 警告的正解)
- ✂️ 学习历史消息裁剪:对话太长会爆 token,学会只带最近 N 轮
- 🗄️ 用
langchain-redis包把历史存进 Redis 试试 - 🤖 正式进入 Tool Calling(工具调用),让模型学会"动手"而不是"动嘴"
🎯 今日总结
📌 一句话:模型天生失忆,Memory 就是给链套一层"记忆外壳"(RunnableWithMessageHistory)------ 按 session_id 取历史、注入插槽、存回新消息;存内存是临时记忆,继承 BaseChatMessageHistory 自己实现存取就是持久化记忆(文件/Redis/SQL 随你挑)。
arduino
🐠 失忆模型 + 🧠 记忆外壳 + 💾 持久化
= 会"记得你"的对话应用 💘
第五天,从"一次性问答"升级到"多轮连续对话" ✅ 下一步,给这套记忆系统换上 LangGraph 的新引擎,再让模型长出双手(Tool Calling)...... 🌱