Git Diff AI:用 LangChain + Ollama 打造智能 Commit 神器

62 阅读12分钟

🐖你平时写 commit message 是不是经常这样:

  • update
  • fix bug
  • 改了点东西
  • 临时提交一下

当你只在自己的小项目里玩,这样也不是不行;但一旦进入团队协作,commit message 会变成“项目日志”和“工作可追溯证据”。写得规范=未来自己和同事都能快速定位问题

于是我们做一个实用工具:输入一段 git diff,让 AI 输出一条更像“高手写的”提交信息。它不是炫技项目,它是“把你学过的知识,逼着你用起来”的训练器。


🧩 需求拆解:我们到底要做什么?

我们要的最终体验非常简单:

  1. 前端拿到 git diff(此处先用字符串模拟)
  2. 前端把 diff 发给后端 /chat
  3. 后端把 diff 交给本地大模型(Ollama 跑在 11434)
  4. 大模型回一段规范的 commit message
  5. 前端展示 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
})

这段建议你用“流水线”来记忆:

  1. Prompt 模板把你的 message 填到 {input}
  2. model 负责把 prompt 交给 Ollama 并拿到返回
  3. parser 把返回收敛成纯字符串(更适合直接返回前端)
  4. 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 渲染时:

  1. useGitDiff('hello') 触发 effect
  2. Hook 调用 chat(diff) → axios 发 POST http://localhost:3000/chat
  3. Express 收到请求,express.json() 解析 body
  4. 路由 /chat 校验 message
  5. LangChain chain.invoke 把 message 交给 Ollama
  6. 返回 reply 给前端
  7. 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、甚至加入用户界面来管理历史提交记录。你的全栈之旅才刚刚开始,保持这份好奇心和动手能力,你将无所不能!