本页目录

记忆与状态

API 是无状态的,agent 的"记忆"全是工程出来的。三层记忆——会话内重发历史、compaction/context editing 管理窗口、memory tool/stores 跨会话持久——各用不同机制,解决不同问题。

概述

API 是无状态的(见 上下文工程):模型一轮里"知道"的全在这次请求里,它不会自动记得上次。所以 agent 的"记忆"⁠全是工程出来的⁠——是调用方维护、按生命周期分层的一套机制。按"活多久"分三层,是想清楚这件事最好的框架:

flowchart TD
    A["① 会话内<br/>重发完整历史 (协议自带)"] --> B["② 临近窗口上限<br/>compaction 摘要 / context editing 裁剪"]
    B --> C["③ 跨会话持久<br/>memory tool / memory stores (落到会话之外)"]

第一层"这轮记得",第二层"对话太长不溢出",第三层"关掉再开还记得"。三层用不同机制、解决不同问题,长 agent 常三层并用。理解它们的边界,才不会用错工具(比如想跨会话记住偏好却只靠重发历史)。

第一层:会话内 —— 重发历史的代价

模型记得上一轮,只是因为整段历史又被发了一遍⁠。这层"免费"(协议自带),但有两个代价:

  • 成本线性增长⁠:每轮重发全部历史,input token 随轮次累加。
  • 终会溢出⁠:历史无限长,迟早逼近窗口上限(stop_reason: "model_context_window_exceeded")。

prompt caching 能让重发的稳定前缀按 ~0.1x 计费(见 上下文工程),缓解成本;但溢出问题要靠第二层。

第二层:临近窗口的管理 —— 压缩 vs 裁剪

两个方向相反的 API 原语:

compaction(压缩)context editing(裁剪)
做什么把早期历史摘要成 compaction block把旧 tool_result / thinking 清除
信息概括保留直接删
Betacompact-2026-01-12context-management-2025-06-27
关键坑必须把 response.content(含 compaction block)⁠整块回传⁠,只回传 text 会丢状态只删块,不改对话结构

记法:compaction 是"概括",context editing 是"删除"。二者都在单次会话内腾窗口,⁠不解决跨会话⁠——关掉进程它们就没了。

第三层:跨会话持久记忆

要"关掉再开还记得",得把要点写到会话之外⁠。两条路径,按自管 vs 托管来选。

路径 A:memory tool(后端自管)

声明工具 {"type": "memory_20250818", "name": "memory"},模型就获得一个 /memories 目录的读写能力,⁠六个命令⁠:

命令语义
view读文件 / 列目录
create新建/覆盖文件
str_replace替换文件中一处字符串
insert在指定行后插入
delete删除文件
rename重命名/移动

存储后端自行实现⁠(本地 FS、对象存储、DB)——模型只发命令,由实现方执行并回 tool_result。Python/TypeScript SDK 提供脚手架(BetaAbstractMemoryTool / betaMemoryTool + handler),只需填 view/create/str_replace/... 的存储逻辑。模型把"用户偏好""项目约定""踩过的坑"写进文件,下次会话先 view 读回来。

这套思路本会话现在就在用:持久记忆就落在 ~/.claude/projects/.../memory/ 下,⁠一条事实一文件 + 一个 MEMORY.md 索引⁠——MEMORY.md 常驻、每条事实按需读。这正是 memory tool 模式的落地(本轮就更新了"本站点构建方式"那条)。

路径 B:Managed Agents memory stores(托管)

工作区级持久记忆库,对象模型三层:

memory store 三层对象模型 memory_store (memstore_…) 工作区级集合,挂进 session memory (mem_…) 一个文本文件,按 path 寻址,≤ 100KB memory_version (memver_…) 每次改动一个不可变快照(created / modified / deleted)

机制要点:

  • 挂载为文件系统⁠:store 以 FUSE 挂到容器 /mnt/memory/<store>/,agent 用普通文件工具读写;系统提示自动注入一段说明,告诉它挂载存在。
  • 访问控制⁠:access: "read_only" | "read_write",在文件系统层强制。
  • 乐观并发⁠:memories.update 支持 precondition: {type:"content_sha256", ...},不匹配返回 409——读-改-写不会互相覆盖。
  • 审计与回滚⁠:每次改动产生 memory_version,可列、可取、可 redact(抹内容但留 actor+时间戳,用于泄密/PII 清除)。
  • 每 session 最多挂 8 个 store(可按"共享只读参考 + 每用户读写"分层)。

记忆 vs 上下文:用哪个?

新手常把"长上下文"当"记忆"。区分:

  • 上下文(context):这一次请求里塞的东西,一轮结束即逝(除非重发)。"窗口"。
  • 记忆(memory):跨请求/跨会话存活的东西,写在会话之外。"硬盘"。

需要"这轮看见"用上下文(RAG 把片段放进窗口);需要"以后还记得"用记忆(写文件)。compaction/context editing 是上下文层的管理,不是记忆。

设计与安全

好的记忆设计(也是本会话记忆的写法):

  • 一条经验一文件⁠,顶部一行摘要,便于召回判断相关性。
  • 只存非显然的⁠:代码/历史里已有的别存(否则是噪声),存"为什么""坑在哪"。
  • 写时更新、错时删除⁠:更新已有条目而非堆重复;过时的删掉。

安全红线:

  • 路径必须校验⁠:模型给的 path 是不可信输出。解析到规范路径(realpath/Path.resolve()),确认仍在记忆根目录内,⁠拒绝 ..、符号链接、绝对越界、URL 编码的遍历(%2e%2e%2f)。别直接 open() 原始 path。
  • 绝不存 secret:API key、密码、token 不进记忆。PII 谨慎(GDPR/CCPA);memory stores 的 redact 是事后补救手段。
  • 多租户隔离⁠:每用户独立记忆目录 + 鉴权;参考实现无内建访问控制。

最佳实践

  • 按"活多久"分三层用对工具。 会话内重发历史、临窗口用 compaction/context editing、跨会话用 memory tool/stores——别用重发历史去"记住偏好"。
  • 一条经验一文件,顶部一行摘要。 便于召回时判断相关性;一个 MEMORY.md 索引常驻、每条按需读(本会话就这么做)。
  • 只存非显然的。 代码/历史里已有的别存(是噪声),存"为什么""坑在哪""定下的约定"。
  • 写时更新、错时删除。 更新已有条目而非堆重复,过时的删掉。
  • 路径先规范化再校验边界。 模型给的 path 不可信,resolve() + 确认在记忆根内,拒绝 ../符号链接/URL 编码遍历(见 安全与防护)。
  • secret 绝不进记忆,多租户按用户隔离。 参考实现无内建访问控制;PII 谨慎,redact 只是事后补救。

权衡与失败模式

  • 什么都往记忆塞⁠:变噪声、召回受干扰 → 只存高价值非显然项。
  • 把记忆当事务数据库⁠:它是给模型读的笔记 → 并发写用 content_sha256 乐观锁(memory stores)或自管版本。
  • 跨会话串户⁠:多租户不隔离 → 按用户分目录 + 鉴权。
  • 路径未校验⁠:目录遍历读写任意文件 → 规范化 + 边界检查。

参考

  • Anthropic 官方文档⁠: Memory Tool、Managed Agents Memory Stores(platform.claude.com)

Keywords: stateless, 会话历史, prompt caching, compaction, context editing, model_context_window_exceeded, persistent memory, memory tool, memory_20250818, /memories, view/create/str_replace/insert/delete/rename, BetaAbstractMemoryTool, memory stores, memstore, FUSE mount, access, content_sha256, precondition, redact, memory version, path traversal, secret, PII, 多租户隔离