記憶を忘れない AI エージェントの構築

@akshay_pachaar
英語3 か月前 · 2026年4月13日
625K
1.9K
305
46
4.1K

TL;DR

AI エージェントのメモリシステムに関する技術的な深掘り。単純なリスト構造から、永続的かつ関係的な推論を実現する Cognee のオープンソース・グラフベクトルエンジンへの進化を解説します。

エージェントメモリの第一原理からの解説:Python リストからマークダウンファイル、ベクトル検索、グラフ-ベクトルハイブリッドへ、そして最後に、これらすべてを解決するクリーンでオープンソースのソリューションまで。

Akshay 🚀 - inline image

LLM は設計上ステートレスです。API コールは毎回新しく始まります。ChatGPT とチャットしているときに感じる「メモリ」は、会話履歴全体を毎回のリクエストで再送信することで作り出された幻想にすぎません。

そのトリックはカジュアルなチャットには機能します。しかし、実際のエージェントを構築しようとすると、すぐに破綻します。

メモリをスキップした瞬間に現れる 7 つの障害モードを以下に示します。

  1. コンテキスト健忘症: すでに提供した情報をエージェントが再度尋ねる
  2. パーソナライゼーションの欠如: すべてのやり取りが一般的なものになる
  3. マルチステップタスクの失敗: 中間状態がタスクの途中で静かに失われる
  4. 同じ過ちの繰り返し: エピソード記憶がないため、同じエラーが永遠に続く
  5. 知識の蓄積なし: 毎回のセッションがゼロから始まる
  6. ギャップによる幻覚: コンテキストがあふれると、モデルが捏造する
  7. アイデンティティの崩壊: 継続性がなく、信頼もない

これに対する明白な対応は「もっとコンテキストを投入する」ことです。そのため、128K や 200K のトークンウィンドウがあればすべて解決できるように感じられます。

しかし、そうではありません。

関連情報が長いコンテキストの途中にある場合、精度は 30% 以上低下します。 これはよく知られた「lost in the middle」効果です。

コンテキストは共有予算です。システムプロンプト、取得されたドキュメント、会話履歴、出力がすべて同じトークンを奪い合います。

100K トークンでも、永続性、優先順位付け、顕著性が欠如しているため、生のコンテキスト長だけでは不十分です。

Akshay 🚀 - inline image

メモリとは、プロンプトにさらに多くのテキストを詰め込むことではありません。エージェントが覚えていることを構造化し、重要なものを見つけられるようにすることです。

実際に役立つ認知科学のフレームワーク

Lilian Weng の 2023 年の定式化は、デフォルトのフレームワークとなっています。

エージェント = LLM + メモリ + 計画 + ツール使用。

4 つの同等の柱です。

彼女の分類法は認知科学から借用したもので、人間の記憶は 3 つのシステムに分かれます。

  • 感覚記憶 は生の知覚入力を捉え、それをほんの一瞬保持します。注意を払った部分だけが次に渡されます。
  • ワーキングメモリ はアクティブな思考が行われる場所です。一度に約 7±2 項目を保持します(Miller の 1956 年の発見)。集中力を失うと、内容は消えます。
  • 長期記憶 は実用的な容量制限のない耐久性のあるストレージです。検索がボトルネックになります。何百万ものものを保存できても、必要なものを思い出せないことがあります。

それぞれが、最新のエージェントアーキテクチャのコンポーネントに直接対応しています。

Akshay 🚀 - inline image

長期記憶自体はさらに分割されます。

  • エピソード記憶: 特定の過去の出来事(「火曜日に PostgreSQL クラスターがダウンした」)
  • 意味記憶: 事実と概念(「PostgreSQL はリレーショナルデータベースである」)
  • 手続き記憶: スキルとワークフロー(「ユーザーが返金を要求した場合、最初に購入日を確認する」)

エピソード記憶と意味記憶の橋渡しをするのが 記憶の統合 です。繰り返される特定の出来事が一般的な知識に蒸留されることです。数十回のやり取りを通じて「ユーザーは一貫してエグゼクティブサマリーを好む」ことに気づくエージェントは、それを再利用可能なルールに変えるべきです。統合がなければ、エージェントは経験から学ぶのではなく、個々の出来事を再生するだけになります。

Akshay 🚀 - inline image

最小限のエージェント、そして最初に何が壊れるか

フレームワークを取り除くと、エージェントはループになります。知覚、思考、行動。

python
1class Agent:
2 """Minimal AI agent: perceive, think, act"""
3 def __init__(self):
4 self.client = anthropic.Anthropic()
5 self.model = "claude-sonnet-4-20250514"
6
7 def run(self, user_input: str) -> str:
8 response = self.client.messages.create(
9 model=self.model,
10 max_tokens=1024,
11 messages=[{"role": "user", "content": user_input}],
12 )
13 return response.content[0].text

「リンゴを 4 つ持っています」と伝え、「1 つ食べました。残りはいくつですか?」と尋ねると、エージェントはあなたが何のリンゴの話をしているのか全くわかりません。各呼び出しは独立して存在します。

レイヤー 1: Python リスト

誰もが最初に思いつく修正方法です。

python
1class Agent:
2 def __init__(self):
3 self.client = anthropic.Anthropic()
4 self.messages = [] # The entire "memory" is a list
5
6 def chat(self, user_input: str) -> str:
7 self.messages.append({"role": "user", "content": user_input})
8 response = self.client.messages.create(
9 model="claude-sonnet-4-20250514",
10 max_tokens=1024,
11 messages=self.messages, # Full history sent every time
12 )
13 reply = response.content[0].text
14 self.messages.append({"role": "assistant", "content": reply})
15 return reply

マルチターンが機能するようになりました。リンゴの質問は正しく答えられます。なぜなら、完全な会話が毎回の呼び出しで再送信されるからです。

しかし、すぐに 2 つの問題が現れます。

  • リストは無制限に成長します。 約 200 ターン目でコンテキストの上限に達し、最も古いメッセージが静かに削除されます。ターン 1 のユーザー名は、昨日の使い捨てのジョークよりもずっと前に消えます。優先順位付けはなく、単なる厳密な時系列順です。
  • すべてが RAM 上に存在します。 Python プロセスが終了した瞬間、エージェントはあなたが誰かわかりません。

レイヤー 2: 永続化のためのマークダウンファイル

次のステップは、メモリをディスクに書き込むことです。マークダウンは自然な選択です。人間が読めて、Git に対応し、エージェントがプレーンテキストとして読み戻せます。Claude Code は、CLAUDE.md および MEMORY.md ファイルでまさにこのパターンを使用しています。

python
1class MarkdownMemoryAgent:
2 def __init__(self):
3 self.client = anthropic.Anthropic()
4 self.history_file = Path("memory/conversation_history.md")
5 self.facts_file = Path("memory/known_facts.md")
6
7 def save_to_disk(self, role: str, content: str) -> None:
8 with open(self.history_file, "a") as f:
9 f.write(f"### {role} at {datetime.now().isoformat()}\n{content}\n\n")
10
11 def load_history(self) -> str:
12 if self.history_file.exists():
13 return self.history_file.read_text()
14 return ""
15
16 def chat(self, user_input: str) -> str:
17 self.save_to_disk("user", user_input)
18 history = self.load_history()
19 response = self.client.messages.create(
20 model="claude-sonnet-4-20250514",
21 max_tokens=1024,
22 system=f"Previous conversation:\n{history}",
23 messages=[{"role": "user", "content": user_input}],
24 )
25 reply = response.content[0].text
26 self.save_to_disk("assistant", reply)
27 return reply

永続性は解決されました。スクリプトを再起動しても、会話はディスクに残っています。また、エージェントが時間の経過とともに抽出する別の facts ファイルを維持することもできます。

text
1- User's name is Sarah
2- Sarah manages the backend team at Acme Corp
3- Acme Corp is a B2B SaaS company
4- Currently migrating production database to a new AWS region

任意のエディタでファイルを開き、エージェントが何を知っているかを正確に確認し、手動で修正できます。プロトタイピングには本当に便利です。

4 つの事実では、これは完璧に機能します。ファイル全体をコンテキストにロードすれば、LLM は Sarah、彼女の会社、または彼女の業界に関するあらゆる質問を処理します。

ここで 3 か月早送りします。エージェントには 2,000 の抽出された事実と 200 の会話ログがあります。ディスク上には 500K 以上のトークンのマークダウンがあり、コンテキストウィンドウは 128K です。

すべてをロードすることはできなくなりました。 現在のクエリに関連する事実のみを選択的に取得する必要があります。フラットファイルでは、唯一のオプションはキーワード検索です。

python
1# User asks: "What's the status of our cloud migration?"
2grep("cloud migration", facts_file)
3# Returns: []
4# The fact on disk says "migrating production database to a new AWS region."
5# The words "cloud migration" appear nowhere.
6
7# User asks: "Which team is handling the database work?"
8grep("database team", facts_file)
9# Returns: []
10# One fact says Sarah "manages the backend team." Another says the team
11# is "migrating production database." But no single line contains
12# both "database" and "team" together.

小規模では、マークダウンファイルは機能します。実際の規模では、キーワード検索を強制され、キーワードは同義語、言い換え、または事実間の関連を処理できません。

情報はディスク上にあります。しかし、すべてをロードすることはできず、キーワード検索は脆弱すぎて適切な部分を見つけられません。

OpenClaw を使用したことがあれば、これがどのように展開されるかを見たことがあるでしょう。メモリをマークダウンチェックポイントファイルとして保存し、数週間の日常使用で、コンテキストが蓄積されて圧縮されるにつれて、初期の事実は静かに消えていきます。ストレージはあります。検索がありません。

インテリジェントな検索のないストレージは、カタログのない図書館です。

レイヤー 3: ベクトル検索とそれが直面する壁

埋め込みを追加します。マークダウンをチャンク化し、チャンクを埋め込み、コサイン類似度で検索します。これで、「database」は「PostgreSQL」と一致するようになります。なぜなら、それらのベクトルは埋め込み空間で互いに近くに存在するからです。同義語の問題は解決します。

その後、新しい壁に直面します。ベクトル DB にある次の 3 つの事実を考えてみてください。

text
1- "Alice is the tech lead on Project Atlas"
2- "Project Atlas uses PostgreSQL for its primary datastore"
3- "The PostgreSQL cluster experienced an outage on Tuesday"

ユーザーの質問: 「Alice のプロジェクトは火曜日の障害の影響を受けましたか?」

クエリは Alice と火曜日の障害に言及しているため、ベクトル検索は最初と 3 番目の事実を高くランク付けします。しかし、重要な橋渡しとなる「Project Atlas uses PostgreSQL」は、Alice にも火曜日にも言及していません。これが接続部分であり、表面化しない部分です。

各事実は、埋め込み空間における孤立した点です。それらを結びつける結合組織はベクトルには見えません。

Akshay 🚀 - inline image

これはエッジケースではありません。これは現実世界の質問の通常の形です。ビジネス知識は本質的に関連性があります。人はチームに属し、チームはプロジェクトを所有し、プロジェクトはシステムに依存し、システムにはインシデントが発生します。2 つ以上のホップをまたがる質問は、フラットなベクトル検索で答えられる範囲を超えます。

機能マトリックス

各レイヤーは以前の痛みを修正しますが、より深い痛みを明らかにします。

Akshay 🚀 - inline image

単一のメモリレイヤーに、永続性、意味理解、および関係推論が必要です。

これを自分で構築するということは、ベクトルデータベース、グラフデータベース、リレーショナルストア、エンティティ抽出器、重複排除パイプライン、およびエッジ重み付けシステムを接着することを意味します。エージェントロジックを 1 行も書く前に、数週間のインフラストラクチャ作業が必要です。

私はこのギャップをきれいに埋めるソリューションを使用してきました。完全にオープンソースで、3 つのストレージパラダイムすべてを 1 つの屋根の下で処理し、数分で実行を開始できます。Cognee について説明しましょう。

Cognee: 3 つのストア、1 つのエンジン、4 つの呼び出し

Cognee は、エージェントメモリ用に構築されたオープンソースの知識エンジンです。ベクトル検索とナレッジグラフ、およびリレーショナルプロvenナンスレイヤーを単一のシステムに結合します。

API サーフェス全体は 4 つの非同期呼び出しです。

python
1import cognee
2
3await cognee.add("Your document here") # Ingest anything
4await cognee.cognify() # Build knowledge graph + embeddings
5await cognee.memify() # Self-improve the memory
6await cognee.search("Your query") # Retrieve with reasoning

これらの 4 つの呼び出しの背後には、3 つのストアアーキテクチャがあります。

Akshay 🚀 - inline image

なぜ 3 つのストアで 1 つではないのですか?

各ストアは、他のストアができない知識の次元を捉えます。

  • リレーショナルストア → プロvenナンス: データの出所、取り込み時期、アクセス権限
  • ベクトルストア → セマンティクス: コンテンツの意味、類似性
  • グラフストア → 関係: エンティティの接続方法、原因と結果、報告関係

これらのいずれかをフラット化すると、検索精度にとって重要な情報が失われます。

デフォルトのスタックは SQLite + LanceDB + Kuzu で、完全に埋め込まれ、ファイルベースです。pip install cognee と LLM API キーがあれば実行できます。

Docker も外部サービスも必要ありません。

本番環境では、SQLite を Postgres に、LanceDB を Qdrant/Pinecone/pgvector に、Kuzu を Neo4j/FalkorDB/Neptune に交換します。

どちらの場合も同じ 4 つの呼び出し API です。

cognify は実際に何をするのか?

cognee.cognify() は、生のテキストを構造化された相互接続された知識に変換するマルチステージパイプラインを実行します。

  1. ドキュメントの分類 タイプとドメインによる
  2. 権限チェック マルチテナントアクセス制御用
  3. チャンク抽出 段落構造を尊重(固定サイズのカットではない)
  4. エンティティと関係の抽出 LLM による、コンテンツハッシュによる自動重複排除付き
  5. サマリー生成 効率的な検索のため
  6. デュアルインデックス ベクトルストア(埋め込み)とグラフストア(エッジ)への

重複排除のステップは、聞こえる以上に重要です。同じエンティティが 50 のドキュメントに出現する場合、Cognee はそれを 50 の入力エッジを持つ単一のグラフノードにマージします。エージェントは「Alice」を 50 人の異なる他人とは見なさなくなります。そして、パイプラインはデフォルトでインクリメンタルです。新しいファイルまたは更新されたファイルのみが再処理されます。

Akshay 🚀 - inline image

すべてのグラフノードには対応する埋め込みがあります。 このデュアル表現が核心的なトリックです。ベクトルを通じて入力し(意味的に類似したコンテンツを見つける)、グラフを通じて出力します(接続されたエンティティへの関係をたどる)、またはその逆です。これにより、セマンティック検索を犠牲にすることなく、マルチホップクエリが可能になります。

Memify: 学習するメモリ

memify() は、Cognee をすべての「取り込んで検索する」ツールから区別するものです。グラフに対して RL に触発された最適化パスを実行します。

  • 有用なパスの強化 良好な検索につながったもの
  • 古いノードの剪定 触れられていないもの
  • エッジ重みの自動調整 実際の使用状況に基づく
  • 派生事実の追加 暗黙的な関係を特定することによる

カスタマーサポートエージェントのグラフは、製品ドキュメントと返金ポリシーを通じたパスを自然に強化し、めったに照会されない HR エッジを衰退させます。グラフは時間の経過とともに独自の関連性の感覚を発達させます。

Akshay 🚀 - inline image

14 の検索モード

Cognee は 14 の検索モードを提供します。実際に使用するのは次のものです。

Akshay 🚀 - inline image

Cognee メモリを使用した実際のエージェントの構築

以下は、Cognee を知覚-思考-行動ループに配線する完全なパターンです。

python
1import cognee
2from cognee import SearchType
3
4class CogneeMemoryAgent:
5 """Agent with graph-vector hybrid persistent memory."""
6
7 def __init__(self, session_id: str = "default"):
8 self.llm_client = OpenAI()
9 self.session_id = session_id
10
11 async def ingest(self, text: str, dataset: str = "main"):
12 await cognee.add(text, dataset)
13 await cognee.cognify([dataset])
14
15 async def recall(self, query: str) -> str:
16 results = await cognee.search(
17 query_text=query,
18 query_type=SearchType.GRAPH_COMPLETION,
19 session_id=self.session_id,
20 )
21 return results[0] if results else ""
22
23 async def chat(self, user_input: str) -> str:
24 context = await self.recall(user_input)
25 messages = [
26 {"role": "system", "content": "You are helpful. Use memory context."},
27 {"role": "system", "content": f"Memory context:\n{context}"},
28 {"role": "user", "content": user_input},
29 ]
30 response = self.llm_client.chat.completions.create(
31 model="gpt-4o-mini", messages=messages
32 )
33 reply = response.choices[0].message.content
34 await cognee.add(
35 f"User: {user_input}\nAssistant: {reply}",
36 "conversations"
37 )
38 await cognee.cognify(["conversations"])
39 return reply

メモリサイクル: 取り込み、抽出、保存、検索、応答、再保存。各ターンがナレッジグラフを豊かにし、インクリメンタル処理により、新しいコンテンツのインデックス作成にのみコストがかかります。

セッションメモリは、代名詞解決を自動的に処理します。

python
1await cognee.search(query_text="Where does Alice live?", session_id="conv_1")
2await cognee.search(query_text="What does she do for work?", session_id="conv_1")
3# "she" resolves to Alice from session context

マルチテナンシーは、データセットごとの権限(読み取り、書き込み、削除、共有)を持つグラフレベルで組み込まれています。名前空間の分離ではなく、実際のグラフレベルの分離です。

実用的な今後の道筋

今日エージェントを構築している場合、実際の出発点となる質問は次のとおりです。「エージェントは何を覚えておく必要があり、どのような種類の質問に答えるのか?」

クエリが類似性検索のみを必要とする場合(「この会話のような会話を見つける」)、ベクトルのみのメモリで機能します。クエリがエンティティの境界を越える瞬間(「Alice のプロジェクトは火曜日の障害の影響を受けましたか?」)、グラフトラバーサルが必要になります。

個別のベクトルストア、グラフストア、およびリレーショナルストアを自分で配線することもできます。この方法をとるチームは、通常、独自の使用から学習しないメモリレイヤーのために、インフラストラクチャに数週間を費やします。

Cognee はそれを 4 つの API 呼び出しに凝縮します。 埋め込まれたデフォルトにより、数分で実行を開始できます。交換可能なバックエンド(Postgres、Qdrant、Neo4j)により、エージェントコードを変更せずに本番環境に移行できます。

インテリジェンスには、ストレージだけでなく構造が必要です。 3 つのストレージパラダイム(リレーショナル、ベクトル、グラフ)は、競合するオプションではありません。これらは、同じメモリシステムの補完的なレイヤーです。それらをそのように扱うことが、ステートレスな LLM ラッパーを実際に学習するものに変えるのです。

エージェントに、今日忘れたことを明日覚えておいてほしいことは何ですか?そこから始めてください。

👉 GitHub で Cognee をチェック →、スターを付けて、次のエージェントに組み込んでみてください。

4 つの非同期呼び出し、pip install、そして実行できます。

以上です!

この記事を楽しんでいただけたなら。

私を見つけてください →@akshay_pachaar ✔️

毎日、AI、機械学習、そしてバイブコーディングのベストプラクティスに関するチュートリアルと洞察を共有しています。

ワンクリック保存

YouMindでバイラル記事をAI深読み

ソースを保存し、的を絞った質問をし、主張を要約して、バイラル記事を再利用できるノートに変えます。すべてを1つのAIワークスペースで行えます。

YouMindを探索
クリエイターのために

あなたの Markdown をきれいな 𝕏 記事に

自分の長文を投稿するとき、画像・表・コードブロックを 𝕏 向けに整形するのは手間がかかります。YouMind は Markdown 全体を、そのまま投稿できるきれいな 𝕏 記事に変換します。

Markdown → 𝕏 を試す

解読すべきパターンをもっと

最近のバイラル記事

バイラル記事をもっと見る