Memory模块深度解析(面试重点)
Memory是LLM突破无状态限制的机制,通过messages数组保留对话历史。基础实现导致token消耗随轮次增长,工程上采用滑动窗口或摘要优化。生产级方案通过向量检索或结构化实体记忆实现长期记忆,Reasonix采用文件系统与MEMORY.md索引管理记忆。
Memory 模块深度解析(面试向)
Memory 的本质是让大模型记住“刚才说了什么”的能力。但这一机制远非表面上那样简单,其中蕴含着工程上的取舍与智慧。
一、什么是 Memory?—— 一句话回答
Memory 是一种让 LLM 突破无状态限制的机制,使模型能够在长对话中保持上下文连贯性。这是其核心功能。
二、为什么需要 Memory?—— 从 HTTP 类比讲起
2.1 LLM 的无状态本质
LLM 本质上是一个纯函数:给定输入,产生输出,之后不保留任何调用者的记忆。这与 HTTP 协议类似,每次请求独立,服务器不会记住用户身份。
2.2 HTTP 如何解决无状态?→ Cookie / Authorization Header
HTTP 的解决方案是在请求头中加入 Cookie 或 Authorization,使服务器能够关联当前请求与之前的操作。LLM 的 Memory 采用相同思路——每次请求时将历史对话完整放入 messages 数组。
2.3 项目代码中的直接体现
参考 memory-test/history-test.mjs 第 25-27 行:

// 核心:把历史消息拼进新一轮请求
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);
这是 Memory 最原始、最纯粹的形态:将之前的对话记录原封不动地拼接进下一次请求的 messages 数组。
三、Memory 的三个层次(面试框架)
为清晰阐述 Memory,可采用递进结构:
Level 1: Messages 数组(最基础)
↓ 问题:context 越来越长,token 消耗爆炸
Level 2: 截断 / 滑动窗口 / 摘要(工程优化)
↓ 问题:丢失长期记忆,无法跨会话
Level 3: 持久化 + 检索(RAG)+ 结构化记忆(生产级)
四、Level 1:Messages 数组 —— 最基础的 Memory
4.1 原理
// 伪代码:每次请求时,消息数组越来越长
const messages = [
new SystemMessage("你是一个友好幽默的做菜助手..."),
new HumanMessage("你今天吃的什么?"), // 第 1 轮
new AIMessage("我今天吃了红烧肉..."), // 第 1 轮回复
new HumanMessage("教我做"), // 第 2 轮
new AIMessage("好的,首先准备五花肉..."), // 第 2 轮回复
new HumanMessage("有素食版本吗?"), // 第 3 轮 ← 当前问题
];
// 这一整坨全部发给 LLM
4.2 LangChain 中的实现
| 类 | 位置 | 作用 |
|---|---|---|
BaseChatMessageHistory |
@langchain/core/chat_history |
抽象基类,定义 addMessage() / getMessages() |
InMemoryChatMessageHistory |
同上 | 最简实现:this.messages = [] 纯内存数组 |
memory-test/history-test.mjs 中使用了 InMemoryChatMessageHistory:
import { InMemoryChatMessageHistory } from '@langchain/core/chat_history';
const history = new InMemoryChatMessageHistory();
await history.addUserMessage(userMessage1); // 写入
const messages1 = [systemMessage, ...(await history.getMessages())]; // 读出
await history.addMessages([response1]); // 写入 AI 回复
4.3 Messages 数组的问题
- Token 消耗线性增长:每轮对话都追加,10 轮后 context 可能已上万 token
- 超出上下文窗口:GPT-4 128K、Claude 200K 也有上限,超限会报错或被截断
- 进程重启后丢失:
InMemory存储在内存中,服务重启记忆清空 - 无优先级区分:远古对话与最近对话权重相同
五、Level 2:截断 / 滑动窗口 / 摘要
5.1 滑动窗口(Buffer Window)—— LRU 思想
只保留最近 K 轮对话,类似于 LRU Cache。
messages.slice(-k * 2) ← 每轮对话包含 2 条消息(Human + AI)
LangChain 实现:BufferWindowMemory(@langchain/classic/memory/buffer_window_memory.js)
// 核心逻辑就一行
loadMemoryVariables() {
return messages.slice(-this.k * 2);
}
类比于力扣的 LRU Cache 题——淘汰最久未使用的,保留最近使用的。
5.2 摘要 Memory(Summary Memory)
当历史过长时,调用 LLM 将旧对话总结为一段话:
"之前用户询问了红烧肉的做法,我给出了详细步骤。用户又询问素食替代..."
并将这段摘要替换原始消息塞入 context。
LangChain 实现:ConversationSummaryMemory(@langchain/classic/memory/summary.js)
// 核心:每次保存后调用 LLM 重新生成摘要
async sa veContext(inputValues, outputValues) {
await super.sa veContext(inputValues, outputValues);
this.summary = await this.predictNewSummary(旧摘要, 新消息);
}
5.3 混合方案(Summary Buffer)—— 工业级常用
保留:最近 N 条原始消息 + 更早对话的 LLM 摘要
这样既保留“细节”(最近对话),又保留“大局”(摘要内容)
LangChain 实现:ConversationSummaryBufferMemory,通过 maxTokenLimit 控制何时触发摘要。
5.4 Cursor 等 IDE 的做法
根据 readme.md 笔记:
- 持续追踪 token 开销
- 自动触发总结(/compact)
- 手动清空(/clear)
六、Level 3:持久化 + 检索 + 结构化记忆
6.1 向量检索 Memory(RAG 模式)
思路:将所有历史对话 embed 为向量 → 存入向量数据库
每次新问题时,检索最相关的历史片段 → 注入 context
无需将全部历史都塞入 context!
LangChain 实现:VectorStoreRetrieverMemory(@langchain/classic/memory/vector_store.js)
// 存:将对话 embed 后写入向量库
async sa veContext(inputValues, outputValues) {
const doc = new Document({ pageContent: `${input} → ${output}` });
await this.vectorStore.addDocuments([doc]);
}
// 取:使用当前问题做相似度检索
async loadMemoryVariables(inputValues) {
const docs = await this.vectorStore.similaritySearch(input, k);
return docs.map(d => d.pageContent).join('\n');
}
6.2 结构化实体 Memory(Entity Memory)
从对话中提取命名实体(人名、菜名、偏好等),为每个实体维护独立的摘要。
LangChain 实现:EntityMemory,内部包含三层 LLM 调用:
- 抽取:从对话中提取实体
- 总结:为每个实体生成/更新摘要
- 注入:将相关实体摘要注入 system prompt
七、Reasonix Code 的 Memory 实现(文件系统 + MEMORY.md 索引)
7.1 架构总览
┌──────────────────────────────────────────────┐
│每次 /new 或启动时 │
│ │
│读取 MEMORY.md(索引文件) │
│ ↓ │
│根据 scope 筛选: │
│ global → ~/.reasonix/memories/*.md │
│ project → .reasonix/memories/*.md │
│ ↓ │
│high priority 内容注入 HIGH PRIORITY 区块 │
│普通 priority 内容注入系统 prompt │
│ ↓ │
│拼入 System Prompt,模型获得记忆 │
└──────────────────────────────────────────────┘
7.2 数据模型
每条 Memory 是一个 Markdown 文件,包含元信息:
# 文件路径:.reasonix/memories/项目偏好.md
type: project # user | feedback | project | reference
scope: project # global | project
priority: high # low | medium | high
expires: project_end # 可选:项目结束时自动清除
description: ≤150字符的一句话摘要(显示在 MEMORY.md 索引中)
content: Markdown 正文(具体规则、偏好、上下文)
7.3 核心 API:三个 Tool
| Tool | 功能 | 类比 |
|---|---|---|
remember |
创建/更新一条记忆,写入 .md 文件 + 更新 MEMORY.md 索引 |
git add + git commit |
recall_memory |
读取某条记忆的完整正文 | cat 详细内容 |
forget |
删除记忆文件 + 从 MEMORY.md 移除条目 |
git rm |
7.4 为何使用文件系统而非数据库?
面试时可这样回答:
- 简单可靠:Markdown 文件人类可读、Git 友好、无需额外基础设施
- 与项目绑定:
.reasonix/目录随项目迁移,clone 即用 - 系统 prompt 注入天然适配:Markdown 文本可直接拼入 prompt,零转换成本
- 优先级分层加载:high priority 强制注入(用于关键约束),普通 priority 按需加载
- MEMORY.md 作为二级索引:系统 prompt 中仅放一行摘要,正文通过
recall_memory按需读取 → 节省 context token
7.5 Memory 类型的设计哲学
| Type | 存储内容 | 面试话术 |
|---|---|---|
user |
用户角色、技能偏好、习惯 | "个性化层,让 Agent 适配不同用户" |
feedback |
用户纠正过的错误做法 | "纠错层,避免重复犯同样错误" |
project |
项目架构决策、约定 | "项目上下文层,新人 clone 后立即获得项目知识" |
reference |
外部系统指针 | "集成层,记住 CI 地址、文档链接等外部资源" |
八、面试回答模板(3 分钟版本)
开场(30 秒)
展开:三个层次(1 分 30 秒)
收官:Reasonix 的实践(1 分钟)
九、可能被追问的深度问题
Q1: "滑动窗口的 K 如何选取?"
Q2: "摘要 Memory 有哪些缺陷?"
Q3: "向量检索 Memory 与 RAG 有何区别?"
Q4: "为何不选用数据库存储 Memory?"
十、代码走读速记(面试前快速过一遍)
| 文件 | 重点查看内容 |
|---|---|
readme.md 第 1-17 行 |
Memory 的宏观定位:LLM + Tool + RAG + Memory 四件套 |
readme.md 第 19-28 行 |
无状态 → messages 数组的推导 |
readme.md 第 32-36 行 |
三种解决方案:截断、摘要、检索 |
memory-test/history-test.mjs 第 25-27 行 |
Memory 的最简实现:[systemMsg, ...history] |
memory-test/history-test.mjs 第 14 行 |
InMemoryChatMessageHistory 实例化 |
