oh-my-pi 编辑基准:结构类变更任务提示词模板(structural-task.md)解析与实战
2026/9/12 3:19:18 网站建设 项目流程

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.mdidentifier-multi-edit"把拼错的标识符全部替换为正确拼写"的重命名指令
structural-task.md7 类结构变更自然语言指令 + 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 个StructuralKindcase-labelduplicate-blockmove-blockwrap-ifswap-blocksswap-linesswap-if-else,它们的底层实现位于 mutations.ts 的ALL_MUTATIONS中。

二、模板骨架:标题、条件分支与期望结果

structural-task.md 是一个 Handlebars 模板({{变量}}/{{#when}}/{{#each}}),整体由三部分构成:

2.1 任务标题与分支选择

标题固定为# Fix a bug in {{filename}}filenamepath.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——期望区域只有在给出行号时才可定位、可解释。

fencelanguage由 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的标签删掉了,修复就是原样补回。这是一个纯插入任务——插入的内容完全由可见的兄弟标签决定,不涉及任何被隐藏的代码,因此指令可以做到唯一确定。

生成器在structuralPromptDatacase-label分支中校验(generate.ts):

  • 期望结果必须恰好新增 1 行(added.length !== 1则拒绝);
  • label必须以case开头,紧随其后的锚点行before必须以casedefault开头;
  • 锚点行在整个文件中必须只出现一次(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.

落到具体手段上有四点:

  1. 锚点行计数:所有指令中引用的锚点(beforeheadcurrentPrevdestinationconditionfirstHeadsecondHead)都通过countTrimmedLine统计全文件出现次数,必须等于预期值(通常是 1,duplicate-block 的head是 2);
  2. 锚点可读性isAnchorLine要求锚点包含至少 2 个字母/下划线/美元符,排除);};这类纯标点行(generate.ts);
  3. 成对匹配:move-block、swap 类任务通过"删除的旧内容 === 插入的新内容"验证确实是同一块在移动,而非内容不同的偶然巧合;
  4. 拒绝而非降级:任何歧义候选直接return null,由调用方重新尝试别的候选(buildPrompt的注释明确:never downgraded to a raw before/after patch,generate.ts)。

五、期望结果区:diff → hunk → 重新求解验证

结构类模板的期望结果区数据来自 hunks.ts 提供的一系列工具,调用链位于 buildPrompt:

  1. placementsFromDiff(input, expected):用 jsdiff 对"注入 bug 后的文件"和"原始文件"做行级 diff,得到变更位置集合(placement);
  2. renderHunks(inputLines, placements):把 placement 渲染成带上下文与唯一性信息的 hunk;
  3. solveRenderedHunks(inputLines, hunks)反向验证——只凭渲染出的 hunk 内容重新求解,如果求解结果与原文件不一致,说明 prompt 有歧义,return null触发重试;
  4. 渲染时 hunk 只保留startLinenewCode两个字段交给模板(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:

kindmutation编辑形态multiHunk
case-labelRemoveCaseLabelMutation纯插入单行
duplicate-blockDuplicateBlockMutation大块删除
move-blockMoveDistantBlockMutation删除 + 插入两 hunk
wrap-ifWrapRedundantIfMutation多行缩进平移
swap-blocksSwapSiblingBlocksMutation大块连续替换
swap-linesSwapAdjacentLinesMutation单行交换
swap-if-elseSwapIfElseBranchesMutation分支体交换

其中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.mdinput/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),制造"近重复代码多处存在、只准改指定块"的极端场景。

八、如何扩展新的结构类变更

若要在基准套件中加入新的结构类任务,需要改动四处(仓库当前为只读,以下仅说明设计思路):

  1. mutation 实现:在 mutations.ts 新增一个category === "structural"Mutation,必要时声明multiHunk,并注册进ALL_MUTATIONSCATEGORY_MAP(mutations.ts);
  2. kind 映射:在 generate.ts 的STRUCTURAL_KINDS中加入新的StructuralKind
  3. 锚点校验:在structuralPromptData中为新 kind 编写case分支,遵循"每个锚点行唯一、可读、成对匹配"的校验纪律,任何歧义返回 null;
  4. 模板文案:在 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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询