跳至主要内容

Gemini

使用 Gemini AI 進行本地 Code Review

前言

作為一個前端工程師,在提交程式碼之前,我總是希望能有個「第二雙眼睛」幫我檢查程式碼。雖然團隊有 code review 的文化,但有時候自己在開發過程中就想先自我檢查一下。

於是我開始思考:能不能讓 AI 幫我在本地進行 code review?

經過一番研究和實作,我使用 Google Gemini AI 打造了一個本地的 code review 工具。這篇文章記錄了整個實作過程,包括遇到的問題和解決方案。

目標

  • ✅ 使用 Gemini AI 分析程式碼變更
  • ✅ 根據開發規範進行審查
  • ✅ 產生詳細的審查報告
  • ✅ 在提交前快速進行自我檢查

技術棧

  • 程式語言:Node.js (ES6+)
  • AI 模型:Google Gemini 2.0 Flash
  • 專案類型:React + TypeScript + Vite

實作步驟

步驟一:取得 Gemini API Key

首先需要取得 Gemini API Key:

  1. 前往 Google AI Studio
  2. 點擊 "Get API key"
  3. 建立新的 API key
  4. 複製 API key 備用

重要:免費版的 Gemini API 每天有 200 次請求的配額,對於一般團隊來說完全足夠。

步驟二:建立 Code Review 腳本

在專案根目錄建立 gemini_code_review.mjs

#!/usr/bin/env node

import { execSync } from "child_process";
import fs from "fs";
import { GoogleGenAI } from "@google/genai";
import path from "path";
import { fileURLToPath } from "url";
import { config } from "dotenv";

// ES6 模組中取得 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

// 載入 .env 檔案(本地開發用)
config();
console.log("✓ 已載入 .env 檔案");

// 初始化變數
const GEMINI_API_KEY = process.env.GEMINI_API_KEY;
const conventionsPath = process.env.CODING_CONVENTIONS_PATH;

// 初始化 Gemini AI
const ai = new GoogleGenAI({ apiKey: GEMINI_API_KEY });
const model = "gemini-2.0-flash";

/**
* 取得 Git diff
*/
function getGitDiff() {
try {
const targetBranch =
process.env.CI_MERGE_REQUEST_TARGET_BRANCH_NAME || "main";

// 先檢查是否有未 commit 的變更(用於本地測試)
let filesOutput = "";
try {
filesOutput = execSync("git diff --name-only", { encoding: "utf-8" });
} catch (error) {
// 忽略錯誤
}

if (filesOutput.trim()) {
// 有未 commit 的變更
console.log(" 檢測到未 commit 的變更");
const changedFiles = filesOutput
.trim()
.split("\n")
.filter((f) => f);
const diffOutput = execSync("git diff", { encoding: "utf-8" });
return { diff: diffOutput, changedFiles };
} else {
// 沒有未 commit 的變更,比較分支
console.log(` 比較當前分支與 origin/${targetBranch}`);
const filesOutput = execSync(
`git diff --name-only origin/${targetBranch}...HEAD`,
{ encoding: "utf-8" }
);
const changedFiles = filesOutput
.trim()
.split("\n")
.filter((f) => f);

const diffOutput = execSync(`git diff origin/${targetBranch}...HEAD`, {
encoding: "utf-8",
});
return { diff: diffOutput, changedFiles };
}
} catch (error) {
console.error("取得 git diff 時發生錯誤:", error.message);
return null;
}
}

/**
* 分析變更檔案的類型
*/
function analyzeFileTypes(files) {
const fileTypes = {
components: [],
hooks: [],
utils: [],
styles: [],
configs: [],
tests: [],
types: [],
other: [],
};

for (const file of files) {
if (!file) continue;

if (
file.toLowerCase().includes("component") ||
file.includes("/components/")
) {
fileTypes.components.push(file);
} else if (
file.toLowerCase().includes("hook") ||
file.includes("/hooks/")
) {
fileTypes.hooks.push(file);
} else if (
file.toLowerCase().includes("util") ||
file.includes("/utils/")
) {
fileTypes.utils.push(file);
} else if (file.match(/\.(css|scss|less|styled\.(ts|js))$/)) {
fileTypes.styles.push(file);
} else if (
file.toLowerCase().includes("config") ||
file.match(/\.config\.(js|ts)$/)
) {
fileTypes.configs.push(file);
} else if (file.includes("test") || file.includes("spec")) {
fileTypes.tests.push(file);
} else if (file.endsWith(".d.ts") || file.includes("/types/")) {
fileTypes.types.push(file);
} else {
fileTypes.other.push(file);
}
}

return fileTypes;
}

/**
* 使用 Gemini AI 分析程式碼
*/
async function analyzeWithGemini(codeDiff, fileTypes) {
// 讀取公司開發規範(如果有設定)
let codingConventions = "";

if (conventionsPath && fs.existsSync(conventionsPath)) {
try {
codingConventions = fs.readFileSync(conventionsPath, "utf-8");
console.log(` ✓ 已載入公司開發規範: ${path.basename(conventionsPath)}`);
} catch (error) {
console.log(` ⚠️ 無法讀取開發規範: ${error.message}`);
}
} else {
console.log(" ℹ️ 未設定開發規範路徑");
}

const fileSummary = [];
for (const [category, files] of Object.entries(fileTypes)) {
if (files.length > 0) {
fileSummary.push(`- ${category}: ${files.length} 個檔案`);
}
}

const fileSummaryText = fileSummary.join("\n");

// 組合 prompt
const prompt = `你是一個專業的前端程式碼審查專家,專精於 React、TypeScript、Vite 專案。

${
codingConventions
? `
## 📋 公司開發規範

請**嚴格遵守**以下開發規範來審查程式碼:

${codingConventions.substring(0, 20000)}

---
`
: ""
}

本次 MR 變更摘要:
${fileSummaryText}

請仔細審查以下的 Git diff,針對前端專案提供專業的分析報告。

**重點檢查項目:**

${codingConventions ? "🔴 **首要任務:檢查是否符合公司開發規範**\n\n" : ""}

🎯 **React 最佳實踐**
- 元件設計是否合理(單一職責、可重用性)
- Hooks 使用是否正確(依賴陣列、記憶化)
- Props 設計是否清晰
- 狀態管理是否恰當

⚡ **效能考量**
- 不必要的重渲染
- 記憶化的使用(useMemo, useCallback, React.memo)

🔒 **型別安全**
- TypeScript 型別定義是否完整
- 是否過度使用 any

🎨 **程式碼品質**
- 程式碼可讀性和維護性
- 命名是否清晰
- 錯誤處理是否完善

🐛 **潛在問題**
- 記憶體洩漏風險
- 非同步處理問題

🧪 **測試覆蓋**
- 是否需要補充測試

Git Diff(前 30000 字元):
\`\`\`
${codeDiff.substring(0, 30000)}
\`\`\`

**請用繁體中文回覆,使用 Markdown 格式,包含:**

${codingConventions ? "## 🔴 開發規範檢查\n\n" : ""}
## 📊 總體評分
## ✅ 優點
## ⚠️ 需要改進
## 🐛 潛在問題
## 💡 具體建議
## 🎯 行動項目
`;

try {
const response = await ai.models.generateContent({
model: model,
contents: prompt,
});
return response.text;
} catch (error) {
console.error("❌ 呼叫 Gemini API 時發生錯誤:", error.message);
if (error.message.includes("429") || error.message.includes("quota")) {
console.error("⏱️ 達到 Rate Limit,請稍後再試");
}
throw error;
}
}

/**
* 儲存審查報告
*/
function saveReviewReport(reviewText, fileTypes) {
const fileSummary = [];
let totalFiles = 0;

for (const [category, files] of Object.entries(fileTypes)) {
if (files.length > 0) {
totalFiles += files.length;
fileSummary.push(`- **${category}**: ${files.length} 個檔案`);

for (let i = 0; i < Math.min(5, files.length); i++) {
fileSummary.push(` - \`${files[i]}\``);
}

if (files.length > 5) {
fileSummary.push(` - ... 還有 ${files.length - 5} 個檔案`);
}
}
}

const fileSummaryText = fileSummary.join("\n");

const report = `# 🤖 Gemini AI Code Review Report

**專案**: React + Vite + TypeScript

**變更摘要**: 共 ${totalFiles} 個檔案

${fileSummaryText}

---

${reviewText}`;

fs.writeFileSync("code_review_report.md", report, "utf-8");
console.log("✅ 審查報告已儲存至 code_review_report.md");
}

/**
* 主程式
*/
async function main() {
console.log("=".repeat(70));
console.log("🚀 Gemini AI Code Review for Frontend (React/Vite/TypeScript)");
console.log("=".repeat(70));

if (!process.env.GEMINI_API_KEY) {
console.error("❌ 錯誤: 未設定 GEMINI_API_KEY 環境變數");
process.exit(1);
}

console.log("✓ GEMINI_API_KEY 已設定");

console.log("\n📝 正在取得程式碼變更...");
const result = getGitDiff();

if (!result || !result.diff || result.diff.trim().length === 0) {
console.log("ℹ️ 沒有偵測到程式碼變更,跳過審查");
process.exit(0);
}

const fileTypes = analyzeFileTypes(result.changedFiles);
const totalFiles = result.changedFiles.length;

console.log(`✓ 偵測到 ${totalFiles} 個檔案變更`);
console.log(`✓ 程式碼變更大小: ${result.diff.length.toLocaleString()} 字元`);

console.log("\n🔍 正在使用 Gemini 分析程式碼...");

try {
const review = await analyzeWithGemini(result.diff, fileTypes);
console.log("✓ 審查完成\n");

console.log("=".repeat(70));
console.log("📋 審查結果");
console.log("=".repeat(70));
console.log(review);
console.log("=".repeat(70));

saveReviewReport(review, fileTypes);

console.log("\n✅ 程式碼審查完成!");
} catch (error) {
console.error("❌ 審查過程發生錯誤:", error.message);
process.exit(1);
}
}

main();

步驟四:安裝必要的套件

npm install @google/genai dotenv

如果你的專案要使用 ES6 模組,在 package.json 中加入:

{
"type": "module",
"dependencies": {
"@google/genai": "^latest",
"dotenv": "^17.2.3"
}
}

步驟五:本地測試

在本地測試腳本:

  1. 建立 .env 檔案:
GEMINI_API_KEY=你的_API_KEY
CI_MERGE_REQUEST_TARGET_BRANCH_NAME=brnach名稱
CODING_CONVENTIONS_PATH=/path/to/your/conventions.md
  1. 確保 .env.gitignore 中:
echo ".env" >> .gitignore
  1. 執行測試:
# 安裝套件
npm install

# 執行 code review
node gemini_code_review.mjs

進階功能:整合開發規範

如果你有自己的開發規範文件,可以讓 Gemini 根據這些規範來審查程式碼:

設定規範檔案路徑

.env 中加入:

CODING_CONVENTIONS_PATH=/path/to/conventions/README.md

這樣 Gemini 就會根據你的開發規範來審查程式碼了!

遇到的問題與解決方案

問題 1:Python vs Node.js

問題:一開始想用 Python 寫,但我的專案是 Node.js,覺得混用兩種語言不太好。

解決:改用 Node.js 的 ES6 語法編寫,與專案技術棧保持一致。

問題 2:選擇哪個 Gemini 模型

問題:文檔中有很多模型,不知道該用哪個。

解決

  • gemini-1.5-flash - 已淘汰
  • gemini-2.0-flash-exp - 實驗版,配額太低
  • gemini-2.0-flash - 穩定版,推薦使用

問題 3:API 配額限制

問題:擔心免費版配額不夠用。

解決:查看 Google AI Studio Rate Limits,發現免費版配額非常充裕:

限制免費版上限實際需求
RPM (每分鐘請求)15通常 < 5
TPM (每分鐘 tokens)1,000,000通常 < 50,000
RPD (每天請求)200通常 < 50

對於一般團隊來說,免費版完全足夠

問題 4:dotenv 無法載入 .env

問題:執行時顯示「未安裝 dotenv」。

解決

npm install dotenv

並確保在程式開頭正確 import:

import { config } from "dotenv";
config();

問題 5:選擇官方 SDK

問題:發現有 @google/genai@google/generative-ai 兩個套件。

解決:兩個都可以用,但 @google/genai 是較新的版本,API 更簡潔:

// 使用 @google/genai(推薦)
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: API_KEY });
const response = await ai.models.generateContent({ model, contents: prompt });

成本分析

Google 提供的方案 可能隨時有變化,使用前,需再次確認免費額度

免費版限制

  • RPM: 15 次/分鐘
  • TPM: 1,000,000 tokens/分鐘
  • RPD: 200 次/天

付費版價格

如果超過免費限制,付費版價格也很便宜:

項目價格
輸入$0.075 / 百萬 tokens
輸出$0.30 / 百萬 tokens

實際成本範例

  • 假設每次 code review 使用 30,000 輸入 + 2,000 輸出 tokens
  • 成本 = (30,000/1,000,000 × $0.075) + (2,000/1,000,000 × $0.30)
  • 每次約 $0.003 USD(不到 0.1 元台幣)

使用效果

審查報告範例

Gemini 會產生一份詳細的 Markdown 報告,包含:

# 🤖 Gemini AI Code Review Report

## 📊 總體評分

8/10 分

## ✅ 優點

- TypeScript 型別定義完整
- 元件職責清晰,符合單一職責原則
- 使用 useMemo 進行適當的效能優化

## ⚠️ 需要改進

1. **useState 依賴陣列缺失**

- ExpenseList.tsx 第 45 行的 useEffect 缺少依賴項
- 建議加入 `[expenses]` 作為依賴

2. **錯誤處理不足**
- API 呼叫缺少 try-catch
- 建議加入錯誤邊界處理

## 🐛 潛在問題

- 可能造成記憶體洩漏:useEffect 中的 subscription 沒有清理

## 💡 具體建議

[程式碼範例和改進建議...]

## 🎯 行動項目

1. 立即修正 useEffect 依賴陣列
2. 補充錯誤處理邏輯
3. 新增單元測試

實際數據

實測中:

  • ✅ 每次審查耗時:約 10-15 秒
  • ✅ 審查準確度:約 85-90%(能抓出大部分問題)
  • ✅ 誤報率:約 5-10%(偶爾會有誤判)
  • ✅ 配額使用:每天約 20-30 次(遠低於上限)

最佳實踐

1. 結合人工審查

AI 審查應該是輔助工具,不應完全取代人工 review:

AI Review (自助檢查) → Human Review (必要) → Commit

2. 定期檢查配額

定期查看 Rate Limits 頁面,確保沒有超過限制。

3. 適當的 Prompt 調整

根據需求調整 prompt:

  • 強調特定的編碼風格
  • 加入個人或團隊的開發規範
  • 針對特定技術棧優化

4. 保存審查歷史

將審查報告保存在固定的資料夾中,方便日後查閱:

# 修改腳本,將報告保存到特定資料夾
mkdir -p code_reviews
mv code_review_report.md code_reviews/review_$(date +%Y%m%d_%H%M%S).md

未來改進方向

1. 客製化評分標準

根據個人或團隊需求,客製化評分權重和閾值。

2. 多語言支援

擴展到其他語言的專案(後端、行動端等)。

3. 審查報告統計

彙整歷史審查數據,產生程式碼品質趨勢報告。

4. 整合到 IDE

開發 VSCode 擴充功能,讓 AI 審查更加方便。

5. Git Hook 整合

在 pre-commit 階段自動執行審查,提早發現問題。

結論

使用 Gemini AI 進行本地 code review 是一個非常實用的實踐:

✅ 優點

  1. 提升效率:在提交前快速進行自我檢查
  2. 一致性:AI 審查標準一致,不受情緒影響
  3. 學習工具:從 AI 建議中學習最佳實踐
  4. 成本低廉:免費版配額充足,付費版也很便宜
  5. 容易使用:只需簡單的腳本設定

⚠️ 注意事項

  1. 不能完全取代人工:AI 會有誤判,仍需人工確認
  2. 需要調教:Prompt 需要根據需求調整
  3. 配額管理:要留意 API 使用量
  4. 敏感資訊:確保不要把機密程式碼送給 AI

💡 適合的使用情境

  • ✅ 個人專案的程式碼檢查
  • ✅ 提交前的自我審查
  • ✅ 學習程式設計最佳實踐
  • ✅ 快速發現潛在問題

參考資源

原始碼

完整的程式碼可以在我的 GitHub 找到:連結