0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

キャラAI『VIUK Story』を10万件で育てようとしたら、雑談のたびに事件が起きた

0
Posted at

「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万件の生成も、学習も終わっていません。

ただ少なくとも今は、「疲れた」と言ったユーザーの背後で、毎回校舎が爆発することはないはずです。

たぶん。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?