本当に記憶するAIエージェントの作り方:プロダクションLLMアプリのためのメモリアーキテクチャ完全ガイド
AIエージェントが、全部忘れました。
ユーザーが20分かけてコードベースの構造、デプロイの制約、チームのコーディングスタイルを説明してくれたんです。それなのにフォローアップの質問をしたら...エージェントが初対面みたいに答え始めた。コンテキストウィンドウが溢れたんですよね。47番目より前のメッセージは全部消えてます。ユーザーは最初からやり直し。これは辛い。
おもちゃのデモじゃなくて本気でLLMを使ったことがあるなら、この壁にぶつかったことがあるはずです。コンテキストウィンドウはメモリじゃないんです。 128Kトークンのウィンドウは大きそうに見えて、コードベースを1回スキャンしたら秒で埋まる。1Mトークンなら無限っぽいけど、毎回のリクエストでフル投入した時のAPIコストを計算したら目が覚めます。しかも最大のウィンドウを使っても永続性はゼロ。セッション閉じた瞬間、エージェントは全部忘れます。
2026年のプロダクションAIアプリで一番の課題がこれなんです:本当に記憶するエージェント、どう作る?
「次の5メッセージまで覚えてる」レベルの話じゃないです。セッションをまたいで記憶する。3週間前のユーザーの好みを覚えている。先週火曜にDBマイグレーションが失敗してワークアラウンドがまだ動いてることを把握している。人間の同僚みたいに。
このガイドでは、プロダクションチームが実際に使っているメモリアーキテクチャパターンを掘り下げます。シンプルなスライディングウィンドウから階層型メモリ、グラフベースの知識ストアまで。実装コードを見ながらMem0・LangChain Memory・Lettaを比較して、各パターンがどこで壊れるかも明らかにしていきましょう。
コンテキストウィンドウがメモリではない理由
解決策に入る前に、問題を正確に理解しましょう。
コンテキストウィンドウという錯覚
すべてのLLMにはコンテキストウィンドウがあります。1回のリクエストで処理できるトークンの最大数です。2026年にはかなり大きくなりました:
| モデル | コンテキストウィンドウ | 概算コスト(入力) |
|---|---|---|
| GPT-4.1 | 1Mトークン | ~$2.00/Mトークン |
| Claude Opus 4 | 200Kトークン | ~$5.00/Mトークン |
| Gemini 2.5 Pro | 1Mトークン | ~$1.25/Mトークン |
| Llama 4 Scout | 10Mトークン | セルフホスト |
「ウィンドウを大きくすればいいじゃん」と思いますよね。でも、それじゃダメな理由があるんです:
1. コストが線形(以上)にスケールする。 本当に必要な関連コンテキストが2Kトークンなのに、毎回500Kトークンを突っ込むのはお金を燃やしてるのと同じなんですよね。1。1日1,000件処理したら日額$1,000で、その99%はゴミコンテキストです。
2. ノイズが増えると性能が落ちる。 これは研究でも繰り返し示されていて、無関係なコンテキストが多いほどLLMの出力品質が下がるんです。いわゆる「干し草の中の針」問題。コンテキストが多い ≠ 良い回答、というのが現実です。
3. レイテンシがコンテキストサイズに比例する。 Time-to-First-Tokenが入力長に応じて増えます。500Kトークンの入力は5Kと比べて体感できるレベルで遅くなって、チャットアプリのリアルタイム感がパンクします。
4. セッション間の永続性がない。 コンテキストウィンドウは使い捨てなんです。タブ閉じたら全部消える。外部ストレージなしにセッション間で情報を引き継ぐ方法はありません。
5. 選択的に忘れられない。 人間は全部を覚えてるわけじゃなくて、重要なことだけ覚えてますよね。でもコンテキストウィンドウには重要度の概念がない。古いトークンからFIFOで捨てるだけで、クリティカルな仕様も雑談も区別なく消えます。
本物のメモリってどんなもの?
人間の記憶は単一バッファじゃないんです。階層構造になっています:
- ワーキングメモリ(即時コンテキスト):今考えてること。容量は小さいけどアクセスは爆速。
- 短期メモリ(最近の出来事):直近数分〜数時間の記憶。中程度の容量。
- 長期メモリ(永続的な知識):時間をかけて蓄積した事実やスキル。大容量だけど検索に時間がかかる。
効果的なAIエージェントのメモリは、この構造を真似るんです。順番に作っていきましょう。
パターン1:スライディングウィンドウ + スマートトランケーション
最もシンプルなメモリパターンです。基本的なチャットボットには十分なことが多いです。
仕組み
最新のN件のメッセージをコンテキストウィンドウに保持します。会話が制限を超えたら、最も古いメッセージから削除します。
interface Message { role: 'user' | 'assistant' | 'system'; content: string; timestamp: number; tokenCount: number; } class SlidingWindowMemory { private messages: Message[] = []; private maxTokens: number; private systemPrompt: Message; constructor(maxTokens: number, systemPrompt: string) { this.maxTokens = maxTokens; this.systemPrompt = { role: 'system', content: systemPrompt, timestamp: Date.now(), tokenCount: this.estimateTokens(systemPrompt), }; } addMessage(role: 'user' | 'assistant', content: string): void { this.messages.push({ role, content, timestamp: Date.now(), tokenCount: this.estimateTokens(content), }); this.trim(); } getContext(): Message[] { return [this.systemPrompt, ...this.messages]; } private trim(): void { let totalTokens = this.systemPrompt.tokenCount + this.messages.reduce((sum, m) => sum + m.tokenCount, 0); while (totalTokens > this.maxTokens && this.messages.length > 2) { const removed = this.messages.shift()!; totalTokens -= removed.tokenCount; } } private estimateTokens(text: string): number { return Math.ceil(text.length / 4); } }
使いどころ
- 履歴が重要でないシンプルなQ&Aチャットボット
- 短く集中した会話のカスタマーサポートボット
- プロトタイプやMVP
限界
- 重要な初期コンテキストが最初に消える。 ユーザーが最初のメッセージで要件を説明していた場合、それが真っ先になくなります。
- セッション間の永続性がない。 新しいセッションは毎回白紙から。
- 何を残すかの判断がない。 意味のない脱線も重要な仕様も同じ優先度で扱われます。
パターン2:会話の要約
インテリジェントなメモリへの第一歩です。古いコンテキストを捨てるのではなく圧縮します。
仕組み
会話が長くなりすぎたら、古い部分を要約して置き換えます。エージェントは[システムプロンプト] + [古いメッセージの要約] + [最近のメッセージ]で動作します。
class SummarizingMemory { private messages: Message[] = []; private summary: string = ''; private maxTokens: number; private recentWindowSize: number; private llm: LLMClient; constructor(maxTokens: number, recentWindowSize: number, llm: LLMClient) { this.maxTokens = maxTokens; this.recentWindowSize = recentWindowSize; this.llm = llm; } async addMessage(role: 'user' | 'assistant', content: string): Promise<void> { this.messages.push({ role, content, timestamp: Date.now(), tokenCount: this.estimateTokens(content), }); if (this.shouldSummarize()) { await this.compactHistory(); } } async getContext(): Promise<Message[]> { const context: Message[] = []; if (this.summary) { context.push({ role: 'system', content: `前回の会話要約:\n${this.summary}`, timestamp: 0, tokenCount: this.estimateTokens(this.summary), }); } context.push(...this.messages.slice(-this.recentWindowSize)); return context; } private shouldSummarize(): boolean { const totalTokens = this.messages.reduce((sum, m) => sum + m.tokenCount, 0); return totalTokens > this.maxTokens * 0.8; } private async compactHistory(): Promise<void> { const messagesToSummarize = this.messages.slice( 0, this.messages.length - this.recentWindowSize ); if (messagesToSummarize.length === 0) return; const conversationText = messagesToSummarize .map(m => `${m.role}: ${m.content}`) .join('\n'); const existingSummary = this.summary ? `既存の要約:\n${this.summary}\n\n` : ''; this.summary = await this.llm.complete({ prompt: `${existingSummary}統合する新しい会話:\n${conversationText}\n\n以下を保持する包括的な要約を作成してください:\n1. 重要な決定事項と結論\n2. ユーザーの好みと要件\n3. 言及された技術仕様\n4. 保留中のアクションアイテム\n5. 今後の参照に重要なコンテキスト\n\n簡潔に、しかし重要な詳細は失わないでください。`, maxTokens: 500, }); this.messages = this.messages.slice(-this.recentWindowSize); } private estimateTokens(text: string): number { return Math.ceil(text.length / 4); } }
段階的要約テクニック
フラットな要約を1つ作る代わりに、階層的に要約します:
class HierarchicalSummarizingMemory { private detailedSummary: string = ''; // 直近約30件の要約 private broadSummary: string = ''; // それ以前の全体要約 private recentMessages: Message[] = []; // 直近約10件 async compactHistory(): Promise<void> { // ステップ1: 既存の詳細要約を広域要約にマージ if (this.detailedSummary) { this.broadSummary = await this.llm.complete({ prompt: `既存の上位要約:\n${this.broadSummary}\n\n統合する詳細要約:\n${this.detailedSummary}\n\n最も重要な事実、決定、ユーザーの好みのみを保持する上位要約を作成してください。最大200語。`, maxTokens: 300, }); } // ステップ2: オーバーフローした最近のメッセージを詳細要約に圧縮 const overflow = this.recentMessages.slice(0, -10); this.detailedSummary = await this.llm.complete({ prompt: `会話セグメント:\n${overflow.map(m => `${m.role}: ${m.content}`).join('\n')}\n\n具体的な技術的詳細、言及されたコードスニペット、正確な要件を保持する詳細要約を作成してください。最大500語。`, maxTokens: 600, }); this.recentMessages = this.recentMessages.slice(-10); } async getContext(): Promise<string> { let ctx = ''; if (this.broadSummary) { ctx += `## 背景コンテキスト\n${this.broadSummary}\n\n`; } if (this.detailedSummary) { ctx += `## 最近のコンテキスト(詳細)\n${this.detailedSummary}\n\n`; } ctx += `## 現在の会話\n`; ctx += this.recentMessages.map(m => `${m.role}: ${m.content}`).join('\n'); return ctx; } }
使いどころ
- 長時間にわたるチャットアプリケーション
- 複雑でマルチターンの会話を伴うカスタマーサポート
- プロジェクトコンテキストが必要なコーディングアシスタント
限界
- 要約で細部が失われる。 要約には「ユーザーがREST APIを求めている」とあるが、HATEOAS準拠の応答やETagヘッダーといった具体的な要件は消えてしまいます。
- 要約の累積エラー。 要約の要約を繰り返すと、情報の喪失が蓄積します。5回の圧縮を経ると、元のニュアンスは失われます。
- 要約のコスト。 各圧縮にLLM呼び出しが必要で、レイテンシとコストが加算されます。
パターン3:エンティティ・ファクト抽出
会話全体を要約するのではなく、構造化された事実を抽出して保存するアプローチです。
仕組み
各インタラクションの後、キーエンティティと事実を永続ストアに抽出します。各応答の前に、現在のクエリに基づいて関連する事実を検索します。
interface Fact { id: string; subject: string; predicate: string; object: string; confidence: number; source: string; timestamp: number; supersedes?: string; } class EntityMemory { private facts: Map<string, Fact[]> = new Map(); private llm: LLMClient; private vectorStore: VectorStore; async extractFacts(messages: Message[]): Promise<Fact[]> { const conversation = messages .map(m => `${m.role}: ${m.content}`) .join('\n'); const response = await this.llm.complete({ prompt: `この会話からキーファクトを構造化データとして抽出してください。 会話: ${conversation} JSON配列で返してください: [{ "subject": "エンティティ名", "predicate": "関係または属性", "object": "値または関連エンティティ", "confidence": 0.0-1.0 }] 注目すべき項目: - ユーザーの好みと要件 - 技術的な決定事項 - プロジェクト仕様 - 締切と制約 - 言及された人物と役割`, responseFormat: 'json', }); return JSON.parse(response).map((f: any) => ({ ...f, id: crypto.randomUUID(), source: 'current_session', timestamp: Date.now(), })); } async storeFacts(facts: Fact[]): Promise<void> { for (const fact of facts) { const key = `${fact.subject}::${fact.predicate}`; const existing = this.facts.get(key) || []; const contradicting = existing.find( e => e.object !== fact.object && e.confidence < fact.confidence ); if (contradicting) { fact.supersedes = contradicting.id; } if (!this.facts.has(key)) { this.facts.set(key, []); } this.facts.get(key)!.push(fact); await this.vectorStore.upsert({ id: fact.id, text: `${fact.subject} ${fact.predicate} ${fact.object}`, metadata: fact, }); } } async recallRelevant(query: string, limit: number = 20): Promise<Fact[]> { const results = await this.vectorStore.search(query, limit); return results .map(r => r.metadata as Fact) .filter(f => !f.supersedes) .sort((a, b) => b.confidence - a.confidence); } async buildContext(query: string, recentMessages: Message[]): Promise<string> { const relevantFacts = await this.recallRelevant(query); let context = ''; if (relevantFacts.length > 0) { context += '## このユーザー/プロジェクトに関する既知の事実\n'; for (const fact of relevantFacts) { context += `- ${fact.subject} ${fact.predicate}: ${fact.object}\n`; } context += '\n'; } context += '## 現在の会話\n'; context += recentMessages.map(m => `${m.role}: ${m.content}`).join('\n'); return context; } }
使いどころ
- 時間をかけてユーザーを学習するパーソナルAIアシスタント
- 複数ミーティングにわたって決定事項を追跡するプロジェクト管理ボット
- 会話の流れより個別の事実が重要なアプリケーション
限界
- 抽出が完璧でない。 LLMがニュアンスを見逃したり、言及されていない事実を生成することがあります。
- 矛盾の処理が難しい。 「締切は金曜日」の後に「やっぱり月曜日に延ばしましょう」が来ると、システムが衝突を検出して解決する必要があります。
- 事実の陳腐化。 明示的な有効期限がなければ、古い事実がコンテキストを汚染します。ユーザーが2ヶ月前に好みのフレームワークを変えたのに、古い事実がまだ残っています。
パターン4:階層型メモリアーキテクチャ
プロダクションシステムが実際に使っている方式です。複数のパターンを組み合わせて、人間の記憶構造を模倣する階層システムを構築します。
3層モデル
┌─────────────────────────────────────────────┐
│ 第1層:ワーキングメモリ │
│ (現在のコンテキストウィンドウ、直近約10件) │
│ アクセス:即時 | 容量:小 │
├─────────────────────────────────────────────┤
│ 第2層:短期メモリ │
│ (セッション要約、最近の抽出事実) │
│ アクセス:高速検索 | 容量:中 │
├─────────────────────────────────────────────┤
│ 第3層:長期メモリ │
│ (知識グラフ、ユーザープロファイル、履歴) │
│ アクセス:セマンティック検索 | 容量:大 │
└─────────────────────────────────────────────┘
実装
class HierarchicalMemory { private workingMemory: Message[] = []; private shortTermMemory: ShortTermStore; private longTermMemory: LongTermStore; private llm: LLMClient; constructor(config: MemoryConfig) { this.shortTermMemory = new ShortTermStore(config.shortTermTTL); this.longTermMemory = new LongTermStore(config.vectorStore, config.graphDB); this.llm = config.llm; } async processMessage(message: Message): Promise<void> { // 1. ワーキングメモリに追加 this.workingMemory.push(message); // 2. 5回ごとに短期メモリへ事実を抽出 if (this.workingMemory.length % 5 === 0) { const recentFacts = await this.extractFacts( this.workingMemory.slice(-5) ); await this.shortTermMemory.store(recentFacts); } // 3. 20回ごとに重要な事実を長期メモリへ昇格 if (this.workingMemory.length % 20 === 0) { await this.consolidate(); } // 4. ワーキングメモリ超過時に圧縮 if (this.getWorkingMemoryTokens() > 8000) { await this.compactWorkingMemory(); } } async buildContext(query: string): Promise<ContextBundle> { // 3つの階層すべてから検索 const [shortTermResults, longTermResults] = await Promise.all([ this.shortTermMemory.search(query, 10), this.longTermMemory.search(query, 15), ]); // 重複排除と関連度ランキング const allFacts = this.deduplicateAndRank([ ...shortTermResults, ...longTermResults, ]); return { systemContext: this.buildSystemContext(allFacts), workingMemory: this.workingMemory.slice(-10), relevantFacts: allFacts.slice(0, 20), tokenBudget: { system: 2000, facts: 3000, working: 8000, response: 4000, }, }; } private async consolidate(): Promise<void> { const shortTermFacts = await this.shortTermMemory.getAll(); // LLMを使ってどの事実が長期保存に値するか判断 const assessment = await this.llm.complete({ prompt: `これらの事実を評価し、長期保存すべきものを判断してください。 事実: ${shortTermFacts.map(f => `- ${f.subject} ${f.predicate}: ${f.object} (信頼度: ${f.confidence})`).join('\n')} 各事実に対して: - KEEP: 今後のインタラクションに重要(ユーザーの好み、重要な決定、プロジェクト仕様) - DISCARD: 一時的または会話的(挨拶、確認、一過性の状態) - MERGE: 他の事実と結合可能 JSON配列で返してください: [{id, action, mergeWith?}]`, responseFormat: 'json', }); const actions = JSON.parse(assessment); for (const action of actions) { if (action.action === 'KEEP') { const fact = shortTermFacts.find(f => f.id === action.id); if (fact) { await this.longTermMemory.store(fact); } } } await this.shortTermMemory.prunePromoted( actions.filter((a: any) => a.action === 'KEEP').map((a: any) => a.id) ); } private async compactWorkingMemory(): Promise<void> { const overflow = this.workingMemory.slice(0, -10); const summary = await this.llm.complete({ prompt: `この会話セグメントを技術的詳細を保持しながら要約してください:\n${overflow.map(m => `${m.role}: ${m.content}`).join('\n')}`, maxTokens: 300, }); const facts = await this.extractFacts(overflow); await this.shortTermMemory.store(facts); this.workingMemory = [ { role: 'system', content: `[前回の会話要約: ${summary}]`, timestamp: Date.now(), tokenCount: this.estimateTokens(summary), }, ...this.workingMemory.slice(-10), ]; } }
使いどころ
- マルチセッション対話のプロダクションAIアシスタント
- 数週間にわたるプロジェクトコンテキストを記憶する必要がある企業コパイロット
- 長期的なユーザーパーソナライゼーションが重要なアプリケーション
パターン5:グラフベースメモリ(GraphRAG)
エージェントメモリの最先端です。事実をフラットなテキストとして保存するのではなく、知識を関係性のグラフとして表現します。
なぜグラフがベクトルに勝るのか
ベクトル類似度検索(従来のRAGの中核)には根本的な限界があります。似た響きのものは見つけますが、構造的に関連するものを見逃します。
例:「AliceがPaymentチームを管理している」と「PaymentチームがCheckoutマイクロサービスを所有している」は意味的には類似していません。しかしグラフでは、Alice → 管理 → Paymentチーム → 所有 → Checkoutサービスと辿れます。「Checkoutのバグについて誰に相談すべき?」と聞かれたとき、グラフなら「Alice」と答えられますが、ベクトルストアにはできません。
class GraphMemory { private graph: GraphDatabase; // Neo4j等 async addKnowledge( subject: string, predicate: string, object: string, metadata: Record<string, any> ): Promise<void> { await this.graph.query(` MERGE (s:Entity {name: $subject}) MERGE (o:Entity {name: $object}) MERGE (s)-[r:${predicate.toUpperCase().replace(/\s/g, '_')}]->(o) SET r += $metadata, r.updatedAt = timestamp() `, { subject, object, metadata }); } async query(question: string): Promise<GraphResult[]> { // ステップ1: 質問からエンティティを抽出 const entities = await this.extractEntities(question); // ステップ2: エンティティ周辺の関連サブグラフを検索 const subgraph = await this.graph.query(` MATCH (e:Entity)-[r*1..3]-(connected:Entity) WHERE e.name IN $entities RETURN e, r, connected LIMIT 50 `, { entities }); // ステップ3: サブグラフをコンテキストとしてフォーマット return this.formatSubgraph(subgraph); } async traverseForContext( startEntity: string, maxDepth: number = 3 ): Promise<string> { const result = await this.graph.query(` MATCH path = (start:Entity {name: $startEntity})-[*1..${maxDepth}]-(end:Entity) RETURN path ORDER BY length(path) LIMIT 30 `, { startEntity }); return result.paths .map(p => this.pathToSentence(p)) .join('\n'); } private pathToSentence(path: GraphPath): string { return path.segments .map(s => `${s.start.name} ${s.relationship.type.toLowerCase().replace(/_/g, ' ')} ${s.end.name}`) .join('、それが '); } }
ハイブリッドアプローチ:ベクトル + グラフ
最も効果的なプロダクションシステムは両方を組み合わせます:
class HybridMemory { private vectorStore: VectorStore; // セマンティック類似性用 private graphStore: GraphMemory; // 構造的関係用 private llm: LLMClient; async recall(query: string): Promise<MemoryResult> { const [vectorResults, graphResults] = await Promise.all([ this.vectorStore.search(query, 10), this.graphStore.query(query), ]); // マージと重複排除 const merged = this.mergeResults(vectorResults, graphResults); // LLMでリランキング const ranked = await this.llm.complete({ prompt: `クエリ: "${query}" 各メモリ項目の関連度を評価してください (0-10): ${merged.map((m, i) => `${i}: ${m.text}`).join('\n')} JSON形式で返してください: [{index, score, reason}]`, responseFormat: 'json', }); return { memories: this.applyRanking(merged, JSON.parse(ranked)), sources: { vector: vectorResults.length, graph: graphResults.length }, }; } }
フレームワーク比較:Mem0 vs LangChain Memory vs Letta
LLMアプリケーション向けの代表的なメモリフレームワーク3つを比較しましょう。
Mem0
Mem0はAIアプリケーション向けのマネージドメモリレイヤーで、マルチストアアーキテクチャ(KVストア + ベクトルストア + グラフレイヤー)を提供します。
import { MemoryClient } from 'mem0ai'; const memory = new MemoryClient({ apiKey: process.env.MEM0_API_KEY }); // 会話からメモリを追加 await memory.add( "I prefer Python over JavaScript for backend work", { user_id: "alice", metadata: { category: "preferences" } } ); // メモリを検索 const results = await memory.search( "What programming language does Alice prefer?", { user_id: "alice" } ); // 返却値: [{memory: "Prefers Python over JavaScript for backend", score: 0.95}] // ユーザーのすべてのメモリを取得 const allMemories = await memory.getAll({ user_id: "alice" });
強み:
- 超シンプルなAPI。add/search/getの3行で完結
- マネージドインフラ(ベクトルDBの設定不要)
- 自動重複排除と矛盾解決
- セッション間メモリがデフォルトで動作
- セルフホスト対応(mem0 OSS)
弱み:
- メモリ表現の制御が限定的
- ランキングアルゴリズムが不透明
- マネージド版はクラウド依存
- グラフレイヤーは新しく、ベクトルストアほど実戦テスト済みでない
LangChain Memory
LangChainは複数のメモリ実装をすぐ使える形で提供します:
import { BufferWindowMemory } from 'langchain/memory'; import { ConversationSummaryMemory } from 'langchain/memory'; import { VectorStoreRetrieverMemory } from 'langchain/memory'; import { CombinedMemory } from 'langchain/memory'; // オプション1: シンプルバッファ const bufferMemory = new BufferWindowMemory({ k: 10 }); // オプション2: 要約 const summaryMemory = new ConversationSummaryMemory({ llm: chatModel, returnMessages: true, }); // オプション3: ベクトルベースの検索 const vectorMemory = new VectorStoreRetrieverMemory({ vectorStoreRetriever: vectorStore.asRetriever(5), memoryKey: 'relevant_history', }); // オプション4: 複数のメモリタイプを組み合わせ const combinedMemory = new CombinedMemory({ memories: [bufferMemory, summaryMemory, vectorMemory], });
強み:
- 最大の柔軟性。メモリタイプを自由に組み合わせ可能
- LangChainエコシステム(エージェント、チェーン、ツール)との深い統合
- コミュニティ提供のストレージバックエンド(Redis、PostgreSQL、MongoDB)
- オープンソースでセルフホスト可能
- 豊富な文書と例
弱み:
- より多くのセットアップとインフラ決定が必要
- シンプルなユースケースにはオーバーエンジニアリングになりがち
- メモリタイプの組み合わせが常にクリーンとは限らない
- 広範なLangChainフレームワークに依存
Letta(旧MemGPT)
Lettaは根本的に異なるアプローチを取ります。メモリ管理をオペレーティングシステムの問題として扱うんです。
import { Letta } from 'letta'; const client = new Letta({ apiKey: process.env.LETTA_API_KEY }); // OS風のメモリ管理を持つエージェントを作成 const agent = await client.createAgent({ name: 'project-assistant', memory: { coreMemory: { // 常にコンテキスト内 — システムプロンプトのように persona: 'You are a senior software engineer...', human: '', // 会話から自動的に入力される }, archivalMemory: true, // 長期ベクトルストレージ recallMemory: true, // 会話履歴検索 }, model: 'gpt-4.1', tools: ['archival_memory_insert', 'archival_memory_search', 'core_memory_replace', 'core_memory_append'], }); // エージェントがツール呼び出しで自身のメモリを管理 const response = await agent.sendMessage( "I'm working on a Next.js project with PostgreSQL and Drizzle ORM" ); // エージェントが内部で以下を呼び出す: // core_memory_append(section="human", content="Works with Next.js, PostgreSQL, Drizzle ORM") // archival_memory_insert(content="User's current project stack: Next.js + PostgreSQL + Drizzle ORM")
強み:
- 自己管理型メモリ。エージェント自身が何を記憶するか判断
- OSに着想を得たアーキテクチャ(コア/アーカイブ/リコール)
- デフォルトで永続化。メモリがセッション間で保持される
- エージェントが何を保存・取得するか明示的に推論可能
- クラウドとセルフホストの両方に対応
弱み:
- メモリ管理のための追加LLM呼び出しが必要(コスト/レイテンシのオーバーヘッド)
- 固定的なアーキテクチャがすべてのユースケースに合うとは限らない
- LangChainと比較してエコシステムが若い
- コアメモリの更新が予測不可能になることがある
フレームワーク判断マトリクス
| 基準 | Mem0 | LangChain Memory | Letta |
|---|---|---|---|
| セットアップ複雑度 | ⭐ 低 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| 柔軟性 | ⭐⭐ 中 | ⭐⭐⭐ 高 | ⭐⭐ 中 |
| セッション間メモリ | ✅ 標準搭載 | ⚙️ 設定が必要 | ✅ 標準搭載 |
| 自己管理型 | ❌ | ❌ | ✅ |
| セルフホスト | ✅ | ✅ | ✅ |
| プロダクション準備度 | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
| 最適な用途 | 素早い統合 | カスタムアーキテクチャ | 自律型エージェント |
プロダクションのパターンとアンチパターン
パターン:メモリアウェアなプロンプトエンジニアリング
最もインパクトのある最適化は、メモリシステム自体ではなく、検索されたメモリをモデルにどう提示するかである場合が多いです。
// ❌ 悪い例: すべてのメモリをフラットテキストとしてダンプ const badPrompt = ` あなたが知っていること: ${memories.map(m => m.text).join('\n')} ユーザー: ${query} `; // ✅ 良い例: 構造化、優先順位付け、鮮度シグナル付き const goodPrompt = ` ## このユーザーについて知っていること ${highConfidenceMemories.map(m => `- ${m.text} (最終確認: ${formatRelativeTime(m.updatedAt)})` ).join('\n')} ## 関連プロジェクトコンテキスト ${projectMemories.map(m => `- ${m.text}`).join('\n')} ## 古い可能性あり(使用前にご確認ください) ${staleMemories.map(m => `- ${m.text} (${formatDate(m.createdAt)}時点、変更されている可能性あり)` ).join('\n')} ## 現在の会話 ${recentMessages.map(m => `${m.role}: ${m.content}`).join('\n')} `;
アンチパターン:忘却なきメモリ
記憶することと同じくらい重要なのが、いつ忘れるかを知ることです。
class MemoryManager { // 減衰の実装 — 一度も検索されないメモリは徐々に弱まる async applyDecay(): Promise<void> { const allMemories = await this.store.getAll(); for (const memory of allMemories) { const daysSinceAccess = (Date.now() - memory.lastAccessedAt) / (1000 * 60 * 60 * 24); // 減衰公式: アクセスされない期間に応じて信頼度を低下 const decayFactor = Math.exp(-0.01 * daysSinceAccess); const newConfidence = memory.confidence * decayFactor; if (newConfidence < 0.2) { await this.store.archive(memory.id); // 削除ではなくアーカイブ } else { await this.store.updateConfidence(memory.id, newConfidence); } } } // 矛盾検出の実装 async addWithContradictionCheck(newFact: Fact): Promise<void> { const existing = await this.store.search( `${newFact.subject} ${newFact.predicate}`, 5 ); const contradictions = existing.filter(e => e.subject === newFact.subject && e.predicate === newFact.predicate && e.object !== newFact.object ); if (contradictions.length > 0) { for (const old of contradictions) { await this.store.markSuperseded(old.id, newFact.id); } } await this.store.add(newFact); } }
アンチパターン:シンプルなケースへの過剰設計
すべてのアプリケーションに自動統合付きGraphRAGの3層メモリシステムが必要なわけではありません。この判断ツリーに従ってください:
1. 会話が20メッセージ以下?
→ スライディングウィンドウで十分です。ここで止めましょう。
2. ユーザーが後で同じ会話に戻る必要がある?
→ 会話要約を追加しましょう。ここで止めてもよいでしょう。
3. エージェントが異なる会話をまたいで事実を記憶する必要がある?
→ エンティティ抽出 + ベクトルストアを追加しましょう。
4. エージェントがエンティティ間の関係を理解する必要がある?
→ グラフベースメモリを追加しましょう。
5. エージェントが自律的にメモリを管理する必要がある?
→ Lettaの自己管理アプローチを検討しましょう。
メモリシステムのベンチマーク
メモリシステムが本当に機能しているかをどう確認するか?以下の指標を定義しましょう:
// テスト: エージェントはN件前のメッセージの事実を想起できるか? async function testRecallAccuracy( agent: Agent, testFacts: { fact: string; queryAfterNMessages: number }[] ): Promise<number> { let correct = 0; for (const test of testFacts) { await agent.processMessage({ role: 'user', content: test.fact }); for (let i = 0; i < test.queryAfterNMessages; i++) { await agent.processMessage({ role: 'user', content: `フィラーメッセージ ${i}: ${randomTopic()}について教えてください`, }); } const response = await agent.processMessage({ role: 'user', content: `${extractSubject(test.fact)}について何を伝えましたか?`, }); if (responseContainsFact(response, test.fact)) { correct++; } } return correct / testFacts.length; }
追跡すべき主要指標
| 指標 | 測定内容 | 目標 |
|---|---|---|
| Recall@N | N件のメッセージ後に事実を想起できるか? | >90%(N=50) |
| 矛盾率 | 古い情報を使ってしまう頻度 | <5% |
| メモリレイテンシ | 関連メモリの検索時間 | <200ms |
| トークン効率 | コンテキスト内の関連トークン対全トークン比 | >60% |
| セッション間想起 | 前のセッションの事実を記憶しているか? | >80% |
まとめ
記憶するAIエージェントを作るのは、最大のコンテキストウィンドウを探すことじゃないんです。自分のアプリで情報がどう流れるべきかを考えて、それに合ったメモリアーキテクチャを設計すること。これが本質です。
実践的なロードマップはこうなります:
-
シンプルに始める。 スライディングウィンドウ + 会話要約で80%のケースはカバーできます。初日からオーバーエンジニアリングする必要はないんです。
-
ユーザーが求めたら永続性を足す。 「さっき言ったこと忘れたの?」とユーザーが感じた瞬間、エンティティ抽出と永続ストアが必要になります。最速ルートはMem0ですね。
-
フラットなメモリで足りなくなったら構造を入れる。 組織図や依存関係グラフ、システムアーキテクチャなどエンティティ間の関係を理解する必要が出てきたら、グラフベースメモリの出番なんです。
-
問題が十分に複雑なら、エージェントに自己管理させる。 何時間もかかるタスクを自律的に実行するエージェントには、Lettaの自己管理アプローチがハードコードされたメモリルールの脆さを回避してくれます。
-
忘却は必ず実装する。 減衰のないメモリシステムは資産じゃなくて負債になるんですよね。古い事実は、事実がないことよりもダメージが大きい。
ツーリングはこの1年で本当に成熟しました。以前ならカスタムインフラが必要だったことが、今はpip install一発やAPIコール1回で済みます。もはや技術が難しいんじゃなくて、自分のユースケースに合うメモリアーキテクチャの設計が難しいんです。
ユーザーは完璧なメモリシステムに感謝してくれることはないでしょう。でもエージェントが忘れた瞬間、確実に気づきますよ。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう