oh-my-pi 的 AI Changelog 撰写规范:changelog-system.md 系统提示词拆解与代码流水线实现
导读
本文以 packages/coding-agent/src/commit/prompts/changelog-system.md 为主线,完整拆解 oh-my-pi(仓库代码仓库中的 AI Coding Agent 项目)如何用一条系统提示词驱动 LLM 从 git diff 自动产出符合 Keep a Changelog 规范的变更日志条目。读完本文,你将掌握该提示词的全部约束设计(分类体系、条目格式、排除规则、JSON 输出契约),并通过源码链路了解它在 omp commit 中是如何被渲染、调用、解析、去重并写回 CHANGELOG.md 的。
一、文档定位:一条被工程化消费的"提示词即契约"
changelog-system.md 并不是给人看的说明文档,而是一条被源码以文本形式导入的系统提示词。在 generate.ts 中可以看到它被直接加载:
import changelogSystemPrompt from "../../commit/prompts/changelog-system.md" with { type: "text" };
import changelogUserPrompt from "../../commit/prompts/changelog-user.md" with { type: "text" };
随后在 generateChangelogEntries 中,系统提示词被渲染并作为 systemPrompt 发送给模型(generate.ts):
const response = await retryTransientCompletion(() =>
completeSimple(
model,
{
systemPrompt: [prompt.render(changelogSystemPrompt)],
messages: [{ role: "user", content: userContent, timestamp: Date.now() }],
tools: [changelogTool],
},
{ apiKey, sessionId, maxTokens: 1200, reasoning: toReasoningEffort(thinkingLevel) },
),
);
也就是说,这条提示词是"AI 变更日志写手(expert changelog writer)"的角色设定与行为契约,配套的用户侧模板 changelog-user.md 负责注入 diff 上下文。两者配合构成了完整的"系统约束 + 任务输入"结构。
二、核心指令(instructions):只写用户可见的变更
提示词开头定义了三条硬性指令:
- Identify only user-visible changes
- Categorize each change (use categories below)
- Omit categories with no entries
拆解如下:
- 只识别用户可见的变更:这是整个提示词的第一原则。内部重构、代码风格调整、纯测试改动、文档微调都被视为"对用户无感",不应当出现在 changelog 中;
- 逐条归类:每条变更必须落入下方七类分类之一,不允许出现游离于分类体系之外的描述;
- 省略空分类:没有任何条目的分类不要输出,保证结果 JSON 的最小化。
这一"用户可见性"原则在源码侧还有一处呼应:types.ts 中定义了 ConventionalDetail 结构,携带 userVisible: boolean 字段(types.ts),说明"是否用户可见"是整个 commit 分析流水线的通用判定标准,而不仅存在于提示词里。
三、七类分类体系(categories)与源码契约
提示词定义的分类与含义如下:
| 分类 | 含义 | 典型场景 |
|---|---|---|
Added |
新增功能、公共 API、面向用户的能力 | 新命令、新参数、新工具 |
Changed |
修改了既有行为 | 默认值调整、流程变化 |
Deprecated |
计划移除的功能 | 标记废弃但暂未删除 |
Removed |
删除的功能或 API | 下线旧接口 |
Fixed |
可观察影响的缺陷修复 | bug 修复 |
Security |
漏洞修复 | 安全问题 |
Breaking Changes |
API 不兼容的变更(谨慎使用) | 移除认证流程等 |
值得注意的是:提示词正文将 Breaking Changes 列在最后,但仓库内部对其顺序另有规定。在 types.ts 中,CHANGELOG_CATEGORIES 定义了规范的渲染顺序:
export const CHANGELOG_CATEGORIES: ChangelogCategory[] = [
"Breaking Changes",
"Added",
"Changed",
"Deprecated",
"Removed",
"Fixed",
"Security",
];
这是 Keep a Changelog 惯例的分类展示顺序(Breaking Changes 置顶),由 renderUnreleasedSections 在 changelog/index.ts 中按此顺序渲染 ### 分类 小节。
同时,在模型工具层,generate.ts 用 arktype 定义了与提示词一一对应的结构化 schema(generate.ts):
const changelogEntriesSchema = type({
"Breaking Changes?": "string[]",
"Added?": "string[]",
"Changed?": "string[]",
"Deprecated?": "string[]",
"Removed?": "string[]",
"Fixed?": "string[]",
"Security?": "string[]",
});
export const changelogTool = {
name: "create_changelog_entries",
description: "Generate changelog entries grouped by Keep a Changelog categories.",
parameters: type({ entries: changelogEntriesSchema }),
};
每个分类都是可选的字符串数组,模型可以调用 create_changelog_entries 工具返回结构化结果,而不是裸文本——这保证了后续解析的可靠性。
四、条目撰写格式(entry-format):四条铁律与正反例
提示词对单条条目给出四条格式要求:
- 以过去式动词开头:
Added、Fixed、Implemented、Updated; - 描述用户可见的影响,而非实现细节:写"用户能感知到什么变化",不写"内部怎么改的";
- 点名具体功能、选项或行为:条目必须可定位到具体对象;
- 保持 1~2 行,不加句尾句号:简洁且语法统一。
好的例子(Good)
- Added --dry-run flag to preview changes without applying them
- Fixed memory leak when processing large files
- Changed default timeout from 30s to 60s for slow connections
坏例子(Bad)及原因
- **cli**: Added dry-run flag → redundant scope prefix
- Added new feature. → vague, trailing period
- Refactored parser internals → not user-visible
提示词明确给出了三类"坏"的原因:
- 多余的作用域前缀(
**cli**:):changelog 条目不应重复 scope 前缀,保持干净的自然语言; - 含糊 + 句号(
Added new feature.):没有点名具体功能,且末尾句号违反格式要求; - 非用户可见(
Refactored parser internals):纯内部重构被明确排除。
Breaking Changes 的专门示例
- Removed legacy auth flow; users must re-authenticate with OAuth tokens
破坏性变更需要写出对用户的后续影响(必须重新认证),这是该分类特有的写作要求。
值得补充的是,"去掉句尾句号"不仅是提示词要求,源码在解析阶段也做了兜底。dedupeEntries 会对每条记录执行 value.trim().replace(/\.$/, "")(generate.ts),normalizeEntries 同样执行 .trim().replace(/\.$/, "")(changelog/index.ts)——即使模型偶发输出句号,写入文件前也会被规范化。
五、排除规则(exclude):什么不该写
提示词明确规定以下内容不得进入 changelog:
Internal refactoring, code style changes, test-only modifications, minor doc updates.
即内部重构、代码风格变更、纯测试修改、次要文档更新四类一律排除。这与第一部分的"只写用户可见变更"相互呼应:被排除的四类恰好都是用户无感的变化。这也解释了为什么坏例中的 Refactored parser internals 会被标记为 Bad。
六、输出格式(output-format):严格的 JSON 契约
提示词对输出格式提出两条强制要求:
Return ONLY valid JSON; no markdown fences or explanation.
即:
- 只返回合法 JSON,禁止出现 markdown 代码围栏(```)或任何解释文字;
- 有条目时返回
{"entries": {"Added": ["entry 1"], "Fixed": ["entry 2"]}}; - 无可写条目时返回
{"entries": {}}。
源码如何消化这个契约
parseChangelogResponse(generate.ts)实现了双通道解析:
function parseChangelogResponse(message: AssistantMessage): ChangelogGenerationResult {
const toolCall = extractToolCall(message, "create_changelog_entries");
if (toolCall) {
const parsed = validateToolCall([changelogTool], toolCall) as typeof changelogTool.parameters.infer;
return { entries: parsed.entries ?? {} };
}
const text = extractTextContent(message);
const parsed = parseJsonPayload(text) as ChangelogGenerationResult;
return { entries: parsed.entries ?? {} };
}
- 优先通道:若模型调用了
create_changelog_entries工具,则用validateToolCall按 arktype schema 严格校验; - 兜底通道:若模型只返回文本,则用
parseJsonPayload从纯文本中提取 JSON。
两个通道都兼容 {"entries": {}} 的空结果——当 Object.keys(generated.entries).length === 0 时,runChangelogFlow 会跳过该文件的写入(changelog/index.ts)。
去重细节
dedupeEntries 按"小写化后的文本"去重,并且再次去掉句尾句号、剔除空字符串(generate.ts)。这意味着即使模型在同一分类下重复输出相似条目,最终也只保留一条。
七、从提示词到完整流水线:omp commit 中的调用链路
changelog-system.md 不是孤立存在的,它被嵌入在 oh-my-pi commit 流水线的完整链路中:
cli.ts --no-changelog 开关
→ pipeline.ts 调用 runChangelogFlow
→ detect.ts 找出每个变更文件"最近的 CHANGELOG.md"边界
→ index.ts 取 cached diff + numstat 统计,截断超大 diff
→ parse.ts 解析现有 [Unreleased] 小节的条目
→ generate.ts 渲染 changelog-system.md + changelog-user.md,调用模型
→ index.ts 合并、去重、按 CHANGELOG_CATEGORIES 顺序渲染并写回
各环节的关键实现证据:
1. 边界探测(detect.ts)
detect.ts 对每个暂存文件从所在目录逐级向上查找最近的 CHANGELOG.md(不区分大小写匹配 changelog.md,且跳过 changelog 文件自身),将多个变更文件按所属 changelog 文件分组。这支持了 monorepo 场景下"一个包一份 changelog"的诉求。
2. diff 与统计信息(index.ts)
runChangelogFlow(changelog/index.ts)会:
- 用
repo.diffText({ cached: true, files: boundary.files })取暂存区 diff; - 用
repo.numstat渲染类似git diff --stat的摘要(renderStat); - 用
truncateDiff将超长 diff 截断到默认120_000字符(DEFAULT_MAX_DIFF_CHARS),超出部分标注[…N ch elided…]。
截断后的 diff 与 stat 都会注入到 changelog-user.md 模板的 <diff-summary> 与 <diff> 块中,作为用户消息发送给模型。
3. 已有条目防重复(changelog-user.md)
changelog-user.md 是配套的用户模板:
<context>
Changelog: {{ changelog_path }}
{{#if is_package_changelog}}Scope: Package-level changelog. Omit package name prefix from entries.{{/if}}
</context>
{{#if existing_entries}}
<existing-entries>
Already documented—skip these:
{{ existing_entries }}
</existing-entries>
{{/if}}
其中两个细节值得注意:
existing_entries:已解析出的 [Unreleased] 现有条目会以"Already documented—skip these"的形式提供给模型,避免重复记录;is_package_changelog:当目标 changelog 不是仓库根CHANGELOG.md时,要求条目省略包名前缀。该判断来自 changelog/index.ts:path.resolve(boundary.changelogPath) !== path.resolve(cwd, "CHANGELOG.md")。
4. 写入与合并(index.ts)
applyChangelogEntries(changelog/index.ts)在解析出的 [Unreleased] 行区间内重写小节:现有条目与新条目按分类合并(mergeEntries 同样做小写去重),并支持 deletions 定向删除。非 dryRun 模式下写入文件后还会 repo.stageFiles 把 changelog 重新加入暂存区。
八、双保险:Agentic 模式下 propose_changelog 工具的强校验
除了提示词约束,oh-my-pi 的 agentic commit 流程还提供了一个独立工具 propose_changelog(propose-changelog.ts),对模型输出做二次强校验:
- 分类白名单校验:
allowedCategories只接受CHANGELOG_CATEGORIES七类,未知分类直接报错Unknown changelog category for {path}; - 条目规范化:
value.trim().replace(/\.$/, "")+ 空值过滤 +Set去重,与提示词格式要求完全对齐; - 目标文件校验:模型只能对预期内的 changelog 文件提条目,越界(
Changelog not expected)、重复(Duplicate changelog entry)、遗漏(Missing changelog entries)都会被判定为校验失败; - 校验失败时返回
"Changelog validation failed."并附带 errors 列表,供 Agent 自我修正。
这体现了该项目的设计哲学:提示词负责引导,schema 与校验代码负责兜底,两者共同保障 changelog 数据的结构化与正确性。
九、运行与调试实操
1. 触发时机
changelog 更新内嵌于 omp commit 流水线,由 pipeline.ts 在生成 commit message 后执行;agentic 模式则由 agentic/index.ts 在流程中调用。CLI 层提供开关(cli.ts):
--no-changelog Skip changelog updates
默认 noChangelog: false,即默认会更新 changelog;显式传入 --no-changelog 可跳过。
2. 调试提示词效果
如果你希望调整模型产出风格,可以修改 changelog-system.md 中的 entry-format 示例或 categories 描述,改动会被源码以文本导入直接生效(无需重新编译模板)。但请保持以下契约不变,否则会破坏解析:
- 输出必须是合法 JSON(
{"entries": {...}}或{"entries": {}}); - 分类名必须与
types.ts中CHANGELOG_CATEGORIES的七类完全一致; - 单条条目不应以句号结尾(解析层会兜底清除,但保持规范更利于模型稳定)。
3. 常见观察点
- 若 changelog 未更新,先检查 [Unreleased] 小节是否存在(parse.ts 中
## [Unreleased]正则不匹配会抛错并跳过); - 若 diff 过大,模型看到的可能是截断内容(默认 120K 字符),复杂变更建议分批暂存提交;
- 若条目重复,检查
dedupeEntries与mergeEntries的小写化去重逻辑是否命中预期。
十、总结
changelog-system.md 虽只有短短 50 行,却浓缩了一套完整的 AI 变更日志工程规范:用户可见性过滤 → 七类分类 → 格式约束 → 排除规则 → 严格 JSON 契约。在 oh-my-pi 中,它并非孤立的提示词,而是与 types.ts 的分类常量、generate.ts 的 arktype schema 与去重逻辑、detect.ts/parse.ts/index.ts 的边界探测与写入流水线、以及 propose_changelog 工具的强校验共同构成闭环。理解这条提示词,就等于理解了如何用"提示词 + 结构化工具 + 代码校验"三件套,把 LLM 的模糊语言输出驯化为机器可验证、可落盘的高质量 changelog 数据。
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 StartedRust0631
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
video-shotcraftAI宣传片skill,使用 Remotion 制作电影级产品视频:提供106 张镜头配方卡和可复用的视频魔板。适用于 Claude Code 与 Codex以及所有其他智能体Markdown00
HivisionIDPhotos⚡️HivisionIDPhotos: a lightweight and efficient AI ID photos tools. 一个轻量级的AI证件照制作算法。Python09
DragonOSDragonOS is an operating system developed from scratch using Rust, with Linux compatibility. It is designed for **Serverless** scenarios. 使用Rust从0自研内核,具有Linux兼容性的操作系统,面向云计算Serverless场景而设计。Rust00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00