前編の最後で、memUとAgent SDKを組み合わせた瞬間に問題が出続ける、という話を書いた。何パターン試してもダメ。で、切り分けのためにAgent SDKを捨てて、Anthropic APIを直接叩く構成に変えた。これでmemU単体は安定するはず・・・と思った。
実際、基本機能は動くようになった。スレッドを跨いだ記憶の保持もできた。
ところが今度は、memUのデータが消える。再起動するたびにメモリが空。調べたら、保存先がデフォルトでinmemoryになっていた。メモリという名前のツールが、メモリ上にしかデータを持ってない。これは笑えない問題。
Codeに頼んでPostgreSQLに切り替えようとしたら、ライブラリ側のバグで断念。公式のmemU-serverに乗り換えたらデータは消えなくなったけど、後でCodeに中身を確認してもらったら、コードは数十行程度・v0.1.0・利用者ほぼなし、ということらしい。本体側にもクリティカルバグがいくつも報告されてる。
memUのコンセプトは好きだった。Markdownで保存できて、出口戦略がある。でもコンセプトと実装の成熟度は別の話で、この上にシステムを積み上げるのは無理がある。
当時の僕は、メモリシステムは既製品を使うものだと思い込んでいた。
Graphiti、Basic-Memory、memU。誰かが作ったメモリ管理ツールを探して、試して、壊れて、次を探す。その繰り返し。自前で作るという発想がそもそもなかった。プログラマーじゃないし、ベクトルDBの使い方なんて知らない。
Codeと何度か話した。Codeが言うには、Qdrant(ベクトル検索エンジン)とSQLite(メタデータ管理)を組み合わせれば、memUがやってることは自前で作れる、と。しかもQdrantはDocker一発で起動できて、Web UIでデータの中身が全部見える。SQLiteはただのファイルで、コマンド一つで中を覗ける。
「中身が全部見える」。これは前編で書いた選定基準そのまま。ブラックボックスを嫌う。逃げ道のあるシステムを選ぶ。
正直、かなり悩んだ。既製品には「エンティティ抽出」「関係性管理」「要約」みたいな高度な機能がある。自前だとそれが全部なくなって、ベクトル検索と保存だけのシンプルな仕組みになる。それで足りるのか。
でもCodeと話しているうちに、考えが変わっていった。Graphitiのエンティティ抽出は哲学的問題に行き着いた。memUの要約はOpenAIのLLMがブラックボックスで処理していた。「高度な機能」が、実際にはトラブルの原因になっていた。必要なのは、確実に保存できて、確実に検索できる基盤のほう。
ちょうどMac移行のタイミングでもあった。環境を全部作り直すなら、メモリも一緒に変えてしまおう、と。Qdrant + SQLiteに決めた。
Codeに実装してもらった。仕組みはシンプルで、ユーザーが何か送ると、バックエンドがQdrantでベクトル検索して、関連する記憶をプロンプトに足して、Claudeが応答して、その会話をQdrant + SQLiteに保存する。全自動。
テスト。動いた。
で、驚いたのが保存のほう。会話するたびに、全部ベクトル化されてQdrantに入っていく。Web UIを開くと、保存されたデータがずらっと並んでる。SQLiteのメタデータもsqlite3コマンドで直接見られる。
全部保存してる(!)。
memUの時はinmemoryで消えていた。Graphitiの時はNeo4jの中身がよく見えなかった。それが今は、全部の記憶が目に見える形で蓄積されてる。ブラックボックスがどこにもない。ようやくまともな基盤に乗った、という感覚があった。
ただ、全部保存するということは、全部検索するということでもある。
俺の生まれた町の名前は?と聞いた。答えられない。
メモリには「福島県郡山市」はちゃんと入ってる。SQLiteで確認した。ある。でもClaudeが答えない。
検索結果を見た。
1位 (0.62): 「俺の生まれた町の名前は?」 ← 質問
2位 (0.54): 「俺の生まれた町の名前を覚えてますか?」 ← 質問
3位 (0.53): 「さっき教えてくれた俺の生まれた街の名前とかって...」← 質問
4位 (0.50): 「ごめん、記憶には断片的な情報しか...」 ← 回答(でも答えなし)
5位 (0.48): 「俺が聞きたいのはその...」 ← 質問
上位5件が全部「質問」で埋まってる。答えの「福島県郡山市」は6位以下に落ちていて、検索結果の上限(5件)に入ってこない。
Codeの説明によると、ベクトル検索は「意味の類似度」で結果を返す。「生まれた町は?」という質問は、「生まれた町を覚えてる?」という別の質問と意味的にほぼ同じベクトルになる。一方で「福島県郡山市」は地名だから、質問文とは意味的に遠い。
質問が質問を呼ぶ。答えは埋もれる。
全部保存していたから、過去に何度も聞いた「生まれた町は?」系の質問がメモリに大量に溜まっていた。ベクトル検索はそれらを「最も関連性が高い」と判断して上位に返す。答えじゃなくて、質問を。
検索手法を工夫する道もいろいろあるらしい。でもその前に、もっと根本的な問題に気づいた。
そもそも全自動が問題だった。
「こんにちは」「なるほど」「あれ?」も全部保存される。何を保存するか判断する主体がいない。全自動だから、何でもかんでも保存して、何でもかんでも検索する。ノイズが溜まって、質問が質問を呼ぶ悪循環が加速する。
全自動は最初めっちゃ面白かったんだけど・・・悩んだ末、やめた。
Claudeにsearch_memoryとsave_memoryという2つのツールを渡して、「いつ検索するか」「何を保存するか」の判断をClaude自身に任せる方式に変えた。
Claudeは「こんにちは」や「なるほど」を保存しない。「ヒロさんの出身地を聞かれた」→ 検索する。「ヒロさんが福島県郡山市出身だとわかった」→ 保存する。人間なら当たり前の判断。それをClaude自身にやらせる。
結果、ノイズが減った。不要な質問がメモリに溜まらなくなって、検索精度も上がった。
Graphiti、Basic-Memory、memU、memU-server。いくつも既製品を試して、結局は自前で作った構成が一番安定した。
迷走していたのは「高機能なツールを探す」方向に走っていた時期だったと思う。機能が多いほど良いと思っていた。でも実際は、機能が多いほどブラックボックスが増えて、トラブルの原因が増えた。
必要だったのは、確実に保存できて、確実に検索できて、中身が全部見える基盤。あとは「何を保存するか」「いつ検索するか」をClaude自身に判断させること。
メモリは、どう貯めるかよりどう使わせるか。