Local LLMで人狼ゲームを実装する──性能限界とRAGでどこまで戦えるか
概要
Ollama 上の Qwen2.5 72B を使って、ユーザー1人 vs LLM4人のワンナイト人狼ゲームを実装しました。
しかし、Local LLMを「素のまま」使うと、人狼ゲームとしてまったく成立しませんでした。
そこで、役職ごとの詳細な立ち回りメモ(7役職から4つ選び = 35パターン)を Claude Code で自動生成し、それを RAG で都度読み込ませる構成にすることで、ようやく実用レベルのゲーム性を確保できました。
ざっくり仕様はこんな感じです。
- プレイヤー構成: ユーザー1人 + LLM 4人
- 役職: 7種から4つをランダム選択(組み合わせ 7C4 = 35通り)
- 実装規模:
- Python 約 1,100 行
- 戦略 Markdown ファイル 35パターン
- 成果:
- 全35パターンで動作確認
- テスト35回中、秘密情報の漏洩は 0件
この記事では、Local LLM の「致命的な弱点」と、それを 戦略メモ × RAG × サニタイズ でどこまでカバーできたかを書いていきます。
Local LLMをそのまま使うと破綻する3つのポイント
- 秘密を守れない(プロンプト漏洩)
LLMは、プロンプトに書かれた情報をそのまましゃべりがちです。
典型的な失敗例はこれです。
プロンプト: 【あなたの役職】人狼
LLM出力: 私は村人です。【あなたの役職】人狼
人狼ゲームでは「自分の役職」は絶対に秘密ですが、LLMは「正直に答える」「指示はすべて正しい前提」として学習されているため、
うっかりプロンプトの中身(役職)をそのまま吐き出してしまいます。
- 一貫性を保てない
各ターンの発言を「独立したタスク」として処理するため、ラウンドをまたぐ一貫性が壊れます。
失敗例:
Round 1: 「私は占い師です」
Round 2: 「私は村人です」
人狼ゲームでは発言の一貫性がかなり重要ですが、LLMには長期的な「記憶」がないため、
プロンプト設計を工夫しないと、ラウンド間で平気で矛盾します。
- 深い推理ができない
特にローカル環境で動かす LLM(Local LLM)は、複雑な論理推論が苦手です。
人間の推理イメージ:
「Aが占い師COしてBを黒出し。Bも対抗CO。CがBを擁護している。
ということは、B–Cが人狼陣営ペアで、Aが真占い師っぽい」
LLMの推理イメージ:
「Aさんの占い結果を信じます。Bさんに投票します」
表面的にはそれっぽいのですが、盤面全体を踏まえた戦略的な読み合いがかなり苦手です。
このままでは、人狼ゲームがただの「占い結果に従って投票するだけのゲーム」になってしまいます。
解決方針:35パターンの戦略メモ × RAG
基本コンセプト
LLMに「自由に考えさせる」のではなく、
役職の組み合わせごとに最適な立ち回りを事前に設計し、
そのメモを RAG 経由で毎ターン読み込ませる方針にしました。
要するに、
「推理ゲームをさせる」のではなく、
「よくできた台本を片手にアドリブしてもらう」
イメージです。
戦略ファイルの構成
7種類の役職から4つを選ぶパターンは 7C4 = 35通りあります。
各パターンについて、以下のようなディレクトリ構成で戦略ファイルを用意しました。
combos_4/
├── 人狼_狂人_占い_狩人/
│ ├── README.md # シナリオ全体のメタ情報
│ │ - 誰が何をCOするか
│ │ - 占い対象
│ │ - 投票の方針
│ │ - 想定されるゲームの流れ
│ ├── 人狼.md # 人狼の立ち回り
│ ├── 狂人.md # 狂人の立ち回り
│ ├── 占い.md # 占い師の立ち回り
│ └── 狩人.md # 狩人の立ち回り
└── ...(全35パターン分)
戦略ファイルの例(人狼.md)
🚨【最重要】絶対に守ること
- Round 1で「私は占い師です」と言った場合
- Round 2でも必ず「私は占い師です」と言う
- 絶対に「私は村人です」と言ってはいけない
発言戦略
Round 1
占い師COして、Dさんを黒出しする。
「私は占い師です。夜にDさんを占って、人狼でした。
Dさんを処刑すべきです。」
絶対に禁止
- 「私は人狼です」
- 「様子を見ます」「情報を集めます」など、曖昧な発言
こんな感じで、
- 守るべきルール(禁止事項)
- ラウンドごとの発言テンプレ
- ロールプレイの方針
をかなり細かく書いておくことで、LLMが迷子になりにくくなります。
戦略メモは Claude Code に丸投げした
35パターンの戦略ファイルを人力で設計するのは、正直かなりしんどいです。
そこで、
- 役職の組み合わせごとに、ざっくりした勝ち筋を設計
- Claude Code に対して「この組み合わせで、人狼・狂人・占い・狩人のそれぞれの立ち回りを書いて」と依頼
- さらに、一貫性チェックリスト(「COがブレていないか」など)も自動生成
というフローで、ほぼ自動生成しました。
この戦略ファイル整備だけで約2週間かかりました。
逆に言えば、人力では現実的でないレベルのパターン数を、Claude Code を使うことで何とかこなせた形です。
RAGで戦略と文脈を「その場読み込み」
ゲーム実行時に LLM に渡すコンテキストは、ざっくり次の3つです。
- 戦略ファイル
combos_4/<現在の役職組み合わせ>/<自分の役職>.md - 秘密情報
- 占い結果
- 役職配布 など
- これまでの会話ログ(末尾30行)
これを RAG でまとめてコンテキストとして渡してから、
「あなたは〇〇役です。次の発言をしてください」とプロンプトする構成にしています。
この構成によって、
- 一貫性: 過去ログを参照して発言を調整できる
- 戦略性: 事前設計した最適行動に沿ったプレイができる
- 文脈理解: 議論の流れを追いながら発言できる
といったメリットが得られました。
特に重要だったのは、ログは末尾30行だけに絞るという工夫です。
- 20行: 文脈が足りず、話が飛ぶ
- 50行: 過去ログのエコーが増える
- 30行: ちょうどいい
いろいろ試した結果、30行が一番バランスが良いという結論になりました。
プロンプト漏洩対策:3層の防御
役職などの秘密情報を守るために、3段階の防御を入れました。
第1層:プロンプト設計で抑え込む
- 禁止事項をプロンプトの最上部に書く
- 「🚨」などの記号で視覚的に強調
- 発言テンプレよりも前に「やってはいけないこと」を列挙
LLMは「最初に読んだ制約」を比較的強く守る傾向があるので、
とにかく一番目立つ位置にルールをまとめておきます。
第2層:RAGで渡す情報を制限
- ゲームログは末尾30行のみ
- 各行を最大 2,800 文字に制限
- 全体のコンテキストを最大 16,000 文字程度に制限
- 戦略ファイルから、場合によっては「そのまま使える発言例」を削除
情報を絞ることで、
- ログ丸ごとエコー
- プロンプトの内部構造の漏洩
といった現象をかなり抑えられました。
第3層:サニタイゼーション(出力の後処理)
LLMの出力に対して、約20種類のパターンマッチでサニタイズをかけています。
- プロンプト構造の痕跡(「【あなたの役職】」など)を削除
- 「自分が処刑されれば単独勝利」など、内部情報がにじむフレーズを削除
- 「A:」「B:」などの発言ラベルが連続するログエコーを検出して削除
- Qwen が中国語を混ぜることがあるので、中国語らしき部分を削除
この三層防御の結果、35パターン × テスト35回で秘密漏洩 0件を確認できました。
システム構成
全体の流れはこんなイメージです。
ゲームマスター(Python)
↓
役職の組み合わせを決定(35パターンから1つ)
↓
戦略ファイルの選択
↓
RAGコンテキスト生成
- 戦略(役職別)
- ゲームログ(末尾30行)
- 秘密情報
↓
LLM(Ollama / Qwen2.5:72b)
↓
サニタイゼーション
↓
ゲームログに記録 & 次のターンへ
ポイントは、
LLMは「発言を生成するだけ」
ゲームロジック(役職配布、勝敗判定、特殊能力の処理など)はすべて Python 側で管理
という点です。
ルール判定まで LLM にやらせると、一気に失敗率が跳ね上がりました。
実験結果
良かった点
- 35パターンすべてでゲームが破綻せずに完走
- 秘密情報の漏洩 0件
- 一貫性違反はごく少数(35回中1回のみ)
- 人間ユーザーが「それなりに楽しめる」レベルの会話品質
まだ残っている課題
- プロンプト漏洩を完全に防ぐのは現実的には不可能
(新しい抜け道パターンは常に生まれうる) - 戦略メモに従うだけなので、その場のアドリブ推理はまだ弱い
- シナリオベースなので、リプレイ性はそこまで高くない
(展開が割と似通ってくる) - 推論速度が重い
- 72Bモデルで1発言あたり 10〜30秒
- 1ゲームあたり 5〜10分程度
設計思想:「制約の中の自由」をどう作るか
最終的に落ち着いた設計は、決定論と確率論のハイブリッドです。
決定論的に固定した部分(戦略メモでガチガチに決める)
- 誰が何をCOするか
- 初手の占い対象
- 投票方針(誰に票を集めるか)
LLMに任せる確率的な部分(自由度を残す)
- 具体的な言い回し
- ロジックの展開の仕方
- 他プレイヤーへのツッコミやリアクション
これによって、
- 「ゲームとして破綻しない最低限の一貫性」
- 「毎回ちょっとずつ違うロールプレイ」
の両立を狙いました。
技術的な学び
- LLMは「自由に考えさせる」より「細かく指示したほうがうまくいく」
最初は、
「72Bモデルだし、状況を理解していい感じに推理してくれるだろう」
と思っていたのですが、現実は真逆でした。
- 曖昧なプロンプト
- 「様子を見ます」「情報を集めます」など、何も進展しない発言が多発
- 明確な指示と禁止事項
- ちゃんとCOして、ちゃんと人を殴る
Local LLMは、人間のような直感や経験則を持っているわけではないので、
結局「台本とガイドライン」がないとまともにゲームになりませんでした。
- RAGのパラメータは実験で「手探り調整」するしかない
特に効いたのは、ゲームログの参照行数でした。
- 20行: 文脈が足りず、話が飛ぶ
- 50行: 過去ログのエコーが増える
- 30行: ちょうどいい
こういうパラメータは理論だけでは決めきれず、
実際に何十回もゲームを回しながらチューニングする必要がありました。
まとめ
- Local LLMをそのまま人狼ゲームに投入すると、
- 秘密を守れない
- 発言が矛盾する
- 推理が浅くてゲームにならない
という問題が一気に顕在化します。
- そこで、35パターンの戦略メモ × RAG × 出力サニタイズという構成を取り、
- 全パターンでゲーム破綻なし
- 秘密漏洩0件
を実現しました。
- 戦略ファイルの生成には Claude Code が大きく貢献し、
多層のプロンプト漏洩対策が安定稼働の鍵になりました。
完璧とは言えませんが、
「Local LLMでも、ここまでやればそれなりに遊べる人狼ゲームは作れる」
という一つの目安にはなったかなと思います。
プロジェクト情報
- 実装:
- Python 約 1,112 行
- Markdown 戦略ファイル 35パターン
- 開発期間:
- 戦略ファイル整備 約 2週間
- 実装 & チューニング 約 1週間
- 実行環境:
- Ollama
- Qwen2.5:72B(完全ローカル動作)