1. 为什么提交信息值得单独做一个工具
写了十年代码,我见过太多仓库的提交历史长这样:fix、update、修改、提交、aaa、111。过三个月回头看,没人知道那次到底改了什么。更麻烦的是排查线上问题时,想通过git log定位某次改动,结果满屏都是无意义的字符串,只能一个个 diff 点开看。
VSCode Commit AI这个项目要解决的就是这件事:在 VS Code 里点一下,让 AI 读取当前暂存区的改动,自动生成一条符合规范的提交信息,直接填进提交框。它不是一个独立应用,而是一个 VS Code 扩展,把"写提交信息"这个高频但低价值的动作交给模型处理。
适合谁用?三类人最受益。一是团队协作里需要统一提交规范但总有人偷懒的;二是接手老项目、需要频繁提交小步改动做重构的;三是英文表达不熟练、写不出地道 commit message 的。哪怕你只是个人项目,养成好习惯后回看历史也会舒服很多。
这篇文章我会把这个扩展从设计思路、核心实现、实操配置到踩坑排查完整拆一遍。你不需要有 AI 背景,只要会用 Git 和 VS Code 就能跟上。涉及模型调用的部分我会讲清楚参数怎么选、prompt 怎么写,这些才是决定生成质量的关键。
2. 整体设计与方案选型拆解
2.1 扩展形态为什么选 VS Code 而不是命令行工具
很多人第一反应是写个 CLI,git commit前挂个 hook 调用。但实际用下来,VS Code 扩展的体验明显更好,原因有几个。
提交信息本质上是"写代码时的上下文产物"。你在编辑器里刚改完文件,注意力还在代码上,这时候在侧边栏或命令面板点一下就能生成,比切到终端敲命令顺手得多。VS Code 扩展能直接拿到当前工作区的 Git 状态、暂存区 diff、甚至光标所在文件,这些上下文对生成准确的提交信息至关重要。
另一个原因是可视化。生成结果直接填进源代码管理面板的输入框,你能立刻看到、编辑、再提交,形成"生成-审阅-微调"的闭环。CLI 工具要么把结果打到 stdout 让你复制,要么直接提交,中间缺少这个确认环节,风险更高。
提示:扩展和 hook 并不冲突。我的做法是扩展负责生成,同时配一个
commit-msghook 做格式校验兜底,双保险。
2.2 读取 diff 的策略:暂存区优先,全量兜底
生成提交信息的前提是拿到"这次要提交什么"。这里有个容易踩的坑:如果直接读工作区所有改动,会把没git add的文件也算进去,生成的描述和实际提交内容对不上。
合理的策略是分两层。第一层读暂存区 diff(git diff --cached),这是即将提交的内容,最准确。如果暂存区为空,说明用户还没 add,这时候有两种处理:要么提示"请先暂存文件",要么退而求其次读工作区 diff 并明确告知用户。我倾向于前者,因为提交信息必须和暂存内容严格对应,否则就是误导。
diff 内容还需要做截断。一个几百行的改动,全塞给模型既慢又贵,还可能超出上下文窗口。常见做法是按文件聚合,每个文件只取前若干行改动,或者按 token 数截断。这里要保留的是"改了什么类型的文件、动了哪些函数、增删了多少行"这类结构性信息,而不是每一行代码的细节。
2.3 模型选型:本地还是云端
这是绕不开的决策。云端模型(各类通用大模型 API)生成质量高、对自然语言理解好,但需要网络请求、有成本、代码会离开本机。本地模型(如通过 Ollama 跑的小参数模型)隐私好、免费,但对 diff 的理解能力弱一些,生成的英文可能不够地道。
我的建议是分场景。公司内部项目、涉及敏感业务逻辑的,优先本地模型或走内网部署的模型服务。个人开源项目、对隐私不敏感的,用云端 API 省心。扩展设计上最好把模型调用抽象成一层接口,让用户自己填 endpoint 和 key,这样两种方案都能覆盖。
参数方面,temperature建议设低一点,0.2 到 0.4 之间。提交信息需要的是稳定、准确,不是创意。max_tokens给 100 到 200 足够,一条 commit message 本来就不该长。
2.4 输出格式:Conventional Commits 是事实标准
生成什么样的格式,直接决定工具好不好用。目前社区最认的是Conventional Commits规范,格式是type(scope): description。type 常见取值有feat、fix、docs、style、refactor、test、chore等。
为什么选它?因为它机器可读。后续可以用工具自动生成 CHANGELOG、判断版本号该升 major 还是 minor。而且它强制你思考"这次改动属于哪一类",本身就是一种约束。
prompt 里要明确告诉模型:先判断 type,再提取 scope(影响的模块),最后写一句不超过 72 字符的描述,用祈使句、现在时、首字母小写、结尾不加句号。这些细节不写清楚,模型就会自由发挥,生成一堆风格不统一的句子。
3. 核心细节解析与实操要点
3.1 扩展的目录结构与关键文件
一个能跑的 VS Code 扩展,骨架其实很轻。核心是package.json里的contributes字段,它声明了扩展往编辑器里注入什么。对于这个项目,至少要注册一条命令,比如commitAI.generate,再把它挂到源代码管理面板的标题栏菜单上。
commit-ai/ ├── package.json # 扩展清单,声明命令、菜单、配置项 ├── src/ │ ├── extension.ts # 激活入口,注册命令 │ ├── git.ts # 封装 git diff 读取逻辑 │ ├── ai.ts # 模型调用与 prompt 组装 │ └── config.ts # 读取用户配置 ├── tsconfig.json └── README.mdpackage.json里几个关键点。activationEvents建议用onCommand:commitAI.generate,按需激活,不拖慢启动。contributes.configuration里暴露commitAI.apiKey、commitAI.model、commitAI.language等配置项,让用户能在设置界面直接改,不用动代码。
3.2 用 VS Code 内置 Git API 还是自己调命令行
VS Code 提供了vscode.git扩展 API,能直接拿到仓库对象、暂存区改动、当前分支名。用它比自己在 Node 里 spawngit命令优雅得多,不用处理路径、权限、编码这些破事。
const gitExtension = vscode.extensions.getExtension('vscode.git')?.exports; const api = gitExtension.getAPI(1); const repo = api.repositories[0]; const staged = repo.state.indexChanges; // 暂存区文件列表但内置 API 拿到的 diff 有时不够细,比如它给的是文件级状态,具体行级 diff 还得自己调git diff --cached。所以实际项目里常见的是混合方案:用 API 拿仓库和文件列表,用命令行拿精确 diff。这样既稳又全。
注意:调用命令行时一定要指定
cwd为仓库根目录,并且处理 Windows 和 Unix 的路径分隔符差异,否则在跨平台时会莫名其妙失败。
3.3 prompt 工程:决定生成质量的核心
模型再强,prompt 写得烂也白搭。我调过很多版,最后稳定下来的结构是这样的:系统提示词定角色和规则,用户消息塞 diff 和上下文。
系统提示词大致是:"你是一个 Git 提交信息生成助手。根据提供的代码改动,生成一条符合 Conventional Commits 规范的提交信息。只输出提交信息本身,不要解释,不要加引号,不要用 markdown 代码块包裹。"
用户消息里要包含:当前分支名(有时能推断出任务类型)、改动文件列表、截断后的 diff、以及用户配置的语言偏好。如果用户设了中文,就要求生成中文描述,但 type 和 scope 保持英文。
这里有个细节:few-shot 示例非常有效。在 prompt 里塞两三个输入输出示例,模型生成的格式会稳定很多。比如给一个"改了登录逻辑"对应fix(auth): 修复token过期后未刷新的例子,模型就知道 scope 该怎么提取。
3.4 配置项设计:让用户能调但不至于懵
配置项太多是灾难,太少又不够用。我建议暴露这几个就够:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
commitAI.provider | string | openai | 模型服务商,决定调用格式 |
commitAI.apiKey | string | 空 | 密钥,建议用 SecretStorage 存 |
commitAI.model | string | gpt-4o-mini | 模型名 |
commitAI.language | string | zh | 生成语言,zh 或 en |
commitAI.maxDiffLines | number | 200 | diff 截断行数 |
apiKey千万别明文存在settings.json里,那玩意会被同步到云端。用context.secrets.store()存进系统密钥链,读取时异步取。这是很多新手扩展容易忽略的安全点。
4. 实操过程与核心环节实现
4.1 从零搭建扩展的开发环境
先把地基打好。装 Node.js 18 以上版本,然后全局装yo和generator-code,用官方脚手架生成项目骨架,省得手写一堆配置。
npm install -g yo generator-code yo code # 选择 New Extension (TypeScript)生成后按 F5 会弹出一个"扩展开发宿主"窗口,这就是调试环境。在里面打开任意 Git 仓库,就能测试你的命令。改代码后按 Ctrl+R 重载宿主窗口即可,不用反复重启。
调试时有个技巧:在extension.ts的激活函数里打console.log,输出会显示在宿主窗口的"调试控制台"里。别用vscode.window.showInformationMessage调试,弹窗会打断操作流。
4.2 读取暂存区 diff 的完整实现
这是整个扩展的数据源头,必须写扎实。核心逻辑是判断暂存区是否有内容,有就取暂存 diff,没有就提示用户。
import { exec } from 'child_process'; import { promisify } from 'util'; const execAsync = promisify(exec); async function getStagedDiff(repoPath: string, maxLines: number): Promise<string> { try { const { stdout } = await execAsync('git diff --cached --unified=3', { cwd: repoPath, maxBuffer: 1024 * 1024 * 10, }); if (!stdout.trim()) { throw new Error('暂存区为空,请先 git add 要提交的文件'); } const lines = stdout.split('\n'); if (lines.length > maxLines) { return lines.slice(0, maxLines).join('\n') + '\n...(diff 已截断)'; } return stdout; } catch (err) { throw new Error(`读取 diff 失败: ${(err as Error).message}`); } }--unified=3控制上下文行数,3 行是默认值,够模型理解改动位置又不至于太长。maxBuffer要调大,大仓库的 diff 可能超过默认的 1MB 限制,不设会直接报错。
提示:如果仓库用了 Git LFS,
git diff对大文件可能返回指针而非内容,这时候生成的提交信息会不准。可以在配置里加个开关,遇到 LFS 文件时只报文件名不报内容。
4.3 调用模型并解析返回
拿到 diff 后组装请求。以兼容 OpenAI 格式的接口为例,用fetch直接发就行,不必引第三方 SDK,减少依赖体积。
async function generateMessage(diff: string, config: Config): Promise<string> { const systemPrompt = `你是 Git 提交信息生成助手。根据代码改动生成一条 Conventional Commits 格式的提交信息。 格式:type(scope): description type 取值:feat/fix/docs/style/refactor/test/chore description 用${config.language === 'zh' ? '中文' : '英文'},祈使句,不超过 72 字符,结尾不加句号。 只输出提交信息本身。`; const res = await fetch(`${config.endpoint}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${config.apiKey}`, }, body: JSON.stringify({ model: config.model, temperature: 0.3, max_tokens: 150, messages: [ { role: 'system', content: systemPrompt }, { role: 'user', content: `改动如下:\n${diff}` }, ], }), }); const data = await res.json(); return data.choices[0].message.content.trim(); }返回结果要做清洗。模型有时会自作主张加反引号、加"提交信息:"前缀、或者输出多行。用正则把首尾的引号和代码块标记去掉,只取第一行有效内容。
4.4 把结果写回源代码管理输入框
生成完要填进 VS Code 的提交框。通过 Git API 的repo.inputBox.value直接赋值即可。
repo.inputBox.value = generatedMessage;如果用户已经手动输入了内容,别直接覆盖,弹个确认框问一下。这个细节很关键,我见过有人辛苦写了一半被扩展冲掉,直接卸载。
命令注册和菜单挂载在package.json里配好:
{ "contributes": { "commands": [ { "command": "commitAI.generate", "title": "AI 生成提交信息", "icon": "$(sparkle)" } ], "menus": { "scm/title": [ { "command": "commitAI.generate", "group": "navigation" } ] } } }这样源代码管理面板标题栏就会出现一个小图标,点一下触发。也可以绑定快捷键,在keybindings里配ctrl+alt+g之类不冲突的组合。
4.5 参数选择背后的计算逻辑
maxDiffLines设多少合适?这取决于模型的上下文窗口和成本。假设模型上下文 8k token,系统提示词加示例占 500 token,留给 diff 的约 7000 token。代码平均每行 10 个 token,那大概能放 700 行。但为了控制成本和延迟,200 行是个平衡点——大多数单次提交的改动不会超过这个量。
temperature为什么是 0.3 而不是 0?完全为 0 时模型容易陷入重复和死板,偶尔会生成奇怪的措辞。0.3 保留一点灵活性,又不至于跑偏。这个值我实测过 0.1 到 0.7 的范围,0.3 附近最稳。
5. 常见问题与排查技巧实录
5.1 生成结果为空或报错
最常见的原因是 API key 没配或配错。排查顺序:先看设置里 key 是否填了,再看 endpoint 地址对不对(有些服务商要带/v1,有些不带),最后看网络能否通。如果用的是需要特定请求头的服务,检查 header 是否完整。
另一个隐蔽原因是 diff 为空。用户以为改了文件,其实没保存,或者改了但没git add。扩展里要把这个错误明确提示出来,别笼统报"生成失败"。
5.2 生成的提交信息太长或格式不对
模型不听话,八成是 prompt 约束不够。检查三处:系统提示词里有没有明确"不超过 72 字符"、有没有给 few-shot 示例、max_tokens是不是设太大了。把max_tokens压到 100,模型想写长都没空间。
如果 type 总是判断错,比如把文档改动标成feat,可以在 prompt 里把每个 type 的适用场景列清楚。模型对明确规则的理解比模糊描述好得多。
5.3 中文生成出现中英混杂
这是中文用户的典型痛点。模型经常生成fix(auth): 修复token过期问题这种半中半英。解决办法是在 prompt 里明确:type 和 scope 保持英文,description 部分全部用中文,专有名词如 API、token 可保留英文。给个正例和反例对比,效果立竿见影。
5.4 大仓库下响应慢
diff 太大是主因。除了截断,还可以按文件类型过滤,比如忽略package-lock.json、*.min.js、图片等自动生成或二进制文件。这些文件的改动对理解提交意图没帮助,反而占满上下文。
const IGNORE_PATTERNS = [/package-lock\.json$/, /\.min\.(js|css)$/, /\.(png|jpg|svg)$/];在读取 diff 后按文件切分,过滤掉匹配的文件段再拼接。实测在大型前端仓库里,这一招能把 diff 体积砍掉一半以上。
5.5 常见问题速查表
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| 命令点了没反应 | 扩展未激活或命令未注册 | 检查 activationEvents 和 contributes.commands |
| 提示暂存区为空 | 文件没 add | 先 git add,或改用工作区 diff 模式 |
| 请求超时 | 网络或 endpoint 错误 | 检查地址、代理设置、服务商状态 |
| 生成内容带引号 | 模型输出未清洗 | 加正则去除首尾引号和代码块标记 |
| 中文变英文 | language 配置未生效 | 确认配置读取路径和默认值 |
| 覆盖了手写内容 | 未做覆盖确认 | 加 inputBox 非空判断和确认弹窗 |
注意:调试模型调用时,别把完整 diff 和 key 打到日志里。日志可能被收集或同步,泄露代码和密钥。要打就只打长度和文件数这类元信息。
6. 让工具真正融入日常提交习惯
工具做出来只是第一步,能不能坚持用才是关键。我的经验是把它嵌进固定动作里:改完代码,git add,然后顺手按快捷键生成,扫一眼没问题就提交。形成肌肉记忆后,写提交信息这件事几乎不占脑力。
还有几个可以继续打磨的方向。一是支持多候选,一次生成三条让用户挑,适合改动复杂、type 不好判断的场景。二是记住用户对生成结果的修改,比如你总把chore改成refactor,下次可以把这个偏好喂回 prompt。三是和 CHANGELOG 生成打通,提交规范了,发版时自动生成变更日志就是顺手的事。
我在实际使用中最大的体会是:提交信息的质量,反映的是你对这次改动想清楚了没有。AI 能帮你把话说漂亮,但"这次到底改了什么、为什么改"还得你自己心里有数。工具是放大器,不是替代品。把它当成一个帮你保持规范的助手,而不是替你思考的拐杖,用起来才踏实。