📝

LEGOの組み立て説明書をAIが自動生成 — 物理エンジンで「やり直し」させるAgentic AIの仕組み

に公開

BrickQuest

はじめに — きっかけは子どもたちのLEGO

うちには6歳と8歳の子どもがいて、二人ともLEGOが大好きです。

100円ショップや300円ショップで売っているブロックセットも喜んで買うのですが、毎回同じことが起きます。説明書がどこかに消える。パーツがなくなる。バラバラになったブロックが箱に溜まっていく。そして最終的にはゴミ箱へ。

「説明書がなくても、手元にあるブロックだけで何か新しいものを作れたら?」

それがBrick Questの出発点でした。普段から解決したいと思っていたテーマだったので、今回のハッカソンの題材に選びました。

ただ、正直に言うと、開発を始めたのは2月14日です。提出期限の前日。完成度はまだまだ低いですし、恥ずかしい部分もたくさんあります。それでも、「とにかく動くものを作って提出する」ことを目標に、できる限り形にしました。

Brick Questの2つのモード

Brick Questには2つの遊び方があります。アプリの上部メニューとの対応を示しながら説明します。

モード①「デザイン」— この写真をLEGOにして!

好きなキャラクターや動物の写真をアップロードすると、AIがそれをLEGO Brickheadz風の3Dフィギュアに変換するモードです。メニューの 「デザイン」 から始めます。

① 写真をアップロードしてデザインを依頼

メニューの「デザイン」を開き、好きな画像をアップロードします。メモ欄に「座った姿勢で」などの指示を追加することもできます。

② 送信 → バックグラウンドでAIが処理開始

「レゴデザインを作成」ボタンを押すと、ジョブがキューに入ります。処理はバックグラウンドで行われるので、待っている間に他の操作もできます。

③ デザイン履歴でステータスを確認

デザイン履歴では各ジョブの進行状況がリアルタイムに更新されます。「ビュー生成中」「ビュー完了」「プラン生成中」「完了」「失敗」などのステータスが表示されます。

④ AIが生成した正投影ビューを確認・承認

Geminiが参照画像を分析し、前面・側面・背面・斜め方向のLEGOブロック設計図を自動生成します。気に入ったら「OK — 組み立て説明書を生成」をクリック。気に入らなければ再生成もできます。

⑤ ワークスペースで3Dモデルを確認

承認後、AIが3D組み立て手順を生成します。完成したモデルはThree.jsの3Dビューアでステップごとに確認できます。

ステップ 操作
デザイン(画像アップロード) キャラクター、動物、何でもOK。参照画像をアップロード
正投影ビュー生成 Geminiが参照画像を分析し、前面・側面・背面のLEGOブロック設計図を自動生成
確認・承認 生成された設計図を確認。気に入らなければ再生成も可能
ビルド生成 承認すると、設計図をもとに3Dの組み立て手順を自動生成
ワークスペース Three.jsの3Dビューアでステップ再生

モード②「マイブリック」— 手元のブロックで何か作って!

家に転がっているバラバラのLEGOブロックを活用するモードです。メニューの 「マイブリック」 から始めます。

① ブリックスキャン — カメラでLEGOブロックを撮影

メニューの「マイブリック」→「ブリックスキャン」を開きます。写真を撮影するか、ギャラリーからアップロードします。サンプル画像で試すこともできます。

写真を選んだら、ブリックの量に応じてレベルを選択し「ブリックスキャン」ボタンを押します。

② スキャン結果 — AIが検出したパーツを確認

Gemini AIが写真を分析し、「2x4の赤いブリック」「1x2の青いプレート」などを自動認識します。検出されたパーツ一覧が表示されます。

各パーツをタップすると、詳細情報(パーツ名、サイズ、色、タイプ、形状)を確認できます。

③ コレクション — 蓄積されたパーツ一覧

スキャンを繰り返すことで、パーツがコレクションに蓄積されていきます。色別にフィルタリングしたり、手動で修正することもできます。

④ 組み立て — コレクションのパーツでAIビルド生成

コレクションに十分なパーツが溜まったら、「組み立て」画面で難易度とテーマを指定してAIにビルドプランを生成させます。

⑤ ワークスペース — 完成した3Dモデルを確認

生成が完了すると、ワークスペースで3Dモデルをステップごとに確認できます。回転・ズームで360度確認可能です。

メニュー 操作
ブリックスキャン カメラでLEGOブロックを撮影 → Gemini AIが「2x4の赤いブリック」「1x2の青いプレート」などを自動認識
コレクション 認識結果の一覧を確認・手動で修正できる。スキャンを重ねてパーツを蓄積していく
組み立て コレクションのパーツを使ってビルドを生成。難易度と作りたいテーマを指定するとAIが3D組み立て手順を生成
ワークスペース 生成されたビルドをThree.jsの3Dビューアで確認。ステップごとにブロックが積み上がっていく

2つのモードに共通すること

どちらのモードも、最終的に 「ワークスペース」 で3Dのビルド結果を確認します。そして、その手前にある 「AIが3Dのビルドプランを生成する」 ステップ。ここが最も難しいところでした。


短い時間の中で作っていくと、掘り下げるほど壁が見えてきます。

  • LEGOのパーツは種類が膨大 — 形・サイズ・特殊パーツ……AIに全部認識させられるのか?
  • 写真をLEGOに変換? — 参照画像からブロック化した設計図を作って、さらにそこから3D設計書まで生成できるのか?
  • テーマ選び間違えた? と思う瞬間も正直ありました。

それでも一つずつ潰していく中で、両方のモードに共通する一番大きな壁にぶつかりました。

問題 — AIは「空間」が苦手

Gemini(Google の LLM)に「このパーツで家を作って」と頼むと、各ブロックの座標・サイズを含むビルドプランを返してくれます。

しかし、実際に3D空間に並べてみると……

ブロック同士が重なっている 😱
同じ場所に2つのブロックが配置されてしまう。現実のLEGOでは物理的に不可能。

ブロックが空中に浮いている 😱
下に何もないのに、空中にブロックが配置されている。重力を無視している。

グリッドからずれている 😱
LEGOはスタッド(突起)の間隔に沿って配置する必要があるのに、半端な位置に置かれている。

つまり、LLMは創造的なデザインは得意だけど、物理的な正確さは苦手ということです。

これを人間が毎回手動で直すのは現実的ではありません。そこで、AIに自分で間違いを直させる仕組み = Agentic AI を実装しました。

解決策 — 「AIに間違いを教えて、やり直させる」ループ

アイデアはシンプルです:

AIが作る → 物理エンジンがチェック → ダメなら「ここが間違ってるよ」と教えて作り直させる

これを最大3回繰り返します。人間が介入する必要はありません。AIが自律的に品質を改善していきます。

それぞれのステップを、実際のコードと一緒に見ていきましょう。

Step 1: GeminiにJSON Schemaを渡してビルドプランを生成

ここがポイントです。Geminiには「こういう形のJSONで返してね」という型定義(JSON Schema)を渡します。すると、自由文ではなく構造化されたJSONが確実に返ってきます。

// packages/functions/src/services/geminiBuild.ts より

const response = await ai.models.generateContent({
  model: config.gemini.model,
  contents: { text: prompt },
  config: {
    responseMimeType: 'application/json',  // ← JSON形式で返すよう指定
    responseSchema: buildSchema,            // ← 各ブロックの座標・サイズの型定義
    thinkingConfig: { thinkingBudget: 8192 }, // ← 思考トークンで空間推論を強化
  },
});

Geminiが返すJSONはこんな構造です。各ブロックに座標(x, y, z)・サイズ・色・種類が入っています:

{
  "title": "Small House",
  "steps": [
    {
      "stepId": 1,
      "partName": "2x4 Red Brick",
      "position": { "x": 1.0, "y": 0, "z": 1.5 },
      "size": { "width": 2, "height": 1.2, "length": 4 },
      "color": "Red"
    },
    ...
  ]
}

Step 2: 物理エンジンが全ブロックをチェック

返ってきたビルドプランを4段階のパイプラインで検証・修正します。実際のコードです:

// packages/shared/src/utils/build-physics.ts より

export function fixBuildPhysicsWithReport(steps: BuildStepBlock[]): PhysicsResult {
  const corrections: PhysicsCorrectionEntry[] = [];

  // ① グリッド補正 — LEGOのスタッド間隔に合わせる
  for (const step of steps) {
    snapDimensions(step);   // 寸法を整数に丸める(width: 1.8 → 2)
    snapToStudGrid(step);   // 位置をグリッドに合わせる(x: 1.3 → 1.0)
  }

  // ② 並び替え — 地面に近いブロックから処理
  const sorted = [...steps].sort((a, b) => a.position.y - b.position.y);

  for (const step of sorted) {
    // ③ 重力チェック — 一番高い「支え」の上面まで落とす
    let supportY = 0; // デフォルトは地面
    for (const p of placed) {
      if (xzOverlapArea(box, p.box) >= 0.5) { // XZ方向に0.5スタッド以上の重なりがあれば「支え」
        supportY = Math.max(supportY, p.box.maxY);
      }
    }
    step.position.y = supportY; // 浮いてたら落とす

    // ④ 重なりチェック
    if (hasOverlap(correctedBox, placed)) {
      if (tryNudgeBrick(step, placed)) {
        // ±1スタッドずらして解決できた → セーフ
      } else {
        // ずらしてもダメ → このブロックは削除(dropped)
      }
    }
  }

  // 生き残ったブロックに連番を振り直す
  placed.forEach(({ step }, i) => { step.stepId = i + 1; });

  return { steps: outputSteps, report: { inputCount, outputCount, droppedCount, ... } };
}
段階 やること 具体例
① グリッド補正 位置をスタッド間隔にスナップ x=1.3 → x=1.0
② 並び替え 地面に近い順で処理 土台 → 壁 → 屋根
③ 重力チェック 浮いてるブロックを支えの上まで落とす y=3.6 → y=2.4
④ 重なりチェック ±1スタッドずらす。無理なら削除 (2,0,3) → (3,0,3)、または削除

Step 3: 品質は十分か判定

物理エンジンの結果を見て、やり直しが必要かどうかを判定します。

判定基準は2つだけ:

  • 削除されたブロックが 全体の15%を超えた → やり直し
  • 削除されたブロックが 5個を超えた → やり直し
// packages/functions/src/services/geminiBuild.ts より

const DROP_THRESHOLD_PCT = 15;  // 15%以上削除されたらNG
const DROP_THRESHOLD_ABS = 5;   // 5個以上削除されたらNG

function needsAgentRetry(report: PhysicsValidationReport): boolean {
  return report.droppedPercentage > DROP_THRESHOLD_PCT ||
         report.droppedCount > DROP_THRESHOLD_ABS;
}

Step 4: 「何が失敗したか」をGeminiに教える

削除されたブロックの場所・サイズ・理由を一つずつまとめて、Geminiへの改善指示を生成します。実際にGeminiに渡されるフィードバックはこんな内容です:

// packages/functions/src/services/geminiBuild.ts より(実際の出力)

function buildPhysicsFeedback(report: PhysicsValidationReport): string {
  const dropped = report.corrections.filter(c => c.action === 'dropped');
  const lines = dropped.map(c =>
    `- Step ${c.stepId} "${c.partName}" at (${c.originalPosition.x},${c.originalPosition.y},${c.originalPosition.z}) size ${c.size.width}x${c.size.length}: ${c.reason}`
  );

  return `PHYSICS FEEDBACK — ${report.droppedCount} bricks were REMOVED because they overlapped.
The surviving build has ${report.outputCount} bricks (${report.droppedPercentage.toFixed(1)}% dropped).

Dropped bricks:
${lines.join('\n')}

INSTRUCTIONS FOR IMPROVEMENT:
- Re-place these bricks at VALID positions that don't overlap existing bricks
- Ensure every brick rests on a brick below with ≥1 stud XZ overlap
- Calculate Y positions precisely: brick height=1.2, plate height=0.4
- Keep the same creative design but fix the spatial conflicts`;
}

つまり、「Step 12の赤い2x4ブリック、座標(3, 2.4, 5)に置いたけど重なって削除されたよ。正しい位置に置き直してね」という具体的な指示をGeminiに送っています。

Step 5: 全体のAgent Loop

ここまでの流れを最大3回繰り返すのが、メインのAgent Loopです。実際のソースコードから抜粋します:

// packages/functions/src/services/geminiBuild.ts の generateBuildPlan() より

let bestResult: { plan: BuildPlan; survivingCount: number } | null = null;
let feedbackPrompt = '';

for (let iteration = 1; iteration <= AGENT_MAX_ITERATIONS; iteration++) {
  // 前回のフィードバックがあればプロンプトに追加
  const prompt = feedbackPrompt
    ? `${basePrompt}\n\n${feedbackPrompt}`
    : basePrompt;

  // Geminiにリクエスト → JSONパース(失敗したら最大3回リトライ)
  const iterationSteps = await callGeminiWithRetry(prompt);

  // 物理エンジンで全ブロックをチェック
  const { steps: fixedSteps, report } = fixBuildPhysicsWithReport(iterationSteps);

  // 「一番ブロックが多く残った結果」を記録
  if (!bestResult || fixedSteps.length > bestResult.survivingCount) {
    bestResult = {
      plan: { ...meta, steps: fixedSteps, agentIterations: iteration },
      survivingCount: fixedSteps.length,
    };
  }

  // 品質OKならここで終了
  if (!needsAgentRetry(report)) break;

  // NGならフィードバックを作って次のイテレーションへ
  if (iteration < AGENT_MAX_ITERATIONS) {
    feedbackPrompt = buildPhysicsFeedback(report);
  }
}

return bestResult.plan; // 全イテレーションで最も良かった結果を返す

ここで重要なのは2つのセーフティネットです:

  1. ベスト結果の追跡 — 3回目でかえって悪くなっても、2回目の良い結果が返される
  2. 内側のリトライ — 各イテレーション内でもJSONパースが失敗したら最大3回リトライする(つまり最大 3×3 = 9回のAPI呼び出し)

結果

このループにより、ビルドの品質が段階的に改善される傾向があります。ただし、毎回必ず0%になるわけではありません:

  • 1回目: フィードバックなしの初回生成。重なりや浮きが発生しやすい
  • 2〜3回目: 具体的な「どのブロックがなぜ失敗したか」の情報を受けて、Geminiが改善を試みる

最終的には、全イテレーションの中で最もブロックが多く残った結果がユーザーに返されます。完璧ではないかもしれませんが、フィードバックなしの1回目よりは確実に良い結果になります。

システムアーキテクチャ

全体のシステム構成です。フロントエンドからAIの処理まで、すべてGoogle Cloud / Firebase上で動いています。

データの流れ

  1. ユーザーがWebアプリで「ビルド生成」をクリック
  2. Cloud Functionsがジョブを作成し、Firestoreに保存
  3. Firestoreのトリガーが発火して、Agentic AI Loopが実行される
  4. ループが完了すると、結果がFirestoreに書き戻される
  5. WebアプリがFirestoreの変更をリアルタイムに受信し、3Dビューアに表示

ユーザーから見ると、ボタンを押して少し待つだけ。裏側でAIが何回やり直したかは見えません。

技術スタック

分類 技術 役割
モノレポ Turborepo + pnpm 複数アプリの一括管理
フロントエンド Next.js 16, React 19, Three.js UIと3D描画
スタイリング Tailwind CSS v4 UIデザイン
状態管理 Zustand クライアント状態管理
バックエンド Cloud Functions (2nd gen) サーバーレスAPI
データベース Firestore リアルタイムデータ同期
AI Gemini 3 Pro パーツ認識・ビルド生成
国際化 next-intl 3言語対応(en/ko/ja)
ホスティング Firebase App Hosting デプロイ・配信
CI/CD GitHub Actions 自動デプロイ

なぜGoogle Cloudを選んだか

Gemini APIが最大の理由です。Geminiは構造化出力(JSON Schemaを指定してJSONで返す)をネイティブサポートしており、3Dの座標データを安定して受け取ることができます。

また、Firestoreのリアルタイムリスナー(onSnapshot)を使うと、バックエンドでAIの処理が完了した瞬間にフロントエンドに通知が届きます。ポーリング不要で、UXが非常にスムーズです。

Cloud Functions (2nd gen)onDocumentCreated トリガーにより、「ジョブがFirestoreに作成されたら自動で処理を開始する」という非同期パターンが自然に実装できました。

デモ動画

ブリックスキャン → インベントリ管理 → AIビルド生成 → 3Dビューアでのステップ再生

まとめ

Brick Questで実装したAgentic AIの核心は、とてもシンプルです:

「AIの出力を検証し、問題があれば具体的に教えて、やり直させる」

これを自動化することで、LLMが苦手な空間推論を補い、人間の介入なしで品質を保証しています。

このパターンはLEGO以外にも応用できます。AIに何かを「生成」させて、その結果を「検証」できるルールがある場面なら、同じ仕組みが使えるはずです。例えば:

  • 建築モデルの自動生成(構造計算でチェック)
  • 家具配置の提案(衝突検出でチェック)
  • ゲームレベルのデザイン(プレイアビリティでチェック)

Discussion