URL: /guide/file-edit

---
title: 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` 读取文件时已经获取了带行号的代码，直接指定范围即可。

```json
{
  "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 不需要指定行号，也不需要内容验证——文件末尾的位置没有歧义。

```json
{
  "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 行内容大概是这样的"，我们怎么确认它说的是对的？**

### 算法定义

```rust
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 重读文件后即可修复。

---

## 使用示例

```bash
# 替换：修改 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 快速参考](/built-in/file_edit) — 参数速览和约束限制
- [内置工具参考](/guide/built-in-tools) — 查看所有工具的概览
- [命令参考](/commands/run) — `run` 命令详细用法
