全文导读:大模型真的"记忆力"超群吗?No!它只是个金鱼脑。本文将带你从0到1掌握LangChain记忆系统的核心原理,深入剖析内存记忆与文件持久化两种方案的实现细节,并给出生产环境下的避坑指南。读完本文,你将彻底搞懂AI记忆管理的那些事儿。
一、灵魂拷问:大模型真的记得你上一句说了啥吗?
答:它真不记得!
大模型本质上是一个无状态函数 ------你给它输入,它吐输出,每次调用都是"重新做人"。那为什么ChatGPT能记住我们的上下文呢?秘密就是------Memory(记忆系统) 。
用个生活化的比喻:
大模型就像一个健忘的大厨,每次做菜都要重新问你想吃啥。而Memory系统,就是他的随身小本本,记录着你每次点菜的历史。但这个本本也有讲究------记在脑子里(内存)、记在纸上(文件)、还是记在图书馆里(向量数据库),就各有千秋了。
二、为啥我们需要Memory?------三个业务痛点直击
在真实业务场景中,Memory管理至少解决了以下三大痛点:
| 痛点 | 说明 | 没有Memory的惨状 |
|---|---|---|
| 上下文断裂 | 用户连续提问需要关联性 | 每次都要重新自我介绍,像个失忆症患者 |
| 个性化丧失 | 需要记住用户偏好和历史 | 永远无法提供个性化服务 |
| 成本失控 | 上下文窗口有限(如200k tokens) | 每次都塞进全部历史,token费用爆炸 |
核心公式 :
Agent = LLM + Harness(Tool + RAG + Memory)
Memory是Agent大脑中不可或缺的一环,没有它,AI就是个"一次性用品"。
三、Memory的两大核心维度

四、重难点①:InMemoryChatMessageHistory------最优雅的"数组升级"
4.1 核心原理剖析
先看最基础的内存记忆实现:
typescript
javascript
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history'
import { HumanMessage, SystemMessage, AIMessage } from '@langchain/core/messages'
// 创建记忆容器------本质上就是一个封装好的数组
const history = new InMemoryChatMessageHistory()
// 系统提示词(始终保留)
const systemMessage = new SystemMessage({
content: '你是一个友好、幽默的做菜助手'
})
// 👇 第一轮对话
const userMessage = new HumanMessage({ content: '你今天吃什么' })
await history.addMessage(userMessage) // 添加用户消息
// 构造完整对话上下文 = 系统提示词 + 全部历史
const messages1 = [systemMessage, ...(await history.getMessages())]
const response1 = await model.invoke(messages1)
// ⚠️ 关键!必须把AI的回复也存进去,否则下轮对话没有上下文
await history.addMessage(response1)
4.2 设计者为什么这么写?
💡 核心设计思想 :InMemoryChatMessageHistory 就是把一个普通数组 升级成了自带CRUD能力的消息容器。
typescript
typescript
// 本质上它在内部维护了这样一个数组
class InMemoryChatMessageHistory {
private messages: BaseMessage[] = [] // 👈 核心数据结构
async addMessage(message: BaseMessage) {
this.messages.push(message) // 添加
}
async getMessages() {
return this.messages // 获取全部
}
}
设计智慧:
- 统一了消息的存储接口(addMessage/getMessages)
- 屏蔽了底层实现细节(内存/文件/数据库切换无感知)
- 支持多模态消息类型(Human/AI/System/Tool)
4.3 🚨 新手最容易犯的错
typescript
csharp
// ❌ 错误示范:只存用户消息,不存AI回复
const userMsg = new HumanMessage({ content: '你好' })
await history.addMessage(userMsg)
const response = await model.invoke([...history.getMessages()])
// 忘记存 response!下一轮对话AI完全不记得自己说过什么
// ✅ 正确示范:双向存储
await history.addMessage(userMsg)
const response = await model.invoke([...history.getMessages()])
await history.addMessage(response) // 必须存!
五、重难点②:FileSystemChatMessageHistory------让记忆"断电不丢失"
5.1 文件持久化实现
typescript
javascript
import { FileSystemChatMessageHistory } from '@langchain/community/stores/message/file_system'
import path from 'node:path'
const filePath = path.join(process.cwd(), 'chat_history.json')
const sessionId = 'user_session_001' // 👈 多用户隔离
const history = new FileSystemChatMessageHistory({
filePath, // 存储路径
sessionId, // 会话ID,不同用户互不干扰
})
// 使用方式与InMemory完全一致!
await history.addMessage(new HumanMessage('红烧肉怎么做'))
const allMessages = await history.getMessages() // 自动从文件读取
5.2 文件存储结构解密
chat_history.json 内部存储格式:
json
css
[ { "type": "human", "content": "红烧肉怎么做" }, { "type": "ai", "content": "红烧肉是一道经典的家常菜..." }]
5.3 多用户隔离原理
typescript
arduino
// 同一个文件,不同sessionId自动隔离
const historyA = new FileSystemChatMessageHistory({
filePath: './chat.json',
sessionId: 'user_001' // 用户A的会话
})
const historyB = new FileSystemChatMessageHistory({
filePath: './chat.json',
sessionId: 'user_002' // 用户B的会话
})
// 内部实现:getMessages() 时只返回匹配 sessionId 的消息
5.4 完整的存储+恢复闭环

六、重难点③:Memory管理的"三把刀"------截断策略详解
上下文窗口是有限的(GPT-4是128K,Claude是200K),无限制增长最终会撑爆窗口或耗尽token预算。以下是三种主流截断策略:
6.1 策略一:按消息数量截断
typescript
ini
// 最简单粗暴:只保留最近N条消息
const maxMessages = 4
const trimmedMessages = allMessages.slice(-maxMessages)
适用场景:消息长度均匀,对时效性要求高。
6.2 策略二:按Token数量截断(精确控制成本)
typescript
javascript
import { getEncoding } from 'js-tiktoken'
import { trimMessages } from '@langchain/core/messages'
async function countTokens(messages, enc) {
let total = 0
for (const msg of messages) {
const content = typeof msg === 'string' ? msg.content : JSON.stringify(msg.content)
total += enc.encode(content).length // 精确token计数
}
return total
}
const enc = getEncoding('cl100k_base') // OpenAI使用的编码
const trimmed = await trimMessages(allMessages, {
maxTokens: 100, // token上限
tokenCounter: async (msgs) => countTokens(msgs, enc),
strategy: 'latest' // 保留最新的消息
})
为什么用二分查找?
trimMessages内部使用二分查找算法,在保证不超token上限的前提下,尽可能多地保留消息:
text
markdown
1. 从所有消息开始
2. 如果总token > maxTokens,移除最早的一条
3. 重复直到token数符合要求
4. 返回截断后的消息列表
6.3 三种策略对比

七、面试高频考点
Q1:InMemoryChatMessageHistory和FileSystemChatMessageHistory的区别和适用场景?
| 维度 | InMemory | FileSystem |
|---|---|---|
| 存储位置 | 内存 | 磁盘文件 |
| 持久化 | ❌ 重启丢失 | ✅ 永久保存 |
| 读写速度 | 极快 | 相对慢(I/O) |
| 适用场景 | 单次会话、临时对话 | 客服系统、长期记忆 |
| 多会话隔离 | 需要手动管理 | sessionId自动隔离 |
Q2:为什么要用await history.getMessages()而不是直接读取内部数组?
答 :getMessages()是一个抽象方法,不同实现类有不同的获取逻辑:
InMemoryChatMessageHistory:直接返回内存数组FileSystemChatMessageHistory:从文件读取并反序列化- 向量数据库实现:执行相似性检索
统一使用getMessages()保证了多态性,切换存储方式无需修改业务代码。
Q3:截断时为什么推荐使用Token计数而非消息数量?
答 :因为不同消息的长度差异巨大:
"你好"≈ 1-2 tokens"请详细解释量子力学的波函数坍缩..."≈ 50+ tokens
按数量截断可能导致token超限,而按Token计数截断是精确可控的。
八、避坑指南------生产环境血泪经验
🕳️ 坑1:文件并发写入导致数据损坏
typescript
javascript
// ❌ 多请求同时写入同一文件
const history = new FileSystemChatMessageHistory({
filePath: './chat.json',
sessionId: 'global' // 所有用户共享sessionId
})
// 并发写入 → 文件损坏
// ✅ 每个用户独立sessionId
const history = new FileSystemChatMessageHistory({
filePath: './chat.json',
sessionId: `user_${userId}` // 用户隔离
})
🕳️ 坑2:忘记添加SystemMessage
typescript
csharp
// ❌ 缺少系统提示词
const messages = await history.getMessages()
// AI不知道自己的角色定位
// ✅ 始终在第一条放SystemMessage
const messages = [systemMessage, ...(await history.getMessages())]
🕳️ 坑3:Token计算使用错误的编码
不同模型的tokenizer不同:
- OpenAI系列 →
cl100k_base - Claude系列 →
claude专用tokenizer - 开源模型 → 需使用对应tokenizer
typescript
javascript
// ✅ 正确获取编码
import { getEncoding } from 'js-tiktoken'
const enc = getEncoding('cl100k_base') // OpenAI GPT-4/Turbo
九、总结一张图看懂整个架构
