本页目录
记忆与状态
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 清除 |
| 信息 | 概括保留 | 直接删 |
| Beta | compact-2026-01-12 | context-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(托管)
工作区级持久记忆库,对象模型三层:
机制要点:
- 挂载为文件系统: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, 多租户隔离