はじめに
「LLMを組み込んだゲームを作りたい」——最初に思いついたのはそんな軽いノリだった。
「自分と相性のいいMBTIの性格とは何なのか」「そもそも、この手のマッチングに”当たり”のパターンなど存在するのか」。人間同士でこれを検証するには人生が何周あっても足りないので、LLMに性格を演じさせて、高速に何十世代分もシミュレーションさせればいいのでは、というのが本音の出発点だった。統計的にどのMBTIの組み合わせが「うまくいきやすい」のか、AIの目を通して眺めてみたかった。
条件を一つ自分に課した。
LLMを「会話できるNPC」という演出のために使うのではなく、ゲームメカニクスの中核に据えること。
攻略Wikiが作れないゲーム、プレイするたびに結末が変わるゲームを作りたかった。
行き着いた企画が、これだ。
男女8人が一つ屋根の下で30日間を過ごす。MBTIで性格を設定されたキャストたちが、LLMに人格を演じさせられながら出会い、交際し、同棲し、結婚するか一人で去るかを選ぶ。プレイヤーは観測者として、「どの性格が結ばれやすいか」「LLMは年収や年齢で相手を選ぶバイアスを持つのか」を眺める。
名付けて MBTI LOVE OBSERVATORY。自宅サーバー2台(GPU役の「タチコマ」とCPU指揮役の「KITT」)で動かす、ローカルLLM駆動の恋愛リアリティショーだ。
結論から言うと、動いた。動いたのだが、「LLMをゲームの中核に据える」という設計思想は、想像の3倍くらいLLMを食うということを、身をもって学ぶことになった。今日はその記録。
作ろうとしたもの(概要)
細かい仕様は省くが、骨格だけ書いておく。
- 8人のキャスト(男女4人ずつ)。MBTIタイプ・年齢・年収・職業をそれぞれ持つ
- 三層の記憶構造: 全会話を保存する「生ログ層」、キャスト本人が持つ主観的な「記憶層」(毎晩の日記で要約・歪みも許容)、スコア更新用の「関係イベント層」
- 関係の進行: 告白→交際→同棲→(毎朝の継続判定)→最終日に結婚するか破談するか
- 毎日のイベント: 輪番制の夕食当番、デートプラン(散歩・手料理・書斎での語らい・飲み会)、同棲中の外部デートは相手の許可制
設計の大原則はシンプルに一つだけ決めていた。
日常はルールベース、決断はLLM
移動や疲労やスコアの微増減は全部コードで機械的に処理し、LLMは「告白するかどうか」「同棲を続けるかどうか」といった性格が滲み出るべき瞬間だけに使う。この原則自体は、今振り返っても正しかったと思っている。問題は、その「決断」の数を甘く見積もっていたことだ。
現実その1: ハードウェアがしょぼい
まず前提から。自宅にあるのは以下の機材だ。
- タチコマ: NVIDIA T1000、VRAM 4GB
- KITT: Core i7-8700、GPUなし(内蔵GPUのみ)
設計当初、内心では「7Bモデルくらいなら余裕だろう」と思っていた。甘かった。T1000 4GBはTuring世代でTensorコアも持たない、いわば「ちょっと良いオフィスPC」のGPUだ。7Bモデルを載せようとすると、VRAMから溢れてCPUオフロードが発生し、生成速度が絶望的に落ちる。
結局、会話生成には2〜3Bクラスの小型モデル(最終的にqwen3.5:2b)を採用することになった。これ自体は妥当な判断だったが、「小型モデルに何を背負わせるか」を甘く見ていたツケは、後で盛大に回収することになる。
現実その2: 「1日」を回すのに何回LLMを呼ぶのか
8人が30日間を過ごすシミュレーションを設計していて、途中で数えてみたことがある。1ゲーム内の1日に発生しうるLLM呼び出しを列挙すると、こうなった。
- 日中の雑談イベント: 10〜20回
- 夕食当番の会話: 1回(確定発生)
- 同棲ペアの夜の会話: 同棲組数分(確定発生)
- 告白・デート誘い・同棲打診・破局などの閾値判断: 0〜5回
- 毎朝の同棲継続判定: 同棲している全ペア×2人分
- 就寝前の日次リフレクション(日記): 8人分、毎日確定発生

これを30日分積み上げると、1回のプレイで数百回のLLM呼び出しが発生する計算になる。「LLMをゲームの中核に据える」というのは、聞こえはいいが、実装してみるとLLMを日常茶飯事のインフラとして扱うという意味だったのだ。クラウドAPIでこれをやったら、財布が先に離婚届を出してくるレベルである。ローカルLLMを選んだ判断だけは、最初から正しかったらしい。
現実その3: 小型モデルはJSONの約束を平気で破る
会話やデート成立の可否をゲームロジックに反映させるため、LLMには「必ずJSON形式で、こういうスキーマで返してくれ」と頼んでいる。もちろんOllamaのformat: jsonでJSON自体は強制できる。問題はその中身だ。
実機ログにはこんな記録が残っている。
JSON検証失敗: dialogue List should have at least 2 items, not 1
JSON検証失敗: romance_a Input should be less than or equal to 10 (実際の値: 11)
会話を2人分(最低2行)返してくれと頼んでいるのに1行しか返ってこない。感情値の上限を10と決めているのに17を返してくる。3回リトライしても、性懲りもなく同じ失敗を繰り返す個体もいた。

これは意地悪でもバグでもなく、2Bクラスのモデルにとって「ペルソナを演じながら、複数ターンの会話を作り、かつ複数の数値制約を守ったJSONを吐く」という注文自体が、単純に荷が重かったというだけの話だ。
対策として組んだのが、こういう多段の防御ラインだった。
format: jsonでJSON自体を強制- 数値が範囲を数点超えていたら、検証で弾く前にコード側で丸め込む(romance=17は10にクランプすればいい。中身の対話が良質なら、それを活かさない手はない)
- 会話が1行しか返ってこなかったら、リトライで粘るより相手役の相槌を1行機械的に足して即座に救済する(3回リトライしても改善しないことが実測でわかっていたので、粘るだけ無駄だった)
- それでも壊れていたら、最後は定型文のフォールバックに逃がしてゲームを止めない
多段防御と呼んでいるが、要するに「小さいモデルの言うことを真に受けすぎない仕組み」を何重にも張っているだけである。
現実その4: 「ルールベース化」の見積もりが外れた話
これは自分への戒めとして書いておきたい。
途中で「LLM呼び出しをもっと減らせないか」と考え、結果がほぼ読めている判定はルールベースで即決してしまうという最適化を入れた。たとえば「恋愛感情が78以上ならデートの誘いはほぼ確実に受けるだろうから、わざわざLLMに聞くまでもない」というロジックだ。
導入前は「これで呼び出しを3〜4割は減らせるだろう」と皮算用していた。
実測してみると、ほとんど発火しなかった。
理由は単純で、そもそも「デートに誘う」というイベント自体が「恋愛感情40以上」という条件でしか発生しない。その狭い母集団の中で、さらに78以上のような極端値に達するケースは稀だったのだ。発火条件の絞り込みと最適化ロジックが、こちらの想定以上に競合していたわけである。
結局、実測ベースで唯一しっかり効いていたのは、まったく別の場所だった。「その日、大した出来事もなく雑談を1回しただけのキャストには、LLMに日記を書かせず定型文で済ませる」という、地味な間引きである。これが日記生成の1〜2割を削っていた。
見積もりを外した話をブログに書くのは若干気恥ずかしいが、「効くと思った場所ではなく、地味な場所が効いた」というのは、この手の最適化にありがちな教訓だと思うので、正直に記録しておく。

学んだこと
一連の開発を経て、今のところの結論はこうだ。
「LLMをゲームの中核に据える」というのは、LLM呼び出しを増やすことではない。むしろ真逆で、LLMの呼び出しを極限まで削り、本当に性格が滲み出るべき一点に予算を集中させる設計のことだ。
日常の9割はルールベースの決定論的なコードで処理し、LLMは「告白するかどうか」「同棲を続けるかどうか」のような、性格演技そのものが観測対象になる瞬間にだけ使う。これを徹底しないと、ローカルLLM環境ではあっという間に計算資源が焼き切れる。
そしてもう一つ。小型モデルは、指示を字義通りに守ってくれるとは限らない。JSON形式、数値の範囲、往復回数——どれも「だいたい守るが、たまに平気で破る」ものとして設計しないといけない。多段防御は「賢さの不足を補う仕組み」ではなく、「賢さの不足を前提にした上で成立させる仕組み」だ。

おわりに
今のところ、8人・30日のシミュレーションは一応最後まで完走できるところまで来ている。
何組が結ばれ、何人が独り身のまま去るか——そして肝心の「どのMBTIの組み合わせがうまくいきやすいのか」というデータは、まだ十分な回数を回せていないので、また別の記事にしたい。
小型モデル(qwen3.5:2b)の日本語の生成品質についても、正直まだ様子見の部分がある。プロンプトの調整でどうにかなる話なのか、それとも「このサイズにこれを背負わせるのが土台無理」という話なのか——これは近いうちに、別モデルとの比較で白黒つけるつもりだ。
LLMを「ゲームの飾り」ではなく「ゲームの骨格」に据えるというのは、思っていたよりずっと泥臭い作業だった。ただ、泥臭さの先に、攻略法の存在しないゲームが本当に生まれつつある実感はある。












