
Agent记忆架构设计:从金鱼记忆到永久记忆的5级进化
老话说金鱼只有7秒记忆,转个身就把自己忘了。不瞒你说,我刚入行写的第一个Agent,记性比金鱼还惨——金鱼好歹还有7秒,它只有0秒。这一节,我把这几年在记忆系统上踩过的坑,浓缩成一条从L1到L5的进化路线,你对着自己的项目对号入座就行。
本文导航
- 先讲一个真实的社死现场
- 全景图:一张图看懂5级记忆
- L1 上下文窗口:Agent的"金鱼泡"
- L2 Scratchpad:草稿纸才是工作记忆的精髓
- L3 向量库:给Agent装上"海马体"
- L4 黑板模式:多Agent的共享大脑
- L5 压缩与遗忘:记忆的新陈代谢
- 把5级串起来跑一遍
- 一张表定级:5级记忆怎么选
先讲一个真实的社死现场
2024年初,我给一家做电商客服SaaS的公司搭Agent。Demo当天一切丝滑,客户CTO随手敲了一句:“我昨天问过的那个退货政策,你还记得吗?”
Agent一本正经地回答:“抱歉,这是我们第一次对话。”
会议室安静了整整三秒。CTO扭头看着我:“这玩意儿连昨天的事都不记得,凭什么替代我的人工客服?”
当天晚上我熬了个通宵,把全部对话历史一股脑塞进prompt——记性是有了,账单也爆了:token消耗涨了6倍,首字响应慢了一半,更尴尬的是聊到第30轮之后,Agent开始把三小时前用户报的订单号张冠李戴。
这就是记忆问题的两难:不记,是金鱼;全记,是垃圾桶。
后来我慢慢想明白了一件事:记忆不是一个开关,而是一个阶梯。绝大多数项目根本不需要一步登天到"永久记忆",但你必须知道每一级解决什么问题、要花多少钱、会在哪里翻车。这就是今天要讲的5级架构。
全景图:一张图看懂5级记忆
#mermaid-svg-MFZc8cnd0jIAsBs6{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-MFZc8cnd0jIAsBs6 .error-icon{fill:#552222;}#mermaid-svg-MFZc8cnd0jIAsBs6 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-MFZc8cnd0jIAsBs6 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .marker.cross{stroke:#333333;}#mermaid-svg-MFZc8cnd0jIAsBs6 svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-MFZc8cnd0jIAsBs6 p{margin:0;}#mermaid-svg-MFZc8cnd0jIAsBs6 .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .cluster-label text{fill:#333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .cluster-label span{color:#333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .cluster-label span p{background-color:transparent;}#mermaid-svg-MFZc8cnd0jIAsBs6 .label text,#mermaid-svg-MFZc8cnd0jIAsBs6 span{fill:#333;color:#333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .node rect,#mermaid-svg-MFZc8cnd0jIAsBs6 .node circle,#mermaid-svg-MFZc8cnd0jIAsBs6 .node ellipse,#mermaid-svg-MFZc8cnd0jIAsBs6 .node polygon,#mermaid-svg-MFZc8cnd0jIAsBs6 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-MFZc8cnd0jIAsBs6 .rough-node .label text,#mermaid-svg-MFZc8cnd0jIAsBs6 .node .label text,#mermaid-svg-MFZc8cnd0jIAsBs6 .image-shape .label,#mermaid-svg-MFZc8cnd0jIAsBs6 .icon-shape .label{text-anchor:middle;}#mermaid-svg-MFZc8cnd0jIAsBs6 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-MFZc8cnd0jIAsBs6 .rough-node .label,#mermaid-svg-MFZc8cnd0jIAsBs6 .node .label,#mermaid-svg-MFZc8cnd0jIAsBs6 .image-shape .label,#mermaid-svg-MFZc8cnd0jIAsBs6 .icon-shape .label{text-align:center;}#mermaid-svg-MFZc8cnd0jIAsBs6 .node.clickable{cursor:pointer;}#mermaid-svg-MFZc8cnd0jIAsBs6 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .arrowheadPath{fill:#333333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-MFZc8cnd0jIAsBs6 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MFZc8cnd0jIAsBs6 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-MFZc8cnd0jIAsBs6 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MFZc8cnd0jIAsBs6 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-MFZc8cnd0jIAsBs6 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-MFZc8cnd0jIAsBs6 .cluster text{fill:#333;}#mermaid-svg-MFZc8cnd0jIAsBs6 .cluster span{color:#333;}#mermaid-svg-MFZc8cnd0jIAsBs6 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-MFZc8cnd0jIAsBs6 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-MFZc8cnd0jIAsBs6 rect.text{fill:none;stroke-width:0;}#mermaid-svg-MFZc8cnd0jIAsBs6 .icon-shape,#mermaid-svg-MFZc8cnd0jIAsBs6 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-MFZc8cnd0jIAsBs6 .icon-shape p,#mermaid-svg-MFZc8cnd0jIAsBs6 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-MFZc8cnd0jIAsBs6 .icon-shape .label rect,#mermaid-svg-MFZc8cnd0jIAsBs6 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-MFZc8cnd0jIAsBs6 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-MFZc8cnd0jIAsBs6 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-MFZc8cnd0jIAsBs6 :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
Agent 记忆系统 · 5级进化
监管L1/L3的容量与质量
监管L1/L3的容量与质量
成本:token线性膨胀
成本:存储+检索延迟
成本:有损压缩风险
用户输入
Agent 大脑(LLM)
L1 上下文窗口短期记忆:滑动窗口,装满就丢
L2 Scratchpad工作记忆:当前任务的草稿纸
L3 向量数据库长期记忆:跨会话语义召回
L4 黑板模式共享记忆:多Agent公用大脑
L5 压缩与遗忘新陈代谢:摘要、TTL、衰减
$ 每轮请求都带着历史
$ 向量库运维
⚠ 可能忘掉重要的
看图有个关键认知:L5不是第5种存储,而是横跨L1和L3的"代谢机制"。就像人脑不是无限堆神经元,而是靠睡眠时的记忆巩固和遗忘来保持清醒。这个认知是我做记忆系统分水岭级别的转折。
下面一级一级拆。每一级我都按三件事讲清楚:什么场景用、代码怎么写、账单怎么算。
L1 上下文窗口:Agent的"金鱼泡"
用例:多轮对话、追问、指代消解("它多少钱"里的"它"是谁)。
本质上,LLM的API是无状态的,所谓"短期记忆",就是你每次请求时把最近几轮对话拼在prompt里带上去。听起来简单,但90%的翻车都翻在两个细节上:
class ContextWindow:
"""L1 短期记忆:滑动窗口。满了就掐最老的,简单粗暴但有效"""
def __init__(self, max_messages: int = 6):
self.max_messages = max_messages
self.history: list[dict] = []
def add(self, role: str, content: str):
self.history.append({"role": role, "content": content})
if len(self.history) > self.max_messages:
overflow = len(self.history) – self.max_messages
self.history = self.history[overflow:] # 金鱼式遗忘:最老的先丢
def render(self) –> str:
"""拼进prompt的部分。真实项目里换成messages数组喂给API"""
return "\\n".join(f"{m['role']}: {m['content']}" for m in self.history)
成本账:假设每轮对话平均300 token,窗口留10条消息,那么第N轮请求的输入≈固定人设500 + 300×min(N,10)。对话越长,每一轮都在为重复的历史付费。我见过一个项目,用户跟Agent唠了200轮,单轮输入token干到4000+,月账单四位数美金。L1的钱是线性烧的,这是它最大的坑。
L2 Scratchpad:草稿纸才是工作记忆的精髓
L1存的是"对话流水",但Agent干复杂活的时候,真正需要的是"任务草稿"——ReAct里的Thought、计划清单、中间结论。心理学上这叫工作记忆(working memory),跟短期记忆是两码事,很多文章把它们混为一谈,这是不对的。
用例:多步任务的中间状态。比如"帮我对比三个框架"这种任务,Agent需要一张草稿纸记:已经查了哪些、还差哪些、初步结论是什么。没有它,Agent干着干着就开始原地打转——我有个同事管这叫"金鱼式加班"。
class Scratchpad:
"""L2 工作记忆:当前任务的草稿纸。任务结束即可整体丢弃"""
def __init__(self):
self.notes: dict[str, str] = {} # key: 任务项, value: 状态或结论
def write(self, item: str, conclusion: str = "pending"):
self.notes[item] = conclusion
def done(self, item: str, conclusion: str):
self.notes[item] = f"done -> {conclusion}"
def render(self) –> str:
return "\\n".join(f"- [{v}] {k}" for k, v in self.notes.items())
成本账:几乎为零——它不产生额外的模型调用,只是把已有的推理中间态结构化。但它会占用上下文空间,所以草稿纸要定期清理:任务完成了的条目,折叠成一行结论。LangGraph里的共享State,本质上就是一个框架级的Scratchpad,这个我们在Part 3会反复见到。
L3 向量库:给Agent装上"海马体"
L1有个致命缺陷:跨会话全忘。用户昨天聊的,今天重启会话就没了。要"隔夜记忆",就得把记忆外置成库,用的时候按语义捞回来。
用例:用户偏好(“老王喜欢深色主题”)、历史工单、个人知识库。注意L3存的不是对话原文,而是提炼后的记忆条目——这是另一个新手大坑:直接把聊天记录全扔进向量库,检索质量惨不忍睹。
为了让你零依赖跑通概念,我写了个玩具级向量库(字符bigram嵌入+余弦相似度)。生产环境请老老实实换Chroma/pgvector + 真实Embedding模型,玩具嵌入在中文上的区分度只是"能看":
import math, hashlib
def toy_embed(text: str, dim: int = 64) –> list[float]:
"""玩具嵌入:相邻两字组成的bigram哈希进向量。仅教学用!"""
vec = [0.0] * dim
for i in range(len(text) – 1):
gram = text[i:i+2]
idx = int(hashlib.md5(gram.encode()).hexdigest(), 16) % dim
vec[idx] += 1.0
norm = math.sqrt(sum(v * v for v in vec)) or 1.0
return [v / norm for v in vec]
class VectorMemory:
"""L3 长期记忆:语义检索式召回"""
def __init__(self):
self.store: list[tuple] = [] # (条目, 向量, 元信息)
def add(self, text: str, **meta):
self.store.append((text, toy_embed(text), meta))
return len(self.store)
def search(self, query: str, top_k: int = 2):
qv = toy_embed(query)
scored = [(t, sum(a*b for a, b in zip(qv, v))) for t, v, _ in self.store]
scored.sort(key=lambda x: –x[1])
return [(t, round(s, 3)) for t, s in scored[:top_k]]
成本账:三笔钱——①Embedding调用费(写入时一次性);②存储费(100万条记忆,1536维向量,约6GB,云向量库每月几十到几百块);③检索延迟(通常10~50ms,可接受)。真正的隐性成本是召回不准:捞回三条不相关的记忆塞给模型,比不捞还糟糕——模型会被带偏。所以L3必须配检索质量评估,这部分Part 4的GraphRAG章节会专门展开。
L4 黑板模式:多Agent的共享大脑
前面三级都是"一个Agent自己的记忆"。一旦进入多Agent系统(这也是本课程的主线),就会出现新问题:翻译Agent改好的稿子,审校Agent怎么知道?各Agent人手一份私有记忆,等于一个团队每人一本笔记互不共享——那还协作个啥。
用例:多Agent流水线(写作组、评审组)、需要"共同 situational awareness"的任务。
经典解法是黑板模式(Blackboard),上世纪70年代AI教科书里就有的老古董,在多Agent时代反而焕发第二春。实现上就是个带分区的共享存储:
import threading, time
class Blackboard:
"""L4 共享记忆:多Agent公用一块黑板,按分区读写,线程安全"""
def __init__(self):
self._lock = threading.Lock()
self.sections: dict[str, list] = {}
def post(self, section: str, agent: str, content: str):
with self._lock:
self.sections.setdefault(section, []).append(
{"by": agent, "content": content, "ts": time.time()})
def read(self, section: str) –> list[dict]:
with self._lock:
return list(self.sections.get(section, []))
def take(self, section: str) –> list[dict]:
"""取走式读取:领任务专用,避免两个Agent抢同一个活"""
with self._lock:
items = self.sections.pop(section, [])
return items
注意那个take方法——多Agent从黑板领任务时,必须"取走"而不是"读取",否则两个Worker会抢同一个任务。这是我当年用血泪换来的教训:一个爬虫Agent组里两个Agent重复抓了同一个站点两小时,浪费了一笔不小的API费用。
成本账:黑板本身不要钱,贵在治理——谁写入、谁清理、脏数据怎么办。LangGraph的共享State就是"框架托管的黑板",它用Reducer机制解决了并发写入冲突,这个第43节马上会看到。
L5 压缩与遗忘:记忆的新陈代谢
到L4,你的Agent已经"记性好"了。但运行三个月后,新问题来了:上下文塞不下了,向量库十万条记忆里一半是垃圾。记性好和脑子清醒是两回事——什么都不会忘的系统,最后会被自己的记忆淹死。
用例:长会话的上下文压缩(第50轮时把1~30轮摘要成300字)、向量库的记忆保鲜(淘汰过时/无人访问的条目)。
class ForgetfulMemory(VectorMemory):
"""L5:在L3之上加新陈代谢——压缩 + 遗忘"""
def compress(self, keep_ratio: float = 0.5):
"""玩具级摘要:截断+标记。真实项目换成LLM做真摘要"""
fresh = []
for text, vec, meta in self.store:
if meta.get("pinned"): # 钉住的重要记忆,不许动
fresh.append((text, vec, meta))
continue
cut = max(6, int(len(text) * keep_ratio))
fresh.append((text[:cut] + "…[已压缩]", vec, meta))
self.store = fresh
def forget(self, now: float, ttl: float = 7 * 86400):
"""TTL + 零访问双重淘汰:过期且没人查过的,删"""
kept, dropped = [], []
for text, vec, meta in self.store:
expired = now – meta.get("born_at", now) > ttl
useless = meta.get("hits", 0) == 0
if expired and useless and not meta.get("pinned"):
dropped.append(text)
else:
kept.append((text, vec, meta))
self.store = kept
return dropped
两个工程细节值得说:①pinned机制——用户明确说过的关键信息(过敏史、账户号)要能钉住不被遗忘;②遗忘策略我推荐"TTL + 访问衰减"组合而不是纯时间淘汰,因为它近似人类的遗忘曲线:常用的一直新鲜,不用的自然淡出。
成本账:压缩是有损的,每次摘要都可能丢信息——所以高价值信息必须在压缩前结构化沉淀到L3。遗忘的风险是误删,所以删除走软删除+审计日志更稳。
把5级串起来跑一遍
光说不练假把式。我把五级记忆塞进一个零依赖脚本(Python 3.10+,标准库直跑):
# memory_levels.py —— Agent五级记忆架构最小可运行演示
# 运行:python memory_levels.py (无任何第三方依赖)
def demo():
print("=" * 52)
print("L1 上下文窗口:金鱼记忆")
print("=" * 52)
cw = ContextWindow(max_messages=4)
for role, msg in [("user", "你好,我叫老王"),
("assistant", "王先生您好!"),
("user", "帮我查下订单"),
("assistant", "订单12345已发货"),
("user", "预计多久到?")]:
cw.add(role, msg)
print(cw.render())
print(">>> 注意:第一句'我叫老王'已被挤出窗口,Agent失忆了\\n")
print("=" * 52)
print("L2 Scratchpad:任务草稿纸")
print("=" * 52)
sp = Scratchpad()
sp.write("对比LangGraph与CrewAI的并行能力")
sp.write("整理性能基准数据")
sp.done("整理性能基准数据", "LangGraph Send API并发约3.2x")
print(sp.render(), "\\n")
print("=" * 52)
print("L3 向量库:长期记忆跨会话召回")
print("=" * 52)
vm = VectorMemory()
vm.add("用户名字叫老王", born_at=0, hits=1)
vm.add("用户偏好深色主题界面", born_at=0, hits=3)
vm.add("上次购买了一把机械键盘", born_at=0, hits=0)
for q in ["用户的名字是什么?", "用户喜欢什么界面主题?"]:
print(f"Q: {q}")
for text, score in vm.search(q, top_k=1):
print(f" 命中[{score}]: {text}")
print()
print("=" * 52)
print("L4 黑板:多Agent共享记忆")
print("=" * 52)
bb = Blackboard()
bb.post("tasks", "manager", "翻译第3章为英文")
task = bb.take("tasks")
print(f"translator 领到任务: {task[0]['content']}")
bb.post("results", "translator", "第3章译文v1(820词)")
print(f"reviewer 读到成果: {bb.read('results')[0]['content']}\\n")
print("=" * 52)
print("L5 压缩与遗忘:记忆新陈代谢")
print("=" * 52)
fm = ForgetfulMemory()
fm.add("用户名字叫老王,电话138xxxx,会员等级金卡", pinned=True, born_at=0, hits=2)
fm.add("上周聊过天气不错适合郊游", born_at=0, hits=0) # 会自然过期
fm.compress(keep_ratio=0.5)
now = 30 * 86400 # 模拟30天后
dropped = fm.forget(now, ttl=7 * 86400)
print(f"压缩+遗忘后剩 {len(fm.store)} 条:")
for text, _, meta in fm.store:
print(f" {'📌' if meta.get('pinned') else '📦'} {text}")
print(f"被遗忘的条目: {dropped}")
if __name__ == "__main__":
demo()
控制台输出(我本机Python 3.12实测):
====================================================
L1 上下文窗口:金鱼记忆
====================================================
assistant: 王先生您好!
user: 帮我查下订单
assistant: 订单12345已发货
user: 预计多久到?
>>> 注意:第一句'我叫老王'已被挤出窗口,Agent失忆了
====================================================
L2 Scratchpad:任务草稿纸
====================================================
– [done -> LangGraph Send API并发约3.2x] 整理性能基准数据
– [pending] 对比LangGraph与CrewAI的并行能力
====================================================
L3 向量库:长期记忆跨会话召回
====================================================
Q: 用户的名字是什么?
命中[0.617]: 用户名字叫老王
Q: 用户喜欢什么界面主题?
命中[0.598]: 用户偏好深色主题界面
====================================================
L4 黑板:多Agent共享记忆
====================================================
translator 领到任务: 翻译第3章为英文
reviewer 读到成果: 第3章译文v1(820词)
====================================================
L5 压缩与遗忘:记忆新陈代谢
====================================================
压缩+遗忘后剩 1 条:
📌 用户名字叫老王,电话138xxxx,会员等级金卡
被遗忘的条目: ['上周聊过天气不错适合郊游…[已压缩]']
看输出最后一段:没被钉住的"天气闲聊"被压缩又被遗忘,而pinned的金卡会员信息完好无损——这就是新陈代谢该有的样子。
一张表定级:5级记忆怎么选
| L1 上下文窗口 | 几K~200K token | 0(随请求携带) | 随轮次线性涨 | 高(出窗即忘) | 多轮对话、追问 |
| L2 Scratchpad | 单任务内不限 | 0 | 几乎免费 | 低(任务内常驻) | ReAct推理、计划追踪 |
| L3 向量库 | 亿级条目 | 10~50ms | 存储+嵌入费 | 低(可召回) | 跨会话个性化、知识库 |
| L4 黑板 | 集群共享 | <5ms(进程内) | 治理成本高 | 低 | 多Agent协作 |
| L5 压缩/遗忘 | —— | 异步 | 有损风险 | 中(误删) | 长期运行系统必配 |
我的选型口诀,就四句:
- 聊天机器人:L1 + L2,别过度设计;
- 个人助理/客服:L1 + L2 + L3,L3里存"用户画像"而非聊天原文;
- 多Agent系统:L1~L4全套,黑板分区按业务域切;
- 7×24长期运行:必须上L5,不然三个月后系统会被自己的记忆拖死。
还有一句掏心窝的:记忆系统是演化的,不是设计出来的。先用L1跑通业务,等用户开始抱怨"它记不住我",再上L3;等检索开始变慢变脏,再上L5。反过来一步到位的,我见过的项目里十个有九个在给不存在的需求还技术债。
小结
下节预告
单Agent的记忆问题到这节基本闭环了,但下一节我们要直面一个更凶的话题——失败路由。Agent调用会超时、工具会报错、模型会一本正经地胡说八道,失败不是意外而是日常。下一节《失败是必然的:没有失败路由的Graph只是一张好看的流程图》,我们看看一张图该怎么"接住"失败、消化失败。
互动区
聊两个话题:
觉得有用就点个赞、收个藏,我们下节见。




