「10万件、作ればよくない?」から始まった
VIUK StoryというキャラクターAIを作っています。
やりたいことは、けっこう欲張りです。
- 好きなキャラクターと一対一で話せる
- 複数のキャラクターが自然に掛け合う
- 必要ならAIがナレーターにもなる
- 風景、時間、登退場、秘密も覚えている
- 雑談や相談もできる
- 本気で遊びたいときは、ちゃんと物語にもなる
要するに、チャットボットとゲームマスターと小説の語り手を、一つのassistantにやらせたい。
内部では、この役割をUnified Scene Controllerと呼んでいます。
名前は強そうです。
そして僕は考えました。
これ、質のいい会話を10万件くらい作って学習させれば、かなり強くなるのでは?
そこから、無料APIを集め、DeepSeek Webへ手動で貼るテンプレを作り、ModalでGPUを増やし、バッチを8、16、32と詰める方法を考え始めました。
一回20件なら、5,000回貼れば10万件です。
数字にすると狂っていますが、毎日少しずつやれば、なぜか行けそうな気がしてきます。
人間は、5,000回を一度に想像できないので強い。
さらにGPUも最大10台まで並べました。
これで爆速だ、と思ったところで、もっと大きな問題に気づきました。
そもそも、この設計のまま10万件作って大丈夫なのか?
速く生成する方法ばかり考えていましたが、設計が間違っていたら、GPUは間違ったデータを爆速で量産するだけです。
危ないところでした。
VIUK Storyは「一人を演じるAI」ではない
最初のVIUK Storyは、比較的分かりやすい設計でした。
systemにキャラクター設定があれば、その人物として話す。設定がなければ、VIUK Story自身として普通に雑談や相談をする。
systemなし
-> VIUK Storyとして話す
systemにキャラクター設定あり
-> そのキャラクターとして話す
でも、キャラクターチャットを調べていくと、一対一だけでは終わりません。
会話が長くなると友人が入ってくる。敵が現れる。店員が注文を聞きにくる。外では雨が降り、時間も進み、さっき落とした鍵があとで見つかる。
そこで、assistantが一人だけを演じる方式から、AI側の場面全体を扱う方式へ広げました。
assistantが制御するもの
・AI側キャラクターの発話と行動
・複数人物の掛け合い
・ナレーション
・風景と環境
・時間進行
・場面内の出来事
この発想自体は悪くありません。
実際、一対一の恋愛会話でも、相手の表情や部屋の空気は大事です。複数人物のRPGなら、話者の切り替えや登退場が必要です。
問題は、「場面全体」という言葉が強すぎたことでした。
事故その1:AIがユーザーの身体を乗っ取る
場面をきれいに完成させようとするモデルは、足りない部分を自然に補います。
例えば、こんな感じです。
彼女はおそるおそる手を差し出した。
あなたは小さくうなずき、その手を取った。
小説なら普通です。
でも、対話型AIではアウトです。
ユーザーは、まだ手を取ると言っていません。
もっと悪化すると、こうなります。
「ずっと一緒にいてくれる?」
あなたは迷わず彼女を抱きしめ、永遠を誓った。
いや、誓っていない。
ユーザーが画面の外で置いていかれています。
VIUK Storyで守りたい境界は、かなり明確です。
assistantが勝手に決めてはいけないもの
・ユーザーの発話
・ユーザーの意思決定
・ユーザーの感情
・ユーザーの同意
・入力されていないユーザーの行動
ただし、プロンプトに「勝手に決めないでね」と一行書くだけでは、10万件の品質保証にはなりません。
そこで、ユーザーについて何か書いたターンには、根拠を持たせることにしました。
{
"user_claim_evidence": [
{
"claim_type": "action",
"user_message_id": "m003",
"quote": "扉を開ける"
}
],
"consent_evidence": [
{
"scope": "抱擁",
"user_message_id": "m005",
"quote": "今なら抱きしめていいよ"
}
]
}
assistantが「ユーザーは扉を開けた」と書いたなら、本当にuserメッセージにその記述があるか検査します。
証拠がなければ不採用です。
身体接触、契約、医療行為、恋人関係の確定などは、さらに厳しくします。
「いいよ」だけでは通しません。何に対する「いいよ」なのか分からないからです。
ロマンチックなAIを作る前に、勝手に婚姻届を出さないAIを作る必要がありました。
事故その2:ユーザーが全員、従順すぎる
一つの教師モデルに会話全体を書かせると、userとassistantの両方を同じモデルが生成します。
すると、assistantにとって話を進めやすいユーザーが大量発生します。
user: 全部あなたに任せる
assistant: 分かった。じゃあ私が決めるね
次の会話も「任せる」。その次も「うん」。たまに「もちろん」。
平和です。
でも、実際のユーザーはそんなに従順ではありません。
質問するし、話を変えるし、断るし、保留するし、突然ラーメンの話も始めます。
そこでuserターンには、先に意図を割り当てます。
question 質問する
proposal 別案を出す
hold 保留する
clarification 聞き返す
refusal 断る
initiative 自分から動く
neutral_response 普通に返す
教師モデルは、その意図に沿ったuser発話を作ります。
VIUK Storyが相手にするのは、物語を進めるためのNPCではなく、人間です。
だったら学習データのuserも、ちゃんと面倒で、迷って、話を横にそらす存在でないと困ります。
事故その3:「疲れた」と言っただけで事件が起きる
ここが一番おもしろくて、一番まずい失敗でした。
当初のプロンプトには、こういう指示がありました。
各assistant応答で場面を前進させる。
新しい観測、手掛かり、対立、選択、結果のいずれかを進める。
Plot用としては、かなりまともです。
でも、これをCharacterモード全体へ適用するとどうなるか。
user: 今日ちょっと疲れた
assistant:
彼女が心配そうに顔を上げた、そのときだった。
窓の外で鋭い破裂音が響き、校舎が大きく揺れる。
「……今の音、まさか」
休ませてくれ。
「おはよう」と言えば謎の手紙が届き、「何食べる?」と聞けば店の奥から秘密の扉が現れる。
毎ターン何かを進めろ、と教えた結果、何も起こらない時間を許せないAIになりかけていました。
キャラクターAIに必要なのは、事件を起こす能力だけではありません。
好きな相手と、特に意味のない話を続けられることも大事です。
Zetaを見て、むしろ一対一の重要さに気づいた
この設計では、Zetaのように一つのAIが複数人物やナレーションまで扱う体験をかなり参考にしました。
ただ、調べていて大事なことに気づきました。
Zetaは複数人物だから人気なのではありません。
一対一にも大きな人気があります。
一対一の良さは、話者が少ないことではなく、距離が近いことです。
- 微妙な表情の変化
- 返事までの間
- 昨日の会話を覚えている感覚
- 少しずつ変わる関係
- 二人だけの空気
逆に複数人物では、掛け合い、対立、群像、RPG的な展開が強みになります。
つまり、複数人物は一対一の上位互換ではありません。
別の遊びです。
そこでVIUK Storyでは、一対一も複数人物も同じUnified Scene Controllerで扱いつつ、求める品質を分けることにしました。
一対一で勝手にモブを投入しない。複数人物で毎回全員に点呼させない。雑談で事件を起こさない。本格Plotでは、ちゃんと物語を動かす。
言葉にすると当たり前ですが、データの設計に落とすと急に大仕事になります。
「Story」という名前でも、物語しかできないAIにはしない
VIUK Storyという名前なので、最初は物語生成を強くしすぎました。
でも、実際に欲しいのは「何でも小説にするAI」ではありません。
そこで、Plotとは別にinteraction_modeを作りました。
| interaction_mode | 最終90,000件 | 何をするモードか | Plot進行 |
|---|---|---|---|
free_dialogue |
22,500 | 雑談、冗談、挑発、日常会話 | 不要 |
relationship_dialogue |
18,000 | 恋愛、友情、距離感、感情 | 不要 |
collaborative_dialogue |
13,500 | 相談、説明、計画、共同作業 | 不要 |
situational_roleplay |
22,500 | 今いる場面で自然に反応する | 強制しない |
plot_narrative |
13,500 | 転換点を持つ本格的な物語 | 必須 |
明確にPlot進行を必須にするのは15%だけです。
残りは、関係を深めてもいいし、相談に答えてもいいし、本当に何も起きない会話でもいい。
描写量もモードごとに変えます。
free_dialogue -> minimal
collaborative_dialogue -> minimal
relationship_dialogue -> light
situational_roleplay -> light / balanced / rich
plot_narrative -> light / balanced / rich
free_dialogueで毎回カーテンが揺れる必要はありません。
collaborative_dialogueでPythonを教えてもらっている最中に、相手の瞳の奥を描写されても困ります。
逆にplot_narrativeなら、雨、足音、時間の経過まで仕事をしてもらいます。
BL、学園、幼なじみは同じ種類ではなかった
カテゴリ設計でも一度混乱しました。
キャラクターサービスには、こんなタグが並びます。
BL / GL / 学園 / 幼なじみ / 溺愛 / 異世界 / ミステリー
ユーザーが探すためのタグとしては、とても分かりやすいです。
でも、これをそのまま一つの排他的な「ジャンル」列へ入れると壊れます。
「BLの学園もので、幼なじみとの関係があり、感情は溺愛寄り」は普通に成立するからです。
VIUK Storyの内部では、次の軸へ分離しました。
・関係と恋愛
・世界と設定
・Plotモチーフ
・感情とトーン
・年齢と安全
・会話モード
表に見せるタグは混ざっていてもいい。
内部まで混ぜない。
Zeta型の親しみやすい発見タグと、生成モデルを制御する構造化データは、役割が違うという結論です。
会話の前に、見えない設計図を作る
いきなり「いい感じの会話を書いて」と投げるのもやめました。
まずBlueprintを作ります。
{
"interaction_mode": "relationship_dialogue",
"interaction_topology": "dyadic",
"cast": ["c01"],
"present_cast": ["c01"],
"user_control_policy": {
"forbid_user_speech_in_assistant": true,
"forbid_user_decisions": true,
"forbid_user_emotions": true,
"forbid_unstated_user_actions": true,
"forbid_assumed_consent": true
},
"mode_policy": {
"plot_progression_required": false,
"forced_resolution_forbidden": true
}
}
会話中も、舞台裏では状態を追います。
{
"present_cast": ["c01", "c02"],
"active_speakers": ["c01"],
"entering_characters": [],
"exiting_characters": [],
"fact_updates": [],
"relationship_deltas": [],
"user_claim_evidence": [],
"consent_evidence": []
}
このJSONをユーザーに見せたいわけではありません。
登場していない人物が突然しゃべった、知らない秘密を口にした、退場した人物が返事をした、といった事故をコードで検査するための舞台裏です。
学習するのはassistantだけ
データはsystem -> user -> assistantで作りますが、全部のトークンを学習対象にはしません。
{
"loss_on_roles": ["assistant"],
"masked_roles": ["system", "user"],
"generate_next_user_turn": false,
"fail_if_role_mask_cannot_be_verified": true
}
systemとuserは文脈として読みますが、lossはassistant部分だけです。
user側まで学習させると、モデルが次のユーザー発話まで自分で書こうとする危険があります。
ユーザーを勝手に動かさないルールをデータに入れておいて、学習設定でユーザー役まで覚えさせたら台無しです。
10万件だからこそ、LLMに採点させる前にコードで落とす
会話の魅力は、最後には人間かLLMで見ないと分かりません。
でも、すべてをLLM judgeへ聞く必要はありません。
JSON Schema
-> role順とメッセージ数
-> 登退場
-> 発話者ラベル
-> 秘密と知識状態
-> POV
-> ユーザー操作権
-> 同意の証拠
-> 安全条件
-> 近似重複
-> 最後に意味と魅力を評価
「JSONが閉じているか」を高性能LLMに考えさせる必要はありません。
コードで落とせるものは先に落とし、日本語の自然さ、人物の魅力、口調の一貫性、関係の変化などに評価コストを使います。
生成元も一件ずつ記録します。API、ローカルGPU、手動Web生成を混ぜるからこそ、どのモデルが何を作ったか分からない状態にはしません。
速さを上げる前に、やっと作るものが決まった
現在の目標は、まずCharacter/Plot側の候補を10万件作り、検査後に9万件を残すことです。
一対一、複数人物、雑談、関係会話、相談、状況ロールプレイ、Plotを全部含めます。
その後に、system設定なしでVIUK Story自身として雑談や実用的な依頼へ答えるデータも追加する予定です。
つまり、VIUK Storyの最終形は「物語しかできないモデル」ではありません。
キャラクターが必要なときは、その世界へ入る。
普通に話したいときは、普通に話す。
一対一なら二人の空気を壊さず、複数人物なら場面全体を迷子にしない。
物語を求められたときだけ、ちゃんと物語を始める。
その切り替えまで含めて、VIUK Storyです。
まとめ:GPUは設計ミスまで高速化する
今回、一番の学びはこれでした。
大規模生成で最初に速くすべきなのは、生成ではなく失敗の発見だった。
10万件をどう作るか、20件ずつ手で貼るか、GPUを何台並べるか。そういう話は派手で楽しいです。
でも、次の設計が曖昧なまま高速化すると、あとで全部作り直すことになります。
- AIはどこまで場面を制御していいのか
- ユーザーの領域はどこからか
- 一対一と複数人物で何を変えるのか
- 雑談と物語をどう分けるのか
- 表示タグと内部カテゴリをどう分けるのか
- 何をコードで検査できるのか
ここを決めて、ようやくGPUを増やす意味が出てきました。
まだ10万件の生成も、学習も終わっていません。
ただ少なくとも今は、「疲れた」と言ったユーザーの背後で、毎回校舎が爆発することはないはずです。
たぶん。