cc Tools result 缓存机制分析

1. FileStateCache:文件读取缓存

1.1 核心数据结构

src/utils/fileStateCache.ts 定义了 FileStateCache,基于 LRU(Least Recently Used) 算法的文件状态缓存:

typescript 复制代码
export type FileState = {
  content: string          // 文件内容(已读的部分)
  timestamp: number        // 文件修改时间(mtime)
  offset: number | undefined  // 读取起始行
  limit: number | undefined   // 读取行数上限
  isPartialView?: boolean     // 是否为部分视图(如自动注入的 CLAUDE.md)
}

// 默认限制:100 个条目,25MB 总大小
export const READ_FILE_STATE_CACHE_SIZE = 100
const DEFAULT_MAX_CACHE_SIZE_BYTES = 25 * 1024 * 1024

路径自动 normalize() 处理,确保 relative/foo/../bar、Windows 反斜杠等不同形式路径命中同一缓存条目。

1.2 读取时写入

FileReadTool.call() 每次读取文件后执行 readFileState.set()src/tools/FileReadTool/FileReadTool.ts:1032-1037):

typescript 复制代码
readFileState.set(fullFilePath, {
  content,                            // 读取到的文本内容
  timestamp: Math.floor(mtimeMs),     // 文件当前 mtime
  offset,                             // 读取起始行
  limit,                              // 读取行数限制
})

注意 :图片/PDF 不缓存(readFileState.set() 只在文本/notebook 分支调用),缓存仅适用于可编辑文件内容。

1.3 重复读取去重

当模型再次读取同一文件时(src/tools/FileReadTool/FileReadTool.ts:540-573):

css 复制代码
检查 readFileState 中是否存在该文件
    ↓ 存在且 offset/limit 相同 ↓
检查文件 mtime 是否变化
    ↓ mtime 未变 ↓
返回 file_unchanged 类型结果
    → tool_result 内容为:"File unchanged since last read. The content from the earlier Read tool_result in this conversation is still current --- refer to that instead of re-reading."
    ↓ mtime 已变 ↓
重新读取并更新缓存(反映文件实际变化)

去重效果:BQ Proxy 数据显示约 18% 的 Read 调用是同一文件的重复读取,去重后避免了约 2.64% 的全局 cache_creation token 浪费。

开关控制 :通过 GrowthBook 标志 tengu_read_dedup_killswitch 可关闭此优化(默认开启)。

1.4 变更检测

getChangedFiles()src/utils/attachments.ts:2063-2161)在每轮对话开始时执行:

  1. 遍历 readFileState 中所有文件
  2. 检查当前 mtime 是否 > 缓存中的 timestamp
  3. 仅对已变更的文件重新读取并生成 diff
  4. 结果以 edited_text_file / edited_image_file attachment 形式注入系统消息
  5. 注意:仅跟踪 offset=undefined 的文件(Edit/Write 写入的文件),Read 的 offset 条目跳过变更检测

2. Tool Result 持久化(磁盘缓存)

2.1 大结果溢出持久化

当工具返回结果过大时,不直接截断,而是持久化到磁盘文件(src/utils/toolResultStorage.ts):

bash 复制代码
工具结果大小 > 阈值
    ↓
写入 {sessionDir}/tool-results/{toolName}.{uuid}.txt
    ↓
tool_result 内容替换为文件路径引用 + 前 500 字符预览

默认阈值:MAX_TOOL_RESULT_BYTES(在 constants/toolLimits.ts 中定义)。

不持久化的例外Read 工具的结果标记为 Infinity(持久化会形成 Read→文件→Read 的死循环)。

2.2 跨轮结果替换预算

ContentReplacementStatesrc/utils/toolResultStorage.ts:390-457)追踪每轮对话中哪些工具结果已被替换或持久化,避免同一结果在后续轮次中被反复处理。


3. Prompt Cache(API 级别的缓存)

3.1 系统提示词缓存

系统提示词按 SYSTEM_PROMPT_DYNAMIC_BOUNDARY 分为两部分(src/constants/prompts.ts:114):

部分 范围 缓存行为
静态内容(Intro、System、Doing tasks 等) scope: 'global' 跨组织共享缓存
动态内容(Session guidance、Memory 等) scope: 'org' / 无缓存 按会话组织缓存

3.2 Tool Schema 缓存

toolToAPISchema()src/utils/api.ts:119-240)在会话级 ToolSchemaCache 中缓存每个工具的 base schema(name + description + input_schema + strict + eager_input_streaming):

  • 缓存键:工具名(对于 StructuredOutput 工具,键为 工具名:JSON_Schema_序列化 以避免同名不同 Schema)
  • 缓存内容不包括 cache_controldefer_loading(每轮不同)
  • zodToJsonSchema() 使用 WeakMap 按 Zod 对象引用缓存转换结果

4. File Edit/Write 与缓存的交互

4.1 Edit/Write 更新缓存

FileEditToolFileWriteTool 在写入文件后也会更新 readFileState,但写入的 offsetlimit 均为 undefined

typescript 复制代码
// FileEditTool 写入后
readFileState.set(fullFilePath, {
  content: newContent,
  timestamp: mtimeMs,   // 写入后的新 mtime
  offset: undefined,    // 标记为非 Read 来源
  limit: undefined,
})

这确保了:

  1. getChangedFiles() 能检测到通过 Edit/Write 修改的文件变更
  2. Read 去重逻辑不会 对 Edit 写入的条目进行去重(existingState.offset !== undefined 检查阻止)

4.2 特殊 stub 标记

FILE_UNCHANGED_STUBsrc/tools/FileReadTool/prompt.ts:7-8):

css 复制代码
"File unchanged since last read. The content from the earlier Read
 tool_result in this conversation is still current --- refer to that
 instead of re-reading."

模型收到此 stub 后应使用历史消息中的内容,而不是要求重新读取。


5. 会话间与子任务缓存继承

5.1 压缩(Compact)时缓存

压缩会话时,compress.ts:109 中的 cacheToObject()readFileState 序列化为普通对象,压缩完成后通过重新注入消息重新构建缓存状态。

5.2 子任务 Fork

cloneFileStateCache()src/utils/fileStateCache.ts:122-126)用于子任务分叉时复制父任务的 FileStateCache

typescript 复制代码
export function cloneFileStateCache(cache: FileStateCache): FileStateCache {
  const cloned = createFileStateCacheWithSizeLimit(cache.max, cache.maxSize)
  cloned.load(cache.dump())
  return cloned
}

5.3 子任务结果合并

mergeFileStateCaches()src/utils/fileStateCache.ts:129-142)合并子任务的缓存状态回父任务:

typescript 复制代码
export function mergeFileStateCaches(first, second) {
  const merged = cloneFileStateCache(first)
  for (const [filePath, fileState] of second.entries()) {
    const existing = merged.get(filePath)
    if (!existing || fileState.timestamp > existing.timestamp) {
      merged.set(filePath, fileState)
    }
  }
  return merged
}

保留时间戳最新的条目(防止子任务读取的旧版本覆盖父任务的新版本)。


6. 总结:会话中缓存哪些内容

缓存类型 作用域 容量限制 失效时机 跨会话
FileStateCache(文件内容) 单会话 100 条目 / 25MB LRU mtime 变化、手动 Evict ❌ 否
Tool Result 持久化(大结果溢写) 单会话 磁盘容量 会话结束清理 ❌ 否
Tool Schema Cache(工具 Schema) 单会话 工具数量 会话结束 ❌ 否
System Prompt Cache(API 端) API 级别 API 控制 系统提示词变更 / TTL ❌ 否
zodToJsonSchema 缓存 单进程 Schema 引用数(WeakMap) 无(随 Schema 对象 GC) ❌ 否

核心流程

kotlin 复制代码
模型第一次读取 package.json
    ↓
FileReadTool.call() 读文件
    ↓
readFileState.set("package.json", { content, mtime, offset, limit })
    ↓
返回文件内容

第二轮模型又读取 package.json(相同 offset/limit)
    ↓
FileReadTool.call() 检查缓存命中
    ↓
对比 mtime(未变)
    ↓
返回 "File unchanged since last read."  stub
    ↓
模型从历史消息中拿到实际内容

工具(如 Bash)修改了 package.json
    ↓
下一轮开始时 getChangedFiles() 检测到 mtime 变化
    ↓
重新读取并注入 edited_text_file attachment
    ↓
模型感知到文件已变更

关键文件索引

文件 功能
src/utils/fileStateCache.ts LRU 缓存核心实现(100条目/25MB)
src/tools/FileReadTool/FileReadTool.ts 读取去重(mtime 对比 + stash stub)
src/tools/FileReadTool/prompt.ts FILE_UNCHANGED_STUB 常量
src/utils/attachments.ts getChangedFiles() 变更检测 + diff
src/utils/toolResultStorage.ts 大结果持久化 + 替换预算
src/utils/api.ts toolToAPISchema() Schema 缓存
src/utils/zodToJsonSchema.ts Zod→JSON Schema WeakMap 缓存
src/constants/prompts.ts 系统提示词动态边界(缓存范围)
src/services/api/claude.ts API prompt caching 配置
相关推荐
Marsloting2 小时前
Fable 5 入职第一天,我没让它转正
claude
147API4 小时前
Claude Fable 5 接入拆解:从 Messages API 到 fallback 要改哪些地方
状态模式·claude·fable5
得物技术6 小时前
让 Claude Code 拥有自我进化和记忆系统|得物技术
程序员·ai编程·claude
DO_Community6 小时前
Mythos级最强 AI 模型 Claude Fable 5 现已上线 DigitalOcean无服务器推理
人工智能·serverless·agent·ai编程·claude
字节跳动数据库6 小时前
AI 失控处理术
人工智能·claude
Nturmoils7 小时前
把 GitNexus 接进 Codex:安装、索引、Web UI 和项目分析实操
openai·claude
糖葫芦君8 小时前
Claude code并行运行代理
人工智能·claude
钱多多_qdd9 小时前
claude code(九):【Claude Code官方最佳实践7️⃣】:通过多 Claude 工作流程提升水平
ai·claude
Aqoo1 天前
Claude Fable 5 发布:最强模型来了,但带了把锁
claude