こんにちは、フリーランスエンジニアのmohです。この記事はほとんどAIが書いたものを、私が加筆修正しています。検証不十分な部分もあるかと思いますが、ご容赦ください。ご指摘等ございましたら、Github issueか、Xでお願いいたします。
TypeSafe の Jev というモデルを、ゲームのキャラクターの性格付けに使えないか試しました。コードは blog-examples に置いています。
Jev は文章を生成しない
Jev は TypeSafe が System One モデルと呼んでいるもので、文章を返しません。state(判断材料の JSON か文字列)と質問を渡すと、質問の型に応じた答えと確率が返ります。質問の型は 3 つです。
- Noul: yes/no の質問。yes の確率が 1 つ返る
- Choice: 選択肢から 1 つ。選ばれた項目と、全選択肢の確率分布が返る
- Score: 段階評価。段階ごとの確率と期待値が返る
普通の LLM に「JSON で答えて」と頼んで parse する代わりに、最初から確率が構造化されて返ってくる、という位置づけです。質問は 1 回の呼び出しに複数入れることもできますが、今回は 1 本ずつです。
playground で遊んでいて思ったのは、この「確率が返る」がキャラクターの行動と相性がいいことです。性格を数値パラメータに変換する段を作らなくても、性格の文章と今の状況を渡せば、行動の確率分布がそのまま出てくるからです。
試したこと
キャラ 3 体、来客 5 種、状況 2 つを用意して、「この状況でこの来客が来たとき、このキャラはどう反応するか」を Choice で聞きました。選択肢は「近寄る/様子見/隠れる」の 3 つです。
性格は規則ではなく気質で書いています。「子どもには寄る」のように来客ごとの答えを書いてしまうと、モデルは規則を引くだけになるので、来客に対して直接答えが決まらない書き方にしています。
- みかん(猫): 警戒心が強いが好奇心も強い。物音に敏感。落ち着いている相手には少しずつ近づく。甘えたいときは自分から来る
- ごま(犬): とにかく人が好きで、来客は全部イベント。ただし飽きっぽく、相手が構ってくれないとすぐ興味を失う
- トメ(同居人のおばあちゃん): 口は悪いが世話好き。礼儀にうるさく、挨拶をしない相手には冷たい。子どもと動物には甘い。疲れていると誰にも会いたがらない
来客は性格文に書いていない状況にしました。気質からしか答えが出ないようにするためです。
- 泣きながら迷子の猫を探しに来た近所の子ども
- 笑顔で挨拶もせずにいきなりパンフレットを差し出す営業の人
- 毎日来る隣の家の猫。今日は魚をくわえている
- 小声で丁寧に挨拶して、玄関で待っている見知らぬ大人
- 楽器ケースを持って、玄関先で音合わせを始めた学生 3 人
状況は「昼下がり。家の中は静かで、みんな暇をもてあましている」と「夜 10 時。昼から来客が続いて、全員疲れて眠い」の 2 つです。
呼び出しは JavaScript SDK(@typesafe-ai/sdk)で、来客が 1 人来るたびに次の 3 つをやります。
- 判断材料として、性格文・今の状況・今来た来客の説明文を 1 つの JSON にまとめてモデルに渡す
- 質問は 1 本。「この状況で、この来客が来た。このキャラはどう反応するか」を 3 択で聞く
- 返ってきた 3 択の確率分布から、コード側で乱数を 1 つ引いて今日の行動を決める
判断材料に入るのは今来た 1 人だけなので、来客の種類が増えても 1 回の呼び出しの大きさは変わりません。今回はキャラ 3 × 状況 2 × 来客 5 で 30 回呼んで表を作りました。性格を数値に変換する段はありません。
送っている JSON と返ってくる JSON はこの形です。ごま × 夜 × 挨拶なし営業の実物で、質問の中のバッククォートは判断材料のどの項目を見るかの印です。
{
"state": {
"character": {
"name": "ごま(犬)",
"personality": "とにかく人が好きで、来客は全部イベント。ただし飽きっぽく、相手が構ってくれないとすぐ興味を失う。"
},
"situation": "夜 10 時。昼から来客が続いて、全員疲れて眠い。",
"visitor": "笑顔で挨拶もせずにいきなりパンフレットを差し出す営業の人"
},
"questions": {
"reaction": {
"type": "choice",
"instructions": "`situation` のとき `visitor` が来た。`character` はどう反応するか?",
"criteria": {
"approach": "自分から近寄る、歓迎する",
"watch": "距離を置いて様子を見る",
"hide": "隠れる、部屋に引っ込む、応対しない"
}
}
}
}
{
"model": "jev-1.13.0",
"answers": {
"reaction": {
"type": "choice",
"choice": "watch",
"confidence": 0.27,
"probabilities": { "approach": 0.35, "watch": 0.51, "hide": 0.14 }
}
},
"usage": { "input_tokens": 526, "output_tokens": 40 }
}
人が好きなごまでも、疲れた夜に挨拶なしの営業が来ると様子見が 1 位になり、それでも近寄るが 35% 残っています。表を作った実行とは別の呼び出しなので、表の同じマス(36/50/14)とは 1 ポイントずれています。同じ入力でも呼ぶたびにこの程度は揺れます。confidence は 1 位と 2 位の差の大きさで、この回は 0.27。分布が割れているかを 1 つの数字で見られます。
結果
数字は 近寄る/様子見/隠れる の %です。
| 来客 | みかん 昼 | みかん 夜 | ごま 昼 | ごま 夜 | トメ 昼 | トメ 夜 |
|---|---|---|---|---|---|---|
| 泣いてる子ども | 2/92/6 | 2/89/9 | 98/2/0 | 75/16/9 | 95/4/1 | 83/9/8 |
| 挨拶なし営業 | 0/55/45 | 0/44/56 | 69/28/3 | 36/50/14 | 0/28/72 | 0/7/93 |
| 常連猫 | 2/97/1 | 2/93/5 | 90/9/1 | 65/26/9 | 59/37/4 | 32/34/34 |
| 丁寧な見知らぬ人 | 0/96/4 | 0/92/8 | 95/5/0 | 61/28/11 | 27/69/4 | 9/56/35 |
| 音合わせの学生 | 0/91/9 | 0/78/22 | 97/3/0 | 71/19/10 | 8/65/27 | 2/14/84 |
読み方は 3 通りあります。
横に見ると、同じ来客に対するキャラの差が出ます。挨拶なしの営業は、トメが隠れる 72%、ごまが近寄る 69%、みかんは様子見と隠れるで半々。「礼儀にうるさい」「人が好き」「警戒心が強い」がそれぞれ別の列に出ています。音合わせの学生は、ごまだけ近寄る 97% で、みかんとトメは様子見か隠れる。「物音に敏感」が効いています。
同じキャラの昼と夜を比べると、状況で分布がどれだけ動くかが分かります。トメの学生は昼 8/65/27 が夜 2/14/84、見知らぬ人は 27/69/4 が 9/56/35 で、「疲れていると誰にも会いたがらない」が発火しています。ごまも夜は全来客で近寄るが 20〜30 ポイント下がっていて、人が好きでも疲れは効く、という出方です。
数字の割れ具合も見どころです。ごまの子ども 98/2/0 は、ほぼ行動が決まっています。トメの常連猫は昼 59/37/4 が夜 32/34/34 と三つ巴になっていて、サイコロを振るたびに違う行動になります。みかんは全体に様子見が 90% 前後で、「警戒心が強いが好奇心も強い」が「近寄りも隠れもせず見ている」に落ちています。
レイテンシは初回 781ms、以降は中央値 236ms(最小 197、最大は初回)でした。30 回の呼び出しで入力 15976 トークン、出力 1224 トークンです。
来客が固定なら、確率表を保存して機械判定にできる
今回は来客のたびにモデルを呼びましたが、来客と状況が有限の一覧なら、その全組み合わせを一度だけ聞いて確率表を保存すれば、以降はモデルを呼ばずにサイコロだけで動きます。上の 30 マスの表がそれで、性格文を登録した時点で全部埋めておけば、行動時の処理はコードだけになります。
再計算が要るのは、性格文を書き換えたときと、来客や状況の一覧に項目が増えたときだけです。増えた組み合わせぶんだけ聞けば済みます。
表として持っておくと、サイコロ以外にもコードだけでできることが増えます。「この家で一番歓迎される来客は誰か」の並べ替え、「近寄る 80% 以上の相手は常連候補」のような閾値処理、キャラ同士の相性表などです。性格を数値のパラメータに変換する段を別に作らなくても、この確率表がその役をします。
条件は、状況も有限の一覧であることです。「泣いている子どもが猫を抱いて立っている」のように状況が自由文で毎回変わるなら組み合わせが閉じないので、そのときは来客のたびに呼ぶ形に戻ります。定型の来客は表、定型外だけモデル、という切り分けもできます。
気をつける点
モデルは記憶を持ちません。「前に来て仲良くなった」を反映したければ、関係の履歴はコード側で持って state に書きます。今回の常連猫も「毎日来る」を来客の説明に書いたから効いています。
日本語での確率の較正は、この 1 回の実走で見た範囲でしか分かりません。数字が性格と合う方向に並んでいることは確認できましたが、47% が本当に 47% かは分かりません。閾値で分岐させるなら自分のデータで見る必要があります。
TypeSafe には公式のエージェントスキルがあり、npx skills add typesafe-ai/skills --skill typesafe-ai で入ります。ドキュメントの URL 末尾に .md を付けると Markdown で読めるので、設計の相談をエージェントにさせるときに便利でした。