# Step 14: Write 工具

一句话导读:把 Read 里的 readFileSync 换成 writeFileSync,代码几乎对称,却跨过了 AI 助手最重要的一道分水岭——从「只能看」变成「能改你的文件」。这一步顺带引出一个生产级工具都绕不开的抉择:全量覆盖 vs 补丁编辑。


# 一、这一步做了什么(What)

step14 实现 tools/write.ts,第三个真实工具,也是能「改变世界」的第二个(第一个是 Bash):

  1. WriteTool:实现 ToolDef,execute 用 writeFileSync 全量写入文件;
  2. 参数校验 + 错误兜底:file_path 缺失提前返回,写失败 catch 成 ToolResult;
  3. 写入反馈:成功返回「已写入 X (N 字节, M 行)」,让调用方确认结果;
  4. 注册进 registry:此刻四个工具同时在册——Write + Bash + Read + Echo,各司其职。

从此工具系统凑齐了「执行 / 读 / 写」三种基本能力,一个能干活的 AI 助手的雏形成型。


# 二、面试官视角:为什么要做?(Why)

面试题:这个 Write 工具是「全量覆盖」——每次把整个文件重写一遍。真实 Claude Code 的编辑工具却是「补丁模式」只改几行。为什么?全量写不是更简单可靠吗?

因为在 AI 场景下,全量写入有个致命的经济问题:token 成本。

维度 全量覆盖(我们) 补丁/编辑模式(真实)
实现复杂度 极简,writeFileSync 一行 复杂:定位、替换、行范围
改 1000 行文件的 1 行 AI 要重新输出整个 1000 行 AI 只输出改动的那几行
token 消耗 和文件大小成正比,巨贵 只和改动量成正比,便宜
出错风险 AI 重写整文件时可能改错没动的部分 只碰目标行,其余不受影响
速度 输出长,慢 输出短,快

核心洞察:全量写入的成本是「文件有多大」,补丁写入的成本是「改动有多大」。对 AI 编程助手,改动通常远小于文件,全量写等于每次让模型把整个文件抄一遍——又慢、又贵、还容易在抄写中引入新错误。这就是真实版宁可用 6 个文件实现补丁编辑、也不用一行 writeFileSync 的原因。我们这版选全量,是为了先讲清「写」这个能力本身;成本优化是下一层的事。


# 三、原理:它是怎么工作的(How)

# 和 Read 高度对称

import { writeFileSync } from 'fs';
import { ToolInput, ToolResult } from '../types.js';

export const WriteTool = {
  name: 'Write',
  description: '写入内容到文件(覆盖已有内容)',
  inputSchema: {
    type: 'object',
    properties: {
      file_path: { type: 'string', description: '文件路径' },
      content: { type: 'string', description: '写入内容' },
    },
    required: ['file_path', 'content'],
  },

  async execute(input: ToolInput): Promise<ToolResult> {
    const path = input.file_path as string;
    const content = input.content as string;
    if (!path) {
      return { content: [{ type: 'text', text: '错误: 未指定文件路径' }], isError: true };
    }
    try {
      writeFileSync(path, content || '', 'utf-8');   // ← 核心就这一行,全量覆盖
      const lines = (content || '').split('\n').length;
      return { content: [{ type: 'text',
        text: '已写入 ' + path + ' (' + content.length + ' 字节, ' + lines + ' 行)' }] };
    } catch (err: any) {
      return { content: [{ type: 'text', text: '写入失败: ' + (err.message || String(err)) }], isError: true };
    }
  },
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

对比 step13 的 Read,结构完全一致:校验 → try 里干活 → catch 兜底,成败都返回 ToolResult。这种对称正是 step11 统一接口的红利——写一个新工具,套用同一个骨架即可。

# 一个细节:content || '' 容忍空写入

writeFileSync(path, content || '', 'utf-8');
1

用 content || '' 而非直接 content,是为了让「写入空内容」(如创建空文件、清空文件)成为合法操作而不是 undefined 报错。小细节,但体现了对边界输入的容忍。

# 反馈里带「字节数 + 行数」

成功不是简单返回「OK」,而是「已写入 X (N 字节, M 行)」。这个反馈是给调用方(AI/用户)确认用的——AI 拿到「M 行」能核对是否符合预期,比一句「成功」信息量大得多。工具的返回值也是它和调用方的沟通语言。

# 数据流

AI 调用 Write({file_path, content})
   ↓
path 为空? ──是──> {isError:true}
   ↓ 否
writeFileSync(path, content||'', 'utf-8')   ← 整个文件被覆盖
   ↓成功                         ↓抛异常(EACCES/EISDIR...)
{text:'已写入 X (N字节 M行)'}   catch → {text:'写入失败:...', isError:true}
   ↓
统一 ToolResult
1
2
3
4
5
6
7
8
9

同 step12/13:Write 已注册但 AI 尚不能自主调用(对话循环还没接工具)。此刻四个工具在册,是给后续「AI 自主调工具」那一步备好的弹药。


# 四、深入追问(面试常见 follow-up)

Q:Write 和 Read 代码几乎一样,为什么说 Write「危险得多」? A:因为方向不同。Read 是只读,最坏结果是读到不该读的;Write 是破坏性的——全量覆盖会无条件抹掉文件原有内容,写错路径就毁了别的文件,且没有备份、没有撤销。AI 自动调用一个能覆盖任意文件的工具,风险量级远高于读。这也是为什么真实版给写操作套了权限确认。

Q:全量覆盖除了贵,还有什么正确性风险? A:并发/意图丢失。如果文件在 AI 读取后、写入前被别处改动,全量覆盖会把那些改动静默抹掉(丢失更新问题)。补丁模式因为只动目标行、且能校验上下文,能更早发现「文件变了」。全量覆盖是「我说了算,全按我的来」,很容易踩掉别人的改动。

Q:既然补丁模式这么好,为什么这一步不直接实现它? A:教学节奏。step14 的目的是讲清「写文件」这个能力和它的接口对称性,把它做成和 Read 呼应的最简形态。补丁编辑涉及行定位、上下文匹配、替换算法,是另一个复杂度量级,值得单独展开。先建立「AI 能写」的概念,再优化「怎么写得省」。

Q:content.length 是字节数还是字符数?这个反馈准吗? A:其实是 JS 字符串长度(UTF-16 码元数),对纯 ASCII 等于字节数,但含中文/emoji 时和真实 UTF-8 字节数不一致。作为给人看的量级提示够用,但严格说标签写「字节」不够精确。真实工具会用 Buffer.byteLength 算真字节数——这是个容易被面试官抓的细节。


# 五、踩坑 / 设计权衡

  • 无「文件已存在」保护:writeFileSync 直接覆盖,不提示、不备份。AI 写错路径就是数据丢失。生产版会做存在性检查 + 权限确认 + 可选备份。
  • 不自动创建父目录:目标目录不存在时 writeFileSync 抛 ENOENT。我们靠 catch 接住报错,但不自动 mkdir -p——是否该自动建目录是个有争议的取舍(便利 vs 意外创建)。
  • 字节数标签不精确:content.length ≠ UTF-8 字节数,含多字节字符时偏小。

# 六、与真实源码的对照

我们的实现 Claude Code 源码
write.ts 单文件全量覆盖 tools/FileEditTool/ 6 个文件
writeFileSync 整文件重写 补丁模式:只改指定行 / 插入 / 替换 / 行范围
无成本优化 只传改动,省 token、更快
直接覆盖无确认 权限确认 + 覆盖保护
content.length 当字节数 Buffer.byteLength 精确字节 + diff 展示

# 七、一句话总结

step14 = AI 从「看」到「改」的分水岭:writeFileSync 换掉 readFileSync,接口与 Read 对称(统一骨架的红利),但方向从只读变成破坏性覆盖——而全量覆盖那一行简洁的代价,是「成本随文件大小而非改动大小增长」,这正是真实版宁用 6 个文件做补丁编辑的根本原因。至此四个工具在册,能干活的助手雏形已成。

# 下一节预告

工具备齐了,但 AI 还只能靠 /run 被动触发。接下来的步骤会把工具接进对话循环——让 AI 读到 registry.toAPI()、自己决定调哪个工具、处理 tool_use 与 tool_result,真正实现「AI 自主动手」。