AI Agents

AIエージェントにメモリを追加する方法:本番環境における状態永続化パターン

Alejandro Rioja
Alejandro Rioja
2 分で読める
TL;DR

ステートレスなエージェント——Workerが終了するとすべてを忘れるタイプ——は1回限りのタスクには適しています。エージェントが昨日何が起きたかを覚えておく必要がある、リピーターの顧客を認識する必要がある、あるいは以前の出力に基づいて作業する必要があるとき、メモリが必要です。3つのパターンがあります:ワーキングメモリ(実行中のコンテキスト、1回の実行期間中KVに保存)、エピソードメモリ(何が起きたか、いつ起きたか、クエリ可能なログ)、セマンティックメモリ(あなたが知っていること、ベクトル検索や構造化データで取得)。正しいパターンを正しいジョブに対応させましょう。

無料ニュースレター

毎週水曜。28,400人以上の読者。無駄なし。

目次

2026年6月更新。

TL;DR: ステートレスなエージェント——Workerが終了するとすべてを忘れるタイプ——は1回限りのタスクには適しています。エージェントが昨日何が起きたかを覚えておく必要がある、リピーターの顧客を認識する必要がある、あるいは以前の出力に基づいて作業する必要があるとき、メモリが必要です。3つのパターンがあります:ワーキングメモリ(実行中のコンテキスト、1回の実行期間中KVに保存)、エピソードメモリ(何が起きたか、いつ起きたか、クエリ可能なログ)、セマンティックメモリ(あなたが知っていること、ベクトル検索や構造化データで取得)。正しいパターンを正しいジョブに対応させましょう。

[オペレーターの見解] ステートレスの壁に何度もぶつかってきました。ソーシャルリプライエージェントは20回会話した顧客に何度も自己紹介し続けました。デイリーブリーフエージェントは昨日すでに報告したことを覚えておらず、同じ問題を4日連続でフラグしました。正しい種類のメモリを追加することで両方を解決しました。これが私が使っている方法です。

ステートレスなエージェントがなぜ失敗し続けるのか

ステートレスなエージェントは、明示的に渡されたものだけで各実行を開始します:システムプロンプト、ユーザーメッセージ、呼び出し時に取得した新しいデータです。以前の実行、以前のユーザー、以前の決定を認識していません。

1回限りの分類タスク——コメントを読んでカテゴリを返す——にはステートレスで十分です。速く、安く、予測可能です。

継続性が必要になった瞬間に障害が発生します:

  • 顧客の履歴を認識しない顧客向けエージェント
  • 先週すでに推薦した記事を推薦するコンテンツエージェント
  • 解決済みのケースをエスカレートし続けるモデレーションエージェント
  • 同じ古いアラートを無期限に表示するデイリーブリーフ

これらはすべて同じ問題の症状です:エージェントには実行をまたいでコンテキストを運ぶ方法がありません。

3種類のメモリ

本番環境で役立つフレームワーク:

  1. ワーキングメモリ — 単一の実行中に、エージェントが_今_知っていること。呼び出しの期間中、KVまたはメモリに保持されます。
  2. エピソードメモリ — 何が起きたか、いつ起きたか。各実行の開始時にエージェントが読んで自分自身を方向付けるための構造化されたログ。
  3. セマンティックメモリ — 世界、顧客、またはナレッジベースについて知っていること。関連するときに構造化クエリまたはベクトル検索で取得されます。

常に3つすべてが必要なわけではありません。私が実行するほとんどのエージェントはワーキング+エピソードメモリを必要とします。セマンティックメモリは構築が最も難しく、ナレッジベースがコンテキストウィンドウに収まらないほど大きい場合にのみ価値があります。

ワーキングメモリ:実行中のコンテキスト

ワーキングメモリは、1回のエージェント実行の期間中存在する状態です。最も単純な形は関数スコープ内の変数です。より興味深い形は、同じ実行内のサブタスクが読み書きする共有KVキーです。

私のソーシャルリプライエージェントは、1つのキューメッセージのコメントバッチを処理しながらコンテキストを蓄積するためにワーキングメモリを使用します。開始時に各顧客の最近の会話履歴をKVから読み込み、処理中に新しいコンテキストを追加し、終了時に書き戻します。

typescript
// workers/social-reply.ts

async function processComment(
  comment: SocialCommentEvent,
  env: Env
): Promise<void> {
  // この顧客の最近の履歴をKVから読み込む(ワーキングメモリ)
  const historyKey = `customer:${comment.userId}:history`;
  const rawHistory = await env.AGENT_KV.get(historyKey);
  const history: ConversationTurn[] = rawHistory
    ? JSON.parse(rawHistory)
    : [];

  // 履歴からコンテキスト対応のシステムプロンプトを構築する
  const systemPrompt = buildSystemPrompt(history);

  const response = await anthropic.messages.create({
    model: "claude-opus-4-8",
    max_tokens: 512,
    system: systemPrompt,
    messages: [{ role: "user", content: comment.text }],
  });

  const reply =
    response.content[0].type === "text" ? response.content[0].text : "";

  // 履歴を更新する——最後の10ターンを保持、TTL 30日間
  const updatedHistory: ConversationTurn[] = [
    ...history.slice(-9),
    { role: "assistant", content: reply, timestamp: comment.timestamp },
  ];
  await env.AGENT_KV.put(historyKey, JSON.stringify(updatedHistory), {
    expirationTtl: 60 * 60 * 24 * 30,
  });

  await postReply(comment, reply, env);
}

2つ注目すべき点があります。履歴は10ターンに制限されています——スライディングウィンドウを挿入し、無制限に増やさないでください。TTLは30日間です:顧客が1ヶ月沈黙すれば、履歴が期限切れになり、エージェントが最初からやり直します。どちらも意図的です。

エピソードメモリ:何が起きたか、いつ起きたか

エピソードメモリはエージェントのログです。各新しい実行の開始時にエージェントが読んで繰り返しを避けるための過去の実行の構造化された記録です。

私のデイリーブリーフエージェントは、各実行がすでにフラグされたものを認識していなかったため、毎日同じ古いアラートを表示していました。解決策:エージェントがブリーフを生成する前に読む過去のアラートの構造化ログです。

typescript
// workers/daily-brief.ts

interface AlertLogEntry {
  id: string;
  surfacedAt: string; // ISOタイムスタンプ
  resolvedAt?: string;
  summary: string;
}

async function buildDailyBrief(env: Env): Promise<void> {
  const [emails, calendar, tasks] = await Promise.all([
    fetchOvernightEmails(env),
    fetchTodayCalendar(env),
    fetchTopTasks(env),
  ]);

  // エピソードメモリを読み込む:すでにフラグされたもの
  const rawLog = await env.AGENT_KV.get("brief:alert-log");
  const alertLog: AlertLogEntry[] = rawLog ? JSON.parse(rawLog) : [];

  // 最近の未解決のアラートのみにフィルタリング
  const sevenDaysAgo = new Date(
    Date.now() - 7 * 24 * 60 * 60 * 1000
  ).toISOString();
  const recentAlerts = alertLog.filter(
    (e) => e.surfacedAt > sevenDaysAgo && !e.resolvedAt
  );

  const brief = await synthesizeBrief(
    { emails, calendar, tasks, recentAlerts },
    env
  );

  // この実行でフラグされた新しいアラートでログを更新する
  const newAlerts: AlertLogEntry[] = brief.newAlerts.map((a) => ({
    id: crypto.randomUUID(),
    surfacedAt: new Date().toISOString(),
    summary: a,
  }));

  const updatedLog = [...alertLog, ...newAlerts].slice(-100); // 最後の100件を保持
  await env.AGENT_KV.put("brief:alert-log", JSON.stringify(updatedLog));

  await writeToWorkspace(brief.content, env);
}

エージェントは自分が何を言ったかを知るようになりました。根本的な問題が変わるまで、重複したアラートはブリーフに含まれません。アラートを解決済みとしてマークすると、アクティブリストから消えます。

このパターンは一般化できます:決定、フラグ、または推薦を生成するエージェントはすべてログから恩恵を受けます。ログは安価(KVに数KB)で、見返りは高い(冗長な出力なし)。

セマンティックメモリ:あなたが知っていること

セマンティックメモリはナレッジベースです。システムプロンプトにすべてを詰め込む代わりに、クエリ時に「Xについて何を知っていますか?」に答えます。

最も単純な形はKVまたはデータベースの構造化ルックアップです。私のPicklerand予約エージェントは確認書を作成する前に顧客プロファイルとコート好みを検索します:

typescript
// workers/booking-agent.ts

interface CustomerProfile {
  userId: string;
  preferredCourts: string[];
  experienceLevel: "beginner" | "intermediate" | "advanced";
  specialNotes: string;
}

async function draftConfirmation(
  booking: BookingEvent,
  env: Env
): Promise<string> {
  // KVから顧客プロファイルを取得する(セマンティックメモリ——事実的な知識)
  const profileKey = `customer:${booking.userId}:profile`;
  const rawProfile = await env.AGENT_KV.get(profileKey);
  const profile: CustomerProfile | null = rawProfile
    ? JSON.parse(rawProfile)
    : null;

  const systemPrompt = profile
    ? `あなたはパーソナライズされた予約確認書を作成します。この顧客は${profile.preferredCourts.join("、")}を好み、${profile.experienceLevel}レベルのプレイヤーです。${profile.specialNotes}`
    : "あなたはピックルボール施設の予約確認書を作成します。";

  const response = await anthropic.messages.create({
    model: "claude-haiku-4-5-20251001",
    max_tokens: 256,
    system: systemPrompt,
    messages: [
      {
        role: "user",
        content: `次の予約の確認書を作成してください:${JSON.stringify(booking)}`,
      },
    ],
  });

  return response.content[0].type === "text" ? response.content[0].text : "";
}

より大きなナレッジベース——製品ドキュメント、サポートナレッジベース、コンテキストウィンドウに収まらないほど大きいもの——にはベクトルストアが必要です。ワークフローは:クエリを埋め込み、最も関連性の高いk個のチャンクを取得し、コンテキストに注入します。Cloudflare Vectorizeは、すでにWorkersを使用している場合にこれをネイティブに処理します。より大きなインデックスにはUpstash Vectorを使用しました。選択はスケールによって決まり、原則ではありません。

セマンティックメモリについての正直な注記:3つの中で構築と維持が最も難しいです。インデックスを最新に保つ必要があります。取得品質が変動します。構造化ルックアップ——KV、D1のテーブル——から始め、構造化アプローチで必要なナレッジサーフェスをカバーできない場合にのみベクトル検索に頼りましょう。

メモリ意思決定フレームワーク

エージェントにメモリを追加する前に、3つの質問に答えましょう:

  1. エージェントは実行をまたいで記憶する必要がありますか? 各呼び出しが本当に独立している場合——翻訳、分類、1回限りの生成——メモリをスキップしてください。ステートレスはよりシンプルで安価です。

  2. エージェントは自分の履歴に目をつぶって繰り返していますか? そうなら、まずエピソードメモリを追加しましょう。最も労力の少ない修正であり、「エージェントがXをし続ける」という苦情のほとんどをカバーします。

  3. エージェントはすべきでないのに各ユーザーまたはエンティティを同一に扱っていますか? そうなら、ワーキングメモリ(顧客履歴、ユーザープロファイル)またはセマンティックメモリ(検索または取得システム)を追加しましょう。

私が最もよく見る間違い:誰かがエピソードメモリがなかったために失敗していたエージェント——すでに何をしたかのログがなかったエージェント——に巨大なナレッジベース(セマンティックメモリ)を追加します。複雑さが問題と一致していません。

本番環境で実際に使っているもの

30以上のエージェントで:

  • すべてが少なくともワーキングメモリを持っています——実行内の何らかの状態の形、たとえそれがコンテキストウィンドウ自体だとしても。
  • 約半数がエピソードメモリを持っています——過去の実行、決定、フラグのログ。これはほぼ常に追加する価値があります。
  • 3〜4個がベクトルストアに支えられた真のセマンティックメモリを持っています。これらは大規模で動的なナレッジベースに対して質問に答えるエージェントです。

Cloudflare KVは、ワーキングメモリとエピソードメモリのデフォルトストレージです。高速で安価で、Workersにネイティブに統合されています——追加のクライアントなし、別の認証情報なし。制限:KVは最終的に一貫性があり、高頻度の書き込みには適していません。エージェントが1秒に何度も状態を書き込む場合は、代わりにDurable ObjectsまたはD1データベースを使います。

ベクトルに支えられたセマンティックメモリには、小〜中規模のインデックス(〜10万ベクトル未満)にCloudflare Vectorizeを、それより大きいものにUpstash Vectorを使用します。どちらも一流のJavaScriptクライアントを持っています。

オペレーターの結論

ステートレスな動作が本当の問題を引き起こしているときだけエージェントにメモリを追加しましょう——繰り返しの出力、顧客履歴の盲点、過去の決定への無知。次に、正しいレイヤーを選びましょう:実行中のコンテキストにはワーキングメモリ、過去に起きたことにはエピソードメモリ、知っていることにはセマンティックメモリ。確信が持てない場合はエピソードから始めましょう——最も少ない複雑さで最も一般的な障害モードを修正します。構造化ルックアップを使い切るまでベクトルデータベースに頼らないでください。最良のメモリシステムは、エージェントを正しく動作させる最もシンプルなものです。


関連: 30以上の本番エージェントを実行するために使用するエージェントスタック · イベントトリガー型エージェントとスケジュールエージェント · AIエージェントが実際に機能しているかどうかを測定する方法

あなたのユースケースにエージェントメモリを設計する手助けが必要ですか? お問い合わせ — オペレーターチームのための本番エージェントシステムを設計しています。

続きを読む

関連記事

続きを読む

AIプレイブックをメールでお届け

毎週水曜。28,400人以上の読者。無駄なし。

↵ すべての結果を見る esc esc で閉じる