开篇: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 这件事本身没错。错的是:

  1. 让 AI 帮你想"为什么" ——它不知道,它只能编
  2. 盲目相信 AI 的概括 ——它看到的只是一个 diff,看不到你的决策过程
  3. 把 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

  1. 大量的跨文件重命名
    - 几十个文件的统一改名,AI 可以从 diff 中准确提取
    - 人工写反而容易漏

  2. 格式化/代码风格统一
    - 引入 linter、格式调整等

不适合 AI 的场景
1. 复杂 bug fix
- 需要记录排查过程和 root cause
- 这些信息 diff 里没有

  1. 有 Tradeoff 的设计决策
    - 做了选择但必须记录为什么这样选

  2. 涉及团队协作的改动
    - 比如:为了接另一个团队的 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 环境下测试。