发布时间:2026-07-09
标签:AI Agent|LLM|Memory|上下文工程|工程实践
系列导航
上一篇:AI Agent 工程实践(02):为什么 Rules 要分层?
下一篇:待定(Review 机制 / Benchmark / Audit Trail)
本文是 AI Agent 工程实践 系列的第 03 篇。
给 Agent 加上"记忆"那天,我以为它会变聪明。
结果三天后,它开始答非所问:记得住我上周随口提的一句玩笑,却忘了当前任务的关键约束。
我一度以为是模型不行,直到翻了它的上下文------里面塞满了几百条"记忆",真正有用的不到一成。
问题不是它记得太少,而是它什么都记,于是什么都没记住。
本文你将学到
- ✓ 为什么"给 Agent 加记忆"经常让它更笨,而不是更聪明
- ✓ 短期 / 工作 / 长期 / 归档:记忆到底该分几层,各层边界在哪
- ✓ 项目记忆 vs 用户记忆------为什么必须拆开,混在一起会怎样
- ✓ Memory 的完整生命周期:什么时候写、什么时候晋升、什么时候清理
适合阅读
- ✓ 正在给 Agent / Copilot 做"记忆"功能的开发者
- ✓ 用 Claude Code / Cursor / LangGraph 且被"上下文越来越乱"困扰的人
- ✓ 对"上下文工程(Context Engineering)"感兴趣的人
问题背景
在 系列第 02 篇 里,我讲了 Rules 怎么分层------那解决的是"规则该在什么时候出现"。这一篇讲一个更棘手的问题:记忆该记什么、记多久。
几乎所有 Agent 框架都在宣传"我们有 Memory"。教程也都在教你怎么"把历史对话存起来,下次再喂回去"。听起来很美好------Agent 有了记忆,就像人一样能积累经验。我照做了。然后遇到一连串问题:
上下文越来越臃肿。 每开一次对话,Agent 先"回忆"一大堆历史,token 消耗蹭蹭涨,响应越来越慢。
关键信息被稀释。 当前任务真正需要的那几条,淹没在几百条陈年记忆里。模型的注意力被无关信息分走,输出质量反而下降。
旧信息污染新决策。 上个项目用的是 FastAPI,这个项目用 Flask,可它"记得"FastAPI,于是不停给我推 FastAPI 的写法。
记忆之间开始打架。 我三个月前说"喜欢详细注释",上周又说"代码尽量精简"。两条都被记着,Agent 到底该听谁的?
一句话概括:加了记忆,Agent 没变聪明,反而被自己的记忆拖垮了。 这不是"有没有记忆"的问题,是"怎么管理记忆"的问题------和 Rules 分层一样,本质是规模问题。
错误尝试
第一次尝试:什么都记,全量回灌
最朴素的想法:把所有历史对话都存下来,下次对话时全部塞回上下文。"记得越多越聪明"嘛。
结果:上下文迅速膨胀到几千 token 起步。更糟的是,模型在长上下文里出现了典型的"迷失在中间"(Lost in the Middle)现象------放在开头和结尾的信息还记得,夹在中间的关键约束直接被忽略。
记得多 ≠ 用得上。
第二次尝试:只留最近 N 条(滑动窗口)
既然全量太重,那就只保留最近 20 条消息,老的丢掉。简单粗暴。
结果 :短期是清爽了,但把"这个项目用红涨绿跌配色"这种说过一次、长期有效 的关键约定也一起丢了。过几轮它又开始问"配色用什么"。滑动窗口砍掉的不是噪声,是按时间一刀切,而重要性和时间根本不成正比。
两次尝试指向同一个教训:记忆的问题,不能用"记多少"来解决,要用"怎么分类、怎么流转"来解决。
关键观察
我把 Agent 一周的上下文导出来做了个统计,按"这条信息在后续任务里被真正用到"来分类:
| 记忆类型 | 举例 | 后续被用到的比例 |
|---|---|---|
| 当前任务上下文 | 正在改的文件、当前报错 | ~95% |
| 项目级约定 | 技术栈、目录规范、命名 | ~70% |
| 用户长期偏好 | 语言、代码风格、汇报格式 | ~60% |
| 一次性事实 | 某次临时路径、搜索结果 | ~3% |
| 闲聊 / 情绪 | 随口的玩笑、寒暄 | ~1% |

记住一切,等于什么都没记住。
超过一半的"记忆"是一次性的噪声,把它们和真正的长期约定平等地塞进上下文,就是在稀释注意力。
定位真正原因
问题不在"记得多少",而在所有记忆被平等对待、生命周期完全相同:一次性的临时路径和"这个项目用 Flask"这种长期约定,被存在同一个地方、同样长久地活着、同样默认加载。
正确思路是给记忆分层,让不同价值的信息走不同的"寿命"和"加载策略":短期记忆用完即弃,长期记忆按需晋升,噪声定期清理。
这其实和上一篇的 Rule RAG 是同一个哲学:不在推理时把所有东西硬塞给模型,而是让对的信息在对的时候出现。 Rules 管的是"规则何时出现",Memory 管的是"记忆何时出现"------两者都是**上下文工程(Context Engineering)**的一部分。
最终方案:四层记忆 + 生命周期
我把记忆拆成四层,每层有不同的存活时间和加载策略:
| 层级 | 存什么 | 生命周期 | 是否默认加载 |
|---|---|---|---|
| Conversation(对话层) | 当前对话的原始消息流 | 会话内,session 结束即弃 | 是(但滚动裁剪) |
| Working Memory(工作记忆) | 本次会话提炼的关键状态、当前任务进度 | 单次会话 | 是(精简版) |
| Long Memory(长期记忆) | 跨会话的持久约定:项目记忆 + 用户记忆 | 长期,可更新 | 按需检索 |
| Archive(归档层) | 冷数据、历史日志、过期记忆 | 永久但压缩 | 否,仅显式检索 |
信息在这四层之间单向流动 + 定期回收:新信息先进对话层,被判定为"有长期价值"才晋升到长期记忆,长期记忆里过时的再降级到归档。
长期记忆为什么要拆成"项目"和"用户"
这是最容易被忽略、也最关键的一刀。很多 Agent 把两者混成一个大 memory,于是换个项目就开始串味。
- 项目记忆(Project Memory) :只在当前项目生效。技术栈、目录规范、命名约定、这个项目特有的坑。换项目就不该带过去。
- 用户记忆(User Memory) :跟着人走、跨所有项目。语言偏好、代码风格、汇报格式、工作习惯。

判据很简单:问一句"换个项目还成立吗?" 成立的进用户记忆,不成立的进项目记忆。"我喜欢中文回复"是用户记忆;"这个项目用红涨绿跌"是项目记忆。分开存,才不会拿 A 项目的约定去污染 B 项目。
实际收益
| 指标 | 全量回灌 | 分层 + 生命周期 |
|---|---|---|
| 每次对话默认加载的记忆量 | 全部历史(数百条) | 精简工作记忆 + 3 条常驻(个位数) |
| 换项目时的串味 | 频繁(旧栈污染新项目) | 基本消除(项目记忆隔离) |
| 关键长期约定的存活 | 易被滑动窗口误删 | 稳定(晋升到长期层) |
| 上下文噪声占比 | ~55% | 显著下降(噪声不晋升) |
数据来自主观经验估算,不是严格 A/B Test,但反映了分层前后的量级差异。
架构图 / 流程图
记忆的四层流动

关键点 :越往下越持久、越精炼;只有对话层是易失的,其余都要落盘。晋升(promotion)是主动决策,不是自动全存。
记忆的生命周期(状态流转)

图片多样性:这里用了 Mermaid 的 pie / flowchart / stateDiagram 三种图;实际发文时可再补一张 ProcessOn/draw.io 版的四层示意图,进一步提升 CSDN 图片多样性得分。
代码或配置示例
记忆的目录结构
我把落盘的记忆按"归属"组织,一眼就能看出哪些跟人走、哪些跟项目走:
bash
# 用户级(跨项目,跟人走)
~/.agent/
└── MEMORY.md # 用户长期记忆:语言、风格、习惯
项目级(仅当前项目)
<project>/.agent/memory/
├── MEMORY.md # 项目长期记忆:技术栈、规范、约定(精炼)
├── 2026-07-09.md # 每日工作日志(append-only,短期→归档)
└── archive/ # 归档层:过期日志压缩沉淀
记忆管理策略(YAML 配置)
用配置显式声明每层的寿命和晋升条件,而不是让它们"自然生长":
yaml
# memory-policy.yaml --- 记忆生命周期策略
conversation:
scope: session # 会话内有效
max_messages: 30 # 滚动裁剪,超出丢弃
persist: false # 不落盘
working_memory:
scope: session
persist: false
distill: true # 会话结束前蒸馏关键状态,决定是否晋升
long_memory:
project: # 项目记忆
scope: project # 仅当前项目生效
max_chars: 3000 # 超出触发蒸馏压缩
user: # 用户记忆
scope: global # 跨项目生效
max_chars: 4000
promotion:
rule: "reused >= 2 OR user_explicit == true" # 复用≥2次 或 用户明确要求 才晋升
archive:
trigger: "not_hit_days > 30" # 30 天未命中 → 降级归档
compress: true
ttl_days: 365 # 归档超期清理
写入与晋升逻辑(伪代码)
python
def on_new_info(info: Info, ctx: Context) -> None:
"""一条新信息进来时的处理"""
# 1. 一律先进对话层
ctx.conversation.append(info)
# 2. 与当前任务相关 → 提炼进工作记忆
if relevant_to_current_task(info, ctx.task):
ctx.working_memory.upsert(info)
def on_session_end(ctx: Context) -> None:
"""会话结束时决定谁能"活下来" """
for item in ctx.working_memory:
if should_promote(item): # 复用≥2 或 用户明确要求
target = "user" if is_cross_project(item) else "project"
long_memory.write(item, scope=target) # 关键:分流到项目/用户
# 对话层直接丢弃,不落盘
def cleanup(long_memory: LongMemory) -> None:
"""定期清理:去重、降级、蒸馏"""
long_memory.dedup() # 合并重复/冲突项,保留最新
for item in long_memory.stale(days=30):
archive.move(item) # 长期未命中 → 归档
if long_memory.over_limit():
long_memory.distill() # 超出容量 → 蒸馏压缩
代码不多,但把三个动作说清楚了:分流写入、按条件晋升、定期清理。 这三个动作缺一个,记忆迟早失控。
设计权衡
| 候选方案 | 优点 | 缺点 | 为什么不选 |
|---|---|---|---|
| 全量回灌历史 | 实现最简单,"绝不遗漏" | 上下文爆炸、迷失在中间、噪声稀释注意力 | 记得多不等于用得上,规模一大必崩 |
| 滑动窗口(只留最近 N 条) | 上下文恒定、实现简单 | 按时间一刀切,误删长期约定 | 重要性和时间不成正比,砍错了对象 |
| 全部塞向量库,用时检索 | 容量无上限、语义召回 | 召回噪声多、无生命周期、冷热不分 | 只解决"存",没解决"该不该记、记多久" |
| 四层 + 生命周期 | 冷热分层、项目/用户隔离、可晋升可清理 | 需设计晋升与清理策略 | 选择理由:唯一同时管住"记什么、记多久、给谁"的方案 |
分层不是越多越好。 如果你的 Agent 只是个单轮问答机器人,压根不需要跨会话记忆------一个滑动窗口就够了。不要在不需要长期记忆的地方强行搭四层。 向量检索也不是对立面:它可以作为 Archive 层的召回手段,和分层是互补而非二选一。
总结
- ✅ 记忆的问题不是"记多少",而是"怎么分类、怎么流转"------和 Rules 一样是规模问题。
- ✅ 分四层:Conversation(易失)→ Working(会话)→ Long(持久)→ Archive(冷藏),越往下越精炼。
- ✅ 长期记忆必须拆项目和用户------判据是"换个项目还成立吗?",否则必然串味。
- ✅ 晋升是主动决策(复用≥2 或用户明确要求),不是自动全存。
- ✅ 没有清理机制的记忆系统,最终都会被噪声拖垮------去重、降级、蒸馏一个都不能少。
参考资料
- Lost in the Middle 论文(Liu et al., 2023) → 为什么长上下文里中间信息会被忽略,直接决定了"不能全量回灌"的结论
- MemGPT: Towards LLMs as Operating Systems → 用分层记忆 + 分页调度管理超长上下文,本文四层模型的重要参照
- Anthropic --- Claude 上下文与记忆管理文档(待补链接)→ 工作记忆与长期记忆分离的工程实践基础
- Generative Agents (Park et al., 2023) → 记忆的"重要性打分 + 检索 + 反思"机制,启发了本文的晋升与蒸馏设计
- PARA / Zettelkasten 知识管理法 → 用"归属"而非"时间"组织信息,是项目/用户记忆拆分的方法论来源
系列导航
- 上一篇:AI Agent 工程实践(02):为什么 Rules 要分层?
- 下一篇:待定(Review 机制 / Benchmark / Audit Trail)