LangChain学习:Memory实战——让你的大模型记住你

前言

最近在研究怎么让大模型真正"记住"用户说过的话,而不是每次对话都像失忆了一样从头来过今天就把我的学习笔记、代码示例、踩坑心得全部整合起来,写成这篇超详细的实战指南。

这篇文章的目标很简单:让你看完就能上手,在自己的项目里快速加上"记忆"功能。咱们从最基础的无状态调用开始,一步步深入到多轮对话、Token 控制、多用户场景、持久化存储,最后再聊聊生产环境的最佳实践。

一、为什么 LLM 默认没有记忆?

所有大模型的 API 调用(OpenAI、DeepSeek、Claude、Gemini 等)本质上都是无状态的。每次请求都是独立的,模型完全不知道你上一次说了什么。

来看一个最简单的例子:

JavaScript 复制代码
import { ChatDeepSeek } from "@langchain/deepseek";


const model = new ChatDeepSeek({
  model: "deepseek-chat",
  temperature: 0,
});

const res1 = await model.invoke("我叫华高俊,喜欢打和平精英");
console.log(res1.content);  // 正常回复
console.log('///////')
const res2 = await model.invoke("我叫什么名字?");
console.log(res2.content);  // 模型:???我不知道啊...

第二次问名字,模型直接懵了。因为两次调用之间没有任何关联。

要让模型有记忆,最原始的办法就是手动维护一个消息列表,每次调用都把历史带上:

JavaScript 复制代码
let messages = [
  { role: "user", content: "我叫华高俊,喜欢打和平精英" },
  { role: "assistant", content: "好的,我记住了!" },
  { role: "user", content: "我叫什么名字?" }
];

await model.invoke(messages);

短期看没问题,但对话轮数一多,这个列表就会像滚雪球一样越来越大,导致:

  • Token 消耗暴涨,成本高
  • 容易超出模型上下文窗口(比如 128k token 上限)
  • Prompt 结构容易写乱

LangChain 的 Memory 模块就是为了优雅地解决这个问题而生的。

二、Memory 的核心:RunnableWithMessageHistory

在现代 LangChain JS(基于 LCEL 架构)里,实现记忆最推荐的方式是使用 RunnableWithMessageHistory。它能自动完成:

  1. 根据会话 ID 加载历史消息
  2. 把历史注入到 Prompt 的 {history} 占位符
  3. 调用模型
  4. 把本次用户输入和模型回复自动保存回历史

核心代码长这样:

JavaScript 复制代码
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { InMemoryChatMessageHistory } from "@langchain/core/chat_history";

const prompt = ChatPromptTemplate.fromMessages([
  ["system", "你是一个有记忆的助手,用中文友好回复。"],
  ["placeholder", "{history}"],  // 历史消息注入点
  ["human", "{input}"],         // 当前用户输入
]);

const chain = prompt.pipe(model);

// 多用户存储(生产必备)
const messageHistories = new Map<string, InMemoryChatMessageHistory>();

const chainWithHistory = new RunnableWithMessageHistory({
  runnable: chain,
  getMessageHistory: async (sessionId) => {
    if (!messageHistories.has(sessionId)) {
      messageHistories.set(sessionId, new InMemoryChatMessageHistory());
    }
    return messageHistories.get(sessionId)!;
  },
  inputMessagesKey: "input",     // 输入对象中的键名
  historyMessagesKey: "history", // prompt 中的占位符键名
});

调用方式:

JavaScript 复制代码
await chainWithHistory.invoke(
  { input: "我叫华高俊,喜欢打和平精英" },
  { configurable: { sessionId: "user-001" } }
);

await chainWithHistory.invoke(
  { input: "我是?" },
  { configurable: { sessionId: "user-001" } }  // 相同 sessionId 才有记忆
);

第二次调用时,模型就能正确回答"华高俊"。

三、完整调用流程深度拆解

很多新手(包括曾经的我)看到代码能跑,但不清楚内部到底是怎么工作的。下面把一次 invoke 的完整流程拆开来说:

  1. 接收输入和 sessionId 你调用 chainWithHistory.invoke({ input: "..." }, { configurable: { sessionId: "xxx" } })

  2. 获取对应会话的历史实例 调用你提供的 getMessageHistory("xxx"),返回一个 BaseChatMessageHistory 实例(比如 InMemoryChatMessageHistory)

  3. 读取历史消息 自动执行 await history.getMessages(),得到所有之前的消息数组

  4. 构造完整输入对象

    JavaScript 复制代码
    {
      history: [所有旧消息],
      input: "当前用户输入"
    }
  5. 渲染 Prompt

    • {history} 被替换成完整的历史消息列表
    • {input} 被替换成本次用户输入(作为 HumanMessage)
    • 最终形成一个完整的 messages 数组传给模型
  6. 调用模型得到回复

  7. 自动保存本次对话(关键!) RunnableWithMessageHistory 会自动:

    • history.addMessage(new HumanMessage(本次输入))
    • history.addMessage(new AIMessage(模型回复))
  8. 返回模型回复

整个过程除了提供 getMessageHistory 外,其他全是自动的。这就是为什么说它"优雅"的原因。

四、关键组件逐个详解

1. ChatPromptTemplate.fromMessages

这是构建聊天 Prompt 的首选方式,写法直观:

JavaScript 复制代码
ChatPromptTemplate.fromMessages([
  ["system", "系统提示"],
  ["placeholder", "{history}"],  // 必须和 historyMessagesKey 一致
  ["human", "{input}"],          // 必须和 inputMessagesKey 一致
]);

支持的 role 有:system、human、ai、placeholder。还可以插入 few-shot 示例:

JavaScript 复制代码
[
  ["human", "你是谁?"],
  ["ai", "我是华高俊的私人助手。"],
  ["human", "{input}"]
]

2. InMemoryChatMessageHistory

最简单的内存存储类,内部就是一个 messages: BaseMessage[] 数组。

  • 优点:简单、快速、适合本地测试和调试
  • 缺点:进程重启就丢失、不支持多实例共享

构造函数支持预加载:

JavaScript 复制代码
new InMemoryChatMessageHistory([
  new HumanMessage("旧消息"),
  new AIMessage("旧回复")
]);

手动操作方法:

  • await history.getMessages() 获取
  • await history.addMessage(msg) 添加
  • await history.clear() 清空(比如用户点"新对话")

3. sessionId 的重要性

如果你像这样写:

JavaScript 复制代码
getMessageHistory: async () => new InMemoryChatMessageHistory()

那所有用户都会共享同一个历史!上线后用户A看到用户B的聊天记录,灾难。

必须用 Map/Set 或数据库按 sessionId(通常是用户ID + 设备ID)隔离。

五、Token 爆炸怎么办?高级 Memory 策略

全量保存历史(Buffer 风格)简单,但对话长了 Token 很容易爆。LangChain 提供了多种控制策略:

  1. 窗口记忆(Window) 只保留最近 N 轮对话。实现方式:在 getMessageHistory 中 slice:

    JavaScript 复制代码
    const all = await baseHistory.getMessages();
    return all.slice(-maxTurns * 2);  // 每轮两句
  2. 摘要记忆(Summary) 用 LLM 把旧对话压缩成摘要,保留关键信息不丢失细节。

  3. 混合摘要缓冲(Summary Buffer) 最近几轮保留原文,旧的用摘要替换。最推荐的生产方案。

    LangChain JS 有现成实现(旧版 Memory 类):

    JavaScript 复制代码
    import { ConversationSummaryBufferMemory } from "langchain/memory";
    
    const memory = new ConversationSummaryBufferMemory({
      llm: model,
      maxTokenLimit: 1000,  // 超过就摘要旧部分
    });

    新版 LCEL 也可以通过自定义 getMessageHistory 实现类似逻辑。

六、生产环境必备:持久化存储

InMemory 只能用于开发测试,上线必须换持久化实现:

存储 场景
Upstash Redis @langchain/upstash Serverless(如 Vercel)首选
Redis @langchain/redis 高性能传统服务
MongoDB @langchain/mongodb 需要复杂查询
Postgres @langchain/community 已有 SQL 生态

替换方式只需改 getMessageHistory 返回对应类即可,其他代码不动。

七、实际项目中的最佳实践

  1. 多用户必用 sessionId ,建议格式:user: <math xmlns="http://www.w3.org/1998/Math/MathML"> u s e r I d : c h a t : {userId}:chat: </math>userId:chat:{chatId}
  2. 提供"清除对话"功能:手动调用 history.clear()
  3. 监控 Token 使用:用 LangSmith 或自己统计,及时触发摘要
  4. 结合 RAG:Memory 存对话历史,VectorStore 存知识库,职责分开
  5. 错误处理:模型调用失败时不要保存本次消息,避免脏数据
  6. 预加载历史:用户重新打开旧对话时,从数据库加载后 addMessages

八、总结

LangChain 的 Memory 模块本质上就是帮你自动化了"读历史 → 发模型 → 存新消息"这个循环。核心只有三件事:

  • 用 ChatPromptTemplate.fromMessages 写好带 {history} 和 {input} 的模板
  • 用 RunnableWithMessageHistory 包装 chain
  • 提供可靠的 getMessageHistory(开发用 InMemory,上线用 Redis 等)

掌握了这套流程,你就能轻松做出各种有记忆的聊天应用:客服机器人、个人助手、写作搭档、角色扮演......

从我最初的"手动滚雪球"消息列表,到现在几行代码就搞定多用户持久化记忆,LangChain 确实省了很多心。希望这篇干货能帮你在项目里快速落地。

相关推荐
BD_Marathon2 小时前
Promise基础语法
开发语言·前端·javascript
BOF_dcb3 小时前
网页设计DW
前端
千寻girling3 小时前
计算机组成原理-全通关源码-实验(通关版)---头歌平台
前端·面试·职场和发展·typescript·node.js
karshey3 小时前
【前端】解决:点击一个button,发现不触发点击事件
前端
用泥种荷花3 小时前
【前端学习AI】Function Calling
前端
2301_796512523 小时前
ModelEngin平台开发工作流,“前端职业导航师”通过直观的图形化界面,让用户像“搭积木”一样,轻松串联各种智能节点
前端·modelengine
Aotman_3 小时前
JavaScript MutationObserver用法( 监听DOM变化 )
开发语言·前端·javascript·vue.js·前端框架·es6
酷柚易汛3 小时前
酷柚易汛ERP 2025-12-26系统升级日志
java·前端·数据库·php
Onlyᝰ3 小时前
前端调用接口进行上传文件
前端