BLOG.HIRO UENO.ART
← index
2025.12.27

メモリ迷走(前編)— 脱Graphiti Memory

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と組み合わせた瞬間に問題が出続ける。今日はここで手を止めた。メモリ迷走は続く予想。