Files
cs-note/claude-code-best/docs/context/project-memory.md
T

219 lines
9.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- 项目记忆
- 跨对话
- 自动记忆
- Sonnet召回
create time: 2026-06-09 22:15
---
# 项目记忆系统 - 文件级跨对话记忆架构
## 概述
Claude Code 的记忆系统是纯文件驱动的——没有数据库、没有向量存储,只有 Markdown 文件和目录结构。通过四类型分类法约束记忆内容,使用轻量级 Sonnet 侧查询智能召回相关记忆,并与 Session Memory 压缩深度集成。
## 正文
### 记忆系统的存储架构
源码路径: `src/memdir/paths.ts`、`src/memdir/memdir.ts`
Claude Code 的记忆系统是**纯文件**的——没有数据库、没有向量存储,只有 Markdown 文件和目录结构。
#### 目录布局
```
~/.claude/projects/<sanitized-git-root>/memory/
MEMORY.md <- 入口索引(每次对话加载)
user_role.md <- 用户记忆
feedback_testing.md <- 反馈记忆
project_mobile_release.md <- 项目记忆
reference_linear_ingest.md <- 参考记忆
logs/ <- KAIROS 模式: 每日日志
2026/04/2026-04-01.md
```
路径解析链路(`getAutoMemPath()`):
1. `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` 环境变量(Cowork SDK 全路径覆盖)
2. `autoMemoryDirectory` 设置(仅限 `policySettings`/`localSettings`/`userSettings`——**故意排除** `projectSettings`,防止恶意仓库将记忆路径指向 `~/.ssh`)
3. 默认: `<memoryBase>/projects/<sanitized-git-root>/memory/`
同一个 Git 仓库的所有 worktree 共享一个记忆目录(通过 `findCanonicalGitRoot()` 找到真正的 `.git` 根)。
#### MEMORY.md 索引
`MEMORY.md` 是记忆的入口索引,每次对话都完整加载到上下文中:
```typescript
// memdir.ts:34-38
export const ENTRYPOINT_NAME = 'MEMORY.md'
export const MAX_ENTRYPOINT_LINES = 200
export const MAX_ENTRYPOINT_BYTES = 25_000
```
索引有**双重上限**: 200 行 AND 25KB。超过任何一条都会被 `truncateEntrypointContent()` 截断并追加警告。
> [!info] 为什么需要双重上限?
> p97 的索引文件用 200 行就能覆盖,但有些索引条目特别长(p100 观测到 197KB/200 行),字节上限捕捉这种长行异常。
索引条目格式: `- [Title](file.md) -- one-line hook`,每条一行,约 150 字符以内。`MEMORY.md` 本身没有 frontmatter——它只是一个链接列表,不是记忆内容。
### 四类型分类法
源码路径: `src/memdir/memoryTypes.ts`
记忆被约束为一个**封闭的四类型系统**,每种类型有明确的 `<when_to_save>`、`<how_to_use>` 和 `<body_structure>` 规范:
| 类型 | 存储内容 | 典型触发 |
|------|---------|---------|
| **user** | 用户角色、偏好、技术背景 | "我是数据科学家"、"我写了十年 Go" |
| **feedback** | 用户对 AI 行为的纠正和确认 | "别 mock 数据库"、"单 PR 更好" |
| **project** | 非代码可推导的项目上下文 | "合并冻结从周四开始"、"auth 重写是合规要求" |
| **reference** | 外部系统指针 | "pipeline bugs 在 Linear INGEST 项目" |
关键设计约束: **只存储无法从当前项目状态推导的信息**。代码架构、文件路径、git 历史都可以实时获取,不需要记忆。
#### 反馈类型的双通道捕获
`feedback` 类型的 `when_to_save` 指令特别强调:
> Record from failure AND success: if you only save corrections, you will avoid past mistakes but drift away from approaches the user has already validated, and may grow overly cautious.
这意味着 AI 不仅在用户说"不要这样做"时保存,也在用户说"对,就是这样"时保存。后一种更难捕捉,但同等重要——它防止 AI 的行为随时间漂移。
#### 每条记忆的 Frontmatter 格式
```markdown
---
name: {{memory name}}
description: {{one-line description -- 用于未来判断相关性}}
type: {{user, feedback, project, reference}}
---
{{memory content -- feedback/project 类型建议包含 **Why:** 和 **How to apply:** 行}}
```
`description` 字段是关键: 它不是给人读的摘要,而是给 AI 召回系统做相关性判断的搜索关键词。
### 智能召回机制
源码路径: `src/memdir/findRelevantMemories.ts`、`src/memdir/memoryScan.ts`
不是所有记忆都适合每次对话。系统使用一个**轻量级 Sonnet 侧查询**来筛选最相关的记忆。
#### 召回流程
```mermaid
flowchart TD
A["用户消息"] --> B["findRelevantMemories query, memoryDir"]
B --> C["scanMemoryFiles 扫描所有记忆文件的frontmatter"]
C --> D["selectRelevantMemories Sonnet侧查询"]
D --> E["返回最多5条相关记忆"]
```
核心是 `selectRelevantMemories()` 函数,它调用 `sideQuery()`(一个独立的轻量 API 调用):
```typescript
// findRelevantMemories.ts:98-121
const result = await sideQuery({
model: getDefaultSonnetModel(), // 用 Sonnet 做筛选(非主模型)
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: { ... } },
})
```
#### 近期工具去噪
当 AI 正在使用某个工具时,召回该工具的使用文档是噪音(对话中已有工作上下文)。`recentTools` 参数让召回系统跳过这些记忆,但**仍然要选择**关于这些工具的警告、陷阱或已知问题——这正是使用时最关键的信息。
#### 已展示去重
`alreadySurfaced` 参数过滤之前轮次已展示过的文件路径,让 Sonnet 的 5 槽预算花在新的候选上,而不是重复召回同一文件。
### 记忆注入 System Prompt 的链路
源码路径: `src/memdir/memdir.ts` -> `src/context.ts`
`loadMemoryPrompt()` 是记忆注入的入口,每会话调用一次(通过 `systemPromptSection('memory', ...)` 缓存):
```typescript
// memdir.ts:419-507
export async function loadMemoryPrompt(): Promise<string | null> {
// 优先级: KAIROS 日志模式 -> TEAMMEM 组合模式 -> 纯自动记忆
if (feature('KAIROS') && autoEnabled && getKairosActive()) {
return buildAssistantDailyLogPrompt(skipIndex)
}
if (feature('TEAMMEM') && teamMemPaths!.isTeamMemoryEnabled()) {
return teamMemPrompts!.buildCombinedMemoryPrompt(...)
}
if (autoEnabled) {
return buildMemoryLines('auto memory', autoDir, ...).join('\n')
}
return null
}
```
注入时机: `context.ts` 中 `getSystemContext()` 调用时,记忆 Prompt 作为 system prompt 的一个 section 被组装。`MEMORY.md` 的内容作为 **user context message** 注入(而非 system prompt),这样可以利用 Prompt Cache 的 prefix 共享。
### KAIROS 模式: 每日日志
源码路径: `src/memdir/memdir.ts`(`buildAssistantDailyLogPrompt`)
长期运行的 assistant 会话使用不同的记忆策略:
- **标准模式**: AI 维护 `MEMORY.md` 作为实时索引 + 独立记忆文件
- **KAIROS 模式**: AI 只往日期文件追加日志(`logs/YYYY/MM/YYYY-MM-DD.md`),不做重组
一个独立的夜间 `/dream` 技能负责将日志蒸馏为主题文件 + `MEMORY.md` 索引。
### 记忆漂移防御
源码路径: `src/memdir/memoryTypes.ts`(`TRUSTING_RECALL_SECTION`)
记忆可能过期。系统在 Prompt 中设置了一个专门的 section "Before recommending from memory":
```
A memory that names a specific function, file, or flag is a claim
that it existed *when the memory was written*. It may have been
renamed, removed, or never merged. Before recommending it:
- If the memory names a file path: check the file exists.
- If the memory names a function or flag: grep for it.
```
> [!tip] A/B 测试验证
> 这个 section 的标题经过 A/B 测试: "Before recommending from memory"(行动导向)比 "Trusting what you recall"(抽象描述)效果好(3/3 vs 0/3)。
#### 忽略记忆的严格语义
当用户说"忽略"或"不使用"记忆时,系统要求 AI 按 `MEMORY.md` 为空处理——不应用、不引用、不比较、不提及记忆内容。这解决了 AI 的一个常见反模式: 用户说"忽略关于 X 的记忆",AI 虽然正确识别了代码但仍然加上"不像记忆中说的 Y"——这不是"忽略",而是"承认然后覆盖"。
### Session Memory 与压缩的联动
源码路径: `src/services/compact/sessionMemoryCompact.ts`
记忆系统与上下文压缩有深度集成。当 `tengu_session_memory` 和 `tengu_sm_compact` 两个 feature flag 同时开启时,压缩优先使用 Session Memory 而非传统摘要:
```typescript
// sessionMemoryCompact.ts:57-61
const DEFAULT_SM_COMPACT_CONFIG = {
minTokens: 10_000, // 压缩后至少保留 10K token
minTextBlockMessages: 5, // 至少保留 5 条文本消息
maxTokens: 40_000, // 最多保留 40K token
}
```
SM-compact 不调用压缩 API(没有摘要模型),而是直接使用已有的 Session Memory 作为摘要——更快、更便宜、且不会丢失信息。
## 关联笔记
- [[system-prompt]] - System Prompt 动态组装与 CLAUDE.md 注入
- [[compaction]] - 上下文压缩三层策略
- [[../conversation/the-loop]] - Agentic Loop 中的记忆召回