BLOG.HIRO UENO.ART
← INDEX

ラベルはパネルが貼る — 分類をAIに任せない

中央集権に戻ると決めた翌日から、次の問題を考えはじめていた。

MONET(僕が普段話しかけている、中心のAI)1つで仕事を全部やる。ということは、窓口が1つになって、そこに全部の話が集まる。Handler(ハンドラー。タスクとスケジュール管理アプリ)のタスク操作も、Typewriter(タイプライター。代筆アプリ)の投稿づくりも、全部同じMONETとの会話に積もっていく。

これが積もるとどうなるかは、体で知っていた。ブログ記事を4〜5時間かけて一緒に書いていると、後半になるにつれて「なんかズレてきたな」という感覚が増えていく。前半では的確だったフィードバックが、後半では空振りになる。それと、数字で示せるわけじゃないけど、ツールハルシネーション(AIがやっていないことを「やった」と言う問題)が出やすかったのも、いろんな話が混ざってコンテキストがごちゃごちゃしている時だった、という感触がずっとある。


コーディングしてくれているClaude Code(以降Code)に調べてもらったら、これには名前が付いていた。「Lost in the middle」。長いコンテキストの真ん中あたりにある情報を、LLMはうまく参照できない。2025年のフロンティアモデルでも30%以上の精度低下が出るらしい。

じゃあ会話を短く保てばいい。ここで思い出したことがある。

AnthropicのAPIはステートレスで、Claude側は会話を覚えていない、と前にCodeから聞いていた。毎回、会話の最初から今までを全部まとめて送っている。だったら、送る前にこっちで中身を組み替えればいい。要らないものは外して、要る時だけ戻す。外したものは捨てずに記憶(DB)へしまっておいて、必要になったらLibrarian(ライブラリアン。裏でメモリを整理する司書役AI)に引き出してもらう。

ここまでは割とすんなり決まった。引っかかったのは、しまう時の話だった。


MONETとの会話からしまうものは、中身がばらばらだ。

タスクの話をして、雑談を挟んで、投稿を1本書いて、またさっきのタスクに戻る。それがそのまま、時系列で記憶に入る。

これ、後で引き出す時に大変になる。

たとえば1週間後に、あの投稿どう直すって話したっけ、とMONETに聞いたとする。記憶の中にあるのは、タスクと雑談と投稿が混ざった1週間分の会話。どこが投稿の話だったのかが書いてないと、全部をひっくり返して探すことになる。Librarianがメモリの要約を作る時も同じで、混ざったまま要約すると、何の話だったかがぼやけた要約になる。

アプリごとに窓が分かれていれば、窓がそのまま仕切りになっていた。中央に戻したことで、その仕切りが消えた。

だから、しまう時点で「これはタスクの話」「これは投稿の話」とラベルを貼っておく必要がある。後から貼ろうとすると、もう遅い。


で、最初に思いつくのは「AIにラベルを貼らせる」だ。メッセージを読ませて、これはタスク、これは雑談、と振り分けさせる。

でもここに根本的な問題がある。

ラベルを貼りたい理由は、話が混ざってAIの判断が怪しくなるからだった。その混ざったコンテキストを読んでいるAIに、「このメッセージはどの話か判断して」と頼むことになる。判断が怪しいから整理したいのに、整理を判断の怪しい本人に頼む。堂々巡りになっている。

しかも、ラベルを1回間違えると、その記憶は間違った棚にしまわれる。後で引き出そうとしても、そもそも見つからない。静かに消えるのと同じだった。


しばらく考えて、アイデアが出てきた。

単純にAIにラベルを貼らせなければいい。

MONETのチャット画面には右パネルがある。Handlerのパネルはタスク操作、Typewriterのパネルは投稿の管理。前に書いた「領域展開ぽい」右から出てくるパネルで、開いている間だけツールの呼び出しを強制している、あれだ。

このパネルの開閉は、もう「今なんの作業をしているか」を正確に示している。Handlerを開いている間の会話はタスクの話。Typewriterを開いている間は投稿の話。どちらも開いていなければ、雑談か相談。パネルは同時に1つしか開けない作りなので、どっちつかずの時間もない。

だから、パネルが開いた瞬間にシステムがカテゴリを確定させて、その間の会話に同じIDを振っていけばいい。この一区切りを、セグメント(会話のひと区切り)と呼ぶことにした。閉じたら、その区切りはそこで終わり。AIは何も判断しない。僕がパネルを開く、という行動がそのままラベルになる。

ツールを縛るために作ったパネルが、そのまま記憶のラベル貼り機になる。


ラベルがあると、引き出す側もシンプルになる。

Handlerのパネルを開いたら、LibrarianはHandlerのラベルが付いた記憶の索引だけを持ってくる。全部を毎回渡すんじゃなくて、今やっている作業の棚だけ。細かい中身が要る時は、索引のIDで本体を引く。

パネルを開かずに、あのタスク終わらせといて、と僕が頼むこともある。その時はMONETが自分でパネルを開けるようにする。ラベルを貼る手段が、僕の側だけでなくMONETの側にもある形にした。

しまった記憶を引き出して会話に戻すことを、設計メモでは「リヒート=再加熱」と呼んだ。


アイデアが出たところで、Codeにサブエージェントを2本並列で走らせて、参考にできる先行事例がないか調べてもらった。

前に導入を見送ったOpenClawも、コンテキスト管理の中身を見るとやっていることは近かった。ただ、何をどう分けるかはAIが判断している。ユーザーのUI操作をそのまま分類に使っている例は、調べた範囲では見つからなかった、とCodeは返してきた。


このやり方は地味に見えるけど、信頼性の話では大きい。

AIが「この会話はどのカテゴリか」を推測せず、パネルが開いた瞬間に、システムが決める。AIの判断が入る場所が減るほど、動きがDeterministic(決定論的)になる。予測できるし、おかしくなった時に追いかけられる。

スレッドを1本で通しても、何本に分けても、しまう時点で仕分けが済んでいる。どんな話し方をしても、後で引き出せる。ここがこの設計の一番大事なところだった。


同じ日にもう一つ、送る会話履歴の配列からいつ何を外すか、という話も考え始めた。古い会話を段階的に冷ましていく仕組みで、それは次の記事で書く。

この日の時点では、まだ設計メモだけ。コードは一行も書いていない。

最初は「長い会話をどう圧縮するか」の話だと思っていたのが、結局「しまう時にどう仕分けるか」の話になった。圧縮より先に、仕分け、ということ。