oh-my-pi 编辑基准:结构类变更任务提示词模板(structural-task.md)解析与实战
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
导读
packages/typescript-edit-benchmark是 oh-my-pi 项目内用于评测 AI 编码 Agent 编辑精度的基准套件,其核心思路是:对真实 TypeScript/JavaScript 源码注入微小的"人为 bug",再让模型修复,从而检验模型在困难上下文中的外科手术式打补丁能力。本篇文章聚焦该套件三套提示词模板中语义最特殊的一套——structural-task.md。它把结构类变更(交换、移动、删除、补标签、去包装)渲染成自然语言指令,而不是机械的 before/after 补丁块。读完本文,你将掌握这套模板的完整语法、七类结构变更各自的指令语义、生成器中"唯一解"锚点校验的底层实现,以及如何把它接入新的结构类变更。
一、模板在基准套件中的定位
1.1 测的是编辑精度,不是找 bug 能力
generate.ts 开篇注释写得很直白:The goal is testing edit precision, not bug-finding ability.注入的 mutation 可以是微不足道的,关键是模型能否在困难上下文里精准打上补丁,这些困难上下文包括:
- 重复行:目标行在文件中多次出现;
- 长文件:300 行以上且改动位于文件中部;
- 相似块:多个结构相似的函数并存;
- 密集代码:极少空白,上下文更难读;
- 深层嵌套:高缩进层级上的空白敏感编辑。
1.2 三套提示词模板的分工
generate.ts 同时导入三套模板:
| 模板 | 适用 mutation | 指令形态 |
|---|---|---|
| mutation-task.md | 运算符、字面量、正则、复合多编辑等 | 显式 before/after 代码块(Delete this block / Replace this with) |
| identifier-task.md | identifier-multi-edit | "把拼错的标识符全部替换为正确拼写"的重命名指令 |
| structural-task.md | 7 类结构变更 | 自然语言指令 + after-state 期望代码块 |
三者有一个共同约束:每条 prompt 都必须唯一确定一个字节级精确的答案(before/after 块会通过重新求解验证,见 generate.ts)。
1.3 结构类 mutation 清单
结构类变更由 generate.ts 中的STRUCTURAL_KINDS映射到模板的kind参数:
const STRUCTURAL_KINDS: Record<string, StructuralKind> = { "remove-case-label": "case-label", "duplicate-block": "duplicate-block", "move-distant-block": "move-block", "wrap-redundant-if": "wrap-if", "swap-sibling-blocks": "swap-blocks", "swap-adjacent-lines": "swap-lines", "swap-if-else": "swap-if-else", };对应的 7 个StructuralKind是case-label、duplicate-block、move-block、wrap-if、swap-blocks、swap-lines、swap-if-else,它们的底层实现位于 mutations.ts 的ALL_MUTATIONS中。
二、模板骨架:标题、条件分支与期望结果
structural-task.md 是一个 Handlebars 模板({{变量}}/{{#when}}/{{#each}}),整体由三部分构成:
2.1 任务标题与分支选择
标题固定为# Fix a bug in {{filename}},filename是path.basename(filePath)(generate.ts)。随后用 7 个{{#when kind "==" "xxx"}}...{{/when}}条件分支渲染不同的指令文案——kind就是上文StructuralKind,运行时只会命中一个分支。
2.2 期望结果区
指令之后是期望结果区(structural-task.md):
After the fix, the affected {{#when hunkCount ">" 1}}regions must{{else}}region must{{/when}} read exactly: {{#each hunks}} {{#if startLine}} Around line {{startLine}}: {{/if}} {{../fence}}{{../language}} {{newCode}} {{../fence}} {{/each}}这里的关键在于:结构类任务不展示被删掉的旧代码,只展示修复后应达到的代码状态(newCode),并且总是携带行号锚点startLine。生成器中的注释解释了原因(generate.ts):After-state regions are only interpretable with a position——期望区域只有在给出行号时才可定位、可解释。
fence和language由 generate.ts 计算:LANGUAGE_BY_EXTENSION[ext]根据.js/.ts/.tsx等扩展名给出语言标签,pickFence(hunks)则根据 hunk 内容自动挑选不会冲突的代码围栏字符。
2.3 收尾约束
Make exactly this change; do not modify anything else.这是所有提示词的统一铁律,配合 2.2 的 after-state 代码块,把"做什么、做到什么程度"完全钉死。
三、逐类拆解:七种结构变更指令的语义
3.1 case-label:补回 switch fall-through 标签
In this file's `switch`, the value in `{{label}}` must be handled exactly like the case after it: add a fall-through `{{label}}` label directly before `{{before}}`.场景:case A: case B:这种 fall-through 对中,RemoveCaseLabelMutation(mutations.ts)把前一个无consequent的标签删掉了,修复就是原样补回。这是一个纯插入任务——插入的内容完全由可见的兄弟标签决定,不涉及任何被隐藏的代码,因此指令可以做到唯一确定。
生成器在structuralPromptData的case-label分支中校验(generate.ts):
- 期望结果必须恰好新增 1 行(
added.length !== 1则拒绝); label必须以case开头,紧随其后的锚点行before必须以case或default开头;- 锚点行在整个文件中必须只出现一次(
countTrimmedLine(inputLines, before) !== 1则拒绝)。
3.2 duplicate-block:删除复制粘贴的重复块
The block starting with `{{head}}` appears twice in a row — the second copy is a copy-paste accident. Delete the second copy and keep the first.场景:DuplicateBlockMutation(mutations.ts)把一段多行语句原样复制一份接到自己后面,模拟复制粘贴事故。它刻意只选择if / for / for-of / while / try / switch / 表达式语句这类重复后仍能通过语法解析的语句(mutations.ts),并拒绝会产生重复声明语法错误的场景;复制后还会再次parseCode验证。
这是纯删除任务:幸存的第一份副本完全可见,任务确定性由此而来。校验时要求期望结果区删除内容非空、新增内容为空,且head锚点在文件中恰好出现 2 次(正是"两份相同块"的特征,generate.ts)。
3.3 move-block:还原被移错位置的块
The block starting with `{{head}}` was moved to the wrong place — it currently sits after `{{currentPrev}}`. Move it back so it comes directly before `{{destination}}`.场景:MoveDistantBlockMutation(mutations.ts)把一个多行语句剪切后拼接到同一语句列表中相隔至少 3 个位置的目标语句之后,对应真实编辑中占比最高的两种 hunk 形态:一个删除 hunk + 一个插入 hunk,且移动的内容完全可见。
这是模板中最复杂的指令,需要 3 个锚点:head(被移动块的首行)、currentPrev(当前所处位置的前一行)、destination(应到达位置的后一行)。校验逻辑(generate.ts):
- 期望结果区必须恰好有 2 个 core(纯插入 + 纯删除);
- 删除的旧内容与插入的新内容必须逐行一致(说明确实是"同一块"被搬走);
- 3 个锚点行都必须包含标识符(
isAnchorLine),且各自在文件中只出现一次。
3.4 wrap-if:移除冗余的调试包装
A leftover debugging wrapper is redundant: remove the `if (true) {` on line {{wrapperLine}} together with its closing brace, and dedent the wrapped body one level.场景:WrapRedundantIfMutation(mutations.ts)把一个 4~160 行的块体包进if (true) { ... }。它刻意避免包装本身就是if (true)的块体(防止重复 mutation 噪声),修复就是删除包装并整体退一格缩进。
这是缩进平移类多行编辑:整个被包住的内容在 buggy 文件中完全可见(无隐藏代码重建),难点在于行号wrapperLine的定位与缩进重排。structuralPromptData中通过查找旧内容里if (true) {的位置换算出行号(generate.ts)。
3.5 swap-blocks / swap-lines:相邻块与相邻语句交换
Two adjacent blocks are in the wrong order: the block starting with `{{secondHead}}` belongs before the block starting with `{{firstHead}}`. Swap the two blocks.Two adjacent statements are in the wrong order: `{{secondHead}}` belongs before `{{firstHead}}`. Swap the two statements.两条指令几乎同构,区别只在规模:
swap-blocks对应SwapSiblingBlocksMutation(mutations.ts):相邻的多行兄弟语句(函数、if 链、循环),至少一个行跨度 ≥ 3,且两边总跨度 ≤ 120 行;swap-lines对应SwapAdjacentLinesMutation(mutations.ts):相邻的单行语句,间距 ≤ 2 行。
两者都用"{{secondHead}}应排在{{firstHead}}之前"的语序来表达交换。渲染时依赖一个微妙的 jsdiff 行为:A,B -> B,A会被渲染成"在 B 前插入 A + 在 A 后删除 A",structuralPromptData会先尝试mergeNearbyPlacements合并成单个 core,否则按插入/删除成对匹配来提取两个 head(generate.ts)。
3.6 swap-if-else:交换 if/else 分支体
The branch bodies of `{{condition}}` are swapped: the current `else` body belongs under the `if`, and vice versa. Swap the two branch bodies.场景:SwapIfElseBranchesMutation(mutations.ts)交换if的 consequent 与 alternate 两个块体。它对候选有严格限制:两侧都必须是块语句、都非空、且每侧不超过 5 条语句,保证交换后提示词依然可读。
锚点condition取自期望 hunk 之前的最近可见行,必须形如if (...)且在文件中唯一(generate.ts)。
四、锚点校验:指令"唯一解"是如何保证的
结构类指令全部是自然语言,没有 before/after 旧代码兜底,所以生成器必须保证指令指代的对象唯一。核心函数是 structuralPromptData(generate.ts 的注释说明了设计意图):
Every referenced anchor line is validated to occur exactly the expected number of times, so the instruction identifies a unique edit; returns null when the instruction would be ambiguous.
落到具体手段上有四点:
- 锚点行计数:所有指令中引用的锚点(
before、head、currentPrev、destination、condition、firstHead、secondHead)都通过countTrimmedLine统计全文件出现次数,必须等于预期值(通常是 1,duplicate-block 的head是 2); - 锚点可读性:
isAnchorLine要求锚点包含至少 2 个字母/下划线/美元符,排除);、};这类纯标点行(generate.ts); - 成对匹配:move-block、swap 类任务通过"删除的旧内容 === 插入的新内容"验证确实是同一块在移动,而非内容不同的偶然巧合;
- 拒绝而非降级:任何歧义候选直接
return null,由调用方重新尝试别的候选(buildPrompt的注释明确:never downgraded to a raw before/after patch,generate.ts)。
五、期望结果区:diff → hunk → 重新求解验证
结构类模板的期望结果区数据来自 hunks.ts 提供的一系列工具,调用链位于 buildPrompt:
placementsFromDiff(input, expected):用 jsdiff 对"注入 bug 后的文件"和"原始文件"做行级 diff,得到变更位置集合(placement);renderHunks(inputLines, placements):把 placement 渲染成带上下文与唯一性信息的 hunk;solveRenderedHunks(inputLines, hunks):反向验证——只凭渲染出的 hunk 内容重新求解,如果求解结果与原文件不一致,说明 prompt 有歧义,return null触发重试;- 渲染时 hunk 只保留
startLine和newCode两个字段交给模板(generate.ts)。
这里有一个与 mutation-task.md 的显著差异值得注意:在 mutation-task 模板中,startLine只在hunk.unique || difficulty === "easy"时才给出(generate.ts),即中等难度以上要求模型自行定位;而 structural-task无条件给出startLine——因为结构类指令的期望代码块只有配合行号才可解释(After-state regions are only interpretable with a position)。定位难度由指令本身承载,而非行号隐去。
六、与底层 mutation 的对应及规模控制
6.1 mutation 与模板 kind 的映射
每个kind都对应 mutations.ts 中一个类别为structural的 mutation:
| kind | mutation | 编辑形态 | multiHunk |
|---|---|---|---|
| case-label | RemoveCaseLabelMutation | 纯插入单行 | — |
| duplicate-block | DuplicateBlockMutation | 大块删除 | — |
| move-block | MoveDistantBlockMutation | 删除 + 插入两 hunk | ✔ |
| wrap-if | WrapRedundantIfMutation | 多行缩进平移 | — |
| swap-blocks | SwapSiblingBlocksMutation | 大块连续替换 | — |
| swap-lines | SwapAdjacentLinesMutation | 单行交换 | — |
| swap-if-else | SwapIfElseBranchesMutation | 分支体交换 | — |
其中move-distant-block显式声明multiHunk = true,因为"一个删除 hunk + 一个插入 hunk"正是真实编辑中占主导的两种 hunk 形态(mutations.ts 注释)。generateCase里对非结构、非多 hunk 的 mutation 强制要求changedHunks === 1 && positionalHunks === 1 && changedLines ≤ 30(generate.ts),而结构类任务不受此限制,以容纳大块交换、移动与删除。
6.2 规模分布:贴合真实编辑形态
MUTATION_PLANS 为每个 mutation 设定用例数量与变更行数目标,校准依据来自真实 Agent 会话的 per-tool-call 编辑规模分布(1 行约 23%、2-5 行约 30%、6-20 行约 29%、21-60 行约 14%、61+ 行约 5%)。块级结构类 mutation(wrap-redundant-if、swap-sibling-blocks、duplicate-block、move-distant-block)通过sizes: BLOCK_SIZES(generate.ts)在[6,20]、[21,60]、[61,150]三个区间循环取值,精确复刻真实编辑的大块分布。
6.3 运行生成
package.json 中的generate脚本:
bun run src/generate.ts --typescript-dir /tmp/pi-mono-source支持--typescript-dir(源码目录,缺省时浅克隆 pi-mono 并扫描packages/)、--output(默认fixtures.tar.gz)、--count-scale、--seed(默认 42)、--categories、--difficulty(默认easy,medium,hard,nightmare)、--min-score、--dry-run等参数(generate.ts)。生成的每个用例目录包含prompt.md、input/、expected/、metadata.json,由 tasks.ts 的loadTasksFromDir加载,或从fixtures.tar.gz解包加载。
七、质量保障:验证器与重试机制
7.1 字节级验证与格式化等价
verify.ts 的verifyExpectedFileSubset对 Agent 输出与期望 fixture 做三层比较:
- 格式化等价:双方都经 Prettier 格式化后比较,代码文件额外忽略空行数量差异(避免"完美的编辑但残留一条缝空白"被判失败),markdown/yaml 等空白敏感格式保持严格相等(verify.ts、verify.ts);
- indentScore 缩进距离:统计 Agent 原始输出与格式化输出之间的缩进差(
computeIndentDistanceForDiff),衡量格式化器替 Agent 修正了多少缩进; - diff 报告:失败时生成带 3 行上下文的紧凑 diff 与行数/字符数统计,便于复盘。
7.2 生成期的重试闭环
generateCase(generate.ts)对每个用例最多尝试 100 次:mutation 无法应用、prompt 构建返回 null、行号与既有用例冲突(regionAvailable,generate.ts)、难度分数不足(easy 0 / medium 2 / hard 5 / nightmare 8)都会触发重试。模板渲染结果超过 20,000 字符同样被拒绝(generate.ts),保证提示词在上下文窗口内可控。nightmare 难度还会额外要求目标行必须是全文件重复出现的行(generate.ts),制造"近重复代码多处存在、只准改指定块"的极端场景。
八、如何扩展新的结构类变更
若要在基准套件中加入新的结构类任务,需要改动四处(仓库当前为只读,以下仅说明设计思路):
- mutation 实现:在 mutations.ts 新增一个
category === "structural"的Mutation,必要时声明multiHunk,并注册进ALL_MUTATIONS与CATEGORY_MAP(mutations.ts); - kind 映射:在 generate.ts 的
STRUCTURAL_KINDS中加入新的StructuralKind; - 锚点校验:在
structuralPromptData中为新 kind 编写case分支,遵循"每个锚点行唯一、可读、成对匹配"的校验纪律,任何歧义返回 null; - 模板文案:在 structural-task.md 中为新 kind 增加
{{#when kind "==" "xxx"}}分支,并确保期望结果区沿用"around line {{startLine}} + 代码围栏 + newCode"的既有结构。
结语
structural-task.md 虽然只有 37 行,却是整个编辑基准中最讲究"提示词可解性"的一环:它以自然语言承载结构变更指令,靠锚点计数、成对 hunk 匹配、反向重新求解三重校验保证每个 prompt 都收敛到唯一字节级答案;又通过 multiHunk 声明与规模分布设计,精确复刻真实 Agent 会话中的编辑形态。对任何想为编码 Agent 构建高质量编辑基准的开发者来说,这套"指令语义 + 锚点验证 + after-state 期望区 + 重试闭环"的组合都极具参考价值。若要深入阅读,可继续查看 hunks.ts(diff 到 hunk 的渲染管线)、tasks.ts(fixture 加载与校验)与 verify.ts(输出验证器)。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考