CLAUDE LABEN
FORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられていますFORK — Claude Code 2.1.212で/forkの挙動が変わりました。会話を新しいバックグラウンドセッションへ複製し、作業を続けたまま並走できます。従来のセッション内サブエージェントは/subtaskに移りましたLIMITS — WebSearchの呼び出しがセッション単位で既定200回に制限されました。サブエージェントの起動も既定200回が上限で、暴走した検索・委譲のループを止められますMCPBG — 2分を超えるMCPツール呼び出しは自動的にバックグラウンドへ移り、セッションが固まらなくなりました。しきい値はCLAUDE_CODE_MCP_AUTO_BACKGROUND_MSで調整できますPLANFIX — プランモードがtouchやrmといったファイルを変更するBashコマンドを、許可プロンプトもcanUseToolコールバックも通さずに実行してしまう不具合が修正されましたSONNET5 — Claude Sonnet 5は導入価格として入力100万トークンあたり2ドル、出力10ドルで提供中です。8月31日を過ぎると3ドルと15ドルに戻りますIPO — Anthropicが早ければ10月の株式公開を視野に、引受銀行が投資家との面談を組み始めたと報じられています
記事一覧/API & SDK
API & SDK/2026-03-29上級

Claude 長期記憶 MCP — パーソナライズドAIの本番実装

MCP を活用した長期記憶システムの本番実装を、ベクトルDBの実運用メトリクス、規模別の DB 選定、Embedding モデル更新の落とし穴まで踏み込んで解説。

長期記憶2MCP45パーソナライゼーション2ベクトルDBClaude API115メモリ管理

プレミアム記事

「会話の続きから話したい」というのは、Claude を本気でアプリに組み込もうとした時に最初にぶつかる壁です。私自身、2014 年から iOS / Android のアプリ開発を続けてきた中で、累計 5,000 万ダウンロードに達したアプリ群でも「ユーザーの好みを覚えておけない」ことが体験品質の足かせになっていました。MCP(Model Context Protocol)を使った長期記憶は、その制約をようやく取り払える設計だと感じています。

ただし運用に入ると、公式ドキュメントには書かれていない落とし穴がいくつもあります。以下では、ベクトル DB の実運用で見えてきたメトリクス、規模別の DB 選定、そして Stripe 課金を持つ 4 サイト(Claude Lab・Gemini Lab・Antigravity Lab・Rork Lab)を並行運営している立場から見えた、長期記憶を「ユーザー体験」として設計する判断軸を整理していきます。

長期記憶の仕組み

MCP による統合アーキテクチャ

MCP は Claude と外部システムをやり取りするための標準プロトコルです。長期記憶を実装する場合、以下の層を統合します。

┌─────────────────────────────────────────┐
│        Claude API                       │
│   (Message + Context Window)            │
└──────────────────┬──────────────────────┘
                   │
        ┌──────────▼──────────┐
        │   MCP Protocol      │
        │  (Tool Definitions) │
        └──────────┬──────────┘
                   │
    ┌──────────────┼──────────────┐
    │              │              │
┌───▼───┐  ┌──────▼──────┐ ┌────▼─────┐
│ Memory│  │ Vector DB   │ │ User DB  │
│ Store │  │ (Pinecone)  │ │(PostgreSQL)
└───────┘  └─────────────┘ └──────────┘

Claude API層: メッセージ送信とトークン管理 MCP層: ツール定義、メモリ操作の仕様 永続化層: ベクトルDB、ユーザーメタデータ、監査ログ

メモリの全ライフサイクル

// lib/memory-lifecycle.ts
interface MemoryLifecycle {
  create: (userId: string, memory: MemoryEntry) => Promise<void>;
  retrieve: (userId: string, query: string) => Promise<MemoryEntry[]>;
  update: (memoryId: string, newContent: string) => Promise<void>;
  delete: (memoryId: string) => Promise<void>;
  expire: (memoryId: string) => Promise<void>;
}
 
class MemoryManager implements MemoryLifecycle {
  private vectorDb: VectorDatabase;
  private userDb: UserDatabase;
  private cache: CacheService;
 
  async create(userId: string, memory: MemoryEntry): Promise<void> {
    // 1. テキストをベクトル化
    const embedding = await this.embedText(memory.content);
 
    // 2. ベクトルDB に保存
    const vectorId = await this.vectorDb.insert({
      userId,
      embedding,
      metadata: {
        createdAt: new Date(),
        importance: memory.importance,
        category: memory.category,
        expiresAt: this.calculateExpiration(memory.ttl),
      },
    });
 
    // 3. ユーザーDB に参照を保存
    await this.userDb.insertMemoryRef({
      userId,
      vectorId,
      originalContent: memory.content,
      category: memory.category,
      createdAt: new Date(),
    });
 
    // 4. キャッシュを無効化
    this.cache.invalidate(`memories:${userId}`);
  }
 
  async retrieve(userId: string, query: string): Promise<MemoryEntry[]> {
    // 1. キャッシュを確認
    const cached = this.cache.get(`memories:${userId}:${query}`);
    if (cached) return cached;
 
    // 2. クエリをベクトル化
    const queryEmbedding = await this.embedText(query);
 
    // 3. ベクトルDB で類似度検索(top-k=5)
    const matches = await this.vectorDb.search({
      embedding: queryEmbedding,
      userId,
      limit: 5,
      filter: {
        // 期限切れメモリを除外
        expiresAt: { $gt: new Date() },
      },
    });
 
    // 4. スコア順にメモリを返す
    const memories = matches.map((m) => ({
      id: m.id,
      content: this.decryptContent(m.metadata.content),
      importance: m.metadata.importance,
      relevanceScore: m.similarity,
      category: m.metadata.category,
    }));
 
    // 5. キャッシュに保存(TTL: 5分)
    this.cache.set(`memories:${userId}:${query}`, memories, 5 * 60);
 
    return memories;
  }
 
  private calculateExpiration(ttl?: number): Date {
    // デフォルト: 30日間のTTL
    const days = ttl || 30;
    const expiry = new Date();
    expiry.setDate(expiry.getDate() + days);
    return expiry;
  }
}

MCP ツール定義

メモリ操作ツールの実装

Claude が長期記憶にアクセスするため、以下の MCP ツールを定義します。

// mcp-tools/memory-tools.ts
const memoryTools = {
  // 重要な会話内容をメモリに保存
  save_memory: {
    name: "save_memory",
    description:
      "Save important information to long-term memory for future reference",
    inputSchema: {
      type: "object",
      properties: {
        content: {
          type: "string",
          description: "The memory content (up to 1000 chars)",
        },
        category: {
          type: "string",
          enum: [
            "user_preference",
            "project_context",
            "skill_profile",
            "past_conversation",
            "decision_log",
          ],
          description: "Memory category",
        },
        importance: {
          type: "number",
          enum: [1, 2, 3, 4, 5],
          description: "Importance level (1=low, 5=critical)",
        },
        ttl_days: {
          type: "number",
          description: "Time to live in days (default: 30)",
        },
      },
      required: ["content", "category", "importance"],
    },
  },
 
  // メモリを検索して内容を取得
  retrieve_memory: {
    name: "retrieve_memory",
    description:
      "Retrieve relevant memories based on a query or topic",
    inputSchema: {
      type: "object",
      properties: {
        query: {
          type: "string",
          description: "Search query for memories",
        },
        category: {
          type: "string",
          description: "Optional filter by category",
        },
        limit: {
          type: "number",
          description: "Maximum number of results (default: 5)",
        },
      },
      required: ["query"],
    },
  },
 
  // メモリを更新
  update_memory: {
    name: "update_memory",
    description: "Update an existing memory entry",
    inputSchema: {
      type: "object",
      properties: {
        memory_id: {
          type: "string",
          description: "ID of the memory to update",
        },
        content: {
          type: "string",
          description: "New content",
        },
        importance: {
          type: "number",
          enum: [1, 2, 3, 4, 5],
          description: "New importance level",
        },
      },
      required: ["memory_id", "content"],
    },
  },
 
  // メモリを削除
  delete_memory: {
    name: "delete_memory",
    description: "Delete a memory entry",
    inputSchema: {
      type: "object",
      properties: {
        memory_id: {
          type: "string",
          description: "ID of the memory to delete",
        },
      },
      required: ["memory_id"],
    },
  },
 
  // メモリサマリーを取得
  get_memory_summary: {
    name: "get_memory_summary",
    description: "Get a concise summary of all memories for this user",
    inputSchema: {
      type: "object",
      properties: {
        category: {
          type: "string",
          description: "Filter by category",
        },
      },
    },
  },
};

MCP ハンドラーの実装

// mcp-handlers/memory-handler.ts
import { Tool } from "@anthropic-ai/sdk/resources/messages";
 
class MemoryMCPHandler {
  private memoryManager: MemoryManager;
  private auditLog: AuditLogger;
 
  async handleToolCall(
    userId: string,
    toolName: string,
    toolInput: Record<string, any>
  ): Promise<string> {
    switch (toolName) {
      case "save_memory":
        return this.handleSaveMemory(userId, toolInput);
 
      case "retrieve_memory":
        return this.handleRetrieveMemory(userId, toolInput);
 
      case "update_memory":
        return this.handleUpdateMemory(userId, toolInput);
 
      case "delete_memory":
        return this.handleDeleteMemory(userId, toolInput);
 
      case "get_memory_summary":
        return this.handleGetSummary(userId, toolInput);
 
      default:
        throw new Error(`Unknown tool: ${toolName}`);
    }
  }
 
  private async handleSaveMemory(
    userId: string,
    input: Record<string, any>
  ): Promise<string> {
    const { content, category, importance, ttl_days } = input;
 
    // バリデーション
    if (!content || content.length > 1000) {
      throw new Error("Content must be 1-1000 characters");
    }
 
    if (!["user_preference", "project_context", "skill_profile",
      "past_conversation", "decision_log"].includes(category)) {
      throw new Error("Invalid category");
    }
 
    // メモリを保存
    const memoryId = await this.memoryManager.create(userId, {
      content,
      category,
      importance,
      ttl: ttl_days,
    });
 
    // 監査ログ
    await this.auditLog.log({
      userId,
      action: "save_memory",
      memoryId,
      category,
      importance,
      timestamp: new Date(),
    });
 
    return JSON.stringify({
      success: true,
      memoryId,
      message: `Memory saved with importance level ${importance}`,
    });
  }
 
  private async handleRetrieveMemory(
    userId: string,
    input: Record<string, any>
  ): Promise<string> {
    const { query, category, limit = 5 } = input;
 
    const memories = await this.memoryManager.retrieve(
      userId,
      query,
      category,
      limit
    );
 
    // 監査ログ(検索は記録するが詳細は記録しない)
    await this.auditLog.log({
      userId,
      action: "retrieve_memory",
      resultCount: memories.length,
      timestamp: new Date(),
    });
 
    return JSON.stringify({
      count: memories.length,
      memories: memories.map((m) => ({
        id: m.id,
        content: m.content,
        category: m.category,
        importance: m.importance,
        relevanceScore: m.relevanceScore,
      })),
    });
  }
 
  private async handleUpdateMemory(
    userId: string,
    input: Record<string, any>
  ): Promise<string> {
    const { memory_id, content, importance } = input;
 
    // 権限確認
    const owner = await this.memoryManager.getMemoryOwner(memory_id);
    if (owner !== userId) {
      throw new Error("Unauthorized");
    }
 
    await this.memoryManager.update(memory_id, { content, importance });
 
    await this.auditLog.log({
      userId,
      action: "update_memory",
      memoryId: memory_id,
      timestamp: new Date(),
    });
 
    return JSON.stringify({ success: true, message: "Memory updated" });
  }
 
  private async handleDeleteMemory(
    userId: string,
    input: Record<string, any>
  ): Promise<string> {
    const { memory_id } = input;
 
    const owner = await this.memoryManager.getMemoryOwner(memory_id);
    if (owner !== userId) {
      throw new Error("Unauthorized");
    }
 
    await this.memoryManager.delete(memory_id);
 
    await this.auditLog.log({
      userId,
      action: "delete_memory",
      memoryId: memory_id,
      timestamp: new Date(),
    });
 
    return JSON.stringify({ success: true, message: "Memory deleted" });
  }
 
  private async handleGetSummary(
    userId: string,
    input: Record<string, any>
  ): Promise<string> {
    const { category } = input;
 
    const summary = await this.memoryManager.getSummary(userId, category);
 
    return JSON.stringify({
      totalMemories: summary.totalCount,
      byCategory: summary.byCategory,
      summary: summary.contentSummary,
      lastUpdated: summary.lastUpdated,
    });
  }
}

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
Embedding モデル更新で上位 5 件の 23% が入れ替わる現象と、新旧モデル並行運用 14 日ルールを実装コード付きで提示
1,000 MAU / 50,000 MAU / 50,000+ MAU の規模別ベクトル DB 選定基準と、月額料金・採用判断のサインを実体験ベースで整理
Embedding API 料金(1,000 MAU で月 $1.5)、recency boost によるヒット率改善 28%、レイテンシ p50/p95 の本番計測値を全て数値で開示
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API & SDK2026-07-08
MCP コネクタを申請・無人運用に回す前に、各ツールを契約テストで確かめる
手元で動いた MCP コネクタをそのまま無人ジョブに繋ぐと、応答形状の誤読や書き込みの二重発火で静かに壊れます。ツール記述・応答契約・冪等性・レイテンシを機械検証する小さなハーネスの作り方を、実測値とともにまとめました。
API & SDK2026-06-30
ツール出力が大きすぎてコンテキストを溶かす問題 — カーソルで小分けに返すページング設計
一覧系のツールが数百件をそのまま返すと、エージェントのコンテキストは一回の呼び出しで溶けます。カーソルベースのページングでツール出力を小分けに返し、トークン予算を守る設計を実装コード付きで解説します。
API & SDK2026-06-28
接続が切れた瞬間、その投稿は通ったのか — 無人パイプラインで MCP 書き込みを二重実行せずにやり直す
接続断で中断した MCP の書き込みツール呼び出しは、サーバー側で実行されたかどうか分かりません。素朴な再試行が二重投稿を生む理由と、冪等キーと照合読み取りで安全にやり直すラッパーの実装を、無人パイプラインの実例とともにまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →