四种记忆策略:让 LLM 真正「记住」对话
作者手记:LLM 本质是无状态的——每次对话对它来说都是「初次见面」。要让它记住上下文,唯一的办法是把历史对话塞进 Prompt。但 Prompt 窗口是有限的,Token 成本也是有限的。于是,「记忆策略」就成了工程落地的必修课。
为什么 LLM 需要「记忆」?
先看一个真实场景:
用户:我想去北京旅游AI: 好的,北京有很多景点推荐,比如故宫、长城...用户:推荐一些住宿吧AI: 请问你想去哪个城市? ← 它「失忆」了!这就是典型的「无记忆」对话——每轮都是独立的,LLM 不知道用户之前说过什么。
要解决这个问题,我们需要做的就是:把历史对话塞进 Prompt 里。
但问题来了:
- 上下文窗口有限(即使 128K,也装不下无限对话)
- Token 成本高昂(每轮都带完整历史,费用爆炸)
- 信息过载(塞太多内容,模型反而「注意力分散」)
于是,记忆策略诞生了——它要解决的核心问题是:在有限的 Token 预算内,尽可能保留最有价值的上下文。
四种主流记忆策略对比
| 策略 | 核心思想 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| Buffer(全量缓冲) | 记住所有历史对话 | 信息完整、实现简单 | Token 消耗大、无法处理长对话 | 短对话、客服场景 |
| Window(滑动窗口) | 只保留最近 N 轮对话 | 成本低、响应快 | 会丢失早期关键信息 | 日常聊天、短任务 |
| Summary(摘要压缩) | 把历史对话压缩成摘要 | 节省 Token、保留核心信息 | 细节丢失、摘要可能失真 | 长对话、多轮任务 |
| Vector(向量检索) | 把历史对话存入向量库,按需检索 | 支持无限长度、可跨会话 | 实现复杂、检索延迟高 | 长期记忆、个性化助手 |
一、Buffer Memory(全量缓冲记忆)
核心思想
把所有历史对话都塞进 Prompt,一轮都不丢。
工作流程
对话 1: 用户:你好 → AI: 你好,有什么可以帮你?对话 2: 用户:我想查天气 → AI: 请问你在哪个城市?对话 3: 用户:北京 → AI: 北京今天晴天...
Prompt 构建:System: 你是一个助手History: [对话 1, 对话 2, 对话 3]Current: 用户:明天呢?我的 TypeScript 实现
import { SystemMessage, HumanMessage, AIMessage, BaseMessage } from "@langchain/core/messages";
// 会话状态interface Session { id: string; messages: BaseMessage[]; // 存储所有历史消息 // ...其他配置}
// Buffer 策略:全量返回所有消息function bufferContext(session: Session): BaseMessage[] { return session.messages;}使用方式
// 添加用户消息session.messages.push(new HumanMessage("你好"));// 添加 AI 回复session.messages.push(new AIMessage("你好,有什么可以帮你?"));
// 获取上下文(直接拿所有 messages)const contextMessages = bufferContext(session);
// 调用 LLMconst response = await llm.invoke(contextMessages);优缺点
| 优点 | 缺点 |
|---|---|
| ✅ 实现最简单 | ❌ Token 消耗随对话线性增长 |
| ✅ 信息完整,无丢失 | ❌ 无法处理长对话(超出上下文窗口) |
| ✅ 适合短任务 | ❌ 成本高(每轮都带全量历史) |
适用场景
- 客服对话(通常 3-5 轮结束)
- 简单问答(不需要长期记忆)
- 原型验证(快速测试)
二、Window Memory(滑动窗口记忆)
核心思想
只保留最近 N 轮对话,超出的自动丢弃。
工作流程
窗口大小 = 3 轮(6 条消息,一问一答算一轮)
对话 1: 用户:你好 → AI: 你好 ← 这一轮会被丢弃对话 2: 用户:查天气 → AI: 哪个城市?对话 3: 用户:北京 → AI: 北京晴天对话 4: 用户:明天呢? → AI: 明天也是晴天
Prompt 构建(只保留最近 3 轮):History: [对话 2, 对话 3, 对话 4]Current: 用户:大后天呢?我的 TypeScript 实现
interface Session { id: string; messages: BaseMessage[]; memoryStrategy: "buffer" | "window" | "summary" | "vector"; windowSize: number; // ← 窗口大小:保留最近 N 轮}
function windowContext(session: Session): BaseMessage[] { // 去掉 System Message,取最近的 windowSize × 2 条消息(一轮两条) const nonSystem = session.messages.filter(m => m._getType() !== "system"); const recent = nonSystem.slice(-session.windowSize * 2);
// 如果有 System Message,放最前面 const sys = session.messages.find(m => m._getType() === "system"); return sys ? [sys, ...recent] : recent;}配置方式
// 创建会话时,指定窗口大小为 3const session: Session = { id: "test-001", messages: [new SystemMessage("你是一个助手")], memoryStrategy: "window", windowSize: 3, // 只保留最近 3 轮对话 // ...其他配置};优缺点
| 优点 | 缺点 |
|---|---|
| ✅ Token 消耗可控 | ❌ 会丢失早期关键信息 |
| ✅ 实现简单 | ❌ 固定窗口大小,不够灵活 |
| ✅ 响应快 | ❌ 用户说「记住我之前说的…」会失效 |
适用场景
- 日常聊天(不需要长记忆)
- 短任务(如单次查询、简单代办)
- 成本敏感场景(控制 Token 用量)
三、Summary Memory(摘要压缩记忆)
核心思想
把历史对话压缩成一段摘要,只保留关键信息。
工作流程
原始对话(10 轮,5000 tokens):用户:我想去北京旅游AI: 好的,北京有很多景点...用户:推荐几个吧AI: 故宫、长城、颐和园......(省略 6 轮)
摘要压缩后(200 tokens):「用户计划去北京旅游,已了解故宫、长城、颐和园等景点,正在询问交通和住宿建议。」
Prompt 构建:System: 你是一个旅游助手Summary: [上述摘要]Recent: [最近 3 轮对话(保留原始文本)]Current: 用户:住宿推荐什么区域?我的 TypeScript 实现
import { PromptTemplate } from "@langchain/core/prompts";import { StringOutputParser } from "@langchain/core/output_parsers";
interface Session { id: string; messages: BaseMessage[]; memoryStrategy: "buffer" | "window" | "summary" | "vector"; summaryThreshold: number; // 超过 N 条消息触发压缩 recentToKeep: number; // 压缩后保留最近 M 条原文 summary?: string; // 压缩后的摘要}
// 摘要 Promptconst summaryPrompt = PromptTemplate.fromTemplate(`请将以下对话历史压缩为一段简洁的摘要,保留所有关键信息(人名、偏好、决策、重要事实等)。
{previous_summary}
对话历史:{history}
请输出压缩后的摘要(只输出摘要内容,不要加任何前缀):`);
function formatMessages(messages: BaseMessage[]): string { return messages.map((msg) => { const role = msg instanceof HumanMessage ? "用户" : msg instanceof AIMessage ? "助手" : "系统"; const content = typeof msg.content === "string" ? msg.content : String(msg.content); return `${role}:${content}`; }).join("\n");}
async function summaryContext(session: Session): Promise<BaseMessage[]> { // 不管有没有摘要,都保留最近几条的原文 const nonSystem = session.messages.filter(m => m._getType() !== "system"); const recent = nonSystem.slice(-session.recentToKeep * 2);
// 消息数量超过阈值 → 触发压缩 if (nonSystem.length >= session.summaryThreshold) { const oldMessages = nonSystem.slice(0, -session.recentToKeep * 2); const historyText = formatMessages(oldMessages);
// 调用 LLM 生成摘要 const chain = summaryPrompt.pipe(llm).pipe(parser); session.summary = await chain.invoke({ previous_summary: session.summary || "", history: historyText, }); }
// 构建上下文:System + 摘要 + 最近几条原文 const result: BaseMessage[] = []; const sys = session.messages.find(m => m._getType() === "system"); if (sys) result.push(sys); if (session.summary) { result.push(new SystemMessage(`之前的对话摘要:${session.summary}`)); } result.push(...recent); return result;}配置方式
const session: Session = { id: "test-001", messages: [new SystemMessage("你是一个助手")], memoryStrategy: "summary", summaryThreshold: 6, // 超过 6 条消息触发压缩 recentToKeep: 4, // 压缩后保留最近 4 轮原文};优缺点
| 优点 | 缺点 |
|---|---|
| ✅ Token 消耗大幅降低 | ❌ 细节丢失(摘要无法保留所有信息) |
| ✅ 支持超长对话 | ❌ 摘要可能失真(LLM 概括错误) |
| ✅ 保留核心意图 | ❌ 需要额外调用 LLM(增加成本和延迟) |
适用场景
- 长对话(10+ 轮)
- 多轮任务(如旅行规划、项目讨论)
- 需要跨会话记忆(把摘要持久化存储)
四、Vector Memory(向量检索记忆)
核心思想
把历史对话存入向量数据库,根据当前问题检索相关记忆。
工作流程
历史对话(100 轮) → 切分成片段 → 向量化 → 存入向量库
用户提问:「我之前说想去哪个城市来着?」 ↓问题向量化 → 在向量库检索最相似的历史片段 ↓找到:「用户计划去北京旅游」 ↓把检索结果 + 当前问题 → 构建 Prompt → LLM 回答我的 TypeScript 实现
import { Embeddings } from "@langchain/core/embeddings";import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";
interface VectorEntry { text: string; vector: number[]; metadata: { user: string; assistant: string };}
interface Session { id: string; messages: BaseMessage[]; memoryStrategy: "buffer" | "window" | "summary" | "vector"; vectorEntries: VectorEntry[]; // 向量化的历史对话 recentToKeep: number;}
// 纯 JS 实现的 TF-IDF Embedding(不需要 API)class TfIdfEmbeddings extends Embeddings { // ...省略实现(详见 RAG 那篇)}
const embeddings = new TfIdfEmbeddings();
// 每轮对话结束,把这一轮存入向量库export async function addToVectorStore( session: Session, userInput: string, aiResponse: string,): Promise<void> { const text = `用户:${userInput}\n助手:${aiResponse}`; const [vector] = await embeddings.embedDocuments([text]); session.vectorEntries.push({ text, vector, metadata: { user: userInput, assistant: aiResponse }, });}
// 余弦相似度计算function cosineSimilarity(a: number[], b: number[]): number { const dot = a.reduce((sum, v, i) => sum + v * b[i], 0); const normA = Math.sqrt(a.reduce((sum, v) => sum + v * v, 0)); const normB = Math.sqrt(b.reduce((sum, v) => sum + v * v, 0)); return dot / (normA * normB);}
// 构建上下文时,先检索相关历史async function vectorContext( session: Session, currentInput: string,): Promise<BaseMessage[]> { const result: BaseMessage[] = [];
// System Message 放最前面 const sys = session.messages.find(m => m._getType() === "system"); if (sys) result.push(sys);
// 检索相关历史对话(取 Top 3) if (session.vectorEntries.length > 0) { const queryVector = await embeddings.embedQuery(currentInput); const topK = session.vectorEntries .map(entry => ({ entry, similarity: cosineSimilarity(queryVector, entry.vector), })) .sort((a, b) => b.similarity - a.similarity) .slice(0, 3) .map(item => item.entry.text);
if (topK.length > 0) { result.push(new SystemMessage( `以下是从历史对话中检索到的相关内容:\n${topK.join("\n\n")}` )); } }
// 再加上最近 N 轮对话原文 const nonSystem = session.messages.filter(m => m._getType() !== "system"); const recent = nonSystem.slice(-session.recentToKeep * 2); result.push(...recent);
return result;}使用方式
const session: Session = { id: "test-001", messages: [new SystemMessage("你是一个助手")], memoryStrategy: "vector", vectorEntries: [], recentToKeep: 4,};
// 对话过程中,每轮结束都把对话存入向量库await addToVectorStore(session, "我想去北京旅游", "好的,北京有很多景点...");await addToVectorStore(session, "推荐几个吧", "故宫、长城、颐和园...");
// 下一轮提问时,自动检索相关历史const context = await vectorContext(session, "我之前说想去哪个城市来着?");// 检索结果会包含「用户计划去北京旅游」相关的历史片段优缺点
| 优点 | 缺点 |
|---|---|
| ✅ 支持无限长度历史 | ❌ 实现复杂(需要向量库) |
| ✅ 跨会话记忆(持久化) | ❌ 检索延迟高(相比直接读内存) |
| ✅ 按需检索(只带相关信息) | ❌ 检索质量依赖 Embedding 模型 |
适用场景
- 个性化助手(记住用户偏好、历史决策)
- 长期项目(跨天、跨周的持续讨论)
- 企业知识库(结合 RAG 使用)
四种策略横向对比
| 维度 | Buffer | Window | Summary | Vector |
|---|---|---|---|---|
| 实现难度 | ⭐ | ⭐ | ⭐⭐ | ⭐⭐⭐ |
| Token 成本 | 高 | 中 | 低 | 低 |
| 信息完整性 | 100% | 部分 | 核心信息 | 按需检索 |
| 支持长对话 | ❌ | ⚠️ | ✅ | ✅ |
| 跨会话记忆 | ❌ | ❌ | ✅(需持久化) | ✅ |
| 响应延迟 | 低 | 低 | 中(需摘要) | 高(需检索) |
| 我的项目代码行数 | 3 行 | 10 行 | 60 行 | 120 行 |
选型决策树
你的对话通常有多长? │ ├── 3-5 轮 → 用 Buffer(最简单) │ ├── 5-10 轮 → 用 Window(k=5 或 k=3) │ └── 10+ 轮 → 用 Summary 或 Vector │ ├── 需要细节 → Vector(向量检索) │ └── 只需要核心意图 → Summary(摘要压缩)混合策略(生产环境推荐)
实际生产环境中,往往采用组合策略:
方案一:Summary + Buffer(短期 + 长期)
- 最近 3 轮 → 用 Buffer(保留原始对话)- 3 轮之前 → 用 Summary(压缩成摘要)优点:既保留近期细节,又控制 Token 用量。
方案二:Window + Vector(滑动窗口 + 长期记忆)
- 最近 5 轮 → 用 Window(快速响应)- 历史对话 → 存入 Vector(按需检索)优点:平衡性能和记忆能力。
我的项目实现(统一入口)
export async function getContextMessages( session: Session, currentInput?: string,): Promise<BaseMessage[]> { switch (session.memoryStrategy) { case "buffer": return bufferContext(session); case "window": return windowContext(session); case "summary": return await summaryContext(session); case "vector": return await vectorContext(session, currentInput || ""); default: return bufferContext(session); }}Handler 中调用:
// 不管用什么策略,Handler 只需要调用这一行const contextMessages = await getContextMessages(session, content);这就是策略模式的魅力——新增策略时,Handler 一行代码都不用改!
实战踩坑经验
坑 1:System Message 丢了怎么办?
问题:Window/Vector 策略取消息时,不小心把 System Message 过滤掉了。
解决:专门处理 System Message,永远放在最前面:
const sys = session.messages.find(m => m._getType() === "system");if (sys) result.push(sys);坑 2:摘要 Prompt 输出了「以下是摘要:」
问题:LLM 喜欢在输出前加解释文字。
解决:在 Prompt 末尾强制加一句:
请输出压缩后的摘要(只输出摘要内容,不要加任何前缀):坑 3:向量检索到了不相关的内容
问题:TF-IDF 对中文语义理解有限,可能检索到不相关的历史。
解决:
- 检索 Top 5 而不是 Top 3,让 LLM 自己筛选
- 检索结果加一个相似度阈值(低于 0.3 的直接丢弃)
- 生产环境换成中文 Embedding 模型(如 BGE)
一句话总结
Buffer 记住所有,Window 记住最近,Summary 记住重点,Vector 记住你想问的。
根据你的场景选择合适的策略,或者组合使用——没有最好的记忆,只有最适合的记忆。
系列预告
这是 LangChain.js 工程化实战 系列的第 0 篇(基础篇),完整学习路径:
| 篇序 | 主题 | 状态 |
|---|---|---|
| 第 0 篇 | 四种记忆策略(本文) | ✅ 已完成 |
| 第 1 篇 | RAG 检索增强生成 | ✅ 已完成 |
| 第 2 篇 | Prompt Engineering | ✅ 已完成 |
| 第 3 篇 | LangChain 工程模块化 | ✅ 已完成 |
| 第 4 篇 | LangGraph 多步推理 | ✅ 已完成 |
| 第 5 篇 | Agent 多工具调用 | ✅ 已完成 |
系列主线:如何把 LLM 从一个「聊天工具」,变成一个「可控的工程系统」。
项目源码:learn-langchain.js 核心文件:
src/server/memory-strategies.ts
Some information may be outdated