🐖你平时写 commit message 是不是经常这样:
updatefix bug改了点东西临时提交一下
当你只在自己的小项目里玩,这样也不是不行;但一旦进入团队协作,commit message 会变成“项目日志”和“工作可追溯证据”。写得规范=未来自己和同事都能快速定位问题。
于是我们做一个实用工具:输入一段 git diff,让 AI 输出一条更像“高手写的”提交信息。它不是炫技项目,它是“把你学过的知识,逼着你用起来”的训练器。
🧩 需求拆解:我们到底要做什么?
我们要的最终体验非常简单:
- 前端拿到
git diff(此处先用字符串模拟) - 前端把 diff 发给后端
/chat - 后端把 diff 交给本地大模型(Ollama 跑在 11434)
- 大模型回一段规范的 commit message
- 前端展示 loading 和结果
你会在这个过程中把这些能力练扎实:
- ✅ Express:从 0 起一个 API 服务(监听端口、路由、请求体解析)
- ✅ HTTP & 状态码:200/400/500 的语义化返回
- ✅ LangChain:用 Prompt + LLM + Parser 组合工作流(pipe)
- ✅ Ollama:本地部署模型,像调 OpenAI 一样调它
- ✅ Axios:模块化 API 请求
- ✅ React 自定义 Hooks:把“副作用+状态”从组件里剥离
- ✅ CORS:跨域为什么会报错、怎么用中间件放行
🏗️ 第一部分:后端(Express + LangChain + Ollama)
1)初始化项目:为什么要先 npm init?
常见命令是:
npm init -y
它会生成 package.json,让依赖、脚本、模块类型都可管理。
package.json里有一行很关键:
"type": "module"
这意味着 Node 使用 ESM(import/export)语法,而不是老的 require。所以你在 index.js 里直接 import express from 'express' 是成立的。
2)安装 Express:它到底是什么?
Express 是 Node 生态里最经典的 Web 框架之一,核心价值就三件事:
- 更舒服地写路由(
app.get/app.post) - 中间件机制(
app.use,把请求处理拆成流水线) - 统一封装请求对象 req 和响应对象 res(让你更像在写“接口”)
安装:
pnpm i express
项目里 express 版本是 ^5.2.1Express 5 已经比较“现代”,但用法和 Express 4 非常接近。
🚪 3)先把服务跑起来:监听 3000 端口
后端能不能跑,第一件事就是:监听端口。
观察如下代码
app.listen(3000, () => {
console.log('server is running on port 3000');
})
这三行非常关键,建议你把它理解成:
- 端口 3000 是“这个进程对外提供服务的入口”
- 没有 listen,你写再多路由也“没人能访问到”
- 回调只是告诉你“监听成功了”
用 GET /hello 验证服务器伺服(hello world)
浏览器访问 http://localhost:3000/hello,看到 hello world 说明伺服正常。
这一步的意义不是“hello world 有用”,而是建立一个极重要的工程直觉:
- ✅ 浏览器能访问到服务 → 说明监听、端口、进程都 OK
- ❌ 访问不到 → 先别急着怀疑 LangChain / Ollama,先把基础链路打通
🤖 4)引入 LangChain + Ollama:开始“AI 工作流”
import { ChatOllama } from '@langchain/ollama';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { StringOutputParser } from '@langchain/core/output_parsers';
这三件套分别负责:
- ChatOllama:告诉 LangChain“我要用 Ollama 当 LLM 提供方”
- ChatPromptTemplate:把提示词结构化(system/human),并支持参数
{input} - StringOutputParser:把模型返回的内容转成“纯字符串”(方便直接 JSON 返回前端)
为什么说 LangChain 实用?
因为它让你把“调用大模型”写成可组合的工作流:
- Prompt(输入怎么组织)
- Model(用哪个模型、连到哪里)
- Parser(输出怎么处理)
这比“到处拼字符串、到处写 fetch”更像工程。
🧠 5)配置本地大模型:Ollama 在 11434 端口
const model = new ChatOllama({
baseUrl: 'http://localhost:11434',
model: "deepseek-r1:8b",
temperature: 0.1 // 严格
})
逐行拆解一下:
baseUrl: 'http://localhost:11434'- Ollama 默认会起一个本地服务,11434 是它的 HTTP 接口端口
- 本质上它让你的本地模型“长得像 OpenAI 那种 API 服务”
model: "deepseek-r1:8b"- 指定用哪个模型(你已经通过
ollama run ...拉起/拉取过)
- 指定用哪个模型(你已经通过
temperature: 0.1- 越低越“严谨稳定”,越高越“发散创意”
- 这里做 commit message 更适合低温:输出更稳定、更少跑题
📮 6)先把 POST /chat 跑通:接口是怎么设计出来的?
app.post('/chat',async (req, res) => {
console.log(req.body);
const { message } = req.body;
...
})
这里你就已经进入“真正的后端接口开发”了。一个靠谱的接口至少需要:
- 输入校验(参数缺失、类型错误怎么办)
- 明确的状态码(别永远 200)
- 失败兜底(try/catch)
- 稳定的返回结构(前端好写)
🧯 7)你遇到的第一个坑:req.body 为 undefined
你大纲里的剧情非常真实:用 Apifox 发 POST,结果发现 req.body 打印 undefined。
这个坑的本质是:Express 默认不会帮你解析 JSON 请求体。所以你写 const { message } = req.body 时,req.body 根本就不存在。
解决方式就是这一行中间件:
app.use(express.json());
把它理解成一个“请求预处理器”:
- 浏览器/Apifox 发来的是 JSON 字符串(原始 bytes)
express.json()会把它解析成 JS 对象- 然后挂到
req.body上 - 你的路由里才能“像访问对象一样”访问请求体
中间件是什么?为什么它这么重要?
中间件就是 Express 的核心机制之一:请求从进来到出去会经过一串 app.use(...),每一个都可以:
- 修改 req(比如塞
req.body) - 修改 res(比如设置响应头)
- 提前结束请求(比如鉴权失败直接 401)
- 或交给下一个处理器
所以你后面用 cors() 放行跨域,本质也是同一套机制。
🧾 8)输入校验 + 状态码:让后端“稳定第一”
if(!message || typeof message !== 'string') {
return res.status(400).json({
error: "message 必填,必须是字符串"
});
}
这段写得很“工程化”,原因是:后端必须假设用户会乱来。
400 Bad Request:请求格式不对、参数不合法 → 用户的锅- 返回 JSON:前端更好解析(而不是一会儿文本一会儿 JSON)
常用状态码:
- 1XX 请求中....
- 200 OK 成功
- 201 Created 资源创建成功
- 3XX 重定向 redirect
- 400 Bad Request 合适的状态码
- 404 Not Found 资源不存在
- 401 Unauthorized 未授权
- 500 Internal Server Error 服务器错误
上面列举了状态码小抄,这里顺便把它“落地成直觉”:
- 200:成功拿到结果
- 400:你传参不对,别怪服务器
- 500:服务器内部出错(比如模型调用失败)
🔗 9)LangChain 工作流:Prompt → Model → Parser
不了解的可以看看我的langchain相关文章: 📕LangChain 全能手册 - 从零打造你的 AI 工作流嗨,同学!欢迎来到 AIGC 的奇妙世界。
核心工作流
const prompt = ChatPromptTemplate.fromMessages([
['system','You are a helpful assistant'],
['human','{input}']
]);
const chain = prompt
.pipe(model)
.pipe(new StringOutputParser());
const result = await chain.invoke({
input: message
})
res.status(200).json({
reply: result
})
这段建议你用“流水线”来记忆:
- Prompt 模板把你的
message填到{input} - model 负责把 prompt 交给 Ollama 并拿到返回
- parser 把返回收敛成纯字符串(更适合直接返回前端)
invoke({ input: message })是执行入口
为什么要用 system/human 结构?
因为聊天模型通常更吃“角色信息”:
- system:告诉模型你是谁、要做什么(全局指令)
- human:用户的具体输入
在“生成 commit message”这个任务里,后续你可以把 system 改成更明确的规则,比如:
- 输出必须是 Conventional Commits
- 必须包含 scope
- 必须中英文之一
- 必须简短不超过 72 字符
这就是“Prompt 工程”真正的实战点:不是让模型“回答”,是让模型“按格式产出”。
🧪 10)Apifox + nodemon:为什么一开始“请求失败”?
这很常见:如果改了代码但服务没重启,接口行为将还是旧的。
工程上常用的方式是用 nodemon:
- 它监控文件变动
- 自动重启 Node 服务
- 你不需要每改一行都手动 Ctrl+C 再启动
你可以在 server 目录安装并运行(示例命令):
pnpm i -D nodemon
pnpm nodemon index.js
🌍 第二个大坑:跨域(CORS)——浏览器为什么要拦你?
同源策略就像“签证”,如果你想出国如去柬埔寨,这是一件有风险的事情,为了人身安全需要多重验证让你为自己的行为负责。就像在微信里打开某些网站,微信一定会提示你存在风险,让你自己去浏览器打开,这就是一份免责声明,告诉用户出了问题不是我们微信的锅🐖
核心结论:
- 浏览器为了用户安全,默认执行同源策略
- 协议 / 域名 / 端口 任意一个不同,都算跨域
- 你的前端是
http://localhost:5173 - 你的后端是
http://localhost:3000 - 端口不同 → 必跨域 → 浏览器会拦截(即使后端其实返回了数据)
解决方式:后端明确告诉浏览器“我允许这个源访问我”。
cors 中间件可以简单完成:
app.use(cors());
它会在响应里加上类似 Access-Control-Allow-Origin 的头,浏览器看到“签证”就会放行。
🎨 第二部分:前端(React + TailwindCSS + Axios + 自定义 Hooks)
主线:先把网络请求封装好,再把业务逻辑抽成 Hook,最后在 App 里消费 Hook 的结果。这就是“从能跑 → 好维护”的前端工程成长路线。
📡 1)Axios 封装:为什么说它比 fetch 更适合“项目化”?
import axios from 'axios';
const service = axios.create({
baseURL:'http://localhost:3000',
headers:{
'Content-Type':'application/json',
},
timeout:60000
});
export const chat = (message) =>
service.post('/chat',{ message });
这里已经是非常典型的“请求模块化”写法了。
对比 fetch,axios 在项目里更香的点通常是:
- 统一 baseURL:不需要每次写完整地址
- 默认 JSON 处理更顺手
- 超时、拦截器(后面做鉴权/统一错误处理非常好用)
- 请求函数统一管理:团队协作时不会散落在组件里
这个 chat(message) 让你的 UI 层完全不用关心:
- 后端路径叫什么
- headers 怎么写
- body 怎么拼
UI 只关心“我要发 message”。
🪝 2)自定义 Hook:把“副作用+状态”从组件里剥离
之前讲过的自定义hooks 🎣 拒绝面条代码!手把手带你用自定义 Hooks 重构 React 世界
import { useState, useEffect } from 'react';
import { chat } from '../api/index.js';
export const useGitDiff = (diff) => {
const [loading,setLoading] = useState(false);
const [content,setContent] = useState('');
useEffect(() => {
(async () => {
if(!diff) return;
setLoading(true);
const { data } = await chat(diff);
setContent(data.reply);
setLoading(false);
})()
},[diff])
return { loading, content }
}
这段代码是“写 React 项目的人必须掌握的套路”,因为它解决了一个大问题:
组件的职责应该尽量纯粹:渲染 UI
数据请求、状态机、副作用逻辑,应该被抽走。
逐步拆解一下它在干什么:
- 输入:
diff(依赖项) - 输出:
{ loading, content }(给组件消费) - effect:当 diff 改变时触发请求
- async IIFE:因为
useEffect不能直接写async,所以用立即执行函数包一层 - loading:请求开始 true,请求结束 false
- content:用后端返回的
data.reply更新
这里有个很关键的工程直觉:
- Hook 像“业务函数”
- 组件像“展示层”
- 当你以后要把 UI 换成 textarea + button、要加历史记录、要加错误提示,你会发现 Hook 的结构非常好扩展
🧱 3)App.jsx:组件只负责“用 Hook + 展示结果”
剩下的App.jsx就可以非常简洁:
import { useEffect, useState } from 'react'
import { useGitDiff } from './hooks/useGitDiff.js';
export default function App() {
const { loading, content } = useGitDiff('hello');
useEffect(() => {
},[])
return (
<div className="flex">
{loading ? 'loading....' : content}
</div>
)
}
现在传的是 'hello',相当于模拟 diff。后续要接入真实 git diff,也只需要把 diff 字符串换成真实输入来源(比如 textarea、文件读取、或者命令行输出)即可。
TailwindCSS 在这里的意义
className="flex"。Tailwind 的核心价值在“原子化 CSS”:
- 你不需要为了一个
display: flex单独去写一个 class - 样式和组件结构靠得更近,改 UI 更快
⭐Tailwind 不会替你思考布局,但它能极大降低“写样式的摩擦力”。
之前的TailwindCSS文章:告别 CSS 焦虑,拥抱原子化!Tailwind CSS 助你效率起飞!
🧠 把整条链路串起来:请求一次会发生什么?
当 App 渲染时:
useGitDiff('hello')触发 effect- Hook 调用
chat(diff)→ axios 发 POSThttp://localhost:3000/chat - Express 收到请求,
express.json()解析 body - 路由
/chat校验message - LangChain chain.invoke 把 message 交给 Ollama
- 返回
reply给前端 - Hook 更新
content,UI 从 loading 切换成结果
这就是你要训练的“全栈链路思维”:每一层都知道自己负责什么,不越界,不耦合。
✅ 这套项目最值得强调的“成长点”
- ✅ 不是“写了个 AI demo”,而是把“接口工程化”做对了:校验、状态码、try/catch、稳定 JSON 返回
- ✅ 中间件思维一次练到位:
express.json()解决 body、cors()解决跨域 - ✅ 前端不是“在组件里乱写请求”,而是 axios 模块化 + Hook 业务封装
- ✅ LangChain 工作流清晰:Prompt / Model / Parser 分层,后续扩展非常自然
🎉 结语:从“能跑”到“好用”,你的全栈之路才刚刚开始!
恭喜你!通过这个“Git Diff AI”小项目,你不仅亲手搭建了一个能与本地大模型交互的全栈应用,更重要的是,你把那些散落在各处的知识点——Express 的中间件、HTTP 状态码的语义、LangChain 的工作流、Axios 的封装、React 自定义 Hook 的妙用,以及跨域的原理与解决——全部串联了起来。这不再是纸上谈兵,而是实实在在的“代码在跑,知识在脑”!
记住,技术栈本身只是工具,真正有价值的是你解决问题的思路和工程化的能力。现在,你已经掌握了将 AI 能力融入日常开发的“魔法”,不妨尝试将这个项目进一步完善:比如接入真实的 git diff 命令输出、优化 Prompt 让 AI 生成更精准的 commit message、甚至加入用户界面来管理历史提交记录。你的全栈之旅才刚刚开始,保持这份好奇心和动手能力,你将无所不能!