AIに記憶を持たせる。言葉にすると簡単だけど、これがまあ厄介だった。
このプロジェクトで最初に使ったメモリシステムはGraphitiという。Neo4j(グラフデータベース)をバックエンドに持つ、ナレッジグラフベースのメモリ管理ツール。会話から「誰が」「何を」「いつ」やったかをエンティティとして抽出して、関係性をグラフに保存する。
仕組みとしては悪くなかった。ただ、使い込んでいくと問題が出てきた・・・。
一番の問題は、エンティティ抽出が哲学的になること。
「ヒロがVoiceChatBotのUIを改善した」という事実をグラフに保存する時、「ヒロ」と「VoiceChatBot」と「UI改善」をエンティティとして切り出す。ここまではいい。でも「UI改善」は一つのエンティティなのか。「UIの色を変えた」と「レイアウトを直した」は同じエンティティか、別か。粒度をどこで切るかが、どんどん曖昧になっていく。
日本語だとさらに厳しい。主語が省略される言語だから、「改善した」の主語が僕なのか、コーディングを任せているClaude Code(以降 Code)なのか、文脈から推測するしかない。Graphitiの英語前提のエンティティ抽出に日本語を通すと、主語の混同が頻発した。
もう一つ。Neo4jに入れたデータが取り出しにくい。グラフDBは関係性の探索には強いけど、「最近の会話をまとめて見たい」みたいな単純な用途では使い勝手が悪い。データは入る。でも出すのが大変。
「メモリ管理層」と呼んでたけど、実態は会話を要約してDBに放り込んでるだけだった。
このGraphitiから、教訓を一つ持ち帰った。
「良いシステム」より「逃げ道のあるシステム」を選ぶこと。
Graphitiの技術自体は面白かった。でもNeo4jにロックインされる。データを別のシステムに移そうとすると、グラフ構造ごと引っ越すことになる。簡単じゃない。次を選ぶ時は、まず出口があるかを見る。そう決めた。
次の候補を調べていくうちに、メモリシステムの構造が見えてきた。2層に分かれている。
┌──────────────────────────────────┐
│ 上位層: メモリ管理 │
│ 長期/短期の分離、圧縮、検索、 │
│ エンティティ抽出、関係性管理 │
│ 例: Letta, memU, Graphiti │
└──────────────┬───────────────────┘
↓
┌──────────────┴───────────────────┐
│ 下位層: ストレージ │
│ データ保存、ベクトル検索 │
│ 例: Qdrant, Chroma, Neo4j │
└──────────────────────────────────┘
Graphitiは上位層で、Neo4jを下位層として使ってた。この2層の組み合わせで性質が決まる。上位層の賢さと、下位層のポータビリティ。両方見ないと判断を間違える。
で、候補を調べた。
Letta。UC Berkeley発のスタートアップ。$10M調達、Jeff Dean(Google DeepMind)やClem Delangue(HuggingFace CEO)が投資。オープンソースで開発が活発。信頼性は候補の中で一番高い。
ただ、.afという独自フォーマットで保存する。バイアウト狙いのスタートアップの匂いを感じて、買収されたらプロダクト方針が変わるリスクが気になった。VCが入ってるから安全、とは限らない。入ったら出にくいタイプ。
memU。NevaMind AIという小規模チームが開発。GitHub 2.6k stars、毎日コミット、v0.8.0まで進化。ベンチマーク(LoCoMo)92%でトップクラス。
そして最大の特徴は、Markdownで保存すること。最悪memUが消えても、データはただのテキストファイルとして読める。出口が最初から組み込まれてる。Graphitiの教訓がそのまま効く選定基準だった。
チームの透明性は低い。でも開発が活発で、コンセプトに共感できた。
下位層の候補としてはQdrant。Rust製で高速、Web UIでデータの中身が確認できる。日本語対応も優秀で、以前からVoiceChatBotプロジェクトで推奨されてた。
memUに決めかけた、のだが。先にBasic-Memoryというものを試してる。
Basic-Memoryは名前の通りシンプルなメモリシステムで、Markdownで保存、MCPプロトコルで接続できる。読み書きは動いた。
検索が壊れてた・・・。
FTS5(SQLiteの全文検索)のインデックスが構築されない。トークナイザ(unicode61)が日本語に最適じゃない問題もあった。「俺は誰?」と聞いても「ヒロさんについて」というノートがヒットしない。キーワードベースの検索だから、意味的な検索もできない。
さらに、Basic-Memoryのメモリコンテキストをプロンプトに追加すると、Agent SDKがユーザーのメッセージを正しく受け取れなくなるバグにも当たった。原因不明。MEMORY_ENABLED = Falseで無効化して、撤退。
Basic-Memoryがダメで、memUに戻ってきた。
Codeにインストールしてもらって、テストした。
まずバグにぶつかった。_llm_clientsの初期化順序がおかしい。Codeがローカルで直して、基本動作は通った。memorize(保存)もretrieve(検索)も動く。
日本語テスト。「プロジェクトの状況は?」でVoiceChatBotの情報がスコア0.74で返ってくる。埋め込みはOpenAIのtext-embedding-3-smallだけど、日本語検索は実用レベルだった。
日本語特化の埋め込みモデルRuri(名古屋大学のチーム開発)との統合も可能なことがわかった。LM StudioやOllamaからローカルで動かせて、memUの設定を変えるだけで切り替えできる。軽量モデル(37Mパラメータ)でOpenAIの汎用埋め込みを日本語で上回るベンチマークが出てる、というのも気になった。
memU採用決定。Markdownで保存されて、ベクトル検索が効いて、日本語が通って、出口がある。Graphitiの教訓をクリアしてる。
ただ、ここで終わらなかった。
memUをVoiceChatBotに統合する段階で、Agent SDKとの競合が出た。
memU単体テスト(asyncio.run())では正常動作するのに、VoiceChatBot(Quart)の中でservice.retrieve()を呼ぶと500エラー。Codeが原因をたどってきた。起動時にasyncio.run()を呼んでたせいで新しいイベントループが作られて、Quartが起動すると別のイベントループになり、memUが持つ非同期オブジェクトが無効になる、らしい。起動時の初期化を削って、最初のリクエスト時にQuartのイベントループ内で初期化する形にCodeが直した。これで動いた。
でもさらにその先、Agent SDKのsystem_promptにメモリコンテキストを渡す段階でまた壁にぶつかった。日本語を含むsystem_promptを渡すとエラーが出る。claude_codeプリセットを使うと英語モードになる。何パターンも試して、どれもダメ。
memU自体の基本機能は動く。なのにAgent SDKと組み合わせた瞬間に問題が出続ける。今日はここで手を止めた。メモリ迷走は続く予想。