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_path | string | 是 | 要修改的文件的绝对路径 |
start_line | integer | 是 | 要替换的起始行号(从 1 开始) |
end_line | integer | 是 | 要替换的结束行号(包含) |
expected | string | 是 | 预期的当前内容,用于验证行号是否准确。至少需要 3 行非空代码行 |
new_content | string | 是 | 替换后的新内容 |
安全性:工具会自动验证 expected 与实际内容是否一致(见"内容验证算法"章节),验证不通过则拒绝执行。
2. append
在文件末尾追加内容。
适用场景:
- 文件末尾追加:在已有文件尾部添加新函数、配置项、导入语句等
- 空文件写入:文件没有任何内容时,append 是唯一可用的编辑方式(line_range 无法定位行号)
- 不关心行号:如果不想计算要写入的位置,直接追加到文件末尾
append 不需要指定行号,也不需要内容验证——文件末尾的位置没有歧义。
{
"file_path": "src/lib.rs",
"mode": "append",
"content": "\n// 新函数\npub fn helper() {}"
}| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
file_path | string | 是 | 文件的绝对路径 |
mode | string | 是 | 必须为 "append" |
content | string | 是 | 要追加的内容 |
内容验证算法
这是 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. 否则 → 拒绝
}设计原则
- 保守拒绝:宁可拒绝也不要静默放行错误位置。被拒绝后 AI 可以重读文件再试,但改错位置可能导致难以发现的 bug。
- 简单可预测:没有百分比模糊,没有多层计算。规则是"每一行都必须匹配",不匹配就拒绝。行为是二元的。
- 语言无关:通过在匹配前 trim 掉空白,消除了缩进差异的影响。set 匹配消除了行顺序差异。
- 方向性:只要求 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'相关文档
- file_edit 快速参考 — 参数速览和约束限制
- 内置工具参考 — 查看所有工具的概览
- CLI 使用指南 —
run命令详细用法