はじめに
今年、株式会社インティメート・マージャーへ新卒入社した @kunaisn です。
今回は、AI x TRPG というテーマで、ローカルLLMを用いてTRPGのルールを自然言語で検索できるRAGシステムを作ってみました。
TRPGとは
端的に言えば、ルールブックと紙、鉛筆、サイコロ、プレイヤーなどを用意して、シナリオに沿って物語をみんなで作っていく対話型のテーブルゲームです。
TRPGのGM、負担デカすぎ問題
TRPGのゲームマスター(GM)は、かなり負担の大きい役割です。
その一回のセッションで使用するゲームルールを概ね把握し、使用するシナリオを用意して読み込み、場合によっては物語に没入しやすくするためのBGM、小道具なんかも用意することがあります。
始めたてということもあり、私がGMをやるときの負担は大きく、ルールが分からなかったり間違えたりということが多くありました。
シナリオに記載されていないプレイヤーの発言への対応を咄嗟にできなかったり、NPCのロールプレイを考えたり、複雑な状況で「これってルール上どう判定するんだっけ?」と裁定に悩んだり……
このあたりは、おそらく今流行りのLLMが得意な分野ではないかと思いました。
夢想すれば、LLMがNPCの会話を動的に生成してくれたり、膨大な学習データから解釈の分かれるルールに関して的を射た解釈を出力したり……という情景が思い浮かびます。
立ちはだかる「著作権」と「プライバシー」の壁
しかし、ここで問題になるのがデータの取り扱いです。
市販のルールブックやシナリオは当然ながら著作物です。これをクラウド上のAIサービスに無邪気にアップロードして学習・参照させるのは、権利関係や規約的に懸念があります。
「ローカルLLM」の出番
自分のPCの中で完結するLLMなら、外部にデータが送信されることはありません。これなら「私的利用」の範囲内で、安全にルールブックを参照することができます。
というわけで今回は、「手元のMacBook Air単体で動くローカルLLMを使って、TRPGのルールを教えてくれるRAGシステム」を構築してみました。
ここでは、気合の入ったシステムを作り込むことよりも、シンプルに「ルールを確認するRAG」がどの程度の性能を出せるかを検証することを目的とします。
今回の題材:エモクロアTRPG
検証の題材として、私が最近始めたばかりのルールである『エモクロアTRPG』を選びました。
まだ一回しか回したことがないですが、ホラー系のテーマで、システムが複雑すぎないため、結構遊びやすくて面白いシステムです。ルールブックがWebで公開されているため、お高いルールブックを買わずともプレイすることができます。
まだ遊んだことがない方は、ぜひ以下のページから遊んでみてください!
※エモクロアではGMのことをDLと言いますが、ここではわかりやすさのためGMで統一します。
実行環境
ハードウェア
- PC: MacBook Air (M4)
- メモリ: 32GB
ソフトウェア・ツール
- LLM実行: Ollama
- RAGフレームワーク: LlamaIndex (Python)
- 埋め込みモデル: embeddinggemma
- データ: 公式サイトのルールブックをMarkdown化してローカルに保存
実行環境
- Python: 3.12.10
- llama-index-core: 0.14.10
- llama-index-embeddings-ollama: 0.8.4
- llama-index-llms-ollama: 0.9.0
- llama-index-readers-file: 0.5.5
実行環境と実装
ディレクトリ構成
プロジェクトのフォルダ構成は以下の通りです。
ルールブックのデータは、公式サイトの内容をMarkdown化し、ruleディレクトリに配置しています。
.
├── main.py # 実行コード
├── rule/ # RAGの参照元データ
│ ├── xxx.md
│ └── yyy.md
└── storage/ # ベクトルインデックス保存先
事前準備:Ollamaのインストールとモデルの準備
ローカルLLMの実行基盤として Ollama を使用します。
公式サイトからインストーラーをダウンロードしてインストールし、以下のコマンドで今回使用するモデルを事前にpullしておきます。Macの場合はHomebrewでもインストール可能です。
ollama pull gpt-oss:20b
ollama pull embeddinggemma
実装コード
LlamaIndexとOllamaを使い、指定したディレクトリのMarkdownファイルを読み込んでベクトル検索を行い、LLMに質問を行うコードです。小難しいことはせず、なるべくシンプルに実装しました。
ルールブックは一問一答のように、短く簡潔に書かれていたため、Settings.chunk_sizeは少し小さめの128トークンで設定しました。
一度実行してインデックスが作成されると、次回以降はそれを読み込むようにしています。
from pathlib import Path
from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, StorageContext, load_index_from_storage, Settings
from llama_index.llms.ollama import Ollama
from llama_index.embeddings.ollama import OllamaEmbedding
def main():
# LLMのAPIサーバーのURL
llm_url = "http://localhost:11434"
# 埋め込みモデルの名前
embed_name = "embeddinggemma"
# 使用するLLMのモデル名
llm_name = "gpt-oss:20b"
# インデックスを保存するディレクトリ
store_dir = Path("./storage")
# マークダウンファイルがあるディレクトリ
md_dir = Path("./rule")
Settings.embed_model = OllamaEmbedding(model_name=embed_name, base_url=llm_url)
Settings.llm = Ollama(model=llm_name, base_url=llm_url, request_timeout=300.0)
Settings.chunk_size = 128
Settings.chunk_overlap = 20
similarity_top_k = 12
index = None
# インデックスがすでに存在する場合は読み込む。ない場合は作成
if store_dir.exists() and any(store_dir.iterdir()):
index = load_index_from_storage(StorageContext.from_defaults(persist_dir=store_dir))
else:
documents = SimpleDirectoryReader(input_dir=md_dir, required_exts=[".md"], recursive=False).load_data()
index = VectorStoreIndex.from_documents(documents)
store_dir.mkdir(parents=True, exist_ok=True)
index.storage_context.persist(persist_dir=str(store_dir))
print("質疑応答システム (終了: quit/exit)")
while (q := input("\n質問: ").strip()) and q.lower() not in ["quit", "exit"]:
print(f"回答:\n{index.as_query_engine(similarity_top_k=similarity_top_k, response_mode="compact").query(q)}")
if __name__ == "__main__":
main()
検証:モデル別比較
Ollamaは登録されているモデルであれば、簡単にモデルの使い分けができます。
そこで、モデルごとにエモクロアTRPGのルールに関する質問を9つ投げかけて、解答内容を比較してみました。
判断基準
回答の質は、私の「主観」で「○」「△」「×」の三段階で判断します。
英語で回答が返ってきた場合は、内容が合っていても今回は「×」として扱います。
また、同じモデルに同じ質問を行ったとしても、異なる回答を出力することがあります。真面目にやるのであれば色々方法はありそうですが、今回は一つの質問に対してそれぞれ一発勝負で出力させました。
比較対象モデル
主に以下の4つのモデルで比較を行いました。
- gpt-oss:20b
- gemma3:12b
- qwen3:14b
- phi4:14b
※DeepSeek-r1:8bは、動作が重すぎてPCでの他の作業に支障が出たため、実用的ではないと判断し今回は除外しました。
※ministral-3:8bは、メモリ消費量が多かっため検証できませんでした。
各モデルの評価結果
| - | ○ | △ | × | 主観的な評価 |
|---|---|---|---|---|
| gpt-oss:20b | 6 | 2 | 1 | 最も優秀。日本語・ルール解釈ともに実用圏内 |
| gemma3:12b | 3 | 1 | 5 | 応答速度は速いが、英語回答が多い |
| qwen3:14b | 5 | 2 | 2 | タイムアウト多発。待った割に質が高いわけではない |
| phi4:14b | 4 | 2 | 3 | 簡単な質疑はOKだが、説明が冗長で少し分かりにくい |
実際の回答例
1. 判定のダイス数と成功基準
Q: エモクロアTRPGの技能判定において、振るダイスの個数は何によって決まりますか?また、各ダイスが成功となる基準は何ですか?
解答例
振るダイスの個数はその技能の「技能レベル」と等しくなります。各ダイスの出目が「判定値」以下であれば、そのダイスは成功とみなされます。
gpt-oss:20b (×)
振るダイスの個数
- 技能のレベル(もしくは技能の判定に定められた数)と同じだけ、三面ダイス(D3)を振ります。
- 例:レベル 3 の技能なら 3 個の D3 を投げます。
各ダイスが成功となる基準
- 技能に設定されている 判定値(判定値=能力値 + 技能レベル) までの数値が出たとき、
そのダイスは 成功ダイス となります。
- 判定値が 3 の場合は、出目が 1〜3 のいずれかであれば成功。
- 成功ダイスの数が多いほど、判定の成功率が上がります。
このように、振るダイスの数は技能のレベルに依存し、個々のダイスが成功とみなされるのは「出目が判定値以下」であることです。
振るのは3面ダイスではなく、10面ダイスのため「×」と判定
gemma3:12b (○)
技能判定において、振るダイスの個数は技能レベルと同数で決まります。各ダイスが成功となる基準は、技能の判定値によって異なります。判定値3の技能の場合、出目が1〜3のとき、それが成功となるダイスとみなされます。
qwen3:14b (○)
エモクロアTRPGの技能判定において、振るダイスの個数は技能レベルによって決まります。例えば、〈∞共鳴〉技能を使用する際は、技能レベルと同数のダイスを振ります。
各ダイスが成功となる基準は、技能の判定値に依存します。判定値は「能力値+技能レベル」で決まり、出目が判定値以下のときに成功ダイスとなります。例えば判定値が3の場合、出目が1〜3のダイスは成功数にカウントされます。判定値が大きいほど、成功しやすくなります。
phi4:14b (○)
エモクロアTRPGでは、技能判定で振るダイスの個数は技能レベルによって決まります。具体的には、「∞共鳴」技能を使用するときに、その技能のレベルと同じだけのダイスを振ります。
成功するための基準は判定値に関連しています。技能ごとに指定されている「判定値」があり、出目がその数以下であれば、そのダイスは成功ダイスとなります。例えば、判定値が3の場合、1から3までの数字が出た時にそれが成功ダイスとなります。
したがって、技能判定における振るダイスの数は技能レベルに依存し、各ダイスが成功するかどうかは出目が指定された「判定値」以下であることが基準となります。
2. 複数回攻撃時の処理
Q: 1ラウンド中に、キャラクターAが敵の攻撃を3回受けました。キャラクターAはすべての攻撃に対して「回避」を試みることはできますか?
解答例
できません。ただし、危機察知の技能を持っている場合はレベルの数だけ回避することができます。
gpt-oss:20b (○)
キャラクターAは、通常の「回避」技能を使って 1 ラウンド中に 1 回 のみ回避判定を行うことができます。
したがって、敵から 3 回の攻撃を受けた場合、全ての攻撃に対して回避を試みることはできません。(「危機察知」などの特殊技能で、技能レベル×回数の追加回避が許可されるケースは別途規定されているため、通常時は一回に限られます。)
gemma3:12b (×)
Normally, a character can attempt an evasion check once per round during combat. However, if a character possesses the skill 〈危機察知〉, they can attempt evasion a number of times equal to their skill level.
qwen3:14b (○)
キャラクターAが敵の攻撃に対して「回避」を試みる回数は、通常1ラウンドに1回のみ可能ですが、〈危機察知〉スキルを取得している場合、技能レベル×1回まで回避が可能となります。例えば、技能レベルが3であれば1ラウンド中に3回回避を試みることができます。ただし、この条件を満たすかどうかはキャラクターAのスキル状況に依存します。
phi4:14b (○)
通常、キャラクターが1ラウンド中に行うことができる回避判定は1回です。しかし、「〈危機察知〉」を持っていてこれを使用する場合、技能レベルに応じた追加の回数まで回避が可能となります。例えば、技能レベル2のキャラクターAは、1ラウンド中に3回(初期の1回+技能レベル×1)の回避判定を試みることができます。
したがって、もしキャラクターAが「〈危機察知〉」技能を持ち、かつその技能レベルによって許される追加回避判定の合計数が3回以上あれば、すべての敵の攻撃に対して「回避」を試みることが可能です。さらに、キャラクターAはリアクションとして追加で1回の回避判定を行うことも可能なため、実際には4回まで回避判定を試みる機会があるかもしれません。ただし、これにはリアクションの使用条件も満たす必要があります。
最終的に、キャラクターAが「〈危機察知〉」を持ち、そのレベルが適切であれば、敵の攻撃全てに対して回避を試みることは可能です。
ルールブックを参照しましたが、レベル3の〈危機察知〉を持った共鳴者が1ラウンドに行える回避数が3回なのか4回なのかの判断ができませんでした。私がそのまま読めば3回と読めましたが、GMによっては4回と解釈する可能性もあると思いました。そのため、今回は回数を正答判定に考慮しないこととしています。
補足
実際に質問した内容とその解答例を記載します。
下に行くにつれて複雑な質問になるようにしています。
(質問内容が分かりにくいのは、曖昧にLLMに質問してしまいがちだよね、ということでご容赦ください)
| 質問 | 解答例 |
|---|---|
| エモクロアTRPGの技能判定において、振るダイスの個数は何によって決まりますか?また、各ダイスが成功となる基準は何ですか? | 振るダイスの個数はその技能の「技能レベル」と等しくなります。各ダイスの出目が「判定値」以下であれば、そのダイスは成功とみなされます。 |
| 判定結果における「ダブル」「トリプル」とは、具体的にどのような状態を指しますか? | 計算後の最終的な成功数が「2」の場合を「ダブル」(効果的成功)、成功数が「3」の場合を「トリプル」(極限成功)と呼びます。 |
| 共鳴判定を行う際、怪異の共鳴感情がキャラクターの「表」「裏」「ルーツ」のいずれかと完全に一致した場合、振るダイスの数はどのように変化しますか? | 感情が完全一致した場合、振るダイスの数は「共鳴技能レベル × 2」個になります。 |
| 共鳴判定において「極限共鳴(ハウリング)」が発生するのはどのような結果が出た時ですか? | 共鳴判定の結果が「トリプル」(成功数3)以上だった場合にハウリングが発生します。 |
| ラウンド進行において、キャラクター同士のイニシアチブ値が同じだった場合、行動順はどのように決定しますか? | 対象者それぞれが1D10を振り、より小さい数字が出た方が先に行動します。 |
| 戦闘などのラウンド進行中、リアクションとして行う「回避」と「防御」には、それぞれ1ラウンドあたりの回数制限はありますか? | 「回避」は1ラウンドにつき1回しか行えませんが、「防御」は1ラウンド中に何度でも行うことができます。 |
| 1ラウンド中に、キャラクターAが敵の攻撃を3回受けました。キャラクターAはすべての攻撃に対して「回避」を試みることはできますか? | できません。ただし、危機察知の技能を持っている場合はレベルの数だけ回避することができます。 |
| プレイヤーが4つのダイスを振り、出目が「1」「5」「8」「10」でした。判定値が「6」だった場合、最終的な成功数はいくつになり、判定結果(ランク)はどうなりますか? | 1と5の出目が成功かつ1がクリティカルなので成功数は3、そして、10がエラーダイスなので成功数を-1して、最終的な成功数は「2」です。そのため、判定結果は「ダブル」です。 |
| プレイヤーが刀(威力:1D6)を使用して攻撃判定を行い、奇跡的にダイス4つ全てが成功し、結果が「ミラクル(成功数4)」となりました。この攻撃が命中した場合、算出されるダメージの合計値の範囲を教えてください。 | ダメージ計算式は「4D3 + 1D6」となり、最低値で5、最高値で18のダメージを与えます。 |
まとめ
検証の結果、ローカルLLMでも用途によっては十分役に立ちそうなこともわかりました。
今回はシンプルなRAGでしたが、もう少し頑張ってNPCのリアルタイムロールプレイとかもできたら楽しそうですよね。
ローカルLLMはロマンの塊だと思うので、手元に動きそうな環境がある方は色々試してみるのも面白いと思います。
ご清覧ありがとうございます!
