LangChain Memory 详解:实现 AI 连续对话不丢失上下文

为什么 AI 需要"记忆"?

核心痛点:大模型本身没有记忆

传统的 LLM 调用方式存在一个显著局限:每次请求都是独立的,模型无法获取之前的对话内容。这种"无状态"特性导致:

问题 表现 用户体验
上下文断裂 用户说"帮我订机票",下一轮说"改到周三",AI 不知道"改"什么 😤 重复输入信息
信息重复询问 用户已告知"我是前端工程师",AI 又问"您的职业是?" 😫 感觉 AI 很"蠢"
长对话失效 超过上下文窗口后,早期信息被截断 😵 对话无法继续

Memory 组件的核心价值

LangChain 中的 Memory 组件正是为解决这一问题而设计,它作为对话系统的"记忆中枢",能够高效管理对话状态。其核心功能可概括为:

  • 存储管理:维护对话历史的状态
  • 上下文构建:将历史信息转换为模型可理解的格式
  • 状态更新:在每次交互后更新记忆内容

记忆困境的本质原因

大模型每次调用都是独立的,我们通过 Prompt 拼接历史对话来模拟"记忆",本质上是在输入端重建对话上下文。但这种方式面临两个核心挑战:

  1. Token 限制:主流模型的上下文窗口通常在 4K-128K tokens 之间,超长对话会导致信息截断
  2. 成本问题:全量保存历史会让 Token 消耗随对话轮次线性增长

常用 Memory 类型详解

LangChain 提供了多种 Memory 实现,每种类型针对特定场景优化。以下是 5 种最常用的 Memory 类型对比:

类型 存储内容 触发条件 优点 缺点 适用场景
BufferMemory 全部原始消息 无(手动截断) 零信息丢失 Token 线性增长 短对话、调试阶段
BufferWindowMemory 最近 K 轮 固定轮数 K 长度可控 超 K 轮直接丢弃 聊天室、客服轮次有限
SummaryMemory 全程总结字符串 每轮结束后重新总结 远早于 Buffer 达到上限 总结耗 token,可能丢细节 长会话、会议纪要
SummaryBufferMemory 近期原始 + 远期总结 Max token 上限 近详远略,不丢早期主旨 总结仍耗 token 大多数生产场景首选
TokenBufferMemory 全部原始消息 Max token 数 直观对齐模型窗口 早期信息被硬截断 实时聊天、快速原型

类型详解

1. ConversationBufferMemory - 完整记忆

最简单的实现,完整存储所有对话消息:

typescript 复制代码
import { BufferMemory } from "langchain/memory";
import { ChatOpenAI } from "@langchain/openai";
import { ConversationChain } from "langchain/chains";

const memory = new BufferMemory();
const model = new ChatOpenAI({ model: "qwen-turbo" });

const chain = new ConversationChain({ llm: model, memory });

// 连续对话自动保存历史
await chain.call({ input: "你好,我叫张三" });
await chain.call({ input: "我叫什么名字?" }); // AI 知道答案是"张三"

⚠️ 注意:LangChain.js v0.3.1 已弃用传统 Memory 类,推荐使用新方案。本文示例展示的是核心概念,实际开发建议使用 LangGraph 或 db0-ai/langchain。

2. ConversationBufferWindowMemory - 滑动窗口

只保留最近 K 轮对话,有效控制上下文长度:

typescript 复制代码
import { BufferWindowMemory } from "langchain/memory";

// 只保留最近 3 轮对话
const memory = new BufferWindowMemory({ k: 3 });

选型建议:当对话轮次固定(如客服系统只需要最近 3-5 轮上下文)时选用。

3. ConversationSummaryMemory - 摘要记忆

当对话超过一定轮次时,使用 LLM 将历史对话压缩为摘要:

typescript 复制代码
import { ConversationSummaryMemory } from "langchain/memory";

const memory = new ConversationSummaryMemory({
  llm: model,
  maxTokenLimit: 200,  // 达到此阈值触发摘要
});

核心机制:将 10 轮对话压缩为"用户询问了 Java 线程池的核心参数,AI 解答了 maximumPoolSize 的含义..."这样的摘要。

4. ConversationSummaryBufferMemory - 混合策略(推荐)

结合近期全量和远期摘要的分层存储机制:

typescript 复制代码
import { ConversationSummaryBufferMemory } from "langchain/memory";

const memory = new ConversationSummaryBufferMemory({
  llm: model,
  maxTokenLimit: 200,  // 总 Token 容量
});

工作原理

  • 短期记忆区:存储最近 5-10 轮对话的完整原文,保留细节信息
  • 长期记忆区:将早期对话压缩为结构化摘要,存储关键决策点、用户偏好
  • 动态压缩:当总 Token 接近上限时,自动将最早轮次压缩为摘要,替换原文,节省约 70% 空间

前端实现带记忆对话实战

使用 LangGraph 实现带记忆对话

由于 LangChain.js 传统 Memory 类已弃用,推荐使用 LangGraph 的 MessageGraphStateGraph 实现记忆功能:

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage } from "@langchain/core/messages";
import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph";
import dotenv from "dotenv";

dotenv.config();

// 1. 初始化模型
const model = new ChatOpenAI({
  apiKey: process.env.DASHSCOPE_API_KEY,
    configuration: {
      baseURL: process.env.DASHSCOPE_API_URL,
    },
  model: "qwen-plus",
  temperature: 0.7,
});

// 2. 定义对话节点(AI 回复)
async function callModel(state: typeof MessagesAnnotation.State) {
  const response = await model.invoke(state.messages);
  return { messages: [response] };
}

// 3. 构建工作流(自动管理对话历史)
const workflow = new StateGraph(MessagesAnnotation)
  .addNode("model", callModel)
  .addEdge(START, "model")
  .addEdge("model", END);

const app = workflow.compile();

// 4. 多轮对话测试
async function multiTurnChat() {
  // 第一轮对话
  let state = { messages: [new HumanMessage("你好,我叫张三,是一名前端开发")] };
  let result = await app.invoke(state);
  console.log("AI:", result.messages[result.messages.length - 1]?.content);
  
  // 第二轮对话 - AI 记得之前的对话
  state = { messages: [...result.messages, new HumanMessage("我叫什么名字?")] };
  result = await app.invoke(state);
  console.log("AI:", result.messages[result.messages.length - 1]?.content);
  
  // 第三轮对话
  state = { messages: [...result.messages, new HumanMessage("我的职业是什么?")] };
  result = await app.invoke(state);
  console.log("AI:", result.messages[result.messages.length - 1]?.content);
}

multiTurnChat();

预期输出

text 复制代码
AI: 你好,张三!很高兴认识你...
AI: 你叫**张三**!😊  
(刚刚自我介绍时就提到啦~"你好,我叫张三,是一名前端开发" ✅)...
AI: 你的职业是**前端开发**!💻✨  
(你之前说:"我叫张三,是一名前端开发" ------ 这个身份很酷,是构建用户界面、连接设计与逻辑、让网页"活起来"的关键角色!)...

实现记忆持久化(本地存储)

typescript 复制代码
import { ChatOpenAI } from "@langchain/openai";
import { HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
import { StateGraph, MessagesAnnotation, START, END } from "@langchain/langgraph";
import fs from "fs/promises";
import dotenv from "dotenv";

dotenv.config();

const SESSION_FILE = "./chat-history.json";

// 保存会话历史到文件
async function saveHistory(sessionId: string, messages: BaseMessage[]) {
  const history = messages.map(msg => ({
    type: msg._getType(),
    content: msg.content,
  }));
  
  const data = { sessionId, history, timestamp: Date.now() };
  await fs.writeFile(SESSION_FILE, JSON.stringify(data, null, 2));
}

// 加载会话历史
async function loadHistory(sessionId: string): Promise<BaseMessage[]> {
  try {
    const content = await fs.readFile(SESSION_FILE, "utf-8");
    const data = JSON.parse(content);
    if (data.sessionId === sessionId) {
      return data.history.map((item: any) => 
        item.type === "human" 
          ? new HumanMessage(item.content)
          : new AIMessage(item.content)
      );
    }
  } catch {
    // 文件不存在,返回空数组
  }
  return [];
}

async function persistentChat() {
  const sessionId = "user-123";
  const model = new ChatOpenAI({
    apiKey: process.env.DASHSCOPE_API_KEY,
    configuration: {
      baseURL: process.env.DASHSCOPE_API_URL,
    },
    model: "qwen-plus",
  });

  // 加载历史记忆
  const savedMessages = await loadHistory(sessionId);
  console.log(`📂 加载了 ${savedMessages.length} 条历史消息\n`);

  // 构建工作流(使用已有历史)
  const workflow = new StateGraph(MessagesAnnotation)
    .addNode("model", async (state) => {
      const response = await model.invoke(state.messages);
      return { messages: [response] };
    })
    .addEdge(START, "model")
    .addEdge("model", END);

  const app = workflow.compile();

  // 对话循环
  const userInputs = ["我喜欢用 React 和 TypeScript", "我常用的 CSS 框架是 Tailwind", "我最喜欢的前端框架是什么?"];
  
  let state = { messages: savedMessages };
  
  for (const input of userInputs) {
    console.log(`👤 用户: ${input}`);
    state = { messages: [...state.messages, new HumanMessage(input)] };
    const result = await app.invoke(state);
    const aiResponse = result.messages[result.messages.length - 1];
    console.log(`🤖 AI: ${aiResponse?.content}\n`);
    state = result;
  }
  
  // 保存记忆
  await saveHistory(sessionId, state.messages);
  console.log(`💾 已保存 ${state.messages.length} 条消息到本地`);
}

persistentChat();

预期输出

text 复制代码
📂 加载了 0 条历史消息

👤 用户: 我喜欢用 React 和 TypeScript
🤖 AI: 太棒了!React + TypeScript 是现代前端开发中非常强大且受欢迎的组合 🎉 它能带来...

记忆内容优化技巧

技巧一:设置合理的窗口大小

不同类型的应用适合不同的记忆窗口:

应用场景 推荐窗口大小 说明
智能客服 3-5 轮 用户通常只需要近期上下文
技术问答 5-10 轮 需要记住之前讨论的技术细节
代码助手 10-20 轮 需要跟踪完整的代码修改过程
个性化助手 跨会话持久化 需要长期存储用户偏好
typescript 复制代码
// 滑动窗口示例(使用 LangGraph 的 MessagesAnnotation 自动管理)
// 如需限制窗口,可以在 invoke 前手动裁剪
const MAX_MESSAGES = 10;
const trimmedMessages = state.messages.slice(-MAX_MESSAGES);

技巧二:使用混合记忆策略

对于长对话场景,推荐采用"近期全量 + 远期摘要"的分层策略:

typescript 复制代码
// 混合记忆的核心思路
// 1. 保留最近 N 轮对话的完整内容
const recentMessages = allMessages.slice(-6);

// 2. 将早期对话压缩为摘要
const earlySummary = await generateSummary(allMessages.slice(0, -6));

// 3. 组合摘要 + 近期对话
const context = `【对话历史摘要】${earlySummary}\n\n【最近对话】${recentMessages}`;

技巧三:过滤无效信息

并非所有对话内容都值得记住,可以通过规则过滤:

typescript 复制代码
// 过滤无意义的交互
function shouldRemember(message: string): boolean {
  const ignorePatterns = [
    /^嗯+$/,
    /^哦+$/,
    /^好的$/,
    /^明白了$/,
    /^谢谢/,
  ];
  return !ignorePatterns.some(pattern => pattern.test(message));
}

第四步:不同场景 Memory 选型建议

选型决策树

arduino 复制代码
开始
 │
 ├─ 对话轮次 < 10 轮?
 │   ├─ 是 → 使用 BufferMemory(简单完整)
 │   └─ 否 ↓
 │
 ├─ 需要精确引用细节(如"第二个参数")?
 │   ├─ 是 → 使用 BufferWindowMemory 或 SummaryBufferMemory
 │   └─ 否 ↓
 │
 ├─ 对话可能超过 50 轮?
 │   ├─ 是 → 使用 SummaryBufferMemory(混合策略)
 │   └─ 否 ↓
 │
 └─ 需要跨会话记忆?
     └─ 是 → 持久化存储 + LangGraph

场景速查表

业务场景 推荐 Memory 理由
智能客服 BufferWindowMemory (k=5) 用户只需要近期上下文,历史过长无意义
技术问答助手 SummaryBufferMemory 需要保留精确技术细节,但对话可能较长
个性化推荐 持久化 + 向量检索 需要跨会话记住用户偏好
代码审查助手 BufferWindowMemory (k=10) 代码审查需要完整上下文,但轮次有限
教学辅导 SummaryBufferMemory 学生可能连续提问,需要保留学习进度
闲聊机器人 SummaryMemory 不需要精确记忆,压缩细节可接受

常见问题与解决方案

问题 1:记忆丢失

现象:AI 无法记住之前讨论的内容

排查步骤

  1. 检查 Memory 实例是否在 Chain 中正确传递
  2. 验证 save_context 是否被调用
  3. 检查序列化/反序列化过程是否正确

问题 2:Token 消耗过大

现象:对话轮次增加后,API 费用激增

解决方案

  • 使用 BufferWindowMemory 限制保留轮次
  • 使用 SummaryMemory 将历史压缩为摘要
  • 实施记忆清理策略,定期删除无关内容

问题 3:回答质量下降(注意力稀释)

现象:对话变长后,AI 开始忽略早期重要信息

原因:上下文过长导致模型注意力分散。研究表明,当对话长度超过上下文窗口的 70% 时,回答准确率下降 23%

解决方案

  • 使用 SummaryBufferMemory 保持上下文精简
  • 将关键信息(用户偏好、决策点)单独存储,始终保留在上下文中

问题 4:多用户记忆混淆

现象:不同用户的对话历史互相干扰

解决方案:使用会话 ID 隔离

typescript 复制代码
interface SessionManager {
  [sessionId: string]: BaseMessage[];
}

const sessions: SessionManager = {};

function getSessionMemory(sessionId: string) {
  if (!sessions[sessionId]) {
    sessions[sessionId] = [];
  }
  return sessions[sessionId];
}

结语

通过这篇教程,我们全面学习了 LangChain 中 Memory 组件的核心概念和实践方法。Memory 是构建智能对话系统的关键,掌握它就能让 AI 真正"记住"与用户的每一次交互。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
知彼解己1 小时前
LLM-based Planning:从后端视角理解 Agent 规划层
后端·golang·ai编程
wuhen_n1 小时前
LangChain Function Call 实战:让 AI 调用自定义工具
前端·langchain·ai编程
用户3499904939191 小时前
我用 AI 做了一个 PR Review 工作流
ai编程
DyLatte1 小时前
很多人把坚持,误以为成长
前端·后端·程序员
canonical_entropy1 小时前
自进化的两个尺度:RMSP Agent 与 AGE 方法论的深层结构对应
aigc·agent·ai编程
yingyima2 小时前
凌晨3点的警报声:定时任务监控与告警的最佳实践
前端
zach2 小时前
React中的兄弟通讯之发布订阅模式
前端·react.js
书中枫叶2 小时前
我用 Vue3 写了一个 Chrome 步骤录制插件:自动截图、本地存储、一键导出教程
前端·vue.js
达达尼昂2 小时前
AI Native 工程实践 : agent 自动化测试
前端·后端·架构