首页
/ Zed 编辑预测评测体系剖析:tuple-to-struct-literal 评测样例与 Edit Prediction 数据格式

Zed 编辑预测评测体系剖析:tuple-to-struct-literal 评测样例与 Edit Prediction 数据格式

2026-09-04 10:29:14作者:裴麒琰

Zed 的编辑预测(Zeta/edit prediction)功能依赖一套"评测样例"体系来持续验证模型在真实代码重构场景下的表现。本文以仓库中的评测样例 tree-sitter--tuple-to-struct-literal.md 为主线,完整解读这类样例的三段式结构(编辑历史、光标位置、期望补丁)、Rust 重构的具体技术细节,以及该样例如何被 edit_prediction_cli 工具链读取、加载、预测与打分。读完后,你可以独立看懂、编写并运行这类评测样例。

评测样例在 Zed 工程中的位置

该文件位于 crates/edit_prediction_cli/evals/ 目录,与多个同构样例并列存放,例如:

文件名遵循 仓库名--重构场景描述 的命名约定(双横线分隔),与解析逻辑一致:example.rs 中的 read_example_files 会按扩展名分发——.mdparse_markdown_example(即 ExampleSpec::from_markdown),.json 反序列化为单个 Example.jsonl 按行反序列化,且当 ExampleSpecname 为空时默认以文件名为样例名。

驱动这些样例的工具是名为 ep 的命令行程序,其子命令定义在 main.rsread(读取样例)、load-project(为每个样例创建 git worktree 并加载文件)、context(检索上下文)、format-prompt(生成模型提示词)、predict(执行编辑预测)、parse-output(把模型输出解析为统一 diff)、score / eval(按实际补丁与期望补丁计算得分)等。仓库还提供入口脚本 script/run-unit-evals,其核心是:

GPUI_TEST_TIMEOUT=1500 cargo nextest run --workspace --no-fail-fast \
  --features unit-eval --no-capture -E 'test(::eval_)'

即通过 unit-eval feature 编译并以 ::eval_ 前缀筛选测试用例,把评测样例跑成集成测试。

样例文件的通用格式规范

example_spec.rs 中定义的分节标题常量可以看出,一个 Markdown 评测样例由以下可选分节构成(标题匹配不区分大小写):

分节标题 内容 对应字段
front matter(+++ ... +++ 包裹的 TOML) repository_urlrevision 指定样例所基于的外部仓库及精确提交
## Uncommitted Diff(可选) 光标时刻的未提交改动 uncommitted_diff
## Edit History 之前若干次编辑构成的 diff 序列 edit_history
## Cursor Position 光标所在文件的摘录片段,用标记注释标出光标列 cursor_path + cursor_position
## Expected Patch 一个或多个期望的统一 diff,多个即代表多个可接受答案 expected_patches
## Rejected Patch(可选) 被拒绝的预测,用于 DPO 训练 rejected_patch

解析实现(from_markdownexample_spec.rs)用 pulldown-cmark 遍历事件:## Cursor Position 代码块围栏后的 info string 就是文件路径(如 ```tree-sitter/crates/loader/src/loader.rs),块内文本是光标摘录;## Expected Patch 下的每个 diff 代码块都追加为一个期望补丁;缺少光标位置代码块会直接报错 "Missing cursor position codeblock"。

光标列的标注格式由 example_spec.rscursor_excerpt 文档注释规定:光标所在行的下一行写一条注释,包含 [CURSOR_POSITION] 字符串,注释中用一个箭头指明列号——^ 表示光标列就是 ^ 字符所在位置,< 表示光标位于该行第一个非空白字符处。此外还支持行内标记 <|user_cursor|>INLINE_CURSOR_MARKER),直接嵌在代码中,常见于期望补丁的 added 行里,用来同时表达"预测出的代码"与"预测后光标应落在何处"。

本样例详解:从元组到结构体字面量的重构

front matter:锁定外部仓库与提交

+++
repository_url = "git@github.com:tree-sitter/tree-sitter"
revision = "24007727d42b4caceda3095ac685c463fae1ba1a"
+++

样例并不针对 Zed 自身代码,而是引用 tree-sitter 仓库在指定 revision 处的状态。ep load-project 阶段会按 example.rsrepo_name / worktree_path 逻辑解析出 owner/repo(支持 git@github.com:owner/repo.git 与 http 两种 URL 形式),并在 worktree 目录中 checkout 到该 revision,从而让编辑历史与光标摘录中的代码路径可被真实解析。

Edit History:此前已完成的编辑

编辑历史是喂给模型的核心"上下文信号",本样例记录了在 tree-sitter/crates/loader/src/loader.rs 中把匿名元组类型逐步重构为具名结构体 LanguageEntry 的过程:

--- a/tree-sitter/crates/loader/src/loader.rs
+++ b/tree-sitter/crates/loader/src/loader.rs
@@ -604,7 +604,7 @@

 pub struct Loader {
     pub parser_lib_path: PathBuf,
-    languages_by_id: Vec<(PathBuf, OnceCell<Language>, Option<Vec<PathBuf>>)>,
+    languages_by_id: Vec<LanguageEntry>,
     language_configurations: Vec<LanguageConfiguration<'static>>,
     language_configuration_ids_by_file_type: HashMap<String, Vec<usize>>,
     language_configuration_in_current_path: Option<usize>,
@@ -619,6 +619,12 @@

     #[cfg(feature = "wasm")]
     wasm_store: Mutex<Option<tree_sitter::WasmStore>>,
+}
+
+struct LanguageEntry {
+    path: PathBuf,
+    language: OnceCell<Language>,
+    external_files: Option<Vec<PathBuf>>,
 }

 pub struct CompileConfig<'a> {
@@ -767,7 +773,7 @@
     pub fn get_all_language_configurations(&self) -> Vec<(&LanguageConfiguration, &Path)> {
         self.language_configurations
             .iter()
-            .map(|c| (c, self.languages_by_id[c.language_id].0.as_ref()))
+            .map(|c| (c, self.languages_by_id[c.language_id].path.as_ref()))
             .collect()
     }

@@ -920,13 +926,17 @@
     }

     fn language_for_id(&self, id: usize) -> LoaderResult<Language> {
-        let (path, language, externals) = &self.languages_by_id[id];
+        let LanguageEntry {
+            path,
+            language,
+            external_files,
+        } = &self.languages_by_id[id];
         language
             .get_or_try_init(|| {
                 let src_path = path.join("src");
                 self.load_language_at_path(CompileConfig::new(
                     &src_path,
-                    externals.as_deref(),
+                    external_files.as_deref(),
                     None,
                 ))
             })
@@ -1532,10 +1542,9 @@
                     // Determine if a previous language configuration in this package.json file
                     // already uses the same language.
                     let mut language_id = None;
-                    for (id, (path, _, _)) in
-                        self.languages_by_id.iter().enumerate().skip(language_count)
+                    for (id, entry) in self.languages_by_id.iter().enumerate().skip(language_count)
                     {
-                        if language_path == *path {
+                        if language_path == entry.path {
                             language_id = Some(id);
                         }
                     }
--- a/tree-sitter/crates/loader/src/loader.rs
+++ b/tree-sitter/crates/loader/src/loader.rs
@@ -1553,10 +1553,10 @@
                     let language_id = if let Some(language_id) = language_id {
                         language_id
                     } else {
-                        self.languages_by_id.push((
-                            language_path,
-                            OnceCell::new(),
-                            grammar
+                        self.languages_by_id.push(LanguageEntry {
+                            path: language_path,
+                            language: OnceCell::new(),
+                            external_files: grammar
                                 .external_files
                                 .clone()
                                 .into_vec()

这段历史覆盖了五处改动点:字段类型替换、新结构体定义、按位置索引 .0 的访问改为命名字段访问、解构从元组模式改为结构体模式、循环中的元组迭代改为按 entry 字段访问,最后一段则是把 push((...)) 元组构造改写为 push(LanguageEntry { ... }) 字面量——注意最后一个 diff 块在 into_vec() 处被截断,说明用户此刻正写到一半。

Cursor Position:光标正在编辑的结构体字面量

                    let language_id = if let Some(language_id) = language_id {
                        language_id
                    } else {
                        self.languages_by_id.push(LanguageEntry {
                            path: language_path,
                            language: OnceCell::new(),
                            external_files: grammar
                                .external_files
                                .clone()
                                .into_vec()
                                .map(|files| {
                                    files
                                        .into_iter()
                                        .map(|path| {
                                            let path = parser_path.join(path);
                                            // prevent p being above/outside of parser_path
                                            if path.starts_with(parser_path) {
                                                Ok(path)
                                            } else {
                                                Err(LoaderError::ExternalFile(
                                                    path.to_string_lossy().to_string(),
                                                    parser_path.to_string_lossy().to_string(),
                                                ))
                                            }
                                        })
                                        .collect::<LoaderResult<Vec<_>>>()
                                })
                                .transpose()?,
                                //            ^[CURSOR_POSITION]
                        ));
                        self.languages_by_id.len() - 1
                    };

按前述格式规范解读:围栏 info string 给出文件路径 tree-sitter/crates/loader/src/loader.rs;标记行 // ^[CURSOR_POSITION] 中的 ^ 指向上方 // 前第 12 列处,即 transpose()?, 之后、)); 之前的位置——用户刚写完字段值的 .transpose()? 表达式,正准备敲结构体字面量的收尾括号。

Expected Patch:一行括号修复

--- a/tree-sitter/crates/loader/src/loader.rs
+++ b/tree-sitter/crates/loader/src/loader.rs
@@ -1578,7 +1578,7 @@
                                         .collect::<LoaderResult<Vec<_>>>()
                                 })
                                 .transpose()?,
-                        ));
+                        });
                         self.languages_by_id.len() - 1
                     };

期望补丁只有一行:把收尾的 )); 改成 });。这正是编辑历史所引出的"陷阱"——上一段历史编辑把 self.languages_by_id.push(( 元组构造改成了 self.languages_by_id.push(LanguageEntry { 结构体字面量构造,但用户尚未同步修改与之配对的右括号。模型必须从编辑历史中理解"元组构造 → 结构体字面量构造"这一语义转变,而不是机械地补一个右圆括号。这个用例考察的正是编辑预测模型对跨 hunk 配对语法(开括号与闭括号类型一致)的敏感度,也是"tuple-to-struct"系列用例(definition / destructuring / field-access / for-loop / literal)中"字面量构造"这一环。

运行与扩展这个评测样例

运行方式上,ep 的输入参数支持本地 .md 文件路径(也支持 - 表示 stdin 的 JSONL,以及 captured-after:rejected-after: 等远端数据源标记,说明见 main.rsINPUTS_HELP)。一条典型的评测链路为:

# 1. 读取并规范化样例(md -> jsonl)
ep read crates/edit_prediction_cli/evals/tree-sitter--tuple-to-struct-literal.md -o out.jsonl

# 2. 按 repository_url/revision 建立 git worktree 并载入光标文件
ep load-project out.jsonl

# 3. 采集上下文(默认 LSP 类型,可用 --type= 指定)
ep context out.jsonl

# 4. 生成提示词 -> 预测 -> 解析输出 -> 打分
ep format-prompt out.jsonl
ep predict out.jsonl --provider=<provider>
ep parse-output out.jsonl
ep score out.jsonl

全局参数还包括 --limit--offset--name--repo(按样例名或仓库过滤)、--markdown(把结果按每样例一个 .md 文件写回)等(见 main.rs)。

若要以集成测试形式跑全量评测,则使用 script/run-unit-evals:设置 UNIT_EVAL_COMMIT 可先固定到指定提交,然后以 --features unit-eval 编译并用 nextest 过滤 ::eval_ 前缀的测试。

若要为这条流水线新增用例,按 example_spec.rs 的解析约束编写即可:front matter 中 repository_urlrevision 必填(解析时 FrontMatter 要求这两个字段);## Cursor Position 代码块必须存在且围栏 info string 写文件路径;## Expected Patch 下可放多个 diff 块表示多个可接受答案;若期望预测后光标移动,可在 added 行内嵌 <|user_cursor|> 标记,序列化时通过 encode_cursor_in_patch 编入补丁、extract_cursor_from_patch 取出(example_spec.rs)。

小结

tuple-to-struct-literal 样例 是 Zed 编辑预测评测体系的一个缩影:front matter 把样例锚定到外部仓库的精确 revision,Edit History 提供"用户正在做什么重构"的语义线索,Cursor Position 用标准化的 ^[CURSOR_POSITION] / <|user_cursor|> 标记精确描述编辑点,Expected Patch 给出唯一的正确修复());});)。整套格式由 edit_predictionExampleSpec 定义、由 edit_prediction_cliep 工具链消费,配合 unit-eval feature 的集成测试形成闭环。理解并遵循这一格式,就能为编辑预测模型贡献可复现、可打分的回归用例。

登录后查看全文
热门项目推荐
相关项目推荐