ZapMyCo
指南

file_edit 文件编辑工具

file_edit 是 zapmyco 最核心的工具之一,负责对本地文件进行精确、安全的内容修改。

设计哲学

file_edit 的核心思想是:让工具来适配 AI,而不是让 AI 来适配工具

这意味着我们分析 AI 的能力特点,然后围绕它们设计接口:

AI 擅长的AI 不擅长的
理解代码逻辑和意图精确复现缩进、空格、空行
生成正确的代码提供 exact match 的原文
描述"改什么"记住读过的每一行格式细节

所以 file_edit 采用行号定位 + 内容验证的策略——AI 只需要说"把这几行改成这样",工具负责"找位置"和"验证安全"。


两种编辑模式

1. line_range

通用的行范围编辑模式,覆盖替换、插入、删除三种操作。

适用场景:

  • 替换 — 修改文件中的指定行,直接指定行号范围和新内容即可
  • 插入 — 在任意位置插入新行(文件头、行间、文件尾),new_content 中包含原有行的内容,将新行插入其中
  • 删除 — 移除指定行,new_content 中跳过要删除的行即可

工作方式: AI 在通过 file_read 读取文件时已经获取了带行号的代码,直接指定范围即可。

{
  "file_path": "src/main.rs",
  "start_line": 11,
  "end_line": 13,
  "expected": "    let x = 1;\n    return x;\n}",
  "new_content": "    let x = 42;\n    return x;\n}"
}

插入示例(在第 3 行后插入新行):

第 3 行: let a = 1;
第 4 行: let b = 2;
第 5 行: let c = 3;

line_range(start=4, end=4,
  expected="let b = 2;\nlet c = 3;\nlet d = 4;",
  new_content="let b = 2;\nlet inserted = 999;\nlet c = 3;")

结果: 在第 4 行位置展开为 3 行(保留原有行 + 插入新行)

删除示例(删除第 5-6 行):

line_range(start=5, end=6,
  expected="let e = 5;\nlet f = 6;\nlet g = 7;",
  new_content="let g = 7;")

结果: 第 5-6 行被压缩为 1 行(跳过被删除的行)
参数类型必填说明
file_pathstring要修改的文件的绝对路径
start_lineinteger要替换的起始行号(从 1 开始)
end_lineinteger要替换的结束行号(包含)
expectedstring预期的当前内容,用于验证行号是否准确。至少需要 3 行非空代码行
new_contentstring替换后的新内容

安全性:工具会自动验证 expected 与实际内容是否一致(见"内容验证算法"章节),验证不通过则拒绝执行。

2. append

在文件末尾追加内容。

适用场景:

  • 文件末尾追加:在已有文件尾部添加新函数、配置项、导入语句等
  • 空文件写入:文件没有任何内容时,append 是唯一可用的编辑方式(line_range 无法定位行号)
  • 不关心行号:如果不想计算要写入的位置,直接追加到文件末尾

append 不需要指定行号,也不需要内容验证——文件末尾的位置没有歧义。

{
  "file_path": "src/lib.rs",
  "mode": "append",
  "content": "\n// 新函数\npub fn helper() {}"
}
参数类型必填说明
file_pathstring文件的绝对路径
modestring必须为 "append"
contentstring要追加的内容

内容验证算法

这是 file_edit 最核心的设计。算法目的很简单:AI 说"11-13 行内容大概是这样的",我们怎么确认它说的是对的?

算法定义

fn validate_content(expected: &str, actual: &str) -> bool {
    // 0. 对 expected 和 actual 都做引号归一化
    //    (弯引号 → 直引号,消除 LLM 输出格式差异)
    // 1. 提取 expected 中的所有非空行,trim 掉空白
    // 2. 如果不足 3 行 → 拒绝(判别力不够)
    // 3. 如果有行短于 3 字符且总行数 < 4 → 拒绝(避免 "}" 三连匹配)
    // 4. 每一行 trim 后都必须在 actual 中能找到 → 通过
    // 5. 否则 → 拒绝
}

设计原则

  1. 保守拒绝:宁可拒绝也不要静默放行错误位置。被拒绝后 AI 可以重读文件再试,但改错位置可能导致难以发现的 bug。
  2. 简单可预测:没有百分比模糊,没有多层计算。规则是"每一行都必须匹配",不匹配就拒绝。行为是二元的。
  3. 语言无关:通过在匹配前 trim 掉空白,消除了缩进差异的影响。set 匹配消除了行顺序差异。
  4. 方向性:只要求 AI 说的行在文件里出现,不要求文件里的每一行都在 AI 的描述中出现。这允许 AI 省略注释、空行等非关键内容。

边界 Case

场景expected文件实际内容结果
完全一致let x = 1;\nlet y = 2;\nlet z = 3;同 left✅ 通过
缩进不同 let x = 1;\n let y = 2;\n let z = 3; let x = 1;\n let y = 2;\n let z = 3;✅ 通过
一行不对let x = 1;\nlet y = 2;\nlet z = 3;let x = 1;\nlet y = 999;\nlet z = 3;❌ 拒绝
完全无关let x = 1;\nlet y = 2;\nlet z = 3;fn foo() {\n bar()\n}❌ 拒绝
不足 3 行let x = 1;\nlet y = 2;同 left❌ 拒绝(判别力不足)
含空行(过滤后不足 3 行)let x = 1;\n\nlet y = 2;同 left❌ 拒绝
短行三连x\n}\ny同 left❌ 拒绝(需至少 4 行)
实际多出内容let x = 1;\nlet y = 2;\nreturn x;let x = 1;\nlet y = 2;\n// debug\nreturn x;✅ 通过(额外行不影响)
顺序不同let y = 2;\nlet z = 3;\nlet x = 1;let x = 1;\nlet y = 2;\nlet z = 3;✅ 通过(set 匹配)

安全模型

file_edit 的安全模型是多层防护的:

第一层:权限控制
  └── CLI 层通过 --permission-mode 控制
      ├── Full: 所有工具可用
      ├── ReadWrite: shell_exec 被禁止
      └── ReadOnly: file_write, file_edit, shell_exec 被禁止

第二层:预读检查
  └── 编辑已有文件前,必须先通过 file_read 读取
      ├── 防止 AI 在不了解文件内容的情况下盲目覆盖
      └── 追踪文件的 mtime,文件被外部修改后拒绝操作

第三层:内容验证
  └── 在执行替换前验证 AI 的描述与实际内容一致
      ├── 防止 AI 数错行号导致改错位置
      └── 验证不通过时返回详细差异信息,帮助 AI 修正

第四层:文件安全
  └── 二进制文件检测(null byte 检查)
  └── UTF-8 编码验证
  └── .ipynb 文件拒绝
  └── 目录路径拒绝

多编辑的行号安全

当 AI 在一轮中发出多个编辑时,行号偏移是一个经典问题。

问题

AI 想要同时修改文件的两个位置:

file_edit(file.txt, lines 5-6)   → 替换为 3 行(行数 +1)
file_edit(file.txt, lines 10-12) → 替换为 1 行(行数 -2)

如果从上到下执行,改完 5-6 行后,10-12 行在文件中的实际位置变成了 11-13 行,原始行号就错了。

解法:降序执行

所有编辑都基于原始文件的行号计算,按行号从大到小排序执行:

收到的编辑:
  edit A: lines 5-6
  edit B: lines 10-12

排序后执行:
  1. edit B: lines 10-12(先执行:不影响上面的行号)
  2. edit A: lines 5-6(后执行:10-12 行长度变化不影响 5-6 行)

编辑在内存中依次执行,一次性写回文件。AI 不需要关心行号偏移问题——即使在一轮中发出多个编辑,系统会自动按降序处理。


设计考量

为什么不用 diff/patch 格式?

diff/patch 对人类可读,但对 AI 来说反而增加了复杂度。AI 需要算对行号偏移、确保上下文行完全匹配、用 +/- 前缀标记每一行——这些都是额外的出错点。行号 + 内容验证是更简单、更可靠的方式。

为什么不用百分比模糊匹配?

百分比阈值(如 70% 相似度)引入了一个模糊地带——哪些差异算不算匹配?不同的场景需要不同的阈值吗?这些问题不直观。

我们的方案是二元的:要么每一行都在,要么拒绝。没有模糊地带,行为可预测,调试简单。

为什么不少于 3 行?

1 行的判别力太低。"}" 在几乎所有代码文件中都会出现。2 行稍好但仍不够,如 "}" + "}" 的组合在 Rust、Go 中也不罕见。3 行确保 AI 提供了足够的上下文,不太可能出现两个不同位置有完全一样的 3 行。

如果行号错了怎么办?

行号错误是 line_range 模式下唯一残留的摩擦点。但失败后果很低——内容验证会拒绝执行,返回详细的差异信息,AI 重读文件后即可修复。


使用示例

# 替换:修改 src/main.rs 的第 11-13 行内容
zapmyco run '将 src/main.rs 的第 11-13 行内容改为返回 42'

# 插入:在文件第 5 行后插入新导入语句
zapmyco run '在 src/main.rs 的第 5 行后插入 use std::collections::HashMap;'

# 删除:移除文件第 20-22 行
zapmyco run '删除 src/main.rs 的第 20-22 行'

# 追加:在文件末尾添加新函数
zapmyco run '在 src/lib.rs 末尾追加一个 helper 函数'

# 空文件写入:创建并写入初始内容
zapmyco run '在 src/config.rs 中写入默认配置'

# 同时编辑多个位置(自动合并为降序执行)
zapmyco run '将 Cargo.toml 中的版本号改为 0.2.0,并将 edition 改为 2024'

相关文档

On this page