世の中のことは何も答えない。僕のことだけ話す AI チャットを作った

Web サイトの隅っこにいるチャットボット、あるじゃないですか。困って開いてみると、選択肢に自分の聞きたいことがなくて、無理やり近いものを選ぶと見当違いの回答が返ってきて、あれこれ辿っても解決せず、結局「オペレーターにつなぐ」を押してやっと人間が出てくる。あれ、いつも「なんだかなぁ」って思ってたんですよね。
「これじゃない」を押し続けた 40 年
僕がコンピューターと会話したいと思ったのは、たぶん人生で一番古い「作りたい」の記憶です。FAMILY BASIC でプログラムを覚えたての頃、画面の向こうから何か返事が返ってくるのが嬉しくて、言葉を打つと反応してくれるものをさっそく作っていました。IRC の時代には「人工無脳」っていう、特定の言葉にだけ反応する bot が流行って、あれも作りました。単語に反応するだけの、今から見ればおもちゃみたいなものです。でも、打ち込んだ言葉に何かが返ってくるだけで、当時は本当にわくわくしたんです。
要するに、40 年ずっと同じことをやってるんです。コンピューターとしゃべりたい。ただそれだけ。だから冒頭のチャットボットへの「なんだかなぁ」も、裏返せば「自分ならこう作りたい」の裏返しだったんだと思います。
世の中のことは何も知らない AI を作る
そして今週、また同じ衝動に負けました。ウォーゲーム風のゲームを作っていたはずなのに、急に AI チャットを作り出してしまったんです(どういうわけなんだ?)。
作ったのは、世の中のことは何も聞けない、ただ僕のことしか教えてくれない、超ニッチで誰得な AI チャットです。今このサイトの /chat で動いています。中身は Cloudflare AI Search を使った RAG (検索して見つけた情報だけをもとに答えを作る仕組み)で、質問に関係するサイトの内容を引っ張ってきて、それを材料に答えを組み立て、返事が Server-Sent Events という仕組みで 1 文字ずつ流れてくる。天気も、ニュースも、一般常識も知りません。知ってるのは僕のことだけ。
ちなみに最初の実装では、1 回の質問につき検索を 2 回走らせていました。1 回目で「そもそも答えられる話題か?」を判定して、2 回目で答えを作る。ところがこの 2 回が別々の検索なので、たまに食い違うんです。「見つかったよ!」と判定したのに、返ってきた答えは「見つかりませんでした」。……お前ら、同じ社内で連携取れてないんかい。というわけで、検索を 1 回にまとめて、判定と答えを同じ材料から作るようにしました。地味だけど、こういうところで信頼が決まるんですよね。
とにかく情報を食わせたい
せっかく作るなら、たくさん情報がいる。というわけで、片っ端から食わせました。順番はこんな感じです。
| # | 食わせたもの | 何が入るか |
|---|---|---|
| 1 | この Web サイト | プロフィール・プロジェクト・ブログ本文 |
| 2 | GitHub のリポジトリ | ドキュメントや README |
| 3 | YouTube ライブの文字起こし | 雑談の中で語った僕の思想 |
古くなった情報を消してから入れ直す、みたいな地味な仕込みもしています。放っておくと同じ話が二重に登録されて、AI が過去の自分と現在の自分を混ぜて喋り出すので。特に効いたのは 3 番目です。1 本の配信が 3 時間前後あって、その中で自分の考えをだらだら喋っているので、情報ソースとしてかなり濃い。壁打ちみたいな会話の中でこそ、本音がぽろっと出るんですよね。会話の中身を文字にして AI が答えられるようにするのは、ずっとやりたかったことだったので、これができたのは本当に嬉しい。
ちなみに、まだ食わせていないものもあります。X の日々の投稿と、最近閉じた GitHub の Issue。このあたりも取り込めば、もっと今の僕に近い受け答えができるはず。情報源は、増やそうと思えばいくらでも増やせるんですよね。というわけで、これは近いうちの宿題にしておきます。
3 時間の雑談から、僕の言葉だけを抜き出す
その主役になった YouTube ライブの文字起こしが、実は一番の難所でした。
まず、3 時間の配信から音声を取り出すところで詰まります。YouTube から音声を落とそうとすると「お前ロボットだろ」と止められるので、ブラウザのログイン情報を渡して人間のふりをして通り抜ける。取り出した音声も、そのままだと文字起こしにかけるには重すぎるので、モノラルの低ビットレートに圧縮して、ぐっと軽くしてから渡す。ここまでで、ようやく文字起こしのスタートラインです。
そして最大の関門が「誰が喋ったか」でした。配信は複数人の雑談なので、放っておくと AI が他人の発言まで僕の意見として書き起こしてしまう。実際、誰かが喋った「日本酒のスムージー」みたいな話が、いつのまにか僕の持論として記事になっていて、青ざめました。僕の思想だけを答えるチャットにとって、これは一番やってはいけない事故です。ユーザーは「ジョシュアの考え」を聞きに来ているのに、赤の他人の意見を僕の顔で返してしまう。
そこで、要約する前の段階で僕の声だけを切り分ける(話者分離)ようにしました。AI に「声で聞き分けてね」とお願いする方式だと、2〜3 時間の雑談ではどうしても取り違える。だから僕の声を先に物理的に抜き出して、要約する AI には僕の発言しか見せない。そうすれば、そもそも他人の言葉を僕のものと間違えようがない。
こうして出来上がった文字起こしは、ブログ記事として組み立てています。記事のほうは、動画を見ながら読めるレイアウトにもしてみました。音を聴きながら文字も追う、というのは同時には頭に入らないかもしれないけれど、それはそれで一つの楽しみ方かなと。文字起こしコンテンツはこれからも増やしていく予定で、新しいライブや音声通話の文字起こしも回していきます。ネタは、喋れば喋るだけ増えるので。
コストを削ろうとしたら、日本語が英語で返ってきた
情報のほうが片付いたので、次はお金の話です。
このチャット、動かすたびにお金(Cloudflare だと Neuron という単位)がかかります。しかも無料枠を、あっさり超えました。「めんどくさいこと考えずに、答えを作るモデルを軽いやつに変えれば安くなるやろ」と思って、Llama 3.3 70B というそこそこ大きいモデルから、もっと小さいモデルに差し替えたんです。
そうしたら——日本語で質問しているのに、返事が全部英語で返ってくる。「絶対嘘やん!さっきまで日本語で返してたやん!」と。
調べてみると、軽いモデルは、参照した資料の言語に引っ張られる癖があったんです。僕の資料は日本語と英語が混ざっているので、そっちに引きずられて英語で答えてしまう。プロンプトをどういじっても、両方向で安定しない。結局 Llama 3.3 70B に戻しました。質問の言語をちゃんと見て、それに合わせて返してくれるのはこの子だけだったんです。コスト削減という「自分側の都合」より、聞いた言語で返ってくるという当たり前を優先した、というわけです。
コストは、モデルを変えるんじゃなくて、同じ質問の答えを使い回すキャッシュを効かせる方向で削りました。それと、モデルや検索の設定はコードに書かず、管理画面側だけで変えると決めました。前にコード側であれこれ上書きしていたら、本番だけ挙動がずれて日本語が英語になった過去があるので、設定の置き場所は一つに絞る。これがこのチャットの、地味だけど大事な設計方針です。
URL の後ろに、日本語がくっついてくる
表示まわりも、これがまた沼でした。
答えはただの文字じゃなくて、見出しや箇条書き、リンクの整った文章として出したい。だから受け取った文字を装飾つきの表示に変換しているんですが、プロンプトを色々試すたびに表示が崩れる。なかでもリンクの表示に一番苦労しました。答えの中で参照元の URL を出すと、その直後の日本語まで巻き込んでリンクの一部にされてしまう。リンクは URL で終わってほしいのに、後ろの文章まで引きずり込んで、日本語がまるごとクリックできる青いリンクになる。「そこで切れてくれ!」と何度もなりました。GitHub から取り込んだ情報のリンクが、内部用のヘンテコな文字列のまま出てきて、クリックしても飛べない、なんてこともありました。ひとつ直すと、隣でまたひとつ崩れる。
さんざん試した結論として、情報ソースは答えの末尾にまとめて出すことにしました。本文の途中にリンクが散らばると、それだけで読みにくい。文章は文章で読ませて、「これはどこから来た話か」は最後にそっと添える。そのほうが落ち着きます。
1 文字ずつ流すのは、こんなに大変なのか
返事を 1 文字ずつ流す、あの気持ちいい演出。あれ、裏側はぜんぜん気持ちよくありませんでした。
文字が 1 つ増えるたびに、それまでの全文をまるごと装飾つきに変換し直していたんです。10 文字なら 10 回、100 文字なら 100 回。長い答えになるほど処理が雪だるま式に膨らんで、画面がガタガタ、カクカクする。おまけに 1 文字増えるたびに画面を一番下までスクロールし直すので、なめらかスクロールが自分自身と喧嘩して、酔いそうな動きになっていました。というわけで、変換は答えが出そろってから 1 回だけ、スクロールも保存もほどよく間引くようにして、ようやく静かになりました。
スマホだともう一段やっかいで、文字を打とうとしてキーボードが出てくると、その分だけ画面が下からせり上がって、いま読んでいた文章がキーボードの裏に隠れてしまう。地味だけど、実際に触るとかなりストレスなんですよね。ここは「画面の下端からの距離」を覚えておいて、キーボードが出ても同じ位置が見えたままになるように直しました。この手の「言われないと気づかないけど、無いと気持ち悪い」やつが、無限に湧いてくる。
友達に「使いにくい」と言われた(それが良かった)
ある程度かたちになったので、アルファ版だと断ったうえで、友達に使ってもらいました。そうしたら、開口一番「使いにくい」。……いや、でもこの一言が、今の形にめちゃくちゃ役に立ったんです。
具体的にはこんな指摘でした。
- 知りたい情報が返ってこない
- 「これ」「それ」で聞くと、前の話とつながらなくて会話にならない
- ふわっとした質問には、何も出てこない
2 つ目の「これ」「それ」問題は、その場で直しました。それまでは 1 問 1 答で、前のやりとりを覚えていなかったんです。会話の履歴を渡すようにして、「じゃあそれは?」がちゃんと続くようになった。人と話すときは当たり前のことなのに、実装だと抜け落ちる。あるあるですよね。
「情報が返ってこない」が、実は正しい動き
一方で、直さなかった指摘もあります。
友達が「配信の主役の『ロンギヌス』について聞いても、知らないって言われた」と。これは意図してそうしているんです。さっきの話者分離で、AI に食わせているのは僕の発言だけを抜き出したもの。だから僕以外の人のことは知らないのが正解なんです。彼にとっては「情報が返ってこないよ」だったけれど、僕にとっては「正しい動き」でした。
冒頭のチャットボットの話とちょうど逆で、あれは「答えられないのに、それっぽく答えてしまう」のがしんどかった。今回のは「知らないことは知らないと言う」。僕の思想だけを、勝手に盛らずに返す。これがこのチャットの背骨です。
とはいえ全部を突っぱねるのも味気ないので、少しだけ融通も利かせました。「初心者向けのプログラム学習のアドバイスが返ってこない」という指摘については、そもそも初心者向けのコンテンツが手元に何もない。だから返さないのが正しいんですが、そこはちょっとだけファジーに応えるように調整しました。この「正しさ」と「やさしさ」のさじ加減が、いまだに一番むずかしい。
ウォーゲーム作ってたはずなんだけどな
……で、最初の話に戻るんですが、僕はウォーゲーム風のゲームを作っていたはずなんです。それがどうして AI チャットになってるんだ(笑)。でも後悔はしていません。会話の中に色々な思想があると思っていて、それを抜き出して残せる形にできたのは、40 年越しの「コンピューターとしゃべりたい」がやっと叶った感じがして、素直に嬉しいんです。
まだまだ情報を食わせて、誰得返答に厚みを持たせていきます。気になった方は /chat で話しかけてみてください。世の中のことは何も答えませんが、僕のことなら、そこそこ答えます。開発の様子は X(#joshai) でつぶやいているので、変な回答を見つけたらぜひ教えてください。一緒に育てていきましょう。
ボツタイトル供養
- 世の中のことは何も答えない。僕のことだけ話す AI チャットを作った(採用)
- 誰得 AI チャットを作った話
- FAMILY BASIC から 40 年、やっとコンピューターとしゃべれた
- コストを削ったら、日本語が英語で返ってきた
- 「情報が返ってこない」が正しい動きだった
- 友達に「使いにくい」と言われたけど、それで良かった
- 3 時間の雑談から、僕の言葉だけを抜き出す
- 他人の意見を僕の顔で返してはいけない
- 1 文字ずつ流すのは、こんなに大変なのか
- ウォーゲーム作ってたはずなのに AI チャットを作ってしまった