Learn-Claude-Code | 笔记 | Memory Management | s09_new Memory
目录
写在前面
learn-claude-code 项目目前有两条教程线,一条是之前的 s12 章节的,另一条是最近更新的 s20 章节的,大家如果刚学习这个项目的话推荐直接看最新的 s20 章节的教程即可,由于博主之前学习过 s12 章节的内容,因此打算把 s20 章节中新增章节的内容给补充学习,内容重复的章节博主这边就跳过了。
下面是旧版到新版的对应关系:
| Legacy 12-lesson track | Current 20-lesson track | Topic |
|---|---|---|
| old s01 | new s01 | Agent Loop |
| old s02 | new s02 | Tool Use |
| old s03 | new s05 | TodoWrite |
| old s04 | new s06 | Subagent |
| old s05 | new s07 | Skill Loading |
| old s06 | new s08 | Context Compact |
| old s07 | new s12 | Task System |
| old s08 | new s13 | Background Tasks |
| old s09 | new s15 | Agent Teams |
| old s10 | new s16 | Team Protocols |
| old s11 | new s17 | Autonomous Agents |
| old s12 | new s18 | Worktree Isolation |
| new only | s03, s04, s09, s10, s11, s14, s19, s20 | Permission, Hooks, Memory, System Prompt, Error Recovery, Cron, MCP, Comprehensive Agent |
从上表中我们可以看出我们需要补充的内容包括 s03(已补充)、s04(已补充)、s09、s10、s11、s14、s19 以及 s20 八个章节的内容。
新版学习路径如下:
主线:能动手 → 能做复杂任务 → 能记住和恢复 → 能长期运行 → 能协作 → 能扩展并合体

前言
在上篇文章 Learn-Claude-Code | 笔记 | Tools & Execution | s04_new Hooks 中,我们介绍了开源项目 learn-claude-code 新版第四个章节 s04_new: Hooks 的内容,这篇文章我们继续跟着教程文档来学习记忆管理相关内容,记录下个人学习笔记,和大家一起分享交流😄
Note:本篇文章主要学习记录 新版教程 第三部分 Memory Management 中 s09: Memory 章节的内容。
github:https://github.com/shareAI-lab/learn-claude-code
reference:https://chatgpt.com/
1. s09: Memory
到了 s09,整个 Agent 系统开始进入一个非常关键的阶段:它不再只是 “当前会话内的智能体”,而是开始拥有跨会话延续能力。
在前面 s08 Context Compact 中,我们已经解决了一个问题:当上下文越来越长时,系统可以通过压缩管线把冗长历史变成更短的摘要,从而让当前会话继续跑下去。但是 s08 的压缩本质上仍然是 “会话内续命”。它关心的是:这一轮对话太长了,怎么压缩后继续执行?
而 s09 Memory 要解决的是另一个更长期的问题:有些信息不应该只活在当前上下文里,而应该跨会话保存下来,并在未来真正需要时重新加载回来。
比如用户说:
I prefer using tabs for indentation, not spaces.
这句话不是某一次任务的临时上下文,而是一个以后写代码时都可能需要遵守的偏好。如果它只存在于当前 messages 里,一旦会话结束、上下文压缩、或者下次重新打开项目,这条信息就可能丢失。Memory 系统的价值就在于,它把这些 “未来仍然有用的知识” 从短期上下文中提取出来,存入一个独立的长期记忆库。
所以,s09 的核心变化可以概括为一句话:s08 解决当前会话怎么不断,s09 解决跨会话知识怎么不丢。
2. 问题
在没有 Memory 之前,Agent 的上下文完全依赖 messages[]。用户输入、模型响应、工具结果都会被不断追加进去,然后再通过 compact 机制压缩。这个机制对于当前任务是有效的,但它有一个天然缺陷:上下文是一条会话内的时间线,不是一个可长期维护的知识库。
如果用户在某一轮对话中表达了偏好、项目约束、常用命令、目录结构、排查经验,这些信息虽然短期内可能有用,但长期来看很容易被压缩掉、截断掉,或者在新会话中彻底丢失。更麻烦的是,如果每次都把所有历史对话塞回 prompt,成本会越来越高,噪声也会越来越多。
因此,Memory 系统要解决的不是 “怎么保存全部历史”,而是要解决三个更工程化的问题:
第一,哪些信息值得长期保存?不是所有聊天内容都应该进入记忆,只有用户偏好、长期反馈、项目事实、参考线索这类未来仍然有用的信息才应该被提取出来。
第二,保存之后怎么低成本加载?如果每轮都把所有记忆文件全部注入上下文,Memory 很快就会变成新的上下文污染源。因此系统需要一个便宜的索引常驻机制,再按需加载少量相关记忆。
第三,记忆越来越多之后怎么整理?长期运行的 Agent 一定会产生重复、过时、冲突的记忆,所以 Memory 不能只是追加文件,还必须有低频的合并、去重和剪枝机制。
这也是 s09 相对 s08 最重要的变化:上下文管理从 “压缩历史” 扩展为 “存储、加载、提取、整理” 的完整长期记忆系统。
3. 解决方案
s09 的解决方案并不是把所有历史都塞进 prompt,而是引入一个独立的 .memory/ 目录,把长期知识从 messages[] 中拆出来。
整体结构大致如下:
.memory/
├── MEMORY.md # 记忆索引
├── user-preference-tabs.md
├── project-facts.md
└── lcc-visual-preference.md
其中 MEMORY.md 是一个轻量索引,保存每条记忆的名称和描述;具体的记忆内容则放在一个个独立的 Markdown 文件中。这样做的好处是,系统每轮都可以低成本读取索引,让 Agent 知道 “有哪些记忆存在”,但不会一上来就把所有完整内容全部加载进上下文。
教程中的 memory-subsystems.svg 图正好概括了这套系统的四个子模块:

从图中可以看到,Memory 被拆成了四个动作:存储、加载、提取、整理。存储负责把值得保留的信息写成 .memory/*.md 文件,并更新 MEMORY.md 索引;加载负责在每轮模型调用前,根据当前请求从记忆库里挑选相关文件注入上下文;提取负责在每轮结束后,从最近对话里识别新的偏好、约束或项目事实;整理则负责在记忆文件积累到一定数量后进行去重、合并和剪枝。
这里最关键的设计不是 “写文件” 本身,而是 Memory 被设计成一个围绕 Agent Loop 的旁路系统。它不会替代原来的工具调用流程,也不会改变模型执行任务的主路径,而是在模型调用前后分别插入两类动作:调用前加载相关记忆,调用后提取新增记忆。
因此,s09 的主线可以理解为:
messages[]
↓
s08 压缩管线
↓
s09 记忆加载
↓
LLM
↓
工具调用 / 返回结果
↓
每轮结束后提取新记忆
↓
必要时整理记忆库
这就让 Agent 拥有了一种更接近真实工程助手的能力:它不需要每次从零开始,也不需要把所有历史都背在当前上下文里,而是可以从长期记忆库中按需借用信息。
4. Memory Library 流程图分析
Web 教程中的 Memory Library 用 8 张图把 Memory 的生命周期讲得非常清楚。它不是在展示某个复杂 UI,而是在把 “学习、建索引、召回” 这三个阶段可视化。
1. A Fact Worth Keeping:一条值得保留的事实出现

第一张图中,用户在 Session A 中说:
Please keep LCC pages concrete for beginners.
这句话不是一次性的任务指令,而是一个未来仍然会影响输出风格的偏好:用户希望 LCC 页面面向初学者时更加具体,不要太抽象。
所以它符合 Memory 的保存条件。Memory 不是保存所有对话,而是保存 “未来还会用到的事实”。这正好对应代码中的 extract_memories():它会从最近对话里提取 user preferences、constraints、project facts,而不是把整段会话原封不动写入文件。
2. Stamp It After the Turn:在回合结束后盖章保存

第二张图强调 Memory 提取发生在有用工作结束之后,而不是一边执行任务一边不断打断主流程。
这和代码中的设计一致:只有当 response.stop_reason != "tool_use" 时,系统才执行:
extract_memories(pre_compress)
consolidate_memories()
也就是说,Memory 提取不是模型每次输出一个 token 就运行,也不是每次工具调用后都运行,而是在一次 turn 阶段性结束后再进行。这样主 loop 不会被记忆逻辑污染,模型仍然专注于当前任务;长期记忆则在任务完成后作为后处理发生。
3. Write One Memory File:写入单条记忆文件

第三张图展示了记忆写入后的状态:.memory 目录里出现了一个独立的记忆文件,比如 lcc_visual_preference.md。
这对应代码中的 write_memory_file()。一条记忆不是被塞进一个巨大的 JSON 文件里,而是被写成单独的 Markdown 文件,并带有 frontmatter。这样的好处是后续可以单独读取、单独选择、单独整理,也方便开发者直接打开查看。
这一步可以理解为:系统把对话里的短期信号,沉淀成了一个长期知识单元。
4. Update the Catalog:更新 MEMORY.md 索引

第四张图展示的是 catalog 更新。新记忆写入后,MEMORY.md 不再为空,而是出现了这条记忆的标题、描述和类型。
这正好对应代码中 _rebuild_index() 的作用。每次写入新的 .md 文件后,系统都会重建索引,确保 MEMORY.md 始终反映当前记忆库。
这个索引非常关键。因为未来 Agent 不应该每次都读取全部记忆正文,而是先看 catalog,判断哪些记忆可能相关。也就是说,MEMORY.md 是长期记忆系统的 “目录层”,.memory/*.md 是 “内容层”。
5. A Future Request Arrives:未来请求到来

第五张图中,Session B 出现了一个新请求:
Continue improving the web lesson visuals.
此时用户没有再次说明 “希望面向初学者、要具体一点”,但这个偏好已经存在于 Memory 中。
这正是 Memory 的价值所在:用户不需要反复告诉 Agent 同样的偏好,Agent 可以在未来请求中通过记忆库找回相关信息。
如果没有 Memory,这条偏好只会存在于 Session A;到了 Session B,Agent 就会像第一次见到用户一样重新开始。而有了 Memory,跨会话连续性才真正成立。
6. Catalog Picks One:索引选择相关记忆

第六张图展示了按需选择过程。Memory Library 里有多条记忆,例如:
Beginner visual preference
LCC web paths
Verification commands
但当前请求和 “web lesson visuals” 最相关的是 Beginner visual preference。因此系统只选择这一条,而不是把所有记忆都加载进上下文。
这对应代码中 select_relevant_memories() 的逻辑。它先构造记忆 catalog,再让 LLM side-query 根据当前对话选择相关记忆。这样做的关键价值是控制上下文噪声:不相关的记忆应该留在架子上,只有当前需要的记忆才进入阅读栈。
7. Build the Reading Stack:构造模型调用前的阅读栈

第七张图展示了模型调用前的阅读栈。当前请求在上,选中的记忆详情在下,它们一起进入 LLM。
这对应代码 load_memories() 返回的 <relevant_memories> 内容。系统会读取选中的记忆文件,把完整内容注入到 system prompt 后面:
if memories_content:
system += "\n\n" + memories_content
因此模型看到的不只是当前请求:
Continue improving the web lesson visuals.
还会看到相关记忆:
Prefer cards, boards, shelves, and workbenches over abstract flowcharts.
这时模型生成的答案自然会更贴合用户过去的偏好。
8. Continuity Without Clutter:没有上下文污染的连续性

最后一张图总结了 Memory 系统最核心的价值:连续性不是靠把所有历史都塞回上下文实现的,而是靠 “索引 + 选择 + 按需加载” 实现的。
当前请求只加载相关记忆,不相关的 LCC web paths、Verification commands 等文件仍然留在记忆库里,不进入本轮上下文。
这就解决了长期记忆系统最容易踩的坑:如果什么都记、什么都加载,Memory 很快会变成噪声;而 s09 的设计是让记忆库变成一个 “可检索的书架”,不是一个 “越来越长的聊天记录”。
5. 工作原理(代码分析)
在正式开始代码分析之前,我们先来看看教程中提供的一张 memory-overview.svg 总览图,它展示了 Memory 是如何插入 s08 压缩管线和 Agent Loop 的:

这张图的重点是对比 s08 和 s09。
蓝色部分表示 s08 已经存在的能力:messages[] 会先经过压缩管线,包括 budget、snip、micro 和 autoCompact。也就是说,s09 并没有删除 s08 的上下文压缩能力,而是在它上面继续扩展。
紫色部分表示 s09 新增的 Memory 能力。模型调用前,会通过 Loading 阶段从 .memory/ 中选择相关记忆,并把它们注入上下文;模型调用后,如果一轮结束,则通过 Extraction 阶段从对话中提取新记忆;当记忆文件数量增长到一定程度时,Consolidation 会触发整理,进行去重、合并和剪枝。
所以这张图传达的核心是:Memory 不是替代 Context Compact,而是接在 Compact 管线上的长期知识层。
从执行路径看,主循环仍然是:
messages[] → compact → LLM → tools → tool_result → messages[]
但 s09 在这个循环上额外插入了两个方向的记忆流:
.memory/ → Loading → LLM
LLM / messages → Extraction → .memory/
也就是说,Memory 和 messages 之间是双向关系。Memory 会影响当前模型调用,而当前对话又会反过来更新 Memory。这让 Agent 从 “一次性上下文驱动” 变成了 “长期知识驱动”。
OK,下面我们就来看看代码具体是如何实现的。
1. Memory 的存储结构
我们先来看 s09 新增的 Memory 存储部分。代码一开始就创建了 .memory/ 目录,并定义了 MEMORY.md 作为索引文件:
WORKDIR = Path.cwd()
MEMORY_DIR = WORKDIR / ".memory"; MEMORY_DIR.mkdir(exist_ok=True)
MEMORY_INDEX = MEMORY_DIR / "MEMORY.md"
这几行代码非常关键,因为它把 Memory 从普通的对话上下文里剥离出来,变成了工作区内一个独立的持久化目录。也就是说,Memory 不再依赖 Python 进程的内存,也不依赖当前 messages[] 是否还在,只要 .memory/ 目录存在,记忆就可以跨会话保留下来。
接着,代码定义了 Memory 的四种类型:
MEMORY_TYPES = ["user", "feedback", "project", "reference"]
这四类记忆分别对应不同的长期信息。user 更偏向用户身份、习惯和偏好;feedback 更偏向用户对 Agent 输出风格、工作方式的反馈;project 保存项目事实,比如目录结构、构建命令、业务约束;reference 则保存一些外部线索、文档入口、常用排查命令等。
这个分类看起来很简单,但它实际上是在给长期记忆加一个最小的语义结构。没有分类的话,Memory 就只是一堆文本;有了分类之后,系统后续做加载、提取、整理时,就可以更清楚地区分 “这是用户偏好” 还是 “这是项目事实”。
2. 单条记忆文件:Markdown + YAML Frontmatter
Memory 写入的核心函数是 write_memory_file:
def write_memory_file(name: str, mem_type: str, description: str, body: str):
"""Write a single memory file with YAML frontmatter."""
slug = name.lower().replace(" ", "-").replace("/", "-")
filename = f"{slug}.md"
filepath = MEMORY_DIR / filename
filepath.write_text(
f"---\nname: {name}\ndescription: {description}\ntype: {mem_type}\n---\n\n{body}\n"
)
_rebuild_index()
return filepath
这段代码做了三件事。
第一,它把记忆名称转换成文件名。比如 Beginner visual preference 会被转换成类似 beginner-visual-preference.md 的文件。这样每条记忆都有一个稳定的文件入口。
第二,它把记忆写成 Markdown 文件,并在文件顶部放入 YAML frontmatter:
---
name: Beginner visual preference
description: Use concrete mental models for LCC web pages.
type: feedback
---
Prefer cards, boards, shelves, and workbenches over abstract flowcharts.
这个设计很重要,因为它同时满足了机器可读和人类可读两个要求。机器可以解析 name、description、type,人也可以直接打开 .md 文件查看和修改具体内容。
第三,每次写入新记忆后都会调用 _rebuild_index()。这说明 MEMORY.md 不是手动维护的,而是由所有记忆文件自动生成的索引。换句话说,.memory/*.md 是详细内容,MEMORY.md 是轻量目录。这个结构正是后续 “索引常驻 + 内容按需加载” 的基础。
3. MEMORY.md:便宜的长期记忆索引
代码中还有一个 _rebuild_index(),它负责扫描 .memory/ 目录下的所有记忆文件,然后生成索引:
def _rebuild_index():
files = list_memory_files()
lines = ["# Memory Index", ""]
for f in files:
lines.append(f"- [{f['name']}]({f['filename']}) — {f['description']} [{f['type']}]")
MEMORY_INDEX.write_text("\n".join(lines))
这里的关键点是:索引只保留简短描述,不保留完整正文。
这和我们平时做知识库很像。真正的大段内容放在独立文件里,而目录页只写标题、链接和一句话摘要。这样 Agent 每轮都可以看到有哪些记忆,但不会因为记忆库越来越大而把上下文撑爆。
这也是 Memory 系统与普通 RAG 的一个区别。这里不是先做 embedding 检索,而是先用一个轻量可读索引,让模型通过 name + description 判断哪些记忆可能相关。教程文档里也强调了这一点:教学版保留了 LLM side-query 的选择方式,并提供关键词匹配作为降级路径。
4. 记忆加载:索引常驻 + 相关内容按需注入
Memory 加载部分主要由 build_system()、select_relevant_memories() 和 load_memories 三个函数组成。
先看 build_system():
def build_system() -> str:
index = read_memory_index()
memories_section = f"\n\nMemories available:\n{index}" if index else ""
return (
f"You are a coding agent at {WORKDIR}."
f"{memories_section}\n"
"Relevant memories are injected below. Respect user preferences from memory.\n"
"When the user says 'remember' or expresses a clear preference, extract it as a memory."
)
这里做的是第一层加载:把 MEMORY.md 索引注入到 system prompt 中。
注意,它注入的不是所有完整记忆,而只是索引。这样模型每轮都知道 “有哪些记忆文件可用”,但不会把全部内容一次性塞进上下文。这个设计非常符合长期记忆系统的工程原则:目录可以常驻,正文必须按需加载。
接着看 load_memories():
def load_memories(messages: list) -> str:
"""Load relevant memory content for injection into context."""
selected_files = select_relevant_memories(messages)
if not selected_files:
return ""
parts = ["<relevant_memories>"]
for filename in selected_files:
content = read_memory_file(filename)
if content:
parts.append(content)
parts.append("</relevant_memories>")
return "\n\n".join(parts)
这个函数做的是第二层加载:先通过 select_relevant_memories(messages) 选出当前请求相关的记忆文件,再读取完整内容,并包在 <relevant_memories> 标签中注入到上下文。
这里的 <relevant_memories> 很有工程意义。它给模型一个明确边界:下面这些不是用户当前说的话,也不是工具结果,而是系统从长期记忆中加载出来的相关背景。这样模型在理解上下文时不容易混淆来源。
真正决定 “选哪些记忆” 的是 select_relevant_memories()。它会把最近对话和记忆目录一起发给模型,让模型返回相关记忆的索引:
response = client.messages.create(
model=MODEL,
messages=[{"role": "user", "content": prompt}],
max_tokens=200
)
如果模型选择失败,代码还会退回到关键词匹配:
keywords = [w.lower() for w in recent.split() if len(w) > 3]
selected = []
for f in files:
text = (f["name"] + " " + f["description"]).lower()
if any(kw in text for kw in keywords):
selected.append(f["filename"])
if len(selected) >= max_items:
break
return selected
这就是一个典型的 harness 工程思路:优先使用模型做语义选择,但不能完全依赖模型;一旦 side-query 失败,还要有简单、稳定、便宜的降级路径。文档中也提到,教学版采用 “LLM side-query + 关键词降级” 的方式来控制开销和稳定性。
5. 记忆提取:每轮结束后从对话中抽取长期信息
Memory 系统不能只加载,还必须能自动增长。否则它就只是一个手动维护的笔记目录,而不是 Agent 自己能持续积累的长期记忆。
这部分由 extract_memories() 完成:
def extract_memories(messages: list):
"""Extract new memories from recent dialogue. Runs after each turn."""
dialogue_parts = []
for msg in messages[-10:]:
role = msg.get("role", "?")
content = msg.get("content", "")
...
if isinstance(content, str) and content.strip():
dialogue_parts.append(f"{role}: {content}")
dialogue = "\n".join(dialogue_parts)
它首先从最近 10 条消息里整理出对话文本。这里不是扫描全部历史,而是只看最近窗口,因为新记忆通常来自刚刚发生的交互。这样可以避免提取 prompt 太长,也可以降低重复提取旧信息的概率。
接着,它会读取已有记忆,用来避免重复:
existing = list_memory_files()
existing_desc = "\n".join(
f"- {m['name']}: {m['description']}" for m in existing
) if existing else "(none)"
然后构造一个专门的提取 prompt:
prompt = (
"Extract user preferences, constraints, or project facts from this dialogue.\n"
"Return a JSON array. Each item: {name, type, description, body}.\n"
"- name: short kebab-case identifier (e.g. 'user-preference-tabs')\n"
"- type: one of 'user' (user preference), 'feedback' (guidance), "
"'project' (project fact), 'reference' (external pointer)\n"
"- description: one-line summary for index lookup\n"
"- body: full detail in markdown\n"
"If nothing new or already covered by existing memories, return [].\n\n"
f"Existing memories:\n{existing_desc}\n\n"
f"Dialogue:\n{dialogue[:4000]}"
)
这段 prompt 的设计非常明确:只提取用户偏好、约束、项目事实;返回结构化 JSON;如果没有新信息或者已有记忆已经覆盖,就返回空数组。
这说明 Memory 提取不是 “见什么都存”,而是带有筛选标准的。真正值得保存的是未来还会影响 Agent 行为的信息,而不是每一轮对话的流水账。
如果模型返回了有效记忆,代码会逐条写入文件:
for mem in items:
name = mem.get("name", f"memory_{int(time.time())}")
mem_type = mem.get("type", "user")
desc = mem.get("description", "")
body = mem.get("body", "")
if desc and body:
write_memory_file(name, mem_type, desc, body)
count += 1
因此,Memory 的写入链路就是:
recent dialogue
↓
LLM extraction
↓
JSON array
↓
write_memory_file()
↓
.memory/*.md
↓
_rebuild_index()
↓
MEMORY.md
这也是 s09 相比 s08 最核心的新增能力之一:上下文不再只是被压缩,而是会在每轮结束后被反向提炼成长期资产。
6. 记忆整理:Consolidation / Dream
长期记忆如果只追加不整理,很快就会失控。所以 s09 还加入了 consolidate_memories():
CONSOLIDATE_THRESHOLD = 10
def consolidate_memories():
"""Merge duplicate/stale memories. Triggered when file count ≥ threshold."""
files = list_memory_files()
if len(files) < CONSOLIDATE_THRESHOLD:
return
教学版的触发条件非常简单:当记忆文件数量达到 10 个时,触发整理。真实 Claude Code 中的 Dream 机制更复杂,有时间门控、扫描节流、会话门控和文件锁等多层保护;教学版这里用文件数量阈值做了简化。教程文档也明确说明,CC 的 Dream 实际有更复杂的四层门控,而教学版简化成文件数阈值。
整理时,代码会把所有记忆拼成 catalog:
catalog = "\n\n".join(
f"## {f['filename']}\nname: {f['name']}\ndescription: {f['description']}\n{f['body']}"
for f in files
)
然后要求模型执行四件事:
prompt = (
"Consolidate the following memory files. Rules:\n"
"1. Merge duplicates into one\n"
"2. Remove outdated/contradicted memories\n"
"3. Keep the total under 30 memories\n"
"4. Preserve important user preferences above all\n"
"Return a JSON array. Each item: {name, type, description, body}.\n\n"
f"{catalog[:16000]}"
)
这里的整理逻辑非常像知识库维护:合并重复项,删除过时或冲突信息,控制总量,并优先保留重要用户偏好。
如果整理成功,代码会删除旧文件,再写入整理后的新文件:
for f in MEMORY_DIR.glob("*.md"):
if f.name != "MEMORY.md":
f.unlink()
for mem in items:
...
write_memory_file(name, mem_type, desc, body)
这个过程意味着 Memory 不是单纯 append-only 的日志,而是一个会周期性重写和修剪的知识库。它关注的不是 “记录所有发生过的事”,而是 “保留未来最有用的信息”。
7. Agent Loop:Memory 如何插入主循环
最后看 agent_loop(),这是理解 s09 的关键。
def agent_loop(messages: list):
reactive_retries = 0
while True:
# s09: rebuild system with current memory index + relevant memories
system = build_system()
memories_content = load_memories(messages)
if memories_content:
system += "\n\n" + memories_content
每轮进入模型前,系统都会重新构建 system prompt,并加载相关记忆。也就是说,Memory 加载发生在模型调用之前。
接着,代码保存了一份压缩前快照:
pre_compress = [m if isinstance(m, dict) else {"role": m.get("role",""),
"content": str(m.get("content",""))} for m in messages]
这一步非常值得注意。因为 s09 仍然保留了 s08 的压缩管线:
messages[:] = tool_result_budget(messages)
messages[:] = snip_compact(messages)
messages[:] = micro_compact(messages)
如果直接从压缩后的 messages 中提取记忆,可能会漏掉一些原始细节。因此这里先保存 pre_compress,等到当前 turn 结束时,再从压缩前快照里提取记忆。这个细节体现了 Memory 与 Compact 的关系:Compact 服务于当前上下文长度,Memory 提取需要尽量保留原始语义。
当模型不再调用工具时,系统进入结束分支:
if response.stop_reason != "tool_use":
# s09: extract from pre-compression snapshot for full fidelity
extract_memories(pre_compress)
consolidate_memories()
return
这就是 Memory 的提取时机:不是工具执行之前,也不是每个工具调用之后,而是在当前轮模型停止调用工具时。换句话说,等一次任务阶段性结束后,系统才回头看这轮对话里有没有值得长期保存的信息。
这和 s04 Hooks 中的 Stop Hook 思路非常接近。主 loop 保持简单,扩展逻辑挂在关键生命周期点上。只不过 s09 教学版没有显式复用 Hook 注册系统,而是直接把 extract_memories() 和 consolidate_memories() 写在 stop_reason != "tool_use" 分支中。
所以 s09 的 agent loop 可以总结成:
- 每轮开始:
build_system()load_memories()- s08 compact
- 模型调用:
- LLM decides tool_use or final answer
- 如果 tool_use:
- 执行工具
- tool_result 回灌 messages
- 进入下一轮
- 如果 stop:
extract_memories(pre_compress)consolidate_memories()- 结束本轮
这也是 s09 最重要的工程结论:Memory 并没有推翻原来的 Agent Loop,而是在 loop 的前后加了两层长期知识管理能力。
博主在给定下面的提示词情况下:
I prefer using tabs for indentation, not spaces. Remember that.
想通过调试看看整个过程发生了什么,我们来具体分析下:
Prompt 1:Loop 1

Prompt 1:第一次循环加载记忆

Prompt 1:第一次循环模型响应

Prompt 1:第一次循环工具执行结果
先看第一轮循环开始前的加载阶段。agent_loop() 一进入 while True,就会先执行 build_system(),然后调用 load_memories(messages)。从调试结果里可以很清楚地看到,此时 system 中只有基础的 system prompt 与 memory 相关说明,而 memories_content 为空字符串,这说明当前 .memory/ 目录中还没有任何可供检索的历史记忆。换句话说,s09 并不会 “假装自己记得”,而是会先显式检查当前记忆库里到底有没有东西,再决定是否往上下文中注入。
接着再看模型第一次响应。可以看到,这一轮返回的依旧是我们熟悉的 思考 block + 工具 block 结构:思考 block 说明模型已经识别出 “用户刚刚表达了一个明确偏好”,而工具 block 则选择执行一个 bash 调用,把这句偏好原样输出出来。
工具执行结果也证明了这一点。第一轮工具调用本身并没有写 memory 文件,它只是把用户偏好显式地落入了对话轨迹,使后续的 extract_memories(pre_compress) 能够基于完整上下文,把这条信息识别为 “值得长期保存的知识”。因此,这一轮最重要的意义不是 “执行了什么强工具操作”,而是 把一个未来可提取的长期事实,送进了当前会话链路里。
Prompt 1:Loop 2

Prompt 1:第二次循环模型响应

Prompt 1:第二次循环提取记忆
Note:针对提取记忆函数 extract_memories(...) 博主简单修改了源码,遍历 response.content,找到第一个包含 text 属性的块。源码中是直接提取的 response.content 列表中的第一个元素的 text 文本,而针对于思考模型例如 deepseek-v4-pro 而言,第一个元素其实是 ThinkingBlock(没有 .text 属性),第二个才是真正的 TextBlock。

Prompt 1:第二次循环写入记忆
到了第二轮循环,上一轮的 tool_result 已经被追加回 messages[],所以模型这次不再需要继续请求工具,而是直接给出自然语言确认,明确告诉用户:已经记住了这个偏好,之后会优先使用 tabs 进行缩进。这一点非常关键,因为它意味着此时 response.stop_reason != "tool_use",程序会进入 s09 新增的后处理分支,也就是:
extract_memories(pre_compress)
consolidate_memories()
这里最值得注意的是,extract_memories() 使用的不是已经被压缩过的 messages,而是前面保留下来的 pre_compress 快照。这样做的工程意义非常强:提取记忆时要看的是最完整、最高保真的原始对话,而不是已经被 budget / snip / micro 裁剪后的版本。
调试里也能看到,提取函数会再次调用模型,让它根据 dialogue 与 existing_desc 去生成结构化 JSON。最终返回的文本 block 中,已经包含了一条完整记忆对象:name 是 user-preference-tabs,type 是 user,description 是 “User prefers tabs for indentation instead of spaces”,而 body 则是更完整的正文描述。
随后,系统把这条结构化记忆真正写入磁盘。一方面,在 .memory/ 目录下新建了 user-preference-tabs.md 文件,文件内部带有 YAML frontmatter;另一方面,又把这条记忆的摘要追加进 MEMORY.md 作为索引项。调试界面里还能看到控制台输出 [Memory: extracted 1 new memories],这说明从这一刻开始,这条偏好已经不再只是当前会话中的一句话,而是变成了 跨会话可复用的长期知识。也就是说,s09 在这一轮真正完成的,不是模型 “口头上说记住了”,而是系统层面把 “记住” 这件事落实成了可持久化的工程机制。
OK,本轮对话结束后我们继续使用下面的提示词观察下 Agent 是否用了 tab:
Create a Python file called test.py
Prompt 2:Loop 1

Prompt 2:第一次循环加载记忆

Prompt 2:第一次循环模型响应

Prompt 2:第一次循环工具执行结果
第二组提示词 "Create a Python file called test.py" 开始时,最应该观察的就是:这次循环开始前,系统是否真的把上一轮保存下来的记忆重新读了回来。调试结果给出的答案是肯定的。可以看到,build_system() 之后,load_memories(messages) 已经不再返回空字符串了:system 中先列出了 Memories available 的索引摘要,而 memories_content 里又进一步注入了一段 <relevant_memories> 内容,其中完整包含了 user-preference-tabs 这条记忆的 name、description、type 和正文内容。
这说明 s09 的记忆加载不是粗暴地把整个 .memory/ 目录全塞进上下文,而是先利用 MEMORY.md 这个轻量 catalog 做筛选,再把当前请求真正相关的条目正文加载进来。也正因为这一段记忆在模型调用前已经可见,所以模型第一次响应里非常自然地写出了 “应该按照用户偏好使用 tabs” 的思考过程,并生成了一个 write_file 的工具调用。继续看工具参数中的 content,可以清楚看到函数体和 if __name__ == "__main__": 下面的缩进都采用了 \t,也就是制表符而不是空格。
工具执行结果进一步坐实了这一点。调试中 results[0]["content"] 显示文件已经成功写入,而右侧实际打开的 test.py 文件也能直观看到 print("Hello, world!") 和 main() 调用前面都是 tab 缩进。这个现象非常重要,因为它说明第二轮并不是模型 “临时灵机一动” 想起了用户偏好,而是 Memory Loading 在 loop 的前半段已经把上一轮保存下来的长期知识重新回灌到了 prompt 中,从而直接影响了这一轮工具调用参数的生成。
Prompt 2:Loop 2

Prompt 2:第二次循环模型响应
进入下一轮循环后,工具结果同样已经被追加回 messages[],因此模型不再继续请求工具,而是直接给出自然语言确认:test.py 已经创建完成,并且所有缩进都使用了制表符(tabs)。这一点很好地说明了,虽然 s09 引入了 Memory 机制,但 原来的两段式 loop 节奏并没有被破坏:需要执行工具时,模型先走 tool_use;工具执行完成后,下一轮再根据结果给出总结或解释。Memory 的加入,并没有改写这个基本流程,只是在模型每次进入循环之前,先给它补充了一份跨会话上下文。
从工程角度看,这一轮更有价值的地方在于,它验证了 “memory 注入 → 影响工具行为 → 结果再反馈回 loop” 这条完整链路是通的。模型之所以能够稳定地在结果说明里强调 “用了 tabs”,根本原因并不是它在本轮内部临时推断出了偏好,而是上一轮提取下来的长期记忆已经成功参与了本轮推理。至于这一轮结束时的 extract_memories(),逻辑上依然会照常执行,但因为这轮对话没有新增值得长期保存的新偏好或新事实,所以不会重复写出一条等价 memory,从而保持记忆库的整洁性。这也进一步体现出 s09 的 Memory 并不是 “逢对话必记”,而是 只在有长期价值时才落盘。
OK,本轮对话结束后我们继续使用下面的提示词观察下 Agent 是否记得:
What did I tell you about my preferences?
Prompt 3:Loop 1

Prompt 3:第一次循环模型响应

Prompt 3:第一次循环工具执行结果
第三组提示词变成了一个纯问答问题:"What did I tell you about my preferences?"。这一轮其实最能体现 Memory 机制的真正价值。调试结果显示,在 loop 一开始,系统再次调用 load_memories(messages),把 .memory/ 中与当前问题最相关的那条 user-preference-tabs 记忆重新加载进 system context。此时,模型在真正生成响应之前,就已经拥有了这条跨会话保存下来的背景知识。
也正因如此,模型第一次响应就不再需要任何工具调用,而是直接给出文本回答:你告诉过我,你更喜欢使用 tabs 而不是空格来进行缩进。这一点非常关键,因为它说明 Memory 在这里已经不是 “辅助工具调用的附属机制” 了,而是直接变成了模型回答问题所依赖的知识来源。换句话说,一旦记忆被成功注入,它在模型眼里就和普通上下文没有本质区别,可以直接进入推理链条,支持理解、生成与回答。
因此,这一轮最值得强调的不是 “模型回答对了” 这么简单,而是:s09 让 Agent 真正获得了一种跨会话、可持久化、可检索、可回灌的长期知识能力。第一轮把事实提取出来,第二轮把事实用于行为生成,第三轮则把事实用于直接问答,这三步完整串起来以后,Memory 的工程价值就非常清晰了。
把这三组提示词连起来看,s09 最重要的工程结论就非常明确了:Memory 并没有推翻原来的 Agent Loop,而是在 loop 的前后各插入了一层长期知识管理能力。进入循环之前,系统先通过 load_memories() 从 .memory/ 和 MEMORY.md 中检索与当前请求相关的历史知识,并把它按需注入到 system prompt;结束循环之后,系统再通过 extract_memories(pre_compress) 和 consolidate_memories(),把本轮对话中新产生、又值得长期保存的信息提取出来并持久化写入磁盘。
第一组提示词验证的是 “没有历史记忆时,系统如何在回合结束后完成提取与存储”;第二组提示词验证的是 “历史记忆如何在下一轮加载回来,并真实影响工具调用与代码生成”;第三组提示词则进一步验证了 “记忆一旦被重新注入,就可以像普通上下文一样直接支持问答,而无需再借助工具”。所以从工程实现上看,s09 的核心并不是重新发明了一套 Agent Loop,而是 在既有 loop 前面补上了 recall,在既有 loop 后面补上了 extraction / consolidation,从而把原本一次性的上下文,扩展成了一个可持续积累、可重复利用的长期知识闭环。
OK,以上就是 s09 Memory 工作原理的完整分析了。
那大家感兴趣的话可以试试下面这些 prompt 感受下记忆机制引入后的一些变化(分多轮输入,观察记忆的累积和加载):
1. I prefer using tabs for indentation, not spaces. Remember that.
2. Create a Python file called test.py(观察 Agent 是否用了 tab)
3. What did I tell you about my preferences?(观察 Agent 是否记得)
4. I also prefer single quotes over double quotes for strings.
6. 相对 s08 的变更
| 组件 | 之前 (s08) | 之后 (s09) |
|---|---|---|
| 记忆能力 | 无(压缩后偏好随摘要退化) | 存储 + 加载 + 提取 + 整理 |
| 新函数 | — | write_memory_file, select_relevant_memories, load_memories, extract_memories, consolidate_memories |
| 存储 | — | .memory/MEMORY.md 索引 + .memory/*.md 文件 |
| 工具 | bash, read, write, edit, glob, todo_write, task, load_skill, compact (9) | bash, read_file, write_file, edit_file, glob, task (6) |
| 循环 | 每轮只做压缩 | 每轮注入记忆 + 压缩 + 每轮结束后提取 + 定期整理 |
7. 小结
s09 Memory 是 Learn Claude Code 这条教程线中非常关键的一节。前面的章节让 Agent 具备了工具调用、权限控制、Hook 扩展、任务拆解和上下文压缩能力,但这些能力大多还停留在 “当前会话” 里。到了 s09,系统第一次真正开始拥有跨会话的长期知识。
从工程实现上看,Memory 并没有把架构变复杂。它仍然遵循这个项目一直强调的原则:主循环保持简单,能力层不断外接。.memory/ 负责存储,MEMORY.md 负责索引,load_memories() 负责按需加载,extract_memories() 负责回合结束后提取,consolidate_memories() 负责低频整理。所有这些能力都围绕原来的 Agent Loop 展开,而不是重新发明一个新的运行框架。
更重要的是,s09 让我们看到一个成熟 Agent 系统必须具备的能力:它不能只会处理眼前的 prompt,还要能积累用户偏好、项目事实和长期反馈,并在未来合适的时候重新使用这些信息。
所以,如果说 s08 的关键词是 “压缩”,那么 s09 的关键词就是 “沉淀”。压缩让当前会话继续跑下去,沉淀让未来会话不再从零开始。
OK,以上就是本期想要分享的全部内容了。
结语
本篇文章我们围绕 s09 Memory 这一节,完整梳理了 Agent 是如何将跨会话长期知识引入系统,使其不再只依赖当前上下文的即时信息。
相比 s08 的 Context Compact,s09 的核心变化在于:Agent 不再只是压缩当前会话,而是能够主动沉淀、存储、检索和整理未来仍会使用的知识。通过
.memory/目录存储记忆、MEMORY.md维护索引,以及load_memories()、extract_memories()、consolidate_memories()等函数,系统形成了一个按需回灌、低噪声的长期知识闭环。从工程视角来看,Memory 的设计非常克制:它没有改变原有 Agent Loop 的执行节奏,而是在循环前加载相关记忆,循环后提取和整理新的长期信息。这样既保证了主循环的简洁性,又让 Agent 获得了跨会话连续性,能够记住用户偏好、项目事实和长期反馈,并在未来使用时自动注入上下文。
进一步来看,s09 的真正价值不只是 “存储信息”,而是让整个系统第一次具备了沉淀能力:过去的交互可以转化为可复用资产,未来的交互可以直接利用这些知识,从而显著提升长期任务执行的连续性和一致性。Memory 使 Agent 从一次性对话工具,进化为真正能够长期服务、可持续运作的智能助手。
总而言之,如果 s08 的关键词是 “压缩”,那么 s09 的关键词就是 “沉淀”。压缩保证当前会话顺利进行,沉淀保证未来会话不再从零开始。这一步,不仅完善了 Agent 的长期运行能力,也为后续章节的协作、多 Agent 系统和复杂任务管理奠定了坚实的知识基础。
下篇文章我们将来学习新版教程 s10 System Prompt 章节的内容,敬请期待🤗
参考
更多推荐



所有评论(0)