引言
一句话概括:用 React + Express + Ollama,三分钟自动生成符合规范的 Git Commit Message!
在现代软件开发中,写好 Git 提交信息不仅是一种专业素养,更是团队协作、代码审查和项目追溯的关键。但现实是——很多人提交的都是 fix bug、update 这种毫无意义的信息 😩。
别担心!今天我们就来手把手打造一个 AI 驱动的 Git Commit Message 生成器,让你像资深工程师一样写出清晰、规范、专业的提交日志 ✨。
项目准备:工欲善其事,必先利其器
在开始编码前,我们需要准备好以下“武器库”:
1. 技术栈全景图
- 前端:React(Vite 脚手架) + TailwindCSS(样式) + Axios(HTTP 请求)
- 后端:Node.js + Express(Web 服务器)
- AI 引擎:Ollama(本地运行开源大模型) + LangChain(AI 编排框架)
- 模型选择:
deepseek-r1:8b(推理能力强,适合代码理解)
2. 必装工具
# 安装 Ollama(macOS / Linux / Windows)
curl -fsSL https://ollama.com/install.sh | sh
# 拉取 DeepSeek 模型(约 5GB,需耐心等待)
ollama pull deepseek-r1:8b
# 启动 Ollama 服务(默认监听 11434 端口)
ollama serve
💡 小贴士:确保
http://localhost:11434可访问,这是 Ollama 的 API 入口。
3. 项目初始化
# 创建项目根目录
mkdir git-commit-ai && cd git-commit-ai
# 初始化前端(使用 Vite + React + JS)
npm create vite@latest frontend -- --template react
cd frontend && npm install axios
# 初始化后端
mkdir ../server && cd ../server
pnpm init -y
echo '{ "type": "module" }' > package.json # 启用 ES Module
pnpm add express cors @langchain/ollama @langchain/core
pnpm add -D nodemon # 自动重启服务器
前后端分离架构解析
我们的项目采用经典的 前后端分离架构:
- 前端(
frontend/):运行在浏览器(http://localhost:5173) - 后端(
server/):运行在 Node.js(http://localhost:3000) - AI 服务(Ollama):运行在本地(
http://localhost:11434)
三者通过 HTTP 协议通信,形成完整闭环。
后端实现:Express + LangChain 构建 AI 接口
我们聚焦于 server/index.js —— 这是整个系统的“神经中枢”,负责接收前端请求、调用本地大模型、返回结构化响应。
第一步:引入依赖(逐行解读)
// langchain 支持ollama
import {ChatOllama} from '@langchain/ollama';
✅ 作用:从
@langchain/ollama包中导入ChatOllama类。
📦 背景:LangChain 是一个用于构建 LLM 应用的框架,它抽象了不同模型(OpenAI、Ollama、Anthropic 等)的调用方式。ChatOllama是专为 Ollama 设计的聊天模型封装器。
⚠️ 注意:必须确保已安装@langchain/ollama,否则会报Module not found。
// 引入提示词模板
import {ChatPromptTemplate} from '@langchain/core/prompts';
✅ 作用:用于构建结构化的对话提示(prompt)。
💡 为什么不用字符串拼接?
因为直接拼接容易出错(如忘记换行、角色混淆),而ChatPromptTemplate提供了类型安全、可复用、可测试的 prompt 构建方式。它支持system、human、ai三种消息角色,符合 OpenAI 的聊天格式标准。
// 输出格式化模板
import {StringOutputParser} from '@langchain/core/output_parsers';
✅ 作用:将大模型返回的复杂对象(如
{ content: "xxx", role: "assistant" })自动提取为纯字符串。
🔧 原理:LangChain 的链式调用(.pipe())中,每个环节处理一种数据格式。模型输出是AIMessage对象,而前端只需要字符串,所以用StringOutputParser做“翻译”。
// 引入后端框架
import express from 'express';
✅ 作用:创建 Web 服务器的核心库。Express 是 Node.js 最流行的轻量级 Web 框架,以中间件机制著称。
// 引入cors 中间件 处理跨域请求
import cors from 'cors';
✅ 作用:解决浏览器同源策略限制。
🌐 跨域场景:前端运行在http://localhost:5173,后端在http://localhost:3000,端口不同即跨域。若不启用 CORS,浏览器会直接拦截响应,即使服务器返回了 200。
第二步:配置 Ollama 模型(参数深挖)
const model = new ChatOllama({
baseUrl: 'http://localhost:11434', // ollama 服务器地址
model: 'deepseek-r1:8b',
temperature: 0.1, // 严格 0-1 0 最严格 1 最宽松
});
| 参数 | 说明 | 最佳实践 |
|---|---|---|
baseUrl | Ollama 的 HTTP API 地址。默认为 http://127.0.0.1:11434 | 若 Ollama 运行在 Docker 或远程机器,需修改为对应 IP |
model | 要加载的模型名称。必须已通过 ollama pull 下载 | 可替换为 qwen:7b、llama3 等,但需测试效果 |
temperature | 控制输出随机性。值越低越确定 | Commit Message 必须稳定,故设为 0.1;创意写作可用 0.7+ |
实验建议:尝试
temperature: 0(完全 deterministic),看是否每次输入相同 diff 都得到相同 commit。
第三步:搭建 Express 服务器(中间件详解)
const app = express();
app.use(express.json());
app.use(cors());
-
express():创建 Express 应用实例。 -
app.use(express.json()):- 作用:自动解析请求体中的 JSON 数据,并挂载到
req.body。 - 不加会怎样? →
req.body为undefined,无法获取前端传来的message。 - 原理:这是一个“中间件”(middleware),在请求到达路由处理器前执行。
- 作用:自动解析请求体中的 JSON 数据,并挂载到
-
app.use(cors()):-
开发模式:允许任意域名跨域(等价于
cors({ origin: '*' }))。 -
生产警告:绝对不要在生产环境使用!应明确指定前端域名,如:
app.use(cors({ origin: 'https://your-git-commit-app.com' }));
-
第四步:定义 /chat 接口(逐行拆解)
app.post('/chat', async (req, res) => {
📌 使用
POST方法,因为需要发送请求体(git diff内容可能很长,不适合放 URL 中)。
console.log(req.body, '/////')
🔍 调试技巧:打印原始请求体,便于排查前端是否传参正确。
const { message } = req.body;
if (!message || typeof message !== 'string') {
return res.status(400).json({ error: 'message 必填,必须是字符串' });
}
✅ 防御性编程:
- 防止
null、undefined、数字、对象等非法输入。- 返回 400 Bad Request 是 RESTful 规范的最佳实践。
- 使用
return立即终止函数,避免后续逻辑执行。
try {
const prompt = ChatPromptTemplate.fromMessages([
['system', '你是一个专业的代码审查员'],
['human', '{input}'],
]);
🧠 Prompt Engineering 核心:
system消息:设定 AI 的“人格”和任务目标。这里强调“专业代码审查员”,引导其关注代码变更的语义、影响范围、规范性。{input}:占位符,将在.invoke({ input: message })时被真实 diff 替换。- 为什么不用单条字符串? → 多轮对话结构更符合现代 LLM 的训练数据分布,效果更好。
const chain = prompt
.pipe(model)
.pipe(new StringOutputParser());
⛓️ LangChain Chain 机制:
.pipe(model):将格式化后的 prompt 发送给 Ollama 模型。.pipe(new StringOutputParser()):接收模型响应(AIMessage对象),只取.content字段作为字符串输出。- 链式调用优势:可轻松插入日志、缓存、重试等中间处理逻辑。
console.log('正在调用大模型')
⏳ 用户体验提示:告知开发者模型正在处理(实际项目中可移除或改为 debug 日志)。
const result = await chain.invoke({input: message});
🔄 异步调用:
invoke是 LangChain 推荐的同步式调用方法(尽管底层是异步)。- 传入的对象 key 必须与 prompt 中的占位符名一致(这里是
input)。
res.status(200).json({ reply: result });
✅ 成功响应:
- 状态码
200 OK表示请求成功。- 响应体为 JSON 格式:
{ reply: "feat(auth): add login validation" }。- 字段命名:
reply清晰表达这是 AI 的回复,避免与业务字段冲突。
} catch (e) {
res.status(500).json({ error: '调用大模型失败' });
}
🛡️ 错误兜底:
- 捕获所有异常(网络中断、Ollama 崩溃、模型加载失败等)。
- 返回
500 Internal Server Error,前端可据此显示友好错误。- 进阶建议:记录
e.message到日志系统(如 Winston),但不要暴露给前端(防信息泄露)。
前端实现:React Hook 封装 AI 调用(超细粒度解析)
自定义 Hook:useGitDiff.js
import { useState, useEffect } from 'react';
import { chat } from '../api/index.js';
📦 模块化思想:
chat函数抽离到api/目录,便于统一管理接口、添加拦截器(如 token、loading 全局控制)。
export const useGitDiff = () => {
const [content, setContent] = useState('');
const [loading, setLoading] = useState(false);
🧵 状态管理:
content:存储 AI 生成的 commit message。loading:控制 UI 加载状态,提升用户体验。
useEffect(() => {
(async () => {
setLoading(true);
const { data } = await chat('你好');
setContent(data.reply);
setLoading(false);
})()
}, [])
⚙️ 副作用详解:
空依赖数组
[]:确保只在组件首次挂载时执行一次(模拟componentDidMount)。IIFE(立即执行函数) :因
useEffect不支持async/await直接写法,故包裹一层。Loading 状态流:
- 请求开始 →
setLoading(true)- 等待响应(UI 显示 "loading.....")
- 响应到达 →
setContent(...)+setLoading(false)当前问题:硬编码
'你好'仅为测试。真实场景应传入git diff输出(后续通过文件读取或粘贴框实现)。
❗ 潜在 Bug:若组件在请求完成前卸载,
setContent会报 warning。修复方案:useEffect(() => { let isMounted = true; (async () => { setLoading(true); const { data } = await chat('你好'); if (isMounted) { setContent(data.reply); setLoading(false); } })() return () => { isMounted = false; }; }, []);
return {
loading,
content,
}
}
🎁 自定义 Hook 的价值:
- 将“获取 AI commit message”的逻辑封装成可复用单元。
- 组件只需
const { loading, content } = useGitDiff();即可消费状态。- 未来可轻松扩展:添加
error状态、refetch函数等。
主组件:App.jsx(极简主义之美)
import { useGitDiff } from './hooks/useGitDiff.js';
export default function App() {
const { loading, content } = useGitDiff();
return (
<div className="flex">
{loading ? 'loading.....' : content}
</div>
)
}
✨ 设计哲学体现:
- 无状态组件:不管理任何数据,只负责渲染。
- 单一职责:只做一件事——展示 commit message 或 loading。
- TailwindCSS:
className="flex"为后续布局扩展预留空间(如居中、响应式)。
补充内容:Express 工作原理与跨域(CORS)机制全解
在我们欣赏完 server/index.js 的优雅代码后,有必要停下来问一句:这些代码背后,到底发生了什么?
为什么一个简单的 app.post('/chat', ...) 就能接收前端请求?
为什么必须加 express.json() 才能读到 req.body?
为什么前端明明发了请求,浏览器却说“被阻止”?
答案藏在两个关键技术中:Express 的请求处理模型 和 浏览器的同源策略(CORS) 。下面,我们一层层揭开它们的面纱。
一、Express:Node.js 的 Web 通信中枢
Express 是 Node.js 生态中最经典、最轻量的 Web 应用框架。它的设计哲学是 极简 + 可组合,核心只做一件事:把 HTTP 请求路由到对应的处理函数,并返回响应。
1. 基础结构:app、listen、路由
const app = express(); // 创建 Express 应用实例
app.listen(3000, () => {
console.log('server is running on port 3000');
});
-
app是你的整个后端服务的“容器”。 -
listen(3000)表示:在本机的 3000 端口上启动一个 TCP 服务器,等待客户端(如浏览器、Postman、Axios)连接。 -
当你在浏览器输入
http://localhost:3000/hello:http是协议(规定通信规则)localhost是主机名(指向 127.0.0.1)3000是端口号(标识本机上的具体应用)/hello是路径(path),用于区分不同资源
💡 网站的本质:不是展示页面,而是提供服务。每一次 URL 访问,都是一次对“服务”的调用。
2. 路由与 CRUD
app.get('/hello', (req, res) => {
res.send('hello world');
});
-
app.get()定义了一个 GET 路由,用于“获取资源”。 -
(req, res) => { ... }是路由处理函数:req(Request):包含客户端发来的所有信息(URL、headers、body、IP 等)res(Response):用于构建并发送响应(状态码、headers、body)
HTTP 支持多种方法,对应不同的操作语义(即 CRUD):
GET:获取数据(无请求体,参数通常在 URL 查询字符串中)POST:创建数据(有请求体,如 JSON、表单)PUT/PATCH:更新数据DELETE:删除数据
在我们的项目中,前端需要发送一段 git diff 文本给后端,这属于“提交新内容”,因此使用 POST 方法。
3. 中间件(Middleware):Express 的灵魂
Express 的强大之处在于 中间件机制。你可以把它想象成一条装配流水线:
请求 → [中间件1] → [中间件2] → [路由处理器] → [中间件N] → 响应
每个中间件都可以:
- 读取/修改
req或res - 终止请求(如返回错误)
- 将控制权交给下一个中间件(调用
next())
关键中间件:express.json()
app.use(express.json());
-
问题:Express 默认不会自动解析请求体中的 JSON 字符串。
-
后果:如果你 POST 了
{ "message": "fix bug" },req.body会是undefined! -
解决方案:
express.json()是一个内置中间件,它会:- 检查请求头
Content-Type: application/json - 读取请求体的原始字符串
- 用
JSON.parse()转为 JavaScript 对象 - 挂载到
req.body
- 检查请求头
✅ 最佳实践:所有需要处理 JSON 的项目,第一行中间件就应该是
app.use(express.json())。
4. HTTP 状态码:API 的“表情包”
状态码是服务器向客户端传递意图的标准化语言。合理使用,能让前端精准判断结果:
| 类别 | 状态码 | 含义 | 使用场景 |
|---|---|---|---|
| 1xx | 100 Continue | 临时响应,继续发送 | 很少用 |
| 2xx(成功) | 200 OK | 请求成功 | 普通查询、操作成功 |
| 201 Created | 资源已创建 | POST 新增成功 | |
| 3xx(重定向) | 301 Moved Permanently | 永久重定向 | 域名迁移 |
| 4xx(客户端错误) | 400 Bad Request | 请求格式错误 | 缺少参数、类型不对 |
| 401 Unauthorized | 未认证 | Token 缺失 | |
| 403 Forbidden | 无权限 | 角色不足 | |
| 404 Not Found | 资源不存在 | 路径写错 | |
| 5xx(服务器错误) | 500 Internal Server Error | 服务器内部异常 | 代码崩溃、依赖失败 |
在我们的 /chat 接口中:
- 输入校验失败 →
400(用户的问题) - 模型调用异常 →
500(我们的责任)
这种明确的反馈,是专业 API 的标志。
5. 开发利器:nodemon 与 Apifox
-
nodemon:监听文件变化,自动重启服务器。避免每次改代码都要手动
Ctrl+C再node index.js。npx nodemon index.js -
Apifox / Postman:用于手动测试 API。例如:
- POST
http://localhost:3000/chat - Body:
{ "message": "console.log('test')" } - 查看是否返回 200 和 AI 生成的 commit message
- POST
🛠️ 调试技巧:在路由开头加
console.log(req.body),快速验证数据是否正确到达。
二、跨域(CORS):浏览器的安全围栏
现在,假设后端已完美运行在 http://localhost:3000,前端 React 应用运行在 http://localhost:5173(Vite 默认端口)。
当你在前端用 axios.post('http://localhost:3000/chat', ...) 发起请求时,很可能收不到任何响应——即使后端日志显示“收到了请求”!
这就是 跨域问题(Cross-Origin Resource Sharing, CORS) 。
1. 什么是“同源”?
浏览器规定:只有当 协议(protocol) + 域名(host) + 端口(port) 三者完全相同时,才视为“同源”。
| URL A | URL B | 是否同源 | 原因 |
|---|---|---|---|
http://localhost:5173 | http://localhost:3000 | ❌ 否 | 端口不同 |
https://api.example.com | http://api.example.com | ❌ 否 | 协议不同 |
http://www.example.com | http://example.com | ❌ 否 | 域名不同(子域也算不同) |
⚠️ 注意:
127.0.0.1和localhost在某些浏览器中也被视为不同源!
2. 同源策略(Same-Origin Policy)
这是浏览器的一项安全机制,目的是防止恶意网站窃取你的数据。
例如:
- 你登录了银行网站(
bank.com) - 同时打开了一个钓鱼网站(
evil.com) - 如果
evil.com能随意向bank.com发 AJAX 请求并读取响应,就能盗走你的账户信息!
因此,浏览器默认禁止脚本向非同源地址发起带凭证(cookie、token)的请求,或读取其响应。
🛑 关键点:跨域请求其实发出去了(后端能收到),但浏览器拦截了响应,不让前端 JavaScript 读取!
3. CORS:安全地开放跨域访问
CORS 是 W3C 制定的标准,允许服务器主动声明:“我信任哪些外部网站来访问我的资源”。
实现方式:在 HTTP 响应头中添加特定字段。
简单请求 vs 预检请求(Preflight)
-
简单请求(如 GET、POST with
application/json):-
浏览器直接发送请求
-
服务器需在响应中包含:
Access-Control-Allow-Origin: http://localhost:5173
-
-
非简单请求(如自定义 header、PUT/DELETE):
-
浏览器先发一个 OPTIONS 预检请求
-
询问服务器:“我打算发一个 POST + 自定义 header,你允许吗?”
-
服务器需在 OPTIONS 响应中说明允许的方法和头:
Access-Control-Allow-Origin: http://localhost:5173 Access-Control-Allow-Methods: POST, GET Access-Control-Allow-Headers: Content-Type, Authorization -
预检通过后,才发送真正的 POST 请求
-
在我们的项目中,前端发送的是 POST + Content-Type: application/json,属于简单请求,但仍需 Access-Control-Allow-Origin。
4. 在 Express 中启用 CORS
手动设置响应头很麻烦,所以社区提供了 cors 中间件:
pnpm add cors
import cors from 'cors';
app.use(cors()); // 允许所有来源跨域(开发阶段)
这行代码等价于自动添加:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: ...
但 生产环境绝不能用 * !应明确指定信任的前端域名:
app.use(cors({
origin: 'http://localhost:5173', // 或你的线上域名
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type'],
}));
✅ 重要顺序:
app.use(cors())必须写在app.use(express.json())之后、路由定义之前,否则可能不生效。
5. CORS 请求的完整流程(以本项目为例)
-
前端(
http://localhost:5173)发起 POST 请求到http://localhost:3000/chat -
浏览器自动在请求头中添加:
Origin: http://localhost:5173 -
Express 服务器收到请求,
cors()中间件检查Origin是否在白名单中 -
如果是,自动在响应头中加入:
Access-Control-Allow-Origin: http://localhost:5173 -
浏览器收到响应,检查该头是否匹配当前页面源
-
匹配成功 → 允许前端 JavaScript 读取
res.data -
匹配失败 → 抛出
CORS error,res为 undefined
🔍 调试技巧:打开浏览器 DevTools → Network → 点击请求 → 查看 Response Headers,确认是否有
Access-Control-Allow-Origin。
前后端通信的完整链路
现在,我们可以完整描述一次成功的 AI Commit Message 生成过程:
-
用户在前端点击“生成”
-
React 调用
useGitDiff(),触发chat('...diff...') -
Axios 发送 POST 请求到
http://localhost:3000/chat -
浏览器因跨域,自动添加
Origin头 -
Express 服务器:
- 通过
cors()验证并放行 - 通过
express.json()解析 body - 调用 LangChain + Ollama 生成回复
- 返回 200 + JSON 响应(含
Access-Control-Allow-Origin)
- 通过
-
浏览器验证 CORS 头通过
-
前端收到
data.reply,更新 UI 显示 commit message
每一步都不可或缺。理解这些底层机制,你才能真正掌控全栈开发!
项目运行测试验证
-
启动后端:
cd server && npx nodemon index.js -
启动前端:
cd frontend && npm run dev -
打开
http://localhost:5173,看到:你好!我是专业的代码审查员,请提供您的代码变更内容(git diff),我将为您生成规范的 commit message。
🎯 下一步优化:将
'你好'替换为真实的git diff输出!
总结:为什么这个项目值得你拥有?
| 优势 | 说明 |
|---|---|
| 本地 AI | 无需联网,数据隐私安全 |
| 规范提交 | 自动生成符合 Conventional Commits 的消息 |
| 全栈实践 | 覆盖 React、Express、LangChain、Ollama |
| 可扩展 | 轻松替换模型(如 qwen:7b)、添加历史记录 |
未来展望
- ✅ 读取本地
.git目录,自动获取git diff - ✅ 添加 commit 类型选择(feat / fix / docs...)
- ✅ 支持多语言 commit message
- ✅ 集成 VS Code 插件,一键生成并提交
最后的话:技术不是目的,提升开发体验和代码质量才是。
用 AI 辅助我们写出更好的 Git 提交,是对团队、对未来的尊重 ❤️。
快去试试吧!你的下一个 commit,将由 AI 赋能,专业如大师 👨💻✨
项目源码结构:
git-commit-ai/
├── frontend/
│ ├── src/
│ │ ├── hooks/useGitDiff.js
│ │ ├── api/index.js
│ │ └── App.jsx
└── server/
└── index.js