第14章:文件三剑客——Read、Write、Edit

三种工具,三种能力

Claude Code 提供了三个文件操作工具:

工具能力比喻
FileRead读取文件内容打开书来看
FileWrite创建/覆写文件写一本新书
FileEdit修改文件的一部分在书上做批注

为什么要三个工具而不是一个"万能文件工具"?因为关注点分离——每个工具做一件事,做好一件事。这让权限控制更精细:你可以允许 AI 读文件,但禁止它写文件。

FileRead:读取文件

基本功能


// AI 发出的读文件请求
{
  name: "FileRead",
  input: {
    file_path: "/Users/alice/project/src/app.ts",
    offset: 0,     // 从第几行开始(可选)
    limit: 2000,   // 读取多少行(可选)
  }
}

返回的结果带有行号:


1	import React from "react"
2	import { useState } from "react"
3
4	function App() {
5	  const [count, setCount] = useState(0)
6	  return <button onClick={() => setCount(count + 1)}>{count}</button>
7	}

行号很重要——当 AI 说"第 5 行有问题"时,你能直接找到对应的代码。

不只是文本

FileRead 不只能读文本文件。它支持多种格式:


async function readFile(path: string) {
  const extension = getExtension(path)

  if (isImageFile(extension)) {
    // 图片文件 → 返回图片内容供 AI 视觉分析
    return { type: "image", data: readAsBase64(path) }
  }

  if (extension === ".pdf") {
    // PDF 文件 → 提取文本内容
    return { type: "document", text: extractPDFText(path, pages) }
  }

  if (extension === ".ipynb") {
    // Jupyter Notebook → 解析 cells 和输出
    return { type: "notebook", cells: parseNotebook(path) }
  }

  // 普通文本文件
  return { type: "text", content: readAsText(path) }
}

大文件处理

如果文件有 10 万行怎么办?一次性读取会占用太多 token。


const DEFAULT_LINE_LIMIT = 2000  // 默认最多读 2000 行

if (!input.limit && fileLineCount > DEFAULT_LINE_LIMIT) {
  // 只读前 2000 行
  return readLines(path, 0, DEFAULT_LINE_LIMIT)
  // 并提示:"文件有 100,000 行,只显示了前 2000 行。
  //          使用 offset 和 limit 参数来读取其他部分。"
}

AI 可以使用 offsetlimit 来分批读取大文件:


第一次:read(path, offset=0, limit=2000)     → 第 1-2000 行
第二次:read(path, offset=2000, limit=2000)  → 第 2001-4000 行
...

FileWrite:创建和覆写文件

基本功能


{
  name: "FileWrite",
  input: {
    file_path: "/Users/alice/project/src/Button.tsx",
    content: "import React from 'react'\n\nfunction Button() {\n  return <button>Click me</button>\n}\n"
  }
}

FileWrite 会完全覆盖文件内容。如果文件已存在,旧内容会被替换。

安全检查

写文件之前,FileWrite 会做几项检查:


async function checkBeforeWrite(path, content) {
  // 1. 路径检查:不能写到工作目录外面
  if (!isWithinWorkDir(path)) {
    throw new Error("不能写入工作目录之外的路径")
  }

  // 2. 敏感文件检查
  if (isSensitiveFile(path)) {
    // .env, credentials.json, private_key 等
    warn("警告:这个文件可能包含敏感信息")
  }

  // 3. 必须先读再写
  if (!hasBeenReadBefore(path) && fileExists(path)) {
    throw new Error("修改已有文件前必须先读取它")
  }

  // 4. 文件大小限制
  if (content.length > MAX_FILE_SIZE) {
    throw new Error("文件内容超过大小限制")
  }
}

特别注意第 3 点:必须先读再写。这是为了防止 AI 在不了解文件内容的情况下覆盖它。想象 AI 不小心覆盖了一个重要的配置文件——灾难!

FileEdit:精确修改

FileEdit 是最精细的文件操作工具。它不是覆盖整个文件,而是替换文件中的一段文字


{
  name: "FileEdit",
  input: {
    file_path: "/Users/alice/project/src/app.ts",
    old_string: "const [count, setCount] = useState(0)",
    new_string: "const [count, setCount] = useState(10)",
  }
}

就像"查找替换"——找到 old_string,替换成 new_string

为什么不用行号替换?

你可能会问:为什么不直接说"替换第 5 行"?

原因是:行号会变。如果你在第 3 行插入了一行新代码,原来的第 5 行就变成了第 6 行。用文本内容来定位比用行号更可靠。

唯一性要求

FileEdit 要求 old_string 在文件中是唯一的。如果有多个匹配,编辑会失败:


function editFile(path, oldString, newString) {
  const content = readFile(path)

  // 检查匹配次数
  const matchCount = countOccurrences(content, oldString)

  if (matchCount === 0) {
    throw new Error("找不到要替换的文本")
  }

  if (matchCount > 1) {
    throw new Error(
      `找到 ${matchCount} 个匹配。请提供更多上下文使其唯一。`
    )
  }

  // 只有恰好 1 个匹配时才执行替换
  return content.replace(oldString, newString)
}

如果有多个匹配,AI 需要提供更多的上下文(比如包含上下几行)来确保唯一性。

replace_all 模式

如果你确实想替换所有出现的地方(比如重命名变量),可以用 replace_all


{
  name: "FileEdit",
  input: {
    file_path: "src/app.ts",
    old_string: "myOldFunction",
    new_string: "myNewFunction",
    replace_all: true  // 替换所有出现的地方
  }
}

差异显示

编辑完成后,Claude Code 会显示一个漂亮的差异对比:


  src/app.ts
  ──────────
  function App() {
-   const [count, setCount] = useState(0)
+   const [count, setCount] = useState(10)
    return <button>{count}</button>
  }

红色(-)是被删除的,绿色(+)是新增的。这让用户一眼就能看到改了什么。

文件历史追踪

每次编辑都会被记录下来:


fileHistoryTrackEdit({
  path: "src/app.ts",
  previousContent: "旧内容...",
  newContent: "新内容...",
  timestamp: Date.now(),
  editType: "FileEdit",
})

如果编辑出了问题,可以通过历史记录回退到之前的版本。

三个工具的协作

在实际使用中,三个工具经常配合使用:


场景:AI 修复一个 bug

1. FileRead("src/app.ts")
   → 看到完整的代码

2. FileRead("src/app.test.ts")
   → 看到测试用例,理解期望行为

3. FileEdit("src/app.ts", old, new)
   → 精确修改有 bug 的那一行

4. Bash("npm test")
   → 运行测试验证修复

5. 如果测试失败:
   FileRead("test-output.log")
   → 查看失败信息
   FileEdit("src/app.ts", old2, new2)
   → 再次修改

6. 如果需要新文件:
   FileWrite("src/utils/helper.ts", content)
   → 创建辅助函数

设计决策:为什么不用 Bash 来操作文件?

AI 完全可以用 Bash 来操作文件:cat(读)、echo >(写)、sed(编辑)。为什么还要专门的文件工具?

三个原因:

1. 更精细的权限控制


允许 FileRead,禁止 FileWrite
→ AI 可以看代码但不能改代码

用 Bash 做不到这种区分——catecho > 都是 Bash 命令。

2. 更好的安全检查

文件工具可以做专门的检查(路径验证、敏感文件检测、先读后写要求),Bash 做这些检查更困难。

3. 更好的用户体验

FileEdit 能显示漂亮的差异对比,FileRead 能显示行号。用 Bash 操作文件,输出就是纯文本。

本章小结

  • - FileRead:读文件,支持文本/图片/PDF/Notebook,大文件分批读取
  • - FileWrite:创建或覆写文件,必须先读后写,路径和敏感文件检查
  • - FileEdit:精确替换文本,要求唯一匹配,差异显示,历史追踪
  • - 三个工具分离是为了权限控制、安全检查和用户体验
  • - 文件操作有完整的历史记录,支持回退

文件操作的安全金字塔

从 FileRead 到 FileWrite,每个工具的"危险等级"递增,安全检查也递增:


                    ╱╲
                   ╱  ╲
                  ╱    ╲
                 ╱ Write ╲      ← 最危险:可能覆盖重要文件
                ╱  创建/覆写 ╲     安全检查:路径 + 敏感文件 + 先读后写
               ╱──────────────╲
              ╱                ╲
             ╱      Edit        ╲   ← 中等:修改文件的一部分
            ╱   精确替换文本      ╲    安全检查:唯一性 + 差异显示
           ╱────────────────────╲
          ╱                      ╲
         ╱         Read           ╲  ← 最安全:只看不改
        ╱     读取文件内容          ╲   安全检查:路径验证
       ╱────────────────────────────╲

这种分层设计让你可以给 AI 精细的权限:

  • - 只允许 Read → AI 只能看代码,不能改
  • - 允许 Read + Edit → AI 可以精确修改,但不能创建新文件
  • - 允许 Read + Edit + Write → AI 有完全的文件操作能力

原则:给予完成任务所需的最小权限。

下一章,我们将看看搜索工具:Grep 和 Glob。


本书由 everettjf 使用 Claude Code 分析泄露源码编写 | 保留出处即可自由转载