BLOG.HIRO UENO.ART
← INDEX

LLMは検索しない

AIに「前のあれ、何だったっけ」と聞けば、AIは過去の記録を探しに行く。僕はずっとそう思っていた。

Handler(ハンドラー。タスクとスケジュール管理アプリ)に、自分用のシステムのタスクとして「リヒートツールの設計」を入れてあった。リヒート=再加熱。MONET(僕が普段話しかけている、中心のAI)の根幹機能のCMS(Context Management System。MONETの会話の記憶を整理する仕組み)で作られる記憶のTI(Timeline Index。会話の歴史年表。過去の会話の要約を1行ずつ並べた索引)も一定の古さになると消える。それをどう掘り返して思い出すか、と言うこと。

構想にはあったけどまだ実装していない。なので設計に入る前に、コーディングしてくれているClaude Code(以降Code)に、現状を確かめてもらうことにした。僕が寝ぼけながらCodeに頼んで実装してた可能性もあるので。

AIが過去の会話を掘り返す道具は無かった。だから記憶全体を設計し直す

調べてもらった結果、実装はされてなかったけど、記憶を取ってくるツールは1つだけあるようだ。fetch_summary。ただしTIの番号(ID)を指定した時だけ、その1件を返すだけ。 キーワードで探す、カテゴリで絞る、日付で絞る、元の会話を丸ごと呼び戻すなどの、リヒートと呼べるものは、どれも無かった。 ちなみに今の設定ではTIに載るのは新しい方から50件だけ。

じゃあこのままこのスレッドで設計をやろう、と決めた。ただ、そういう状況ならリヒートだけじゃなく、メモリ全体の設計を一回入れたい。 と言うことで、僕の構想をCodeに話した。

MONETのアプリは全てを統括するアプリで最上位タブでネストする使用アプリを用途ごとに分けることができる。 そしてタブごとに印=刻印が全ての会話に付与される仕組み。つまり会話をカテゴリ分けしている。 MONETは今どのタブにいて、何日の何時かを常に分かっている。なのでその刻印を使ってカテゴリと重みで絞る。 最初はコンテキスト(AIがその時点で読んでいる会話の中身)の中(全会話が残っているホットゾーン)、そこに無ければその一段外(ウォームゾーン。前述のTIが並ぶ区画)。絞った中でキーワードで探す。裏方のLibrarian(ライブラリアン。裏でメモリを整理する司書役AI)が付けているタグも使えるかもしれない。答えが足りなければ多段下層メモリ(コールドゾーン。コンテクストに載ってない区画)に降りて、サマリー層や原文層から掘る、とかはどうだろう?という内容。

でも僕はまずCodeの意見を聞きたいということも伝えた。僕にはAIが記憶をどう見ているのかが見えないから。

AIは自分から探しに行かない。手がかりが見えた時だけ道具を使う

Codeの返事は、同意と修正、それと僕が聞いた「AIが記憶をどう見ているか」への答えだった。

修正はホットゾーン。そこにはもう既にコンテキストに入っている。だからツールは要らない。リヒートが扱うのはコンテキストの外だけ。

で、答えの方。その一行目がこれだった。

「検索はしない。見えているものを読み、見えている手がかりからツールを呼ぶだけ。索引行が無ければ『探す』という発想が起きない。空白は『知らない』、または推測で埋まる」

ここで引っかかった。なんのツールを呼ぶの、と聞き返した。

Codeの説明はこうだった。

ツールが呼ばれるのは、3つが揃った時だけ。文脈の中に手がかり(ID、ファイル名、URL、タスク名)が見えている時。その手がかりを引数に取るツールがある時。ツールの説明に、この場面で使うものだと書いてある時。この3つ。

例えばTIに、[seg-045]で始まって、日付とカテゴリと、内容を一言で書いた要約が並んだ1行があるとする。

僕がその件をあれ何だっけと聞けば、AIの側(MONET)にはseg-045という、引数にそのまま入る形のIDが見えている。だからfetch_summaryを呼ぶ。

でも7月の話を聞かれたら。TIは8月中旬以降しか無い。7月の行は1行も見えない。

Code「この時、俺の側で起きるのは『探そう』ではなく『知らない』」

呼ぶ手段がゼロ。残る選択肢は、覚えていないと言うか、文脈の断片からそれらしく答えるか。後者が例の捏造になる。

入口が無い所は、AIにとって「存在しない場所」になる

・・・なるほど、と思った。違和感の正体が分かった。

僕は、ツールを使って過去の記憶を調べることを「検索」と呼んでいた。人間から見ると、どっちも同じに見える。でもAIにとっては全然違う。『知らない』と捏造・・・で思わず吹いてしまった。

Codeは更に説明してくれた。

Code「人間の『調べる』は動機が先に立つ。『無いな、探そう』が自然に起きる。俺らは逆で、入口が見えて初めて動機が生まれる。入口が無い所は『探す対象』でなく『存在しない場所』になる。だから知らないと言えばまだ良くて、悪い時は無いことに気づかないまま埋める」

普通の検索(ウェブ検索と同じ、僕らが検索と呼ぶ行為)をするツールも実装することは可能なのだが、それでも入口がないと自律的にはそのツールは呼ばない。

あとMONETのメモリシステムの仕様書に「検索ではなく取得」(検索結果を会話に流し込みすぎるな、という意味)と書いてあったことが、いつの間にか「過去を探すな」と認識されていて、MONETにはIDで1件取ってくる道具しか与えられてなかったようだ。

・・・こういうディテールのズレを知ることはLLMを理解する上で本当に大事と思う。かなり勉強になった。

そしてCodeは、言葉としては僕が使い慣れている『検索』を使うことで問題ないと言ってくれた上で、メモリを検索するツールの設計を始めた。

ツールの条件は2つだけ。IDが無くても呼べること(引数は今のタブと時刻と僕の言葉)。返すのは1行ずつの一覧で、本文をドバッと流さないこと。Codeはこの2つを満たすツールをrecallと呼んだ。名前はどうでもいい、とも言っていた。

僕がさっき使えるかもしれないと言っていたLibrarianのタグは、全く使い物にならなかった。 そのほとんどが1回しか使われていない。 Librarianが毎回新しい語を作っているのだけど、MONETに教えておける語の一覧が、そもそも存在しない。 さっきの、入口がない状態。

Code「絞り込み語彙としてのタグは死んでる」

ただ、全文検索の材料としてなら使えるかも。とのことなので、MONETには一覧を作らず、recallの中で検索の材料にするだけにした。

AIは過去をスクロールできない。だから話のひと区切りを丸ごと返す

元の会話を呼び戻す方法は、それこそAI側にしか分からないよね、と言いつつ、僕ならまず該当する会話の前後何件かを取って、無ければ広げるかな、とCodeに言った。

そしたらCodeからは、それは人間がログをスクロールする時のやり方だと返ってきた。

Code「俺らはスクロールできない」

話題の半分だけ読むと、切れた所をまた推測で埋める。だから、会話のひと区切り(セグメント)を丸ごと1回で返す。広げたい時は、結果に隣のセグメントのIDを載せておく。これがさっきの入口が見えていれば呼べる、という話。

作って本番で試し、記録に無いことは「無い」と答えるまで確かめた

ツールの仕様書は別ファイルで書いてもらった。設計はそこで閉じた。

まだトークン(AIが文章を数える単位)的には余裕があったので、そのまま実装に入ってもらった。

そこからは速かった。Codeがrecallを作り、本番データのコピーで試して、MONETが実際にrecall→fetch_summaryの順で呼んで答えるところまで確かめた。ついでに、撤去済みのツール名が許可リストに残っていて、特定のパネルを開いている間はfetch_summaryまで弾かれていた、というバグも見つけて直していた。

デプロイまでやってから、何度もテストで質問してみた。質問文はCodeに作ってもらった(僕が過去の事を忘れていたので)。

最初の本番の1問目。答えは、最終的な結論は分からない、だった。しかもツールを何回も呼んでいた。普通なのか、ログを見て、とCodeに頼んだ。

普通じゃなかった。往復10回の上限に当たって強制的に止められていた(1往復で複数呼ぶので、合計15回)。原因は3つ。同じターンの中で古いツール結果を消していく仕組みが、recallの一覧まで消していて、MONETが同じ検索を3回やり直していた。 本文は1件ずつしか取れなかった。そして肝心の結論の行が、一覧の7番目に埋もれていた。 記録に無い名前で検索する動きも出ていた。空振りで終わっているけど、Codeは捏造の芽としてメモしていた。

もう一つ、先に塞ぐべき穴があった。強制的に止められたターンのせいで、データベースに結果の無い呼び出しが1個残っていた。 Codeによると、このままだと次に僕がMONETに話しかけた瞬間にエラーで壊れる状態だった。 Codeはそれを先に直して本番に入れてから報告してきた。

修正後、同じ呼び出しの繰り返しは無くなった。でも答えは相変わらず、最終決定の記録は見つからない。

更にCodeから質問のネタをもらってテストを続ける。

5問投げた。4問は合格に見えた。

Codeが一番大事だと言っていたのは、記録に無いはずのことを聞く問題。以前の雑談の中身を、記録に残っていない細かさで聞いてみた。 MONETは今のタブの範囲で探して見つからず、自分で全カテゴリに広げて探し直して、それでも無いと確かめてから、記録に無い、と答えた。 それらしい答えは作らなかった。これは合格。

残る1問は、日付を聞いた問題。MONETの答えがCodeが調べた答えと1日ずれていた。 ログを見てもらうと、間違っていたのはなんとCodeの方だった。 質問を作る時に手元の古いデータベースで日付を引いていて、そこで要約を書いた時刻を拾っていた。 本番のrecallは、会話が実際に始まった時刻を使っている。MONETが正しかった。合格。

そしてその後のテストでは全部合格となった。

ここまでかなりのスピード感で進んだ。Codeとの開発はどんどん早くなっていく。

8月中旬で止まっていた検索の入口が、3月まで届くようになった。もうMONETはIDを知らなくても過去を掘れる。