开篇:Commit 历史变成了 AI 的语录集
三个月前,我决定让 Cursor 帮我写所有 commit message。
理由是:AI 更擅长文字工作,让我专心写代码就行。听起来合理。
结果一周后,我打开项目 history 看到了这个:
$ git log --oneline -20
a3f2b1d feat: enhance user profile functionality and improve code organization
b4c2e3f fix: resolve issue with data processing pipeline
c5d4e3g refactor: optimize database query performance
d6e5f4h chore: update dependency versions
e7f6g5i feat: add new feature for user authentication module
f8g7h6j fix: correct bug in payment processing flow
g9h8i7k refactor: improve code structure and readability
h0i9j8l chore: update configuration files
i1j0k9m feat: implement changes to improve system reliability
j2k1l0n fix: address problem identified in recent testing
读了两行我就不想读了。这些 commit message 像同一台机器写的——因为它们确实是的。
每个 commit message 都包含"feat/fix/refactor/chore"前缀,都有"enhance/improve/resolve/optimize"这类听起来很专业但毫无信息的动词。
但真正让我觉得尴尬的不是这个——是当团队有人问我"你能不能解释一下那个支付流程的改动"时,我去翻 commit 记录,发现 AI 只写了"fix: correct bug in payment processing flow"。
没有具体改了哪里、为什么改、怎么改的。一个在 code review 里讨论了 45 分钟的变更,被 AI 浓缩成了一行没人看得懂的废话。
这篇文章是"教训分享"——我踩了坑,然后找到了更好的方式。
一、AI 写 commit message 的问题在哪
1.1 AI 擅长模式模仿,不擅长信息提取
我给 Cursor 的 commit 上下文是 git diff 的内容。AI 从 diff 中提取了什么信息?
—— diff 告诉 AI 的:
删除了什么文件
添加了什么代码
改了什么函数
—— diff 没有告诉 AI 的:
为什么要改这个
为什么选择这个方案而不是另一个
这个改动的风险是什么
要不要做特殊的部署操作
AI 能看到的只是"代码的变化",看不到"人的决策过程"。
1.2 具体的翻车案例
以下是我的项目中 AI 生成的 commit message 和实际情况的对比:
案例 1:AI 觉得我在"优化"
AI 写的: "refactor: optimize database query performance"
实际情况:
花了 3 小时把一个 100ms 的查询优化到了 15ms,
但把另一个不相关的模块搞坏了,
第二天又花 2 小时修。
AI 只看到了查询变快了——没看到排查过程、没看到 tradeoff、没看到"优化一个地方搞坏了另一个地方"的事实。这种 commit message 让人觉得改动很常规,但实际上是一场大型 debug 马拉松。
案例 2:AI 编造了"改动"
AI 写的: "fix: correct bug in data validation logic"
实际情况:
删了一行 lint warning 的注释,完全没改 data validation。
AI 从 diff 中看到删了"# TODO: fix validation",就脑补成"修复了验证 bug"。
案例 3:AI 把所有变化高度概括
AI 写的: "feat: implement changes to improve user experience"
实际情况:
改了 12 个文件,包括:
- 3 个真正的前端 UI 改进
- 4 个重构的常量名
- 2 个误改的错误文件
- 3 个格式化工具自动改的
AI 把所有改动塞进一句"improve user experience"。这不是说错了,而是信息量接近于零。
二、好的 commit message 长什么样
2.1 Conventional Commits 的真相
很多人觉得"feat/fix/refactor"这种格式就是好的 commit message。
不是的。Conventional Commits 只规定了前缀格式,没规定内容。
以下是两种 commit message 的对比:
❌ "feat: add new feature for user authentication module"
—— 用了正确的前缀,但信息量为零
✅ "feat(auth): add email+password login with bcrypt hashing
Why: 用户反馈只支持 SSO 登录不够灵活,
有些小团队没有自己的 SSO 系统。
Risk: 新的登录方式需要额外的安全审查。
密码重置流程在设计中(下一步任务 #142)。
Migration: 不需要,新功能和现有 SSO 登录并行运行。
"
关键区别:
| 维度 | AI 默认生成 | 好的 commit |
|---|---|---|
| 前缀 | ✅ 正确 | ✅ 正确 |
| 范围 | ✅ 有时有 | ✅ 有 |
| 为什么改 | ❌ 没有 | ✅ 有 |
| 改了哪里 | ❌ "优化/改进" | ✅ 具体文件名 + 描述 |
| 风险和注意事项 | ❌ 没有 | ✅ 需要知道的事 |
2.2 我自己总结的模板
经历那次尴尬后,我给自己定了一个 commit message 模板:
<type>(<scope>): <short description>
Why: <为什么需要这个改动,而不是具体改了什么>
What: <改了哪里,目录级别即可>
Risk: <这个改动的潜在影响>
- <如果有需要注意的地方写在这里>
这个模板强制我在 commit 时回答三个问题:
- Why:这个改动解决了什么问题?为什么用这个方案?
- What:改了什么范围?让 reviewer 知道去哪里看
- Risk:有什么需要注意的?有没有已知的影响?
# 使用模板的示例
$ git commit -m "fix(cart): prevent negative quantity when user spams button
Why: 用户快速点击减号按钮时,quantity 会变成负数
导致结算模块解析失败,返回 500 错误。
原因是减操作的防抖没覆盖到并发请求。
What: cart_controller.rb 中 quantity 变更的原子性检查
前端按钮的防抖间隔从 200ms 改为 500ms
Risk:
- 快速操作的用户可能会觉得响应变慢(500ms 间隔)
- 已和 PM 确认可以接受
"
三、让 AI 写更好的 commit message
3.1 第一版:用 Prompt 控制输出
我并没有完全放弃 AI 写 commit message——我放弃了"无脑用 AI 写"。改进方式:给 AI 更多的上下文。
#!/bin/bash
#
# ai-commit.sh - 生成更好的 AI commit message
# 用法: ai-commit.sh <branch-name>
BRANCH_NAME="${1:-$(git rev-parse --abbrev-ref HEAD)}"
DIFF_CONTENT=$(git diff --cached | head -2000)
CHANGED_FILES=$(git diff --cached --name-only)
GIT_LOG=$(git log --oneline -10)
STATS=$(git diff --cached --stat)
# 获取相关 issue 信息(如果有)
ISSUE_NUMBER=$(echo "$BRANCH_NAME" | grep -oE '[0-9]+' | head -1)
# 构建 prompt
PROMPT=$(cat <<EOF
你是一个 commit message 生成器。给定以下信息,生成 commit message。
分支名: $BRANCH_NAME
变更文件:
$CHANGED_FILES
变更统计:
$STATS
最近的 commit:
$GIT_LOG
$([ -n "$ISSUE_NUMBER" ] && echo "关联 Issue: #$ISSUE_NUMBER")
变更内容 (diff):
$DIFF_CONTENT
请生成 commit message。要求:
1. 格式: <type>(<scope>): <描述>
2. 必须有 Why 和 Risk 两部分
3. Why: 说明为什么要做这个改动(从 diff 推断)
4. Risk: 说明这个改动有什么风险或注意事项
5. 不要虚构信息。如果不知道 Why,就写"无法从 diff 中推断"
6. 不要用"enhance/improve/optimize"这种空洞的词
7. 用具体的函数名/文件名描述改动
EOF
)
# 调用 AI API(这里用 Claude API 为例)
curl -s https://api.anthropic.com/v1/messages \
-H "Content-Type: application/json" \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-d "{
\"model\": \"claude-sonnet-4-20250514\",
\"messages\": [{\"role\": \"user\", \"content\": \"$PROMPT\"}],
\"max_tokens\": 500
}" | jq -r '.content[0].text'
3.2 第二版:加入交互式检查
AI 给出的 commit message 不一定靠谱。加一个验证步骤:
#!/bin/bash
#
# verified-commit.sh - 生成 commit message + 人工确认
GENERATED_MSG=$(ai-commit.sh)
echo "===== AI 生成的 commit message ====="
echo "$GENERATED_MSG"
echo ""
echo "====================================="
echo ""
echo "你要用这个 commit message 吗?"
echo "y) 直接用"
echo "e) 编辑后再用"
echo "n) 不用,我自己写"
echo ""
read -r choice
case "$choice" in
y|Y)
echo "$GENERATED_MSG" > /tmp/commit_msg.txt
git commit -F /tmp/commit_msg.txt
;;
e|E)
echo "$GENERATED_MSG" > /tmp/commit_msg.txt
${EDITOR:-vim} /tmp/commit_msg.txt
git commit -F /tmp/commit_msg.txt
;;
*)
git commit
;;
esac
3.3 第三版:基于代码变更的类型自动生成
对不同类型的变更,使用不同的 prompt 模板:
#!/usr/bin/env python3
"""
smart-commit.py - 智能 commit message 生成器
根据变更类型使用不同的 prompt 模板,
避免 AI 生成"通用废话"
"""
import subprocess
import json
from typing import Dict, List
# 不同变更类型的 prompt 模板
PROMPT_TEMPLATES = {
"bugfix": """
你分析一个 bug fix 的 git diff。请生成 commit message。
重点关注:
1. bug 的具体表现(从 diff 推断)
2. root cause 是什么
3. 为什么用这个方式修复
4. 是否有回归风险
不要写:
- "修复了一个问题"(太笼统)
- "优化了代码"(如果是 bug fix 那就不是优化)
请用以下格式:
fix(<scope>): <描述>
Why: <bug 表现 + root cause>
Fix: <具体如何修复的>
Risk: <回归风险>
""",
"feature": """
你分析一个新功能的 git diff。请生成 commit message。
重点关注:
1. 新功能解决什么需求
2. 涉及哪些模块/文件
3. 有没有破坏性变更
不要编造"用户反馈"之类的话。如果不确定需求背景,写"从代码推断"。
请用以下格式:
feat(<scope>): <描述>
Why: <从代码推测的需求背景>
What: <新增/改了哪些关键文件>
Breaking: <如果有破坏性变更写在这里,没有就不写>
""",
"refactor": """
你分析代码重构的 git diff。请生成 commit message。
重点区分:
1. 纯重构(功能不变):写清楚重构了什么、为什么
2. 重构 + 小功能变化:两者都要写
3. 如果看起来是重构但其实是 bug fix:纠正分类
请用以下格式:
refactor(<scope>): <描述>
Why: <重构动机,如可维护性/性能/准备新功能>
Key Changes: <主要变化点>
Behavior Change: <如果功能行为有变化写在这里>
""",
}
def classify_diff(files: List[str], diff_stats: str) -> str:
"""根据变更内容分类”""
# 简单的启发式分类
bugfix_keywords = ["bug", "fix", "error", "crash", "null", "undefined", "broken", "incorrect"]
test_keywords = ["test", "spec"]
all_text = " ".join(files) + " " + diff_stats
if any(kw in all_text.lower() for kw in bugfix_keywords):
return "bugfix"
elif any(kw in all_text.lower() for kw in test_keywords):
return "test"
else:
return "feature"
def get_git_diff() -> Dict:
"""获取 git diff 信息"""
files = subprocess.check_output(
["git", "diff", "--cached", "--name-only"]
).decode().strip().split("\n")
diff_content = subprocess.check_output(
["git", "diff", "--cached"]
).decode()[:3000]
stats = subprocess.check_output(
["git", "diff", "--cached", "--stat"]
).decode().strip()
return {
"files": [f for f in files if f],
"diff": diff_content,
"stats": stats,
}
def build_prompt(diff_info: Dict, change_type: str) -> str:
"""构建 prompt"""
template = PROMPT_TEMPLATES.get(change_type, PROMPT_TEMPLATES["feature"])
return f"""
变更文件:
{chr(10).join(diff_info['files'])}
变更统计:
{diff_info['stats']}
变更内容 (前 3000 字符):
{diff_info['diff']}
---
{template}
"""
# 主函数
if __name__ == "__main__":
diff_info = get_git_diff()
change_type = classify_diff(diff_info["files"], diff_info["stats"])
prompt = build_prompt(diff_info, change_type)
# 输出 prompt(实际使用时会调用 AI API)
print(f"变更类型: {change_type}")
print("=" * 50)
print(prompt)
四、一个真实的改善案例
4.1 改善前后的对比
用改进后的方案,同一个改动的 commit message:
改善前(纯 AI 生成):
fix: correct bug in data processing pipeline
改善后(模板 + AI + 人工确认):
fix(pipeline): handle nil UserSession in DataProcessor.process
Why: 用户在某些场景下没有 UserSession 对象
(API token 认证的场景),但 DataProcessor 假设
它始终存在,导致 SystemStackError。
bug 日志: https://sentry.io/xxx/yyy
Fix: DataProcessor#process 中加入 nil check,
如果 UserSession 不存在则跳过 session 相关处理
Risk: 低。跳过 session 处理不影响核心数据流程。
已添加测试覆盖 nil session 的场景。
这不仅仅是一个更长的 commit message——它是一个更好的记录。6 个月后如果有人回来看这个 commit,能知道当时发生了什么、为什么那么改。
4.2 团队的实际效果
我们团队 5 个人用了这个模板 + AI 辅助方案 2 个月后,统计了一些数据:
| 指标 | 之前 | 之后 |
|---|---|---|
| commit message 包含 Why 的比例 | ~5% | ~80% |
| bug 回溯时靠 commit 定位到准确原因 | 几乎不可能 | 经常可以 |
| code review 中的"这是什么"问题 | 每月 10+ 次 | 每月 2-3 次 |
| commit message 平均长度 | 8 个词 | 35 个词 |
最明显的改善不是 commit message 本身,而是减少了团队沟通的成本——很多"这是个什么改动"的问题,reviewer 可以从 commit message 直接找到答案。
五、我的观点:Commit message 是写给未来的自己
我知道有人会觉得花时间写 commit message 是浪费时间。"代码说了算"、"commit message 没人看"。
但我见过太多场景:
- 深夜 debug 一个 3 个月前引入的 bug
- 翻 git blame 看到代码改了但不知道为什么
- 看到一个"优化性能"的 commit,但代码除了格式化什么都没动
- 版本回滚时,完全不知道哪些 commit 是"安全的回滚点"
好的 commit message 不是在写历史,是在写给未来的自己。 6 个月后的你,不会记得为什么改了这个文件——但如果 commit message 里有 Why,你就省了 30 分钟的重构思路重建。
AI 写 commit message 这件事本身没错。错的是:
- 让 AI 帮你想"为什么" ——它不知道,它只能编
- 盲目相信 AI 的概括 ——它看到的只是一个 diff,看不到你的决策过程
- 把 commit message 变成形式主义 ——"feat: add feature"这种写法还不如不写
5.1 什么时候 AI 写 commit 是有价值的
虽然我有诸多不满,AI 写 commit 在某些场景下确实有价值。
适合 AI 的场景:
1. 纯粹的技术重构或依赖升级
- 没有功能变化,只是改依赖版本或重构命名
- 例如:chore(deps): bump react from 18.2.0 to 18.3.1
-
大量的跨文件重命名
- 几十个文件的统一改名,AI 可以从 diff 中准确提取
- 人工写反而容易漏 -
格式化/代码风格统一
- 引入 linter、格式调整等
不适合 AI 的场景:
1. 复杂 bug fix
- 需要记录排查过程和 root cause
- 这些信息 diff 里没有
-
有 Tradeoff 的设计决策
- 做了选择但必须记录为什么这样选 -
涉及团队协作的改动
- 比如:为了接另一个团队的 API 做的改动
- Reviewer 需要知道这个改动的背景
5.2 git commit-ai 插件思路
受这次尝试启发,我做了一个 git alias,可以在 commit 时灵活调用 AI:
# ~/.gitconfig
[alias]
# 快速描述:让 AI 描述改了什么(不含 why/risk)
describe = !git diff --cached | head -2000 | xargs -0 sh -c 'ai-commit-summary "$@"'
# 完整模板:生成模板后自动打开编辑器
commit-ai = !ai-commit --edit
# 只生成 scope 和 type 建议
suggest-type = !python3 -c "
import subprocess
files = subprocess.check_output(['git', 'diff', '--cached', '--name-only']).decode()
if 'test' in files: print('test')
elif 'config' in files: print('chore')
elif 'spec' in files: print('test')
else: print('feat or fix - check yourself')
"
这样每次 commit 时,可以选择纯自己写(git commit)、AI 描述改动后我补充 Why(git commit-ai)、AI 全写后我审核(git describe)。
5.3 AI commit 质量检查表
如果你坚持让 AI 写 commit message,至少做以下检查:
| 检查项 | 合格标准 | 不合格(需要重写) |
|---|---|---|
| Why 是否合理 | 能从 diff 推断出动机 | "improve performance" 但看不出哪里快了 |
| 具体文件 | 提到了改动的主要文件 | "优化了多个模块" |
| 风险提示 | 写了可能的影响范围 | 没写风险或只说"低风险" |
| 动词准确 | fix/add/change/remove | enhance/optimize/improve |
| 不编造信息 | 实事求是 | "用户反馈"但实际没这回事 |
这条检查表用了一个月后,我发现自己能用 AI 生成可用 commit message 的比例从 20% 提到了 60%。剩下的 40% 要么需要我补充 Why,要么 AI 完全理解错了。
这就是"AI 辅助"和"AI 替代"的差别——AI 辅助意味着你仍然是决策者,AI 只是加快了你描述的过程。
我现在的方式是:
- AI 生成"改了哪里"的部分
- 我自己补充"为什么改"和"风险"的部分
- 再让 AI 检查格式
这样既发挥了 AI 的优势(快速描述改动),也保持了我的核心职责(解释决策过程)。
结尾
让 AI 写 commit message 不可怕。可怕的是让它替你想。
AI 应该负责文字工作,而你负责思考工作。
下次写 commit message 时,问自己一个问题:如果 6 个月后有人看到这个 commit,他能理解当时发生了什么吗?如果答案是不确定,那就多写两句 Why。
文章由文字工作者编写。prompt 模板基于 Claude API 实测效果迭代。git 脚本在 macOS/Linux 环境下测试。