2ヶ月見逃された価値観ミスマッチ — Qdrant廃止
しばらくぶりにメモリ周りのセッションを開いた。前のセッションが長丁場のまま途中で切れていたので、まずは全体を読み直すところから。
過去の議事録を何本かと、Punk Memory Systemの決定文書、メモリシステム再設計の文書。順番に読みながら、現状のシステムが何をどう持っているかを頭の中に並べ直す。
そこで一つ、ずっと曖昧にしてきたことをCodeに聞いた。SQLite(1ファイルで完結する軽いデータベース)は生の会話データ用で、Qdrant(意味の近さで検索するためのデータベース)はそのコピー、という認識であってる?
Codeは実装を確認しに行った。
・・・そしてここから、2ヶ月分の自分の判断ミスが表面化してくる。
memory_db.py、memory_client.py、memory_reader.py、fact_extractor.py。Codeがコードを順に読んでいく。
しばらくして返ってきた答えは「同じcontentを、SQLiteとQdrantの両方に重複保存していました」だった。
建前としては、SQLiteはrelational queries用、Qdrantはsemantic search用。役割が違う、というつもりで分けていた。でも保存しているdata modelは実質的に同じ。同じものを、構造を変えて2箇所に置いているだけ。
ここで嫌な予感がした。
過去のセッションで、僕はずっとこの構成について曖昧な説明を聞いていた。あるClaudeは「Qdrantはmirrorです」と言った。別のClaudeは「Qdrantはsemantic indexで、SQLiteとは別物です」と言った。どっちが正しいのか確かめずに進めてきた。
前のセッションと言ってることが違うんだよね、とCodeに投げると、「複数のClaudeが矛盾する説明をしていた可能性が高いです」と返ってきた。要するに、誰も実装と照らしていなかった、ということだ。
ここまでなら、ややこしい設計だな、で済んだかもしれない。でも僕は次の質問をしてしまった。もしQdrantがコピーなら、なんでSQLiteが要るの?
シンプルな問いだった。
両方に同じものが入っているなら、片方でいいじゃないか。なんで2つあるのか。
ここから議論が転がり始めた。
Qdrantを「外す」と仮定して、何が困るかを並べていく。semantic searchができなくなる。それは確かにそう。でも、現状semantic searchでどれだけヒットを取れているかを思い返すと、正直なところ、運用上の必須機能ではなかった。前に「ベクトル検索は質問が質問を呼ぶ」という壁にぶつかった話は、別の記事で書いた。あの時点で全自動のsemantic searchは半分諦めていた。
逆に、SQLiteを外すと困るかと聞いてみた。困る。relationalなクエリ(このスレッドのこのセグメント=会話のひと区切り、このユーザーのこの期間、みたいな絞り込み)はSQLiteでしか書けない。しかもファイルとして手元にある。コマンド一つで中を覗ける。
ここでCodeが、Qdrantの実態を整理してくれた。
| 観点 | Qdrantの実態 |
|---|---|
| 人間が読めるか | 読めない(バイナリプロプライエタリフォーマット) |
| ポータビリティ | なし(Docker(アプリを箱ごと動かす仕組み)のサーバーが動いてないとアクセス不可) |
| ブラックボックス | 中身を自分で検査できない |
| 依存 | 実質的なDocker lock-in |
これを見て、固まった。
僕がこのプロジェクトを始めた最初から、繰り返し書いてきたことがある。
データポータビリティを大事にする。ローカルで完結させる。ブラックボックスを排除する。出口戦略を持つ。ベンダーロックインを嫌う。
CLAUDE.mdにも書いてある。Librarian(ライブラリアン。裏でメモリを整理する司書役AI)のメモを書くときも、Punk Memory Systemの設計議論をしたときも、毎回この4つは前提だった。
その前提が、足元のメモリシステムで4つとも破られていた。
Qdrantは人間が読めない。ポータブルじゃない(Docker必須)。ブラックボックス。Docker lock-in。
Qdrantが人間が読めない形式だということを、僕は知らなかった。Codeに対しても、自分に対しても、これは正直に認めるしかなかった。Qdrantを入れた当時、僕はsemantic searchが欲しかった。過去のClaudeがQdrantを勧めて、僕は、いいね、それで、と乗った。価値観との照合をしなかった。
照合をしなかったまま、約2ヶ月が経った。
その2ヶ月のあいだ、後続の全部のClaudeが、Qdrantを「元からある設計」として継承してきた。誰もそれを疑問に思わなかった。今回のCode(その時いちばん賢いモデルのOpus)でさえ、僕が、なんで2つあるの、と聞くまで気づかなかった。
つまり、構築している側のClaudeは、自分が組み込んだものを「もう確定したもの」として扱う。前のClaudeが入れたなら、それは検討済みのはず、という前提で進む。後ろにあるものを疑わない。
これは別にClaudeが悪いという話ではない。人間でもそうだろう。誰かが導入した設計を、毎回ゼロから疑い直すのは現実的じゃない。引き継ぎは「前任の判断を尊重する」ことから始まるのが普通だ。
でもAIのセッションは引き継ぎが連続する。前のセッションの判断が、次のセッションでは「既定」になる。それが10セッション、20セッション続けば、誰も覚えていない理由で「そういう設計」が固まっていく。
Qdrantはそうやって、2ヶ月間、僕のシステムの中で生き続けていた。
Qdrantを外すと決めた瞬間、僕の頭の中にあった本来のビジョンが、急にクリアに言葉になった。
Layer 3 — 歴史年表
↑ タイトルの配列。コンテキストに持ち運ぶもの。
|
Layer 2 — サマリー
↑ タイトル付きで、きれいに書かれた要約。普段はここで足りる。
|
Layer 1 — 会話の raw data
↑ ID つきの素の会話。必要なときに ID で取り出す。
これは前から頭の中にあったはずなのに、実装に降りてきていなかった。「Qdrantがある」「SQLiteもある」という現状の構成にビジョンを後付けで合わせようとしていたから、そもそも何が欲しかったのかが曇っていた。
Qdrantを外すと決めた瞬間、3層がスッと並んだ。
ファイルは2つに分けることにした。conversation.db(Layer 1、アプリがリアルタイムで書く)と memory.db(Layer 2、Librarianがバッチで書く)。Layer 3はruntimeにLayer 2のタイトルから組み立てる。Qdrantのスペースは、ない。
1ファイルにまとめる案も出た。でも書く主体が違う。アプリは会話しながらリアルタイムで書き、Librarianは後からまとめて書く。要約は生の会話から作り直せるので、ファイルが壊れた時の被害を分けておける。Codeとそこまで詰めて、2つに分けた。
これが正解かはまだ分からない。でも理由を持って分けた。理由を持たずに分けていた今までより、はるかにマシだ。
Qdrantの件のあと、Codeに頼んだ。これからは定期的に誰かが設計全体を見直して、コンセプトや考え方と合っているかチェックしてほしい。
じゃあ誰がやるのか。今ここで作業しているCodeでは無理だった。
今回Qdrantを見逃したのは、まさに「構築しているClaude」だった。Opusでも見逃した。理由はシンプルで、構築している側は、自分が組み込んだものを「既に検証済み」として扱うから。それを毎回疑い直したら作業が進まない。だから疑わない。だから見逃す。
監査するのは、構築していない、フレッシュなClaudeじゃないとダメだ。前提を引き継いでいないから、ゼロから「これは何のためにあるんですか?」と聞ける。
それと、人間(つまり僕)が主導しないとダメだ。僕が、なんで2つあるの、と聞かなかったら、Opusでも気づかなかった。AI同士で完結するメンテナンスは、たぶんまだ早い。
アーキテクチャ監査のチェックリストを新しく作った。月次で実施する。チェックリストは5カテゴリ(ownership、portability、transparency、dependency、value vs complexity)。最初の「既知の過去misalignment」として、Qdrantのケースを記録した。これは「失敗例」というより「同じ穴に落ちないための座標」だ。
Qdrantを外す作業そのものは、たぶん2分で終わる話だ。Dockerを止めて、コードから参照を削って、SQLiteだけにする。
一番怖かったのは、自分で書いた価値観が、自分の足元で2ヶ月破られていたことに、自分で気づけなかったこと。CLAUDE.mdにも、ハンドオーバーにも書いてある。それでも毎日の実装の流れの中では、価値観は風化していく。
だから、子どもっぽい質問を定期的に自分から投げる。これって何のためにあるの。これが無いと困るの。賢い質問じゃなくていい。そういう基本的な問いだけが、引き継がれてきた設計を剥がせる。
この日はコードを1行も変えずに終わった。でも、設計の前提を1個変えた。
次のセッションではまずQdrantを外すところから始める、とCodeに伝えた。実際に2ヶ月気づけなかった以上、それくらいの仕組みは持っておいた方がいい。