oh-my-pi 提交信息自动生成中的 summary-rewrite:基于 LLM 的单行摘要合规改写机制解析
导读
本文剖析 oh-my-pi 提交信息自动生成管线中一个关键但容易被忽略的环节:summary-rewrite 提示词(packages/coding-agent/src/commit/conventional/prompts/summary-rewrite.md)。当 LLM 生成的首版 conventional commit 摘要因时态、长度、标点等规则校验不通过时,系统会调用该提示词,让模型以"复制编辑"身份在尽量少改动原文的前提下将其改写为合规摘要。读完本文,你将掌握该提示词的完整约束体系、模板变量的来源与语义、底层校验规则如何驱动改写,以及它在"生成 → 校验 → 修复 → 重写 → 兜底"整条降级链中的准确位置,并可直接借鉴该设计思路搭建自己的 LLM 提交信息纠错环节。
一、它在流水线中的位置:一次"挽救性"的二次生成
oh-my-pi 的常规提交信息生成流程位于 packages/coding-agent/src/commit/conventional/generate.ts。核心入口 generateConventionalCommit 会根据改动规模选择快速工作流(fast)或标准工作流(analysis + summary)。标准工作流中,摘要的生成与挽救逻辑集中在 generateSummaryFromAnalysis(generate.ts#L250-L314):
- 基于 holistic analysis 生成一份摘要草稿;
- 调用
validateSummaryQuality校验; - 校验失败先尝试
repairSummaryTense做纯规则的时态修复; - 规则修复不可行时,才调用
rewriteSummaryForCompliance把草稿连同拒绝原因交给summary-rewrite提示词重写; - 重写仍失败则落入
fallbackSummaryForCommit确定性兜底。
也就是说,summary-rewrite 是摘要质量保障链上的第三道防线:它处理的是"语义正确但形式不合规"的草稿,属于 LLM 的二次生成,而非首轮生成。
调用处 rewriteSummaryForCompliance(generate.ts#L330-L362)完整展示了该提示词如何被渲染与消费:
const prompts = renderConventionalPrompt("summary-rewrite", {
commit_type: commitType,
chars,
draft,
rejection,
});
const summary = await inference.complete(
{
operation: "summary-rewrite",
role: "summary",
promptFamily: "summary-rewrite",
systemPrompt: prompts.system,
userPrompt: prompts.user,
toolName: "create_commit_summary",
progressLabel: "Rewriting commit summary…",
},
response => stripTypePrefix(parseSummaryMarkdown(response.text)).trim(),
);
返回结果会再次经过 acceptSummary 校验(先验证、再尝试时态修复),只有最终通过 validateSummaryQuality 的文本才会被采用;否则返回空字符串,交由上层继续走兜底逻辑。这保证了"重写"本身也不会绕过规则。
二、提示词的主体:一个"复制编辑"角色的五条约束
summary-rewrite.md 的系统提示词将模型定位为 copy editor(复制编辑),强调这是一次纯粹的文本编辑任务,模型不需要理解底层代码变更,只需在保留原意的前提下让草稿满足全部约束。原文中的五条约束是:
- 以小写过去时动词开头(added、replaced、migrated、restructured 等),永远不要用提交类型 token 本身开头;
- 不超过用户消息中给定的字符上限;
- 无类型/作用域前缀、无结尾句号、无引号、无 markdown 格式;
- 尽可能少改动措辞,绝不添加草稿中不存在的信息;
- 只修复被报告的问题,不做任何其他改动。
这五条约束与校验层一一对应:第 1 条对应 validation.ts 的 isPastTenseFirstWord(过去时首词检查)与 type_word_repetition(首词不得重复类型 token);第 2 条对应三级长度阈值;第 3 条对应 trailing_period、前缀剥离与 parseSummaryMarkdown 的解析契约;第 4、5 条则是工程化约束,防止模型借重写之机扩大信息量或引入新事实。
值得注意的是约束 1 的措辞 ——"never the commit type token itself"。这与校验器中的 type_word_repetition 检查(validation.ts#L259-L268)互为表里:例如类型是 feat 时,摘要不能以 "feat" 开头,因为 feat(scope): feat something 是语义重复。
三、输出格式契约:<summary> 标签与宽容解析
提示词要求模型不带代码围栏地返回如下格式:
<summary>rewritten text only</summary>
但模型输出往往不可控。为此消费端没有依赖严格解析,而是使用了宽容的多级解析器 parseSummaryMarkdown(markdown.ts#L99-L117),依次尝试:
- JSON 解析:若输出形如
{"summary": "..."}或带title/message键,则直接取值; - 标签提取:宽容匹配
<summary>...</summary>(extractTagLenient),不要求标签闭合规范; - 纯文本降级:剥离
#标题标记、Title:等标签前缀、成对包裹引号,再规整空白。
随后统一经过 stripTypePrefix(markdown.ts#L40-L55)剥掉可能残留的 type(scope): 前缀并去掉结尾句号。该设计让模型即使输出 Markdown 代码围栏、JSON 或带前缀文本,也能被正确还原为摘要本体。测试 commit-conventional.test.ts#L160-L180 覆盖了 Title: Added JWT auth、含围栏的 <summary> 等多种形态,验证了解析器的健壮性。
四、用户消息模板变量:commit_type、chars、draft、rejection 从哪来
提示词文件以 <!-- USER --> 为界分成系统段与用户段。渲染机制见 prompts.ts:renderConventionalPrompt 用该分隔符切分模板,系统段原样使用,用户段交给 prompt.render(Handlebars 风格模板)填充上下文。六个提示词家族(analysis、fast、map、reduce、summary、summary-rewrite)共用这套渲染管线。
summary-rewrite 的用户段模板包含三个占位区:
<commit_metadata>
commit_type: {{ commit_type }}
max_summary_chars: {{ chars }}
</commit_metadata>
<draft_summary>
{{ draft }}
</draft_summary>
<rejection_reason>
{{ rejection }}
</rejection_reason>
对应调用方 rewriteSummaryForCompliance 传入的四个参数,语义如下:
- commit_type:analysis 阶段判定的提交类型(feat / fix / refactor 等),用于让模型避开类型 token 并选用合适的过去时动词;
- chars:可用的摘要字符额度。它由
generateSummaryFromAnalysis预先算好 ——chars = Math.max(20, config.summaryGuideline - prefixLength),其中prefixLength是type:或type(scope):前缀的码点长度(generate.ts#L257-L258)。也就是说提示词里的限制是扣除前缀后的正文额度,并保底下限 20 字符; - draft:被判定不合规的草稿摘要(取首份失败的候选,经
stripTypePrefix清洗); - rejection:校验器返回的错误信息拼接串(
report.errors.map(issue => issue.message).join("; ")),例如 "Summary must start with a past-tense verb (ending in -ed/-d or irregular)"。
模型的任务就是基于 rejection 指出的具体问题修正 draft,这正是"只修复被报告的问题"约束得以落地的信息基础 —— 模型能确切看到自己哪里违规了。
五、裁判规则:校验层如何定义"合规"
要理解提示词为何这样写,需要先看它的裁判 —— validateSummaryContent(validation.ts#L243-L294)及其支撑数据。合规定义包含四类检查:
5.1 过去时首词判定
isPastTenseFirstWord(validation.ts#L88-L101)是核心难点。判定一个 token 是否为过去时动词,依赖 resources/validation_data.json 中的大规模词表:
past_tense对照表:数百对"现在时 → 过去时"映射,如 add→added、rewrite→rewrote、run→ran、take→took,覆盖大量不规则动词(rewrite/rewrote、freeze/froze 等)与工程领域高频动词(rebuild/rebuilt、benchmark/benchmarked);irregular_past列表:left、felt、meant、sent、spent、lost、slept 等无法靠后缀推断的不规则过去式;ed_blocklist:hundred、red、bed、need、seed、sacred 等以 -ed 结尾但并非过去式的单词;d_blocklist:and、bad、had、good、food、should 等以 -d 结尾的干扰项。
判定逻辑是:命中对照表中的过去式、或以 -ed 结尾且不在 blocklist、或以元音+d 结尾(长度≥4)且不在 blocklist、或命中不规则列表,即视为过去时。此外还支持剥离 token 中的非字母后缀(如 re-add 拆出 re + add 再递归判定),防止标点粘连干扰首词识别。
5.2 类型重复、标点与内容质量
- 首词不得与提交类型同名(
type_word_repetition); - 摘要不得以句号结尾(
trailing_period); - 命中
filler_words(comprehensive、various、seamless、robust、world-class 等营销空洞词)触发 warning; - 命中
meta_phrases("this commit"、"this change"、"some changes"、"refactored code" 等元描述短语)触发 warning —— 这正是提示词要求"描述变更本身"的校验依据。
5.3 长度三级阈值
长度校验(validateSummary,validation.ts#L188-L241)按第一行总长(类型+作用域+分隔符+摘要)设定三级阈值,默认值见 config.ts#L67-L86:
| 级别 | 默认值 | 超限后果 |
|---|---|---|
| summaryGuideline | 72 | warning:超出建议长度 |
| summarySoftLimit | 96 | warning:超出软限制 |
| summaryHardLimit | 128 | error:硬性超限,校验失败 |
注意 summary-rewrite 中 chars 用的是 summaryGuideline - prefixLength,即提示模型尽量压到建议值以内;但最终决定成败的是 summaryHardLimit。长度按 Unicode 码点(而非 UTF-16 单元)计算,由 text.ts 的 codePointLength 实现,并对齐 Python 字符串长度语义,避免 emoji 或 CJK 字符导致字节数误判。
5.4 纯规则修复:repairSummaryTense
在校验失败进入重写前,还有一个零成本修复手段 repairSummaryTense(validation.ts#L49-L55):如果首词在 presentToPast 映射中存在,直接替换为过去式即可通过校验。例如草稿 "replace dependencies with local implementations" 会被机械修复为 "replaced dependencies with local implementations"(该行为有测试覆盖,见 commit-conventional.test.ts#L182-L189)。只有这种修复不可行时(如首词根本不是动词、长度超标、或含禁止短语),才轮到 summary-rewrite 出场。
六、设计要点:为什么强调"最小改动"与"不新增信息"
对比同目录的初代摘要提示词 summary.md 可以更清楚 summary-rewrite 的分工差异:
- summary.md 是"撰写者":基于 detail points 与 diff stat 综合提炼整个变更集的伞状描述,明确禁止照抄单一 detail point,并附有类型→动词参考表(feat→added/introduced、fix→corrected/resolved、refactor→restructured/migrated 等)和禁用词表(improved、enhanced、now 等);
- summary-rewrite.md 是"编辑者":输入已有一份语义正确的草稿,不负责重新理解代码,只负责让形式合规。因此它明确写出 "Change as little wording as possible; never add information that is not in the draft",避免二次生成引入幻觉或偏离草稿原意 —— 这是生成类任务中最实用的防漂移手段。
两者的衔接在 generateSummaryFromAnalysis 中一目了然:首轮用 summary.md 生成 → 校验 → 失败则尝试规则修复 → 仍失败才用 summary-rewrite.md 重写。重写后的结果如果还是不行,最终由 fallbackSummaryForCommit(generate.ts#L364-L395)兜底 —— 它内部也维护了一张类型→过去时动词映射表(feat→added、fix→fixed、refactor→restructured、docs→documented、perf→optimized、build/ci/chore→updated、style→formatted、revert→reverted),并结合 stat 中的文件路径生成确定性摘要。整条链路上每一次 LLM 尝试都有规则校验把关,确保最终提交信息要么合规、要么带着 validationError 返回给人工处理。
七、测试证据与配置可调性
自动化测试 commit-conventional.test.ts 直接验证了本机制:用例 "repairs a rejected generated summary before rewrite or fallback"(L229-L243)构造一个首轮输出 "correct null dereference"(现在时、不合规)的场景,断言管线依次执行 analysis、summary 两次调用后产出合规的 "corrected null dereference"。同时 "uses one fast call for changes at the 200-line threshold"(L193-L212)验证了 200 行阈值的快速路径,说明摘要修复逻辑与工作流路由是解耦的。
涉及的重写行为由配置间接调控:maxRetries(默认 3)控制摘要生成的重试次数;三级长度阈值、mapReduceEnabled 等项可通过 CommitSettings 覆盖(见 config.ts#L88-L96 的 conventionalGenerationConfig 合并逻辑)。如果你的团队希望摘要更短或更宽松,只需调整 summaryGuideline / summarySoftLimit / summaryHardLimit 即可,提示词中的 chars 会随之自动重算。
结语
summary-rewrite 提示词看似只有寥寥数行,却是 oh-my-pi 提交信息质量工程中"规则驱动 + LLM 修复 + 确定性兜底"三层防线的重要一环。它的可借鉴之处在于:把校验规则显式地喂给模型(通过 rejection_reason)、限定模型角色为纯文本编辑(最小改动、不新增信息)、用宽容解析消化不可控输出(标签/JSON/纯文本三级降级)、以及每次 LLM 输出都回归校验器。这套"先生成、后校验、再修复"的闭环,比单纯依赖模型自觉更可靠,值得在各类 LLM 文本生成后处理场景中复用。
<输出文章>
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 StartedRust4.21 K637- DDeepSeek-V4.1-FlashDeepSeek-V4.1-Flash 是一个多模态混合专家(MoE)模型,拥有 5520 亿骨干参数,并支持最多一百万 token 的上下文长度。该模型原生支持图像和文本输入,并以自回归方式生成文本Python230
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java281
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java200
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript180
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python300