oh-my-pi 结构化代码修复任务模板解析:用 Handlebar 模板生成精确的 bug 修复指令
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
导读
本文深入解析 oh-my-pi 项目中用于生成"结构化代码修复任务"的 Handlebar 模板 structural-task.md。该模板把 7 类常见代码缺陷(fall-through 标签、重复块、错位块、冗余调试包装、块/语句顺序颠倒、if-else 分支互换)的参数化描述与精确的代码片段 hunk 结合起来,渲染出"只改一处、其余不动"的强约束修复指令。读完本文,你将掌握该模板的分支逻辑、变量占位、hunk 渲染机制,以及它背后的模板引擎实现与黄金测试验证方法,可直接照此模式为 Agent 定制高质量的结构化任务提示词。
一、模板的定位与整体结构
structural-task.md位于 packages/utils/test/fixtures/template/ 目录下,与file-operations.md、frontmatter.md、identifier-task.md、session-user.md并列,是 oh-my-pi 为回归测试准备的真实模板黄金样本(golden fixture)。
它是一个 Handlebar 语法模板,输入一组结构化上下文(文件路径、缺陷类型、hunk 列表等),输出一段面向编码 Agent 的自然语言修复指令。模板整体由三部分组成:
- 标题区:
# Fix a bug in \{{filename}}``,用变量承载目标文件路径; - 缺陷描述区:一个
{{#when kind "==" ...}}多分支条件链,根据kind字段输出对应缺陷类型的描述文案; - 修复目标区:遍历
hunks数组,用代码围栏逐段给出"修复后必须精确呈现"的代码片段,并以一句强约束收尾。
这种"参数化描述 + 精确目标代码"的结构,保证了不同任务之间格式统一、机器可解析,同时让 Agent 的修改范围被严格限定。
二、缺陷类型分支:when条件链
模板核心是一个七分支条件结构,全部通过{{#when kind "==" "xxx"}}实现。每个分支描述一种典型的结构性 bug:
| kind 取值 | 缺陷描述 | 期望修复动作 |
|---|---|---|
case-label | switch中{{label}}的值必须与紧随其后的 case 完全一致处理 | 在{{before}}之前直接补一个 fall-through 的{{label}}标签 |
duplicate-block | 以{{head}}起始的代码块连续出现两次,第二次是复制粘贴事故 | 删除第二个副本,保留第一个 |
move-block | 以{{head}}起始的代码块被移动到错误位置,当前位于{{currentPrev}}之后 | 移回,使其直接位于{{destination}}之前 |
wrap-if | 残留的调试包装是多余的 | 删除第{{wrapperLine}}行的if (true) {及其右花括号,并将被包裹的代码体整体减少一层缩进 |
swap-blocks | 两个相邻代码块顺序颠倒 | 交换两个代码块 |
swap-lines | 两个相邻语句顺序颠倒 | 交换两条语句 |
swap-if-else | {{condition}}的两个分支体互换 | 交换 if 与 else 的分支体 |
when并不是 Handlebar 内置块级助手,而是 oh-my-pi 在 prompt.ts 中注册的自定义助手。它支持==、===、!=、!==、>、<、>=、<=八种比较运算符,运算符不在支持集合内时回退到inverse分支:
handlebars.registerHelper( "when", function (this: unknown, lhs: unknown, operator: string, rhs: unknown, options: Handlebars.HelperOptions): string { const ops: Record<string, (a: unknown, b: unknown) => boolean> = { "==": (a, b) => a === b, // ... ">": (a, b) => (a as number) > (b as number), // ... }; const fn = ops[operator]; if (!fn) return options.inverse(this); return fn(lhs, rhs) ? options.fn(this) : options.inverse(this); }, );这段实现也解释了为什么模板中同时出现"=="(宽松写法)与"===":两者在实现上都映射到严格相等比较,兼容不同风格的调用。
三、hunk 渲染:精确目标代码的注入机制
描述完缺陷后,模板通过{{#each hunks}}遍历所有需要修改的区域,为每个 hunk 输出"修复后必须读起来完全一致"的代码:
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}} Make exactly this change; do not modify anything else.这里有三个值得注意的机制:
- 单复数自适应:
{{#when hunkCount ">" 1}}regions must{{else}}region must{{/when}}根据 hunk 数量输出regions must或region must,保证语法的自然性; ../父级上下文引用:hunk 处于{{#each hunks}}的循环作用域内,而fence、language定义在顶层上下文,因此模板使用{{../fence}}、{{../language}}向上回溯取值。这与 template.ts 中resolvePath对../前缀的处理一一对应——每消耗一个../就沿frame.parents向上跳一层;- 可选行号锚点:
{{#if startLine}}为startLine > 0的 hunk 输出Around line N:提示,而startLine为 0 或缺失的 hunk(例如删除型修改)则直接输出代码块,不附带行号。
在黄金测试 template.test.ts 中,swap-lines用例给出了真实的渲染输入输出:
- 输入上下文:
filename: "src/a.ts"、kind: "swap-lines"、secondHead: "b();"、firstHead: "a();"、hunkCount: 2、fence: "```"、language: "ts",以及两个 hunk(startLine: 4的"a();\nb();"与startLine: 0的"c();"); - 渲染结果:
# Fix a bug in `src/a.ts` Two adjacent statements are in the wrong order: `b();` belongs before `a();`. Swap the two statements. After the fix, the affected regions must read exactly: Around line 4: ```ts a(); b();c();Make exactly this change; do not modify anything else.
可以看到:`hunkCount > 1` 触发了复数形式 `regions must`;第二个 hunk 因 `startLine` 为 0 被 `{{#if startLine}}` 跳过,直接输出代码块。整段输出不含任何多余修饰,正是为了让 Agent 可以逐字符对照。 ## 四、渲染链路:从模板到最终指令 模板不是直接拼接字符串,而是经过一条完整的渲染流水线。核心入口是 [prompt.ts](https://link.gitcode.com/i/61c11be24eb29c090bb784319b82450c) 中的 `render`: ```ts export function render(template: string, context: TemplateContext = {}): string { const compiled = compile(template); const rendered = compiled(context ?? {}); return format(rendered, { renderPhase: "post-render" }); }链路分三步:
- 编译:
compile调用 template.ts 中基于parseTemplate实现的兼容 Handlebar 的编译器,并带有基于原始模板字符串的编译结果缓存(compiledTemplateCache),重复渲染同一模板直接命中缓存跳过解析; - 渲染:用上下文执行编译产物,过程中解析
when、join、xml等自定义助手以及if、each、with等内置块级助手; - 后处理:
format在post-render阶段统一清理输出——压缩 Markdown 表格间距、折叠多余空行、在{{/闭合标签行前清除尾随空行等,保证最终指令干净、紧凑、适合直接进入上下文窗口。
一个容易被忽略的细节是编译选项:compile使用{ noEscape: true, strict: false }。也就是说,模板中的{{newCode}}等内容不做 HTML 转义,代码中的<、&等字符得以原样进入指令——这是代码片段类模板正确工作的前提。而 template.ts 中的escapeExpression仍保留着完整的转义实现与SafeString机制,供需要转义的场景使用。
五、与同系列模板的协同
structural-task.md并非孤立存在,它与同目录下的其他 fixture 共同覆盖了 Agent 指令生成的典型场景:
- identifier-task.md:标识符拼写错误修复,使用
{{#when count "==" 1}}做单复数处理、{{join affectedLines ", "}}拼接受影响行号; - file-operations.md:文件读写清单,通过
{{#xml "files"}}助手把文件列表包装成<files>...</files>结构(xml助手在 prompt.ts 中实现,内容为空时整体不输出); - frontmatter.md:Agent 元信息(name、model、thinking-level、blocking 等)的 frontmatter 生成;
- session-user.md:会话上下文与 changelog 目标的注入。
这些模板共享同一套渲染引擎与助手注册表。template.test.ts中还有一个全局保障测试:遍历仓库内packages/*/src/**/*.md下所有含{{的模板文件逐一编译,断言编译成功数量大于 100(见 template.test.ts),从侧面说明这套模板体系被广泛用于项目内的提示词生成。
六、实战:如何自定义一个新的结构化修复任务模板
理解了模板机制后,可以按同样的模式扩展新缺陷类型。一个最小化的新分支只需在条件链中追加:
{{#when kind "==" "your-new-kind"}} The {{placeholder}} is misconfigured. Replace it with {{correctValue}}. {{/when}}并在 hunk 区提供修复后的目标代码。关键约束包括:
- 上下文契约:模板引用的每个变量(
filename、kind、hunks、fence、language、hunkCount等)都必须在渲染时由调用方提供,缺失路径在非严格模式下渲染为空串,不会报错但会产出残缺指令; ../回溯:循环体内的 hunk 如需引用顶层变量,必须使用{{../name}}形式;- 精确性措辞:收尾句
Make exactly this change; do not modify anything else.与"read exactly"的强约束是结构化任务的精髓,新模板不应削弱这类限定语; - 黄金测试:参照 template.test.ts 的
GOLDENS数组,为每个新分支固化"输入上下文 → 期望输出"的配对,防止后续改动破坏渲染结果。
七、总结
structural-task.md是 oh-my-pi 提示词工程的一个缩影:用一套行为兼容 Handlebar 的模板引擎(template.ts)、一组面向提示词定制的助手(when、join、list、xml等,见 prompt.ts)和严格的后处理格式化,把 7 类结构性代码缺陷的参数化描述渲染为精确、可执行的修复指令。它既是对 Agent 行为的强约束工具,也是理解项目提示词生成管线的最佳入口——模板文件本身即是文档,黄金测试即是规格。
【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考