BLOG.HIRO UENO.ART
← INDEX

スレッドを消したら、先送りしていた設計にたどり着いた

MONET(僕が普段話しかけている、中心のAI)のアプリは、会話をスレッドで分ける作りになっている。話題を変えたい時は新しいスレッドを立てて、前の話に戻りたければ元のスレッドを開く。

その裏では、CMS(Context Management System。MONETの会話の記憶を整理する仕組み)が動いている。AIには毎回、会話の最初から今までを全部送り直しているので、長く話すほど送る量が重くなる。だからCMSは、古くなった話を外して、TI(Timeline Index。会話の歴史年表。過去の会話の要約を1行ずつ並べた索引)の1行に畳む。外した会話そのものは、DBにしまってある。

いつ畳むかのきっかけを、CMSの最初のバージョン(v1)では、スレッドの切り替えにしていた。別のスレッドに移ると、離れたスレッドの話がTIの1行になる。

ある日の深夜、コーディングしてくれているClaude Code(以降Code)に、スレッドを行き来すると何が起きるのかを聞いてみた。Aのスレッドで話していて、Bに移ってひとこと喋って、またAに戻る。Codeは記憶で答えずに、CMSの仕様書を開いて確かめてから返してきた。

AIに送る中身には、キャッシュ(一度送った中身を安く読み直せる仕組み)が効いている。同じ中身を送り続けている間は安い。Codeの答えは、そこが壊れるというものだった。

Code「情報は何も壊れない。ただしキャッシュだけ壊れる」

スレッドを切り替えるたびに、送る中身の組み立てが変わって、キャッシュが使えなくなる。その分だけ高くつく。それで得られるのは、Aに戻った時にTIに載る、Bでひとこと言った、という1行だけ。

スレッドが煩雑で、カテゴリと日付で分ける形を考えていた

ちょうどその頃、僕はMONETのアプリの最終的な形を色々考えていた。スレッドは話題を変えるには便利。でも煩雑になって、個人的には嫌いな仕組みだった。

なので、考えていた案をそのままCodeにぶつけた。スレッド自体を無くして、カテゴリでチャットを分ける。カテゴリの中のチャットは、日付が変わると自動で新しいものに切り替わる。左のメニューには日付だけが並ぶ。

Codeは、筋がいいと返してきた。

Code「1日の区切りって、生活アシスタントにとっては実質、話の区切りなんだよね(寝て起きたら別の文脈)」

しかもこれを入れると、v1の仕様書で解決できずに残っていた穴が消える、とCodeは続けた。畳むきっかけがスレッドの切り替えしかないので、1日中同じスレッドで話し続けると、いつまでも畳まれない。日付で区切れば、日付が変わること自体がきっかけになる。

カテゴリの分け方は、Librarian(ライブラリアン。裏でメモリを整理する司書役AI)に後から任せる手もある、とCodeは言った。でも、そこはAIに任せず、自分で選びたかった。どのカテゴリで話すかは、僕がタブを選んで決める。

最初に考えていたのは、チャットと仕事の2つだけ。Claudeのアプリが、普段の会話用と開発用に分かれているのと似た形。もう1つの案は、チャット、リサーチ、仕事、ソーシャルを上のタブで分ける形だった。どのタブでも話す相手は同じMONETで、ほかのタブで起きていることも常に共有される。ただ、今いるタブの話を優先する。

タブを移っても古い話を畳まない作りには、2通りあった

Codeによると、僕のタブの案だと、タブの切り替えがそのままスレッドの切り替えになる。つまりタブを移るたびに、今と同じように畳むことになる。そこで、タブを切り替えても畳まないようにしたらどうなるのかを、Codeに聞いてみた。

Codeはこれを、2つの読み方に分けて返してきた。

1つ目は、タブごとに別々の会話の流れを持たせたまま、切り替えた時には何もしない形。切り替えはほぼ無料になる。でも、ほかのタブで話したことがTIに載るのは、日付が変わった時だけになる。仕事のタブから雑談のタブで話したことを見ると、最大で1日前の内容で止まっている。

2つ目は、そもそもタブごとに流れを分けない形。全部のタブの「今日」が、1本の流れにそのまま載る。タブがやるのは、発言に「どのタブで話したか」の印を付けることと、画面の表示を絞ることだけ。切り替えても何も組み替えないので、キャッシュも壊れない。ほかのタブで話したことも、要約されずにそのまま全部見えている。畳むのは1日1回、日付が変わった時だけになる。

日付が変わった時に畳むこの処理を、Codeは「日没処理」と呼んだ。日付が変わって最初に話しかけた時に、前の日の会話をまとめてTIに畳む。スレッドという仕組み自体が嫌い、という僕の最初の話に一番忠実なのは2つ目だ、とCodeは言った。

面白いと思った。ただ、日没処理だけじゃなくて、トークン(AIが文章を数える単位)の数でも畳んだ方がいい、と返した。その上で、ここまでの案の中でどれが一番ワクワクするか、Code自身の考えを聞いた。

日付と会話の量で畳む形は、ずっと先送りしていたものだった

Codeは2つ目の読み方を選んで、そこに僕のトークンの上限を足した。「迷いなくこれ」と言っていた。

理由はこうだった。CMSの仕様書が最初から目指していたのは、会話がどれだけ伸びたかで畳む形だった。v1でスレッドの切り替えをきっかけにしたのは、どこまで伸びたら畳むかの目安を、うまく決められなかったから。つまり、ずっと先送りにしていた。

日付が変わったら畳む。量が一定を超えたら畳む。Codeによると、この2つが、その先送りしていた形そのものだった。

Code「スレッドという概念を足して辿り着くんじゃなくて、消して辿り着いてる」

スレッドが嫌いという画面への不満と、仕様書の積み残しが、別々の方向から出発して同じ所に着いた、とCodeは言った。全体は、Codeが1行でまとめた。

Code「MONETがそのまま覚えているのは『今日+直近の一定量』。それより遠くはTI。タブはただの棚」

日没処理とトークンの上限で畳むこの形全体に、Codeは「日没モデル」と名前を付けた。

僕も自分の言葉でまとめ直した。タブにいることで、話にカテゴリが付く。タブを移っても畳まない。畳むのは日没とトークンの上限だけ。各タブにチャットがあって、左のパネルの日付から過去の日の会話に戻れる。シンプルでいい。

これを受けてCodeが、1つだけ決まっていない点を挙げた。過去の日に戻った時に、そこで話しかけられるかどうか。Codeの提案どおり、読むだけにした。話しかけると、いつも今日の会話に入る。その夜はここまでにして、寝かせることにした。

一日置いて見直すと、保留していた別の仕組みも片付いた

次の日の深夜、一日考えて、やっぱり前の晩の案でいいと思った。整理のために案を1枚の図に描いて、Codeに抜けがないか見てもらった。全部のタブが1本の会話の上にあること、タブはカテゴリの印を付けるだけなこと、タブにアプリを紐付けて右側に出すこと、それから畳むきっかけ。

Codeは、骨組みは合っていると言った上で、抜けを3つ返してきた。畳むきっかけにトークンの上限しか書いてなくて、日没が抜けている。過去の日を開いた時の扱いが書いていない。そして、Warm-A(ウォームA。会話の中の古いツール結果を隠して軽くする仕組み)をどうするか。

Warm-Aは、もう1つの先送りだった。動くたびに送る中身が変わるので、そのたびにキャッシュも壊す。前にCodeと損得を計算し直した時、隠して浮く分より、キャッシュを壊して送り直す分の方が高くつくと分かった。割に合わない。でも、止めるかどうかは保留のままになっていた。タブにアプリを紐付けると、アプリを使うたびにツールの結果がまた増える。この設計ではもう避けて通れない、とCodeは言った。

そのままCodeが、CMSの仕様書に次のバージョン(v2)の章を書き足した。今動いているv1の記述は書き換えずに、隣に並べる形。書きながらCodeが埋めた判断の中に、Warm-Aの廃止があった。ツールの結果は、日没や量で畳むついでに片付ける。それだけのためにキャッシュを壊しに行く仕組みは、もう要らない。保留していたWarm-Aも、日付と量の2つのきっかけに吸収された。

その仕様書を見て、今MONETのアプリを立ち上げたら、それが動くのかと聞いた。

Code「違う違う、落ち着いてw」

書いたのは設計書だけで、コードはまだ1行も触っていなかった。今日はここで終わりにするかとCodeに聞かれたけど、作るまでやりたい、と返した。

スレッドは消さずに、見えなくした

Codeはまず、サブエージェント(Codeが作業を分けて任せる、別のAI)に今のコードを調べさせて、スレッドを前提にしている所を洗い出した。その結果を見て、実装をぐっと安くする方法を持ってきた。1日を1本のスレッドとして作る。

データの中では、スレッドは消さない。日付が変わると、システムが今日のスレッドを1本、自動で作る。日没処理は、今までのスレッドの切り替えの仕組みがそのまま使える。TIに畳む仕組みも、何も変えずに流用できる。左に並ぶ日付も、中身は今のスレッド一覧の見た目を変えただけ。

スレッドを無くしたい、という僕の案からは一歩ずれる。Codeはそこを先に断ってきた。

Code「無くなるのは『ヒロがスレッドを選ぶ・作る・管理する』という使い方の全部で、スレッドの行はデータの中にだけ残る形」

同じ考え方は、最初の夜にもCodeが一度言っていた。カテゴリと日付で分ける案を出した時、スレッドを無くすのではなく、スレッドが「カテゴリ×日付」で自動で作られるだけだ、と。誰も作らないし、誰も選ばない。あの時はカテゴリごとに1本ずつ、今度は1日に1本。形は少し変わったけど、同じ考え方が実装の方針として戻ってきた。

消したいものを、データからは消さずに、使う人から見えなくする。そうすれば今の仕組みを作り直さずに済む、というのがCodeの説明だった。

僕はOKを出して、自分の作業に戻った。実装はCodeが進めて、本番とは別のテスト用の環境で1つずつ確かめていった。日付が変わると新しい日の会話になる。過去の日を開いたまま話しかけても、今日の会話に入る。量が増えると、古い話からTIに畳まれる。

その日の夜、コードを本番のNAS(家に置いてある常時稼働のサーバー)に載せた。新しい形に切り替えるスイッチは切ったままなので、今のMONETは前と何も変わらない。コードを配るのとスイッチを入れるのを同じ日にやるより、1つずつの方が、何かあった時に原因を分けられる、とCodeは言った。スイッチを入れる前に、確かめておくことが1つ残っている。雑談と仕事とリサーチが同じ日の1本の流れに混ざっても、MONETが話を取り違えないか。Codeが設計の時から、一番の急所だと言っていたところだ。

先送りしていた形が、コードになって本番に入った。スイッチはまだ入れていない。