首页
/ Zed 编辑预测 Teacher 提示词设计:解析 teacher.md 模板与 ep 管线中的提示工程实现

Zed 编辑预测 Teacher 提示词设计:解析 teacher.md 模板与 ep 管线中的提示工程实现

2026-09-04 09:35:10作者:乔或婵

Zed 的编辑预测(Edit Prediction,即 Zeta 系列补全功能)采用“Teacher-Student”蒸馏路线:先用大参数 Teacher 模型在真实编辑样例上生成预测,再蒸馏出低延迟的 Student 模型。crates/edit_prediction_cli/src/prompts/teacher.md 正是发送给 Teacher 模型的提示词模板——它定义了角色、任务步骤、严格的预测规则、输入输出格式,并用六个完整示例约束模型行为。本文完整继承该模板的内容,结合 edit_prediction_cli(命令名为 ep)的源码,讲清模板中每个占位符如何被填充、每条规则如何被解析逻辑兜底,以及如何在仓库中查看这套提示工程管线。

模板在 ep 管线中的位置

edit_prediction_cli 是 Zed 编辑预测的离线实验 CLI(main.rs),支持 readload-projectcontextformat-promptpredictparse-outputscoredistillsynthesize 等子命令。teacher.md 的加载与填充发生在 format-prompt 阶段:

  • prompt_assets.rs 中的 get_prompt(name) 负责读取模板:开启 dynamic_prompts feature 时从 CARGO_MANIFEST_DIR/src/prompts 目录读取(带缓存),否则通过 util::fs_embed!prompts 目录 的内容嵌入二进制,因此该文件既可直接编辑热更新,也可随构建产物分发。
  • format_prompt.rsTeacherPrompt::format_prompt 取出模板后做四个占位符替换:
let prompt_template = crate::prompt_assets::get_prompt("teacher.md");
let prompt = prompt_template
    .replace("{{context}}", &context)
    .replace("{{edit_history}}", &edit_history)
    .replace("{{diagnostics}}", diagnostics.as_deref().unwrap_or(""))
    .replace("{{cursor_excerpt}}", &cursor_excerpt);

这与模板末尾的“Your task”部分一一对应:

# 1. User Edit History

{{edit_history}}


# 2. Related excerpts

{{context}}

# 3. Current File

{{cursor_excerpt}}

{{diagnostics}}

-----

Based on the edit history and context above, predict the user's next edit within the editable region.
```

也就是说,`teacher.md` 前半部分是**静态的指令与示例**,末尾才是**动态拼接的样例数据**。

## 角色与任务定义

模板开头的 “Instructions” 部分向模型声明了角色与三步任务:

> You are an edit prediction assistant in a code editor. Your task is to predict the next edit to a given region of code surrounding the user's cursor.
>
> 1. Analyze the edit history to understand what the programmer is trying to achieve
> 2. Identify any incomplete refactoring or changes that need to be finished
> 3. Make the remaining edits that a human programmer would logically make next (by rewriting the code around their cursor)

“Focus on” 部分进一步收窄优化目标:

- Completing any partially-applied changes made(补全用户做了一半的修改);
- Ensuring consistency with the programming style and patterns already established(与既有代码风格和模式保持一致);
- Making edits that maintain or improve code quality(保持或提升代码质量)。

这里的设计意图很明确:预测的不是“模型认为更好的写法”,而是“这位程序员按当前轨迹下一步会做什么”。源码中的去重与采样逻辑(`deduplicate_examples` 使用 MinHash LSH 按 Jaccard 相似度聚类样例,见 [main.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/main.rs?utm_source=gitcode_repo_files))也印证了这一点——评测集强调的是覆盖不同“编辑轨迹”,而不是覆盖不同代码主题。

## 核心规则:绝不回退用户的编辑

模板中最重的一组规则是“NEVER undo or revert the user's recent edits”。逐条拆解如下:

1. **删除行不可恢复**:编辑历史 diff 中以 `-` 开头的行,即使恢复它能让代码重新完整,也不得还原;
2. **新增行不可删除**:以 `+` 开头的行不得删除或大改;
3. **NO_EDITS 兜底**:如果用户的编辑让代码看起来“坏了”或“不完整”,正确输出是 `NO_EDITS`,而不是通过回退去“修复”;
4. **关键判别式(Key test)**:如果你的预测会让代码**更接近用户编辑前的样子**,就输出 `NO_EDITS`;
5. **永远不要假设删除是误操作**:即使删除破坏了语法或模式,用户也可能正在重写中(mid-rewrite),不得“补全”半截文本靠恢复删除内容实现;
6. **自动生成的代码可以修改**:Hunk 前带有 `// User accepted prediction:` 标记的内容来自上一次被用户接受的预测。与用户亲手输入的内容不同,这些 hunk 可以被编辑、纠正甚至替换。“never undo/revert”规则保护的是用户**当前的键入意图**,自动生成的脚手架不享受此保护;
7. **不要机械套模式**:要结合上下文和程序员目标推理哪些修改是合理的;
8. **不要只修语法错误**:要识别更宽的 refactor 模式并系统性地在整个代码中应用;
9. **保持既有格式**,除非绝对必要;
10. **历史与周边代码冲突时**,优先信任编辑历史中最近的编辑,因为它最能反映当前意图;
11. **把光标附近的半截文本视为用户正在输入的内容**,基于上下文补全;
12. **宁可做出可被拒绝的实质性预测**,也不要只省几个键位的最小预测;
13. **散文/文档场景要保守**:补全当前片段或句子即可,不要额外生成自由内容行,因为散文约束弱、错误续写概率高。

第 6 条是理解整个模板的关键:它显式区分了“用户意图(不可逆)”与“模型生成的脚手架(可改)”。这一点与解析侧的实现相呼应——编辑历史中的 `// User accepted prediction:` 标记由 `ep` 管线在构建样例时写入,模型需要据此调整对 hunk 的保护等级。

## 输入格式:三段式上下文 + 特殊标记

模板的 “Input Format” 一节定义了模型会收到什么:

1. **User Edit History**:按时间顺序的编辑历史(unified diff 形式),用于推断用户轨迹;其中 `// User accepted prediction:` 前缀的 hunk 表示被接受的自动生成代码;
2. **Related excerpts**:代码库中相关文件摘录,用于跨文件推理;文件内的 `…` 表示中间跳过了部分代码;
3. **Current file**:当前文件摘录,其中:
   - `<|editable_region_start|>` 与 `<|editable_region_end|>` 界定**可编辑区域**——模型只能预测该区域内的编辑;
   - `<|user_cursor|>` 标记最后一次编辑后的光标位置。

这四个特殊字符串与 [format_prompt.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/format_prompt.rs?utm_source=gitcode_repo_files) 中的常量一一对应:

```rust
impl TeacherPrompt {
    pub(crate) const EDITABLE_REGION_START: &str = "<|editable_region_start|>\n";
    pub(crate) const EDITABLE_REGION_END: &str = "\n<|editable_region_end|>";
    pub(crate) const USER_CURSOR_MARKER: &str = "<|user_cursor|>";
    pub(crate) const NO_EDITS: &str = "NO_EDITS";
    const MAX_HISTORY_LINES: usize = 128;
```

“只能编辑 editable region”的约束并非只写在提示词里,解析侧同样强制:`TeacherPrompt::parse` 只会从响应中抽取 editable region,并对 region 新旧内容做 diff,region 之外的任何输出都会被丢弃(见下文“输出解析”一节)。

## 占位符的填充细节(源码级补充)

模板静态文本之外,三个数据占位符的填充逻辑值得展开,因为它们决定了模板实际拿到什么样的数据:

**`{{edit_history}}`**:`TeacherPrompt::format_edit_history` 将历史按行截断到最近 `MAX_HISTORY_LINES = 128` 行,超出部分以 `[...truncated...]` 标注;空历史则填 `(No edit history)`。模板规则“优先信任最近的编辑”与此截断策略自洽——模型看到的本来就是最靠近当前意图的那一段。

**`{{context}}`**:`format_context` 从 `prompt_inputs.related_files` 取摘录,用 `format_related_files_within_budget` 在 **1024 token 预算**内选取,渲染为 `````` 代码块(五个反引号),无上下文时填 `(No context)`。模板中提到的 `…` 跳过标记就是由此类摘录的拼接方式产生。

**`{{cursor_excerpt}}`**:`format_cursor_excerpt` 按“上下文前缀 + editable region 标记 + 光标标记 + 上下文后缀”的结构拼装当前文件摘录,外层用 ``````{路径}``` 标注文件路径。

**`{{diagnostics}}`**:只有当 zeta 格式为 `V0420Diagnostics` 时,`format_prompt` 才调用 `format_diagnostics` 并替换为非空内容(以 `# 4. Diagnostics` 小标题形式注入,预算 2000 token),其余情况该占位符被替换为空字符串。这解释了模板为何把 `{{diagnostics}}` 放在 `# 3. Current File` 之后、作为可选段存在。

## 输出格式:说明 + 唯一代码块 + NO_EDITS 逃生阀

模板规定输出必须包含:

1. 基于编辑历史与光标位置,**简要说明用户当前意图**;
2. 一个 markdown 代码块,**只包含**应用了预测编辑后的 editable region;代码块必须以 `<|editable_region_start|>` 开头、以 `<|editable_region_end|>` 结尾,前后不得有其他内容;
3. 若无需编辑(代码已完整正确,或没有清晰的下一步编辑),代码块中只输出 `NO_EDITS`;
4. 若预测结果中存在用户下一步很可能继续编辑的位置,用 `<|user_cursor|>` 标出。

解析侧对这套格式的执行非常严格,位于 [format_prompt.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/format_prompt.rs?utm_source=gitcode_repo_files) 的 `TeacherPrompt::parse`(被 [parse_output.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/parse_output.rs?utm_source=gitcode_repo_files) 和 [predict.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/predict.rs?utm_source=gitcode_repo_files) 复用):

- 先用 `extract_last_codeblock` 抽取**最后一个**代码块,若其内容为 `NO_EDITS` 直接返回空 patch——与模板第 3 条对应;
- 否则用 `extract_editable_region`(`rfind` 起止标记)取出 region,定位其中的 `<|user_cursor|>` 得到新光标偏移,再把该标记从文本中剔除;
- 与 prompt 中的旧 region 做 `unified_diff_with_context` 对比,生成 `--- a/{path} / +++ b/{path}` 形式的 `actual_patch`;
- 光标位置映射为 `ActualCursor`,用于后续评分。

也就是说,模板里的“codeblock 必须且只能包含 region”不是风格建议:模型若把 region 外内容混入代码块,会被解析逻辑静默丢弃或导致 region 抽取失败。

## 六个官方示例:模板的 few-shot 核心

模板用六个完整示例(Example 1–6)覆盖了规则空间的全部关键分支。以下逐一继承原文示例并点明其对应的规则。

### Example 1:用 related excerpts 补全缺失代码

光标处缺少代码,相关摘录中提供了相关类型定义,应据此补全。

**Related Excerpts**

````
struct Product {
    name: String,
    price: u32,
}
````

**User Edit History**

````
--- a/src/calculate.rs
+++ b/src/calculate.rs
@@ -100,6 +100,7 @@
 fn calculate_total(products: &[Product]) -> u32 {
     let mut total = 0;
     for product in products {
+        total += ;
     }
     total
 }
````

**Current File**

````src/calculate.rs
fn calculate_total(products: &[Product]) -> u32 {
<|editable_region_start|>
    let mut total = 0;
    for product in products {
        total += <|user_cursor|>;
    }
    total
<|editable_region_end|>
}
````

**Output**

> The user is computing a sum based on a list of products. The only numeric field on `Product` is `price`, so they must intend to sum the prices.

````
<|editable_region_start|>
    let mut total = 0;
    for product in products {
        total += product.price;
    }
    total
<|editable_region_end|>
````

对应规则:把光标附近半截文本视为“正在输入”,并优先使用 related excerpts 做跨文件推理。

### Example 2:继续用户的打字轨迹,而不是修正拼写

用户正在输入 `eprintln!` 调用。不能通过**删除**新输入内容来“修正拼写”,必须延续用户轨迹。要打印什么并不明确——应填入显然意图的代码,并把光标放在用户能继续填写的位置。

**User Edit History**

````
--- a/src/modal.rs
+++ b/src/modal.rs
@@ -100,4 +100,4 @@
 fn handle_close_button_click(modal_state: &mut ModalState, evt: &Event) {
     modal_state.close();
-     modal_state.dismiss();
+     eprmodal_state.dismiss();
 }
````

**Current File**

````src/modal.rs
// handle the close button click
<|editable_region_start|>
fn handle_close_button_click(modal_state: &mut ModalState, evt: &Event) {
    modal_state.close();
    epr<|user_cursor|>modal_state.dismiss();
<|editable_region_end|>
}
````

**Output**

> The user is clearly starting to type `eprintln!()`, however, what they intend to print is not obvious. I should fill in the print call and string literal, with the cursor positioned inside the string literal so the user can print whatever they want.

````
<|editable_region_start|>
fn handle_close_button_click(modal_state: &mut ModalState, evt: &Event) {
    modal_state.close();
    eprintln!("<|user_cursor|>");
    modal_state.dismiss();
<|editable_region_end|>
````

注意输出中 `<|user_cursor|>` 被放在字符串字面量内部——这是模板第 4 条输出要求(标记下一个可能编辑点)的直接体现。

### Example 3:函数名不确定时做合理猜测并保存键位

用户在新增函数,函数名无法确知。此时应合理猜测函数名和签名,并把光标放进函数体:猜对了就节省可观键位,文件也处于一致状态。

**User Edit History**

````
--- a/src/modal.rs
+++ b/src/modal.rs
@@ -100,4 +100,4 @@
 fn handle_close_button_click(modal_state: &mut ModalState, evt: &Event) {
     modal_state.close();
     modal_state.dismiss();
 }
+
+fn

 fn handle_keystroke(modal_state: &mut ModalState, evt: &Event) {
````

**Current File**

````src/modal.rs
// handle the close button click
fn handle_close_button_click(modal_state: &mut ModalState, evt: &Event) {
    modal_state.close();
<|editable_region_start|>
    modal_state.dismiss();
}

fn<|user_cursor|>

fn handle_keystroke(modal_state: &mut ModalState, evt: &Event) {
<|editable_region_end|>
    modal_state.begin_edit();
````

**Output**

> The user is adding a new function. The existing functions I see are `handle_close_button_click` and `handle_keystroke`, which have similar signatures. One possible function they might be adding is `handle_submit`.

````
<|editable_region_start|>
    modal_state.dismiss();
}

fn handle_submit(modal_state: &mut ModalState, evt: &Event) {
    <|user_cursor|>
}

fn handle_keystroke(modal_state: &mut ModalState, evt: &Event) {
<|editable_region_end|>
````

对应规则:“prefer predictions that save meaningful keystrokes, even if this requires making educated guesses” 与 “better to make a substantive prediction that might be rejected”。

### Example 4:代码已完整,输出 NO_EDITS

代码已经完整且没有清晰的下一步编辑,应输出 `NO_EDITS`。

**User Edit History**

````
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -10,7 +10,7 @@
 fn add(a: i32, b: i32) -> i32 {
-    a - b
+    a + b
 }
````

**Current File**

````src/utils.rs
<|editable_region_start|>
fn add(a: i32, b: i32) -> i32 {
    a + b<|user_cursor|>
}
<|editable_region_end|>
````

**Output**

> The user just fixed a bug in the `add` function, changing subtraction to addition. The code is now correct and complete. There is no clear next edit to make.

````
NO_EDITS
````

### Example 5:删除后看似不完整,仍然 NO_EDITS(规则的核心压力测试)

用户刚删除了代码,留下看起来不完整的内容。绝不能通过恢复被删内容去“补全”——那等于撤销用户的编辑。**即使代码看起来是坏的,NO_EDITS 也是正确响应。**

**User Edit History**

````
--- a/config.nix
+++ b/config.nix
@@ -10,7 +10,7 @@
     # /etc/modular/crashdb needs to be mutable
-    ln -s /tmp/crashdb $out/etc/modular/crashdb
+    ln -s /tmp/cr $out/etc/modular/crashdb
   '';
````

**Current File**

````config.nix
<|editable_region_start|>
    # /etc/modular/crashdb needs to be mutable
    ln -s /tmp/cr<|user_cursor|> $out/etc/modular/crashdb
  '';
<|editable_region_end|>
````

**Output**

> The user deleted `ashdb` from `/tmp/crashdb`, leaving `/tmp/cr`. Although this looks like incomplete text that I could "complete", doing so would restore deleted content. The user intentionally removed that text—I must not undo their deletion.

````
NO_EDITS
````

这是 Key test 规则的典型场景:补全 `/tmp/crashdb` 会让代码更接近编辑前状态,因此拒绝。

### Example 6:用户接受过预测的脚手架可以被改写

用户先接受了模型生成的函数预测,随后开始重命名。原参数是自动生成的(带 `// User accepted prediction:` 标记),因此**可以**随新函数名一起更新。这不是回退用户输入,而是改进自动生成的脚手架。

**User Edit History**

````
--- a/math_utils.py
+++ b/math_utils.py
@@ -3,3 +3,5 @@
 def calculate_rectangle_area(width, height):
     return width * height

+de

// User accepted prediction:
--- a/math_utils.py
+++ b/math_utils.py
@@ -3,5 +3,7 @@
 def calculate_rectangle_area(width, height):
     return width * height

-de
+def calculate_rectangle_perimeter(width, height):
+

--- a/math_utils.py
+++ b/math_utils.py
@@ -5,5 +5,5 @@
     return width * height

-def calculate_rectangle_perimeter(width, height):
+def calculate_sq_perimeter(width, height):
````

**Current File**

````math_utils.py
def calculate_rectangle_area(width, height):
    return width * height

<|editable_region_start|>
def calculate_sq<|user_cursor|>_perimeter(width, height):

<|editable_region_end|>
````

**Output**

> The user accepted a prediction for `calculate_rectangle_perimeter(width, height)`, then started renaming `rectangle` to `square`. Since squares have equal sides, the arguments should change from `(width, height)` to `(side)`. The arguments were auto-generated (from an accepted prediction), so modifying them is appropriate.

````
<|editable_region_start|>
def calculate_square_perimeter(side):
    <|user_cursor|>
<|editable_region_end|>
````

注意这里同时演示了两点:模型不仅改名,还把 `(width, height)` 改为 `(side)`(结合数学常识的系统性重构);且这合法,因为参数来自已接受的预测而非用户手敲。

## 从模板到管线:Teacher 后端、批处理与蒸馏

模板之外,仓库源码揭示了它被使用的完整方式:

**后端选择**。[main.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/main.rs?utm_source=gitcode_repo_files) 定义了 `TeacherBackend` 枚举(`sonnet45` 为默认、`sonnet46`、`gpt52`、`gpt54`、`gpt55`),对应模型名如 `claude-sonnet-4-5`、`gpt-5.2` 等;provider 字符串形如 `teacher:sonnet46` 或 `teacher:gpt52`。`teacher.md` 是 `Teacher` / `TeacherNonBatching` provider 共用的模板;另有 `teacher_jumps.md` 供 `TeacherJumps` 的长程编辑预测使用(带 hash region 标记,模板结构相近但标记体系不同)。

**批处理请求**。批处理路径通过 [anthropic_client.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/anthropic_client.rs?utm_source=gitcode_repo_files) 中的 `PlainLlmClient`(依赖 `ANTHROPIC_API_KEY` 环境变量)把格式化好的 prompt 作为单条 user message 发给模型 API,支持流式与非流式。

**解析与评分**。`ep parse-output` 调用上文所述的 `TeacherPrompt::parse` 把模型原始输出(`actual_output`)转为 `actual_patch`;`ep score` 对比 expected/actual patch 计算得分;`ep qa` 用 LLM-as-a-judge 复核质量,`ep repair`([repair.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/repair.rs?utm_source=gitcode_repo_files))对低分预测重新生成并同样委托 `TeacherPrompt::parse` 解析——模板的解析契约在修复回路中被复用。

**蒸馏**。[distill.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/distill.rs?utm_source=gitcode_repo_files) 的 `run_distill` 将样例中实际的预测 patch(若存在 `repair` provider 的预测则优先取用)写入 `expected_patches_with_cursor_positions`,并清空原始 prompt、predictions 与 score 字段,产出只保留“输入 + 期望输出”的蒸馏数据集——`teacher.md` 的产出最终服务于训练 Student 模型。

**评测样例**。[evals 目录](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/evals?utm_source=gitcode_repo_files) 中的 markdown 文件(如 `tree-sitter--tuple-to-struct-destructuring.md`、`flask--rename-accepted-prediction.md`、`zed--add-eprintln.md` 等)与模板六个示例同构,可视为模板规则的回归评测集;其中 `flask--rename-accepted-prediction.md` 正是 Example 6 场景(重命名已接受的预测)的评测版本。

## 小结:这份提示词模板的工程要点

- **约束闭环**:模板中的每一条输出约定(唯一代码块、region 标记、`NO_EDITS`、`<|user_cursor|>`)在 `TeacherPrompt::parse` 中都有对应的机械解析,提示词格式即解析器契约;
- **意图优先**:以“用户轨迹”为第一性原则,用 Key test(预测是否使代码回到编辑前)作为 `NO_EDITS` 的判别式,显式保护删除操作,同时为已接受的自动生成代码留出可改写空间;
- **预算控制**:编辑历史截断 128 行、相关上下文 1024 token、诊断 2000 token 的预算在源码中硬编码,保证模板在不同样例规模下输入长度可控;
- **可复现**:模板文件经 `get_prompt` 缓存或嵌入,后端以 provider 字符串显式指定,整个 format → predict → parse → score → distill 流程可由 `ep` 子命令逐步执行和复现。

对希望研究编辑器内 AI 编辑预测的团队而言,[teacher.md](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/prompts/teacher.md?utm_source=gitcode_repo_files) 与 [format_prompt.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/format_prompt.rs?utm_source=gitcode_repo_files) 是一对值得对读的文档:前者定义了“要模型做什么”,后者定义了“系统如何验证模型做了”——两者共同构成 Zed 编辑预测 Teacher 侧提示工程的完整骨架。
登录后查看全文
热门项目推荐
相关项目推荐