Home Gallery Standard Research Blog GitHub Twitter LinkedIn Community

how-claude-code-works system prompt

Category: Coding agents. Audited against the AISPA standard.

What is in how-claude-code-works's system prompt?

how-claude-code-works's full system prompt: 1 version, 21,845 characters. Audited against AISPA.

The full text of 1 prompt is reproduced below, 21,845 characters in all, each read instruction by instruction against the eight AISPA dimensions. Nothing was flagged as working against the person on the other end.

1 Prompts on record
0 Flagged instructions
AI audit Audit source
D2 · Truthfulness & Information Integrity D3 · Privacy & Data Protection D4 · Tool/Action Safety

how-claude-code-works - docs 08 memory system

21845 characters

# 第 6 章:记忆系统 > 没有记忆的 Agent 每次对话都是初见——记忆让 Claude Code 从"无状态工具"进化为"跨会话学习的编程伙伴"。 ## 6.1 为什么 Agent 需要记忆? 想象这样的场景:你连续三天和 Claude Code 在同一个项目上协作。第一天你告诉它"不要在响应末尾总结",第二天你又说了一遍,第三天你开始烦躁——为什么它记不住? 这就是没有记忆的 Agent 的根本问题:**每次会话都从零开始**。用户偏好丢失、项目上下文重置、之前的纠正被遗忘。 Claude Code 的记忆系统解决这个问题,但它不是一个简单的"把所有信息存下来"的系统。它有一个核心约束: > **只记忆不可从当前项目状态推导的信息。** 这个约束不是为了省存储空间,而是为了**防止记忆与现实漂移**。如果记忆记录了"认证模块在 `src/auth/`",一次代码重构就会让这条记忆变成误导。代码模式、架构、git 历史等信息是**自描述的**——从代码本身读取永远比从记忆中回忆更准确。 ### 记忆 vs CLAUDE.md:互补而非竞争 | 维度 | CLAUDE.md | 记忆系统 | |------|-----------|---------| | 性质 | 静态配置文件 | 动态知识库 | | 维护方式 | 用户手动编辑,签入 Git | Agent 自动写入或 `/remember` | | 作用范围 | 团队共享(项目级)或用户全局 | 个人私有(可选团队共享) | | 内容类型 | 项目规范、编码约定、CI 配置 | 用户偏好、行为纠正、项目动态 | | 加载方式 | 每次会话完整加载 | 索引预加载 + 语义召回按需加载 | 两者互补:CLAUDE.md 存"项目是什么",记忆存"和这个人协作时要注意什么"。 关键文件:`src/memdir/` ## 6.2 四种记忆类型:封闭分类法 记忆系统使用**封闭的四类型分类法**(closed taxonomy),每种类型有明确的职责边界和结构要求: ```mermaid graph TB subgraph 个人记忆 ["个人记忆(始终私有)"] direction TB User[user 用户记忆<br/>角色/目标/偏好/知识领域] Feedback[feedback 反馈记忆<br/>用户对行为的纠正与指导<br/>结构:规则 + Why + How to apply] User ~~~ Feedback end subgraph 共享记忆 ["共享记忆(通常团队共享)"] direction TB Project[project 项目记忆<br/>进行中的工作/目标/截止日期<br/>决策与原因<br/>相对日期 → 绝对日期转换] Reference[reference 引用记忆<br/>外部系统指针<br/>信息定位] Project ~~~ Reference end 个人记忆 ~~~ 共享记忆 ``` | 类型 | 记什么 | 示例 | 触发时机 | |------|--------|------|---------| | **user** | 用户身份、偏好、知识背景 | "用户是数据科学家,专注可观测性" | 了解到用户角色/偏好时 | | **feedback** | 对 Agent 行为的纠正 | "不要在响应末尾总结,用户能自己看 diff" | 用户纠正行为时("不要..."、"别再...") | | **project** | 项目进展、决策、截止日期 | "2026-03-05 合并冻结,移动端发布" | 了解到谁在做什么、为什么、截止日期时 | | **reference** | 外部系统的定位信息 | "管道 Bug 追踪在 Linear INGEST 项目" | 了解到外部系统中信息位置时 | **为什么是四种类型而非自由标签?** 封闭分类法强制 Agent 做出明确的语义分类,避免标签膨胀导致召回时的模糊匹配。每种类型有不同的保存结构和使用方式——这让模型在写入和读取时都有明确的行为指引。 ### feedback 类型深度分析:不只记录失败 源码 `memoryTypes.ts` 中 feedback 类型的定义揭示了一个微妙的设计决策——feedback 不仅记录用户的纠正,还记录用户的肯定: ``` Guidance or correction the user has given you. These are a very important type of memory to read and write as they allow you to remain coherent and responsive to the way you should approach work in the project. ``` 为什么同时记录成功和失败?源码注释中有一段关键解释(意译): > 如果你只保存纠正,你会避免过去的错误,但会偏离用户已经验证过的好方法,并可能变得过于谨慎。 这是一个深刻的观察。假设用户说"这次的代码风格很好,以后就这样写",如果不记录这个正面反馈,Agent 可能在下次会话中"改进"代码风格——结果反而偏离了用户满意的方向。 ### feedback 和 project 的结构化要求 这两种类型要求特定的正文结构: ```markdown 规则或事实本身。 **Why:** 用户给出这个反馈的原因——通常是一个过去的事故或强烈偏好。 **How to apply:** 什么时候/在哪里应用这条指导。 ``` **为什么需要 Why?** 源码提示词中明确说明:"Knowing *why* lets you judge edge cases instead of blindly following the rule." 举个例子:如果记忆只记录"不要 mock 数据库",Agent 会在所有测试中避免 mock。但如果记忆还包含"Why: 上季度 mock 测试通过但生产环境迁移失败",Agent 就能判断——这条规则适用于集成测试,单元测试中的轻量级 mock 可能没问题。 ### project 类型:相对日期 → 绝对日期 project 类型有一个特殊要求:**必须将相对日期转换为绝对日期**。 当用户说"周四之后合并冻结",记忆必须存为"2026-03-05 后合并冻结"。原因很简单:记忆可能在几周后被另一次会话读取,此时"周四"已经毫无意义。 ### 什么不该保存 记忆系统有一个明确的排除列表,来自源码中的 `WHAT_NOT_TO_SAVE_SECTION`: ``` - 代码模式、约定、架构、文件路径、项目结构——读当前代码即可获得 - Git 历史、最近的改动、谁改了什么——git log / git blame 是权威来源 - 调试方案或修复步骤——修复在代码里,上下文在 commit 消息中 - 已经记录在 CLAUDE.md 中的内容 - 临时任务细节:进行中的工作、临时状态、当前对话上下文 ``` 关键设计点:这些排除规则**即使用户明确要求保存也生效**。如果用户说"记住这个 PR 列表",Agent 应该引导用户思考"这个列表中有什么是不可推导的?是关于它的某个决策、某个意外发现,还是某个截止日期?" ### 记忆决策流程 ```mermaid flowchart TD Input[获取到一条信息] --> Q1{能否从代码/Git/文档<br/>直接获取?} Q1 -->|能| Skip[不保存] Q1 -->|不能| Q2{已经在 CLAUDE.md 中?} Q2 -->|是| Skip Q2 -->|否| Q3{属于哪种类型?} Q3 -->|用户身份/偏好| User[保存为 user] Q3 -->|行为纠正/肯定| FB[保存为 feedback<br/>必须含 Why + How to apply] Q3 -->|项目动态/决策| Proj[保存为 project<br/>相对日期→绝对日期] Q3 -->|外部系统位置| Ref[保存为 reference] Q3 -->|都不是| Skip ``` ## 6.3 存储架构 ### 目录结构 记忆文件存储在项目特定目录中: ``` ~/.claude/projects/{project-hash}/memory/ ├── MEMORY.md ← 索引文件(每次会话自动加载) ├── user_role.md ← 用户记忆 ├── feedback_terse.md ← 反馈记忆 ├── project_freeze.md ← 项目记忆 └── reference_linear.md ← 引用记忆 ``` ### 路径解析:三级优先 记忆目录的位置通过三级优先级链确定(`src/memdir/paths.ts`): | 优先级 | 来源 | 用途 | |--------|------|------| | 1 | `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` 环境变量 | Cowork/SDK 集成,完全绕过标准路径 | | 2 | `autoMemoryDirectory` in settings.json | 用户自定义记忆存储位置(支持 `~/` 展开) | | 3 | `~/.claude/projects/{sanitized-git-root}/memory/` | 默认路径 | **安全决策:为什么 projectSettings 被排除?** `getAutoMemPathSetting()` 只从 user/managed settings 读取,**不**从 projectSettings 读取。原因是安全:projectSettings 来自项目的 `.claude/settings.json` 文件,它是被签入代码仓库的。一个恶意的仓库可以设置 `autoMemoryDirectory: "~/.ssh"`,让 Claude Code 的记忆写入操作(Edit/Write 工具)获得对用户 SSH 密钥目录的写访问权限。这与权限系统中"不信任项目级设置用于安全敏感路径"的原则一致。 ### 存储格式 每条记忆是独立的 Markdown 文件,带 YAML frontmatter: ```markdown --- name: 简洁回复偏好 description: 用户不希望在响应末尾看到总结 type: feedback --- 不要在每次响应末尾总结已完成的操作。 **Why:** 用户明确表示可以自己阅读 diff。 **How to apply:** 所有响应保持简洁,省略尾部总结。 ``` 关键设计:`description` 字段不仅是元数据,它是**召回系统的核心依据**。当 Sonnet 模型在选择相关记忆时,主要依赖 description 判断相关性,因此 description 必须足够具体——"用户偏好"太泛,"用户不希望在响应末尾看到总结"才够精确。 ### Git Worktree 共享 `findCanonicalGitRoot()` 确保同一仓库的所有 Git worktree 共享同一个记忆目录。如果不这样做,`git worktree add` 创建的新工作目录会生成一个独立的记忆空间,导致记忆"孤岛化"——在主工作目录中保存的偏好在 worktree 中消失。 ### 目录预创建:避免浪费模型回合 系统通过 `ensureMemoryDirExists()` 在会话开始时保证目录存在。这一步是幂等的——底层的 `fs.mkdir` 自动处理 `EEXIST`,整个路径链在一次调用中创建。 **为什么要保证目录预创建?** 实践中发现,Claude 会浪费回合执行 `ls` / `mkdir -p` 来检查目录是否存在。系统提示词中会注入 `DIR_EXISTS_GUIDANCE`,明确告诉模型: > "This directory already exists — write to it directly with the Write tool (do not run mkdir or check for its existence)." 这是一个典型的"用系统设计消除模型低效行为"的例子——与其期望模型学会不检查目录,不如直接预创建并明确告知。 ### 是否启用记忆:五级优先 `isAutoMemoryEnabled()` 的判断链: ``` CLAUDE_CODE_DISABLE_AUTO_MEMORY 环境变量 → 禁用 --bare 启动标志 → 禁用 远程模式(无持久化存储) → 禁用 settings.json 中 autoMemoryEnabled → 按配置 以上都不满足 → 默认启用 ``` ## 6.4 MEMORY.md:索引而非容器 `MEMORY.md` 是记忆系统的**入口点**(entrypoint),扮演两个角色: 1. **索引**:列出所有可用的记忆文件及其简短描述,供模型快速定位相关记忆 2. **快速检查**:每次会话启动时,MEMORY.md 的内容会通过 `getClaudeMds()` 自动加载到用户上下文中(与 CLAUDE.md 同一批次加载),让模型在第一个回合就知道有哪些记忆可用 正因为 MEMORY.md 每次会话都完整加载,它必须保持紧凑——它是索引,不是记忆容器。每个条目应为一行链接: ```markdown - [用户角色](user_role.md) — 数据科学家,专注可观测性 - [简洁回复偏好](feedback_terse.md) — 不要尾部总结 - [合并冻结](project_freeze.md) — 2026-03-05 移动端发布冻结 - [Bug 追踪](reference_linear.md) — 管道 Bug 在 Linear INGEST 项目 ``` **为什么是索引而非容器?** 类比数据库:MEMORY.md 是索引,记忆文件是数据行。索引必须紧凑——因为 MEMORY.md **每次会话都完整加载到系统提示词中**,它的大小直接挤占有效上下文空间。实际的记忆内容只有被 Sonnet 选中时才按需读取。 ### 双层截断机制 MEMORY.md 有严格的大小限制,由 `truncateEntrypointContent()` 实现: ```typescript // src/memdir/memdir.ts export const MAX_ENTRYPOINT_LINES = 200 export const MAX_ENTRYPOINT_BYTES = 25_000 // ~125 chars/line at 200 lines export function truncateEntrypointContent(raw: string): EntrypointTruncation { const contentLines = trimmed.split('\n') const wasLineTruncated = lineCount > MAX_ENTRYPOINT_LINES const wasByteTruncated = byteCount > MAX_ENTRYPOINT_BYTES // 第一步:按行截断(自然边界) let truncated = wasLineTruncated ? contentLines.slice(0, MAX_ENTRYPOINT_LINES).join('\n') : trimmed // 第二步:如果仍超过字节上限,在最后一个换行处截断(不切断行中间) if (truncated.length > MAX_ENTRYPOINT_BYTES) { const cutAt = truncated.lastIndexOf('\n', MAX_ENTRYPOINT_BYTES) truncated = truncated.slice(0, cutAt > 0 ? cutAt : MAX_ENTRYPOINT_BYTES) } // 追加警告信息 return { content: truncated + `\n\n> WARNING: MEMORY.md is ${reason}. Only part of it was loaded.`, lineCount, byteCount, wasLineTruncated, wasByteTruncated, } } ``` **为什么有两层截断?** - **行截断**(200 行):正常情况——索引条目太多,按行截断保持完整条目。 - **字节截断**(25KB):防御措施——捕捉行数在 200 以内但单行极长的异常索引。实际观察到 p100 场景:197KB 在 200 行内(有人把整篇文档作为单行条目)。 返回的元数据(`wasLineTruncated` / `wasByteTruncated`)用于遥测追踪,帮助团队了解用户的索引增长模式。 **警告消息的设计**:截断时追加的警告不只是报告问题,还**教模型如何修复**——提示模型"keep index entries to one line under ~200 chars; move detail into topic files"。这体现了一个设计原则:错误消息应该包含修复指引。 ### skipIndex 模式 一个实验性的 feature gate(`tengu_moth_copse`)正在测试移除 MEMORY.md 索引要求。启用后,记忆提取 Agent 直接写记忆文件而不更新 MEMORY.md。 为什么测试这个?两步保存流程(写文件 + 更新索引)是记忆系统中**最容易出错的部分**——模型可能写了文件但忘了更新索引,或者索引格式错误。如果 skipIndex 模式的召回质量不下降(因为 `scanMemoryFiles()` 直接扫描目录而非依赖索引),就可以简化整个保存流程。 ## 6.5 记忆召回:语义检索 当用户提交查询时,系统自动寻找相关记忆。这个过程分为扫描、评估、过滤三个阶段: ```mermaid flowchart TD Input[用户输入 + 最近工具使用] --> Scan["1. scanMemoryFiles()<br/>扫描记忆目录所有 .md 文件<br/>只读每个文件前 30 行 frontmatter<br/>按 mtime 降序排列<br/>保留最新 200 个"] Scan --> Format["2. formatMemoryManifest()<br/>格式化为清单:<br/>[type] filename (timestamp): description"] Format --> Eval["3. selectRelevantMemories()<br/>sideQuery() + Sonnet 模型<br/>输入:query + 清单 + recentTools<br/>输出:最多 5 个文件名"] Eval --> Filter["4. 过滤<br/>去除已展示的记忆(alreadySurfaced)<br/>验证文件名存在于已知集合"] Filter --> Return["5. 返回 RelevantMemory[]<br/>包含 path + mtimeMs"] ``` ### scanMemoryFiles():单次遍历优化 `src/memdir/memoryScan.ts` 中的扫描实现采用了一个巧妙的性能优化——**单次遍历**(read-then-sort)而非传统的两步法(stat-sort-read): ```typescript export async function scanMemoryFiles(memoryDir: string, signal: AbortSignal) { const entries = await readdir(memoryDir, { recursive: true }) const mdFiles = entries.filter(f => f.endsWith('.md') && basename(f) !== 'MEMORY.md') // 并行读取所有文件的 frontmatter(只读前 30 行) const headerResults = await Promise.allSettled( mdFiles.map(async (relativePath) => { const { content, mtimeMs } = await readFileInRange(filePath, 0, FRONTMATTER_MAX_LINES) const { frontmatter } = parseFrontmatter(content, filePath) return { filename: relativePath, filePath, mtimeMs, description, type } }) ) // 单次遍历:读取后排序,而非 stat-排序-读取 return headerResults .filter(r => r.status === 'fulfilled') .map(r => r.value) .sort((a, b) => b.mtimeMs - a.mtimeMs) .slice(0, MAX_MEMORY_FILES) // 保留最新 200 个(MAX_MEMORY_FILES = 200) } ``` 注意 `MAX_MEMORY_FILES = 200` 不是扫描上限,而是**返回结果数限制**。`readdir` 会读取目录中所有 `.md` 文件,每个都读取 frontmatter 并获取 mtime,然后按修改时间降序排列,最后 `.slice(0, 200)` 只保留最新的 200 个。如果记忆目录中有 500 个文件,500 个都会被扫描,但只有最新的 200 个会参与后续的语义召回。 **为什么这样更快?** 传统方法是: 1. `stat()` 所有文件获取 mtime → N 次 syscall 2. 按 mtime 排序,取前 200 3. `read()` 前 200 个文件的 frontmatter → 200 次 syscall 4. 总计:N + 200 次 syscall 单次遍历方法是: 1. `read()` 所有文件的前 30 行(`readFileInRange` 同时返回 mtime)→ N 次 syscall 2. 排序并保留最新 200 个 3. 总计:N 次 syscall 对常见场景(N ≤ 200),syscall 数量减半。代价是多读了一些最终被丢弃的文件的 frontmatter,但每个文件只读 30 行,开销极小。 **FRONTMATTER_MAX_LINES = 30**:只读前 30 行是因为 frontmatter 始终在文件顶部。读取完整文件对召回来说是浪费——选择阶段只需要 description 字段。 ### formatMemoryManifest():清单格式 扫描结果被格式化为清单,提供给 Sonnet 评估: ``` - [feedback] feedback_terse.md (2026-03-28T10:30:00Z): 用户不希望在响应末尾看到总结 - [project] project_freeze.md (2026-03-01T09:00:00Z): 2026-03-05 合并冻结,移动端发布 ``` 格式中的 **ISO 时间戳**至关重要——它让 Sonnet 能判断记忆的新鲜度。一个月前的"合并冻结"记忆很可能已过时,Sonnet 可以据此降低其优先级。 ### selectRelevantMemories():Sonnet 语义评估 ```typescript const SELECT_MEMORIES_SYSTEM_PROMPT = `You are selecting memories that will be useful to Claude Code as it processes a user's query. Return a list of filenames for the memories that will clearly be useful (up to 5). - Be selective and discerning. - If recently-used tools are provided, do not select usage reference docs for those tools. DO still select warnings, gotchas, or known issues about those tools.` const result = await sideQuery({ model: getDefaultSonnetModel(), system: SELECT_MEMORIES_SYSTEM_PROMPT, messages: [{ role: 'user', content: `Query: ${query}\n\nAvailable memories:\n${manifest}${toolsSection}` }], max_tokens: 256, output_format: { type: 'json_schema', schema: { /* selected_memories: string[] */ } }, }) ``` **为什么用 Sonnet 而非关键词匹配?** 语义相关性评估比关键词匹配更准确。例如,用户问"部署流程"时,关键词匹配可能错过标题为"CI/CD 注意事项"的记忆,但 Sonnet 能理解语义关联。 **为什么限制 5 个?** 上下文空间有限。记忆内容作为 user message 注入对话,过多的记忆会挤占工作空间。5 个是召回价值和上下文成本的平衡点。 ### recentTools 参数:精确的噪声过滤 `recentTools` 参数是一个巧妙的设计。当 Claude Code 正在使用某个工具(如 `mcp__X__spawn`)时: - 该工具的**参考文档型记忆**是噪声——对话中已经包含了使用方法 - 但关于该工具的**警告和已知问题**仍然有价值 提示词中明确区分这两种情况:"do not select usage reference docs for those tools. DO still select warnings, gotchas, or known issues about those tools." 这让选择器在工具使用的上下文中做出更精确的判断。 ### alreadySurfaced 预过滤 `findRelevantMemories()` 在调用 Sonnet **之前**就过滤掉已展示的记忆路径。这不是为了避免重复展示(虽然也有这个效果),而是为了**不浪费 5 个召回槽位**——如果不预过滤,Sonnet 可能选中 3 个已展示的记忆,只留下 2 个新记忆的空间。 ### 异步预取:不阻塞主循环 记忆召回通过 `pendingMemoryPrefetch` 实现**异步预取**——在模型开始生成响应的同时,后台通过 `sideQuery()` 查询 Sonnet。当模型实际需要记忆时,结果通常已经就绪。 这个设计确保记忆召回的 ~250ms 延迟不叠加到用户感知的响应时间上。对用户来说,记忆召回是"免费"的。 ## 6.6 记忆新鲜度与漂移防御 记忆记录的是**写入时的事实**,但时间会让记忆过时。记忆系统通过多层防御机制来处理这个问题。 ### 人类可读的时间距离 `memoryAge.ts` 将 mtime 转为人类可读的字符串: ``` 0 天 → "today" 1 天 → "yesterday" 47 天 → "47 days ago" ``` **为什么不用 ISO 时间戳?** 模型不擅长日期算术。给模型 `2026-02-12T10:30:00Z` 并告诉它今天是 `2026-04-01`,它可能算不清楚过了多少天。但 "47 days ago" 直接触发模型的"这可能过时了"推理。 ### 新鲜度警告 对于超过 1 天的记忆,系统注入新鲜度警告文本(`memoryFreshnessText`): > "Memories are point-in-time observations, not live state — claims about code behavior or file:line citations may be outdated." 这个警告的出发点是:用户报告过 Agent 将过时的记忆(如"X 函数在 line 42")作为事实断言,导致错误的代码修改。 ### 记忆访问三规则 源码中的 `WHEN_TO_ACCESS_SECTION` 定义了三条访问规则: 1. **当已知记忆与任务相关时**:主动查阅 2. **当用户明确要求时**:**必须**访问记忆(用 MUST 强调) 3. **当用户说"忽略记忆"时**:视为记忆不存在 第三条规则背后有一个 eval 失败案例:用户说"忽略关于 X 的记忆",但 Claude 回复"不是 Y(如记忆中所述),而是..."——它承认了记忆的存在并试图"修正",违背了用户的意图。 ### 信任召回:验证而非盲信 `TRUSTING_RECALL_SECTION` 是记忆系统中最关键的安全网之一: > "记忆说 X 存在" ≠ "X 现在存在" 规则要求:如果记忆提到一个文件路径,用 Glob/Read 验证它是否存在。如果记忆提到一个函数,用 Grep 确认它是否还在。 这个节的效果在 eval 中得到了验证:**没有这个节,通过率 0/2;加入后,通过率 3/3。** 这说明模型默认会信任记忆中的具体引用,但记忆中的代码位置信息衰减很快——一次重构就可能全部失效。 ## 6.7 后台记忆提取 除了模型主动写入和用户通过 `/remember` 保存外,Claude Code 还有一个**后台记忆提取 Agent**(`src/services/extractMemories/extractMemories.ts`),在每次对话回合结束后自动运行。 ### 整体架构 ```mermaid sequenceDiagram participant User as 用户 participant Main as 主 Agent participant Hooks as Stop Hooks participant Extract as 提取 Agent (Forked) participant Memory as 记忆目录 User->>Main: 提交查询 Main->>User: 生成响应(无工具调用) Main->>Hooks: 触发 handleStopHooks Hooks->>Hooks: hasMemoryWritesSince() 检查 alt 主 Agent 已写记忆 Hooks->>Hooks: 跳过提取,推进游标 else 主 Agent 未写记忆 Hooks->>Extract: runForkedAgent()<br/>共享 prompt cache Extract->>Memory: Turn 1: 并行读取已有记忆 Extract->>Memory: Turn 2: 并行写入新记忆 Extract->>Hooks: 完成 Hooks->>User: 系统消息 "Memory saved: ..." end ``` ### 触发、互斥与重叠防护 提取 Agent 的运行受三层控制,确保既不遗漏也不重复: **1. 触发时机**:提取 Agent 在 `handleStopHooks` 中被触发——即主 Agent 完成响应(没有更多工具调用)时。 **2. 频率控制**:不是每次回合结束都触发提取,有两道过滤: - **互斥检查**:`hasMemoryWritesSince()` 检查主 Agent 是否在最近的消息范围内已经写入了记忆文件。如果主 Agent 已经主动保存了记忆(比如用户说"记住这个",主 Agent 直接调用 Write 写入),提取 Agent 就**跳过**——避免对同一段对话产生重复记忆。 - **回合节流**:`turnsSinceLastExtraction` 计数器控制提取频率。很多回合(如简单的问答)没有值得记忆的信息,不需要每次都提取。 **3. 并发防护**:如果上一次提取还在运行时新的回合结束了,系统不会启动并发提取,而是通过 `pendingContext` 暂存 + trailing run 机制处理: ``` inProgress = true → 将新请求暂存为 pendingContext(后到的覆盖先到的) 当前提取完成 → 检查 pendingContext,如果有则启动 trailing run trailing run → 只处理自游标推进后的新消息 ``` 这个设计确保:(1) 不会有两个提取 Agent 同时写入记忆目录(避免冲突);(2) 不会遗漏任何对话内容——即使提取来不及处理,最新的上下文会被暂存并在当前提取结束后立即处理。 ### 工具权限:严格的写入白名单 提取 Agent 的工具权限由 `createAutoMemCanUseTool()` 定义: | 工具 | 权限 | |------|------| | Read / Grep / Glob | 无限制——需要读取已有记忆和代码 | | Bash | 只读命令(ls, find, grep, cat, stat, wc, head, tail)| | Edit / Write | **仅限记忆目录内**(通过 `isAutoMemPath()` 校验)| | 其他所有工具 | 拒绝 | 这是**最小权限原则**的体现——提取 Agent 只需要读取对话上下文和已有记忆,然后写入新记忆。它不需要执行代码、修改项目文件或调用外部服务。 ### 提取提示词设计 提取 Agent 的提示词(`src/services/extractMemories/prompts.ts`)有几个关键设计: **高效的回合预算**:提示词明确指导 Agent 的执行策略——"Turn 1: 并行发起所有读取;Turn 2: 并行发起所有写入"。这最大化了工具调用的并行度,通常 2 个回合就能完成工作(硬上限是 5 个回合)。 **防止重复**:提示词注入已有记忆的清单(manifest),并指导 Agent "先检查是否已有类似记忆,再决定创建新的"。 **范围限制**:`MUST only use content from last ~${newMessageCount} messages`——只从最新的消息中提取,不重新处理已处理过的历史。 ### 共享 Prompt Cache 提取 Agent 通过 `runForkedAgent()` 创建,这与技能系统的 fork 模式使用相同的底层机制。关键优势是**共享父级的 prompt cache**——系统提示词不需要重新计算和传输,大幅降低提取的 token 消耗。 ## 6.8 记忆提示词构建层级 记忆系统的提示词构建分为三个层级,每层叠加不同的内容: ```mermaid flowchart TD L1["buildMemoryLines()<br/>行为指令层<br/>四类型分类法 + 保存/访问规则<br/>+ 记忆 vs Plan/Task 区分"] L2["buildMemoryPrompt()<br/>内容层<br/>= buildMemoryLines() + MEMORY.md 内容<br/>(经 truncateEntrypointContent 截断)"] L3["loadMemoryPrompt()<br/>分发层<br/>按 feature gate 选择构建方式"] L1 --> L2 L2 --> L3 L3 -->|KAIROS 模式| K["buildAssistantDailyLogPrompt()<br/>追加式日期命名日志"] L3 -->|TEAMMEM 模式| T["buildCombinedMemoryPrompt()<br/>私有 + 团队两个目录"] L3 -->|普通模式| N["buildMemoryLines()<br/>单目录"] L3 -->|禁用| Null["返回 null"] ``` ### buildMemoryLines():行为指令的八个子节 `buildMemoryLines()` 构建的指令包含八个子节: 1. **持久化记忆介绍**:告知模型记忆目录路径,`DIR_EXISTS_GUIDANCE` 说明目录已存在 2. **显式保存/遗忘**:用户说"记住"→ 立即保存,说"忘记"→ 查找并删除 3. **四类型分类法**:user / feedback / project / reference 的完整定义、示例、保存时机 4. **什么不该保存**:代码模式、git 历史、CLAUDE.md 已有内容等排除列表 5. **如何保存**:两步流程(写文件 + 更新 MEMORY.md)或单步(skipIndex 模式) 6. **何时访问**:三条规则 + "用户说忽略则忽略" 7. **信任召回**:验证记忆中的引用,不盲信 8. **记忆 vs 其他持久化**:Plan 用于对齐实施方案,Task 用于追踪当前会话进度,记忆用于跨会话信息 第 8 点的区分特别重要——模型容易混淆何时用记忆、何时用 Plan、何时用 Task。记忆系统的提示词明确划定了边界: > - Plan:非平凡实现任务的方案对齐,变更应更新 Plan 而非保存记忆 > - Task:当前会话中的步骤分解和进度追踪 > - 记忆:跨会话有价值的信息 ### KAIROS 模式 KAIROS 是一个实验性的"助手模式",为长期运行的会话设计。与普通模式维护 MEMORY.md 实时索引不同,KAIROS 模式将信息追加到**日期命名的日志文件**中: ``` ~/.claude/projects/{hash}/logs/ └── 2026/ └── 04/ └── 2026-04-01.md ← 今天的日志 ``` 每天的日志是追加式的,避免了频繁更新 MEMORY.md 索引的开销。定期通过 `/dream` 技能将日志**蒸馏**为结构化的主题记忆文件。这种"先追加、后整理"的模式适合高频交互场景。 ## 6.9 团队记忆 当启用团队记忆(`TEAMMEM` feature gate)时,系统管理两个记忆目录: ``` ~/.claude/projects/{hash}/memory/ ← 私有记忆(仅自己可见) ~/.claude/projects/{hash}/memory/team/ ← 团队记忆(项目成员共享) ``` ### 作用域指导 在团队模式下,类型分类法增加了 `<scope>` 标签来指导记忆的存储位置: | 类型 | 默认作用域 | 原因 | |------|-----------|------| | **user** | 始终私有 | 个人偏好不应强加给团队 | | **feedback** | 偏向私有,项目约定可团队共享 | "不要总结"是个人偏好;"测试必须用真实数据库"是团队约定 | | **project** | 偏向团队 | 里程碑、决策对所有成员有价值 | | **reference** | 偏向团队 | 外部系统位置是共享知识 | **敏感数据防护**:团队记忆的提示词中明确要求"MUST NOT save sensitive data (API keys, credentials) in team memories"。私有记忆也不建议存储敏感信息,但团队记忆中这是强制要求——因为团队记忆会被其他成员的 Agent 读取。 **架构细节**:`isTeamMemoryEnabled()` 要求先启用自动记忆。团队目录是自动记忆目录的子目录——`mkdir(teamDir)` 会通过递归创建自动创建父目录。两个目录各有独立的 MEMORY.md 索引,都加载到系统提示词中。 ## 6.10 Agent 记忆 除了主 Agent 的记忆系统,Claude Code 还为**子 Agent**(通过 Agent 工具创建的)提供了独立的记忆系统(`src/tools/AgentTool/agentMemory.ts`)。 ### 三个作用域 ``` user 作用域: ~/.claude/agent-memory/{agentType}/ project 作用域: .claude/agent-memory/{agentType}/ local 作用域: .claude/agent-memory-local/{agentType}/ ``` - **user**:跨所有项目的 Agent 级知识(如"这种类型的探索 Agent 应该如何工作") - **project**:项目特定的 Agent 知识(如"这个项目的测试 Agent 应该使用哪个测试框架") - **local**:本地机器特定,不会签入版本控制 ### 为什么与主记忆分离? 子 Agent 的知识类型与主 Agent 不同。一个 "explorer" Agent 学到的代码导航技巧、一个 "test-runner" Agent 学到的测试模式——这些是 Agent 类型特有的操作知识,与用户偏好和项目决策没有关系。分离存储避免了主记忆被 Agent 操作细节污染。 `agentType` 在路径中的作用是隔离不同类型 Agent 的知识空间。路径中的冒号被替换为破折号(`sanitizeAgentTypeForPath()`)以兼容文件系统。 ### 记忆注入方式 Agent 记忆通过与主记忆相同的 `buildMemoryPrompt()` 函数构建,但带有 Agent 特有的行为指导。注入方式也相同——MEMORY.md 索引进系统提示词,具体记忆按需通过语义召回加载。 ## 6.11 记忆注入对话的方式 记忆通过两条路径到达模型的上下文窗口——MEMORY.md 走系统提示词(每次会话必加载),召回的记忆走用户消息(按需注入)。理解这两条路径对于理解记忆系统的上下文开销至关重要。 ### MEMORY.md:用户上下文注入 MEMORY.md 的内容通过 `getMemoryFiles()` → `getClaudeMds()` 流程加载,与 CLAUDE.md 走同一条路径,最终作为用户上下文(`getUserContext()`)的一部分注入。系统提示词中还有一段独立的记忆行为指令,通过 `systemPromptSection('memory', () => loadMemoryPrompt())` 注入,包含四类型分类法、保存规则等。 这意味着: - 每次会话自动加载,无需模型主动请求 - MEMORY.md 内容经过 `truncateEntrypointContent()` 截断(200 行 / 25KB) - 行为指令位于系统提示词中,MEMORY.md 内容位于用户上下文中 实际注入到上下文中的 MEMORY.md 内容大致如下: ``` Contents of ~/.claude/projects/a1b2c3d4/memory/MEMORY.md (user's auto-memory, persists across conversations): - [用户角色](user_role.md) — 数据科学家,专注可观测性 - [简洁回复偏好](feedback_terse.md) — 不要尾部总结 - [合并冻结](project_freeze.md) — 2026-03-05 移动端发布冻结 - [Bug 追踪](reference_linear.md) — 管道 Bug 在 Linear INGEST 项目 ``` 这段文本由 `getClaudeMds()` 拼接生成(`src/utils/claudemd.ts`),格式为 `Contents of {path}{description}:\n\n{content}`。`description` 部分根据文件类型不同而变化——MEMORY.md 对应的是 `(user's auto-memory, persists across conversations)`。 ### 召回的记忆:用户消息注入 通过 Sonnet 选中的记忆作为 **user message**(带 `isMeta: true`)注入对话: ```typescript case 'relevant_memories': { return wrapMessagesInSystemReminder( attachment.memories.map(m => { const header = m.header ?? memoryHeader(m.path, m.mtimeMs) return createUserMessage({ content: `${header}\n\n${m.content}`, isMeta: true }) }) ) } ``` `memoryHeader()` 根据记忆的新鲜度生成不同的头部(`src/utils/attachments.ts`): - **新鲜记忆**(今天/昨天):`Memory (saved today): ~/.claude/projects/.../feedback_terse.md:` - **过时记忆**(>1 天):先输出新鲜度警告,再输出路径。例如: ``` This memory is 47 days old. Memories are point-in-time observations, not live state — claims about code behavior or file:line citations may be outdated. Verify against current code before asserting as fact. Memory: ~/.claude/projects/.../project_freeze.md: --- name: 合并冻结 description: 2026-03-05 合并冻结,移动端发布 type: project --- 2026-03-05 后合并冻结,移动端 v3.2 发布。 **Why:** 产品团队要求冻结期间不合并非紧急 PR。 **How to apply:** 03-05 之后的 PR 推迟到下周合并。 ``` 记忆被包裹在 `<system-reminder>` 标签中(通过 `wrapMessagesInSystemReminder`),与其他上下文信息(如 Read/Grep 结果)归为同一组。 `isMeta: true` 标记确保这些消息在 UI 中不作为用户消息显示,但模型能看到它们。这意味着用户不会被大量的记忆注入打扰,但模型的每个回合都能参考这些信息。 ## 6.12 设计洞察 1. **只记忆不可推导的信息**:代码模式从代码读,git 历史从 git 查,记忆只存"元信息"——这个约束是整个系统的根基,防止记忆成为过时的代码映射 2. **语义召回优于关键词匹配**:用 Sonnet 评估相关性,能理解"部署"和"CI/CD"的语义关联。代价是 ~250ms 额外延迟,但通过异步预取完全隐藏 3. **两层截断防御长索引**:行截断捕捉正常增长,字节截断捕捉异常长行(实际观察到 197KB 在 200 行内)——面向实际数据设计,而非理论场景 4. **后台提取 Agent 模式**:将"从对话中提取记忆"封装为独立的 forked agent,共享 prompt cache 降低成本,互斥机制避免重复,最小权限限制写入范围。这个模式可推广到任何"后台智能"场景 5. **eval 驱动的提示词工程**:TRUSTING_RECALL_SECTION 的加入直接由 eval 数据驱动(0/2 → 3/3)。记忆系统的每个提示词节都经过测评验证,不是凭直觉添加的 6. **用系统设计消除模型低效行为**:预创建目录 + `DIR_EXISTS_GUIDANCE` 比"教模型不要检查目录"更可靠。这是一个通用原则:如果模型反复犯某个错误,优先考虑改变环境而非改变提示词 7. **frontmatter 作为统一接口**:记忆和技能使用相同的 Markdown + YAML frontmatter 格式,降低了模型的认知负担——只需学习一种文件格式就能操作两个系统 --- > **动手实践**:在 [claude-code-from-scratch](https://github.com/Windy3f3f3f3f/claude-code-from-scratch) 的 `src/session.ts` 中,可以看到一个最小的会话持久化实现。尝试在此基础上增加记忆系统——将用户偏好写入 `~/.mini-claude/memory/` 目录,并在系统提示词中注入。 上一章:[技能系统](./09-skills-system.md) | 下一章:[Hooks 与可扩展性](./06-hooks-extensibility.md)

Questions about how-claude-code-works's system prompt

Does how-claude-code-works's system prompt contain instructions that work against the user?

No. Nothing in how-claude-code-works's system prompt was flagged as working against the person the product is talking to. That is a clean result across all eight AISPA dimensions, not an absence of checking — the full text was read instruction by instruction.

How long is how-claude-code-works's system prompt?

21,845 characters across 1 prompt on this page. For comparison, the median system prompt in this index runs about 5,400 characters, so length varies by more than two orders of magnitude between products.

How many versions of how-claude-code-works's system prompt are on record?

1. Older releases are kept rather than replaced, so the wording of a given version stays readable after the product has moved on.

Where did this how-claude-code-works system prompt come from?

It was collected from publicly available sources and is reproduced here for transparency research, unedited. This site does not extract prompts from products itself.

How was how-claude-code-works's system prompt audited?

Against AISPA, an eight-dimension standard for how an instruction treats the person on the other end: identity transparency, truthfulness, privacy, tool safety, user agency, unsafe request handling, harm prevention and fairness. This audit was ai audit. The method is described in the paper behind the standard.

How this page was made

The prompt text above is reproduced verbatim from a public source. Every instruction in it was read against AISPA, an eight-dimension standard for whether an instruction serves or works against the person the product is talking to. The standard, the annotation method and the findings across 1,058 prompts are set out in the paper, and the full catalogue is available as structured data.

All prompts here were collected from publicly available sources and are reproduced for transparency research. Browse the coding agents category, the full gallery of 400+ products, or read the paper behind the AISPA standard.