HagiCode 中 AI 提交使用的提示词:设计思路与实现拆解
背景
用 AI 辅助开发这事,其实也算是经历了一整天敲代码的疲惫了吧。攒了一堆没提交的改动,配置文件、文档、业务逻辑、测试用例全混在一起,看着就让人头疼。手动分组、手写符合规范的 commit message、再切分支 push 一遍——光是这些"收尾活",半小时就这么没了。
其实这事儿自然就有了诉求——能不能一次性把未提交的改动扔给 AI,让它自己分析、分组、写 message、甚至直接 commit + push?
想法是好的,真做起来坑可不少。AI 很容易只改--author不改 Committer,提交历史里作者对了、提交者错了,看着就撕裂;它可能自由发挥写一堆花里胡哨的 message,完全不对齐你仓库的风格;它可能擅自切到主干分支把事情搞砸;它可能漏掉Co-Authored-By,或者乱加Signed-off-by触发合规问题。
这一个个坑,踩下来也都是教训罢了。为了填平这些痛点,我们把"AI 提交"做成了一个参数化的 Agent 任务契约。这份契约长什么样、为什么这么设计,就是这篇文章想聊清楚的事。
关于 HagiCode
本文分享的方案来自我们在 HagiCode 项目里的实践。HagiCode 是一个面向开发者工作流的 AI 代码助手,把 Git 提交、代码审查、构建发布这些日常环节都做成了 AI 可参与的任务。下文拆解的提示词系统,正是 HagiCode 后端里真实在跑的那一套。说到底,也只是想把那点琐碎的"收尾活"交给 AI 罢了。
提示词的真实形态:模板加元数据,而不是一段写死的字符串
很多人以为"提示词"就是一段写死的自然语言,丢给模型就完事了。其实 HagiCode 的做法完全不是这样。
真正驱动"AI 提交"的提示词叫auto-compose-commit,对应代码里的PromptScenario.AutoComposeCommit。它位于repos/hagicode-core/src/PCode.Web/Resources/Prompts/下,结构是这样的:
Resources/Prompts/ |
├── auto-compose-commit.en-US.hbs # 英文 Handlebars 模板 |
├── auto-compose-commit.en-US.json # 英文元数据(参数 schema、版本、标签) |
├── auto-compose-commit.zh-CN.hbs # 中文模板 |
└── auto-compose-commit.zh-CN.json # 中文元数据 |
也就是说,一个提示词是一份 Handlebars 模板 + 一份 JSON 元数据的组合,按 locale 平铺成多套。
为什么要这么拆呢?其实背后有几个考量。
第一,元数据和提示词正文解耦。JSON 描述参数 schema——参数叫什么、什么类型、是否必填、默认值是什么;.hbs只管"这段话怎么说"。这样一来,前端可以在完全不知道模板正文的前提下,依据 JSON 自动渲染出正确的输入表单:Git 身份选择器、Co-Authored-By 模式、目标分支策略、要不要 push……这些控件都是 JSON 驱动出来的。
第二,多语言平铺,而不是用 i18n key 做翻译。每个 locale 一整套完整的.hbs+.json,避免了"翻译 key 漂移"。不同语言不只是把词替换掉,连分组示例、命令示例都可以本地化。中英文仓库的提交习惯本就不同,硬塞进一套模板再翻译,反而别扭罢了。
第三,从 Scriban 迁到 Handlebars,是为了性能。HandlebarsTemplateRenderer选用了Handlebars.Net,因为它能"compile templates directly to IL bytecode",比解释执行快得多。迁移过程中还做了个有意思的兼容处理:把渲染结果里的True/False替换成true/false,兼容旧 Scriban 的布尔输出习惯——这种细节不留意,旧测试会全红。
提示词长成这样,背后有五个关键决策
把auto-compose-commit.zh-CN.hbs拆开看,骨架大致是:
非交互模式说明 |
├── <task> 任务定义:分析变更、智能分组、多提交 |
├── <context> 上下文:projectPath + push 控制 + 目标分支控制 |
├── <working_directory> |
├── <git_profile> 身份:Author 加 Committer 双写 |
├── <tools> 工具白名单 |
├── <requirements> 硬性要求(分支、分组、Co-Authored-By、Signed-off-by、Conventional Commits) |
├── <historical_format_analysis> 历史一致性 |
├── <constraints> 约束(禁止 reset、忽略 .gitignore) |
├── <workflow> 分步执行流程 |
├── <output_format> 严格的 `---` 分隔输出 |
└── <final_instruction> |
下面挑五个最能体现设计意图的点展开聊聊。
决策一:直接执行,而不是只生成计划
提示词里反复强调一句话:直接使用 Git 命令执行每个提交,不返回计划,直接操作。
这是"Auto Compose Commit"区别于早期方案的根本不同。早期的ai-git-commit-message-generator(对应 OpenSpec 里的ai-commit-message-generation规范)只做一件事:调一个POST /api/git/generate-commit-message,返回一段 commit message 字符串,剩下的用户自己手动去提交。
可是auto-compose-commit不一样,它是一个Agent 自动任务。模型必须自己调用Bash(git:*)工具,把 add → commit → push 的全链路跑完。这一区别,就决定了整段提示词的基调——它不能只描述"要写什么样的 message",还得规定"按什么流程操作、用什么工具、出错怎么办"。
决策二:为什么 Git 身份要写得这么啰嗦
<git_profile>和<requirements>里有一大段关于 Author 与 Committer 的说明,乍看挺冗余:
- `--author="Name <email>"` 只会修改 Author |
- `git -c user.name="Name" -c user.email="email" commit ...` 只会修改这一次命令的 Committer |
- 对于每一个生成的提交,你都必须同时把 Author 和 Committer 设置为选定身份 |
- 首选命令形式: |
git -c user.name="..." -c user.email="..." commit --author="... <...>" ... |
这其实是真实踩坑换来的。Git 提交里有两个身份字段,模型很容易只改--author,结果 Committer 还是全局配置的那个身份。提交历史里"作者是对的、提交者是错的",看着就撕裂。所以提示词直接把首选命令模板贴出来,还要求模型用git log --format=fuller -1做自检。
类比一下,这就像你寄快递,"寄件人"和"实际经手人"是两张不同的单子。你只在一张单子上写了名字,另一张还印着公司的名字——快递是寄出去了,可记录对不上,终归是别扭罢了。
决策三:分组决策树加历史一致性
模型最擅长的就是"自由发挥",可自由发挥在提交分组这事上,往往是灾难。所以提示词给了一棵明确的决策树:配置文件单独一组、文档单独一组、同模块的代码改动合并、跨模块的改动看情况。还配了正例,比如src/auth/login.ts加上auth.service.ts应该进同一个提交。
更关键的是<historical_format_analysis>这一段。它要求模型:
- 使用
git log -n 15 --pretty=format:"%H|%s|%b%n---%n"获取最近的提交历史- 分析结构模式、语言模式、常用类型、特殊格式
- 生成遵循检测到的模式的提交信息
也就是说,模型不能想怎么写就怎么写,得先去对齐目标仓库已有的风格。HagiCode Mono 主仓用英文 + Conventional Commits,某些子仓库用中文段落式,AI 必须入乡随俗。这个能力对应归档提案2026-02-23-auto-commit-compose-history-consistency-optimization,是后来补上的优化。毕竟,谁也不希望自家提交历史像一锅乱炖罢了。
决策四:Co-Authored-By 和 Signed-off-by 的条件渲染
提示词里有大量嵌套的{{#if}},根据运行参数决定要不要加 trailer:
coAuthoredByIsNone时,完全不加Co-Authored-BycoAuthoredByIsCustom时,用用户给的自定义 trailersignedOffByEnabled加上gitProfileName时,加Signed-off-by,缺失身份时必须报错而不是臆造一个
trailer 这块涉及署名归属和合规(DCO sign-off),必须由用户显式控制,绝不能让模型自作主张。HagiCode 在这块陆续落地了git-commit-coauthor-standardization、ai-commit-consent-management等一系列提案,才把边界划清楚。这种事,宁可严一点,也不能含糊。
决策五:---分隔的输出契约
<output_format>规定每次返回必须用---分隔多个 commit 块,格式写死:
--- |
Commit 1: {hash} |
{message} |
--- |
Commit 2: {hash} |
{message} |
--- |
这可不是为了好看。模型一次任务可能产出 N 个提交,后端要靠这个分隔符把每条提交的 hash 和 message 解析出来,回传给前端展示。一旦输出协议松动,后端解析直接崩。所以---这条规则在<output_format>和<final_instruction>里被强调了两次——重要的事,本就该说三遍罢了。
提示词是怎么被组装和投递的
光看模板还不够,得知道它怎么跑起来。
加载与渲染
后端在PCodeClaudeHelperModule里注册了两个单例:
