项目实战第十六天(下):拒绝 "fix bug"!手写一个 AI Git 提交助手,从此 Commit 优雅得像首诗 📝✨

5 阅读7分钟

前言: 各位全栈练习生们,欢迎回来!👋 刚刚我们搞定了 RAG 知识库,是不是觉得 AI 的能力深不可测?

现在,请大家摸着良心问自己一个问题:你平时的 Git Commit Message 是怎么写的? 是 fix bugupdate?还是 111asdf?🙈

如果你是团队里的 Leader,看到这样的提交记录,血压是不是已经上来了?😤 其实,写好 Commit Message 不仅是为了显得专业,更是为了团队协作、自动化生成 Changelog 以及日后的代码回溯。

但是!人都是懒惰的。与其每天盯着规范文档抓耳挠腮,不如让 AI 来帮我们写! 今天下半场,我们就来做一个 AI Git Commit 助手,丢进去一坨 git diff,吐出来一行标准的 Conventional Commits!🚀


🧐 为什么要死磕 Commit 规范?

在开始写代码之前,我们先来聊聊“为什么”。

很多新手(甚至老手)觉得 Commit Message 只要自己看得懂就行。错!大错特错!❌ 想象一下,三个月后线上出了 Bug,你需要回滚代码,打开 Git Log 一看:

fix
fix bug
update
final fix
really final fix

这一刻,你是不是想穿越回去掐死那个写这些 Log 的自己?💀

📜 Conventional Commits 国际公约

为了解决这个问题,业界大神们制定了 Conventional Commits(约定式提交) 规范。它的标准格式长这样:

<type>(<scope>): <subject>
// 空一行
<body>
// 空一行
<footer>

看起来很复杂?其实核心就是第一行:

  • type:告诉大家这次改动是什么类型的。
  • scope:(可选) 改动了哪个模块,比如 user-servicelogin-page
  • subject:简短描述改了啥,不要超过 50 个字符,要言简意赅。

🏷️ 常用 Type 速查表

为了让 AI 能够精准工作,我们需要先把这些规则“喂”给它。根据我们的 README.md,这里有一份大厂通用的 Type 列表:

  • feat: 新增功能 (Feature)
  • 🐛 fix: 修复 Bug
  • 📚 docs: 文档更新 (Documentation)
  • 💅 style: 代码格式调整(不影响逻辑,比如空格、分号,不是 CSS 样式哦)
  • ♻️ refactor: 代码重构(既不新增功能也不修 Bug)
  • perf: 性能优化 (Performance)
  • test: 增加或修改测试用例
  • 🏗️ build: 构建系统变动 (npm, webpack 等)
  • 🔧 chore: 杂项(构建过程或辅助工具的变动)
  • revert: 回滚提交

我们的目标,就是让 AI 像一个资深代码审核专家一样,分析你的代码变更,自动生成符合上述规范的提交信息。🎯


🛠️ 前端实战:打造丝滑的工具入口

好,理论储备完毕,开工!

第一步:入口配置 - Mine.tsx

我们要把这个功能放在个人中心里。打开 frontend/notes/src/pages/Mine.tsx,在刚才 RAG 的下面,再加一个入口。

<div 
  className="flex justify-between items-center py-2 border-b last:border-b-0 cursor-pointer hover:bg-gray-50 transition-colors"
  // 👇 点击跳转到 /git 路由
  onClick={() => navigate('/git')}
>
  <div className="flex items-center gap-2">
    {/* 加个小图标,显得精致一点 */}
    <span className="text-xl">🐙</span>
    <span>AI Git 助手</span>
  </div>
  <span className="text-gray-400 text-sm">&gt;</span>
</div>

别忘了去路由配置文件(App.tsxroutes.tsx)里把 /git 指向我们要创建的 Git 组件哦!


第二步:极简交互页面 - Git.tsx

这个页面逻辑很简单:左边放代码 Diff,点击按钮,右边(或下面)出结果。

打开 frontend/notes/src/pages/Git.tsx。这里我们用了 shadcn/ui 的组件,配合 lucide-react 的图标,瞬间逼格满满。

// 篇幅原因,只展示核心逻辑,UI 代码大家可以直接 copy
import { useGitStore } from '@/store/git';

const Git: React.FC = () => {
  // 🎣 从 Store 中获取状态
  const { loading, diff, setDiff, getCommit, commit } = useGitStore();

  const handleSubmit = async () => {
    if (!diff.trim()) return;
    
    // 🚀 触发 Store 中的 action
    await getCommit(diff);
  };

  return (
    // ... UI 布局代码 ...
    <textarea
      value={diff}
      onChange={(e) => setDiff(e.target.value)} // 📝 双向绑定 Diff 输入
      placeholder="请粘贴 git diff 后的内容..."
    />
    
    <Button onClick={handleSubmit} disabled={loading}>
      {loading ? 'AI 思考中...' : '生成 Commit 日志'}
    </Button>
    
    {/* ✨ 展示结果区域 */}
    {commit && (
      <div className="bg-gray-100 p-4 rounded">
         <p className="font-mono text-green-700">{commit}</p>
         {/* 一键复制功能 */}
         <button onClick={() => navigator.clipboard.writeText(commit)}>复制</button>
      </div>
    )}
    // ...
  )
}

第三步:状态管理的艺术 - store/git.ts

前端的核心逻辑依然交给 Zustand。这里我们需要处理一个典型的异步流程:loading -> fetch api -> save result

import { create } from 'zustand';
import { fetchCommit } from '@/api/git';

interface GitState {
   loading: boolean;
   diff: string;      // 用户输入的 git diff 内容
   commit: string;    // AI 生成的结果
   
   setLoading: (loading: boolean) => void;
   setDiff: (diff: string) => void;
   
   // ⚡ 核心 Action
   getCommit:(diff: string) => Promise<void>;
}

export const useGitStore = create<GitState>((set, get) => ({
    loading: false,
    diff: '',
    commit: '',
    
    setLoading: (loading) => set({ loading }),
    setDiff: (diff) => set({ diff }),
    
    getCommit: async(diff: string) => {
        // 1. 开始 Loading
        set({ loading: true });
        try {
            // 2. 调用 API
            const res = await fetchCommit(diff);
            console.log('AI 生成结果:', res);
            // 3. 存入 Store
            set({ commit: res });
        } catch (error) {
            console.error(error);
        } finally {
            // 4. 无论成功失败,结束 Loading
            set({ loading: false });
        }
    }
}))

💡 为什么要这么写? 把业务逻辑封装在 Store 里,View 层(页面)就只需要负责渲染和触发事件,完全不用关心“怎么发请求”、“怎么处理 loading 状态”这些脏活累活。这就是 关注点分离(Separation of Concerns)


第四步:对接后端 - api/git.ts

import instance from "./config";

export const fetchCommit = async (diff: string) => {
    // POST 请求,把 diff 传给后端
    const res = await instance.post('/ai/git', { diff });
    // 假设后端返回结构是 { result: "feat: add git ai tool" }
    return res.result;
}

🧠 后端实战:LangChain 的链式魔法

现在压力来到了后端。我们要用 NestJS 接收这个 diff,然后让 LangChain 把它变成符合规范的 Commit Message。

第五步:路由配置 - ai.controller.ts

@Post('git')
// 接收前端传来的 { diff: string }
async git(@Body() { diff }: { diff: string }) {
  // 调用 Service
  return this.aiService.git(diff);
}

简单明了。👉


第六步:Prompt Engineering 与 LCEL - ai.service.ts

这里是全篇最硬核的地方!我们将使用 LangChain 的 LCEL (LangChain Expression Language) 语法来构建处理链。

我们需要做三件事:

  1. Prompt Template:定义“人设”和“任务”。
  2. LLM:调用大模型。
  3. OutputParser:把 AI 的输出转成字符串。
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

// ... Inside AiService class ...

async git(diff: string) {
    // 1️⃣ 定义 Prompt 模板
    // 这是 Prompt Engineering 的关键!告诉 AI 它是谁,要干嘛,有什么约束。
    const prompt = ChatPromptTemplate.fromMessages([
      ["system", `你是资深代码审核专家。请根据用户提供的 git diff 内容生成一段
        符合 Conventional Commits 规范的提交日志。
        
        要求:
        1. 格式为 <type>(<scope>): <subject>。
        2. 保持简洁,不要废话。
        3. 不要输出 markdown 格式(如 \`\`\`),只输出纯文本。
        4. 如果改动很多,取最重要的一个。
      `],
      // {diff_content} 是一个占位符,稍后会被替换
      ["user", "{diff_content}"]
    ]);

    // 2️⃣ 构建 Chain (链)
    // 管道操作符 pipe 极其优雅:Prompt -> Model -> StringParser
    // 数据像水流一样流过这个管道
    const chain = prompt.pipe(this.chatModel).pipe(new StringOutputParser());

    // 3️⃣ 执行 Chain
    // 传入具体的 diff 内容
    const result = await chain.invoke({
      diff_content: diff
    });

    console.log('AI 生成的 Commit:', result);
    
    // 4️⃣ 返回结果
    return {
      result // 直接返回字符串给 Controller
    }
}

🔍 硬核解析:

  • System Message(系统提示词): 这是给 AI 的“上帝指令”。我们不仅设定了角色(资深代码审核专家),还明确了输出格式(Conventional Commits)。最重要的是,我们加了负面约束(Negative Constraints),比如“不要输出 markdown”,这对 LLM 非常重要,否则它经常会给你画蛇添足加个代码块包裹。

  • LCEL (Prompt.pipe.pipe): 如果你用过 Linux 管道命令(|),对这个肯定不陌生。

    • prompt 负责把输入的 diff 塞进模板,生成完整的提示词对象。
    • this.chatModel 接收提示词对象,进行推理,吐出 AIMessage 对象。
    • StringOutputParserAIMessage 里的 content 提取出来,转成纯字符串。 这种声明式的写法,比传统的 await model.call(prompt) 更加清晰、易读,也更容易扩展(比如中间加个日志记录、或者输出格式校验)。

🎉 效果演示与总结

开发完成后,让我们来试一下。

假设我在项目里修改了 README.md,加了一行关于 Git 工具的说明。 我在终端运行 git diff,复制内容,粘贴到我们的网页里,点击生成。

AI 可能会输出:

docs(readme): add introduction for AI git tool and conventional commits

完美!🎉

通过今天这个实战,我们不仅复习了 React + NestJS 的全栈流程,更深入实践了 Prompt EngineeringLangChain LCEL 语法。

从此以后,你的 Git Log 将变得整整齐齐,像军队一样规范。你的同事会夸你专业,你的 Leader 会给你点赞,甚至连你自己回顾代码时,都会被这份优雅感动!

全栈之路,道阻且长,但有 AI 相伴,我们不仅走得快,还能走得帅!😎

点个赞再走吧!下期见!👋


代码已在本地环境测试通过,Happy Coding!