构建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 数据流设计
用户操作流程如下:
- 用户执行
git diff命令,复制输出内容。 - 粘贴到前端文本框,点击"生成 Commit 日志"按钮。
- 前端将 diff 通过 API 发送到后端。
- 后端构建提示词,调用 DeepSeek 模型。
- 模型返回生成的 commit 信息,后端返回给前端。
- 前端展示结果,用户可复制使用。
整个过程状态由 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 字段,然后调用 AiService 的 git 方法处理,并直接返回结果。
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:修复 bugdocs:文档更新style:代码格式调整(不影响功能)refactor:重构(既不是新增功能也不是修复 bug)test:增加测试chore:构建过程或辅助工具变动
- scope:影响范围,例如模块名、组件名等(可选)。
- subject:简短描述,使用一般现在时,首字母小写,结尾不加句号。
例如:
scss
feat(auth): add JWT token validation
fix(parser): handle empty input correctly
5.2 提示词工程
为了引导 AI 生成符合规范的提交信息,提示词的设计至关重要。我们的系统消息包含以下几个关键点:
- 角色设定:"你是一个资深的代码审查专家"------让 AI 从专业角度理解代码变更。
- 任务描述:"根据用户提供的 git diff 内容,生成符合 Conventional Commits 规范的 commit 提交日志"------明确目标。
- 格式约束 :
<type>(scope): <subject>,并说明"保持简洁,不超过 50 个字符"------这符合常规的 subject 长度限制。 - 输出格式:"不要输出 markdown 格式,只输出纯文本"------防止 AI 添加额外的格式符号,确保结果可以直接使用。
这种提示词设计遵循了 CLEAR 原则(Concise, Logical, Explicit, Adaptive, Reflective),能够有效引导模型输出预期结果。
5.3 模型选择与参数调优
我们选择 DeepSeek 模型,因其在代码理解和生成任务上表现出色,且支持中文和英文混合输入。参数 temperature=0.3 是一个折中值,既保证了一定程度的确定性,又允许一些灵活性。如果希望输出更严格遵循规范,可以降低到 0.1;如果希望更多样化,可以提高到 0.5 以上。但根据测试,0.3 左右的效果最佳。
六、前后端联调与测试
6.1 联调流程
- 启动后端服务(NestJS 默认端口 3000)。
- 启动前端服务(Next.js 或 Vite,假设端口 3001)。
- 前端通过环境变量配置 API 地址,例如
NEXT_PUBLIC_API_BASE=http://localhost:3000。 - 用户在前端粘贴 diff 并点击生成,前端发送 POST 请求到后端
/ai/git。 - 后端处理并返回结果,前端展示。
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 赋能开发的新可能!