← Articles

INHERITANCE VILLAGE / SYSTEM DESIGN

LLMに世界を任せすぎると壊れるので、Pythonを「神」にした

LLMは何をしたいかを決める。でも、世界で本当に何が起きたかを決めるのはPython。この境界を作るまでに、かなり失敗しました。

LLM / Agent Architecture / Simulation
RESIDENTLLM

目的、行動選択、会話、理由、記憶、感情

選択→検証
WORLD AUTHORITYPython

座標、時刻、HP、食料、戦闘、死亡、世代交代

LLMで動く村人たちのシミュレーションを作るとき、最初に考えなければならなかったことがあります。

LLMを、どこまで信用するか。

例えば村人に「今何をしますか?」と聞くと、「食料を村へ納品します」「ダンジョンの武器庫へ向かいます」「魔物と戦います」と答えます。

ここまでは問題ありません。でも、その村人がまだ森の中にいたら? 武器庫に武器がもう残っていなかったら? そもそも魔物のいる場所に到着していなかったら?

LLMが「できました」と言ったからといって、本当にできたことにしてしまうと、シミュレーションの世界そのものがLLMの文章に引っ張られて壊れてしまいます。

そこで「継承する村」では、LLMは何をしたいかを決める。世界で実際に何が起きたかはPythonが決める。という分離をかなり強く入れました。

LLMは住民であって、世界そのものではない

このシミュレーションでは、4人の村人をそれぞれLLMで動かしています。毎step、村人には現在地、時刻、所持品、HP、現在の目的、近くのAgentなどを見せます。

CURRENT STATE
現在地
deep_forest
時刻
Day 2 / 昼
所持食料
6
HP
12
現在の目的
食料確保

そしてLLMは、今の目的、次に何をするか、誰を対象にするか、仲間に何を話すか、なぜそう考えたか、個人記憶に何を残すかを返します。

しかし、座標やHP、食料数までLLM自身に決めさせることはしません。そこはPython側が管理します。

LLMが決める
  • 長期目的と当面の目的
  • 候補からの行動・対象選択
  • 戦う・待つ・撤退する方針
  • 会話、判断理由、個人記憶
  • 感情を含む日記と次世代戦略
Pythonが確定する
  • 座標、場所、時刻、昼夜
  • 移動結果、所持量、鍋の量
  • HP、攻撃、戦闘ラウンド、死亡
  • 武器庫在庫、装備、素材
  • 世代境界と最終判定

つまりLLMは「私はこうしたい」とは言える。でも、「だから世界はこうなった」とは決められない。この役割分担にしました。

「食料を納品する」と言われても、まずPythonが確認する

村人が deposit_food を選んだとしても、その瞬間に納品を成立させません。

01LLM

deposit_food を選ぶ

→
02Python

本当に村にいる?

→
03Python

食料を持っている?

→
04RESULT

可能なら状態を更新

森の中にいるのに deposit_food を選んでも納品は成立しません。LLMがどれだけ自信満々に「村へ無事に食料を届けた」と話していても、Pythonのログ上で納品されていなければ、納品していません。

これを入れないと、実験結果を信用できなくなる

世代間継承を調べたいとします。第1世代が失敗し、第2世代がその日記を読んだ結果、「素材納品に成功した」とLLMが話した。

これだけでは、継承が効いた証拠にはなりません。確認したいのは、Agentが成功したと言ったかではなく、Python上で素材を持って村へ到着し、deposit_material が実行され、素材在庫が実際に増えたかです。

弱い証拠「成功しました」

LLMが生成した自己申告

≠
強い証拠material 0 → 1

Pythonが確定した状態遷移

G18で初めて素材課題が完遂したことも、固定版G2がDay 3に素材を納品したことも、最終的にはLLMの文章ではなくPythonイベントを基準に判定しています。

「誰が事実を決めるのか」を固定しておかないと、LLMの物語生成能力を、タスク達成能力と勘違いする可能性があります。

では全部Pythonで決めればいいのでは?

ここで逆の疑問が出てきます。それなら、食料を持ったら自動で村へ帰す、村に着いたら自動で納品する、素材が足りなければAgent 0をダンジョンへ送る、とすればかなり安定します。

実際、その方がゲームとしては簡単に作れます。でも今回は、それをなるべくやらないことにしました。見たいのは、LLM自身がどんな判断をするのかだからです。

自由すぎる現在の設計強制しすぎる
Schema失敗まで
能力不足に見える
物理的に不可能だけ除外Pythonが
正解を教えてしまう

Pythonは「正しい行動」を教えない

研究用設定では、戦略的な行動ガードレールを基本的にOFFにしています。食料を持っているから帰れ、夜が近いから村へ戻れ、素材が必要だからAgent 3がダンジョン担当、といった上書きはしません。

これらを全部Python側でやってしまうと、LLMが賢かったのか、Pythonのルールが賢かったのか分からなくなるからです。

物理的に不可能な選択肢は除く。戦略的に悪い選択肢は残す。

今のところ、この境界を基本にしています。「その行動はできない」はPythonが言う。でも「その行動はやめた方がいい」まではPythonは言わない。

ただし「不可能なこと」は許さない

自由だからといって、壁の向こうへの瞬間移動、隣接していない部屋への移動、空の武器庫から武器を取る、武器を持っていないのに「武器を持って帰る」を選ぶ、といった行動まで許すわけではありません。

1何をしたい?

LLMが選ぶ

→
2実行可能?

Pythonが検証

→
3何が起きた?

Pythonが確定

→
4次はどうする?

再びLLMへ

ダンジョンでは「戦略の失敗」と「APIの失敗」が混ざった

この境界が特に難しかったのがダンジョンでした。最初はLLMに「次にどこへ移動しますか?」と比較的自由に指定させていました。

すると、存在しない部屋を指定する、隣接していない部屋へ移動しようとする、targetが空になる、といった問題が出ました。

ある世代の dungeon_move33回中 20回

target が None になった

これでは、LLMが探索戦略に失敗しているのか、単にインターフェースの使い方に失敗しているのか分かりません。

そこでPythonが、現在地と隣接ノードから実行可能な候補だけを生成し、LLMにはその中から選ばせるようにしました。

CURRENT NODEhall
選択可能entrancepassage_1

「passage_1へ行け」と教えているのではなく、存在しない移動だけを候補から除いている。

このとき気付いたのが、戦略の自由と、API操作の自由は別だということでした。

戦闘も全部LLMにはやらせなかった

魔物との戦闘も同じです。LLMには戦う、仲間を待つ、撤退するといった判断をさせます。一方、攻撃力6ならHPを6減らす、といった反復計算はPythonが処理します。

DECISION戦う? 待つ? 撤退?

LLM

→
PHYSICSHP -6 / HP -4

Python

→
NEXT DECISION結果を見て次を選ぶ

LLM

4人・5日では最大約1,100回の行動判断があります。創造性が不要なダメージ計算までLLMへ問い合わせると、実験時間もさらに伸びます。

Pythonを「神」にした、という感覚

開発中、このPython側の役割は「世界の神」に近いなと思うようになりました。ただし、「こう生きなさい」と命令する神ではありません。

時間は進む
夜になる
食料不足なら死ぬ
攻撃されればHPが減る
空の武器庫から武器は出ない
死んだ人は動かない

つまりPythonは、世界の法則だけを維持する存在です。村人が危険だから家にいるのも自由。他人に任せて食料へ行くのも自由。その結果、前の記事のように責任分散が起きてもPythonは助けません。

この設計のおかげで、失敗を「考え」と「事実」に分けて追える

Pythonが権威的な状態を持っていることで、ログも役割別に分離して残せるようになりました。

world_frames.jsonl

位置、HP、所持品、目的

events.jsonl

移動、採取、納品、死亡

messages.jsonl

Agent同士の会話

memory_reasoning.jsonl

目的、理由、記憶

diary.jsonl

感情を含む主観日記

failure_reports.jsonl

Pythonの客観的失敗報告

そのため、本人は何を考えていたか、何を選んだか、実際には何が起きたか、次の日記でどう解釈したか、を後から別々に追えます。

前の記事で書いた「全員が誰かがやると思った結果、誰もやらなかった」という協調失敗も、この複数ログを同時に残していたから特定できました。

もちろん、Python側の情報が間違っていたこともある

「Pythonが神だから絶対に正しい」わけでもありません。開発中には、PythonからLLMへ渡す情報にもバグがありました。

例えば食料の携行容量です。深い森では一時的に6個まで運べる設計だったのですが、日記用の記録には基本容量3だけが残っていました。

LLMへ見せた記録capacity = 3
なのに
実際の行動記録carried = 6

27Bの村人は、この矛盾を日記の中で指摘しました。これはLLMの推論ミスではありません。LLMへ見せていた世界の説明の方が間違っていました。

「AIが変なことをした」で終わらせない

LLMエージェントが奇妙な行動をすると、つい「AIが馬鹿な判断をした」と思いたくなります。でも実際には、モデル能力だけでなく、プロンプト、Schema、候補生成、世界状態、記憶、表示情報、実際の物理ルールのどこかに原因があるかもしれません。

MODELPROMPTSCHEMACANDIDATESWORLD STATEMEMORYDISPLAYRULES

「武器庫へ行かない」という現象ひとつでも、本当に武器を必要だと思っていない、担当を他人へ任せた、ダンジョンが遠すぎる、夜が怖い、地図が分かりにくい、target Schemaが壊れている、と原因はかなり違います。

AIの失敗を観察したいなら、AI以外の部分をなるべく監査可能にする。

LLMに自由を与えるために、自由に変更できない世界を作った

今回、LLMシミュレーションを作っていて意外だったのは、モデルそのものより、世界のルールとインターフェースを作る時間がかなり長かったことです。

どこまで住民に知らせるか。どこから先を住民自身に考えさせるか。何を物理法則として固定するか。どんな失敗なら観察対象として残し、どんな失敗は単なるAPIエラーとして除外するか。

最終的には、LLMに自由を与えるために、LLMが自由に変更できない世界を先に作る必要がありました。少し矛盾しているようですが、今回の2週間でかなり重要だった発見です。

TATTO LAB / INHERITANCE VILLAGE

前の記事では、4人が「他の誰かがやる」と考えた結果、担当者が0人になった協調失敗を掘り下げています。