前端转 Agent 开发 · 第五节:Memory 会话记忆

🚀 前端转 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_configconfigurable 里传 ------ 不同 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 已经停止维护了,现在的选择:

  1. 🛠️ 像第二节那样,继承 BaseChatMessageHistory 手动构建(理解原理必备)
  2. 📦 引入各细分官方包,比如用 Redis 就装 langchain-redis

🗺️ 明日计划

  • 🔄 实践 LangGraph 的内置持久化(deprecated 警告的正解)
  • ✂️ 学习历史消息裁剪:对话太长会爆 token,学会只带最近 N 轮
  • 🗄️ 用 langchain-redis 包把历史存进 Redis 试试
  • 🤖 正式进入 Tool Calling(工具调用),让模型学会"动手"而不是"动嘴"

🎯 今日总结

📌 一句话:模型天生失忆,Memory 就是给链套一层"记忆外壳"(RunnableWithMessageHistory)------ 按 session_id 取历史、注入插槽、存回新消息;存内存是临时记忆,继承 BaseChatMessageHistory 自己实现存取就是持久化记忆(文件/Redis/SQL 随你挑)。

arduino 复制代码
   🐠 失忆模型  +  🧠 记忆外壳  +  💾 持久化
        =  会"记得你"的对话应用 💘

第五天,从"一次性问答"升级到"多轮连续对话" ✅ 下一步,给这套记忆系统换上 LangGraph 的新引擎,再让模型长出双手(Tool Calling)...... 🌱

相关推荐
掘金挖土1 小时前
前端手摸手跑路之 AI 应用开发(二)
前端·后端
mayaairi2 小时前
Vue2 组件通讯(四):v-model、scoped样式、mixins与plugins
前端·javascript·vue.js
梦曦i2 小时前
@meng-xi/uni-router 未来展望:夯实基础、深化体验、探索前沿
前端·uni-app
Yeyu2 小时前
AAOS AppCard 实践:怎么把自己的 App 塞进别人的卡片里
前端
2601_962077532 小时前
程序员会消失吗?
ai·程序员·职业发展·未来趋势·技术变革
IT_陈寒3 小时前
Vite静态资源路径这个大坑害我调了一下午
前端·人工智能·后端
摆烂工程师3 小时前
别只拿 GPT-6 Astra 聊天,它真正恐怖的是开始会“干活”了
人工智能·程序员·vibecoding
狗哥哥3 小时前
分享下我的读书清单
前端
合天网安实验室3 小时前
Log4J2 FilteredObjectInputStream RCE 漏洞分析
前端·黑客