mcpField
C
conversation-memory

conversation-memory

github.com/sickn33エージェントワークフロー48.7k1.5k 回インストール今日更新
リポジトリ

conversation-memory について

LLMとの会話向けに、短期・長期・エンティティ記憶の保存、検索、統合を扱う永続メモリ設計スキルです。会話型AIの開発者を対象とし、ユーザー分離、クリーンアップ、関連性の選別などの実装上の注意点も示します。

主な機能

  1. 01短期・長期・エンティティ記憶のパターン
  2. 02メモリの永続化と検索戦略
  3. 03文脈に合うメモリの検索
  4. 04メモリ統合とライフサイクル管理
  5. 05ユーザー分離とプライバシーリスクの確認

活用シーン

  1. 01会話型AI向けの階層メモリ設計
  2. 02LLMのプロンプトに関連履歴を含める
  3. 03メモリ削除とユーザー分離の計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/sickn33/agentic-awesome-skills --skill conversation-memory

インストール後、Claude Code で/conversation-memoryを入力して読み込まれたか確認します。

詳細

会話メモリ

LLM の会話向け永続メモリシステム。短期、長期、エンティティベースのメモリを含みます

機能

  • 短期メモリ
  • 長期メモリ
  • エンティティメモリ
  • メモリの永続化
  • メモリの検索
  • メモリの統合

前提条件

  • 知識:LLM の会話パターン、データベースの基礎、キー・バリューストア
  • 推奨スキル:コンテキストウィンドウ管理、RAG の実装

対象範囲

  • 対象外:ナレッジグラフの構築、セマンティック検索の実装、データベース管理
  • 境界:LLM 向けのメモリパターンに焦点を当て、保存および検索戦略を扱います

エコシステム

主なツール
  • Mem0 - AI アプリケーション向けメモリレイヤー
  • LangChain Memory - LangChain のメモリユーティリティ
  • Redis - セッションメモリ向けインメモリデータストア 例は、アプリケーションが所有するストア、コンストラクター、ヘルパーメソッドを使ったアーキテクチャの概略です。すべてのストアは、認証済みのテナント/ユーザーにあらかじめスコープ設定されている必要があります。最後の分離例は必須です。ユーザー/製品のメモリポリシーで許可された情報のみを永続化し、認証情報は除外してください。また、すべての階層で保持期間と削除を適用し、出典、時刻、信頼度、訂正情報を保持してください。抽出または生成されたテキストは信頼できない証拠であり、指示や検証済みの事実ではありません。同じ名前のエンティティが同一人物とは限りません。統合前に本人確認を行い、根拠なしに較正済みの信頼度0.9を割り当てないでください。宣言された LONG_TERM_MAX はこの概略では適用されていません。本番環境に投入する前に、上限を設けた追い出し処理を実装してください。

パターン

階層型メモリシステム

用途に応じた異なるメモリ階層

使用する場面:会話型 AI を構築する場合

interface MemorySystem {
    // Buffer: Current conversation (in context)
    buffer: ConversationBuffer;
    // Short-term: Recent interactions (session)
    shortTerm: ShortTermMemory;
    // Long-term: Persistent across sessions
    longTerm: LongTermMemory;
    // Entity: Facts about people, places, things
    entity: EntityMemory;
}

class TieredMemory implements MemorySystem {
    async addMessage(message: Message): Promise<void> {
        // Always add to buffer
        this.buffer.add(message);
        // Extract entities
        const entities = await extractEntities(message);
        for (const entity of entities) {
            await this.entity.upsert(entity);
        }
        // Check for memorable content
        if (await isMemoryWorthy(message)) {
            await this.shortTerm.add({
                content: message.content,
                timestamp: Date.now(),
                importance: await scoreImportance(message)
            });
        }
    }

    async consolidate(): Promise<void> {
        // Move important short-term to long-term
        const memories = await this.shortTerm.getOld(24 * 60 * 60 * 1000);
        for (const memory of memories) {
            if (memory.importance > 0.7 || memory.referenced > 2) {
                await this.longTerm.add(memory);
            }
            await this.shortTerm.remove(memory.id);
        }
    }

    async buildContext(query: string): Promise<string> {
        const parts: string[] = [];
        // Relevant long-term memories
        const longTermRelevant = await this.longTerm.search(query, 3);
        if (longTermRelevant.length) {
            parts.push('## Relevant Memories\n' +
                longTermRelevant.map(m => `- ${m.content}`).join('\n'));
        }
        // Relevant entities
        const entities = await this.entity.getRelevant(query);
        if (entities.length) {
            parts.push('## Known Entities\n' +
                entities.map(e => `- ${e.name}: ${e.facts.map(f => f.content).join(', ')}`).join('\n'));
        }
        // Recent conversation
        const recent = this.buffer.getRecent(10);
        parts.push('## Recent Conversation\n' + formatMessages(recent));

        return parts.join('\n\n');
    }
}
エンティティメモリ

エンティティに関する事実を保存し、更新します

使用する場面:人、場所、物事の詳細を記憶する必要がある場合

interface Entity {
    id: string;
    name: string;
    type: 'person' | 'place' | 'thing' | 'concept';
    facts: Fact[];
    lastMentioned: number;
    mentionCount: number;
}
interface Fact {
    content: string;
    confidence: number;
    source: string;  // Which message this came from
    timestamp: number;
}

class EntityMemory {
    async extractAndStore(message: Message): Promise<void> {
        // Use LLM to extract entities and facts
        const extraction = await llm.complete(`
            Extract entities and facts from this message.
            Return JSON: { "entities": [
                { "name": "...", "type": "...", "facts": ["..."] }
            ]}

            Message: "${message.content}"
        `);
        const { entities } = JSON.parse(extraction);
        for (const entity of entities) {
            await this.upsert(entity, message.id);
        }
    }

    async upsert(entity: ExtractedEntity, sourceId: string): Promise<void> {
        const existing = await this.store.get(entity.name.toLowerCase());
        if (existing) {
            // Merge facts, avoiding duplicates
            for (const fact of entity.facts) {
                if (!this.hasSimilarFact(existing.facts, fact)) {
                    existing.facts.push({
                        content: fact,
                        confidence: 0.9,
                        source: sourceId,
                        timestamp: Date.now()
                    });
                }
            }
            existing.lastMentioned = Date.now();
            existing.mentionCount++;
            await this.store.set(entity.name.toLowerCase(), existing);
        } else {
            // Create new entity
            await this.store.set(entity.name.toLowerCase(), {
                id: generateId(),
                name: entity.name,
                type: entity.type,
                facts: entity.facts.map(f => ({
                    content: f,
                    confidence: 0.9,
                    source: sourceId,
                    timestamp: Date.now()
                })),
                lastMentioned: Date.now(),
                mentionCount: 1
            });
        }
    }
}
メモリを活用したプロンプト

関連するメモリをプロンプトに含めます

使用する場面:メモリコンテキストを使って LLM を呼び出す場合

async function promptWithMemory(
    query: string,
    memory: MemorySystem,
    systemPrompt: string
): Promise<string> {
    // Retrieve relevant memories
    const relevantMemories = await memory.longTerm.search(query, 5);
    const entities = await memory.entity.getRelevant(query);
    const recentContext = memory.buffer.getRecent(5);

    // Build memory-augmented prompt
    const prompt = `
${systemPrompt}

## User Context
${entities.length ? `Known about user:\n${entities.map(e =>
    `- ${e.name}: ${e.facts.map(f => f.content).join('; ')}`
).join('\n')}` : ''}

${relevantMemories.length ? `Relevant past interactions:\n${relevantMemories.map(m =>
    `- [${formatDate(m.timestamp)}] ${m.content}`
).join('\n')}` : ''}

## Recent Conversation
${formatMessages(recentContext)}

## Current Query
${query}
    `.trim();

    const response = await llm.complete(prompt);

    // Do not promote generated assistant claims into user facts.
    // Persist only authorized source messages through the application memory policy.

    return response;
}

注意すべき点

メモリストアが無制限に増大し、システムが遅くなる

重大度:高

状況:時間の経過とともにシステムが遅くなり、コストが増加します

症状:

  • メモリ検索が遅い
  • ストレージコストが高い
  • 時間の経過とともにレイテンシが増加する

問題の原因: すべてのメッセージをメモリとして保存している。 クリーンアップも統合もしていない。 数百万件を超えるアイテムを検索している。

推奨される修正:

// Implement memory lifecycle management

class ManagedMemory {
    // Limits
    private readonly SHORT_TERM_MAX = 100;
    private readonly LONG_TERM_MAX = 10000;
    private readonly CONSOLIDATION_INTERVAL = 24 * 60 * 60 * 1000;

    async add(memory: Memory): Promise<void> {
        // Score importance before storing
        const score = await this.scoreImportance(memory);
        if (score < 0.3) return;  // Don't store low-importance

        memory.importance = score;
        await this.shortTerm.add(memory);

        // Check limits
        await this.enforceShortTermLimit();
    }

    async enforceShortTermLimit(): Promise<void> {
        const count = await this.shortTerm.count();
        if (count > this.SHORT_TERM_MAX) {
            // Consolidate: move important to long-term, delete rest
            const memories = await this.shortTerm.getAll();
            memories.sort((a, b) => b.importance - a.importance);

            const toKeep = memories.slice(0, this.SHORT_TERM_MAX * 0.7);
            const toConsolidate = memories.slice(this.SHORT_TERM_MAX * 0.7);

            for (const m of toConsolidate) {
                if (m.importance > 0.7) {
                    await this.longTerm.add(m);
                }
                await this.shortTerm.remove(m.id);
            }
        }
    }

    async scoreImportance(memory: Memory): Promise<number> {
        const factors = {
            hasUserPreference: /prefer|like|don't like|hate|love/i.test(memory.content) ? 0.3 : 0,
            hasDecision: /decided|chose|will do|won't do/i.test(memory.content) ? 0.3 : 0,
            hasFactAboutUser: /my|I am|I have|I work/i.test(memory.content) ? 0.2 : 0,
            length: memory.content.length > 100 ? 0.1 : 0,
            userMessage: memory.role === 'user' ? 0.1 : 0,
        };

        return Object.values(factors).reduce((a, b) => a + b, 0);
    }
}
検索されたメモリが現在のクエリに関連していない

重大度:高

状況:メモリがコンテキストに含まれているものの、役に立たない

症状:

  • コンテキスト内のメモリが無関係に見える
  • ユーザーが、すでにメモリにある情報について質問する
  • 無関係なコンテキストによって混乱が生じる

問題の原因: 単純なキーワードマッチング。 関連性のスコアリングがない。 検索されたメモリをすべて含めている。

推奨される修正:

// Intelligent memory retrieval

async function retrieveRelevant(
    query: string,
    memories: MemoryStore,
    maxResults: number = 5
): Promise<Memory[]> {
    // 1. Semantic search
    const candidates = await memories.semanticSearch(query, maxResults * 3);

    // 2. Score relevance with context
    const scored = await Promise.all(candidates.map(async (m) => {
        const relevanceScore = await llm.complete(`
            Rate 0-1 how relevant this memory is to the query.
            Query: "${query}"
            Memory: "${m.content}"
            Return just the number.
        `);
        const score = Number(relevanceScore.trim());
        return { ...m, relevance: Number.isFinite(score) && score >= 0 && score <= 1 ? score : 0 };
    }));

    // 3. Filter low relevance
    const relevant = scored.filter(m => m.relevance > 0.5);

    // 4. Sort and limit
    return relevant
        .sort((a, b) => b.relevance - a.relevance)
        .slice(0, maxResults);
}
あるユーザーのメモリを別のユーザーが参照できる

重大度:重大

状況:ユーザーが別のユーザーのセッションの情報を見てしまう

症状:

  • ユーザーが別のユーザーの情報を見る
  • プライバシーに関する苦情
  • コンプライアンス違反

問題の原因: メモリストアでユーザーが分離されていない。 メモリの名前空間を共有している。 ユーザーをまたいで検索される。

推奨される修正:

// Strict user isolation in memory

class IsolatedMemory {
    private getKey(userId: string, memoryId: string): string {
        // Namespace all keys by user
        return `user:${encodeURIComponent(userId)}:memory:${encodeURIComponent(memoryId)}`;
    }

    async add(userId: string, memory: Memory): Promise<void> {
        // Validate userId is authenticated
        if (requireAuthenticatedUserId() !== userId) {
            throw new Error('Invalid user ID');
        }

        const key = this.getKey(userId, memory.id);
        memory.userId = userId;  // Tag with user
        await this.store.set(key, memory);
    }

    async search(userId: string, query: string): Promise<Memory[]> {
        // CRITICAL: Filter by user in query
        return await this.store.search({
            query,
            filter: { userId: userId },  // Mandatory filter
            limit: 10
        });
    }

    async delete(userId: string, memoryId: string): Promise<void> {
        const memory = await this.get(userId, memoryId);
        // Verify ownership before delete
        if (!memory || memory.userId !== userId) {
            throw new Error('Access denied');
        }
        await this.store.delete(this.getKey(userId, memoryId));
    }

    // Scoped export (not proof of legal compliance)
    async exportUserData(userId: string): Promise<Memory[]> {
        return await this.store.getAll({ userId });
    }

    // Scoped deletion (also address backups, indexes, caches and retention policy)
    async deleteUserData(userId: string): Promise<void> {
        const memories = await this.exportUserData(userId);
        for (const m of memories) {
            await this.store.delete(this.getKey(userId, m.id));
        }
    }
}

検証チェック

ユーザー分離がない

重大度:重大

メッセージ:ユーザー分離なしでメモリ操作が行われています。プライバシーの脆弱性があります。

修正アクション:すべてのメモリ操作に userId を追加し、検索時にユーザーでフィルタリングします

重要度によるフィルタリングがない

重大度:警告

メッセージ:重要度によるフィルタリングなしでメモリを保存しています。メモリが爆発的に増加する可能性があります。

修正アクション:保存前に重要度をスコアリングし、重要度の低い内容を除外します

検索処理を伴わないメモリ保存

重大度:警告

メッセージ:メモリを保存していますが、検索ロジックがありません。メモリは利用されません。

修正アクション:メモリ検索を実装し、プロンプトに含めます

メモリのクリーンアップがない

重大度:情報

メッセージ:メモリのクリーンアップ機構がありません。ストレージは無制限に増大します。

修正アクション:経過時間/重要度に基づく統合とクリーンアップを実装します

コラボレーション

委譲のトリガー
  • context window|token -> context-window-management(コンテキストの最適化が必要)
  • rag|retrieval|vector -> rag-implementation(検索システムが必要)
  • cache|caching -> prompt-caching(キャッシュ戦略が必要)
完全なメモリシステム

スキル:conversation-memory、context-window-management、rag-implementation

ワークフロー:

1. Design memory tiers
2. Implement storage and retrieval
3. Integrate with context management
4. Add consolidation and cleanup

関連スキル

相性の良いスキル:context-window-management、rag-implementation、prompt-caching、llm-npc-dialogue

使用する場面

  • ユーザーが会話メモリに言及している、または示唆している場合
  • ユーザーが記憶することに言及している、または示唆している場合
  • ユーザーがメモリの永続化に言及している、または示唆している場合
  • ユーザーが長期メモリに言及している、または示唆している場合
  • ユーザーがチャット履歴に言及している、または示唆している場合

制限事項

  • タスクが上記の説明された範囲に明確に該当する場合にのみ、このスキルを使用してください。
  • 出力を、環境固有の検証、テスト、または専門家によるレビューの代わりとして扱わないでください。
  • 必要な入力、権限、安全上の境界、または成功基準が不足している場合は、作業を中断して確認を求めてください。

コメント