LangChain 第三课:拒绝“只有七秒记忆”,给 LLM 装上大脑 (Memory)
摘要: 大模型API调用本质是无状态的,导致LLM无法记住对话历史。文章探讨了LangChain的Memory模块解决方案,通过RunnableWithMessageHistory自动管理对话上下文。演示了如何通过内存存储实现多轮对话记忆,并指出生产环境中需解决持久化(如Redis存储)和Token爆炸(窗口记忆或摘要压缩)问题,为构建连贯的AI助手提供实践方案。(149字)
在开发基于 LLM 的应用(如聊天机器人)时,开发者很快会遇到一个核心痛点:大模型默认是“失忆”的。
LLM 的 API 调用本质上和 HTTP 请求一样,都是无状态(Stateless)的。这意味着,无论你之前和它聊得多么热火朝天,在下一轮对话中,对它来说你依然是一个全新的陌生人。
今天的文章,我们将直击这个痛点,深入探讨 LangChain 中的 Memory(记忆) 模块。我们将通过代码实战,手把手教你如何打破“七秒记忆”的限制,让 AI 真正记住你是谁,记住你们聊过什么。
1. 现象:大模型的“健忘症”
首先,我们需要理解一个核心概念:LLM 的 API 调用(类似于 HTTP 请求)是无状态的(Stateless)。
这意味着,当你发给它第一句话“我叫三宸”,它处理完任务后,后台就“销毁”了这次交互的上下文。当你紧接着问“我叫什么”时,对它来说,你是一个全新的陌生人。
让我们看一个典型的“翻车”现场:
// ❌ 错误示范:没有记忆的对话
import { ChatDeepSeek } from '@langchain/deepseek';
import 'dotenv/config';
const model = new ChatDeepSeek({
model: 'deepseek-chat',
temperature: 0
});
// 第一轮对话
const res1 = await model.invoke("我叫三宸");
console.log('User: 我叫三宸');
console.log(`AI: ${res1.content}`);
// AI 回复:你好三宸...
console.log('-------------');
// 第二轮对话
const res2 = await model.invoke("我叫什么名字");
console.log('User: 我叫什么名字');
console.log(`AI: ${res2.content}`);
// AI 回复:抱歉,我不知道您的名字...
这就是无状态带来的问题。对于想要构建智能助手、客服机器人的开发者来说,这是必须跨越的第一道坎。
2. 原理:如何手动“伪造”记忆?
如果不使用任何框架,我们该从原理上怎么解决这个问题?
答案很简单,但也有些“笨重”: “滚雪球”策略。
既然模型记不住,那开发者就帮它记。在每次发起新请求时,我们不仅要发当前的问题,还要把之前所有的聊天记录打包一起发过去。
我们需要在代码中维护一个 messages 数组:
messages = [
{ role: 'user', content: '你好,我是三宸' },
{ role: 'assistant', content: '你好三宸...' }, // AI 的回复也要存
{ role: 'user', content: '你知道我是谁吗?' } // 新的问题
]
这种原生做法有两个主要痛点:
- 维护繁琐:你需要自己写代码去处理数组的追加、存储和读取。
- Token 开销爆炸:随着对话越来越长,这个“雪球”越滚越大,Token 消耗呈指数级增长,很快就会耗尽你的预算或超出模型的上下文限制。
3. 实战:LangChain 的优雅解法
LangChain 提供了一个高度封装的模块 RunnableWithMessageHistory 来自动处理上述流程。在最新的 LCEL(LangChain 表达式语言)语法中,我们可以轻松实现一个“有记性”的聊天助手。
下面是完整的实现步骤:
第一步:引入必要的“记忆组件”
除了模型本身,我们需要引入 InMemoryChatMessageHistory(用于在内存中存取历史)和 RunnableWithMessageHistory(用于自动拼接历史)。
import { ChatDeepSeek } from '@langchain/deepseek';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { RunnableWithMessageHistory } from '@langchain/core/runnables';
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
import 'dotenv/config';
第二步:构建带有“插槽”的 Prompt
这是最关键的一步。我们需要在 Prompt 模板中预留一个位置,专门用来放历史记录。这里我们用 placeholder 占位符。
const model = new ChatDeepSeek({
model: 'deepseek-chat',
temperature: 0
});
// 这里的 '{history}' 就是留给记忆的插槽
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是一个有记忆的助手'],
['placeholder', '{history}'],
['human', '{input}']
]);
第三步:注入记忆能力
我们将 Prompt 和 Model 串联后,再用 RunnableWithMessageHistory 包裹起来。它的作用是:在调用模型前,自动去查阅历史记录,填入 Prompt;在调用结束后,自动把最新的对话追加到历史记录里。
const runnable = prompt.pipe(model);
// 创建一个存储对象(这里演示用内存存储,实际生产通常对接 Redis/DB)
const messageHistory = new InMemoryChatMessageHistory();
const chain = new RunnableWithMessageHistory({
runnable,
// 获取历史记录的工厂函数
getMessageHistory: async (sessionId) => messageHistory,
inputMessagesKey: 'input', // 对应 prompt 中的用户输入变量名
historyMessagesKey: 'history', // 对应 prompt 中的占位符变量名
});
第四步:见证奇迹
现在,当我们调用 chain.invoke 时,必须传入一个 sessionId。同一个 ID 代表同一个会话上下文。
// 第一轮:自我介绍
const res1 = await chain.invoke(
{ input: '我叫三宸' },
{ configurable: { sessionId: 'user_123' } } // 指定会话 ID
);
console.log('AI:', res1.content);
// 输出:你好三宸!...
// 第二轮:询问名字(不带名字信息,纯靠记忆)
const res2 = await chain.invoke(
{ input: '我叫什么名字' },
{ configurable: { sessionId: 'user_123' } } // 只要 ID 一样,记忆就在
);
console.log('AI:', res2.content);
// 输出:你叫三宸。
成功了! 模型准确地说出了名字。LangChain 在后台默默完成了“提取历史 -> 拼接到 Prompt -> 调用模型 -> 更新历史”的一整套复杂动作。
4. 进阶思考:从 Demo 到生产环境
通过今天的实战,我们成功实现了一个具备基础记忆能力的 AI 助手。但在将代码部署到生产环境之前,我们必须正视 InMemoryChatMessageHistory 方案在实际应用中的两个致命瓶颈,并了解相应的解决方案。
4.1 痛点一:记忆的持久化(Persistence)
我们目前使用的 InMemoryChatMessageHistory 是将数据保存在服务器内存中的。这意味着一旦服务重启或程序崩溃,所有的对话记录都会瞬间清零。
- 生产环境解法:在
RunnableWithMessageHistory的getMessageHistory工厂函数中,我们不应该返回内存对象,而应该对接数据库。 - LangChain 生态支持多种外部存储,如 Redis、PostgreSQL 或 MongoDB。通过
sessionId从数据库中加载历史记录,对话结束后再将更新写回数据库,从而实现数据的持久化存储。
4.2 痛点二:Token 的“滚雪球”效应
正如我们在原理部分提到的,随着对话轮数的增加,历史记录列表会像“滚雪球”一样越来越大。
-
问题:如果用户聊了一万句,将这数万 Token 的文本一次性发给模型,不仅会产生高昂的 API 费用,更会直接超出模型支持的最大上下文窗口(Context Window),导致报错。
-
优化策略:LangChain 提供了多种记忆管理策略来解决这个问题,而不仅仅是简单的“全部追加”:
- 窗口记忆 (Window Memory) :只保留最近的 N 轮对话(例如最近 10 句),早期的对话会被自动截断丢弃。这适用于对上下文连续性要求不高的闲聊场景。
- 摘要记忆 (Summary Memory) :这是更高级的做法。当历史记录过长时,后台会调用一个 LLM 专门将旧的对话“压缩”成一段精简的摘要(例如:“用户之前介绍了自己叫三宸,喜欢喝AD钙…”),然后将这段摘要作为历史上下文传给主模型。这样既保留了核心信息,又极大节省了 Token。
总结
LLM 的本质是无状态的,而 Memory(记忆) 模块通过“上下文注入”的方式赋予了它连贯交互的能力。从简单的内存存储到复杂的数据库持久化,从全量记录到智能摘要,选择合适的记忆策略,是构建高可用 AI 应用的关键一步。
更多推荐



所有评论(0)