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

引言

一句话概括:用 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. 必装工具

bash 复制代码
# 安装 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. 项目初始化

bash 复制代码
# 创建项目根目录
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 ------ 这是整个系统的"神经中枢",负责接收前端请求、调用本地大模型、返回结构化响应。


第一步:引入依赖(逐行解读)

javascript 复制代码
// langchain 支持ollama
import {ChatOllama} from '@langchain/ollama';

作用 :从 @langchain/ollama 包中导入 ChatOllama 类。

📦 背景 :LangChain 是一个用于构建 LLM 应用的框架,它抽象了不同模型(OpenAI、Ollama、Anthropic 等)的调用方式。ChatOllama 是专为 Ollama 设计的聊天模型封装器。

⚠️ 注意 :必须确保已安装 @langchain/ollama,否则会报 Module not found

javascript 复制代码
// 引入提示词模板
import {ChatPromptTemplate} from '@langchain/core/prompts';

作用 :用于构建结构化的对话提示(prompt)。

💡 为什么不用字符串拼接?

因为直接拼接容易出错(如忘记换行、角色混淆),而 ChatPromptTemplate 提供了类型安全、可复用、可测试的 prompt 构建方式。它支持 systemhumanai 三种消息角色,符合 OpenAI 的聊天格式标准。

javascript 复制代码
// 输出格式化模板
import {StringOutputParser} from '@langchain/core/output_parsers';

作用 :将大模型返回的复杂对象(如 { content: "xxx", role: "assistant" }自动提取为纯字符串

🔧 原理 :LangChain 的链式调用(.pipe())中,每个环节处理一种数据格式。模型输出是 AIMessage 对象,而前端只需要字符串,所以用 StringOutputParser 做"翻译"。

javascript 复制代码
// 引入后端框架
import express from 'express'; 

作用:创建 Web 服务器的核心库。Express 是 Node.js 最流行的轻量级 Web 框架,以中间件机制著称。

javascript 复制代码
// 引入cors 中间件 处理跨域请求
import cors from 'cors';

作用 :解决浏览器同源策略限制。

🌐 跨域场景 :前端运行在 http://localhost:5173,后端在 http://localhost:3000端口不同即跨域。若不启用 CORS,浏览器会直接拦截响应,即使服务器返回了 200。


第二步:配置 Ollama 模型(参数深挖)

arduino 复制代码
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:7bllama3 等,但需测试效果
temperature 控制输出随机性。值越低越确定 Commit Message 必须稳定 ,故设为 0.1;创意写作可用 0.7+

实验建议 :尝试 temperature: 0(完全 deterministic),看是否每次输入相同 diff 都得到相同 commit。


第三步:搭建 Express 服务器(中间件详解)

ini 复制代码
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: '*' }))。

    • 生产警告:绝对不要在生产环境使用!应明确指定前端域名,如:

      less 复制代码
      app.use(cors({
        origin: 'https://your-git-commit-app.com'
      }));

第四步:定义 /chat 接口(逐行拆解)

dart 复制代码
app.post('/chat', async (req, res) => {

📌 使用 POST 方法,因为需要发送请求体(git diff 内容可能很长,不适合放 URL 中)。

arduino 复制代码
  console.log(req.body, '/////')

🔍 调试技巧:打印原始请求体,便于排查前端是否传参正确。

vbnet 复制代码
  const { message } = req.body;
  if (!message || typeof message !== 'string') {
    return res.status(400).json({ error: 'message 必填,必须是字符串' });
  }

防御性编程

  • 防止 nullundefined、数字、对象等非法输入。
  • 返回 400 Bad Request 是 RESTful 规范的最佳实践。
  • 使用 return 立即终止函数,避免后续逻辑执行。
ini 复制代码
  try {
    const prompt = ChatPromptTemplate.fromMessages([
      ['system', '你是一个专业的代码审查员'],
      ['human', '{input}'],
    ]);

🧠 Prompt Engineering 核心

  • system 消息:设定 AI 的"人格"和任务目标。这里强调"专业代码审查员",引导其关注代码变更的语义、影响范围、规范性。
  • {input}:占位符,将在 .invoke({ input: message }) 时被真实 diff 替换。
  • 为什么不用单条字符串? → 多轮对话结构更符合现代 LLM 的训练数据分布,效果更好。
ini 复制代码
    const chain = prompt
    .pipe(model)
    .pipe(new StringOutputParser());      

⛓️ LangChain Chain 机制

  • .pipe(model):将格式化后的 prompt 发送给 Ollama 模型。
  • .pipe(new StringOutputParser()):接收模型响应(AIMessage 对象),只取 .content 字段作为字符串输出。
  • 链式调用优势:可轻松插入日志、缓存、重试等中间处理逻辑。
arduino 复制代码
    console.log('正在调用大模型')

用户体验提示:告知开发者模型正在处理(实际项目中可移除或改为 debug 日志)。

ini 复制代码
    const result = await chain.invoke({input: message});

🔄 异步调用

  • invoke 是 LangChain 推荐的同步式调用方法(尽管底层是异步)。
  • 传入的对象 key 必须与 prompt 中的占位符名一致(这里是 input)。
scss 复制代码
    res.status(200).json({ reply: result });

成功响应

  • 状态码 200 OK 表示请求成功。
  • 响应体为 JSON 格式:{ reply: "feat(auth): add login validation" }
  • 字段命名reply 清晰表达这是 AI 的回复,避免与业务字段冲突。
lua 复制代码
  } catch (e) {
    res.status(500).json({ error: '调用大模型失败' });
  }

🛡️ 错误兜底

  • 捕获所有异常(网络中断、Ollama 崩溃、模型加载失败等)。
  • 返回 500 Internal Server Error,前端可据此显示友好错误。
  • 进阶建议 :记录 e.message 到日志系统(如 Winston),但不要暴露给前端(防信息泄露)。

前端实现:React Hook 封装 AI 调用(超细粒度解析)

自定义 Hook:useGitDiff.js

javascript 复制代码
import { useState, useEffect } from 'react';
import { chat } from '../api/index.js';

📦 模块化思想chat 函数抽离到 api/ 目录,便于统一管理接口、添加拦截器(如 token、loading 全局控制)。

javascript 复制代码
export const useGitDiff = () => {
  const [content, setContent] = useState('');
  const [loading, setLoading] = useState(false);

🧵 状态管理

  • content:存储 AI 生成的 commit message。
  • loading:控制 UI 加载状态,提升用户体验。
scss 复制代码
  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。修复方案

scss 复制代码
useEffect(() => {
  let isMounted = true;
  (async () => {
    setLoading(true);
    const { data } = await chat('你好');
    if (isMounted) {
      setContent(data.reply);
      setLoading(false);
    }
  })()
  return () => { isMounted = false; };
}, []);
kotlin 复制代码
  return {
    loading,
    content,
  }
}

🎁 自定义 Hook 的价值

  • 将"获取 AI commit message"的逻辑封装成可复用单元。
  • 组件只需 const { loading, content } = useGitDiff(); 即可消费状态。
  • 未来可轻松扩展:添加 error 状态、refetch 函数等。

主组件:App.jsx(极简主义之美)

javascript 复制代码
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、路由

ini 复制代码
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

javascript 复制代码
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 的强大之处在于 中间件机制。你可以把它想象成一条装配流水线:

css 复制代码
请求 → [中间件1] → [中间件2] → [路由处理器] → [中间件N] → 响应

每个中间件都可以:

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

关键中间件:express.json()

ini 复制代码
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 的"表情包"

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

类别 状态码 含义 使用场景
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+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 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.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):

    • 浏览器直接发送请求

    • 服务器需在响应中包含:

      arduino 复制代码
      Access-Control-Allow-Origin: http://localhost:5173
  • 非简单请求(如自定义 header、PUT/DELETE):

    • 浏览器先发一个 OPTIONS 预检请求

    • 询问服务器:"我打算发一个 POST + 自定义 header,你允许吗?"

    • 服务器需在 OPTIONS 响应中说明允许的方法和头:

      makefile 复制代码
      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 中间件:

csharp 复制代码
pnpm add cors
javascript 复制代码
import cors from 'cors';
app.use(cors()); // 允许所有来源跨域(开发阶段)

这行代码等价于自动添加:

makefile 复制代码
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET,HEAD,PUT,PATCH,POST,DELETE
Access-Control-Allow-Headers: ...

生产环境绝不能用 * !应明确指定信任的前端域名:

less 复制代码
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. 浏览器自动在请求头中添加:

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

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

    arduino 复制代码
    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. 启动后端:

    bash 复制代码
    cd server && npx nodemon index.js
  2. 启动前端:

    arduino 复制代码
    cd frontend && npm run dev
  3. 打开 http://localhost:5173,看到:

    sql 复制代码
    你好!我是专业的代码审查员,请提供您的代码变更内容(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 赋能,专业如大师 👨‍💻✨


项目源码结构

bash 复制代码
git-commit-ai/
├── frontend/
│   ├── src/
│   │   ├── hooks/useGitDiff.js
│   │   ├── api/index.js
│   │   └── App.jsx
└── server/
    └── index.js

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

相关推荐
Irene19913 小时前
推荐 React 开发需要在 VS Code 中安装的插件
react.js
吴声子夜歌4 小时前
Node.js——Express框架
node.js·express
人民广场吃泡面4 小时前
React新手快速入门学习指南(2026最新版)
前端·react.js·前端框架
人人常欢笑5 小时前
Grafana 表格自定义下载样式。
javascript·react.js·grafana
ZhaoJuFei6 小时前
React生态学习路线
前端·学习·react.js
早點睡3906 小时前
ReactNative项目OpenHarmony三方库集成实战:react-native-calendar-events(读取不到日历里新增的事件,待排查)
javascript·react native·react.js
Cxiaomu7 小时前
像ChatGPT一样逐字输出:React + TypeScript 流式接收与“打字机”效果实现方案
人工智能·react.js·chatgpt·typescript
诸神缄默不语8 小时前
本地LLM部署工具(写给小白的LLM工具选型系列:第一篇)
llm·大规模预训练语言模型·vllm·ollama
Highcharts.js19 小时前
适合报表系统的可视化图表|Highcharts支持直接导出PNG和PDF
javascript·数据库·react.js·pdf
M ? A20 小时前
解决 VuReact 中 ESLint 规则冲突的完整指南
前端·react.js·前端框架