Memory 模块深度解析(面试向)

Memory 模块深度解析(面试向)


一、什么是 Memory?------ 一句话回答

Memory 是让无状态 LLM 拥有"上下文持续记忆"能力的机制。

LLM 本身是无状态的(stateless)------每次 API 调用都是独立的,不记得上一轮说了什么。Memory 通过在每次请求中注入历史信息,让 LLM "看起来"记住了对话。


二、为什么需要 Memory?------ 从 HTTP 类比讲起

2.1 LLM 的无状态本质

lua 复制代码
LLM 就像纯函数:input → output,不保留任何状态。
这和 HTTP 协议一模一样------每次请求独立,服务器不记得你。
sql 复制代码
HTTP 请求头里带上 Cookie 或 Authorization,服务器就能"认出"用户。
LLM 的 Memory 就是同一个思路------每次请求时,把历史对话塞进 messages 数组。

2.3 项目代码中的直接体现

memory-test/history-test.mjs 第 25-27 行:

javascript 复制代码
// 核心:把历史消息拼进新一轮请求
const messages1 = [systemMessage, ...(await history.getMessages())];
const response1 = await model.invoke(messages1);

这就是 Memory 的最原始形态:把之前的对话记录塞进下一次请求的 messages 数组。


三、Memory 的三个层次(面试框架)

可以用一个清晰的递进结构来讲述:

yaml 复制代码
Level 1: Messages 数组(最基础)
    ↓ 问题:context 越来越长,token 消耗爆炸
Level 2: 截断 / 滑动窗口 / 摘要(工程优化)
    ↓ 问题:丢失了长期记忆,无法跨会话
Level 3: 持久化 + 检索(RAG)+ 结构化记忆(生产级)

四、Level 1:Messages 数组 ------ 最基础的 Memory

4.1 原理

typescript 复制代码
// 伪代码:每次请求时,消息数组越来越长
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

javascript 复制代码
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 数组的问题

  1. Token 消耗线性增长:每轮对话都追加,10 轮后 context 可能已经上万 token
  2. 超出上下文窗口:GPT-4 128K、Claude 200K 也有上限,超了就报错或截断
  3. 进程重启就丢失InMemory 存内存,服务重启记忆清空
  4. 没有优先级:远古对话和最近对话同等重要

五、Level 2:截断 / 滑动窗口 / 摘要

5.1 滑动窗口(Buffer Window)------ LRU 思想

python 复制代码
只保留最近 K 轮对话,类似 LRU Cache。
messages.slice(-k * 2)  ← 每轮对话占 2 条消息(Human + AI)

LangChain 实现:BufferWindowMemory@langchain/classic/memory/buffer_window_memory.js

javascript 复制代码
// 核心逻辑就一行
loadMemoryVariables() {
  return messages.slice(-this.k * 2);
}

类比:力扣 LRU Cache 题 ------ 淘汰最久未使用的,保留最近使用的。

5.2 摘要 Memory(Summary Memory)

arduino 复制代码
当历史太长时,调用 LLM 把旧对话总结成一段话:
"之前用户问了红烧肉的做法,我给出了详细步骤。用户又问素食替代..."
然后将这段摘要代替原始消息塞入 context。

LangChain 实现:ConversationSummaryMemory@langchain/classic/memory/summary.js

javascript 复制代码
// 核心:每次 save 后调用 LLM 重新生成摘要
async saveContext(inputValues, outputValues) {
  await super.saveContext(inputValues, outputValues);
  this.summary = await this.predictNewSummary(旧摘要, 新消息);
}

5.3 混合方案(Summary Buffer)------ 工业级常用

arduino 复制代码
保留:最近 N 条原始消息 + 更早对话的 LLM 摘要
这样既有"细节"(最近的),又有"大局"(摘要的)

LangChain 实现:ConversationSummaryBufferMemory,通过 maxTokenLimit 控制何时触发摘要。

5.4 Cursor 等 IDE 的做法

readme.md 笔记可知:

  • 持续追踪 token 开销
  • 自动触发总结(/compact)
  • 手动清空(/clear)

六、Level 3:持久化 + 检索 + 结构化记忆

6.1 向量检索 Memory(RAG 模式)

markdown 复制代码
思路:把所有历史对话 embed 成向量 → 存入向量数据库
     每次新问题时,检索最相关的历史片段 → 注入 context
     不需要把全部历史都塞进去!

LangChain 实现:VectorStoreRetrieverMemory@langchain/classic/memory/vector_store.js

javascript 复制代码
// 存:把对话 embed 后写入向量库
async saveContext(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 调用:

  1. 抽取:从对话中提取实体
  2. 总结:为每个实体生成/更新摘要
  3. 注入:把相关实体摘要注入 system prompt

七、Reasonix Code 的 Memory 实现(文件系统 + MEMORY.md 索引)

7.1 架构总览

javascript 复制代码
┌──────────────────────────────────────────────┐
│              每次 /new 或启动时               │
│                                              │
│  读取 MEMORY.md(索引文件)                    │
│     ↓                                        │
│  根据 scope 筛选:                             │
│    • global → ~/.reasonix/memories/*.md      │
│    • project → .reasonix/memories/*.md       │
│     ↓                                        │
│  high priority 内容注入 HIGH PRIORITY 区块     │
│  普通 priority 内容注入系统 prompt             │
│     ↓                                        │
│  拼入 System Prompt,模型获得记忆              │
└──────────────────────────────────────────────┘

7.2 数据模型

每条 Memory 是一个 Markdown 文件,包含元信息:

yaml 复制代码
# 文件路径:.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 为什么用文件系统而不是数据库?

面试可以这样回答:

  1. 简单可靠:Markdown 文件人类可读、Git 友好、不需要额外基础设施
  2. 与项目绑定.reasonix/ 目录随项目走,clone 即用
  3. 系统 prompt 注入天然适配:Markdown 文本直接拼入 prompt,零转换成本
  4. 优先级分层加载:high priority 强制注入(用于关键约束),普通 priority 按需加载
  5. MEMORY.md 作为二级索引 :系统 prompt 中只放一行摘要,正文用 recall_memory 按需读取 → 节省 context token

7.5 Memory 类型的设计哲学

Type 存什么 面试话术
user 用户角色、技能偏好、习惯 "个性化层,让 Agent 适配不同用户"
feedback 用户纠正过的错误做法 "纠错层,避免重复犯同样的错"
project 项目架构决策、约定 "项目上下文层,新人 clone 后立即获得项目知识"
reference 外部系统指针 "集成层,记住 CI 地址、文档链接等外部资源"

八、面试回答模板(3 分钟版本)

开场(30 秒)

"Memory 是让无状态大模型拥有上下文持续记忆能力的机制。核心思想很简单:LLM 每次调用是 stateless 的,我们通过在每次请求的 messages 数组中注入历史信息,让它'记住'之前的对话。这和 HTTP 用 Cookie 解决无状态问题是同一个思路。"

展开:三个层次(1 分 30 秒)

"我理解 Memory 有三个递进层次:

第一层是最基础的 Messages 数组 ------把历史对话直接拼进下一次请求。我们在 memory-test/history-test.mjs 里用的 InMemoryChatMessageHistory 就是这个模式的最简实现。但它的问题是 token 线性增长,最终超出上下文窗口。

第二层是工程优化 ------滑动窗口截断(类似 LRU Cache),或者用 LLM 把旧对话总结成摘要再注入。LangChain 提供了 BufferWindowMemoryConversationSummaryMemory、甚至混合的 SummaryBufferMemory

第三层是持久化 + 检索 ------把历史对话 embed 成向量存进向量数据库,每次用语义检索取回最相关的片段(RAG 模式),以及结构化实体记忆(EntityMemory)。"

收官:Reasonix 的实践(1 分钟)

"在 Reasonix Code 中,Memory 用的是文件系统 + Markdown 的方案,非常务实:

  • 记忆存为 .md 文件,MEMORY.md 作为索引文件
  • 启动时根据 scope 加载,注入系统 prompt
  • 分层优先级(high/medium/low),关键约束强制注入
  • 三个工具:remember 写入、recall_memory 读取、forget 删除

这个设计的亮点是:文件系统零依赖、Git 友好、Markdown 可直接拼入 prompt 无需格式转换。"


九、可能被追问的深度问题

Q1: "滑动窗口的 K 怎么选?"

取决于模型上下文窗口和每轮对话的平均 token 数。先用 tokenizer 估算,留出 30% 给 system prompt 和输出。LangChain 的 ConversationTokenBufferMemory 直接按 token 数而非消息数来截断,更精准。

Q2: "摘要 Memory 有什么坑?"

摘要会丢失细节。比如用户说"我讨厌香菜",摘要可能变成"用户有饮食偏好",具体偏好的关键信息丢了。解决方案是混合模式(SummaryBuffer)------最近的消息保留原文,旧的才摘要。

Q3: "向量检索 Memory 和 RAG 有什么区别?"

本质一样。向量检索 Memory 就是 RAG 在对话记忆场景的特化------把历史对话作为文档库,当前问题作为 query。区别在于 Memory 还需要写操作(saveContext),RAG 通常只读。

Q4: "为什么不用数据库存 Memory?"

对于 Agent 场景,文件系统有几个优势:

  1. 零运维------不需要起数据库
  2. 随项目 Git 版本化------git clone 即获得项目记忆
  3. 人类可直接编辑------紧急情况手动改 .md 文件就行
  4. 减少格式转换------Markdown 直接拼入 prompt

但大规模多用户场景下,数据库 + 向量检索是更好的选择。


十、代码走读速记(面试前快速过一遍)

文件 重点看什么
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 实例化

十一、一句话总结(电梯演讲)

Memory 就是把"对话历史"变成"系统 prompt 的一部分",让无状态的 LLM 看起来有记忆。从最简单的数组拼接,到滑动窗口截断,到向量语义检索,再到文件系统持久化------不同方案解决的核心矛盾始终是同一个:如何在有限的 token 预算内,给模型提供最有价值的上下文。

相关推荐
MacroZheng2 小时前
Claude Code官方桌面端正式发布,夯爆了!
java·人工智能·后端
IT_陈寒2 小时前
React的useEffect依赖数组把我坑惨了,真相其实很简单
前端·人工智能·后端
Kapaseker2 小时前
什么?Stack Overflow 给 AI 做了个 Stack Overflow
人工智能
aneasystone本尊2 小时前
让小龙虾自己写手册:Skill Workshop
人工智能
火山引擎开发者社区3 小时前
一篇看懂 VKE AI Profiling:AI 应用性能分析优化实战
人工智能
IT乐手3 小时前
马斯克的AI模型Grok,竟然帮美军炸了伊朗?!
人工智能
AI袋鼠帝3 小时前
斥资500元/上亿Token,深度横评4个顶尖模型的真实排名~
人工智能
大刚测试开发实战12 小时前
TestHub V0.2.2版本发布,附更新指南
人工智能
冬奇Lab14 小时前
Agent 系列(21):Harness 测试工程——45 个测试怎么设计,以及它发现了什么 bug
人工智能·llm·agent