ローカル LLM を社内業務に特化させる 4 段階カスタマイズ — Qwen3 を「より賢く」する仕組み

Claude Code で生成したコードをローカル LLM(Ollama + Qwen3)でレビューする構成を前回の記事で紹介しました。しかし、汎用モデルのままでは「受注ステータスの遷移ルール」や「金額計算に float を使ってはならない」といった社内固有のルールを知りません。

この記事では、Qwen3 を社内業務に特化させ、特定のコーディング規約・業務ルール・過去の障害パターンを踏まえたレビューができるようにする 4 段階のカスタマイズ手法を紹介します。

全体像:4 段階のカスタマイズ

レベル手法導入期間効果専門知識
1Modelfile(システムプロンプト)即日ルールベースの指摘不要
2RAG(社内ドキュメント検索)1〜2 日文脈を踏まえた指摘Docker の基本
3Few-shot(レビュー事例の学習)数日パターン認識の向上不要
4LoRA ファインチューニング1〜2 週間モデル自体の精度向上Python・ML の基本
レベル 1:ルールを「教える」    ← すぐできる
レベル 2:資料を「渡す」       ← 1〜2日
レベル 3:お手本を「見せる」    ← 数日
レベル 4:頭脳を「鍛える」     ← 1〜2週間

推奨: レベル 1 から順に導入し、効果を確認しながらステップアップしてください。多くの場合、レベル 1 + 2 で十分な精度が得られます。

Modelfile とは?

レベル 1 に入る前に、Modelfile の仕組みを理解しておきましょう。

一言でいうと

Modelfile は、Ollama に「どのモデルを、どんな設定で、どんな役割で動かすか」を伝える設定ファイルです。Docker の Dockerfile に似た構文で書きます。

CLAUDE.md / AGENTS.md との違い

Claude Code を使っている方は「CLAUDE.md と何が違うの?」と思うかもしれません。両者は目的は似ていますが、読む相手が違います

CLAUDE.md   → Claude Code(エージェント)が読む指示書
Modelfile   → Ollama(ランタイム)が読む設定ファイル

3 層構造で整理すると明快です。

設定ファイル対象の層誰が読むか書き方
CLAUDE.md / AGENTS.mdエージェント層Claude CodeMarkdown
Modelfileランタイム + LLM 層OllamaDockerfile 風の独自構文

CLAUDE.md は「ドライバー(エージェント)への指示書」、Modelfile は「エンジン(ランタイム + LLM)の設定ファイル」です。

できることの違い

CLAUDE.mdModelfile
ルール・規約を伝えるできるできる
モデルを指定するできない(Claude 固定)できる(FROM qwen3:14b
温度・コンテキスト長を調整するできないできる
LoRA アダプタを適用するできないできる
ファイル形式Markdown独自構文

同じ「金額計算に float を使うな」というルールを伝える場合、書き方はこう変わります。

CLAUDE.md(Claude Code に伝える場合):

1
2
## コーディング規約
- 金額計算には Decimal 型を使用し、float は禁止

Modelfile(Ollama + Qwen3 に伝える場合):

1
2
3
4
5
FROM qwen3:14b
SYSTEM """
金額計算には Decimal 型を使用し、float は禁止する。
"""
PARAMETER temperature 0.1

Modelfile の基本構文

Modelfile で使える命令は以下の通りです。

命令役割
FROMベースモデルを指定FROM qwen3:14b
SYSTEMシステムプロンプト(役割・ルール)SYSTEM "あなたはレビュアーです"
PARAMETERモデルのパラメータ調整PARAMETER temperature 0.1
TEMPLATEプロンプトテンプレートの定義(上級者向け)
ADAPTERLoRA アダプタの適用ADAPTER ./lora_weights.gguf
MESSAGE初期会話履歴の設定MESSAGE user "こんにちは"

最小限の Modelfile はこれだけです。

1
2
FROM qwen3:14b
SYSTEM "あなたは親切な日本語アシスタントです。"

よく使う PARAMETER 一覧

パラメータ効果推奨値(レビュー用)
temperature出力のランダム性。低いほど一貫した回答0.1
num_ctxコンテキストウィンドウの大きさ32768
repeat_penalty同じ表現の繰り返しを抑制1.1
top_p出力の多様性を制御0.9
top_k候補トークン数を制限40

レビュー用途では temperature を低く設定するのがポイントです。毎回異なる指摘が出ると信頼性が下がるため、一貫性を重視します。

モデルの作成・管理コマンド

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Modelfile からカスタムモデルを作成
ollama create our-reviewer -f review-rules.Modelfile

# 作成済みモデルの一覧
ollama list

# モデルの詳細を確認
ollama show our-reviewer

# モデルの削除
ollama rm our-reviewer

# Modelfile を更新したら再作成(上書きされる)
ollama create our-reviewer -f review-rules.Modelfile

ルールの二重管理を避けるには

Claude Code(CLAUDE.md)とローカル LLM(Modelfile)の両方にルールを伝える場合、同じルールを 2 箇所に書く必要が出てきます。ルールの二重管理を避けるには、ルールを 1 つのファイルで管理し、両方から参照する方法が有効です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 1. ルールを単独ファイルで管理
cat rules.md
# ## コーディング規約
# - 金額計算には Decimal 型を使用し、float は禁止
# - N+1 クエリを検出したら必ず指摘する
# ...

# 2. CLAUDE.md から参照(手動でコピー or インクルード)

# 3. Modelfile を自動生成するスクリプト
cat << 'EOF' > generate_modelfile.sh
#!/bin/bash
RULES=$(cat rules.md)
cat > review-rules.Modelfile << MODELFILE
FROM qwen3:14b
SYSTEM """
あなたは当社のシニアエンジニアです。以下のルールに従ってコードレビューしてください。

${RULES}
"""
PARAMETER num_ctx 32768
PARAMETER temperature 0.1
MODELFILE

ollama create our-reviewer -f review-rules.Modelfile
echo "モデルを更新しました"
EOF
chmod +x generate_modelfile.sh
1
2
# ルール更新時に実行
./generate_modelfile.sh

この仕組みなら、rules.md を 1 箇所更新するだけで、CLAUDE.md と Modelfile の両方に反映できます。

レベル 1:Modelfile でルールを埋め込む(即日導入)

Modelfile の仕組みが分かったところで、実際にレビュー用のカスタムモデルを作成しましょう。

Modelfile の作成

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# review-rules.Modelfile
FROM qwen3:14b

SYSTEM """
あなたは当社のシニアエンジニアとしてコードレビューを行います。
以下のルールに従い、違反を検出した場合は具体的な行番号と修正案を提示してください。

## 技術スタック
- バックエンド: Python (Django 5.x)
- API: GraphQL (Strawberry Django)
- DB: PostgreSQL 16
- インフラ: AWS ECS Fargate

## コーディング規約
- Django ORM のクエリでは select_related / prefetch_related を必ず使用する
- N+1 クエリを検出した場合は最優先で指摘する
- GraphQL リゾルバ内で同期 I/O(sync DB access)を使用してはならない
- 変数名・関数名はスネークケース、クラス名はパスカルケース
- 型ヒント(type hints)を必ず付与する
- マジックナンバーは定数として定義する

## セキュリティルール
- .env の値をコード内にハードコードしてはならない
- ユーザー入力は必ずバリデーション・サニタイズする
- SQL の直接組み立て(f-string, format, %演算子)は禁止。ORM またはパラメータバインドを使用する
- SECRET_KEY, API キー, パスワードをログに出力してはならない
- CORS の AllowAll は本番環境で使用禁止

## 業務ロジックルール
- 受注ステータスの遷移は「仮受注 → 確定 → 出荷 → 完了」の順序のみ許可する
- ステータスの逆行や飛び越しを許すコードは必ず指摘する
- 金額計算には Decimal 型を使用し、float は禁止する
- 税計算の端数処理は切り捨て(ROUND_DOWN)で統一する
- レコードの削除は論理削除(is_deleted フラグ)で行い、物理削除は禁止する
- 日時は必ず timezone-aware で扱う(naive datetime 禁止)

## レビュー出力フォーマット
各指摘は以下の形式で出力してください:
- **[重要度]** CRITICAL / WARNING / INFO
- **[カテゴリ]** セキュリティ / パフォーマンス / 業務ロジック / 規約
- **[場所]** ファイル名:行番号
- **[内容]** 問題の説明
- **[修正案]** 具体的な修正コード
"""

# コンテキスト長を拡張(レビュー対象のコードが長い場合に必要)
PARAMETER num_ctx 32768
# 温度を低く設定(一貫性のあるレビューのため)
PARAMETER temperature 0.1
# 繰り返しを抑制
PARAMETER repeat_penalty 1.1

モデルの作成と使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 専用レビューモデルを作成
ollama create our-reviewer -f review-rules.Modelfile

# テスト実行
ollama run our-reviewer "以下のコードをレビューしてください:
def calc_total(items):
    total = 0.0
    for item in items:
        total += item.price * item.quantity
    return total
"

期待される出力:

- [CRITICAL][業務ロジック] calc_total:2行目
  金額計算に float (0.0) を使用しています。
  Decimal 型を使用してください。
  修正案:
  from decimal import Decimal
  total = Decimal('0')

Modelfile のメンテナンス

ルールは時間とともに変わります。Modelfile をリポジトリで管理し、ルール変更時にモデルを再作成します。

1
2
3
# ルール更新後
ollama create our-reviewer -f review-rules.Modelfile
# 古いモデルは自動的に上書きされる

レベル 1 の限界

  • ルールが増えすぎるとシステムプロンプトが膨大になり、性能が低下する
  • 「過去にこういう障害があった」といった文脈は教えられない
  • ルールに書いていないパターンは検出できない

レベル 2:RAG で社内ドキュメントを検索させる(1〜2 日)

RAG(Retrieval-Augmented Generation)は、質問に関連するドキュメントをベクトル検索で取得し、LLM に渡す技術です。これにより、Modelfile に書ききれない大量の社内ドキュメントを参照したレビューが可能になります。

Open WebUI での RAG 構築

Open WebUI にはナレッジベース機能が内蔵されており、最も手軽に RAG を導入できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Open WebUI + Ollama を起動(まだ導入していない場合)
docker run -d -p 3000:8080 \
  -v ollama:/root/.ollama \
  -v open-webui:/app/backend/data \
  --name open-webui \
  --restart always \
  ghcr.io/open-webui/open-webui:ollama

# 埋め込みモデルをダウンロード
docker exec open-webui ollama pull nomic-embed-text:v1.5

リポジトリを丸ごと取り込んでいいのか?

社内のリポジトリにはソースコードと docs/ ディレクトリの両方が含まれています。「リポジトリを丸ごと RAG に入れてしまえばいいのでは?」と考えるのは自然ですが、丸ごと取り込むと検索精度が下がります

丸ごと取り込みの問題点

問題具体例
ノイズが多すぎるmigrations/, node_modules/, .lock ファイルが検索にヒットし、本当に必要な情報が埋もれる
チャンク品質が低いコードを機械的に 1000 文字で分割すると、関数の途中で切れて文脈が失われる
検索精度の低下似たコードが大量にあると、ベクトル検索が関連ドキュメントではなく類似コードを返してしまう
インデックスサイズの肥大化不要なファイルで DB が膨らみ、検索速度が遅くなる

推奨:フィルタリングして取り込む

リポジトリからレビューに役立つファイルだけを選別して取り込みます。

リポジトリ
├── docs/           ✅ 取り込む(設計書、仕様書)
├── README.md       ✅ 取り込む
├── CLAUDE.md       ✅ 取り込む(ルール)
├── models.py       ✅ 取り込む(DB スキーマ定義)
├── serializers.py  ✅ 取り込む(バリデーションルール)
├── tests/          ⚠️ 選択的に(テスト方針の理解用)
├── views.py        ⚠️ 選択的に(主要なビジネスロジック)
├── migrations/     ❌ 除外
├── static/         ❌ 除外
├── node_modules/   ❌ 除外
├── .git/           ❌ 除外
├── *.pyc           ❌ 除外
└── *.lock          ❌ 除外

取り込みスクリプトの例

複数のリポジトリから有用なファイルだけを収集するスクリプトです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#!/bin/bash
# index_repos.sh - 複数リポジトリからレビューに有用なファイルを収集
OUTPUT_DIR="./rag_docs"
mkdir -p "$OUTPUT_DIR"

for REPO_DIR in "$@"; do
  REPO_NAME=$(basename "$REPO_DIR")
  DEST="$OUTPUT_DIR/$REPO_NAME"
  mkdir -p "$DEST"

  # docs/ は丸ごと
  [ -d "$REPO_DIR/docs" ] && cp -r "$REPO_DIR/docs/"* "$DEST/" 2>/dev/null

  # 主要な設計ファイル
  for f in README.md CLAUDE.md AGENTS.md CONTRIBUTING.md; do
    [ -f "$REPO_DIR/$f" ] && cp "$REPO_DIR/$f" "$DEST/"
  done

  # Django のスキーマ・ロジック
  find "$REPO_DIR" -name "models.py" -not -path "*/migrations/*" \
    -not -path "*/.venv/*" \
    -exec sh -c 'cp "$1" "'"$DEST"'/$(echo "$1" | tr "/" "_")"' _ {} \;
  find "$REPO_DIR" -name "serializers.py" -not -path "*/.venv/*" \
    -exec sh -c 'cp "$1" "'"$DEST"'/$(echo "$1" | tr "/" "_")"' _ {} \;
  find "$REPO_DIR" -name "permissions.py" -not -path "*/.venv/*" \
    -exec sh -c 'cp "$1" "'"$DEST"'/$(echo "$1" | tr "/" "_")"' _ {} \;
  find "$REPO_DIR" -name "schema.py" -not -path "*/.venv/*" \
    -exec sh -c 'cp "$1" "'"$DEST"'/$(echo "$1" | tr "/" "_")"' _ {} \;

  echo "$REPO_NAME: $(ls "$DEST" | wc -l) 件のファイルを収集"
done

echo "合計: $(find "$OUTPUT_DIR" -type f | wc -l) 件"
1
2
# 使い方(複数リポジトリを一括収集)
./index_repos.sh ~/projects/order-system ~/projects/billing-api ~/projects/admin-portal

コード vs ドキュメントでチャンク戦略を変える

ファイル種別ごとにチャンクの切り方を変えると、さらに検索精度が上がります。

ファイル種別チャンク戦略理由
Markdown(docs)見出し(##)単位で分割セクションごとに意味がまとまっている
Python(models.py)クラス・関数単位で分割途中で切ると文脈が失われる
YAML / JSONファイル丸ごと 1 チャンク構造が壊れると意味をなさない

Open WebUI のデフォルト設定(チャンクサイズ 1000、オーバーラップ 200)はドキュメント向けです。Python コードを取り込む場合は、チャンクサイズを大きめ(2000〜4000)にするか、前処理で関数・クラス単位に分割してから取り込むのが効果的です。

実践的な運用:Claude Code でドキュメント化 → docs/ のみ RAG

ここまでフィルタリングやチャンク戦略を説明してきましたが、最も効果的な運用はもっとシンプルです。

普段 Claude Code で開発しているなら、ビジネスロジックやバリデーションルールを Claude Code にドキュメント化させ、docs/ ディレクトリに蓄積する。RAG にはその docs/ だけを取り込む。ソースコードは一切 RAG に入れません。

Claude Code(日常の開発作業中)
  ↓ ビジネスロジック・バリデーションルールをドキュメント化
docs/ に Markdown として蓄積
  ↓ docs/ のみを RAG に取り込み
Ollama + Qwen3(レビュー時に参照)

なぜソースコードより docs/ が優れるか

比較ソースコードを直接 RAGdocs/ のみ RAG
チャンク品質コードの途中で切れるMarkdown の見出し単位できれいに分割
検索精度似たコードが大量ヒット意味のある説明文がヒット
メンテナンスコード変更のたびに再インデックスドキュメント更新時のみ
ノイズimport 文や migrations が混入人間が読める説明のみ

ソースコードは LLM にとって「読みにくい資料」ですが、Claude Code が書いた説明文は「読みやすい資料」です。RAG の検索精度はドキュメントの質に直結するので、この差は大きいです。

Claude Code にドキュメント化させる内容

docs/
├── business-logic/
│   ├── order-status-flow.md      # 受注ステータス遷移ルール
│   ├── pricing-calculation.md    # 金額計算・税計算ロジック
│   └── permission-rules.md       # 権限・アクセス制御
├── validation/
│   ├── order-serializer.md       # 受注のバリデーションルール
│   ├── user-form.md              # ユーザー登録フォームのルール
│   └── payment-serializer.md     # 決済のバリデーションルール
└── schema/
    ├── order-models.md           # 受注関連テーブル定義と関係
    └── user-models.md            # ユーザー関連テーブル定義

Claude Code への指示例

開発作業の合間に、以下のようにドキュメント化を依頼します。

orders/serializers.py のバリデーションルールを
docs/validation/order-serializer.md にドキュメント化してください。
各フィールドのバリデーション条件、エラーメッセージ、
ビジネス上の理由を含めてください。
orders/models.py の Order モデルのステータス遷移ロジックを
docs/business-logic/order-status-flow.md にドキュメント化してください。
許可される遷移、禁止される遷移、遷移時の副作用(メール送信等)を
含めてください。

生成されたドキュメントは PR に含めてレビューし、コードと一緒にリポジトリに蓄積していきます。

この運用のメリット

  • RAG の取り込みが簡単: docs/ をそのままナレッジベースにアップロードするだけ
  • チャンク品質が高い: Claude Code が構造化した Markdown なので、見出し単位で自然に分割できる
  • 開発者にも役立つ: ドキュメント自体が新メンバーのオンボーディング資料になる
  • フィルタリング不要: ソースコードを取り込まないので、除外ルールを考える必要がない
  • メンテナンスが楽: ビジネスロジック変更時に Claude Code に「ドキュメントも更新して」と指示するだけ

取り込むべきドキュメント(優先度順)

上記の docs/ に加え、以下のドキュメントも RAG に取り込むと効果的です。

ドキュメント効果優先度
docs/(Claude Code 生成)ビジネスロジック・バリデーションの理解最高
コーディング規約ルール違反の検出
過去の PR レビューコメントレビュー観点の学習
障害報告・ポストモーテム過去の失敗パターンの回避
API 仕様書インターフェース整合性の確認

ナレッジベースの設定手順

  1. Open WebUI にログイン(http://localhost:3000
  2. Workspace → Knowledge を開く
  3. New Knowledge をクリックし、名前を入力(例: coding-standards
  4. ドキュメントをドラッグ & ドロップでアップロード
  5. チャット画面で #coding-standards と入力すると、ナレッジベースを参照しながら回答

埋め込みモデルの設定

デフォルトの埋め込みモデルでも動作しますが、日本語の精度を上げるには設定を調整します。

管理パネル → 設定 → ドキュメント で以下を設定:

設定項目推奨値
埋め込みモデルエンジンOllama
埋め込みモデルnomic-embed-text:v1.5
チャンクサイズ1000
チャンクオーバーラップ200

コードレビュー用のプロンプト例

#coding-standards #incident-reports を参考にして、
以下のコード差分をレビューしてください。
過去の障害パターンに類似する箇所があれば特に重点的に指摘してください。

```diff
(ここに git diff を貼り付け)

### Python スクリプトで RAG レビューを自動化

Open WebUI を使わず、Python で直接 RAG パイプラインを組むこともできます。

```python
# review_with_rag.py
import chromadb
from chromadb.utils import embedding_functions
import subprocess
import requests

# ベクトル DB の初期化
ef = embedding_functions.OllamaEmbeddingFunction(
    model_name="nomic-embed-text:v1.5",
    url="http://localhost:11434"
)
client = chromadb.PersistentClient(path="./review_db")
collection = client.get_or_create_collection(
    name="codebase_knowledge",
    embedding_function=ef
)

def index_documents(docs_dir: str):
    """社内ドキュメントをベクトル DB に取り込む"""
    import glob
    files = glob.glob(f"{docs_dir}/**/*.md", recursive=True)
    for i, filepath in enumerate(files):
        with open(filepath) as f:
            content = f.read()
        collection.add(
            documents=[content],
            ids=[f"doc_{i}"],
            metadatas=[{"source": filepath}]
        )
    print(f"{len(files)} 件のドキュメントを取り込みました")

def review_with_context(diff: str) -> str:
    """差分に関連するドキュメントを検索し、レビューを実行"""
    # 関連ドキュメントを検索
    results = collection.query(query_texts=[diff], n_results=5)
    context = "\n---\n".join(results["documents"][0])

    # Ollama API でレビュー実行
    response = requests.post(
        "http://localhost:11434/api/chat",
        json={
            "model": "our-reviewer",
            "stream": False,
            "messages": [{
                "role": "user",
                "content": f"""以下の社内ドキュメントを参考にしてコードレビューしてください。

## 関連ドキュメント
{context}

## レビュー対象の差分
```diff
{diff}
```"""
            }]
        }
    )
    return response.json()["message"]["content"]

if __name__ == "__main__":
    # 初回: ドキュメントの取り込み
    # index_documents("./docs")

    # レビュー実行
    diff = subprocess.run(
        ["git", "diff", "HEAD~1"],
        capture_output=True, text=True
    ).stdout
    print(review_with_context(diff))

レベル 2 の限界

  • ドキュメントの品質に依存する(古い・不正確な資料があると逆効果)
  • ベクトル検索の精度は完璧ではなく、関連ドキュメントを取りこぼすことがある
  • ドキュメントの更新時に再インデックスが必要

レベル 3:Few-shot でレビューパターンを学習させる(数日)

Few-shot とは、「こういうコードにはこう指摘する」という具体例をプロンプトに含める手法です。レベル 1 のルール記述を、実例で補強します。

Few-shot 事例ファイルの作成

過去の PR レビューから「良い指摘」を集め、JSON ファイルにまとめます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[
  {
    "bad_code": "total = sum(item.price * item.quantity for item in order.items.all())",
    "review": "[CRITICAL][パフォーマンス] order.items.all() で N+1 クエリが発生します。prefetch_related を使用してください。",
    "good_code": "# ビューまたはリゾルバで prefetch\norder = Order.objects.prefetch_related('items').get(id=order_id)\ntotal = sum(item.price * item.quantity for item in order.items.all())"
  },
  {
    "bad_code": "if order.status == '出荷':\n    order.status = '仮受注'\n    order.save()",
    "review": "[CRITICAL][業務ロジック] 受注ステータスが「出荷 → 仮受注」に逆行しています。許可される遷移は「仮受注 → 確定 → 出荷 → 完了」のみです。",
    "good_code": "# ステータス遷移バリデーション\nSTATUS_FLOW = {'仮受注': '確定', '確定': '出荷', '出荷': '完了'}\nnext_status = STATUS_FLOW.get(order.status)\nif next_status:\n    order.status = next_status\n    order.save()"
  },
  {
    "bad_code": "tax = int(price * 0.1)",
    "review": "[CRITICAL][業務ロジック] float 演算と int() による切り捨てが混在しています。Decimal + ROUND_DOWN で統一してください。",
    "good_code": "from decimal import Decimal, ROUND_DOWN\ntax = (Decimal(str(price)) * Decimal('0.1')).quantize(Decimal('1'), rounding=ROUND_DOWN)"
  }
]

Few-shot を含む Modelfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# review-fewshot.Modelfile
FROM qwen3:14b

SYSTEM """
あなたは当社のシニアエンジニアです。以下のルールとレビュー事例に基づいてコードレビューを行います。

## ルール
(レベル 1 と同じルールをここに記述)

## レビュー事例

### 事例 1: N+1 クエリ
悪い例:
```python
total = sum(item.price * item.quantity for item in order.items.all())

指摘: [CRITICAL][パフォーマンス] order.items.all() で N+1 クエリが発生します。 修正案:

1
order = Order.objects.prefetch_related('items').get(id=order_id)

事例 2: ステータス逆行

悪い例:

1
order.status = '仮受注'  # 現在のステータスが「出荷」

指摘: [CRITICAL][業務ロジック] ステータスが逆行しています。 修正案: STATUS_FLOW 辞書でバリデーションを実装してください。

事例 3: 金額の float 計算

悪い例:

1
tax = int(price * 0.1)

指摘: [CRITICAL][業務ロジック] float 演算は金額計算で使用禁止です。 修正案: Decimal + ROUND_DOWN を使用してください。

上記の事例と同様のパターンを検出した場合、同じ形式で指摘してください。 """

PARAMETER num_ctx 32768 PARAMETER temperature 0.1


### Claude Code で Few-shot JSON を自動生成する

事例を手作業で 1 件ずつ書くのは大変です。**GitHub の PR レビュー履歴から Claude Code に自動生成させる**のが最も効率的です。

#### Step 1:PR レビューコメントの一括取得

```bash
# 過去の PR レビューコメントを取得(複数リポジトリ対応)
for repo in order-system billing-api admin-portal; do
  gh api "repos/{owner}/$repo/pulls/comments" \
    --paginate \
    --jq '.[] | {
      body: .body,
      diff_hunk: .diff_hunk,
      path: .path,
      created_at: .created_at,
      user: .user.login
    }' >> pr_reviews_raw.json
done

取得される生データはこのような形式です。

1
2
3
4
5
6
7
{
  "body": "ここ、float で計算してるけど Decimal にしないとまずいよ",
  "diff_hunk": "@@ -12,3 +12,5 @@\n+    tax = int(price * 0.1)",
  "path": "orders/services.py",
  "created_at": "2025-11-15T09:30:00Z",
  "user": "senior-dev"
}

Step 2:Claude Code に選別・構造化を依頼

pr_reviews_raw.json には過去の PR レビューコメントが入っています。
これを以下の形式の JSON に変換して docs/few-shot-reviews.json に
出力してください。

## 出力形式
[
  {
    "bad_code": "問題のあるコード(diff_hunk から抽出)",
    "review": "[重要度][カテゴリ] 指摘内容(body を構造化)",
    "good_code": "修正後のコード(body 内に含まれていれば抽出)",
    "category": "セキュリティ | パフォーマンス | 業務ロジック | 規約"
  }
]

## 選別基準
以下の条件を満たすコメントだけを採用してください:
- 具体的なコード修正案が含まれている
- 業務ルールやコーディング規約に言及している
- 「LGTM」「OK」「typo」だけのコメントは除外
- 曖昧な指摘(「もう少し考えて」「要検討」等)は除外

## 件数
30〜50 件に絞り込んでください。
カテゴリのバランスを考慮し、特定のカテゴリに偏らないようにしてください。

Claude Code はコメントの質を判断できるので、有用な事例だけを選別してくれます。手動で数百件のコメントをふるいにかけるより遥かに効率的です。

Step 3:生成結果の確認と調整

docs/few-shot-reviews.json を読み込んで、以下を確認してください:
1. カテゴリの分布(偏りがないか)
2. 重要度の分布(CRITICAL ばかりに偏っていないか)
3. 当社固有の業務ルールに関する事例が含まれているか
4. 不足しているパターンがあれば追加提案してください

Step 4:定期的な更新

新しい PR レビューが蓄積されたら、定期的に JSON を更新します。

1
2
3
4
5
6
7
8
# 前回更新以降のレビューコメントだけ追加取得
gh api "repos/{owner}/{repo}/pulls/comments" \
  --paginate \
  --jq '.[] | select(.created_at > "2026-03-01T00:00:00Z") | {
    body: .body,
    diff_hunk: .diff_hunk,
    path: .path
  }' >> pr_reviews_new.json
pr_reviews_new.json に新しい PR レビューコメントがあります。
docs/few-shot-reviews.json に追加すべき高品質な事例があれば
追加してください。合計 50 件を超える場合は、古い事例や
重複するパターンの事例を入れ替えてください。

Few-shot JSON から Modelfile を自動生成する

レベル 1 で紹介した generate_modelfile.sh と組み合わせ、JSON から Modelfile のレビュー事例セクションを自動生成できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/bin/bash
# generate_fewshot_modelfile.sh
RULES=$(cat rules.md)

# JSON から Markdown 形式の事例テキストを生成
EXAMPLES=$(python3 -c "
import json
with open('docs/few-shot-reviews.json') as f:
    reviews = json.load(f)
for i, r in enumerate(reviews[:30], 1):
    print(f'### 事例 {i}: {r[\"category\"]}')
    print(f'悪い例:')
    print(f'\`\`\`python')
    print(r['bad_code'])
    print(f'\`\`\`')
    print(f'指摘: {r[\"review\"]}')
    if r.get('good_code'):
        print(f'修正案:')
        print(f'\`\`\`python')
        print(r['good_code'])
        print(f'\`\`\`')
    print()
")

cat > review-fewshot.Modelfile << MODELFILE
FROM qwen3:14b
SYSTEM """
あなたは当社のシニアエンジニアです。以下のルールとレビュー事例に基づいてコードレビューを行います。

## ルール
${RULES}

## レビュー事例
${EXAMPLES}

上記の事例と同様のパターンを検出した場合、同じ形式で指摘してください。
"""

PARAMETER num_ctx 32768
PARAMETER temperature 0.1
MODELFILE

ollama create our-reviewer-fewshot -f review-fewshot.Modelfile
echo "Few-shot モデルを更新しました"

レベル 3 の限界

  • 事例が増えすぎるとコンテキストウィンドウを圧迫する(30〜50 件が目安)
  • 事例にないパターンの検出力は上がらない
  • JSON の定期更新が必要(ただし Claude Code で自動化可能)

レベル 4:LoRA ファインチューニング(1〜2 週間)

LoRA(Low-Rank Adaptation)は、モデルの重みの一部を少量のデータで再訓練する手法です。社内の PR レビュー履歴を学習データとして、Qwen3 自体のコードレビュー能力を向上させます。

レベル 1〜3 との違い

レベル 1〜3レベル 4(LoRA)
カスタマイズ対象プロンプト(入力)モデルの重み(頭脳自体)
コンテキスト消費ルール・事例分だけ消費消費しない(学習済み)
未知のパターン検出困難汎化により検出可能
更新コストファイル編集のみ再訓練が必要

学習データの準備

PR レビュー履歴から「入力(コード差分)→ 出力(レビューコメント)」のペアを作成します。形式は Alpaca 形式(instruction / input / output の 3 フィールド)が標準的です。

1
2
3
4
5
6
7
[
  {
    "instruction": "以下のコード差分をレビューしてください。",
    "input": "diff --git a/orders/views.py\n+    total = sum(float(i.price) * i.qty for i in items)",
    "output": "[CRITICAL][業務ロジック] orders/views.py: 金額計算に float を使用しています。Decimal 型に変更してください。\n[WARNING][パフォーマンス] items の取得方法を確認してください。N+1 クエリの可能性があります。"
  }
]

目安として 200〜500 件の学習データがあれば、ドメイン特化の効果が現れ始めます。

Few-shot JSON(レベル 3)との関係

レベル 3 で作成した docs/few-shot-reviews.json と LoRA の学習データは形式が似ていますが、量と目的が異なります

Few-shot(レベル 3)LoRA 学習データ(レベル 4)
件数30〜50 件200〜500 件以上
使い方プロンプトに埋め込むモデルの重みを更新する
品質基準厳選(代表的なパターンのみ)網羅的(バリエーションが多いほど良い)
同じパターンの重複不要(1 パターン 1 事例)必要(同じルール違反の異なるコード例が汎化を促す)
形式独自 JSONAlpaca 形式(instruction / input / output)

重要な違いは「重複」の扱いです。Few-shot では N+1 クエリの事例は 1 件で十分ですが、LoRA では「views.py での N+1」「serializers.py での N+1」「GraphQL リゾルバでの N+1」のように異なるコンテキストで同じ種類の問題が現れる事例を複数含めることで、モデルがパターンを汎化して学習します。

Few-shot JSON をベースに学習データを拡張する

ゼロから 300 件の学習データを作るのは大変です。レベル 3 で作った JSON を出発点にして、Claude Code に拡張させるのが効率的です。

レベル 3: 厳選 30〜50 件(Claude Code で選別済み)
    ↓ ベースとして再利用
レベル 4: 200〜500 件に拡張(Claude Code でバリエーション追加)

Step 1:既存の事例をバリエーション展開

docs/few-shot-reviews.json の 50 件をベースに、
LoRA ファインチューニング用の学習データを 300 件に拡張してください。

拡張方法:
- 既存の事例の「悪いコード」を別のバリエーションで書き直す
  (変数名を変える、関数の構造を変える、別のモデルで同じ問題等)
- 同じルール違反でも異なるファイル・コンテキストの事例を追加する
- pr_reviews_raw.json から追加の事例を選別して含める
- 「問題なし(LGTM)」の事例も 20% 程度含める
  (問題がないコードに誤って指摘しないように学習させるため)

出力形式(Alpaca 形式):
[
  {
    "instruction": "以下のコード差分をレビューしてください。",
    "input": "差分コード",
    "output": "レビューコメント"
  }
]

出力先: docs/lora-training-data.json

Step 2:カテゴリバランスの確認

docs/lora-training-data.json のカテゴリ分布を確認してください。

以下のバランスが理想的です:
- 業務ロジック: 30%
- パフォーマンス: 20%
- セキュリティ: 20%
- コーディング規約: 15%
- LGTM(問題なし): 15%

偏りがあれば不足カテゴリの事例を追加してください。

「問題なし(LGTM)」の事例を含めるのがポイントです。これがないと、モデルが何にでも問題を指摘する「過剰検出」の傾向を持つようになります。

Step 3:形式変換

Few-shot JSON と LoRA 学習データは形式が異なるため、変換が必要です。

docs/few-shot-reviews.json(レベル 3 形式)を
docs/lora-training-data.json(Alpaca 形式)に変換するスクリプトを
書いてください。

変換ルール:
- bad_code → input(diff 形式に変換)
- review → output
- instruction は固定文「以下のコード差分をレビューしてください。」
- good_code は output に「修正案:」として追記

Unsloth によるファインチューニング

Unsloth は LoRA ファインチューニングを高速化するライブラリです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# fine_tune.py(Google Colab または GPU マシンで実行)
from unsloth import FastLanguageModel
import json

# ベースモデルの読み込み
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Qwen3-14B",
    max_seq_length=8192,
    load_in_4bit=True,
)

# LoRA アダプタの設定
model = FastLanguageModel.get_peft_model(
    model,
    r=16,                # LoRA のランク
    lora_alpha=16,
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    lora_dropout=0,
    bias="none",
)

# 学習データの読み込み
with open("review_training_data.json") as f:
    dataset = json.load(f)

# トレーニング実行
from trl import SFTTrainer
from transformers import TrainingArguments

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset,
    args=TrainingArguments(
        output_dir="./review_model",
        num_train_epochs=3,
        per_device_train_batch_size=2,
        learning_rate=2e-4,
    ),
)
trainer.train()

# GGUF 形式でエクスポート(Ollama 用)
model.save_pretrained_gguf(
    "review_model_gguf",
    tokenizer,
    quantization_method="q4_k_m",
)

Ollama への取り込み

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# fine-tuned.Modelfile
FROM ./review_model_gguf/model-q4_k_m.gguf

SYSTEM """
あなたは当社のコードレビュー専門 AI です。
(レベル 1 のルールをここにも記述)
"""

PARAMETER num_ctx 32768
PARAMETER temperature 0.1
1
ollama create our-reviewer-ft -f fine-tuned.Modelfile

レベル 4 の注意点

  • GPU が必要: ファインチューニングには VRAM 16 GB 以上の GPU が必要(Google Colab の無料枠でも可能)
  • データ品質が命: 不正確なレビューコメントを学習すると逆効果
  • 定期的な再訓練: 新しいレビューパターンが蓄積されたら再訓練が必要
  • 過学習に注意: 学習データが少なすぎると、特定パターンにしか反応しなくなる

実践的な導入ロードマップ

第 1 週:レベル 1(Modelfile)
  ├ 社内のコーディング規約を Modelfile に記述
  ├ 業務ロジックのルールを追加
  └ Git フック(前回の記事パターン 1)と組み合わせて運用開始

第 2〜3 週:レベル 2(RAG)
  ├ Open WebUI を導入
  ├ 障害報告・設計書をナレッジベースに取り込み
  └ レビュー精度を評価し、不足するドキュメントを追加

第 4 週:レベル 3(Few-shot)
  ├ PR レビュー履歴から良い指摘を 20〜30 件選別
  ├ Modelfile に事例を追加
  └ 検出パターンの改善を確認

第 5〜6 週以降:レベル 4(LoRA)(必要に応じて)
  ├ PR レビュー履歴 200 件以上を学習データに整形
  ├ ファインチューニング実行
  └ 既存モデルとの比較評価

各レベルの組み合わせ

4 つのレベルは排他的ではなく、組み合わせて使うのが最も効果的です。

レベル 4(LoRA)で鍛えたモデル
  + レベル 1(Modelfile)でルールを明示
  + レベル 2(RAG)で社内ドキュメントを参照
  + レベル 3(Few-shot)で事例を補強
  = 社内業務に特化した高精度レビュー AI

最終的な構成図:

Claude Code → コード生成
      ↓
Git フック / OpenHands
      ↓
Ollama
      ↓
Qwen3(LoRA ファインチューニング済み)
  + システムプロンプト(業務ルール)
  + RAG(社内ドキュメント検索)
  + Few-shot(レビュー事例)
      ↓
レビュー結果(review.md)

まとめ

  • レベル 1(Modelfile)から始める: 業務ルールをシステムプロンプトに書き込むだけで、即日導入可能
  • レベル 2(RAG)で文脈を与える: 障害報告・設計書・コーディング規約をベクトル DB に格納し、レビュー時に参照させる
  • レベル 3(Few-shot)でパターンを教える: 過去の良いレビュー事例を具体的に示すことで、検出精度が向上する
  • レベル 4(LoRA)でモデル自体を鍛える: PR レビュー履歴を学習データとして Qwen3 を再訓練し、プロンプトに依存しない検出力を獲得
  • 組み合わせが最強: 4 つのレベルは排他的ではなく、すべて組み合わせることで最高精度のレビュー AI が実現できる
  • データ品質がすべての基盤: どのレベルでも、入力するルール・ドキュメント・事例・学習データの品質が精度を決定する

参考

付録:レベル 4 に必要な PC スペックと推奨構成

LoRA ファインチューニングには GPU(VRAM 24GB 以上)が必要です。ただし、Unsloth フレームワークを使えば VRAM 使用量を最大 60% 削減でき、Google Colab の無料枠でも実行可能です。

必要スペック

項目最低要件推奨構成
GPUVRAM 24GB(RTX 3090)VRAM 24GB(RTX 4090)
RAM32GB64GB
ストレージSSD 500GBNVMe SSD 1TB
CPU8コア以上12コア以上

3 つの選択肢

選択肢 1:Google Colab(初期投資ゼロ)— まずはここから

Unsloth を使えば、Google Colab の無料 T4 GPU(16GB VRAM)でも Qwen3 14B の LoRA ファインチューニングが可能です。4bit 量子化と LoRA を組み合わせることで、VRAM 使用量を大幅に削減します。

プランGPUVRAM月額
無料T416GB¥0
Colab ProA10040GB¥1,179

日本語の実践ガイド: Google Colab 無料 GPU で Qwen3 をファインチューニング

選択肢 2:RTX 3090 中古(コスパ重視)

既存の PC に GPU を増設する方法です。RTX 3090 は 24GB VRAM を搭載し、中古市場で 11〜13 万円で入手可能です。

GPUVRAM価格帯備考
RTX 3090(中古)24GB11〜13 万円コスパ最高。消費電力 350W、電源 850W 以上が必要
RTX 4090(新品)24GB30〜40 万円最高性能。推論速度も速い

注意: GPU 増設には電源ユニットの容量確認が必要です。RTX 3090 は 350W、RTX 4090 は 450W を消費します。

選択肢 3:専用ワークステーション購入(本格運用向け)

継続的にファインチューニングを行う場合のみ検討してください。RTX 4090 搭載ワークステーションは 50〜80 万円です。

おすすめの導入順序

ステップ 1: Google Colab(無料)で Unsloth + Qwen3 14B の LoRA を試す
    ↓  効果を確認できたら
ステップ 2: RTX 3090 中古(11〜13 万円)を既存 PC に増設
    ↓  頻繁に再訓練するなら
ステップ 3: RTX 4090(30 万円〜)に移行

結論: まずは Google Colab + Unsloth で無料で試すのが最もリスクが低いです。Qwen3 14B の LoRA なら無料枠でも動作し、効果の検証にはこれで十分です。

Apple Silicon(Mac)で LoRA ファインチューニング

Mac ユーザーには Apple Silicon + MLX という選択肢もあります。Apple Silicon は統合メモリを採用しており、CPU と GPU がメモリを共有します。NVIDIA GPU のように VRAM が 24GB に制限されず、搭載メモリ全体を GPU 的に活用できます。

NVIDIA: CPU RAM 64GB ←→ GPU VRAM 24GB(別々)
Apple:  統合メモリ 64GB(CPU も GPU も共有)

Apple 製品の推奨モデル

モデルメモリ価格(税込)LoRA 対応
Mac mini M4 Pro 48GB48GB約 27 万円Qwen3 8B(余裕)/ 14B(ギリギリ)
Mac mini M4 Pro 64GB64GB約 33 万円Qwen3 14B(余裕)← おすすめ
Mac Studio M4 Max 128GB128GB約 50 万円〜Qwen3 30B まで対応

**最もお手頃は Mac mini M4 Pro 64GB(約 33 万円)**です。実際のベンチマークでも、Mac mini M4 Pro 64GB でローカル LLM が実用的に動作すると報告されています。普段は Mac として使い、LoRA 学習時に MLX を走らせる運用ができます。

MLX による LoRA ファインチューニング

Apple が開発した MLX フレームワークで、1 コマンドで LoRA ファインチューニングが実行可能です。

1
2
3
4
5
6
7
8
9
# MLX LM をインストール
pip install mlx-lm

# LoRA ファインチューニング
mlx_lm.lora \
  --model mlx-community/Qwen3-14B-4bit \
  --data ./lora-training-data \
  --train \
  --iters 1000

MLX は量子化が深く統合されており、4bit 量子化モデル上で LoRA アダプタを訓練できるため、メモリ使用量を大幅に削減できます。

NVIDIA GPU との比較

Mac mini M4 Pro 64GBRTX 3090 中古 + PC
価格約 33 万円11〜13 万円(GPU のみ)
有効メモリ64GB(統合)24GB VRAM
学習速度やや遅い速い
推論速度良好良好
消費電力低い(静音)高い(350W)
日常利用Mac として普段使いも可能GPU 増設のみ
フレームワークMLXPyTorch + Unsloth

既に Mac ユーザーなら Mac mini M4 Pro 64GB が最もバランスの良い選択肢です。Windows PC を持っていて GPU を増設できるなら、RTX 3090 中古がコスパでは勝ります。