Zed 编辑预测评测体系剖析:tuple-to-struct-literal 评测样例与 Edit Prediction 数据格式
Zed 的编辑预测(Zeta/edit prediction)功能依赖一套"评测样例"体系来持续验证模型在真实代码重构场景下的表现。本文以仓库中的评测样例 tree-sitter--tuple-to-struct-literal.md 为主线,完整解读这类样例的三段式结构(编辑历史、光标位置、期望补丁)、Rust 重构的具体技术细节,以及该样例如何被 edit_prediction_cli 工具链读取、加载、预测与打分。读完后,你可以独立看懂、编写并运行这类评测样例。
评测样例在 Zed 工程中的位置
该文件位于 crates/edit_prediction_cli/evals/ 目录,与多个同构样例并列存放,例如:
- tree-sitter--tuple-to-struct-definition.md
- tree-sitter--tuple-to-struct-destructuring.md
- tree-sitter--tuple-to-struct-for-loop.md
- flask--add-test-function.md、vscode--add-async-and-await.md 等。
文件名遵循 仓库名--重构场景描述 的命名约定(双横线分隔),与解析逻辑一致:example.rs 中的 read_example_files 会按扩展名分发——.md 走 parse_markdown_example(即 ExampleSpec::from_markdown),.json 反序列化为单个 Example,.jsonl 按行反序列化,且当 ExampleSpec 的 name 为空时默认以文件名为样例名。
驱动这些样例的工具是名为 ep 的命令行程序,其子命令定义在 main.rs:read(读取样例)、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_url、revision 等 |
指定样例所基于的外部仓库及精确提交 |
## 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_markdown,example_spec.rs)用 pulldown-cmark 遍历事件:## Cursor Position 代码块围栏后的 info string 就是文件路径(如 ```tree-sitter/crates/loader/src/loader.rs),块内文本是光标摘录;## Expected Patch 下的每个 diff 代码块都追加为一个期望补丁;缺少光标位置代码块会直接报错 "Missing cursor position codeblock"。
光标列的标注格式由 example_spec.rs 的 cursor_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.rs 的 repo_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.rs 的 INPUTS_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_url 与 revision 必填(解析时 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_prediction 的 ExampleSpec 定义、由 edit_prediction_cli 的 ep 工具链消费,配合 unit-eval feature 的集成测试形成闭环。理解并遵循这一格式,就能为编辑预测模型贡献可复现、可打分的回归用例。
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 StartedRust0624
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