Claude code学习13 Memory

Active Recall

在Claude code的设计中,长期记忆被视作了一种珍贵的记忆,不是每轮都全量注入的背景资料。系统不先做 embedding 查向量库,也不会读取所有正文,而是在用户请求到来时执行低成本文件扫描。 关于embedding和rag,现在很多教程一上来就教人RAG+embedding,但其实RAG是一个比较重的工程,对于像是代码库你想要引射到向量数据库本身怎么chunk就不好说,并且对于代码这种错一个字符就不行的东西来说RAG的准确率不够 Claude的做法是用一个前缀开头,就像工具一样,写明做什么,搜索的时候先扫这些头,要召回后面再说

/**
 * Scan a memory directory for .md files, read their frontmatter, and return
 * a header list sorted newest-first (capped at MAX_MEMORY_FILES). Shared by
 * findRelevantMemories (query-time recall) and extractMemories (pre-injects
 * the listing so the extraction agent doesn't spend a turn on `ls`).
 *
 * Single-pass: readFileInRange stats internally and returns mtimeMs, so we
 * read-then-sort rather than stat-sort-read. For the common case (N ≤ 200)
 * this halves syscalls vs a separate stat round; for large N we read a few
 * extra small files but still avoid the double-stat on the surviving 200.
 */
export async function scanMemoryFiles(memoryDir: string, signal: AbortSignal): Promise<MemoryHeader[]> {
  try {
    const entries = await readdir(memoryDir, { recursive: true });
    const mdFiles = entries.filter((f) => f.endsWith(".md") && basename(f) !== "MEMORY.md");

    const headerResults = await Promise.allSettled(
      mdFiles.map(async (relativePath): Promise<MemoryHeader> => {
        const filePath = join(memoryDir, relativePath);
        const { content, mtimeMs } = await readFileInRange(filePath, 0, FRONTMATTER_MAX_LINES, undefined, signal);
        const { frontmatter } = parseFrontmatter(content, filePath);
        return {
          filename: relativePath,
          filePath,
          mtimeMs,
          description: frontmatter.description || null,
          type: parseMemoryType(frontmatter.type),
        };
      })
    );

    return headerResults
      .filter((r): r is PromiseFulfilledResult<MemoryHeader> => r.status === "fulfilled")
      .map((r) => r.value)
      .sort((a, b) => b.mtimeMs - a.mtimeMs)
      .slice(0, MAX_MEMORY_FILES);
  } catch {
    return [];
  }
}

/**
 * Format memory headers as a text manifest: one line per file with
 * [type] filename (timestamp): description. Used by both the recall
 * selector prompt and the extraction-agent prompt.
 */
export function formatMemoryManifest(memories: MemoryHeader[]): string {
  return memories
    .map((m) => {
      const tag = m.type ? `[${m.type}] ` : "";
      const ts = new Date(m.mtimeMs).toISOString();
      return m.description ? `- ${tag}${m.filename} (${ts}): ${m.description}` : `- ${tag}${m.filename} (${ts})`;
    })
    .join("\n");
}

在内部查找相关的记忆,根据注释也能看出来是用llm筛选的(我之前一直纳闷是怎么不通过模型来进行筛选例如工具的执行或者是记忆的)

/**
 * Find memory files relevant to a query by scanning memory file headers
 * and asking Sonnet to select the most relevant ones.
 *
 * Returns absolute file paths + mtime of the most relevant memories
 * (up to 5). Excludes MEMORY.md (already loaded in system prompt).
 * mtime is threaded through so callers can surface freshness to the
 * main model without a second stat.
 *
 * `alreadySurfaced` filters paths shown in prior turns before the
 * Sonnet call, so the selector spends its 5-slot budget on fresh
 * candidates instead of re-picking files the caller will discard.
 */
export async function findRelevantMemories(
  query: string,
  memoryDir: string,
  signal: AbortSignal,
  recentTools: readonly string[] = [],
  alreadySurfaced: ReadonlySet<string> = new Set()
): Promise<RelevantMemory[]> {
  const memories = (await scanMemoryFiles(memoryDir, signal)).filter((m) => !alreadySurfaced.has(m.filePath));
  if (memories.length === 0) {
    return [];
  }

  const selectedFilenames = await selectRelevantMemories(query, memories, signal, recentTools);
  const byFilename = new Map(memories.map((m) => [m.filename, m]));
  const selected = selectedFilenames.map((filename) => byFilename.get(filename)).filter((m): m is MemoryHeader => m !== undefined);

  // Fires even on empty selection: selection-rate needs the denominator,
  // and -1 ages distinguish "ran, picked nothing" from "never ran".
  if (feature("MEMORY_SHAPE_TELEMETRY")) {
    /* eslint-disable @typescript-eslint/no-require-imports */
    const { logMemoryRecallShape } = require("./memoryShapeTelemetry.js") as typeof import("./memoryShapeTelemetry.js");
    /* eslint-enable @typescript-eslint/no-require-imports */
    logMemoryRecallShape(memories, selected);
  }

  return selected.map((m) => ({ path: m.filePath, mtimeMs: m.mtimeMs }));
}

async function selectRelevantMemories(
  query: string,
  memories: MemoryHeader[],
  signal: AbortSignal,
  recentTools: readonly string[]
): Promise<string[]> {
  const validFilenames = new Set(memories.map((m) => m.filename));

  const manifest = formatMemoryManifest(memories);

  // When Claude Code is actively using a tool (e.g. mcp__X__spawn),
  // surfacing that tool's reference docs is noise — the conversation
  // already contains working usage.  The selector otherwise matches
  // on keyword overlap ("spawn" in query + "spawn" in a memory
  // description → false positive).
  const toolsSection = recentTools.length > 0 ? `\n\nRecently used tools: ${recentTools.join(", ")}` : "";

  try {
    const result = await sideQuery({
      model: getDefaultSonnetModel(),
      system: SELECT_MEMORIES_SYSTEM_PROMPT,
      skipSystemPromptPrefix: true,
      messages: [
        {
          role: "user",
          content: `Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}`,
        },
      ],
      max_tokens: 256,
      output_format: {
        type: "json_schema",
        schema: {
          type: "object",
          properties: {
            selected_memories: { type: "array", items: { type: "string" } },
          },
          required: ["selected_memories"],
          additionalProperties: false,
        },
      },
      signal,
      querySource: "memdir_relevance",
    });

    const textBlock = result.content.find((block) => block.type === "text");
    if (!textBlock || textBlock.type !== "text") {
      return [];
    }

    const parsed: { selected_memories: string[] } = jsonParse(textBlock.text);
    return parsed.selected_memories.filter((f) => validFilenames.has(f));
  } catch (e) {
    if (signal.aborted) {
      return [];
    }
    logForDebugging(`[memdir] selectRelevantMemories failed: ${errorMessage(e)}`, { level: "warn" });
    return [];
  }
}

findRelevantMemories —— 编排整个检索流程

它的职责包括:

  1. 扫描记忆文件:调用 scanMemoryFiles(memoryDir, signal)
  2. 去重过滤:用 alreadySurfaced 过滤掉之前回合已经展示过的记忆
  3. 调用模型选择:把过滤后的候选记忆交给 selectRelevantMemories
  4. 结果映射:把模型返回的”文件名”映射回完整的 MemoryHeader,再转成 RelevantMemory[]
  5. 遥测上报:如果开启了 MEMORY_SHAPE_TELEMETRY,记录召回形状

selectRelevantMemories —— 只负责”让模型挑文件名”

它的职责非常单一:

  1. 把 MemoryHeader[] 格式化成模型可读的清单(formatMemoryManifest)
  2. 构造 prompt:Query: xxx\n\nAvailable memories:\n…
  3. 调用 sideQuery 让 Sonnet 返回 JSON:{ selected_memories: string[] }
  4. 过滤非法文件名:用 validFilenames 校验模型返回的文件名是否真实存在,防止幻觉

三、数据流:一条完整的调用链

  用户 query
     ↓
  findRelevantMemories(query, memoryDir, signal, recentTools, alreadySurfaced)
     ↓
  scanMemoryFiles(memoryDir, signal) → MemoryHeader[]
     ↓
  过滤 alreadySurfaced → 候选记忆列表
     ↓
  selectRelevantMemories(query, 候选记忆, signal, recentTools)
     ↓
  formatMemoryManifest(候选记忆) → 文本清单
     ↓
  sideQuery({ system: SELECT_MEMORIES_SYSTEM_PROMPT, ... })
     ↓
  模型返回 selected_memories: string[]
     ↓
  校验文件名有效性
     ↓
  映射回 MemoryHeader → RelevantMemory[]

所以 findRelevantMemories 是”端到端检索”,selectRelevantMemories 只是中间那个”模型打分/选择”环节。

四、recentTools 到底什么意思?

这是你最困惑的地方。注释 src/memdir/findRelevantMemories.ts:23 写的是:

▎ If a list of recently-used tools is provided, do not select memories that are usage reference or API documentation for those tools (Claude Code is ▎ already exercising them). DO still select memories containing warnings, gotchas, or known issues about those tools — active use is exactly when those ▎ matter.

src/memdir/findRelevantMemories.ts:87-91 又解释了一遍:

▎ When Claude Code is actively using a tool (e.g. mcp__X__spawn), surfacing that tool’s reference docs is noise — the conversation already contains ▎ working usage. The selector otherwise matches on keyword overlap (“spawn” in query + “spawn” in a memory description → false positive).

翻译成大白话:

  • recentTools = 最近用过的工具列表
  • 作用 = 排除这些工具的使用说明/文档类记忆
  • 原因 = 这些文档已经在当前对话里被用上了,再推给主模型就是噪音,还可能因为关键词重叠造成误选
  • 例外 = 如果这些记忆里包含该工具的 警告、坑、已知问题,仍然要选,因为正在用时这些最重要

所以 recentTools 是用来防止过度推送的。

五、为什么要拆成两个函数?

这是典型的分层设计:

  ┌────────────────────┬────────────────────────┬─────────────────┬───────────────────────────────────────┐
  │        层级        │          函数          │     稳定性      │              测试关注点               │
  ├────────────────────┼────────────────────────┼─────────────────┼───────────────────────────────────────┤
  │ 业务 orchestration │ findRelevantMemories   │ 相对稳定        │ 扫描、过滤、映射、遥测是否正确        │
  ├────────────────────┼────────────────────────┼─────────────────┼───────────────────────────────────────┤
  │ LLM 调用细节       │ selectRelevantMemories │ 容易调整 prompt │ prompt 是否能让模型稳定返回合法文件名 │
  └────────────────────┴────────────────────────┴─────────────────┴───────────────────────────────────────┘

拆开后有几个好处:

  1. 可替换:以后如果想换检索方式(比如向量检索代替 LLM 选择),只需要替换 selectRelevantMemories,findRelevantMemories 的流程不用改。
  2. 可测试:可以单独测试 prompt 构造和结果过滤,而不需要真的扫描文件系统。
  3. 职责清晰:findRelevantMemories 管”找到记忆”,selectRelevantMemories 管”让模型选”。

六、一句话总结

findRelevantMemories 是”检索记忆的整体流程”,selectRelevantMemories 是它内部”让 Sonnet 从候选清单里挑文件名”的那一步;recentTools 不是检索目标,而是用来排除最近已用工具的参考文档,避免重复噪音。

通过使用llm读取处理过后的md文件的前缀(例如description)进行内容memory的召回

为什么召回阶段只读 frontmatter,往往比直接读取完整 memory 文件更适合生产级 Agent?

  1. 首先是因为.md格式即可以是用户写的,也可能是llm写的,即使规定了规则,但大小却是天差地别,如果一次性全读进来的话可能太大
  2. 第二是因为召回阶段不需要读取全文,就好像找书一样,我不需要把海底两万里全文看完才能找到海底两万里这本书
  3. 带有标签的前缀本身就更适合做搜索
  4. 长期记忆里面可能包含的例如链接,具体代码会稀释注意力,不如进过特别处理的description

六层注意力

他们分别是Auto Memory、Session Memory、AutoDream、Agent Memory、Team Memory、CLAUDE.md。

Claude.md

这里引用一下官方文档里面的说明

https://code.claude.com/docs/zh-CN/memory

CLAUDE.md 文件是 markdown 文件,为项目、你的个人工作流或整个组织为 Claude 提供持久指令。你用纯文本编写这些文件;Claude 在每个会话开始时读取它们。 Claude Code 通过从当前工作目录向上遍历目录树来读取 CLAUDE.md 文件,检查沿途的每个目录是否有 CLAUDE.md 和 CLAUDE.local.md 文件。这意味着如果你在 foo/bar/ 中运行 Claude Code,它会从 foo/bar/CLAUDE.md、foo/CLAUDE.md 和沿途的任何 CLAUDE.local.md 文件加载指令。 所有发现的文件被连接到上下文中,而不是相互覆盖。在目录树中,内容从文件系统根目录向下排序到你的工作目录。对于 foo/bar/ 示例,foo/CLAUDE.md 在上下文中出现在 foo/bar/CLAUDE.md 之前,因此更接近你启动 Claude 的位置的指令最后被读取。在每个目录中,CLAUDE.local.md 在 CLAUDE.md 之后附加,因此你的个人笔记是 Claude 在该级别读取的最后内容。 Claude 还在当前工作目录下的子目录中发现 CLAUDE.md 和 CLAUDE.local.md 文件。它们不是在启动时加载,而是在 Claude 读取这些子目录中的文件时包含。

何时添加到 CLAUDE.md

将 CLAUDE.md 视为你写下你本来会重新解释的内容的地方。在以下情况下添加到它: Claude 第二次犯同样的错误 代码审查发现 Claude 应该了解这个代码库的内容 你在聊天中输入的相同更正或澄清是你上个会话输入的 新队友需要相同的上下文才能提高生产力 将其保持为 Claude 应该在每个会话中保持的事实:构建命令、约定、项目布局、“总是做 X”规则。如果一个条目是多步骤过程或仅对代码库的一部分重要,将其移到 skill 或 路径范围规则 中。扩展概述涵盖何时使用每种机制。 CLAUDE.md 文件可以位于多个位置,每个位置有不同的范围。下表按加载顺序列出它们,从最广泛的范围到最具体的范围,因此项目指令在用户指令之后出现在上下文中。 作用域


范围	位置	目的	用例示例	共享对象
托管策略	• macOS: /Library/Application Support/ClaudeCode/CLAUDE.md
• Linux 和 WSL: /etc/claude-code/CLAUDE.md
• Windows: C:\Program Files\ClaudeCode\CLAUDE.md	由 IT/DevOps 管理的组织范围指令	公司编码标准、安全策略、合规要求	组织中的所有用户
用户指令	~/.claude/CLAUDE.md	所有项目的个人偏好	代码样式偏好、个人工具快捷方式	仅你(所有项目)
项目指令	./CLAUDE.md 或 ./.claude/CLAUDE.md	项目的团队共享指令	项目架构、编码标准、常见工作流	通过源代码控制的团队成员
本地指令	./CLAUDE.local.md	个人项目特定偏好;添加到 .gitignore	你的沙箱 URL、首选测试数据	仅你(当前项目)

Auto Memory

一般文件存放在~/.claude/projects/<项目根目录转义>/memory/里面 这类是Claude code自动生成的memory相关md 具体内容可以参照动态prompt1里面插入的内容,这里仅作为补充参考 有个功能叫做kairos KAIROS 让 Claude 从"一次性对话工具"变成"持久运行的 AI 助手":项目根目录转义>

  • 关闭终端后 Claude 仍在后台运行
  • 每天自动写日志
  • 晚上自动”做梦”整理记忆
  • 没人说话时自己找活干
  • 命令超 15 秒自动丢后台
/**
 * Assistant-mode daily-log prompt. Gated behind feature('KAIROS').
 *
 * Assistant sessions are effectively perpetual, so the agent writes memories
 * append-only to a date-named log file rather than maintaining MEMORY.md as
 * a live index. A separate nightly /dream skill distills logs into topic
 * files + MEMORY.md. MEMORY.md is still loaded into context (via claudemd.ts)
 * as the distilled index — this prompt only changes where NEW memories go.
 */
function buildAssistantDailyLogPrompt(skipIndex = false): string {
  const memoryDir = getAutoMemPath();
  // Describe the path as a pattern rather than inlining today's literal path:
  // this prompt is cached by systemPromptSection('memory', ...) and NOT
  // invalidated on date change. The model derives the current date from the
  // date_change attachment (appended at the tail on midnight rollover) rather
  // than the user-context message — the latter is intentionally left stale to
  // preserve the prompt cache prefix across midnight.
  const logPathPattern = join(memoryDir, "logs", "YYYY", "MM", "YYYY-MM-DD.md");

  const lines: string[] = [
    "# auto memory",
    "",
    `You have a persistent, file-based memory system found at: \`${memoryDir}\``,
    "",
    "This session is long-lived. As you work, record anything worth remembering by **appending** to today's daily log file:",
    "",
    `\`${logPathPattern}\``,
    "",
    "Substitute today's date (from `currentDate` in your context) for `YYYY-MM-DD`. When the date rolls over mid-session, start appending to the new day's file.",
    "",
    "Write each entry as a short timestamped bullet. Create the file (and parent directories) on first write if it does not exist. Do not rewrite or reorganize the log — it is append-only. A separate nightly process distills these logs into `MEMORY.md` and topic files.",
    "",
    "## What to log",
    '- User corrections and preferences ("use bun, not npm"; "stop summarizing diffs")',
    "- Facts about the user, their role, or their goals",
    "- Project context that is not derivable from the code (deadlines, incidents, decisions and their rationale)",
    "- Pointers to external systems (dashboards, Linear projects, Slack channels)",
    "- Anything the user explicitly asks you to remember",
    "",
    ...WHAT_NOT_TO_SAVE_SECTION,
    "",
    ...(skipIndex
      ? []
      : [
          `## ${ENTRYPOINT_NAME}`,
          `\`${ENTRYPOINT_NAME}\` is the distilled index (maintained nightly from your logs) and is loaded into your context automatically. Read it for orientation, but do not edit it directly — record new information in today's log instead.`,
          "",
        ]),
    ...buildSearchingPastContextSection(memoryDir),
  ];

  return lines.join("\n");
}

这里区分一下一个很重要的概念,就是什么是可以推断出来的信息,什么是不可以的;代码就在那,写在md里面的是会过期的,什么是不能推断出来的信息呢?想要的前端风格,啥时候提交代码 feedback不只有做的不好的,也要有做的好的相关提示

    '## What to log',
    '- User corrections and preferences ("use bun, not npm"; "stop summarizing diffs")',
    '- Facts about the user, their role, or their goals',
    '- Project context that is not derivable from the code (deadlines, incidents, decisions and their rationale)',
    '- Pointers to external systems (dashboards, Linear projects, Slack channels)',
    '- Anything the user explicitly asks you to remember'

所以这里会这么写

从上面的所写和在5中提到过的四种项目记忆,可以看出Auto Memory是承载项目私有工作经验。

Session Memory

Session Memory 是单会话压缩的降级基底,顾名思义,这轮对话的相关记忆,我之前的博客有写过相关内容,关于怎么管理context,这也算是一种memory记忆管理

extract memories agent

从相对来说最直观的prompt说起

/**
 * Shared opener for both extract-prompt variants.
 */
function opener(newMessageCount: number, existingMemories: string): string {
  const manifest =
    existingMemories.length > 0
      ? `\n\n## Existing memory files\n\n${existingMemories}\n\nCheck this list before writing — update an existing file rather than creating a duplicate.`
      : "";
  return [
    `You are now acting as the memory extraction subagent. Analyze the most recent ~${newMessageCount} messages above and use them to update your persistent memory systems.`,
    "",
    `Available tools: ${FILE_READ_TOOL_NAME}, ${GREP_TOOL_NAME}, ${GLOB_TOOL_NAME}, read-only ${BASH_TOOL_NAME} (ls/find/cat/stat/wc/head/tail and similar), and ${FILE_EDIT_TOOL_NAME}/${FILE_WRITE_TOOL_NAME} for paths inside the memory directory only. ${BASH_TOOL_NAME} rm is not permitted. All other tools — MCP, Agent, write-capable ${BASH_TOOL_NAME}, etc — will be denied.`,
    "",
    `You have a limited turn budget. ${FILE_EDIT_TOOL_NAME} requires a prior ${FILE_READ_TOOL_NAME} of the same file, so the efficient strategy is: turn 1 — issue all ${FILE_READ_TOOL_NAME} calls in parallel for every file you might update; turn 2 — issue all ${FILE_WRITE_TOOL_NAME}/${FILE_EDIT_TOOL_NAME} calls in parallel. Do not interleave reads and writes across multiple turns.`,
    "",
    `You MUST only use content from the last ~${newMessageCount} messages to update your persistent memories. Do not waste any turns attempting to investigate or verify that content further — no grepping source files, no reading code to confirm a pattern exists, no git commands.` +
      manifest,
  ].join("\n");
}

在两轮内尽量完成相关的记忆读写,一次读一次写,用来更新新的记忆的,是纯粹的增量工具 调用方式是使用hook

Autodream

首先看看对比

	CLAUDE.md 文件	自动记忆
谁编写	你	          Claude
包含内容	指令和规则	          学习和模式
范围	项目、用户或组织	 每个工作树,跨 worktrees 共享
加载到	每个会话	          每个会话(前 200 行或 25KB)
用于	编码标准、工作流、项目架构	构建命令、调试见解、Claude 发现的偏好

当你想指导 Claude 的行为时,使用 CLAUDE.md 文件。自动记忆让 Claude 从你的更正中学习,无需手动操作。

在karios模式下,autodream是被禁用的 在karios模式下,claude code是长时间持久话运行的,他在白天的时候只append增加相关记忆,晚上通过/dream的skill总结提炼更新memory.md,跟autodream的功能冲突,autodream是自动的更新memory.md Autodream会更新,整理md,意味着他会删东西,像是hermess agent出现过一个很严重的问题,之前他们会根据用户做过的东西总结成skills,但是skills太多会导致系统很混乱,反而降低了效率,因为这是一个只会熵增的系统

长期记忆如果只增不减,会迅速产生重复、冲突和过期事实。AutoDream 是离线巩固路径:当距离上次整理超过 24 小时且累积 5 个新会话后,系统获取 PID 互斥锁,启动 consolidation Agent,遍历 transcript 与现有记忆,执行合并、去重和冲突消解。

他是作为hook被调用的,满足24小时以及五个新会话开始触发(触发在对话后) MEMORY.md 索引需要被裁剪在 200 行或 25KB 以内,否则索引本身会变成新的 token 黑洞。 AutoDream 是 Claude Code 普通模式下的”夜间整理机器人”:当时间够久、会话够多、没有竞争时,启动一个受权限限制的 forked 子代理,按 Orient → Gather → Consolidate → Prune 四阶段整理记忆目录,合并重复、修正错误、压缩索引,让长期记忆保持整洁可用。

export function buildConsolidationPrompt(memoryRoot: string, transcriptDir: string, extra: string): string {
  return `# Dream: Memory Consolidation

You are performing a dream — a reflective pass over your memory files. Synthesize what you've learned recently into durable, well-organized memories so that future sessions can orient quickly.

Memory directory: \`${memoryRoot}\`
${DIR_EXISTS_GUIDANCE}

Session transcripts: \`${transcriptDir}\` (large JSONL files — grep narrowly, don't read whole files)

---

## Phase 1 — Orient

- \`ls\` the memory directory to see what already exists
- Read \`${ENTRYPOINT_NAME}\` to understand the current index
- Skim existing topic files so you improve them rather than creating duplicates
- If \`logs/\` or \`sessions/\` subdirectories exist (assistant-mode layout), review recent entries there

## Phase 2 — Gather recent signal

Look for new information worth persisting. Sources in rough priority order:

1. **Daily logs** (\`logs/YYYY/MM/YYYY-MM-DD.md\`) if present — these are the append-only stream
2. **Existing memories that drifted** — facts that contradict something you see in the codebase now
3. **Transcript search** — if you need specific context (e.g., "what was the error message from yesterday's build failure?"), grep the JSONL transcripts for narrow terms:
   \`grep -rn "<narrow term>" ${transcriptDir}/ --include="*.jsonl" | tail -50\`

Don't exhaustively read transcripts. Look only for things you already suspect matter.

## Phase 3 — Consolidate

For each thing worth remembering, write or update a memory file at the top level of the memory directory. Use the memory file format and type conventions from your system prompt's auto-memory section — it's the source of truth for what to save, how to structure it, and what NOT to save.

Focus on:
- Merging new signal into existing topic files rather than creating near-duplicates
- Converting relative dates ("yesterday", "last week") to absolute dates so they remain interpretable after time passes
- Deleting contradicted facts — if today's investigation disproves an old memory, fix it at the source

## Phase 4 — Prune and index

Update \`${ENTRYPOINT_NAME}\` so it stays under ${MAX_ENTRYPOINT_LINES} lines AND under ~25KB. It's an **index**, not a dump — each entry should be one line under ~150 characters: \`- [Title](file.md) — one-line hook\`. Never write memory content directly into it.

- Remove pointers to memories that are now stale, wrong, or superseded
- Demote verbose entries: if an index line is over ~200 chars, it's carrying content that belongs in the topic file — shorten the line, move the detail
- Add pointers to newly important memories
- Resolve contradictions — if two files disagree, fix the wrong one

---

Return a brief summary of what you consolidated, updated, or pruned. If nothing changed (memories are already tight), say so.${extra ? `\n\n## Additional context\n\n${extra}` : ""}`;
}

Phase 1 — Orient(定向)

  • ls 记忆目录
  • 读 MEMORY.md
  • 浏览已有 topic 文件
  • 查看 logs/、sessions/ 里的近期条目

目标:了解当前记忆格局,避免重复创建文件。

Phase 2 — Gather(收集信号)

按优先级查找新信息:

  1. Daily logs(logs/YYYY/MM/YYYY-MM-DD.md)
  2. 已漂移的旧记忆(与当前代码矛盾的事实)
  3. Transcript 搜索(用 grep 搜特定上下文)

关键约束:不要全文读 transcript,只搜 narrow term。

Phase 3 — Consolidate(整合)

  • 把新信号合并进已有 topic 文件
  • 转换相对日期为绝对日期
  • 删除被证伪的旧事实

这是最核心的”整理”阶段。

Phase 4 — Prune and index(修剪索引)

  • 更新 MEMORY.md
  • 保持 ≤200 行、≤25KB
  • 每个索引项一行,≤150 字符
  • 删除过期指针
  • 解决文件间矛盾

最后返回一个 summary。

Agent memory

在此之前,需要搞清楚Claude code三种记忆作用域

user (全局):存储在 ~/.claude/agent-memory/<name-of-agent>/。记忆跨项目保留,适用于通用的编码风格和偏好。
project (项目共享):存储在 .claude/agent-memory/<name-of-agent>/。记忆与特定项目相关,可通过 Git 等版本控制工具共享给团队。
local (本地私有):存储在 .claude/agent-memory-local/<name-of-agent>/。记忆仅与当前项目相关,但不应提交到版本控制(通常会被加入 .gitignore)。

在命令行中,可以看到

claude --agents '{
  "code-reviewer": {
    "description": "Expert code reviewer.",
    "prompt": "You are a senior code reviewer...",
    "tools": ["Read", "Grep", "Glob"],
    "memory": "user"
  }
}'

这样就能更加直观理解了这些memory对于subagent的作用域了吧

Claude code有sub agent的功能,而如果把其他agent的上下文跟原来的对话混杂起来,那一定是灾难性的

使得在复杂任务的拆解和委派过程中,不同agent之间的知识能够独立沉淀,互不干扰, 更像是子 Agent 的持久化工作空间

Agent-memory 的本质是”不污染主记忆”的隔离机制。 子 Agent 产生的大量专业细节如果写入 Auto-memory,会严重污染用户的个人画像。

维度	Auto-memory	Agent-memory
存储格式	YAML frontmatter + MEMORY.md 索引	完全相同的格式
底层基础设施	memdir.ts、buildMemoryPrompt	复用同一套 memdir 基础设施
作用域概念	personal / team	user / project / local
注入方式	作为 attachment 注入主对话 system prompt	作为 system prompt appendix 注入子 Agent
目录隔离	按项目隔离	按 agentType + scope 双重隔离

main-agent 同subagent之间的memory隔离:

高度聚焦特定任务的子代理, 产生的大量专业细节和工作草稿会严重污染用户的个人画像和核心项目偏好 需要在子 Agent 的定义中通过 memory 字段显式启用。一旦启用,系统会自动为该子代理注入文件操作工具(Read/Edit/Write) 存储路径 作用域 路径 说明 user ~/.claude/agent-memory// 跨所有项目共享 project /.claude/agent-memory// 项目级,可版本控制 local /.claude/agent-memory-local// 本地机器专用

Team memory

Team Memory Sync 是 Claude Code 的团队协作记忆系统,允许同一 GitHub 仓库的多个协作者共享和同步记忆文件。它构建在 Auto Memory 之上,通过 REST API 实现跨设备的实时同步。

Team Memory 不是独立的系统,而是 Auto Memory 的协作扩展——两者共享相同的格式、相同的提取机制,只是作用域不同。

Team Memory 把个人经验变成团队资产,但一旦涉及上传,安全边界必须比本地记忆更硬。这里的核心是先在本地阻断 secret,再考虑 ETag 同步。

const content = await readFile(fullPath, "utf8");
const secretMatches = scanForSecrets(content);
if (secretMatches.length > 0) {
  skippedSecrets.push({
    path: relPath,
    ruleId: firstMatch.ruleId,
    label: firstMatch.label,
  });
  return;
}
entries[relPath] = content;

在上传前会检查有没有哪些安全信息(例如api-key)被上传了

总结

这套 Memory 系统最反直觉的地方,是它没有把 Vector DB 当默认答案。向量检索能提供语义相似度,但代码任务需要的是当前可验证事实、路径级线索和可审计来源;在 CLI 场景里,引入 embedding、数据库服务和额外索引同步,会增加延迟、部署面和失效模式。 Claude Code 的 Memory 设计是成本约束下的状态调度。它牺牲了向量库的语义检索精细度,换来可审计、低延迟、易部署、易人工修正的文件系统路径。




Enjoy Reading This Article?

Here are some more articles you might like to read next: