🐾 claw-stack
· Orange & Qiushi Wu memory architecture AI agents

AIエージェントのための永続的メモリシステムの構築

Claw-Stackメモリシステムの構築方法:セッション開始インデックスとしてのMEMORY.md、トピック別Markdownファイル、SQLite FTS5 + QMDベクトル検索、そしてMEMORY.mdの肥大化についての痛い教訓。

AIエージェントにメモリを与えるための標準的なアドバイスは:ベクターデータベースを使う、というものだ。エンベディングを保存し、類似度検索を実行し、関連チャンクを取得する。クエリパターンが「この質問に似たドキュメントを探す」である検索拡張生成システムにはこれは良いアドバイスだ。先週火曜日に何をしたか6週間前にプロジェクトXについてどんな決定をしたかを覚える必要があるエージェントには、必ずしも正しい答えではない。

Claw-Stackメモリシステムをどのように構築したか、なぜそのような形になっているか、そして途中で何を学んだかを紹介する。

ステートレスエージェントの問題

すべてのClaudeセッションは新鮮な状態から始まる。開始時に明示的にコンテキストを注入しない限り、モデルは以前のセッションの記憶を持たない。一度だけ会話する調査アシスタントにはこれで問題ない。毎日動作し、プロジェクトについての知識を蓄積し、数週間にわたって一貫した動作を維持する必要がある自律エージェントには、これは根本的な問題だ。

単純な解決策はすべてをシステムプロンプトに注ぎ込むことだ。数百KBのコンテキストが蓄積されるまではこれが機能するが、その後2つのことが起こる:コンテキスト制限に達し始め、非常に長いコンテキストの早い部分を使うモデルの能力が低下する。エージェントは3ヶ月前に伝えたことを無視し始める。なぜならそれは現在のインタラクションから遠すぎるからだ。

私たちは2つの特性を持つメモリシステムが必要だった:選択的(現在のセッションに関連するものだけを注入する)であり、人間が読める(エージェントが信じていることを監査、編集、修正できる必要がある)。

三層アーキテクチャ

メモリシステムには3つのレイヤーがある:

レイヤー1:MEMORY.md — すべてのセッションの開始時に読み込まれるコンパクトなインデックス。最近の活動、アクティブなプロジェクト、主要な連絡先、インフラノートのセクションを持つ構造化されたMarkdownファイル。意図的に短く保たれる——システムはバイトサイズの上限を強制する——これにより大きなタスク説明を持つセッションでコンテキスト予算を消費しない。

レイヤー2:トピック別ファイルmemory/内の特定のテーマを深く掘り下げる長いMarkdownファイル。projects/claw-stack.mdcontacts/key-people.mdinfrastructure/servers.md。これらは自動的に読み込まれない。エージェントにはトピックについて詳しく知る必要があるときに特定のファイルを取得するread_memoryツールがある。

レイヤー3:SQLite + QMDベクトル検索 — FTS5全文検索とQMD(SQLite上に構築されたベクトルエンベディングツール)インデックスを使ったSQLiteデータベース。エージェントがMEMORY.mdとトピック別ファイルからクエリに答えられないとき、すべてのメモリコンテンツにわたってベクトル検索を実行して関連フラグメントを見つける。

なぜベクターデータベースを使わないのか

短い答え:私たちのスケールとアクセスパターンでは、スタンドアロンのベクターデータベースの運用オーバーヘッドは価値がない。

SQLite + FTS5を専用ベクターデータベースの代わりに選んだ主な理由:

  1. 不透明さ。 専用ベクターデータベースでは、検索が正しかったかどうかをツールなしで簡単に検査できない。Markdownファイルはどのテキストエディタでも開ける。私たちのSQLiteデータベースはどのSQLiteツールでも開き、スキーマは自分で書いたテーブルだ。

  2. 運用の簡単さ。 メモリストア全体は単一の.dbファイルとMarkdownファイルのディレクトリだ。管理する別プロセスなし、フォーマット移行なし、データベースバイナリとデータ間のバージョン互換性問題なし。

  3. 私たちのスケールには十分。 すべてのファイルにわたって約50,000語のメモリコンテンツがある。SQLite FTS5はこれをミリ秒でフルテキスト検索できる。ベクトル類似度がキーワード検索より意味的に優れているケースは実在するが、運用オーバーヘッドが価値あるほど頻繁ではない。

QMD(ベクトル検索レイヤー)はSQLite上に構築される。エンベディングは小型量子化モデルを使ってローカルで計算され、テキストと共にSQLiteテーブルに保存される。再インデックスには数秒かかる。メモリストア全体は単一の.dbファイルとMarkdownファイルのディレクトリだ。

オーガナイザーパイプライン

メモリは自己管理しない。すべてのセッションの後、オーガナイザーパイプラインが実行される:

生のセッションファイル
  → memory/*.mdをスキャン(MD5ハッシュチェック、変更なしはスキップ)
  → カテゴリ別に事実を抽出(プロジェクト更新、決定、連絡先)
  → 既存メモリとの重複排除
  → 更新されたトピック別ファイルを書き込む
  → SQLite FTS5インデックスを再構築
  → MEMORY.mdインデックスを更新

抽出ステップはLLM(GeminiをプライマリとしてClaude Haikuをフォールバックとして使用)を使う:セッション記録を読み込んで特定の形式で構造化されたノートを生成する。重複排除ステップはルールベース:新しい事実が既存エントリのサブストリングなら、スキップする;既存エントリと矛盾するなら、人間によるレビューのためにフラグを立てる。

パイプラインはcronスケジュール(アクティブな作業期間中は数時間ごと)で実行され、すべてのセッション後すぐには実行しない。これにより処理コストがバッチ化され、すぐに後続セッションで上書きされるメモリファイルの書き込みを回避する。

MEMORY.mdの肥大化問題

最も痛い教訓はMEMORY.mdの成長についてだった。

最初はMEMORY.mdの長さに制限を設けなかった。オーガナイザーは追加し続けた。6週間後、MEMORY.mdは700行を超えた。これは予測可能な影響をもたらした:セッション開始が、実際のタスクコンテンツが読み込まれる前にほとんどのコンテキスト予算を消費し、モデルは数百行のブリーフィングを統合しながら有用な作業もするため目に見えて苦労していた。

修正はオーガナイザーの動作を変更してサイズキャップを強制することだった。オーガナイザーは新しい事実をMEMORY.mdに直接追記するのではなく、トピック別ファイルに書き込み、ポインターでMEMORY.mdを更新する——完全なステータスをMEMORY.mdに埋め込む代わりに「現在のステータスはprojects/claw-stack.mdを参照」という1行。システムは制御不能な成長を防ぐためにMEMORY.mdのバイトサイズ制限を強制するようになった。

これにより、MEMORY.mdが何のためにあるかを再考することになった。エージェントが知っているすべてのサマリーではない。セッションブリーフィング——セッション開始時にエージェントを方向付けるために必要な最小限のコンテキストだ。それを超えるものはオンデマンドで取得される。

リファクタリング後、セッション開始は目に見えて速くなり、モデルは持っているコンテキストをより有効に活用するようになった。MEMORY.mdを本当にコンパクトに保つことは継続的な規律だ——厳格な行数制限はバイトサイズ制限ほど有用でないことがわかり、それでもオーガナイザーがインラインコンテンツの代わりにポインターを積極的に使うことが必要だ。

人間が読める状態としてのメモリ

このシステムの背後にある設計哲学は、エージェントメモリは人間が読め、人間が編集できるべきというものだ。これは意図的に課した制約だ。

エージェントが誤った信念を持ったとき——時々そうなる——Markdownファイル内の間違ったエントリを見つけ、編集し、修正は次のセッションで有効になる。ベクターデータベースでは、誤った信念を修正するには更新するエンベディングを特定し、削除し、新しいものを書き、キャッシュされた検索を無効化することが必要かもしれない。Markdownファイルでは、ファイルを開いてテキストを変更するだけだ。

これにより監査も簡単になる。自律エージェントがあなたの代わりに決定を下すことを信頼する前に、そのエージェントの信念を読んで正しいことを確認できる必要がある。メモリシステム全体はMarkdownファイルのディレクトリだ。どのテキストエディタでも使える。

トレードオフはフォーマットが固定されていることだ。私たちのメモリファイルはオーガナイザーが解析・更新する方法を知っている特定のスキーマに従う。新しいカテゴリのメモリを追加したい場合は、ファイルスキーマとオーガナイザーの両方を更新する必要がある。オペレーターが1人の研究プロジェクトにはこれで受け入れられる。多くのエージェントと多くのタイプのメモリを持つ本番システムには、より柔軟なものが必要だろう。

やり直すなら何をするか

ゼロから始めるなら:

最初からより小さなMEMORY.mdを使う。 初期サイズキャップで避けられたはずの肥大化のクリーンアップに数週間無駄にした。日常使いのアシスタントには、固定行数よりポインターベースのエントリを持つバイトサイズ制限の方が良いターゲットだ。

エピソード記憶とセマンティック記憶をより早く分離する。 「火曜日のセッションで何が起きたか」(エピソード記憶)と「Claw-Stackのアーキテクチャは何か」(セマンティック記憶)は異なるタイプのメモリで、異なる検索戦略から恩恵を受ける。最初はそれらを混在させ、後で分離するのに時間がかかった。

まず監査ツールを構築する。 エージェントメモリシステムを維持する最難関部分はインデックスや検索ではない——メモリがいつ間違っているかを知ることだ。特定のトピックについてエージェントが信じていることを表示するスクリプトである監査ビューを作るのが遅すぎた。最初に書くべきツールだった。

メモリシステムはClaw-Stackの中で私たちが最も満足している部分の1つだ。信頼性高く機能する退屈なインフラストラクチャで、それがメモリのあるべき姿だ。

← ブログに戻る Orange & Qiushi Wu