Zed 编辑预测单元测试样例格式详解:flask--add-import-statement 案例精读
本文以 flask--add-import-statement.md 这个真实评测样例为切入点,逐段拆解 Zed 编辑预测(Edit Prediction)单元测试样例的完整结构——前置元数据、编辑历史、光标位置标注与多个候选期望补丁——并结合 example_spec.rs、example.rs 等源码说明这些 Markdown 字段如何被解析为 ExampleSpec 结构体、又如何参与预测与打分流程,帮助读者读懂乃至独立编写合规的评测样例。
1. 什么是编辑预测单元测试样例
Zed 的编辑预测功能(即行内代码续写)依赖一套完整的评测基础设施:给定"用户在某个仓库的某个修订版本上、光标处于某处、之前刚做过一串编辑"这一上下文,让模型产出一个补丁(patch)与新的光标位置,再与预先记录的"期望补丁"对比打分。crates/edit_prediction_cli/evals/ 目录存放的就是这一流程的单元测试样例,目录中按 仓库名--行为描述 的命名规则组织了 19 个样例,例如 flask--add-import-statement、tree-sitter--if-let-to-match、vscode--add-async-and-await 等。
从源码看,样例支持三种文件扩展名。example.rs 中的 read_example_files 函数按扩展名分发:.json 与 .jsonl 直接反序列化为 Example(后者每行一条),而 .md 文件则走 parse_markdown_example,最终委托给 ExampleSpec::from_markdown 完成解析。本案例所属的 .md 格式正是人工撰写、代码评审和版本管理最友好的形态。
2. 样例文件逐段精读
下面完整列出 flask--add-import-statement.md 的全部内容,然后分段解释。该样例取材于 Flask 仓库的 src/flask/logging.py:用户把一行合法的 from werkzeug.local import LocalProxy 误敲成了 imfrom werkzeug.local import LocalProxy,光标停在坏行行首,期望模型把这行重写为正确的 import 语句。
2.1 前置元数据:锁定源仓库与修订版本
+++
repository_url = "https://github.com/pallets/flask"
revision = "2fec0b206c6e83ea813ab26597e15c96fab08be7"
+++
文件以 +++ 分隔的 TOML 前置块开头。example_spec.rs 中对应的 FrontMatter 结构体定义了两个必填字段与两个可选字段:
| 字段 | 必填 | 说明 |
|---|---|---|
repository_url |
是 | 评测上下文所属的 git 仓库地址,评测时会据此拉取 worktree 加载真实项目 |
revision |
是 | 精确的 commit 哈希,保证上下文可复现 |
tags |
否 | 样例标签列表,用于筛选 |
uncommitted_diff_requires_edit_history_rollback |
否 | 标记未提交 diff 是否包含需要回滚的编辑历史 |
2.2 Edit History:用户此前的编辑序列
## Edit History
```diff
--- a/src/flask/logging.py
+++ b/src/flask/logging.py
@@ -4,7 +4,7 @@
import sys
import typing as t
-from werkzeug.local import LocalProxy
+imfrom werkzeug.local import LocalProxy
from .globals import request
`## Edit History` 段落用一段(或多段)统一 diff 记录"预测发生之前用户刚刚做过的编辑"。这是编辑预测与静态补全的本质区别:模型看到的不仅是文件内容,还有用户行为的"最近趋势"。本样例中,用户把正确的 import 行改成了 `imfrom ...` 这种残缺写法,模型需要判断这是输入中的中间态并给出整行重写。
该段落还支持一个可选约定:在某段 diff 之前单独写一行 `// User accepted prediction:`。同目录的 [flask--rename-accepted-prediction.md](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/evals/flask--rename-accepted-prediction.md?utm_source=gitcode_repo_files) 就用了它,表示紧随其后的那段 diff 并非用户手打、而是"用户接受了一条模型预测"产生的编辑——这对训练与蒸馏是有价值的信号。解析逻辑在 [example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L79) 中以常量 `ACCEPTED_PREDICTION_MARKER` 实现,`from_markdown` 状态机识别到该标记后会在合并编辑历史时重新插入这行注释,保证序列化/反序列化往返一致(同文件测试 `test_from_markdown_accepted_prediction_marker` 验证了这一点)。
### 2.3 Cursor Position:文件节选与光标标记
```text
## Cursor Position
```src/flask/logging.py
from __future__ import annotations
import logging
import sys
import typing as t
imfrom werkzeug.local import LocalProxy
# ^[CURSOR_POSITION]
from .globals import request
if t.TYPE_CHECKING: # pragma: no cover
from .sansio.app import App
`## Cursor Position` 段落的代码围栏由两部分构成:
1. **围栏 info string 是光标所在文件路径**(本例为 `src/flask/logging.py`)。解析器把围栏 info 直接存入 `cursor_path`,见 [example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L393-L396) 中 `Section::CursorPosition` 分支:`spec.cursor_path = Path::new(block_info)`。
2. **围栏内容是该文件的光标周边节选(excerpt),并内嵌一行光标标记**。标记行以语言注释形式写在光标行的下一行,包含 `[CURSOR_POSITION]` 字符串和一个指向光标列的箭头。
箭头有两种语法,定义在 [cursor_excerpt()](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L417-L473) 的文档注释与实现中:
| 箭头 | 光标列语义 |
| --- | --- |
| `^` | 光标列 = `^` 字符在标记行中的位置(本例 `# ^[CURSOR_POSITION]` 中 `^` 位于第 0 列,即 `imfrom` 行行首) |
| `<` | 光标列 = 光标行上第一个非空白字符的位置(用于列位置小于注释前缀长度的场景) |
解析时,`cursor_excerpt()` 会定位 `[CURSOR_POSITION]`(常量 `CURSOR_POSITION_MARKER`,定义于 [udiff.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/zeta_prompt/src/udiff.rs?utm_source=gitcode_repo_files#L88-L89)),算出标记行范围,按 `^` 或 `<` 规则得出光标列,把光标定位在**标记行的上一行**,然后删除标记行、修剪尾部空行,返回"纯文本节选 + 节选内光标字节偏移"。若文本中直接内嵌了 `<|user_cursor|>`(常量 `INLINE_CURSOR_MARKER`),则走更简单的内联分支:删除标记、其位置即光标偏移。[example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L520-L657) 的 `test_cursor_excerpt_with_caret` 测试覆盖了行首、行内、行尾、文件末尾等边界列位,`test_cursor_excerpt_with_inline_marker` 则验证内联标记。
### 2.4 Expected Patch:多个可接受的期望补丁
```diff
## Expected Patch
```diff
--- a/src/flask/logging.py
+++ b/src/flask/logging.py
@@ -1,21 +1,21 @@
from __future__ import annotations
import logging
import sys
import typing as t
-imfrom werkzeug.local import LocalProxy
+import
# ^[CURSOR_POSITION]
+from werkzeug.local import LocalProxy
from .globals import request
--- a/src/flask/logging.py
+++ b/src/flask/logging.py
@@ -1,21 +1,21 @@
from __future__ import annotations
import logging
import sys
import typing as t
-
-imfrom werkzeug.local import LocalProxy
+import werkzeug
+# ^[CURSOR_POSITION]
+from werkzeug.local import LocalProxy
from .globals import request
本样例的 `## Expected Patch` 段包含**两个** diff,这是该格式的关键能力之一:同一个 prompt 允许存在多个都被判为正确的期望输出。两个候选补丁都完成"把 `imfrom` 坏行修成正确 import"这一意图,但终态与光标落点不同:
- 候选一:该行变为 `import`,光标停在 `import` 之后(第 6 列,`# ^[CURSOR_POSITION]` 中 `^` 的位置);
- 候选二:该行变为 `import werkzeug`,光标停在 `werkzeug` 之后(第 15 列)。
注意期望补丁里的光标标注复用了与 Cursor Position 段完全相同的"标记注释行"语法,只是出现在 diff 的 `+` 新增行之后。`ExampleSpec` 中 `expected_patches` 的类型是 `Vec<String>`([example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L45)),解析时每个独立的 diff 代码块都会 push 进这个向量。`expected_patches_with_cursor_positions()`([example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L489-L502))则把每个补丁展开为 `(补丁, Option<光标偏移>)` 二元组,文档注释明确:光标偏移是"应用补丁后的新文本中、相对于 hunk 起点的偏移"。与之配对的 `encode_cursor_in_patch` / `extract_cursor_from_patch` 实现在 [zeta_prompt 的 udiff 模块](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/zeta_prompt/src/udiff.rs?utm_source=gitcode_repo_files),负责"含光标标注的补丁文本"与"干净补丁 + 光标偏移"之间的双向转换;同文件测试 `test_expected_patches_with_cursor_positions` 还验证了编码的幂等性。
## 3. 源码级解析:Markdown 如何变成 ExampleSpec
`ExampleSpec` 结构体([example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L25-L54))是样例的核心数据模型,除本节已讲到的字段外,还包括:
- `name`:样例名,缺省时取文件名(不含扩展名),见 [example.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/example.rs?utm_source=gitcode_repo_files#L270-L276) 中 `"md"` 分支对 `example.spec.name` 的兜底填充;
- `reasoning` / `uncommitted_diff`:可选的"推理说明"与"未提交 diff"段落,分别对应 `## Reasoning`、`## Uncommitted Diff` 标题;
- `recently_opened_files` / `recently_viewed_files`:对应 `## Recently Opened Files`、`## Recently Viewed Files` 段落,每行一个路径(可附 tab 分隔的光标偏移);
- `rejected_patch`:对应 `## Rejected Patch` 段落,供 DPO(拒绝采样偏好训练)使用——[example.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/example.rs?utm_source=gitcode_repo_files#L64-L73) 的 `ExamplePrompt.rejected_output` 字段注释即标明 "For DPO"。
全部合法的二级标题常量集中在 [example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L71-L79),解析器 `from_markdown`([L258](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L258) 起)用 `pulldown_cmark` 遍历 Markdown 事件流,维护一个 `Section` 状态机(`Start / UncommittedDiff / RecentlyOpenedFiles / RecentlyViewedFiles / EditHistory / CursorPosition / ExpectedPatch / RejectedPatch / Other`),按当前标题把代码块内容归位到对应字段。约束方面:
- 标题层级只允许 H1(作为样例名)与 H2(作为段落),出现 H5 及更深层级会直接报错;缩进代码块(非围栏)也会报错;
- **Cursor Position 是唯一硬性必填的段落**:解析结束时若 `cursor_path` 或 `cursor_position` 为空,`anyhow::bail!("Missing cursor position codeblock")`([example_spec.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction/src/example_spec.rs?utm_source=gitcode_repo_files#L410-L412));
- Edit History 可以为空,`to_markdown` 序列化空历史时会输出 `(No edit history)` 占位文本。
`Example` 结构([example.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/example.rs?utm_source=gitcode_repo_files#L21-L54))在 `ExampleSpec` 之上还挂载了运行期产物:`prompt_inputs`(光标节选、光标偏移、关联文件等 Zeta prompt 输入)、`prompt`(格式化后的 prompt 与期望输出)、`predictions`(模型实际预测)、`score`(与期望补丁的匹配分数)以及 `qa` 结果。也就是说,`.md` 样例只是"输入规格",跑完预测与打分后整个 `Example` 可序列化为 JSON 留档复现。
## 4. 样例如何被消费:从格式化到打分
评测流程由 `edit_prediction_cli`(CLI 命令名 `ep`,见 [main.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/main.rs?utm_source=gitcode_repo_files#L71-L72))串联,与本样例直接相关的环节有两个:
**(1)格式化 prompt。** [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) 不存在——正确路径是 [crates/edit_prediction_cli/src/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` 把 `ExampleSpec` 渲染为教师模型提示词:编辑历史最多保留最后 128 行(`MAX_HISTORY_LINES`),光标节选被 `<|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#L143-L152)),关联文件上下文按 1024 token 预算截断。教师模型的响应再由 `TeacherPrompt::parse` 还原为统一 diff 与实际光标位置。
**(2)预测与打分。** [score.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/score.rs?utm_source=gitcode_repo_files#L24-L51) 的 `run_scoring` 先执行预测(可复用 JSON 中已存的 `predictions`),随后在后台任务中调用 `expected_patches_with_cursor_positions()` 取出所有期望补丁,经 `edit_prediction_metrics::prepare_expected_patches` 归一化后逐条调用 `score_prediction` 比对。因此本样例中"两个候选补丁"的语义就是:模型预测的补丁与光标位置只要与其中任意一个匹配,即被视为正确。评测的上下文检索量默认受 `EVAL_RELATED_CONTEXT_TOKENS_LIMIT = 4000`([score.rs](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/crates/edit_prediction_cli/src/score.rs?utm_source=gitcode_repo_files#L22))约束。
## 5. 如何运行这些单元测试
仓库提供了现成的 CI 入口 [script/run-unit-evals](https://gitcode.com/GitHub_Trending/ze/zed/blob/b1a7ef0cf66dfbf9d7661170c96d97c7df916c68/script/run-unit-evals?utm_source=gitcode_repo_files),其核心就是一行 nextest 调用:
```bash
GPUI_TEST_TIMEOUT=1500 cargo nextest run --workspace --no-fail-fast \
--features unit-eval --no-capture -E 'test(::eval_)'
即:开启 unit-eval feature、用 ::eval_ 过滤器只跑评测相关测试、不捕获输出以便观察进度,并把测试超时放宽到 1500 秒。脚本还支持 UNIT_EVAL_COMMIT 环境变量,先 git fetch 并切换到指定提交再跑,用于对历史版本做回归评测。
若要针对单个样例做更细粒度的分析,可直接使用 ep CLI。main.rs 定义了全局参数:--name 按样例名过滤、--repo 按仓库过滤、--limit/--offset 控制处理数量、--max-parallelism(默认 10)、--output/-o 指定输出、--markdown 把输出写成每样例一个 .md 文件、--failfast 遇错即停、--in-place 原地更新样例文件(把预测与分数写回)。输入文件即本样例所在目录下的 .md/.json/.jsonl 路径。
6. 编写合规样例的要点清单
综合以上解析逻辑,手写一个评测样例时需满足:
- 以
+++包裹的 TOML 前置块开头,repository_url与revision必填; ## Cursor Position必须存在:围栏 info string 写光标文件路径,围栏内是包含光标行的节选文本,下一行用语言注释加^[CURSOR_POSITION](或行首列场景的<[CURSOR_POSITION])标注光标列;## Edit History用统一 diff 描述用户此前的编辑;若其中某段来自被接受的预测,前缀一行// User accepted prediction:;## Expected Patch可写一个或多个 diff,光标用同样的标记注释行标在+新增行上;多个 diff 表示多个可接受答案;- 只使用 H1/H2 标题,代码块一律用围栏形式;
- 文件名遵循
仓库名--行为描述命名(如flask--add-import-statement),样例名缺省即取文件名。
掌握这套格式后,无论是要读懂 Zed 编辑预测评测如何复现真实编辑场景,还是要为新场景补充单元测试样例、定位某次预测质量回退,都可以直接以 evals/ 目录中的这些 Markdown 文件为蓝本进行扩展。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00