概要

クラウド API(Claude、GPT-4 等)を一切使わず、ローカルで動作する Qwen3.5-122B-A10B(Q5_K_M 量子化)だけで Django 5 の旅行予約サイトをフルスタック生成できるか検証した。MCP(Model Context Protocol)経由のコーディングエージェントにファイルの読み書きを任せ、初期仕様のワンショット生成(+手修正)の後、2回の機能拡張指示で Shop・Reviews・Search・Accounts を追加させた。各段階でいくつかの手修正が必要だったが、大枠のコード生成はすべてローカル推論で完結した。

クラウドの大規模モデルであれば、この程度のコード生成はすでに珍しくない。本稿の焦点は「手元のハードウェアで動く量子化 MoE モデルでも同等のことができるか」にある。

ローカル推論環境

項目内容
モデルQwen3.5-122B-A10B(MoE、アクティブ 10B / 総パラメータ 122B)
量子化Q5_K_M(GGUF、3分割シャード)
推論エンジンik_llama.cpp(OpenAI 互換 API サーバー)
MCP ツール(自作)ctree(コードシンボル解析)、pathfinder(パス解決)
MCP ツール(OSS)serena(セマンティックコード操作)、filesystem(ファイル読み書き)、ripgrep(検索)
コンテキスト使用量約 77K プロンプトトークン

ポイントは、推論がすべてローカルマシン上で完結していること。外部 API への通信は発生せず、トークン課金もない。エージェントは serena のシンボル操作ツールや filesystem の読み書きツール、ripgrep による検索、ctree によるコード構造解析、pathfinder によるパス解決といった複数の MCP サーバーを組み合わせて、Django アプリケーションを段階的に構築した。

検証の意図

クラウドではなくローカルで試す理由

Claude Sonnet や GPT-4o を使えばフルスタック Web アプリの生成は実績のある作業になっている。しかし、クラウド API に依存する限り:

  • トークン単価の積み重ねでコストが膨らむ
  • プロプライエタリコードを外部に送信する必要がある
  • API の可用性やレート制限に縛られる

ローカル推論でクラウド同等の結果が得られるなら、これらの制約から解放される。122B パラメータの MoE モデルを Q5_K_M に量子化して ik_llama.cpp で動かした場合、実用的なコーディングエージェントとして機能するのか — それが本検証の問いだった。

ワンショット生成のアプローチ

完全な仕様書を一度に提供し、全ファイルを一括生成させる方式を採用した。これにより:

  • ドメインモデルとビューの整合性が維持されやすい
  • テンプレートとフォームの連携が仕様通りに実装されやすい
  • シードデータがモデル定義と一致する
  • 管理画面の設定がモデルフィールドと矛盾しにくい

仕様設計

技術スタック

要素選定理由
バックエンドDjango 5.xPython 3.13 互換、ORM 完備、管理画面自動生成
フロントエンド JSAlpine.js v3 (CDN)Django テンプレート内で動作、ビルド不要
CSSTailwind CSSMaterial Design 3 インスパイアのユーティリティファースト
パッケージ管理uv高速な Python パッケージマネージャー
データベースSQLite開発環境のみ、設定不要

初期ドメインモデル

初期仕様では3つのエンティティで旅行予約の基本フローを表現した:

  TravelPackage  1 ──── N  Tour
Tour           1 ──── N  Reservation
  

TravelPackage(旅行商品):

  • タイトル、スラグ、リージョン、期間、画像 URL
  • is_published フラグで公開制御
  • min_price プロパティで最安ツアー価格を動的取得

Tour(出発日程):

  • 特定の出発日・帰着日・価格・残席数
  • ステータス管理(available / soldout / cancelled)
  • is_reservable プロパティで予約可否判定

Reservation(予約):

  • 顧客情報(氏名、メール、電話、人数、備考)
  • Tour への FK で紐付け
  • 決済なし(DB 保存のみ)

ビジネスルール

仕様書では以下のルールを明示的に定義した:

  1. is_published=True のパッケージのみフロントエンドに表示
  2. status="available" のツアーのみ予約可能
  3. 価格表示はパッケージ内の最安ツアー価格(Min 集約)
  4. 予約フローはフォーム → 確認プレビュー → 完了の3ステップ
  5. 確認画面での POST で初めて DB に保存(セッション経由)

UI ワイヤーフレーム

仕様書ではレイアウトを ASCII アートで定義し、LLM が視覚構造を理解した上で Tailwind クラスを適用できるようにした:

  ┌─────────────────────────────────────────────────┐
│  HERO SECTION: bg-gradient-to-r from-blue-600   │
│  [SVG airplane animation flying across]         │
│  "Discover Your Next Adventure"                 │
│  [ Browse All Packages → ]                      │
├─────────────────────────────────────────────────┤
│  FEATURED PACKAGES (card grid)                  │
│  ┌──────┐  ┌──────┐  ┌──────┐                  │
│  │Card 1│  │Card 2│  │Card 3│                   │
│  └──────┘  └──────┘  └──────┘                  │
└─────────────────────────────────────────────────┘
  

Material Design 3 デザイン指定

役割Tailwind クラス用途
Primarybg-blue-600ボタン、リンク、アクティブ状態
Surfacebg-whiteカード、モーダル、フォーム背景
Elevation Level 2shadow-mdメインカード、ヘッダー
Border Radiusrounded-2xlカード、rounded-xl ボタン

ホバーエフェクト:hover:shadow-lg hover:-translate-y-1 transition-all duration-200

ローカル LLM が生成したコード

Alpine.js フィルタリング

エージェントは仕様書の Alpine.js 指定を読み取り、クライアントサイドフィルタリングを生成した:

  x-data="{
    region: '',
    maxPrice: ''
}"
  
  x-show="(region === '' || $el.dataset.region === region) &&
        (maxPrice === '' || parseInt($el.dataset.minPrice) <= parseInt(maxPrice))"
  

サーバーサイド API なしで動作するこの方式は、LLM が Alpine.js のリアクティブパターンを正しく理解していることを示している。

セッションベース予約フロー

3ステップの予約フローも仕様通りに生成された:

  1. フォーム入力/reserve/<tour_id>/):バリデーション後、データをセッションに保存
  2. 確認プレビュー/reserve/<tour_id>/confirm/):セッションから読み出し、サマリー表示
  3. 完了/reserve/success/):確認 POST で DB 保存、セッションクリア
  # ReservationCreateView: POST でセッションに保存
request.session['reservation_data'] = form.cleaned_data
return redirect('reservation_confirm', tour_id=tour.id)

# ReservationConfirmView: POST で DB 保存
data = request.session.pop('reservation_data')
Reservation.objects.create(tour=tour, **data)
return redirect('reservation_success')
  

Django 管理画面

インライン編集を含む管理画面設定も、モデル定義と矛盾なく生成された:

  class TourInline(admin.TabularInline):
    model = Tour
    extra = 1

@admin.register(TravelPackage)
class TravelPackageAdmin(admin.ModelAdmin):
    list_display = ["title", "region", "duration_days", "is_published"]
    list_editable = ["is_published"]
    prepopulated_fields = {"slug": ("title",)}
    inlines = [TourInline]
  

シードデータ

seed_demo マネジメントコマンドで5つのパッケージ(計14ツアー)を自動生成。リージョン別のフィルタリングテストに使用可能。

エージェントによる段階的拡張

初期のワンショット生成後に手修正を加えてベースを安定させた上で、2回の追加仕様指示を行った。エージェントは既存コードを読み取りながら以下のモデルとアプリを追加した(各段階でいくつかの手修正が発生):

追加アプリ主なモデルエージェントの作業内容
shopShop(店舗情報)models.py に Shop モデル追加、admin 登録、テンプレート作成
reviewsReview, Rating, ReviewPhotoツアーレビュー機能一式、承認フロー付き
searchSearchIndex, TrendingKeyword検索インデックスとトレンド管理
accountsUser, UserProfile, UserActivityユーザー認証とプロフィール管理

エージェントは既存の tours/models.py を読み込んで FK 関係を確認し、新モデルとの整合性を取りながらコードを生成した。ただし、テンプレートの引用符エスケープや一部のインポート漏れなど、手修正が必要な箇所は各段階で発生した。完全な自動生成というよりは「8割をエージェントが書き、残り2割を人間が直す」という作業配分になった。それでも、この「既存コードの文脈を理解して拡張する」動作がローカル MoE モデルで機能していた点は注目に値する。

生成結果

画面キャプチャ

トップページ:ヒーローセクションと Featured Products カードグリッド
トップページ — ヒーローセクション + Featured Products(ローカル推論で生成)
トップページ下部:ショップ情報と Available Tours セクション
エージェントが追加した Shop Locations + Available Tours セクション
パッケージ詳細ページ:Takayama & Japanese Alps のツアー一覧と価格ソート
パッケージ詳細ページ — ツアー一覧・価格表示・ソート機能
Django 管理画面:6アプリのモデル一覧
Django 管理画面 — Tours + Shop + Reviews + Search + Accounts の全モデル

初期生成ファイル構成

ファイル内容
tours/models.py3モデル + プロパティ + バリデーション
tours/admin.py3モデルの管理画面設定 + インライン
tours/forms.pyReservationForm (ModelForm)
tours/views.py6ビュー(Home, List, Detail, Form, Confirm, Success)
tours/urls.pyURL パターン定義
tours/management/commands/seed_demo.pyシードデータ
tours/templatetags/tour_filters.pyカスタムフィルター(multiply)
tours/templates/tours/*.html7テンプレート
static/css/custom.cssSVG アニメーション
config/settings.pyINSTALLED_APPS 追加

動作確認

ローカル LLM が生成したコードは、手修正を加えた上で以下の動作を確認できた:

  • マイグレーション実行後、サーバー起動可能
  • シードデータの投入でフロントエンドにパッケージが表示
  • フィルタリング・ソートの Alpine.js 機能が正常動作
  • 予約フロー(入力→確認→完了)が完走
  • 管理画面でのインライン編集が機能

手修正が必要だった主な箇所:

  • パッケージ詳細ページのテンプレートで Django テンプレートタグの引用符エスケープ({{ tour.end_date|date:"M j, Y" }} が生文字列として表示)
  • 一部のインポート漏れや型の不整合
  • 機能拡張時のテンプレート間の整合性

SVG アニメーション

ヒーローセクションには飛行機の飛行アニメーション(8秒ループ)と浮遊する雲(15秒ループ)を SVG + CSS キーフレームで生成。

得られた知見

1. ローカル MoE モデルのコーディング能力

Qwen3.5-122B-A10B (Q5_K_M) は、Django のモデル定義・ビュー・テンプレート・管理画面を整合性のある形で生成できた。77K トークンのコンテキストを使った段階的な拡張でも、既存コードとの整合性を維持していた。クラウドモデルと同等とまでは言えないが、仕様が明確であれば十分に実用的な生成結果を得られた。

2. 仕様書の詳細度が成否を分ける

ローカル LLM で精度を上げるには、仕様書側の情報量が鍵になる。特に以下が重要だった:

  • ドメインモデルのフィールド定義(型、制約、デフォルト値)
  • URL パターンとビューの対応表
  • UI ワイヤーフレーム(ASCII アート形式でも十分)
  • Tailwind クラスの具体的な指定

クラウドの大規模モデルは曖昧な指示からでも「いい感じ」に生成してくれることが多いが、ローカルモデルでは仕様の曖昧さがそのまま出力のブレに直結する。

3. 複数 MCP サーバーの連携

自作の ctree(コードシンボル解析)・pathfinder(パス解決)と、OSS の serena(セマンティックコード操作)・filesystem(ファイル読み書き)・ripgrep(検索)を組み合わせた MCP 環境は、ローカル LLM でも安定して動作した。エージェントは serena でシンボルを検索し、filesystem でファイルを読み書きし、ctree でコード構造を把握するという一連の流れを自律的に実行できた。

4. テンプレートのエスケープ問題

Django テンプレートタグの引用符処理は、ローカル LLM が苦手とする領域だった。{{ value|date:"M j, Y" }} のようなフィルター引数内の引用符が、生成時に正しくエスケープされないケースが見られた。この種の問題はクラウドモデルでも発生しうるが、ローカルモデルではやや頻度が高い印象。

まとめ

  1. ローカル推論だけで動作する Web アプリを生成:Qwen3.5-122B-A10B (Q5_K_M) でクラウド API なしにフルスタック Django サイトを構築
  2. 段階的な機能拡張にも対応:初期3モデルから6アプリへの拡張を、既存コードの文脈を理解しながら自律的に実行
  3. MCP エージェントとの統合:ローカル ik_llama.cpp + 複数 MCP サーバー(ctree / pathfinder / serena / filesystem / ripgrep)の組み合わせが実用的に機能
  4. 仕様書の質がローカル LLM の精度を左右:クラウドモデル以上に、明確な仕様定義が重要
  5. 各段階で手修正は発生:完全な自動生成ではないが、生成8割・手修正2割の作業配分でフルスタックアプリが構築できた