はじめに
ローカルの LLM に、ターン制のローグライク Dungeon Crawl Stone Soup(DCSS)を遊ばせていました。
目標は「生きたまま、できるだけ深い階に着く」。プロンプトを直し、道具を足し、比べる。これを何週間も繰り返しました。
振り返ると、直したことの多くは効いていませんでした。正確に言うと、効いたかどうかを測れていませんでした。
この記事は、その間に引っかかった「測り方の落とし穴」の記録です。LLM エージェントを改良していて、「良くなった気がする」で止まっている人の参考になればと思います。
構成
┌───────────────────────────┐
│ 指揮役(LLM, Ollama) │ qwen3.6(35B-A3B)、1 判断 ≒ 1.2 秒
│ 今できる手の一覧から 1 つ選ぶ │
└─────────────┬─────────────┘
│ 状態 / 手の一覧 / 結果
┌─────────────┴─────────────┐
│ 体(Python) │
│ ・DCSS を端末で動かし、キーを送る │
│ ・Lua の rc ファイルで状態を取る │
└───────────────────────────┘
DCSS は Lua で中の状態を読めます(周りの敵、持ち物、地図など)。体はそれを整え、「今できる手」を説明つきで並べます。LLM はその中から 1 つを JSON で選びます。
体は、実行した結果を progressed(進んだ)/ no_op(空振り)などで返します。
比べる時は、ダンジョンの生成を固定する「種(seed)」を揃え、8〜24 種で回しました。
落とし穴 1: ルールを書き直すと、判断は変わるが結果は変わらない
プロンプトには、私が手で書いた戦い方のルールがたくさん入っていました。それを書き直すと、判断の 92% が変わりました。結果(死亡や深さ)は何も変わりませんでした。ルールを丸ごと消すと、むしろ悪くなりました。
一方で、ゲームは多くのことを自分で言ってくれます。
「It was a potion of curing.」(飲んだ薬の正体)
敵の体力の目安、耐性、武器の強さ
アイテムが今の自分に役立たないかどうか(種族・職業・呪いを考慮済み)
記録を見ると、こうした文が 48,278 回分の判断にわたって捨てられていました。
手で書いたルールは、間違いと「使われない選択肢」の出どころでもあります。そこで、ルールを書く前に、ゲームがもう言っていないかを確かめることにしました。
一つだけ例外を置きました。ジャンルの常識です。エージェントは店で一度も買い物をせず(訪問 9 回、購入 0 回)、回復薬を持ったまま戦いで一度も飲みませんでした。「薬は回復する」「店は金で物を売る」といった、どの RPG でも同じ知識が抜けていたのです。そこで、10 行ほどの RPG の基本だけはプロンプトに入れました。DCSS 固有の事実は、引き続きゲームの文から取ります。
落とし穴 2: 少ない事例から作った教訓は害になる
死んだ回を LLM に振り返らせて「教訓」を書かせ、次の回のプロンプトに入れる仕組みがありました。よくある作りだと思います。
数えてみると、ひどいことになっていました。
教訓は、同じ話題で死が 2 回 あればプロンプトに入る作りだった
一方、手元の記録には同じ敵との戦いが数千回あった
敵 記録の戦い 致死率
ribbon worm 3,363 1.0%
adder 3,419 0.5%
kobold 5,114 0.1%
それでも「ribbon worm からはすぐ逃げろ」という教訓が 491 回 発動していました。
振り返りは死んだ回しか読まないので、どの敵も「逃げるべき相手」に見えるのです。
書かれた教訓は 348 個、そのうち 342 個が後で取り下げられ、80 の話題のうち 53 は取り下げ後にまた書き直されていました。発動は 16,941 回、「役に立った」と判定されたのは 79 回です。
教訓をプロンプトに入れるのはやめました。代わりに、最低件数(8 件)を決めて数えた数字だけを渡します。「この状況で戦い続けた場合の致死率は○%(n=○)」のような形です。振り返りの記録は残し、仮説の種として使います。
落とし穴 3: 1 回きりの基準は当てにならない
ある時期、変更を入れるたびに 12 種で回し、基準の回(3,000 ターン)と比べていました。結果は 5 回続けて「230〜350 ターン長く生きた、向きも揃っている」。
ところが、同じ種・同じコードで 6 回回すと、生き残ったターン数はこう散りました。
種 6 回の範囲
58 861〜3050
61 521〜3007
51 1012〜3053
基準の回は、種ごとに 1 回しか 回していませんでした。良くなったように見えた差の大半は、基準の回がたまたま早死にしていた 3 つの種から来ていました。逆に、基準がたまたま長生きした種では、後の回の方が短かった。典型的な平均への回帰です。
「向きが揃っている」のも当然で、5 回とも同じ 1 回きりの基準と比べていたからでした。
種を揃えれば、種ごとの差は消せます。ですが、LLM の回ごとのばらつきは消えず、こちらの方が大きかったのです。
教訓:
基準の側も何回か回し、両方を平均して比べる。基準の種を増やすだけでは弱点が残る
「良くなった」と言う前に、種ごとの表を出し、差が基準の極端な種から来ていないかを見る
何で測るか
ついでに、指標の選び方も書いておきます。24 種で比べた時の解像度はこうでした。
指標 差を見るのに要る種の数
生き残ったターン数 100 ターンの差に 123 種
着いた深さ 半階の差に 55 種(8 種では 1 階未満の差は見えない)
死亡率 20 ポイントの差に 25 種
死亡率は 70〜95% に張り付き、深さはどの設定でも 3 階前後で平らでした。この規模で差が見えたのは、生き残ったターン数だけです。それも落とし穴 3 のとおり、1 回きりの比べでは当てになりませんでした。
落とし穴 4: 結果ではなく、仕組みが動いているかを見る
結局、生存ターンや深さで比べても、何も言い切れませんでした。
一方で、本物の不具合はすべて、仕組みを直接確かめた時に見つかっています。
全アイテムが、ずっと「未鑑定」だった
DCSS の Lua では、アイテムの項目に「値」と「関数」の 2 種類があります。it.name のように関数として呼ぶものと、it.is_identified のように値として読むもの。
体の rc ファイルは、値の方を it:is_identified() と関数として呼んでいました。これはエラーになりますが、エラーを握りつぶす包みで囲んでいたので、毎回 false が返ります。
その結果、プロジェクトの最初から、すべてのアイテムが「未鑑定・役に立つ・呪われていない」扱いでした。回復薬を飲む手は、薬を持っていた 4,697 回の判断で一度も出ていませんでした。
「一度も true にならないフラグ」は、「一度もそういう物を持たなかった」ようにしか見えません。
店に行く手が、11 回とも空振りしていた
「店に行く」手は 83 回の録画で 751 回出て、11 回選ばれ、11 回とも no_op でした。
原因は、体の作りの食い違いでした。体が自分で足した手は、元の状態(rc が書いたもの)には載っていません。それなのに、実行する時は元の状態からその手を探していたので、見つからず、キーを 1 つも送らずに終わっていました。
私は最初、キーの送り方を疑いました。手でキーを送ってみると、ちゃんと動きました。
黙って止まる
ゲームが Yes/No を聞いてきて、どのキーも受け付けなくなることがありました。記録の上では判断が続いているのに、ターンの数だけが止まっています。ある種は、522 ターン目で 199 回判断し続けていました。
確かめる順番
それからは、変更ごとにこう問うことにしました。
その手は 出ているか
選ばれているか
体は 実行しているか(キーを送ったか)
世界は進んだか(ターンや位置が変わったか)
それが入る記録や数字は 実際に変わったか
答えは記録から数えるか、ゲームのウィザードモードで実際に試して出します。一度も true にならないフラグ、一度も選ばれない手、progressed 以外で終わり続ける手。それが見つけるべき物です。
おまけ: 死に方を数えると、作戦が変わる
「どう死んでいるか」を数えたら、直す場所が変わった例です。
致命的だった 93 回を並べると、真ん中の典型はこうでした。体力 67% → 12% まで、5 回の判断のあいだ、ずっと「攻撃」を選び続けている。とどめは 3〜6 ダメージの、ふつうの敵のふつうの攻撃です。
危ない敵に負けていたのではなく、抜け出せない戦いに負けていたのです。それまでの「1 マス下がる」退却は、1,981 回試して一度も接触を切れていませんでした。1 マス下がると、相手も 1 マス付いてくるからです。
逃げた時の結果も数えました。
状況 戦い続ける 歩いて離れる
無傷、まだ接触していない 15% 17%
無傷、もう接触している 11% 28%
怪我、もう接触している 23% 58%
(数字はその戦いで死んだ割合)
逃げるなら、接触する前に決めないと逃げられない。接触していなければ戦いの致死率は 0.5%、接触すると 4.5% です。
逃げ道を増やした版(逃げる手が出る場面が 23% → 46%)は、かえって死亡が増えました(24 種中 14 → 23 種中 22)。効くのは逃げ道の数ではなく、接触する前に何をするか、そもそもその戦いに入るかどうかでした。
まとめ
ルールより、ゲームが言っている事実を渡す。 手で書いたルールは判断を変えても、結果を変えなかった
少ない事例から教訓を作らない。 最低件数を決めて数え、数字で渡す
基準も繰り返す。 1 回きりの基準との差は、平均への回帰かもしれない
結果より仕組み。 出ているか、選ばれるか、実行されるか、世界が進むか。本物の不具合はそこにしかなかった
この後、配信映えを考えて Minecraft に移りました。そちらの話は別の記事に書きました。
ローカルLLMにMinecraftを遊ばせる ― 指揮役をQwen3.6からNemotron 3.5 Lightningに替えて何が変わったか(リンクは公開後に追記)