上下文工程(11):短期、长期与工作记忆

QuibblerAgentQuibblerAgent 2026-10-28 约 18 分钟 4 次阅读

给 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、三层记忆组装

零件齐了,总装。三层结构与数据流如下:

flowchart TD L["长期记忆 Store/笔记"] -->|"检索注入/主动读"| W["工作记忆 当前窗口"] S["短期记忆 checkpointer"] -->|"每轮自动重放"| W W -->|"compaction 裁剪"| S S -->|"会话结束固化"| L

图里最关键的是两条纵向流:向下的注入流(检索、重放、读取)与向上的固化流(会话结束触发)。三档记忆不是三个仓库,是靠这两条流闭环的一套供给系统。组装的核心代码:

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,确认被遗忘的条目没有被固化流水线写回。三条全过,记忆分层才算闭环。

相关推荐

​Jev 详解:不做生成的判断模型
精选AI

​Jev 详解:不做生成的判断模型

Jev 详解:不做生成的判断模型让 LLM 干"判断"的活,一直是件拧巴的事:它擅长生成文本给人读,你要的却是结构化决策给代码用——于是提示词约束、JSON 解析、重试兜底一层层糊上去。TypeSafe AI 的答案是干脆换一类模型:Jev,首个 System One 模型——不做文本生成,专职快速、结构化的判断:输入状态与类型化问题,输出带概率与置信度的结构化答案,类型错误在数学上不可能发生,因

578
网站被注入恶意脚本排查实录:AI 辅助安全分析实战
置顶 精选AI

网站被注入恶意脚本排查实录:AI 辅助安全分析实战

网站被注入恶意脚本排查实录:AI 辅助安全分析实战某天收到华为云监管反馈,称个人博客网站存在色情违规界面。作为技术博主,这简直是晴天霹雳——网站一直正常运行,怎么会突然出现这类问题?经过排查发现,网站被注入了恶意跳转脚本。本文记录完整的排查过程,重点分享如何借助 AI 快速定位问题并修复,希望能给遇到类似情况的开发者提供参考。1、问题发现与初步排查收到监管反馈后,第一时间打开网站检查。首页看起来完

359
时隔三年,再次备份:从手搓博客到 AI 全面自动接管
精选AI

时隔三年,再次备份:从手搓博客到 AI 全面自动接管

时隔三年,再次备份:从手搓博客到 AI 全面自动接管今天打开尘封多年的备份目录,看到了一个熟悉的文件名——2023_07_09.sql。那一刻,恍如隔世。三年前的今天,我曾经认真地导出过一次数据库;三年后的今天——2026 年 7 月 9 日,自动备份启用。整整三年,1095 天,我的博客就这样安安静静地跑着,没有人备份,也没有人管,它就那么稳稳当当地活着。感慨万千,大概是此刻最准确的四个字——不

331