打造你的 Git 提交 AI 神器:从零实现前后端分离的 Commit Message 生成器

0 阅读16分钟

 引言

一句话概括:用 React + Express + Ollama,三分钟自动生成符合规范的 Git Commit Message!

在现代软件开发中,写好 Git 提交信息不仅是一种专业素养,更是团队协作、代码审查和项目追溯的关键。但现实是——很多人提交的都是 fix bugupdate 这种毫无意义的信息 😩。

别担心!今天我们就来手把手打造一个 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 构建方式。它支持 systemhumanai 三种消息角色,符合 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 最宽松
});
参数说明最佳实践
baseUrlOllama 的 HTTP API 地址。默认为 http://127.0.0.1:11434若 Ollama 运行在 Docker 或远程机器,需修改为对应 IP
model要加载的模型名称。必须已通过 ollama pull 下载可替换为 qwen:7bllama3 等,但需测试效果
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.bodyundefined,无法获取前端传来的 message
    • 原理:这是一个“中间件”(middleware),在请求到达路由处理器前执行。
  • 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 必填,必须是字符串' });
  }

防御性编程

  • 防止 nullundefined、数字、对象等非法输入。
  • 返回 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 状态流

    1. 请求开始 → setLoading(true)
    2. 等待响应(UI 显示 "loading.....")
    3. 响应到达 → 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。
  • TailwindCSSclassName="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] → 响应

每个中间件都可以:

  • 读取/修改 reqres
  • 终止请求(如返回错误)
  • 将控制权交给下一个中间件(调用 next()

关键中间件:express.json()

app.use(express.json());
  • 问题:Express 默认不会自动解析请求体中的 JSON 字符串。

  • 后果:如果你 POST 了 { "message": "fix bug" }req.body 会是 undefined

  • 解决方案express.json() 是一个内置中间件,它会:

    1. 检查请求头 Content-Type: application/json
    2. 读取请求体的原始字符串
    3. JSON.parse() 转为 JavaScript 对象
    4. 挂载到 req.body

最佳实践:所有需要处理 JSON 的项目,第一行中间件就应该是 app.use(express.json())

4. HTTP 状态码:API 的“表情包”

状态码是服务器向客户端传递意图的标准化语言。合理使用,能让前端精准判断结果:

类别状态码含义使用场景
1xx100 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+Cnode index.js

    npx nodemon index.js
    
  • Apifox / Postman:用于手动测试 API。例如:

    • POST http://localhost:3000/chat
    • Body: { "message": "console.log('test')" }
    • 查看是否返回 200 和 AI 生成的 commit message

🛠️ 调试技巧:在路由开头加 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 AURL B是否同源原因
http://localhost:5173http://localhost:3000❌ 否端口不同
https://api.example.comhttp://api.example.com❌ 否协议不同
http://www.example.comhttp://example.com❌ 否域名不同(子域也算不同)

⚠️ 注意127.0.0.1localhost 在某些浏览器中也被视为不同源!

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 请求的完整流程(以本项目为例)

  1. 前端(http://localhost:5173)发起 POST 请求到 http://localhost:3000/chat

  2. 浏览器自动在请求头中添加:

    Origin: http://localhost:5173
    
  3. Express 服务器收到请求,cors() 中间件检查 Origin 是否在白名单中

  4. 如果是,自动在响应头中加入:

    Access-Control-Allow-Origin: http://localhost:5173
    
  5. 浏览器收到响应,检查该头是否匹配当前页面源

  6. 匹配成功 → 允许前端 JavaScript 读取 res.data

  7. 匹配失败 → 抛出 CORS errorres 为 undefined

🔍 调试技巧:打开浏览器 DevTools → Network → 点击请求 → 查看 Response Headers,确认是否有 Access-Control-Allow-Origin


前后端通信的完整链路

现在,我们可以完整描述一次成功的 AI Commit Message 生成过程:

  1. 用户在前端点击“生成”

  2. React 调用 useGitDiff(),触发 chat('...diff...')

  3. Axios 发送 POST 请求到 http://localhost:3000/chat

  4. 浏览器因跨域,自动添加 Origin

  5. Express 服务器:

    • 通过 cors() 验证并放行
    • 通过 express.json() 解析 body
    • 调用 LangChain + Ollama 生成回复
    • 返回 200 + JSON 响应(含 Access-Control-Allow-Origin
  6. 浏览器验证 CORS 头通过

  7. 前端收到 data.reply,更新 UI 显示 commit message

每一步都不可或缺。理解这些底层机制,你才能真正掌控全栈开发!


项目运行测试验证

  1. 启动后端:

    cd server && npx nodemon index.js
    
  2. 启动前端:

    cd frontend && npm run dev
    
  3. 打开 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

项目源码地址: lesson_zp/ai/app/git-differ: AI + 全栈学习仓库