はじめに
現在、Sideways という水平思考ゲームを開発しています。
いわゆる「ウミガメのスープ」に近いゲームで、プレイヤーは謎めいた状況に対して質問しながら真相を探します。
例えば、
is it light?
と質問すると、
Yes
と返ってくる。
あるいは、
does the wake up mechanism make sound?
なら、
No
と答える。
一見すると、AIとしてはかなり簡単な処理に見えます。
しかし、実際に作ってみると、この「Yes / Noを自然に返す」ことが思った以上に難しい問題でした。
特に興味深かったのが、
意味判定を細かく設計すればするほど、実際のJevの回答が不自然になった
ことです。
最終的には逆方向に進み、
AIに判断させる内容を減らす
ことでかなり改善しました。
今回は、具体的に何を変更し、実際の回答がどう変わったのかを書きます。
基本設計:AIは意味を判断し、コードが結果を決める
Sidewaysでは、AIにゲーム全体を制御させていません。
基本構造は、
プレイヤーの自然言語
↓
AIによる確率的な意味判定
↓
型付き出力
↓
TypeScriptによる決定論的処理
↓
ゲーム上の結果
です。
考え方としては、
AI = semantic judgment
Code = deterministic policy
User = consequential intent
です。
例えばQuestionなら最終的にユーザーへ返すのは、
YES
NO
PARTLY
IRRELEVANT
UNCLEAR
です。
Theoryなら、
SOLVED
PARTIAL
NOT_SOLVED
です。
AIが勝手に画面を決めたり、自由文で答えたりするわけではありません。
この分離自体は現在も良い設計だと思っています。
問題は、
AIにどれだけ細かい意味情報を判定させるべきか?
でした。
Version 2ではかなり細かく意味を分解した
Version 2のQuestion Judgeでは、だいたい次のような情報をJevから受け取っていました。
{
wellFormed,
relevant,
evidence: {
entailed,
contradicted,
undetermined
},
mixedClaims: {
supported,
contradicted
}
}
それぞれ0〜1の確率です。
その後TypeScript側で、
if (wellFormed < 0.8) {
return "REPHRASE";
}
if (relevant <= 0.2) {
return "IRRELEVANT";
}
if (relevant < 0.8) {
return "NOT_ENOUGH_INFORMATION";
}
if (
mixedClaims.supported >= 0.8 &&
mixedClaims.contradicted >= 0.8
) {
return "PARTLY";
}
if (evidence.entailed >= 0.8) {
return "YES";
}
if (evidence.contradicted >= 0.8) {
return "NO";
}
のような処理をしていました。
Theoryを評価するSolution Judgeはさらに細かく、
{
coreMechanism,
causalRelation,
supportingInsight,
contextualError,
competingMechanism
}
という5軸です。
なぜここまで細かくしたのか
理由は、次のような文章を区別したかったからです。
There is a light.
これは、
光があることには気づいた
程度なので、
PARTIAL
にしたい。
一方、
The light wakes the guest.
なら、
光が客を起こす
という因果関係まで理解しています。
これは、
SOLVED
にしたい。
さらに、
There is a light, but vibration wakes the guest.
なら、light という正しい単語が入っていても、
客を起こしたのは振動
という間違った原因を主張しています。
これは、
NOT_SOLVED
にしたい。
そのため、
core mechanism
causal relation
supporting insight
contextual error
competing mechanism
を分けて評価する設計にしました。
TypeScriptとして見ると、かなり綺麗です。
Offlineテストはかなり良かった
Version 2では、
880 unit/API tests
82 desktop/mobile E2E tests
Leak scan
Lint
Typecheck
Build
などがすべて通りました。
Calibration corpusも作りました。
Generalization用のengineering holdoutも作りました。
Hidden Solutionはserver-only。
通常のQuestionはJev呼び出し1回。
Theoryも1回。
セキュリティやソフトウェア構造としてはかなり良い状態でした。
ところが、実際のJevでテストプレイすると問題が出ました。
実際にプレイするとかなり不自然だった
ある問題では、
音が鳴らず、何も触れていないのに
眠っている客が起きる
という状況を扱っています。
真相には「光」が関係しています。
そこで、
is it light?
と質問しました。
Version 2:
Not enough information
次に、
is it light that wakes the guest?
Version 2:
Not enough information
さらにこれをTheoryとして送ると、
Not quite yet.
でした。
人間のゲームマスターなら、かなり簡単に意味を理解できる文章です。
さらに、
does the wake up mechanism make sound?
は、
Please rephrase
でした。
スペルを少し間違えて、
does the wake up mechines make sound?
としても、
Please rephrase
です。
英語として完璧ではありません。
しかし、
wake up mechines
が、
wake-up mechanism
のような意味だということは、人間ならかなり容易に分かります。
そこで、
Jevの能力というより、こちらのJudge設計が厳しすぎるのでは?
と考え始めました。
原因候補1:0.8という閾値が高すぎた可能性
これは今でもかなりあり得ると思っています。
例えばJev内部では、
wellFormed = 0.74
relevant = 0.92
contradicted = 0.89
くらいまで意味を理解していた可能性があります。
しかしコード側では、
if (wellFormed < 0.8) {
return "REPHRASE";
}
です。
そのため、
wellFormed = 0.74
の時点で、
Please rephrase
になってしまいます。
その後の、
relevant = 0.92
contradicted = 0.89
は使われません。
本来0〜1の連続値を使っていたのに、
0.7999 → NG
0.8000 → OK
という「崖」をアプリ側で作っていたことになります。
閾値を0.6程度まで下げれば改善した可能性はあります。
ただし、Version 2には0.8の条件が複数存在しました。
そのため問題は単純に、
0.8が高かった
だけではないと考えています。
原因候補2:複数の不確実な判断をANDでつないでいた
Version 2は実質的に、
意味が十分分かる
AND
関連性が十分高い
AND
正しい可能性が十分高い
AND
...
という巨大な条件式になっていました。
それぞれの確率がそこそこ正しくても、
どれか1つがthresholdを下回る
と最終結果が変わります。
つまり、
1つの不確実なAI判断
を、
複数の不確実なAI判断 + hard threshold
に分解した結果、逆に壊れやすくなっていた可能性があります。
原因候補3:人間が一度に理解するものを分割しすぎた
例えば、
does the wake up mechines make sound?
を人間が読んだ場合、
"mechines" は mechanism の誤字だろう
↓
wake-up mechanismのことだろう
↓
音が出るか聞いている
↓
No
くらいを一体として理解します。
一方Version 2では、
wellFormedは何点?
relevantは何点?
truthは何点?
と分割しました。
TypeScriptの型としては綺麗です。
しかし、自然言語の意味理解としては分けすぎだった可能性があります。
原因候補4:「何を解こうとしているか」が明示されていなかった
例えば、
is it light?
という文章だけなら、
itとは何か?
は確かに曖昧です。
しかし水平思考ゲームでは、プレイヤーは今、
なぜ客が起きたのか?
を調べています。
人間のゲームマスターなら、
is it light?
を、
客を起こした原因は光?
と自然に補完できます。
そこでVersion 3では、
mysteryFocus
という情報を追加しました。
例えば、
What causes the sleeping guest to wake
when the alarm makes no sound
and nothing touches the guest?
です。
これは答えではありません。
単に、
今プレイヤーが何を解こうとしているか
をserver-sideで明示しています。
Version 3では逆にJudgeをシンプルにした
Version 2をさらに細かく調整するのではなく、構造そのものを簡単にしました。
Question Judgeでは、
type QuestionSemanticClass =
| "SUPPORTED"
| "CONTRADICTED"
| "MIXED"
| "UNKNOWN"
| "IRRELEVANT"
| "AMBIGUOUS";
という1つのChoice distributionだけをJevに返してもらいます。
その後TypeScriptで、
SUPPORTED
→ YES
CONTRADICTED
→ NO
MIXED
→ PARTLY
UNKNOWN
→ NOT ENOUGH INFORMATION
IRRELEVANT
→ NOT RELEVANT
AMBIGUOUS
→ PLEASE REPHRASE
と変換します。
Version 2にあった、
wellFormed >= 0.8
relevant >= 0.8
entailed >= 0.8
のような判定chainはactive pathからなくしました。
Solution Judgeも1 Choiceにした
Theoryについても同じです。
type SolutionSemanticClass =
| "CORE_CAUSAL_EXPLANATION"
| "CORE_WITH_CONTEXT_ERROR"
| "SUPPORTING_INSIGHT_ONLY"
| "COMPETING_WRONG_MECHANISM"
| "NO_MEANINGFUL_INSIGHT";
Jevはこの中のどれに近いかというprobability distributionだけを返します。
TypeScriptでは、
CORE_CAUSAL_EXPLANATION
→ SOLVED
CORE_WITH_CONTEXT_ERROR
→ PARTIAL
SUPPORTING_INSIGHT_ONLY
→ PARTIAL
COMPETING_WRONG_MECHANISM
→ NOT_SOLVED
NO_MEANINGFUL_INSIGHT
→ NOT_SOLVED
と処理します。
つまり、
意味上の違い
は残しました。
しかし、
5つの意味軸を別々に採点する
ことをやめました。
Rubric自体も短くした
Version 3では、
minor spelling mistake
non-native English
missing article
telegraphic wording
ordinary shorthand
natural pronoun resolution
は許容するようにしています。
ただし、それぞれを独立して採点することはしません。
最終的に聞くのは、
このプレイヤーの文章は意味的にどのカテゴリなのか?
です。
実際の結果はかなり改善した
同じ問題で再度テストしました。
Version 3:
is it light?
→ Yes
次:
is brightness involved?
→ Yes
さらに、
something bright wake him?
→ Yes
です。
この英文はかなり崩れています。
それでも意味は正しく理解されました。
以前失敗していた、
does the wake up mechines make sound?
も、
No
になりました。
さらに、
does it buzz?
→ No
is there vibration?
→ No
という短い表現も機能しました。
これは mysteryFocus があることで、短い質問をゲーム文脈から解釈しやすくなった可能性があります。
PARTLYも意図した使い方になった
次のように2つの内容を一度に入力しました。
something bright wake him?
does the wake up mechines make sound?
結果:
Partly
UIでは、
Some of that is right, but another part isn't.
となりました。
これは良い結果です。
PARTLY は、
50%くらい正しそう
という意味ではありません。
正しい主張
+
間違った主張
が実際に同居している場合だけ使いたかったからです。
短いTheoryも正解になった
Question:
does light wake him?
結果:
Yes
同じ文章をTheoryとして送ると、
You got it.
になりました。
さらに、
the room gets bright and that wakes him
も、
Question → Yes
Theory → You got it.
です。
Version 2では同程度の表現でも、
Not quite yet.
になっていました。
この差はかなり大きいです。
正しい単語だけではSOLVEDにならない
シンプル化したことで、何でも正解になるのは困ります。
例えば、
lamp is there but vibration wakes him
では、
Question
→ No
Theory:
Not quite yet.
でした。
lamp という関連単語が入っていても、
客を起こした原因はvibration
と主張しているので正解にはなりません。
これは重要です。
別の問題でも試した
Case 1は「空のframeの中で何かが変化して見える」という問題です。
例えば、
is the frame big
結果:
Not relevant
is it an animal
結果:
No
is it a TV
結果:
No
is it a machine
結果:
No
そして、
it changes its color with sun light
結果:
Yes
同じ文章をTheoryにすると、
You got it.
になりました。
文法的には完璧ではありません。
しかし正しい仕組みは理解されています。
これなら水平思考ゲームとしてかなり自然です。
もちろんまだ判定揺れはある
完璧になったわけではありません。
例えば同じセッションで、
is it the sunrise and sunset
→ Yes
となった一方、
is it sunrise and sunset
→ Not enough information
となることもありました。
これはまだsemantic boundaryに多少の揺れがあることを示しています。
ただし、Version 2の問題とは性質が違います。
Version 2では、
意味が明らかに分かる文章
→ Please rephrase
が頻繁に起きていました。
Version 3では、
かなり境界的な文章
→ YesだったりUnknownだったりする
という程度まで改善しています。
PoCとしては後者の方がかなり良い失敗の仕方だと考えています。
では、具体的に何が効いたのか?
ここは正直に書く必要があります。
Version 2 → Version 3では同時に複数の変更をしています。
複数Noulを1 Choiceへ変更
0.8 / 0.2 threshold chainを削除
mysteryFocusを追加
rubricを短くした
意味回復を個別scoreではなく
最終classificationの中で行うようにした
そのため、
0.8を消したから改善した
とはまだ断定できません。
もしかすると、
0.8 → 0.6
に変えるだけでもVersion 2はかなり改善した可能性があります。
逆に、
mysteryFocus
が一番効いた可能性もあります。
あるいは、
複数の独立判断をやめた
ことが重要だった可能性もあります。
現在分かっているのは、
これらをまとめて単純化したVersion 3の方が、実プレイでは明らかに自然だった
ということです。
Jevは「AI導入」というより、意味を理解できるIF文に近いと感じた
開発を続けていて、Jevに対する感覚も少し変わりました。
一般的な生成AIのように、
AIに仕事を任せる
というより、
自然言語を理解できるIF文を作る
感覚に近いです。
普通のコードなら、
if (text.includes("light")) {
return "YES";
}
かもしれません。
しかしこれでは、
brightness wakes him
something bright wake him
the room gets bright and that wakes him
などに弱い。
Jevを使うと、
この文章の意味は
SUPPORTEDなのか?
という自然言語上の条件を評価できます。
つまり、
if (semantic condition estimated by AI) {
doSomething();
}
です。
個人的には、
Probabilistic semantic IF statement
あるいは、
AI-powered switch/case
と考えるとかなり分かりやすくなりました。
Version 2は巨大なIF文になっていた
そう考えると、Version 2の問題も分かりやすくなります。
最初は、
意味的に正しい?
→ YES
程度だったものが、
IF wellFormed >= 0.8
AND relevant >= 0.8
AND entailed >= 0.8
AND ...
という巨大な条件式になっていました。
Version 3では、
switch (semanticClass) {
case "SUPPORTED":
return "YES";
case "CONTRADICTED":
return "NO";
case "MIXED":
return "PARTLY";
...
}
くらいまで戻しました。
この方が今回の用途には合っていそうです。
JevのコストはAPI料金だけではない
今回もう1つ感じたのが、
calibrationには人間の労働コストがある
ことです。
実際、このゲームでは、
semantic categoryを考える
実際に遊ぶ
短文を試す
typoを試す
false positiveを見る
false negativeを見る
Theoryを試す
別のPuzzleでも確認する
rubricを調整する
再度Previewで確認する
という作業が必要になりました。
これは立派な開発コストです。
Jevによって、
LLMの自由文をJSONに変換する
ような処理は減ります。
一方で、
decision design
calibration
threshold設計
test-play
behavior evaluation
の重要性が高まります。
そのため、
Jevは普通のLLMより労働コストが高い
とまではまだ言えません。
比較実験をしていないからです。
ただ、
Jevでは開発工数の一部が、output parsingからdecision designとcalibrationへ移動する
という感覚はかなりあります。
849テストが通っても、実プレイテストは必要だった
現在Version 3では、
849 unit/API tests
82 desktop/mobile E2E tests
Leak scan
Lint
Typecheck
Build
が通っています。
しかし、それでも実Jevを触らなければ今回の改善は確認できませんでした。
Offlineテストで確認できるのは、
semantic class
↓
TypeScript mapping
↓
API
↓
UI
の正しさです。
一方、本当に知りたいのは、
未知の人間の文章
↓
Jev
↓
適切なsemantic class
です。
この2つは別物です。
今後は、
Layer 1
Software correctness
Layer 2
Model behavior
として分けて考える必要があると思っています。
ここからは、あえて直しすぎない
まだ改善できそうなポイントはあります。
例えば、
sunrise and sunset
の揺れも直そうと思えば直せるかもしれません。
しかし、ここでまた、
特定フレーズをrubricに追加
categoryを追加
thresholdを追加
exceptionを追加
し始めると、Version 2に戻ってしまいます。
そのため現在は、
Version 3をPoCのFreeze候補にする
方向です。
完璧なJudgeを作ることより、
自然な短文を理解する
typoがあっても意味を理解する
正しい因果ならSOLVEDになる
間違った因果ならSOLVEDにならない
Hidden Truthを漏らさない
ゲームとして楽しい
ことを優先します。
それでもダメならYES / OTHERまで落とす
さらにシンプルなfallbackも考えています。
Questionを、
YES
OTHER
だけにする方法です。
OTHERには、
NO
UNKNOWN
IRRELEVANT
AMBIGUOUS
などを全部まとめます。
情報量は大きく減ります。
しかし、もしその方がゲームとして安定するなら採用する価値があります。
このプロジェクトでは、
AIからできるだけ多くの情報を取り出す
こと自体は目的ではありません。
必要なのは、
プロダクトに必要な最小限の判断を、安定して返してもらうこと
です。
まとめ
今回一番大きかった学びは、
AIの意味判定を細かく設計すればするほど良くなるとは限らない
ということでした。
Version 2は高度でした。
semantic signalsが多い
probabilityが多い
thresholdが多い
grading axisが多い
状態です。
しかし実際のプレイでは不自然でした。
Version 3では逆に、
1 semantic Choice
↓
1 probability distribution
↓
TypeScript switch
まで簡単にしました。
すると、
is it light?
→ Yes
something bright wake him?
→ Yes
does the wake up mechines make sound?
→ No
does light wake him?
→ You got it.
のように、かなり自然になりました。
今回の経験から、AIプロダクトでは、
Understand more.
より、
Decide less.
の方が良い場合があると感じています。
そしてJevのようなdecision modelを使う場合には、
「何をAIに判断させないか」を設計することも、重要なAI設計なのかもしれません。