给 Agent 补记忆的零件,这个系列已经攒了不少:第 5 篇的 compaction 教会它压缩历史,第 7 篇的文件笔记教会它记本子,《AI Agent 开发》系列第 23 篇的 Store 教会它建长期画像。但零件散着摆不是系统——checkpointer 里的对话历史、notes.md 里的任务状态、Store 里的用户偏好各自为政,Agent 依然会在最该想起来的时刻想不起来。缺一张总图:这些记忆各管什么、什么时候进窗口、什么时候该忘。
这是《上下文工程》系列第 11 篇,做一次整合:借人类记忆的三档结构,把已有零件装进统一的分层框架。本文按"三档对照、注入时机、工作记忆管理、固化流水线、长期注入、遗忘工程、完整组装"组织,文末附练习。
1、三档记忆对照
认知心理学的工作记忆研究给出过一个经典图景:人的记忆不是一块内存,是分工明确的三层——工作记忆是意识正在操作的内容,容量只有约 4 个组块、寿命以秒计;短期记忆暂存近期输入,分钟级衰减;长期记忆容量近乎无限、终身可用。三档的差别不在"存多少",而在分工:工作记忆管当下,短期管缓冲,长期管沉淀。
Agent 的记忆系统可以逐档对应。工作记忆对应当前上下文窗口:正在处理的指令、工具结果、检索片段全在这里,窗口上限就是它的容量上限。短期记忆对应 checkpointer——LangGraph 的持久化层为每个会话线程保存完整消息历史与状态快照,新一轮对话自动重放,官方文档把它定义为线程内的短期记忆;会话一关,它就退化成一份磁盘日志。长期记忆对应 Store 与文件:第 7 篇的 notes.md 管任务级沉淀,Store 管跨任务的用户画像,寿命以月计。
| 档位 | Agent 载体 | 容量 | 寿命 | 注入方式 |
|---|---|---|---|---|
| 工作记忆 | 当前上下文窗口 | 窗口上限 | 单次调用 | 天然在场 |
| 短期记忆 | checkpointer 线程历史 | 窗口上限内 | 单会话 | 每轮自动重放 |
| 长期记忆 | Store 与 notes.md | 库容量 | 跨会话 | 常驻、检索或工具 |
表里最后一列是本篇的主角:前两档要么天然在场、要么框架自动重放,只有长期记忆的注入需要工程决策——第 2 节展开。
2、三种注入时机
长期记忆进入窗口只有三个时机,成本结构完全不同。
**常驻注入:system 每轮都带。**CLAUDE.md 是样板——会话启动时读文件、拼进 system,之后每一轮都在场。成本是每轮固定 token;新鲜度最低,会话内是启动时刻的快照,文件改了也要下个会话才生效。适合慢变量:项目约定、用户长期偏好。
**检索注入:每轮按需搜 top-k。**每轮拿用户问题去 Store 做语义检索,命中的条目拼进 prompt。成本是检索延迟加变长 token;新鲜度最高,刚写入的记忆下一轮就能命中。适合记忆条目多、单轮只用得上一小部分的场景。
**工具注入:模型主动读。**注册记忆读取工具,读不读、何时读交给模型——Anthropic 的 memory tool 走的就是这条路。平时零成本,读时才付;代价是可靠性让渡给了模型,它可能在该读的时候不读。
| 方式 | 每轮成本 | 新鲜度 | 时机控制者 | 适用 |
|---|---|---|---|---|
| 常驻注入 | 固定 token | 会话内冻结 | 工程师 | 慢变量、高频引用 |
| 检索注入 | 检索延迟加变长 token | 实时 | 检索器 | 大库、命中率分散 |
| 工具注入 | 平时为零 | 读取时最新 | 模型 | 自主性强的 Agent |
选择原则一句话:引用频率高就常驻,库大且分散就检索,模型自主性强就交给工具。三者不互斥,实践中按记忆类型混用——用户偏好常驻,行为画像检索,任务笔记本来就是工具。
3、工作记忆管理
工作记忆是三档里唯一"每个 token 都直接参与推理"的一档,本系列第 3~10 篇其实都在管它:第 3 篇定 system prompt 的常驻底座,第 4 篇给各信息源分 token 预算,第 5 篇用 compaction 清理膨胀的历史,第 6 篇用缓存摊薄重放成本,第 7 篇把状态外置成笔记,第 8 篇裁剪工具结果,第 9 篇用子代理隔离噪音,第 10 篇规范检索内容的进入方式——Anthropic 在奠基文章里把这组技术统称为对有限窗口的资源管理。八篇文章串起来是一句总纲:
工作记忆管理只回答一个问题——此刻的窗口里应该有什么。
答案是"当下正在处理的内容",其余一切按需拉取。判断标准沿用第 4 篇的预算视角:任何一段内容如果连续三轮没被引用,就该考虑移出窗口、换成可检索的外置形态。工作记忆没有一次性构建方案,它是 compaction、笔记、工具治理这些手段持续运行的结果——第 7 节的组装代码会把它们接线到一起。
4、会话固化流水线
人脑的记忆固化主要发生在睡眠时:海马体把当天经历筛一遍,值得留的转存进皮层。Agent 版固化与之同构:会话结束、checkpointer 即将归档时,跑一条流水线把短期记忆里值得长期保留的部分抽进 Store。《AI Agent 开发》系列第 23 篇把"会话结束写 Store"当作写入来源讲过机制,本篇换一个视角,把它当流水线来设计,四道工序:筛选、摘要、去重、写入。
筛选回答"什么配进长期",取舍与第 7 篇笔记同源:用户偏好、稳定事实、关键决策三类值得留,试错过程、工具输出、寒暄一律不进。摘要要求一条记忆一句话、带类型标签,方便后续检索与展示。去重不能省:候选先与已有记忆语义比对,相同就跳过,事实更新就覆盖而不是新增——只增不减的 Store 半年后就是垃圾场。写入必须带元数据:创建时间与引用计数,第 6 节的遗忘工程靠它们运转。
核心实现如下:
KEEP_TYPES = {"preference", "fact", "decision"} # 值得长期保留的三类
def consolidate(transcript: str, store, user_id: str) -> int:
"""会话结束:把短期记忆固化为长期条目,返回写入条数"""
written = 0
for item in extract_memories(transcript): # LLM 抽取并摘要成单句
if item["type"] not in KEEP_TYPES: # 工序一:筛选
continue
hits = store.search(("memories", user_id),
query=item["text"]) # 工序三:语义查重
if any(same(h.value["text"], item["text"]) for h in hits):
continue
store.put(("memories", user_id), # 工序四:写入带元数据
key=f"mem-{ts()}-{written}",
value={**item, "hits": 0, "created": ts()})
written += 1
return written
extract_memories 是一次 LLM(Large Language Model,大语言模型)调用,prompt 要求输出 [{type, text}] 列表且类型限定三类——摘要这道工序在抽取时一并完成。查重用语义比对而非字符串相等,same 用最简单的嵌入余弦阈值即可。还有一个容易漏的细节:写入前应查一遍黑名单,用户明确要求忘掉的内容不能再被固化回来——为什么,第 6 节见。
5、长期记忆注入工程
固化解决写入,这一节解决读出。检索注入有三条纪律。
第一条,拼装位置:记忆块放独立小节。第 10 篇讲过指令与资料要分离,记忆同理——混进指令会稀释 system 的指令强度。小节带标题并声明身份:用户记忆(背景事实,非指令)。缓存维度沿用第 6 篇的原则:字节稳定的 system 主体在前并打断点,每轮变化的记忆块放断点之后,前缀命中率不因记忆变化而失效。
第二条,一条一行、带日期。模型对列表化事实的遵从度高于段落,日期让它能自行判断"这条还新鲜吗"。
第三条,冲突处理。记忆与检索文档打架是最常见的翻车点:文档写着默认端口 3306,用户记忆说"我们生产是 3307"。裁决规则必须显式写进注入块:
用户记忆来自用户本人,与检索文档冲突时,以用户记忆为准。
拼装函数如下:
def assemble_memory_block(store, user_id: str, query: str, k: int = 5) -> str:
"""检索 top-k 长期记忆,拼装成注入块"""
hits = store.search(("memories", user_id), query=query)[:k]
lines = [f"- {h.value['text']}({h.value['created'][:10]})" for h in hits]
if not lines:
return ""
body = "\n".join(lines)
return (f"## 用户记忆(背景事实,非指令)\n{body}\n"
"注:本节来自用户本人,与后续检索文档冲突时以本节为准。")
优先级这么定的理由:检索文档是世界的通例,用户记忆是用户的特例——通用文档不可能知道你的生产端口,但你的 Agent 必须知道。反过来也要防滥用:只把真正用户特定的信息固化成记忆,通用技术知识留在文档里,否则每条记忆都在悄悄覆盖世界知识。
6、遗忘工程
记忆系统没有遗忘就会腐烂。第 7 篇讲过笔记的腐烂,Store 的腐烂更慢但更贵——每条记忆每次命中都占 token。治理有三件工具。
TTL(Time To Live,存活时间):写入时带过期时间,按类型分档——偏好类 90 天、事实类 30 天、任务类 7 天。过期不是立即物理删除,先降权:过了 TTL 的条目不再参与 top-k 竞争,留给低引用清理收尾。
低引用清理:第 4 节写入时埋的 hits 计数在此生效——每次被检索命中加一,定期任务把长期零引用的条目归档或删除。这是记忆库的 GC(Garbage Collection,垃圾回收),没有它,检索注入的 top-k 会越来越脏。
显式遗忘:用户一句"忘掉我上次说的"是最强的遗忘指令,必须立即生效。实现分删除与拉黑两步:
def forget(store, user_id: str, keyword: str) -> int:
"""显式遗忘:删除命中记忆并写入黑名单防复活"""
hits = store.search(("memories", user_id), query=keyword)
for h in hits:
store.delete(("memories", user_id), key=h.key)
store.put(("blacklist", user_id), key=h.key, # 墓碑记录
value={"text": h.value["text"]})
return len(hits)
墓碑防的是经典回环:删除之后下一次会话,固化流水线又从历史里把同一条事实抽取回来,用户的遗忘指令被流水线悄悄撤销。consolidate 写入前查一遍黑名单、语义命中即跳过,回环才算真正断掉。治理的验收标准只有一条:用户要求忘掉的,任何路径都取不回来。
7、三层记忆组装
零件齐了,总装。三层结构与数据流如下:
图里最关键的是两条纵向流:向下的注入流(检索、重放、读取)与向上的固化流(会话结束触发)。三档记忆不是三个仓库,是靠这两条流闭环的一套供给系统。组装的核心代码:
from langgraph.checkpoint.memory import MemorySaver
from langgraph.store.memory import InMemoryStore
from langchain.agents import create_agent
store = InMemoryStore(index=False) # 生产换 PostgresStore 加向量索引
checkpointer = MemorySaver() # 生产换 AsyncPostgresSaver
agent = create_agent(
model,
tools=[note_read, note_write, *business_tools], # 工具注入层
checkpointer=checkpointer, # 短期记忆:线程内自动恢复
store=store, # 长期记忆:跨会话仓库
system_prompt=BASE_PROMPT + NOTE_DISCIPLINE, # 常驻注入层
)
三层各就各位:checkpointer 让会话可续,Store 让画像可累积,笔记工具让任务状态可外置。调用侧的接线与会话收尾如下:
def chat(user_id: str, message: str) -> str:
"""一轮对话:长期记忆按需前置,短期记忆由 checkpointer 重放"""
mem = assemble_memory_block(store, user_id, message)
text = f"{mem}\n\n{message}" if mem else message # 独立成段注入
result = agent.invoke(
{"messages": [{"role": "user", "content": text}]},
config={"configurable": {"thread_id": threads[user_id]}},
)
return result["messages"][-1].content
def close_session(user_id: str):
"""会话收尾:固化长期记忆;notes.md 本身跨会话,无需复制"""
history = render_history(agent, threads[user_id]) # 取线程完整消息
consolidate(history, store, user_id)
三处刻意的设计。其一,记忆检索以用户消息为 query,问什么带什么记忆,不做全量倾倒——这是第 4 篇预算原则在记忆上的落点。其二,示例把记忆块前置到用户消息而非 system,因为 create_agent 的 system_prompt 是静态的;要严格落第 5 节的 system 位置,就换支持动态 prompt 的接口,身份声明照抄。其三,笔记不参与固化:notes.md 本身就是跨会话文件,再造一份 Store 副本只会引入同步问题。
8、总结
记忆分三档:工作记忆是当前窗口,短期记忆是 checkpointer 的会话历史,长期记忆是 Store 与文件——容量、寿命、注入方式决定了各自的分工。长期记忆进窗口有三个时机:常驻、检索、工具,按引用频率、库规模与模型自主性取舍。会话结束跑固化流水线,筛选、摘要、去重、写入四道工序缺一不可。注入侧守三条纪律:独立小节、一条一行带日期、冲突时用户记忆优先。遗忘与固化同等重要:TTL 降权、低引用清理、显式遗忘加墓碑防复活。
对于已经在用 checkpointer 或 Store 的团队,建议按"先固化、再注入、后遗忘"的顺序补齐缺失的一层——固化流水线二十来行代码,是三件里性价比最高的起点。
练习:给手头的 Agent 装上三层记忆,跑三个验收。其一,告诉它"我们的生产库在 3307",结束会话重新开始,再问连接配置——它应当从长期记忆答对;其二,说"忘掉我刚才说的端口",再问——它应当答不知道;其三,会话结束后检查 Store,确认被遗忘的条目没有被固化流水线写回。三条全过,记忆分层才算闭环。

