冷蔵庫の余りもので晩ごはん!Amazon Bedrock AgentCore Memory Episodic でエージェントに"経験"を持たせる

記事タイトルとURLをコピーする

re:Invent 2025 から帰ってきました。ハロー
アプリケーションサービス部の千葉です。

冷蔵庫を開けて「今日の晩ごはん、何にしよう...」と悩んだこと、ありませんか?

そんな日常の困りごとを解決してくれる"料理アシスタント AI"を作りながら、Amazon Bedrock AgentCore Memory の新機能「Episodic Memory(エピソード記憶)」を検証してみました。

今回の検証は Python インタプリタから直接 MemoryClient を操作して検証してみました。
AgentCore Memory の動作を理解するには、直接 API を叩いてみるのが近道です。

AgentCore Memory とは? 〜 4つの記憶戦略

AgentCore Memory は、AI エージェントに「記憶」を持たせるためのマネージドサービスです。
2025年12月現在、以下の4つの記憶戦略が提供されています。

「料理アシスタント」を例に、それぞれの戦略が何を覚えるか整理してみましょう。

戦略 何を覚えるか 料理アシスタントでの例
Summary 会話のあらすじ 「昨日は豚肉とキャベツで野菜炒めを提案した」
Semantic 事実・知識 「ユーザーの家族は4人構成」「冷蔵庫によく豚肉がある」
User Preference 好み・傾向 「辛い料理が苦手」「和食が好き」
Episodic 体験の記憶 「回鍋肉を提案 → "辛いのは苦手"と言われた → 野菜炒めで成功」という一連の体験

Summary / Semantic / User Preference の限界

3つの戦略はそれぞれ便利ですが、以下のような限界があります。

  • Summary: 「何を話したか」は覚えているが、「なぜそうなったか」の文脈が薄い
  • Semantic: 事実は記録されるが、体験としての学習には向かない
  • User Preference: 明示的な好みは覚えるが、失敗から学ぶ仕組みがない

ここで Episodic Memory の出番です。

Episodic Memory が他と違う点

「経験から学ぶ」エージェントへ

Episodic Memory は、会話のやり取りを「エピソード(体験)」として抽出・保存します。

従来の記憶戦略と比較してみましょう。

従来の記憶戦略:
  ユーザー: 「辛いのは苦手です」 → User Preference に保存

Episodic Memory:
  [状況] 豚肉とキャベツの料理を聞かれた
  [意図] おいしい料理を提案したい
  [結果] 回鍋肉を提案 → 「辛いのは苦手」と断られた
  [学び] 辛い料理は避けるべき

単に「好み」を記録するのではなく、「どういう状況で、何を提案して、どうなったか」という体験全体を記録します。これにより、エージェントは文脈を踏まえた判断ができるようになります。

リフレクション機能

Episodic Memory には「リフレクション(振り返り)」機能があります。複数のエピソードを横断して「インサイト」を自動生成してくれます。

例えば、複数セッションで以下のようなエピソードが蓄積された場合を考えてみましょう。

セッション1: 回鍋肉を提案 → 「辛いのは苦手」と断られた
セッション2: 野菜炒めを提案 → 喜ばれた
セッション3: キムチ鍋を提案 → 「辛いのは...」と断られた

→ リフレクション: 「このユーザーには辛くないレシピを優先すべき」

個別のエピソードからパターンを見出し、より高度な学習を実現します。

実装してみた 〜 冷蔵庫アシスタント

では、実際に Episodic Memory を使った料理アシスタントを作ってみましょう。

事前準備

まず、bedrock-agentcore パッケージをインストールします。

pip install bedrock-agentcore

メモリの作成

from bedrock_agentcore.memory import MemoryClient

client = MemoryClient(region_name="us-east-1")

memory = client.create_memory_and_wait(
    name="CookingAssistantMemory",
    description="冷蔵庫の余り食材から料理を提案するアシスタント用メモリ",
    strategies=[
        {
            "episodicMemoryStrategy": {
                "name": "cookingEpisodeStrategy",
                "namespaces": ["/strategy/{memoryStrategyId}/actor/{actorId}/session/{sessionId}"],
                "reflectionConfiguration": {
                    "namespaces": ["/strategy/{memoryStrategyId}/actor/{actorId}"]
                }
            }
        }
    ]
)

memory_id = memory.get('id')
strategy_id = memory['strategies'][0]['strategyId']

ポイントは以下の2つです。

  • namespaces: エピソードの保存階層を指定。セッション単位で保存する設定にしています
  • reflectionConfiguration: リフレクション(振り返り)の生成先を指定。アクター(ユーザー)単位で集約する設定にしています

会話イベントの登録

メモリにイベントを登録するには create_event を使います。messages 引数には (テキスト, ロール) のタプルのリストを渡します。

from datetime import datetime

session_id = f"cooking_session_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
actor_id = "example_user"

# 1往復目:ユーザーの発言とアシスタントの応答をまとめて登録
client.create_event(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("冷蔵庫にキャベツと豚肉があります。何か料理を提案してください", "USER"),
        ("キャベツと豚肉があれば、回鍋肉、野菜炒め、お好み焼き、蒸し豚などが作れますよ!", "ASSISTANT"),
    ]
)

このように、ユーザーとアシスタントの会話を登録していきます。

残りの会話イベントの登録

同様に、残りの会話も登録していきます。

# 2往復目
client.create_event(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("辛い料理は苦手です。他に何かありますか?", "USER"),
        ("辛いものが苦手でしたら、塩バター炒め、コンソメスープ、クリーム煮などがおすすめです。", "ASSISTANT"),
    ]
)

# 3往復目
client.create_event(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("野菜炒めを作ってみます。調味料は何を使えばいいですか?", "USER"),
        ("醤油大さじ1、みりん小さじ1、塩コショウ少々がおすすめです。", "ASSISTANT"),
    ]
)

# 4往復目
client.create_event(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("美味しくできました!ありがとうございます", "USER"),
        ("よかったです!辛くない優しい味付けがお好みなんですね。また何かあればお気軽にどうぞ。", "ASSISTANT"),
    ]
)

# 5往復目(終了シグナル)
client.create_event(
    memory_id=memory_id,
    actor_id=actor_id,
    session_id=session_id,
    messages=[
        ("今日の料理相談はこれで終わりです", "USER"),
        ("ありがとうございました。またいつでもご相談ください!", "ASSISTANT"),
    ]
)

最後のメッセージ「今日の料理相談はこれで終わりです」がポイントです。この終了シグナルについては次のセクションで説明します。

注意点:エピソード生成には時間がかかる

Episodic Memory を使う上で、最も重要な注意点があります。

エピソードの生成には時間がかかります。

他の戦略(Summary 等)は会話中にリアルタイムで記録されますが、Episodic Memory は会話の「終了」を検知してからエピソードを生成します。

# 会話直後に検索しても...
episodes = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/strategy/{strategy_id}/actor/{actor_id}/session/{session_id}",
    query="料理"
)
print(f"エピソード数: {len(episodes)}")  # → 0件

会話直後は 0 件です。今回の検証では約10分でエピソードが生成されました。(公式ドキュメントでは最大1時間程度かかる場合があると記載されています)

なぜ時間がかかるのか?

Episodic Memory は以下のステップで処理されます。

  1. 抽出(Extraction): 進行中のエピソードを分析して完了を判定
  2. 統合(Consolidation): エピソード完了時に抽出結果を単一レコードに統合
  3. リフレクション(Reflection): エピソード間でインサイトを生成

この処理には時間がかかるため、リアルタイム性が求められる用途には向きません。

会話の終了を明確にする

「会話の終了」を明確にするため、最後のメッセージで終了シグナルを送ることを推奨します。

良い例:「今日の相談はこれで終わりです」
曖昧な例:「ありがとう」

エピソードの確認

約10分待ってから再度検索すると、エピソードが生成されていることを確認できます。

# 約10分後に再度検索
episodes = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/strategy/{strategy_id}/actor/{actor_id}/session/{session_id}",
    query="料理"
)
print(f"エピソード数: {len(episodes)}")  # → 1件

生成されたエピソードには、会話全体から抽出された「学び」が含まれています。

import json

for episode in episodes:
    content = json.loads(episode['content']['text'])
    print(f"状況: {content['situation']}")
    print(f"意図: {content['intent']}")
    print(f"振り返り: {content['reflection']}")

実際に生成されたエピソードの一部を見てみましょう。
※: 実際の出力は英語です。あくまで一例として

状況: ユーザーは冷蔵庫にある食材(キャベツと豚肉)を使った料理の提案を求めていた
意図: 美味しい料理を提案したい
振り返り: 最初に幅広い選択肢を提示し、ユーザーの好み(辛いものが苦手)が
         分かった時点で具体的なレシピに絞り込む流れが効果的だった。
         調味料の分量を具体的に示したことで、ユーザーは成功体験を得られた。

このように、単なる会話ログではなく「どういう状況で、何をして、どうなったか」という体験が構造化されて保存されます。

なお、今回の検証は「エピソードの保存と検索」までを対象としています。実際にエージェントがこのエピソードを参照して応答を生成するには、retrieve_memories で取得したエピソードを LLM のプロンプトに含める実装が別途必要です。

リフレクションの確認

リフレクション(振り返り)は、複数のエピソードを横断してインサイトを生成する機能です。メモリ作成時に reflectionConfiguration で指定した namespace に保存されます。

# REFLECTION を取得
reflections = client.retrieve_memories(
    memory_id=memory_id,
    namespace=f"/strategy/{strategy_id}/actor/{actor_id}",
    query="reflection"
)

print(f"リフレクション数: {len(reflections)}")

リフレクションが生成されている場合、以下のように内容を確認できます。

for r in reflections:
    content = json.loads(r['content']['text'])
    record_type = r.get('metadata',{}).get('x-amz-agentcore-memory-recordType', {}).get('stringValue')
    if record_type == 'REFLECTION':
        # 正しいキー名を使用
        print(f"タイトル: {content.get('title')}")
        print(f"ユースケース: {content.get('use_cases')}")
        print(f"ヒント: {content.get('hints')}")
        print(f"確信度: {content.get('confidence')}")

リフレクションは複数のエピソードが蓄積された後に生成されるため、単一セッションの検証では確認が難しい場合があります。
複数セッションで異なる体験を登録し、時間を置いてから検索することで、パターンを見出したインサイトが生成されることを確認できます。

いつ Episodic を使うべきか? 〜 戦略選択フローチャート

4つの戦略をどう使い分ければよいでしょうか?以下のフローチャートを参考にしてください。

「好み」を覚えたい?
  └─ YES → User Preference

「事実・知識」を蓄積したい?
  └─ YES → Semantic

「会話の要約」が欲しい?
  └─ YES → Summary

「体験から学習」させたい?
  └─ YES → Episodic

迷ったら?
  └─ 複数の戦略を組み合わせる

Episodic が向いているユースケース

ユースケース 理由
カスタマーサポート 過去の問い合わせ対応の成功/失敗を学習
コードアシスタント 「このエラーは前回こう解決した」を記憶
トラブルシューティング 診断フローの最適化
料理アシスタント 提案が受け入れられたかを学習

共通点は「成功/失敗のパターンから学習させたい」という点です。

Episodic が向いていないユースケース

一方、以下のケースでは他の戦略の方が適しています。

  • 即時性が求められる: リアルタイムで記憶を参照したい → Summary / Semantic
  • 単純な好みの記録: 「辛いのは苦手」だけ覚えればよい → User Preference
  • 事実の蓄積: 「家族は4人」「予算は1000円」→ Semantic

まとめ

項目 内容
Episodic Memory とは 会話を「エピソード(体験)」として保存し、エージェントに"経験から学ぶ"能力を与える
他の戦略との違い Summary/Semantic/User Preference は「何を覚えるか」、Episodic は「体験として学ぶ」
リフレクション機能 複数エピソードからパターンを見出し、インサイトを自動生成
注意点 エピソード生成に時間がかかる(今回は約10分、最大1時間程度)
向いているケース 成功/失敗のパターンから学習させたいエージェント

「冷蔵庫の余りもので何作ろう?」という日常の悩みを解決しながら、AgentCore Memory の Episodic 戦略を検証しました。

エージェントに「経験」を持たせることで、単なる Q&A ボットから「学習するアシスタント」へ進化させられます。

皆さんもぜひ試してみてください。

後片付け:メモリの削除

検証が終わったら、作成したメモリを削除しておきましょう。

# メモリを削除
client.delete_memory(memory_id=memory_id)

削除には少し時間がかかります。ステータスが DELETING から消えれば完了です。

千葉 哲也 (執筆記事の一覧)

アプリケーションサービス部

コーヒーゼリーが好きです。