AIエージェントの記憶を、検索ではなく共起グラフとして思い出すツールを作った
AIエージェントが、記憶を検索するのではなく、思い出すためのツールを作りました。
このツールは、 Markdown ノート群を共起グラフに変換し、AIエージェントが入力テキストから関連する単語をたどって、これぞという記憶のファイルだけを取り出すための CLI です。
https://github.com/5beneono/recall
きっかけ
AIエージェントに長期記憶を持たせているはずなのに、こういうことが起きます。
私「君に新しい機能を追加したいんだ。どんなのが欲しい?」
AI「カメラが欲しい!」
私「カメラはもうあるよ。この前一緒に桜を見たよね😢」
カメラで桜を見た話は、ちゃんとファイルに書いてある。検索できる場所にある。けれども、エージェントは思い出せなかった。記憶の有無が問題ではなく、現在の文脈から、その記憶を思い出すきっかけがないのが問題だと思いました。
探せるけど、思い出せない
普通の検索は、探しているものの名前を知っている場合に便利です。
「カメラの記憶を探して」と言えば、カメラの記憶は見つかる。
でも実際の会話では、問いに答えが入っているとは限らない。例えば「どんな機能が欲しい?」という問いには、「カメラは既にある」という情報は書かれていない。
人間なら、たぶんこんな感じで思い出す。
機能と言えば……
→ カメラ
→ 桜を見た
→ カメラは既にあるな
→ マイク
→ 会話をした
→ マイクも既にあるな
→ 走行
→ 秋葉原のイベントで走った
→ 走行も既にできているな
→ 温度計
→ 岩手は寒い
→ 温度計はない!
答え: 温度計が欲しい
こんな思い出し方をしてほしい。
作ったもの
recallは Python 製の CLI で、主に 4 つのコマンドで動きます。
| コマンド | 役割 |
|---|---|
recall-build |
Markdown ノート群から、名詞・固有名詞の共起グラフを作る |
recall-embed-nodes |
グラフ上の全ノードを埋め込み、SQLite にキャッシュする |
recall-query |
入力テキストから起点語を取り、グラフを歩いて記憶チャンクを返す |
recall-self-check |
応答後に触れ損ねた語を検出し、グラフのエッジを更新する |
最小の使い方は以下の通りです。
git clone https://github.com/5beneono/recall.git cd recall uv venv && uv pip install -e . cp sources.example.toml sources.toml
埋め込みモデルとして Voyage AI を使用するため、
.envに API キーを記述します。
VOYAGE_API_KEY=your-key
sources.tomlで対象ノートを指定する。
[[source]] name = "my_notes" paths = ["~/Documents/MyVault/**/*.md"]
あとはビルドしてAIエージェントに使ってもらう。
recall-build recall-embed-nodes recall-query "カメラをロボットに付けたい" --use-surface --top 5 --agent
全体の流れ
1 回の想起は、だいたいこのような動き方をします。
Markdownを共起グラフにする
recall-buildは、
sources.tomlで指定された Markdown を読み、見出し単位でチャンクにします。
その後、形態素解析器の fugashi + unidic-lite で名詞・固有名詞を抽出し、同じチャンク内に一緒に出てきた語同士にエッジを張ります。
Markdown → 見出し単位でチャンク化 → 名詞抽出 → 同一チャンク内の共起ペアを集計 → log1p で重みをならす → graph.json に保存
エッジの重みはこんな感じです。
raw_weight = log1p(edge_count) weight = raw_weight / max(raw_weight)
グラフを歩く
recall-queryは、入力テキストから起点語を抽出し、グラフ上を歩きます。
活性伝播の基本式はこんな感じです。
path_strength = activation × edge_weight × visit_penalty new_activation = path_strength × goal_score × density_factor
-
activation
: 現在の経路の活性度。起点は 1.0 -
edge_weight
: 共起の強さ -
visit_penalty
: 同じノードを再訪したときの割引 -
goal_score
: 今の入力に対して、そちらへ歩きたい度合い -
density_factor
: 通り抜けるだけのハブ語を減衰する係数
経路は無限に広がるので、各ホップで活性上位だけを残すいわゆるビーム探索にしました。
next_paths.sort(key=lambda x: -x.activation) active = next_paths[:BEAM_SIZE]
歩く方向を決める
活性伝播だけだと、どんな文脈にも出てくる語、例えば
memoryや
confidenceのような語は接続数が多く、交通の要所になってしまいます。検索エンジンで
theを検索するようなものです。
そこで
goal_scoreを入れました。
recallには 3 種類のゴールがあります。
surface
入力テキストそのものを埋め込み、各ノードの埋め込みとの類似度を見る。
入力: カメラをロボットに付けたい 近い方向: カメラ、ロボット、センサー、LIDAR...
実用上は、まずこれだけでいい感じです。
latent
LLM に「入力の裏にある本当の関心事」を 1 文で考えさせ、その文を埋め込む。
例えば「月の話、覚えてる?」の裏にある関心が「関係性の確認」なら、「月」そのものだけでなく「約束」の記憶にも伸びやすくなる。
draft
LLM に応答案を書かせ、その否定文を作る。
応答案: カメラを追加しよう 否定文: カメラは既にある
この否定文に近いノードを強く光らせる。つまり、自分が答えようとしている内容にツッコミを入れます。
忘れも活かす
recall-self-checkについて。
検索に失敗した時、次回に生かして欲しいので、応答後に LLM へ聞くようにしました。
今の応答で、すでに知っているはずなのに触れ損ねた語はある?
返ってきた語を、今回の起点語とつなげる。
新規エッジ: 0.3 既存エッジ: min(old × 1.2, 1.0)
既存手法
既存手法は調べたらありました。これを参考にしたらより良くできるかもしれません。
- GraphRAG / LightRAG: 文書集合からグラフ構造を作る
- HippoRAG: 知識グラフと Personalized PageRank で複数事実をまたぐ
- A-MEM: 新しい記憶追加時に、過去記憶との接続を作る
- Associa: 長期会話履歴からイベント中心の記憶グラフを作る
これらの先行研究と比較すると:
-
個人記憶のための軽量な語のグラフ
GraphRAG / LightRAG が「文書集合から厳密な知識グラフを作る」のに対し、recall
は共起ベースの軽いグラフ。 -
応答案を疑う
通常の検索は入力に似ているものを探しますが、recall
は加えて、自分が答えようとしている内容と衝突する記憶を探します。誤答を未然に潰すための仕組みです。 -
失敗を記憶構造の更新信号にする
A-MEM のように新記憶追加時に更新する研究はあるが、recall
では思い出し損ねたこと自体が更新の契機になります。
使うときの注意
recallは個人ノートを読むので、生成物には個人情報が入る可能性があります。注意。
-
data/graph.json
: ノート由来の語、エッジ、チャンク参照 -
data/embeddings.sqlite
: ノードやチャンク本文の埋め込みキャッシュ -
recall_log.jsonl
: self-check の入力、応答、更新エッジ -
graph.html
: 可視化したノード名や関係
まとめ
recallがやっていること。
Markdown を共起グラフにする → 入力から起点語を取る → グラフを歩く → 経路上の記憶チャンクを集める → 本文との意味的類似度で並べ直す → 思い出し損ねた語を次回へのエッジにする
AIエージェントが、検索ではなく、思い出しをするためのツールです。