构建Git AI提交助手:从零到全栈实现的学习笔记

构建Git AI提交助手:从零到全栈实现的学习笔记

前言

在日常开发中,Git 提交信息的质量直接影响代码审查效率和项目可维护性。但许多开发者(包括我自己)常常因为时间紧迫或规范意识不足,写出诸如"fix bug"、"update"之类的无效信息。为了解决这一痛点,我开发了一款基于 AI 的 Git 提交助手,它能够根据 git diff 内容自动生成符合 Conventional Commits 规范的提交信息。本文将从需求分析、技术选型、前后端实现到 AI 集成,完整记录这一过程,并深入解析核心代码,希望能为同样致力于提升开发效率的读者提供参考。

一、为什么需要 AI 辅助 Git 提交?

1.1 传统 Git 提交的三大难题

时间成本:编写高质量的提交信息需要思考如何准确描述变更内容。对于复杂的功能开发,组织语言可能需要数分钟,打断开发流。

规范落地难:团队约定规范(如 Angular 规范)往往依赖成员自觉,实际执行中经常出现格式混乱、信息不全的情况。

信息丢失:提交信息通常只记录"做了什么",忽略了"为什么这么做"和"影响范围",导致几个月后回溯代码时难以理解当时的设计意图。

1.2 AI 赋能的可行性

大语言模型(LLM)具备理解代码语义和生成自然语言的能力。通过分析 git diff,模型可以识别出文件修改类型(新增、删除、重构)、模块范围、关键变更点,然后生成符合规范的描述。例如,检测到工具类新增了 validateEmail 函数,AI 可以生成:

scss 复制代码
feat(utils): add email validation function

这种能力使得自动化提交信息成为可能,并且可以保证格式一致、描述准确。

二、技术栈与项目架构

本项目采用前后端分离架构,核心技术栈如下:

层次 技术选型
前端 React + TypeScript + Zustand + TailwindCSS + Lucide React
后端 NestJS + LangChain.js + DeepSeek API
AI 模型 DeepSeek-R1(通过 LangChain 调用)

2.1 系统模块划分

整个系统包含以下核心模块:

  • Diff 输入模块 :前端提供文本框,用户粘贴 git diff 输出。
  • 状态管理模块:使用 Zustand 管理 diff、loading、commit 结果等状态。
  • API 调用模块:前端通过 Axios 将 diff 发送到后端接口。
  • AI 生成模块:后端使用 LangChain 构建提示词模板,调用 DeepSeek 模型生成 commit 信息。
  • 结果展示模块:前端展示生成的 commit 信息,并提供复制功能。

2.2 数据流设计

用户操作流程如下:

  1. 用户执行 git diff 命令,复制输出内容。
  2. 粘贴到前端文本框,点击"生成 Commit 日志"按钮。
  3. 前端将 diff 通过 API 发送到后端。
  4. 后端构建提示词,调用 DeepSeek 模型。
  5. 模型返回生成的 commit 信息,后端返回给前端。
  6. 前端展示结果,用户可复制使用。

整个过程状态由 Zustand store 统一管理,确保组件间数据同步。

三、前端实现详解

3.1 组件结构

前端主要包含一个 Git 组件,负责整体布局。该组件由以下几部分组成:

  • Header:显示页面标题和返回按钮。
  • 文本输入区textarea 用于粘贴 diff。
  • 操作按钮:带加载状态的"生成"按钮。
  • 结果展示区:显示生成的 commit 信息,包含复制按钮和模型标识。

下面我们逐一分析关键代码。

3.2 核心组件代码解析

Git.tsx

tsx 复制代码
import Header from '@/components/Header';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import { useGitStore } from '@/store/git';

const Git: React.FC = () => {
  const { loading, diff, setLoading, setDiff, getCommit, commit } = useGitStore();

  const handleSubmit = async () => {
    if (!diff.trim()) return;
    setLoading(true);
    try {
      await getCommit(diff);
    } catch (error) {
      console.error('生成失败:', error);
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <Header title="git提交助手" showBackButton={true} />
      <div className="w-full max-w-md mx-auto p-6 bg-white rounded-xl shadow-sm border border-gray-100">
        <h3 className="text-sm font-semibold text-gray-800 mb-4">Git Diff 代码片段</h3>

        <div className="space-y-4">
          <textarea
            value={diff}
            onChange={(e) => setDiff(e.target.value)}
            placeholder="粘贴你的 git diff..."
            className="w-full h-40 px-4 py-3 border border-gray-200 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent resize-none"
          />

          <Button
            onClick={handleSubmit}
            disabled={loading || !diff.trim()}
            className="w-full h-12 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-medium transition-all duration-200 shadow-md hover:shadow-lg"
          >
            {loading ? (
              <div className="flex items-center justify-center space-x-2">
                <Loader2 className="h-4 w-4 animate-spin" />
                <span>生成中...</span>
              </div>
            ) : (
              '生成 Commit 日志'
            )}
          </Button>
        </div>

        {commit && (
          <div className="mt-8 animate-fade-in">
            <div className="flex items-center justify-between mb-2">
              <h3 className="text-lg font-semibold text-gray-800 flex items-center gap-2">
                ✨ 生成结果
                <span className="text-xs font-normal px-2 py-0.5 bg-green-100 text-green-700 rounded-full">
                  DeepSeek-R1
                </span>
              </h3>
              <button
                onClick={() => navigator.clipboard.writeText(commit)}
                className="text-sm text-indigo-600 hover:text-indigo-800"
              >
                复制
              </button>
            </div>
            <div className="p-6 bg-white border border-gray-200 rounded-xl shadow-sm">
              <p className="font-mono text-gray-800 whitespace-pre-wrap">{commit}</p>
            </div>
          </div>
        )}
      </div>
    </div>
  );
};

export default Git;

代码解析

  • 状态获取 :通过 useGitStore 获取所有状态和操作函数,包括 loading(加载中)、diff(diff 内容)、setDiff(更新 diff)、getCommit(调用 API 生成 commit)和 commit(生成的结果)。
  • 表单提交handleSubmit 中首先检查 diff 是否为空,然后设置 loading 为 true,调用 getCommit,最后在 finally 中重置 loading。这样保证了无论成功或失败,加载状态都会关闭。
  • 按钮禁用:按钮在加载中或 diff 为空时禁用,防止无效提交。
  • 结果展示 :只有当 commit 存在时才显示结果区域。复制按钮调用 navigator.clipboard.writeText 将 commit 内容写入剪贴板。
  • 样式:使用 TailwindCSS 实现现代化 UI,包括渐变按钮、圆角边框、阴影等,提升用户体验。

3.3 状态管理(Zustand)

store/git.ts

ts 复制代码
import { create } from 'zustand';
import { fetchCommit } from '@/api/git';

interface GitState {
  loading: boolean;
  diff: string;
  commit: string;
  setLoading: (loading: boolean) => void;
  setDiff: (diff: string) => void;
  getCommit: (diff: string) => Promise<void>;
}

export const useGitStore = create<GitState>((set) => ({
  loading: false,
  diff: '',
  commit: '',
  setLoading: (loading) => set({ loading }),
  setDiff: (diff) => set({ diff }),
  getCommit: async (diff: string) => {
    const res = await fetchCommit(diff);
    set({ commit: res });
  },
}));

代码解析

  • 状态定义:包含三个状态变量和三个更新方法。
  • 异步操作getCommit 调用 API 函数 fetchCommit,并将返回结果通过 set 更新到 commit 中。
  • 类型安全:使用 TypeScript 接口定义状态类型,确保类型推断。

这种设计使得状态逻辑与 UI 解耦,便于测试和维护。

3.4 API 调用封装

api/git.ts

ts 复制代码
import axios from './config';

export const fetchCommit = async (diff: string) => {
  const res = await axios.post('/ai/git', { diff });
  return res.result;
};

axios 配置(简化):

ts 复制代码
// api/config.ts
import axios from 'axios';

const instance = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_BASE,
  timeout: 10000,
});

instance.interceptors.response.use(
  (response) => response.data, // 直接返回 data 部分
  (error) => Promise.reject(error)
);

export default instance;

这里通过响应拦截器直接返回 response.data,使得调用方可以直接获取后端返回的数据体。后端返回结构如 { result: '...' },因此 fetchCommit 返回 res.result

四、后端实现详解

4.1 NestJS 控制器

git.controller.ts

ts 复制代码
import { Controller, Post, Body } from '@nestjs/common';
import { AiService } from './ai.service';

@Controller('ai')
export class AiController {
  constructor(private readonly aiService: AiService) {}

  @Post('git')
  async git(@Body() { diff }: { diff: string }) {
    return this.aiService.git(diff);
  }
}

控制器接收 POST 请求,从请求体中解构出 diff 字段,然后调用 AiServicegit 方法处理,并直接返回结果。

4.2 AI 服务实现

ai.service.ts

ts 复制代码
import { Injectable } from '@nestjs/common';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { ChatDeepSeek } from '@langchain/deepseek'; // 假设的包名,实际可按需配置
import { StringOutputParser } from '@langchain/core/output_parsers';

@Injectable()
export class AiService {
  private chatModel: ChatDeepSeek;

  constructor() {
    // 初始化模型,实际应从配置读取 API key 等
    this.chatModel = new ChatDeepSeek({
      apiKey: process.env.DEEPSEEK_API_KEY,
      model: 'deepseek-chat',
      temperature: 0.3,
    });
  }

  async git(diff: string) {
    // 构建提示词模板
    const prompt = ChatPromptTemplate.fromMessages([
      [
        'system',
        `你是一个资深的代码审查专家,请根据用户提供的 git diff 内容,生成一段符合 Conventional Commits 规范的 commit 提交日志。
要求:
1. 格式为 <type>(scope): <subject>
2. 保持简洁,不超过 50 个字符(subject 部分)。
3. 不要输出 markdown 格式,只输出纯文本。`,
      ],
      ['user', '{diff_content}'],
    ]);

    // 构建处理链
    const chain = prompt.pipe(this.chatModel).pipe(new StringOutputParser());

    // 调用模型
    const result = await chain.invoke({
      diff_content: diff,
    });

    // 返回结果
    return { result };
  }
}

代码解析

  • 模型初始化 :创建 ChatDeepSeek 实例,设置 API 密钥和模型名称。temperature 控制输出的随机性,值越低输出越确定。
  • 提示词模板 :使用 ChatPromptTemplate.fromMessages 构建消息列表。系统消息定义了 AI 的角色和任务,并明确输出格式要求;用户消息中通过 {diff_content} 占位符,实际调用时替换为传入的 diff。
  • 处理链prompt.pipe(model).pipe(parser) 构建了一个链式处理流程:先格式化提示词,然后传入模型,最后通过 StringOutputParser 提取模型返回的文本内容。这种链式调用是 LangChain 的典型用法,清晰且易于扩展。
  • 调用chain.invoke 传入变量,得到生成的 commit 信息。
  • 返回 :包装为 { result } 对象返回给控制器。

4.3 关于 LangChain 的更多说明

LangChain 是一个用于构建 LLM 应用的框架,它提供了许多实用组件:

  • 提示词模板:支持动态替换变量,并自动格式化为模型所需的对话格式。
  • 输出解析器:将模型返回的原始内容(可能包含额外元数据)解析为字符串、JSON 等。
  • 链式组合:可以轻松地将多个组件串联,形成处理流水线。

在我们的服务中,使用 LangChain 使得代码结构清晰,同时也便于未来更换模型或增加后处理逻辑(例如对生成结果进行正则校验)。

五、Conventional Commits 规范与提示词设计

5.1 规范详解

Conventional Commits 规范是一种轻量级约定,其核心格式为:

xml 复制代码
<type>(<scope>): <subject>
  • type :提交类型,常用值包括:
    • feat:新功能
    • fix:修复 bug
    • docs:文档更新
    • style:代码格式调整(不影响功能)
    • refactor:重构(既不是新增功能也不是修复 bug)
    • test:增加测试
    • chore:构建过程或辅助工具变动
  • scope:影响范围,例如模块名、组件名等(可选)。
  • subject:简短描述,使用一般现在时,首字母小写,结尾不加句号。

例如:

scss 复制代码
feat(auth): add JWT token validation
fix(parser): handle empty input correctly

5.2 提示词工程

为了引导 AI 生成符合规范的提交信息,提示词的设计至关重要。我们的系统消息包含以下几个关键点:

  1. 角色设定:"你是一个资深的代码审查专家"------让 AI 从专业角度理解代码变更。
  2. 任务描述:"根据用户提供的 git diff 内容,生成符合 Conventional Commits 规范的 commit 提交日志"------明确目标。
  3. 格式约束<type>(scope): <subject>,并说明"保持简洁,不超过 50 个字符"------这符合常规的 subject 长度限制。
  4. 输出格式:"不要输出 markdown 格式,只输出纯文本"------防止 AI 添加额外的格式符号,确保结果可以直接使用。

这种提示词设计遵循了 CLEAR 原则(Concise, Logical, Explicit, Adaptive, Reflective),能够有效引导模型输出预期结果。

5.3 模型选择与参数调优

我们选择 DeepSeek 模型,因其在代码理解和生成任务上表现出色,且支持中文和英文混合输入。参数 temperature=0.3 是一个折中值,既保证了一定程度的确定性,又允许一些灵活性。如果希望输出更严格遵循规范,可以降低到 0.1;如果希望更多样化,可以提高到 0.5 以上。但根据测试,0.3 左右的效果最佳。

六、前后端联调与测试

6.1 联调流程

  1. 启动后端服务(NestJS 默认端口 3000)。
  2. 启动前端服务(Next.js 或 Vite,假设端口 3001)。
  3. 前端通过环境变量配置 API 地址,例如 NEXT_PUBLIC_API_BASE=http://localhost:3000
  4. 用户在前端粘贴 diff 并点击生成,前端发送 POST 请求到后端 /ai/git
  5. 后端处理并返回结果,前端展示。

6.2 示例测试

假设我们有一个简单的代码变更:

diff 复制代码
diff --git a/src/utils/validation.ts b/src/utils/validation.ts
index 1234567..89abcde 100644
--- a/src/utils/validation.ts
+++ b/src/utils/validation.ts
@@ -10,6 +10,11 @@ export function isValidEmail(email: string): boolean {
   const re = /\S+@\S+\.\S+/;
   return re.test(email);
 }
+
+export function isValidPhone(phone: string): boolean {
+  const re = /^\d{11}$/;
+  return re.test(phone);
+}

用户将此 diff 粘贴到前端,点击生成,期望得到类似:

scss 复制代码
feat(utils): add phone number validation function

实际测试中,模型确实能够识别出新增了 isValidPhone 函数,并将其归类为 feat 类型,scope 为 utils,subject 准确描述。

6.3 错误处理与边界情况

目前代码缺少完善的错误处理,但实际生产中需要添加:

  • 前端:捕获 API 调用异常,显示错误提示;对空输入或无效 diff 给出警告。
  • 后端:捕获模型调用异常,返回友好的错误信息;对过长 diff 进行截断或分段处理(防止 token 超限)。

例如,可以在 getCommit 中添加 try-catch:

ts 复制代码
getCommit: async (diff: string) => {
  try {
    set({ loading: true });
    const res = await fetchCommit(diff);
    set({ commit: res });
  } catch (error) {
    set({ commit: '生成失败,请稍后重试。' });
    console.error(error);
  } finally {
    set({ loading: false });
  }
}

七、深度优化与扩展思路

7.1 提升生成质量

  • 增加上下文:如果 diff 包含多文件变更,模型可能难以确定主要变更点。可以尝试在提示词中加入"请识别主要变更并生成提交信息,如果有多个类型的变更,选择最重要的作为 type"。
  • 提供示例:在系统消息中附加一个或少量的示例,帮助模型更好地理解格式要求。
  • 后处理校验:对生成结果进行正则匹配,若格式不符,可尝试二次修正或提示用户手动修改。

7.2 性能优化

  • 缓存:后端可对相同的 diff 进行缓存,避免重复调用 API。可以使用 Redis 或内存缓存。
  • 流式输出:对于长 diff,模型生成可能需要几秒,可以考虑使用流式响应,前端逐步显示生成内容,提升用户体验。
  • 并发控制:防止用户短时间内多次点击,前端可禁用按钮,后端可做限流。

7.3 功能扩展

除了生成提交信息,还可以扩展以下功能:

  • 自然语言转 Git 命令 :例如用户输入"提交所有修改并推送到远程",AI 生成 git add . && git commit -m "..." && git push
  • 自动 CHANGELOG 生成:基于历史提交信息,按类型分类,生成 CHANGELOG.md
  • PR 描述生成:分析当前分支与目标分支的差异,自动生成 Pull Request 标题和描述。
  • 代码审查辅助:基于 diff 识别潜在问题(如安全漏洞、性能瓶颈),并给出建议。

7.4 与现有 Git 工作流集成

理想情况下,开发者不应离开终端。可以开发一个 CLI 工具,直接读取暂存区的 diff,调用后端 API,然后将生成的提交信息填充到默认的 commit 编辑器中。这样完全无缝集成到现有工作流。

例如,设计命令 git ai-commit,其伪代码:

bash 复制代码
#!/bin/bash
diff=$(git diff --cached)
commit_msg=$(curl -X POST http://api/ai/git -d "{\"diff\":\"$diff\"}" | jq -r '.result')
git commit -m "$commit_msg"

八、总结与心得体会

8.1 项目收获

通过这个项目,我深入实践了以下技术点:

  • React + Zustand 的轻量级状态管理方案。
  • NestJS 的模块化设计与依赖注入。
  • LangChain 的提示词模板与链式调用。
  • Conventional Commits 规范及其在自动化工具中的应用。
  • AI 集成 的基本模式:构建清晰提示词 + 解析模型输出。

更重要的是,我认识到 AI 并非取代开发者,而是作为助手,将我们从重复性、低价值的工作中解放出来,让我们更专注于创造性的任务。

8.2 对未来开发的思考

随着大语言模型的普及,类似这样的"AI+开发工具"会越来越多。未来,AI 将深度融入开发的各个环节:从代码编写、测试生成、提交管理到文档维护,甚至架构设计。作为开发者,我们需要思考如何更好地利用 AI 的能力,同时保持对代码质量和项目整体的掌控。

对于 Git 提交助手而言,它只是智能化开发工具的一个起点。我相信,随着技术的演进,AI 将能理解更深层次的业务逻辑和代码语义,提供更加智能的建议和自动化操作。

希望这份学习笔记对你有所帮助。如果你有任何问题或建议,欢迎在评论区留言交流。让我们一起探索 AI 赋能开发的新可能!

相关推荐
wuhen_n1 小时前
JavaScript 防抖与节流进阶:从原理到实战
前端·javascript
小灵不想卷1 小时前
LangChain4j 与 SpringBoot 整合
java·后端·langchain4j
百慕大三角1 小时前
AI Agent开发之向量检索:一篇讲清「稀疏 + 稠密 + Hybrid Search」怎么落地
前端·agent·ai编程
打瞌睡的朱尤1 小时前
Vue day11商品详细页,加入购物车,购物车
前端·javascript·vue.js
温言winslow2 小时前
Elpis NPM 包抽离过程
前端
用户600071819102 小时前
【翻译】Rolldown工作原理:模块加载、依赖图与优化机制全揭秘
前端
SuperEugene2 小时前
《对象与解构赋值:接口数据解包的 10 个常见写法》
前端·javascript
Mr_Xuhhh2 小时前
博客文章:HTML核心概念与常见标签速览
前端
undefinedType2 小时前
rails知识扫盲
数据库·后端·敏捷开发