Skip to main content

三大工具的职责分化

Claude Code 将文件操作拆分为三个独立工具——这不是功能划分,而是风险分级
Read 的 maxResultSizeCharsInfinity,但这并不意味着无限制输出——真正的截断发生在 validateContentTokens() 中基于 token 预算的动态判定,而非字符数硬限制。

FileRead:多模态文件读取引擎

源码路径:packages/builtin-tools/src/tools/FileReadTool/FileReadTool.ts

读取去重机制

Read 工具有一个常被忽视但至关重要的去重层。当 AI 重复读取同一个文件的同一范围时,系统不会浪费 token 发送两份完整内容:
关键设计点:
  • 去重仅对 Read 工具自身的读取生效(通过 offset !== undefined 判定)
  • Edit/Write 也会写入 readFileState,但它们的 offsetundefined,所以不会误命中去重
  • 通过 mtime 比对确保文件未被外部修改
  • 有 GrowthBook killswitch(tengu_read_dedup_killswitch)可紧急关闭
实测数据:BQ proxy 显示约 18% 的 Read 调用是同文件碰撞,占 fleet cache_creation 的 2.64%。

多格式分发:文本、图片、PDF、Notebook 四条路径

Read 工具的 callInner()ext 分发到四条完全不同的处理路径:
图片路径的压缩策略特别精细:
  1. 先用 maybeResizeAndDownsampleImageBuffer() 标准缩放
  2. base64.length * 0.125 估算 token 数
  3. 超出预算时调用 compressImageBufferWithTokenLimit() 激进压缩
  4. 仍然超限时用 sharp 做最后兜底:resize(400,400).jpeg({quality:20})
PDF 路径有页数阈值:超过 PDF_AT_MENTION_INLINE_THRESHOLD(默认值在 apiLimits.ts)时强制分页读取,每请求最多 PDF_MAX_PAGES_PER_READ 页。

安全防线

Read 工具在 validateInput() 中设置了多层安全门:
  1. 设备文件屏蔽BLOCKED_DEVICE_PATHS):/dev/zero/dev/random/dev/tty 等——防止无限输出或阻塞挂起
  2. 二进制文件拒绝hasBinaryExtension):排除 PDF 和图片扩展名后,阻止读取 .exe.so 等二进制文件
  3. UNC 路径跳过:Windows 下 \\server\share 路径跳过文件系统操作,防止 SMB NTLM 凭据泄露
  4. 权限拒绝规则matchingRuleForInput):匹配 deny 规则后直接拒绝

文件未找到时的智能建议

当文件不存在时,Read 不会只报一个 “file not found”:
对 macOS 截图文件名中 AM/PM 前的薄空格(U+202F)做了特殊处理——这是实测中发现的跨 macOS 版本兼容性问题。

FileEdit:精确字符串替换引擎

源码路径:packages/builtin-tools/src/tools/FileEditTool/FileEditTool.ts + utils.ts

引号标准化:AI 无法输出的字符怎么办

AI 模型只能输出直引号(' "),但源码中可能使用弯引号(' ' " ")。findActualString() 函数处理了这个不对齐:
匹配后还有反向引号保持preserveQuoteStyle):如果文件用弯引号,替换后的新字符串也自动转换为弯引号,包括缩写中的撇号(如 “don’t”)。

原子性读-改-写

Edit 工具的 call() 方法实现了一个无锁原子更新协议:
步骤 3-8 之间不允许任何异步操作(源码注释明确写道:“Please avoid async operations between here and writing to disk to preserve atomicity”)。这确保了在 mtime 校验和实际写入之间不会有其他进程修改文件。

防覆写校验

Edit 工具在 validateInput() 中检查两个条件:
  1. 必须先读取readFileState 中有记录且不是局部视图)
  2. 文件未被外部修改mtime 未变,或全量读取时内容完全一致)
Windows 上的 mtime 可能因云同步、杀毒软件等被修改而不改变内容,因此对全量读取做了内容级比对作为兜底。

编辑大小限制

超过 1 GiB 的文件直接拒绝编辑——这是 V8 字符串长度限制(~2^30 字符)的安全边界。

FileWrite:全量写入与创建

源码路径:packages/builtin-tools/src/tools/FileWriteTool/FileWriteTool.ts Write 工具与 Edit 共享大部分基础设施(权限检查、mtime 校验、fileHistory 备份),但有两个关键差异:

行尾处理

Write 工具始终使用 LF 行尾。早期版本会保留旧文件的行尾或采样仓库行尾风格,但这导致 Linux 上 bash 脚本被注入 \r——现在 AI 发什么行尾就用什么行尾。

输出区分

Write 工具返回 type: 'create' | 'update'
  • create:文件不存在,originalFile: null
  • update:文件存在且被覆盖,structuredPatch 包含完整 diff

文件历史快照系统

源码路径:src/utils/fileHistory.ts 每次 Edit/Write 前都会调用 fileHistoryTrackEdit(),快照存储在 FileHistoryState 中:
  • 最多保留 MAX_SNAPSHOTS = 100 个快照
  • 备份使用内容哈希去重(同一文件多次未变只存一份)
  • 支持差异统计(DiffStatsinsertions / deletions / filesChanged
  • 快照通过 recordFileHistorySnapshot() 持久化到会话存储

LSP 通知链路

Edit 和 Write 完成写入后都会:
  1. clearDeliveredDiagnosticsForFile() — 清除旧诊断
  2. lspManager.changeFile() — 通知 LSP 文件已变更
  3. lspManager.saveFile() — 触发 LSP 保存事件(TypeScript server 会重新计算诊断)
  4. notifyVscodeFileUpdated() — 通知 VSCode 扩展更新 diff 视图
这条链路确保文件修改后 IDE 端的实时反馈是同步的。

Cyber Risk 防御

Read 工具在文本内容后追加一个 <system-reminder> 提示:
这个提示只在非豁免模型上生效(MITIGATION_EXEMPT_MODELS 目前包含 claude-opus-4-6)。模型级别的豁免表明:防恶意代码的判断力在不同模型间有差异,这是一个精巧的分级策略。