AI Agent 开发实战(三):记忆不是存聊天记录,搞懂这三层记忆架构才算入门
AI Agent 开发实战(三):记忆不是存聊天记录,搞懂这三层记忆架构才算入门
这是「AI Agent 开发实战」系列的第 3 篇。上一篇讲了 LLM 调用与 Prompt 工程,这一篇继续拆三大基石——记忆系统。很多人的 Agent 跑两轮就"失忆",或者上下文一长就 Token 爆炸,根因都是没搞懂记忆该怎么设计。
一、为什么 Agent 需要记忆
先说一个反直觉的事实:LLM 本身是没有记忆的。
你每次调用 /chat/completions,LLM 看到的只有你传过去的 messages 数组。它不记得上一次对话说了什么,不记得用户的偏好,不记得三步之前做了什么。
┌──────────────────────────────────────────────────┐
│ 无记忆的 Agent │
│ │
│ 用户:我叫张三 │
│ Agent:你好张三! │
│ │
│ 用户:我叫什么? │
│ Agent:抱歉,我不知道你叫什么。 │
│ │
│ 原因:第二次调用时 messages 里没有第一次的内容 │
└──────────────────────────────────────────────────┘
这不是 LLM “笨”,而是它根本看不到。
记忆系统的本质,就是在每次调用 LLM 之前,把相关信息塞进 messages 数组里。
听起来简单,但问题在于:
- 上下文窗口有限(4K ~ 200K tokens),不可能全塞进去
- 什么该塞、什么不该塞、什么时候塞,直接决定 Agent 的智商
- 不同类型的记忆,存储方式、检索方式、过期策略完全不同
二、三层记忆架构
工程上,Agent 的记忆通常分为三层:
┌──────────────────────────────────────────────────────────┐
│ Agent 记忆架构 │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 第一层:短期记忆(Working Memory) │ │
│ │ 存在 LLM 的上下文窗口里 │ │
│ │ 内容:当前对话历史、当前任务的中间状态 │ │
│ │ 生命周期:单次会话 │ │
│ │ 特点:LLM 直接可见,但容量有限 │ │
│ └──────────────────────────────────────────────────┘ │
│ ↑↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 第二层:长期记忆(Long-term Memory) │ │
│ │ 存在向量数据库 / 关系数据库中 │ │
│ │ 内容:用户偏好、历史决策、知识库 │ │
│ │ 生命周期:跨会话持久化 │ │
│ │ 特点:需要检索才能进入短期记忆 │ │
│ └──────────────────────────────────────────────────┘ │
│ ↑↓ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ 第三层:工作记忆(Scratchpad / Task Memory) │ │
│ │ 存在内存 / 缓存中 │ │
│ │ 内容:当前任务的执行计划、已完成步骤、待办事项 │ │
│ │ 生命周期:单次任务 │ │
│ │ 特点:结构化存储,不直接进 LLM 上下文 │ │
│ └──────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────┘
三层对比:
| 维度 | 短期记忆 | 长期记忆 | 工作记忆 |
|---|---|---|---|
| 存储位置 | LLM 上下文窗口 | 向量数据库 / 关系库 | 内存 / Redis |
| 存储内容 | 对话历史 | 用户画像、知识、历史决策 | 执行计划、步骤状态 |
| 生命周期 | 单次会话 | 永久(或手动删除) | 单次任务 |
| LLM 可见性 | 直接可见 | 需检索后注入 | 按需注入 |
| 容量 | 小(受 Token 限制) | 大 | 中 |
| 检索方式 | 不需要(全量在上下文) | 向量相似度 / 关键词 | 直接读取 |
三、短期记忆:上下文窗口管理
短期记忆就是 LLM 的 messages 数组。核心问题是:对话越来越长,Token 放不下怎么办?
朴素方案:全量保留
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
// 每轮对话都往里塞
while (running) {
messages.add(new UserMessage(userInput));
LlmResponse resp = llm.chat(messages, tools);
messages.add(new AssistantMessage(resp.getContent()));
}
// 问题:第 20 轮时,messages 可能已经 50K tokens,窗口爆了
方案一:滑动窗口截断
最简单粗暴——只保留最近 N 轮对话:
public class SlidingWindowMemory {
private final int maxMessages; // 保留最近多少条消息
public List<Message> manage(List<Message> messages) {
if (messages.size() <= maxMessages) {
return messages;
}
// 保留 System Prompt(第一条)+ 最近 N 条
List<Message> result = new ArrayList<>();
result.add(messages.get(0)); // System 一定保留
result.addAll(messages.subList(
messages.size() - maxMessages + 1,
messages.size()
));
return result;
}
}
优点:简单、快、可预测。
缺点:丢掉的对话可能正好是 LLM 需要的。用户第 2 轮说了"我偏好高股息股票",到第 10 轮 Agent 就忘了。
方案二:摘要压缩
用 LLM 把旧对话总结成一段摘要,替换原始消息:
public class SummaryMemory {
private String summary = "";
private final int summarizeThreshold = 10; // 超过 10 轮触发摘要
public List<Message> manage(List<Message> messages) {
if (messages.size() <= summarizeThreshold) {
return messages;
}
// 把旧消息交给 LLM 总结
List<Message> oldMessages = messages.subList(1, messages.size() - 5);
String newSummary = llm.chat(List.of(
new SystemMessage("请将以下对话总结为关键信息,保留用户偏好、重要决策和未完成的事项。"),
new UserMessage(formatMessages(oldMessages))
));
// 用摘要替换旧消息
summary = summary + "\n" + newSummary;
List<Message> result = new ArrayList<>();
result.add(messages.get(0)); // System
result.add(new SystemMessage("之前的对话摘要:\n" + summary));
result.addAll(messages.subList(messages.size() - 5, messages.size())); // 最近 5 轮
return result;
}
}
优点:保留了关键信息,Token 占用大幅降低。
缺点:摘要本身要调一次 LLM(有成本和延迟),且摘要可能丢失细节。
方案三:混合策略(推荐)
实际工程中,通常组合使用:
┌─────────────────────────────────────────────────┐
│ 上下文窗口(128K) │
│ │
│ System Prompt(固定 2K) │
│ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ │
│ 对话摘要(动态 1K) │
│ ████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ │
│ 最近 N 轮原文(动态 10K) │
│ ████████████████████░░░░░░░░░░░░░░░░░░░░░ │
│ │
│ 从长期记忆检索的相关信息(动态 2K) │
│ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ │
│ 当前用户输入(0.5K) │
│ ██░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
│ │
│ 预留输出空间(4K) │
└─────────────────────────────────────────────────┘
四、长期记忆:跨会话持久化
短期记忆解决的是"单次会话内"的问题。长期记忆解决的是:Agent 怎么记住跨会话的信息?
比如:
- 用户上次说"我偏好高股息央企股"——下次会话 Agent 应该记住
- Agent 上次帮用户分析过茅台——下次可以引用之前的结论
- 用户问过的问题模式——Agent 可以预判需求
存储方案选型
| 方案 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 向量数据库 | 语义记忆(用户偏好、知识) | 语义检索强 | 部署复杂 |
| 关系数据库 | 结构化记忆(用户画像、配置) | 查询精确 | 语义检索弱 |
| 键值存储 | 简单事实记忆 | 读写快 | 无法语义检索 |
| 图数据库 | 关系型记忆(实体关联) | 关联推理强 | 过度设计风险 |
工程建议:从向量数据库开始,80% 的场景够用了。
向量记忆的核心流程
写入记忆 读取记忆
──────── ────────
用户说了一句话 ┌──────┐ 新一轮对话开始
│ │ │ │
▼ │ 判断 │ ───────▶│
提取"记忆 worthy" │ 是否 │ 是否 │
的信息 │ 值得 │ 需要 │
│ │ 记住 │ 历史 │
▼ │ │ 记忆? │
Embedding └──────┘ │
向量化 │ Yes │ Yes
│ ▼ ▼
▼ 存入向量库 查询向量库
存入向量库 │
▼
Top-K 相关记忆
│
▼
注入到 messages
的 System 部分
写入:什么值得记住
不是每句话都值得存。Agent 需要判断:
public class MemoryWriter {
private final LlmClient llm;
private final VectorStore vectorStore;
public void maybeSave(String userMessage, String agentResponse) {
// 用 LLM 判断这条对话是否包含值得记住的信息
String judgment = llm.chat(List.of(
new SystemMessage("""
判断以下对话是否包含值得长期记忆的信息。
值得记忆的:用户偏好、个人事实、重要决策、任务结论。
不值得记忆的:寒暄、临时性问题、已过期信息。
只回答 YES 或 NO。
"""),
new UserMessage("用户: " + userMessage + "\n助手: " + agentResponse)
));
if (judgment.trim().toUpperCase().startsWith("YES")) {
// 提取结构化记忆
String memory = llm.chat(List.of(
new SystemMessage("将以下对话中的关键信息提取为一句简洁的记忆陈述。"),
new UserMessage("用户: " + userMessage + "\n助手: " + agentResponse)
));
// 向量化并存储
vectorStore.add(memory, Map.of(
"timestamp", Instant.now().toString(),
"type", "user_preference"
));
}
}
}
读取:怎么找到相关记忆
public class MemoryRetriever {
private final VectorStore vectorStore;
private static final int TOP_K = 5;
public String retrieve(String currentQuery) {
// 1. 把当前查询向量化
// 2. 在向量库中找 Top-K 最相似的记忆
List<MemoryItem> memories = vectorStore.search(currentQuery, TOP_K);
if (memories.isEmpty()) {
return "";
}
// 2. 拼接成上下文
StringBuilder sb = new StringBuilder("相关历史记忆:\n");
for (MemoryItem m : memories) {
sb.append("- ").append(m.getContent()).append("\n");
}
return sb.toString();
}
}
一个完整的记忆增强 Agent 循环
public class MemoryEnhancedAgent {
private final LlmClient llm;
private final MemoryWriter memoryWriter;
private final MemoryRetriever memoryRetriever;
private final ContextManager contextManager;
public String run(String userId, String userInput) {
// 1. 从长期记忆中检索相关信息
String relatedMemories = memoryRetriever.retrieve(userInput);
// 2. 组装 messages
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage(systemPrompt));
if (!relatedMemories.isEmpty()) {
messages.add(new SystemMessage(relatedMemories));
}
messages.addAll(contextManager.manage(sessionHistory));
messages.add(new UserMessage(userInput));
// 3. 调用 LLM
LlmResponse resp = llm.chat(messages, tools);
// 4. 写入长期记忆(异步,不阻塞响应)
memoryWriter.maybeSave(userInput, resp.getContent());
return resp.getContent();
}
}
五、工作记忆:任务执行的黑板
工作记忆是 Agent 在执行单个复杂任务时的"草稿纸"。
和短期记忆的区别:短期记忆是对话历史(谁说了什么),工作记忆是任务状态(做了什么、还要做什么)。
┌──────────────────────────────────────────────┐
│ 工作记忆示例 │
│ │
│ 任务目标:分析 600519 是否值得加仓 │
│ │
│ 执行计划: │
│ ✅ Step 1: 查询最近 30 天收盘价 │
│ ✅ Step 2: 计算 20 日均线和 MACD │
│ ⬜ Step 3: 搜索近期公司新闻 │
│ ⬜ Step 4: 综合分析给出建议 │
│ │
│ 中间结果: │
│ - 30 天均价: 1685.3 │
│ - 20 日均线: 拐头向上 │
│ - MACD: 金叉 │
│ │
│ 待办: │
│ - 需要确认近期是否有利空消息 │
└──────────────────────────────────────────────┘
工作记忆的工程实现
public class Scratchpad {
private String goal; // 任务目标
private List<TaskStep> plan; // 执行计划
private Map<String, Object> results; // 中间结果
private List<String> notes; // 备注
// 序列化为文本,注入到 LLM 上下文
public String toPromptText() {
StringBuilder sb = new StringBuilder();
sb.append("【当前任务】").append(goal).append("\n\n");
sb.append("【执行计划】\n");
for (TaskStep step : plan) {
sb.append(step.isDone() ? "✅" : "⬜")
.append(" Step ").append(step.getIndex())
.append(": ").append(step.getDescription()).append("\n");
}
if (!results.isEmpty()) {
sb.append("\n【中间结果】\n");
results.forEach((k, v) -> sb.append("- ").append(k).append(": ").append(v).append("\n"));
}
if (!notes.isEmpty()) {
sb.append("\n【备注】\n");
notes.forEach(n -> sb.append("- ").append(n).append("\n"));
}
return sb.toString();
}
// 更新步骤状态
public void markStepDone(int index, Object result) {
plan.get(index).setDone(true);
results.put(plan.get(index).getDescription(), result);
}
}
关键点:工作记忆不是全量塞进 LLM 上下文的。聪明的做法是只注入"当前步骤 +/- 2 步"的上下文,而不是整个计划。这样既能让 LLM 知道自己在哪一步,又不浪费 Token。
六、记忆的遗忘策略
记忆不是越多越好。过期的、矛盾的、低价值的信息如果不清理,反而会让 Agent 越来越蠢。
| 遗忘策略 | 触发条件 | 实现 |
|---|---|---|
| TTL 过期 | 超过设定时间 | 每条记忆带 timestamp,定期清理 |
| 重要性衰减 | 长时间未被检索命中 | 检索时降权,长期不命中则删除 |
| 矛盾覆盖 | 新记忆与旧记忆矛盾 | 用 LLM 判断,保留新的,删除旧的 |
| 容量限制 | 超过最大条数 | 按 importance score 淘汰最低的 |
| 用户显式删除 | 用户说"忘掉这个" | 按关键词或时间范围删除 |
public class MemoryJanitor {
private final VectorStore vectorStore;
private static final int MAX_MEMORIES = 1000;
private static final long TTL_DAYS = 90;
// 定期清理
public void cleanup() {
// 1. 删除过期记忆
long cutoff = Instant.now().minus(TTL_DAYS, ChronoUnit.DAYS).toEpochMilli();
vectorStore.deleteByMetadata("timestamp", cutoff);
// 2. 如果还是太多,按重要性淘汰
long count = vectorStore.count();
if (count > MAX_MEMORIES) {
// 按检索命中次数排序,淘汰最少的
List<MemoryItem> all = vectorStore.findAll();
all.sort(Comparator.comparingInt(MemoryItem::getHitCount));
int toDelete = (int) (count - MAX_MEMORIES);
for (int i = 0; i < toDelete; i++) {
vectorStore.delete(all.get(i).getId());
}
}
}
}
七、三种记忆的协同流程
把三层记忆拼在一起,一次完整的 Agent 调用流程:
用户输入
│
▼
┌─────────────────────────────────────────┐
│ 1. 检索长期记忆 │
│ 用用户输入查询向量库 │
│ 拿到 Top-K 相关历史记忆 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 2. 读取工作记忆 │
│ 当前任务计划、已完成步骤、中间结果 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 3. 组装上下文 │
│ System Prompt │
│ + 长期记忆摘要 │
│ + 工作记忆状态 │
│ + 短期记忆(最近 N 轮对话) │
│ + 当前用户输入 │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 4. 调用 LLM │
└──────────────────┬──────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ 5. 更新记忆 │
│ 短期:追加到对话历史 │
│ 工作:更新步骤状态和中间结果 │
│ 长期:异步判断是否值得持久化 │
└─────────────────────────────────────────┘
八、常见踩坑
坑 1:把所有对话原样存向量库
问题:每轮对话都 Embedding 存库,结果向量库里全是"你好""谢谢"这种垃圾数据,检索时噪声巨大。
解法:写入前先过一道 LLM 判断(见上面的 MemoryWriter),只存"记忆 worthy"的信息。
坑 2:检索 Top-K 固定不变
问题:不管什么问题都返回 Top-5,简单问题信息过多(干扰 LLM),复杂问题信息过少(不够推理)。
解法:按相似度分数动态调整。分数 > 0.9 的全要,0.7 ~ 0.9 的最多取 3 条,< 0.7 的不要。
坑 3:记忆只存不删
问题:用户三个月前说"我看好茅台",现在改主意了说"茅台太高了要减仓",但旧记忆还在,Agent 检索到两条矛盾记忆,行为混乱。
解法:写入新记忆时,用 LLM 检查是否有矛盾旧记忆,有则标记旧记忆为"已失效"或直接删除。
坑 4:工作记忆全量注入
问题:任务执行到第 20 步,把前 19 步的所有中间结果全塞进上下文,Token 爆炸。
解法:只注入"当前步骤 + 相关的中间结果"。哪些相关?按步骤间的依赖关系决定。
九、小结
记忆系统的核心要点:
- 三层架构:短期记忆(对话历史)+ 长期记忆(向量库)+ 工作记忆(任务黑板)
- 短期记忆靠管理:滑动窗口截断 + 摘要压缩 + 混合策略
- 长期记忆靠检索:写入要过滤(不是什么都存),读取要排序(Top-K + 分数阈值)
- 工作记忆靠结构化:执行计划 + 步骤状态 + 中间结果,按需注入而非全量
- 遗忘比记忆更重要:TTL 过期 + 重要性衰减 + 矛盾覆盖 + 容量淘汰
下一篇讲三大基石的最后一块——工具调用。Agent 怎么知道有哪些工具可用、怎么让 LLM 选对工具、怎么处理工具执行失败,都会展开讲。
这是「AI Agent 开发实战」系列第 3 篇,后续会持续更新,欢迎关注。如有错误或想法,欢迎评论区交流。
更多推荐

所有评论(0)