引言
你有没有遇到过这样的尴尬?
向 AI 提问:"我叫张三",下一秒再问"我叫什么名字?"------它居然说:"抱歉,我不记得了。" 😅
别怪模型笨,这是所有 LLM 的"先天缺陷":无状态调用。
但今天,我们用 LangChain Memory 给它装上"大脑",实现真正意义上的多轮对话记忆!
本文将带你从零构建一个会"记事"的智能助手,并深入剖析底层原理、性能瓶颈与生产级优化方案。
🧠 一、为什么你的 AI 总是"金鱼脑"?
1.1 大模型的"失忆症"真相
我们知道,大语言模型(LLM)本质上是一个"黑箱函数":
ts
response = model(prompt)
每次调用都是独立的 HTTP 请求,没有任何上下文保留机制 ------ 就像每次见面都重新认识一遍。
举个例子:
js
// 第一次对话
await model.invoke("我叫陈昊,喜欢喝白兰地");
// 输出:很高兴认识你,陈昊!看来你喜欢喝白兰地呢~
console.log('------ 分割线 ------');
// 第二次对话
await model.invoke("我叫什么名字?");
// 输出:呃......不太清楚,能告诉我吗?
👉 结果令人崩溃:前脚刚自我介绍,后脚就忘了!
这在实际项目中是不可接受的。无论是客服机器人、教育助手还是个性化推荐系统,都需要记住用户的历史行为和偏好。
1.2 解决思路:把"记忆"塞进 Prompt
既然模型不会自己记,那就我们来帮它记!
核心思想非常简单:
✅ 每次请求前,把之前的对话历史拼接到当前 prompt 中
✅ 让模型"看到"完整的聊天记录,从而做出连贯回应
这就像是给模型戴上了一副"记忆眼镜"。
而 LangChain 的 Memory 模块,正是对这一过程的高度封装与自动化。
⚙️ 二、LangChain Memory 核心原理拆解
LangChain 提供了一套优雅的 API,让我们可以用几行代码实现"有记忆"的对话系统。
先看最终效果:
bash
User: 我叫陈昊,喜欢喝白兰地
AI: 你好,陈昊!白兰地可是很有品味的选择哦~
User: 我喜欢喝什么酒?
AI: 你说你喜欢喝白兰地呀~是不是准备开一瓶庆祝一下?😉
✅ 成功记住用户名字和饮酒偏好!
下面我们就一步步实现这个功能。
2.1 核心组件一览
| 组件 | 作用 |
|---|---|
ChatMessageHistory |
存储对话历史(内存/文件/数据库) |
ChatPromptTemplate |
定义提示词模板,预留 {history} 占位符 |
RunnableWithMessageHistory |
自动注入历史 + 管理会话生命周期 |
sessionId |
区分不同用户的会话,避免串台 |
2.2 完整代码实战
js
// 文件名:memory-demo.js
import { ChatDeepSeek } from "@langchain/deepseek";
import 'dotenv/config';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { RunnableWithMessageHistory } from "@langchain/core/runnables";
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
// 1. 初始化模型
const model = new ChatDeepSeek({
model: 'deepseek-reasoner',
temperature: 0.3,
});
// 2. 构建带历史的 Prompt 模板
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是一个温暖且有记忆的助手,请根据对话历史回答问题'],
['placeholder', '{history}'], // ← 历史消息自动插入这里
['human', '{input}'] // ← 当前输入
]);
// 3. 创建可运行链(含调试输出)
const runnable = prompt
.pipe(input => {
console.log('\n🔍 最终发送给模型的完整上下文:');
console.log(JSON.stringify(input, null, 2));
return input;
})
.pipe(model);
// 4. 初始化内存历史存储
const messageHistory = new InMemoryChatMessageHistory();
// 5. 创建带记忆的链
const chain = new RunnableWithMessageHistory({
runnable,
getMessageHistory: () => messageHistory,
inputMessagesKey: 'input',
historyMessagesKey: 'history'
});
// 6. 开始对话测试
async function testConversation() {
// 第一轮:告知信息
const res1 = await chain.invoke(
{ input: "我叫陈昊,喜欢喝白兰地" },
{ configurable: { sessionId: "user_001" } }
);
console.log("🤖 回应1:", res1.content);
// 第二轮:提问历史
const res2 = await chain.invoke(
{ input: "我叫什么名字?我喜欢喝什么酒?" },
{ configurable: { sessionId: "user_001" } }
);
console.log("🤖 回应2:", res2.content);
}
testConversation();
📌 运行结果示例:
bash
🤖 回应1: 你好,陈昊!听说你喜欢喝白兰地,真是个有品位的人呢~
🤖 回应2: 你叫陈昊,而且你说你喜欢喝白兰地哦~要不要来点搭配小吃?
🎉 成功!模型不仅记住了名字,还能综合多个信息进行推理回答!
2.3 关键机制图解
text
+---------------------+
| 用户新输入 |
+----------+----------+
|
+-----------------v------------------+
| ChatPromptTemplate |
| |
| System: 你是有记忆的助手 |
| History: [之前的所有对话] ←───────+←─ 从 messageHistory 读取
| Human: 我叫什么名字? |
+-----------------+------------------+
|
调用模型 → LLM
|
返回响应 ←────+
|
+-----------------v------------------+
| 自动保存本次交互 |
| user: 我叫什么名字? |
| assistant: 你叫陈昊... |
| 写入 InMemoryChatMessageHistory |
+------------------------------------+
整个流程全自动闭环,开发者只需关注业务逻辑。
⚠️ 三、真实场景下的三大挑战与破解之道
虽然上面的例子很美好,但在生产环境中你会立刻面临三个"灵魂拷问":
❌ 挑战1:Token "滚雪球"爆炸增长!
随着对话轮数增加,历史消息越积越多,导致:
- 单次请求 Token 数飙升
- 成本翻倍 💸
- 响应变慢 ⏳
- 可能超出模型最大长度限制(如 8192)
✅ 解法:选择合适的 Memory 类型
| Memory 类型 | 特点 | 适用场景 |
|---|---|---|
BufferWindowMemory |
只保留最近 N 轮 | 通用对话、短程记忆 |
ConversationSummaryMemory |
自动生成一句话总结 | 长周期对话、节省 Token |
EntityMemory |
提取关键实体(人名/偏好) | 推荐系统、CRM 助手 |
示例:使用窗口记忆(保留最近3轮)
js
import { BufferWindowMemory } from "langchain/memory";
const memory = new BufferWindowMemory({
k: 3,
memoryKey: "history"
});
📌 推荐组合:短期细节靠窗口 + 长期特征靠总结
❌ 挑战2:重启服务后历史全丢?!
InMemoryChatMessageHistory 是临时存储,服务一重启,记忆清零。
✅ 解法:持久化到数据库或文件
方案一:本地文件存储(轻量级)
js
import { FileChatMessageHistory } from "@langchain/core/chat_history";
const getMessageHistory = async (sessionId) => {
return new FileChatMessageHistory({
filePath: `./history/${sessionId}.json`
});
};
方案二:Redis / MongoDB(高并发推荐)
bash
npm install @langchain/redis
js
import { RedisChatMessageHistory } from "@langchain/redis";
const getMessageHistory = async (sessionId) => {
return new RedisChatMessageHistory({
sessionId,
client: redisClient // 已连接的 Redis 客户端
});
};
💡 生产环境强烈建议使用 Redis:高性能、支持过期策略、分布式部署无忧。
❌ 挑战3:多人同时聊天会不会串消息?
当然会!如果所有用户共用同一个 messageHistory,就会出现 A 用户看到 B 用户的对话。
✅ 解法:用 sessionId 隔离会话
js
await chain.invoke(
{ input: "我饿了" },
{ configurable: { sessionId: "user_123" } } // 每个用户唯一 ID
)
await chain.invoke(
{ input: "我饿了" },
{ configurable: { sessionId: "user_456" } } // 不同用户,不同历史
)
✅ 安全隔离,互不干扰!
🛠️ 四、Memory 的典型应用场景(附案例灵感)
| 场景 | 如何使用 Memory |
|---|---|
| 客服机器人 | 记住订单号、投诉进度、用户情绪变化 |
| 教育辅导 | 追踪学习章节、错题记录、掌握程度 |
| 电商导购 | 记住预算、品牌偏好、尺码需求 |
| 编程助手 | 保持代码上下文、函数定义、项目结构 |
| 心理咨询 | 理解用户情绪演变、关键事件回顾 |
💡 创新玩法:结合 SummaryMemory + VectorDB,实现"长期人格记忆"------让 AI 记住你是内向还是外向、喜欢幽默还是严谨。
📘 五、高频面试题(LangChain 方向):
- LLM 为什么需要 Memory?它的本质是什么?
- Buffer vs Summary Memory 的区别?什么时候用哪种?
- 如何设计一个支持百万用户在线的记忆系统架构?
- 如何防止敏感信息被 Memory 记录?(安全考量)
📝 结语:让 AI 真正"懂你",从一次对话记忆开始
"智能不是回答问题的能力,而是理解上下文的艺术。"
LangChain Memory 虽然只是整个 LLM 应用中的一个小模块,但它却是通往拟人化交互的关键一步。
从"无状态"到"有记忆",我们不只是在写代码,更是在塑造一种新的沟通方式。
未来属于那些能让 AI 记住你、理解你、陪伴你的产品。
而现在,你已经有了打造它的钥匙。