记忆存储:用 Markdown 分层持久化,而不是向量库 🍅
Claude Code 把记忆分成显式与隐式两层,分工很干净。
显式记忆是人写给 Claude 看的,放在 CLAUDE.md 或 .claude/rules/ 里,内容是项目架构说明、编码规范、工作流约束,本质上是一份“在这个代码库里该怎么干活”的指令集。它的价值在于把团队经验和规则固化下来,让 Claude 像个入职培训过的同事,而不是退回到互联网平均水平的通用做法。
隐式记忆是 Claude 记给自己的笔记。会话过程中,系统观察对话和操作轨迹,触发条件满足时调用一个小模型提炼关键信息,写进持久化文件。两层合起来是一套双轨结构:显式记忆自上而下给约束和方向,隐式记忆自下而上攒实践反馈。
更有意思的决定在底座上。多数 AI 记忆系统的第一反应是 embedding 加向量检索,Claude Code 却把全部记忆存成 Markdown 文件,放在 ~/.claude/projects/<project>/memory/ 下:
- MEMORY.md 是一份轻量索引(≤200 行 / ≤25KB,启动时加载),每行一条记忆的标题和简短描述,让模型知道有什么,而不必加载任何正文。
- 主题文件(
*.md)承载真正的内容:结构化的 YAML frontmatter(title、description、type、date 等)加自由正文。
这个选择等于拿检索基础设施换可读性。Markdown 文件用普通工具就能读、能 diff、能手改、能删;没有一个会和内容渐渐脱节的 embedding 索引,也不用运维一套独立的数据库。索引加主题文件的拆分本身就是一种渐进式披露:启动时付一笔很小的固定成本换来“知道有什么”,任何一条记忆的完整成本都推迟到真正用到的那一刻。
记忆的来路有三条:主 agent 在用户明确指示下写入、会话结束后的 Extract Memories 自动提取、用户直接手改文件。三条路最后都汇到 memdir 的主题文件上,MEMORY.md 同步更新,保证入口可发现。同一套形态还能继续外推而不变形:team/ 子目录承载团队共享记忆,Agent Memory 用独立目录做隔离,KAIROS 模式先写 append-only 日志,再蒸馏回主题文件和索引。
记忆召回:让 Sonnet 来挑,而不是算相似度 🍅
召回是“不用向量库”这个决定要接受检验的地方。每轮用户提问,findRelevantMemories.ts 都用 Sonnet 做一次真正的模型调用,而不是 embedding 相似度计算,从全部记忆里最多挑出 5 条注入当前上下文。
流程是这样:递归扫描 .md 文件、提取 frontmatter;过滤掉本次会话已经展示过的条目;发一次轻量的旁路调用做语义筛选(sideQuery:不带主对话历史,用 Sonnet 而非 Opus,max_tokens 只有 256,JSON Schema 输出,只返回文件名列表);最后读取选中文件的全文,给超过一天没更新的条目追加陈旧警告。
整个过程和主 API 调用并行,作为预取执行。预取要是跑输了主调用,本轮直接跳过、下轮重试:召回永远不允许给主链路添延迟。挑选策略有意偏保守,背后的不对称算得很清楚:漏掉一条相关记忆,损失的只是一点质量;注入一条错误记忆,污染的却是之后每一步推理的前提。预取在作用域结束时自动中止;跳过条件包括单词级的 prompt、会话字节上限、可选记忆列表为空。
graph LR
Start((START)) --> Step1[1. Scan MD & Build List]
Step1 --> Step2[2. Filter Displayed]
Step2 --> SonnetAPI(3. Semantic Filtering)
SonnetAPI --> Step4[4. Read File Content]
Step4 --> CheckStaleness{5. Is Stale?}
CheckStaleness -- "Yes - Over 1 Day" --> AddWarning[6. Append Warning]
CheckStaleness -- "No" --> FinalStep[Ready to Inject]
AddWarning --> Context((CONTEXT))
FinalStep --> Context
Auto Dream:给臃肿的记忆做后处理 🍅
只进不出的记忆库一定会退化:条目过时,近似重复越攒越多,索引和它描述的文件渐渐对不上。Auto Dream 就是往回收拾的整理引擎。它在会话结束后经过多层门控(时间间隔、会话数量、功能开关等)低频触发,拉起一个受限权限的 forked agent,对 memdir 做一遍四阶段处理:扫描整体结构,收集变化信号,执行整合(合并重复、修正时间戳、删除冲突条目、清理失效引用),最后更新 MEMORY.md。
运行闭环:提取 → 存储 → 注入 🍅
提取发生在会话内:观察对话,触发即蒸馏,写 Markdown 主题文件,更新 MEMORY.md。
整合离线跑,也就是 Auto Dream(/dream):本地执行,文件锁保护,不调外部网络。
注入发生在新会话启动时:加载 MEMORY.md 前 200 行,连同各层级的 CLAUDE.md,优先级从低到高依次是组织规则 → 项目规则 → 用户规则 → CLAUDE.local.md。这个顺序有讲究:规则离具体开发者越近,加载越靠后,能覆盖的东西就越多。
这条闭环带来三个性质:
- 自动学习:哪些做法有效、哪些失败,系统自己总结,不靠人手工维护。
- 相关性驱动注入:只有最相关的一小片进上下文,记忆永远挤不占正事的位置。
- 工作连续性:技术决策和项目背景跨会话延续,不必每次重新推导一遍。
对 Agent 平台的借鉴 🍅
启示一:把执行轨迹蒸馏成可复用的经验
当某类 Workflow 呈现出稳定的成功模式时,自动提炼为隐式记忆,或沉淀进结构化 Skill 库成为可复用的 Skill。轨迹本来就是已经付过钱的数据;蒸馏这一步做不做,决定它是日志还是资产。
启示二:弱结构化记忆作为 Skill 的前置形态
建一条渐进的 Knowledge → Memory → Skill 链路:先以轻量 Markdown 记忆条目沉淀,等条目证明了自己,再升级为高可靠的正式 Skill。一上来就要求完整结构会扼杀记录,完全不要结构又没法用;分级链路把两头的坑都绕开了。
启示三:离线的记忆整理机制
参考 Auto Dream 的定位 → 收集 → 整合 → 修剪与索引循环,持续保住上下文的信噪比。整理这件事离线做便宜、会话内做昂贵,把它排到交互路径之外是正确的默认。
总结 🍅
Claude Code 的记忆设计展示了记忆从单点功能长成基础设施的样子:人写规则与自我蒸馏经验的双轨拆分,始终可读、可手改的 Markdown 分层持久化,以及让记忆库长期可信的离线整合。对长周期协作型 Agent 来说,这套组合值得借鉴。
Memory Storage: Markdown-Based Layered Persistence Over Vector Databases 🍅
Claude Code splits memory into two layers with a clean division of responsibility.
Explicit memory is written by humans, for Claude. It lives in CLAUDE.md or .claude/rules/ and carries project architecture overviews, coding conventions, and workflow constraints; in essence, an instruction set for how to work in this codebase. Its job is to encode team experience and rules so that Claude behaves like an onboarded teammate instead of defaulting to generic, web-average conventions.
Implicit memory is Claude’s own notes to itself. During each session the system observes the conversation and action trail; when trigger conditions fire, it calls a smaller model to distill the key information and write it to persistent files. The two layers form a dual track: explicit memory supplies constraints and direction from above, while implicit memory accumulates feedback from practice below.
The more interesting decision is the substrate. Where most AI memory systems reach for embeddings and vector retrieval, Claude Code persists all memory as Markdown files under ~/.claude/projects/<project>/memory/:
- MEMORY.md is a lightweight index (≤200 lines / ≤25KB, loaded at startup) listing titles and short descriptions, enough for the model to know what exists without loading any of it.
- Topic files (
*.md) hold the actual content: structured YAML frontmatter (title, description, type, date, etc.) followed by free-form body text.
This choice trades retrieval infrastructure for legibility. Markdown files can be read, diffed, hand-edited, and deleted by the user with ordinary tools; there is no embedding index that can drift out of sync with the content it describes, and no separate database to operate. The index-plus-topic-file split is itself a form of progressive disclosure: pay a small fixed cost at startup for awareness, defer the full cost of any memory until it is actually needed.
Memories arrive by three routes (the main agent writing on user instruction, an Extract Memories pass after a session, or the user editing files by hand), and all routes converge on topic files in memdir, with MEMORY.md kept in sync for discoverability. The same model extends further without changing shape: team memories in a team/ subdirectory, isolated Agent Memory directories, and KAIROS append-only logs that get distilled back into topic files and the index.
Memory Retrieval: Intelligent Recall Powered by Sonnet 🍅
Retrieval is where the no-vector-database decision has to prove itself. On every user query, findRelevantMemories.ts uses Sonnet (a model call, not embedding similarity) to select at most five memory entries for injection into the current context.
The pipeline: recursively scan the .md files and extract frontmatter; filter out entries already shown this session; run a lightweight side-query (Sonnet, no main conversation history, max_tokens 256, JSON Schema output) to make the semantic selection; then read the selected files in full, appending a staleness warning to any entry older than one day.
The whole thing runs in parallel with the main API call, as a prefetch. If the prefetch loses the race, that round simply skips and retries on the next iteration; retrieval is never allowed to add latency to the main path. The selection policy is deliberately conservative, and the asymmetry justifies it: missing one relevant memory costs a little quality, while injecting a wrong one pollutes the context that every subsequent step reasons from. Prefetch aborts when its scope ends; skip conditions include single-word prompts, session byte limits, and empty optional memory lists.
graph LR
Start((START)) --> Step1[1. Scan MD & Build List]
Step1 --> Step2[2. Filter Displayed]
Step2 --> SonnetAPI(3. Semantic Filtering)
SonnetAPI --> Step4[4. Read File Content]
Step4 --> CheckStaleness{5. Is Stale?}
CheckStaleness -- "Yes - Over 1 Day" --> AddWarning[6. Append Warning]
CheckStaleness -- "No" --> FinalStep[Ready to Inject]
AddWarning --> Context((CONTEXT))
FinalStep --> Context
Auto Dream: Post-Processing for Bloated Memories 🍅
Any memory store that only ever appends will degrade: entries go stale, near-duplicates accumulate, and the index drifts away from the files it describes. Auto Dream is the consolidation engine that pushes back. Gated, low-frequency triggers after sessions spin up a restricted forked agent that runs a four-phase pass over memdir: scan the structure, gather change signals, consolidate (merge duplicates, fix timestamps, delete conflicting entries, prune dead references), and update MEMORY.md.
Extract happens in-session: observe the dialogue, distill on trigger, write Markdown topic files, update MEMORY.md.
Consolidate runs offline via Auto Dream (/dream), local and lock-protected, with no external network calls.
Inject happens at session start: load the first 200 lines of MEMORY.md plus all CLAUDE.md tiers, priority low → high: org rules → project rules → user rules → CLAUDE.local.md. The ordering matters: the closer a rule sits to the individual developer, the later it loads and the more it can override.
Together the loop delivers three properties:
- Automatic learning: the system summarizes what worked and what failed without manual curation.
- Relevance-driven injection: only the most relevant slice enters context, so memory never crowds out the actual task.
- Working continuity: technical decisions and project background carry across sessions instead of being re-derived each time.
Insight 1: Distill execution traces into reusable experience
When a Workflow shows stable success patterns, auto-distill them into implicit memory or reusable Skills in a structured Skill library. The trace is already paid for; the distillation step is what converts it from a log into an asset.
Insight 2: Weakly structured memory as a Skill precursor
Build a progressive Knowledge → Memory → Skill pipeline: lightweight Markdown memory entries first, promotion to high-reliability formal Skills only once an entry has proven itself. Requiring full structure up front kills capture; requiring none makes the store unusable, and a staged pipeline avoids both failure modes.
Insight 3: Offline memory housekeeping
Adopt Auto Dream’s locate → collect → consolidate → prune-and-index loop to keep context high-signal. Consolidation is cheap offline and expensive in-session, so scheduling it away from the interactive path is the right default.
Conclusion 🍅
Claude Code’s memory design shows what it looks like when agent memory graduates from a feature into infrastructure: a dual-track split between human-authored rules and self-distilled experience, layered Markdown persistence that stays legible and hand-editable, and offline consolidation that keeps the store trustworthy over time. For long-horizon collaborative agents, that combination is a pattern worth refering.