AIエージェントに「会話の文脈」ではなく「永続的な記憶」を持たせたいと考えたことはないでしょうか。
RAG(検索拡張生成、外部データを検索してLLMに渡す仕組み)やベクトルDBでの類似検索は定着してきました。
ただし「このユーザーは何をスキップしたか」「誰が誰をフォローしているか」のような関係性の記憶は、ベクトル検索だけでは扱いにくい領域です。
この記事では、オープンソースのIssue推薦エージェント「ContriGraph」の設計を題材に、グラフデータベースをエージェントの記憶装置として使う方法を整理します。
対象読者は、Claude等のAIエージェントにツール呼び出し(Tool Use)を組み込んでいる開発者や、ベクトルDB以外の記憶設計を検討しているバックエンドエンジニアです。
何が課題だったのか
ContriGraphが解決しようとした問題は、GitHubで「good first issue」を検索しても良い案件に辿り着けないことです。
検索結果は数万件にのぼり、更新が止まっているIssueや、メンテナーが8か月PRを放置しているリポジトリが大量に混ざります。
重要なのは、この問題の本質が「検索精度」ではなく「記憶の欠如」だと整理されている点です。
昨日スキップしたIssueを、今日も同じように提示されてしまう。
これはエージェントが過去のやり取りを状態として持っていないために起きます。
多くの推薦システムは「開発者とIssueのマッチング」を検索問題として扱います。
ContriGraphはこれをグラフ問題として捉え直しました。
開発者はスキルを持ち、スキルはIssueに紐づき、Issueはリポジトリに属し、リポジトリにはメンテナーがいます。
この関係構造をグラフデータベース「FalkorDB」上に構築し、Cypher(グラフ問い合わせ言語、Neo4jなどでも使われる)で辿る設計です。
アーキテクチャを段階的に見る
全体構成はシンプルです。
React+Viteのフロントエンドから、SSE(Server-Sent Events、サーバーからブラウザへ一方向にストリーミングする仕組み)でFastAPIバックエンドに接続します。
ユーザーの質問はClaudeエージェントに渡り、エージェントが「どのツールを呼ぶか」をTool Use(ツール使用、LLMが外部関数の呼び出しを自律的に判断する仕組み)のループで決定します。
ここで呼ばれるツールが find_matching_issues で、実体はFalkorDBに対するCypherクエリです。
結果はストリーミングでブラウザに返ります。
MCP(Model Context Protocol、LLMと外部ツール・データソースを標準規格で繋ぐ仕組み)を使ったエージェント構成に慣れている方なら、この構造はツールサーバーをMCP経由で公開する形に置き換えても成立する設計だとイメージできるはずです。
ContriGraph自体はMCPを使っていませんが、Tool Use自体はMCPが標準化しようとしている「LLMからのツール呼び出し」と同じ発想です。
グラフスキーマとクエリの中身
グラフのノード(頂点)は、Developer・Skill・Issue・Repository・Maintainerの5種類です。
エッジ(関係)には HAS_SKILL、CONTRIBUTED_TO、FOLLOWS、VIEWED、SKIPPED、APPLIED_TO などの型が付きます。
ここが設計のポイントです。
リレーショナルDBなら中間テーブルで表現する関係を、グラフDBでは型付きエッジとして直接表現します。
推薦ロジックの核心は、次のような1本のCypherクエリに集約されています。
MATCH (d:Developer {username: $username})-[:HAS_SKILL]->(s:Skill)
<-[:NEEDS_SKILL]-(i:Issue)<-[:HAS_ISSUE]-(r:Repository)
-[:MAINTAINED_BY]->(m:Maintainer)
WHERE m.avg_response_days <= $max_response_days
AND NOT (d)-[:SKIPPED]->(i)
AND NOT (d)-[:APPLIED_TO]->(i)
OPTIONAL MATCH (friend:Developer)-[:FOLLOWS]-(d)-[:FOLLOWS]-(friend),
(friend)-[:CONTRIBUTED_TO]->(r)
WITH i, r, m, count(DISTINCT friend) AS friend_contributors
RETURN i.title, i.url, i.complexity, r.full_name,
m.avg_response_days, (friend_contributors * 10) AS social_score
ORDER BY social_score DESC, m.avg_response_days ASC
LIMIT 10このクエリは開発者のスキルからIssueまで4ホップ(関係を4段階辿ること)を一度に処理します。
応答が遅いメンテナーのリポジトリを除外し、過去にスキップ・応募済みのIssueも除外し、さらに「フォローしている開発者が貢献しているリポジトリ」への社会的近さをスコアリングします。
これをリレーショナルDBで実装すると、4回のJOINとベクトル類似検索、そしてPythonでの後処理が必要になると説明されています。
FalkorDB上では1回のグラフ走査(1ms未満)で完結する点が、グラフDBを選ぶ技術的な根拠です。
エージェントの「記憶」はどう実装されているか
最も重要な設計は、エージェントの行動そのものをグラフの書き込みとして記録する点です。
Issueを提示した瞬間に、次のようなCypherでエッジを作成します。
MATCH (d:Developer {username: $username}), (i:Issue {issue_id: $issue_id})
CREATE (d)-[:VIEWED {timestamp: $ts, session_id: $session}]->(i)ユーザーがBookmark・Skip・Applyを押すと、同様に BOOKMARKED や SKIPPED のエッジが追加されます。
別テーブルでのログ管理も、Redisのようなセッションキャッシュも使いません。
次回のクエリ実行時、NOT (d)-[:SKIPPED]->(i) の条件がそのままフィルタとして機能します。
スキップしたIssueはセッションをまたいで、恒久的に除外され続けます。
これは「アプリがユーザーの履歴を覚えている」のではなく「グラフ構造自体に履歴が刻まれている」という発想の転換です。
LLM側に会話履歴を持たせる一般的な「メモリ」実装(会話をベクトル化して類似検索する方式など)とは異なり、記憶の実体がクエリのWHERE句そのものになっています。
既存の記憶設計との比較
AIエージェントの記憶の持たせ方には、大きく分けて3つの方向性があります。
| 方式 | 得意なこと | 弱点 |
|---|---|---|
| ベクトルDB(Pinecone等) | 意味的な類似検索 | 関係性・除外条件の表現が苦手 |
| RDB+キャッシュ(Redis等) | 既存基盤との親和性 | 多段階の関係はJOINが増え複雑化 |
| グラフDB(FalkorDB/Neo4j) | 多ホップの関係走査・除外条件 | 学習コスト・運用実績の蓄積が浅い |
Neo4jはグラフDBの代表格として知られていますが、FalkorDBはRedisをベースにしたグラフモジュールで、低レイテンシでの走査を売りにしています。
エージェントが「過去に何をしたか」「誰と誰が繋がっているか」を判断材料にする場合、ベクトル検索よりグラフ走査のほうが素直に表現できる場面があります。
自分のプロジェクトに当てはめて確認すること
実際に検討する際は、次の観点で自分のユースケースを棚卸ししてみてください。
- エージェントの判断材料に「除外条件」(見た・断った・使わなかった)が多く含まれるか
- データの関係が2〜3ホップ以上にまたがるか(単純なキーバリュー検索で十分なら不要)
- Tool Use機能を持つLLM(Claude、GPT系など)をすでにエージェント構成で使っているか
- FalkorDBやNeo4jのCypherクエリを書ける・学習できるメンバーがいるか
試しに動かす場合は、FalkorDBの公式DockerイメージをローカルでPull し、上記のようなCypherクエリをFalkorDB Browserや redis-cli 経由で実行してみると、JOINなしで多ホップが書ける感覚が掴めます。
既存のエージェントにツールとして組み込む場合は、Tool定義のJSON SchemaにCypherクエリをラップした関数を1つ追加するだけで試作が可能です。
まとめ
ContriGraphの設計から持ち帰れるのは、次の3点です。
- エージェントの「記憶」は、会話ログの保存ではなく、ユーザーの行動を型付きエッジとして書き込む設計でも実現できる
- 多ホップの関係走査や除外条件(スキップ済みを外す等)は、グラフDBのCypherクエリ1本に集約できる
- 自分のユースケースがJOIN地獄や後処理に悩んでいるなら、グラフDBへの置き換えを検討する価値がある
まずは手元のエージェントで「除外したい過去の行動」が何かを書き出し、それをエッジとして表現できないか考えてみると、設計の糸口が見つかるはずです。