【第3回】Amazon Bedrock AgentCore 入門 — メモリ・セッション管理と複数ステップにわたる対話フローの実装

AWS

前回は AgentCore Runtime を使ってエージェントをデプロイし、ツール連携の基本を実装しました(前回の記事はこちら)。今回はその続きで、AgentCore Memory を使った「状態を持つエージェント」の作り方を整理していきます。

正直、最初に Memory という概念を見たとき「DynamoDB で会話履歴を持てばよくない?」って思ってたんですが、使ってみると全然違うんですよね。単なるログ保存じゃなく、セッションをまたいで「記憶」が蓄積される仕組みになっています。

この記事でわかること

  • AgentCore Memory の短期記憶と長期記憶の概念と構造
  • Memory Strategy(記憶戦略)の種類と使い分け
  • メモリリソースとセッションの作成方法
  • 複数ステップにわたる対話フローの実装パターン
  • 実装時のよくあるハマりどころと対処法

AgentCore Memory の全体像

AgentCore Memory は大きく 短期記憶(Short-term Memory)長期記憶(Long-term Memory) の2層で構成されています。

  • 短期記憶:1セッション内のターンバイターンの会話履歴を保持。現在の会話フローを追うために使う
  • 長期記憶:複数セッションをまたいで重要な情報を抽出・永続化。ユーザーの好みや過去に解決した問題などが蓄積される

この2層を組み合わせることで、「今の会話の文脈」と「過去のやりとりから得た知識」の両方をエージェントが利用できるようになります。

長期記憶には Memory Strategy(記憶戦略) という概念があって、何をどう抽出・保存するかをカスタマイズできます。デフォルトで使えるのは以下の種類です。

  • Semantic Strategy:会話から事実・エンティティ・文脈的な知識を抽出。永続的なナレッジベースを構築するのに向いている
  • Episodic Strategy:重要なやりとりを要約してコンパクトに記録。生のイベントを全部保存するのではなく、「意味のある出来事」だけを残す
  • Summarization Strategy:会話を要約して保存。長い会話履歴を圧縮しつつ文脈を維持したいときに使う

余談ですが、この Episodic Memory って人間の記憶の分類と同じ名前なんですよね。認知科学の用語をそのまま持ってきてる感じがして、ちょっとおもしろいなと思いました。

セットアップ

まず SDK をインストールします。

pip install bedrock-agentcore

AWS 側ではコントロールプレーン用のクライアント(bedrock-agentcore-control)とデータプレーン用のクライアント(bedrock-agentcore)の2種類が必要です。メモリリソースの作成・削除はコントロール側、実際の会話の読み書きはデータ側、という使い分けです。

メモリリソースを作成する

まず Memory リソースを1つ作ります。これがいわば「会話履歴を入れる箱」のベースになります。

import boto3
from bedrock_agentcore.memory import MemoryClient

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

memory = client.create_memory(
    name="CustomerSupportAgentMemory",
    description="カスタマーサポート用エージェントのメモリ",
)

memory_id = memory["id"]
print(f"Memory ID: {memory_id}")

長期記憶を有効にしたい場合は、この段階で Memory Strategy を追加しておく必要があります。後から追加もできますが、追加前に保存した会話が長期記憶側の抽出対象として扱われるかは挙動が変わる可能性があるので、本番運用前に設計しておくのが無難です。

import boto3

control_client = boto3.client("bedrock-agentcore-control", region_name="us-east-1")

# Semantic Strategy を追加
control_client.update_memory(
    memoryId=memory_id,
    memoryStrategies=[
        {
            "semanticMemoryStrategy": {
                "name": "SemanticStrategy",
            }
        }
    ]
)

セッションを作って会話を記録する

メモリへの書き込みは MemorySessionManager を経由して行います。actor_id がユーザー識別子、session_id が1回の会話セッションに対応するイメージです。

from bedrock_agentcore.memory import MemorySessionManager
from bedrock_agentcore.memory.constants import ConversationalMessage, MessageRole

session_manager = MemorySessionManager(
    memory_id=memory_id,
    region_name="us-east-1"
)

# actor_id = ユーザー識別子、session_id は省略すると UUID が自動生成される
session = session_manager.create_memory_session(
    actor_id="user-001",
    session_id="session-20260501-001"
)

# 複数ターンをまとめて書き込む
session.add_turns([
    ConversationalMessage("注文した商品がまだ届かないんですが", MessageRole.USER),
    ConversationalMessage("ご注文番号を教えていただけますか?", MessageRole.ASSISTANT),
    ConversationalMessage("ORD-98765 です", MessageRole.USER),
    ConversationalMessage(
        "ORD-98765 を確認しました。現在、配送センターで手続き中です。明日中にお届け予定です。",
        MessageRole.ASSISTANT
    ),
])

add_turns() を呼ぶと、会話内容が短期記憶(イベントストア)に書き込まれます。さらに長期記憶の Strategy が設定されている場合は、バックグラウンドで自動的に情報の抽出・永続化が走ります。ただしこの抽出処理は少し時間がかかるようで、すぐに検索しようとしても出てこないことがあります(最初ここでハマりました)。

複数ステップの対話フローを実装する

ここからが今回の本題です。単発の問い合わせではなく、「前回の会話の内容を踏まえて次のセッションに続く」という複数ステップの対話フローを実装してみます。

基本的な流れはこうです:

  1. 新しいセッション開始時に、search_long_term_memories() で過去の記憶を検索
  2. 取得した記憶をプロンプトのコンテキストに注入してLLMを呼ぶ
  3. やりとりを add_turns() で記録する
import boto3
import json
import time
from bedrock_agentcore.memory import MemorySessionManager
from bedrock_agentcore.memory.constants import ConversationalMessage, MessageRole

MEMORY_ID = "your-memory-id"  # 作成したMemoryのID
REGION = "us-east-1"

bedrock = boto3.client("bedrock-runtime", region_name=REGION)

def chat_with_memory(actor_id: str, session_id: str, user_input: str) -> str:
    session_manager = MemorySessionManager(
        memory_id=MEMORY_ID,
        region_name=REGION
    )
    session = session_manager.create_memory_session(
        actor_id=actor_id,
        session_id=session_id
    )

    # 過去の記憶を検索してコンテキストに使う
    memories = session.search_long_term_memories(
        query=user_input,
        namespace_path="/",
        top_k=3
    )

    memory_context = ""
    if memories:
        memory_context = "\n"
        for m in memories:
            memory_context += f"- {m.get('content', '')}\n"

    prompt = f"{memory_context}\n\n{user_input}"

    response = bedrock.invoke_model(
        modelId="anthropic.claude-3-5-sonnet-20241022-v2:0",
        body=json.dumps({
            "anthropic_version": "bedrock-2023-05-31",
            "max_tokens": 512,
            "messages": [{"role": "user", "content": prompt}]
        }),
        contentType="application/json",
        accept="application/json"
    )

    body = json.loads(response["body"].read())
    answer = body["content"][0]["text"]

    # 今回のターンを記録
    session.add_turns([
        ConversationalMessage(user_input, MessageRole.USER),
        ConversationalMessage(answer, MessageRole.ASSISTANT),
    ])

    return answer

この実装の肝は search_long_term_memories() の部分で、ユーザーの発言をそのままクエリにしてセマンティック検索をかけています。「以前の注文の件」とか「前に話した話」みたいな曖昧な表現でも、ベクトル検索的に関連する記憶を引っ張ってこられるのが便利なところです。

2回目以降のセッションで記憶を活用する

実際に2回目のセッションから使う場合はこんな感じです。

# 1回目のセッション
chat_with_memory(
    actor_id="user-001",
    session_id="session-20260501-001",
    user_input="注文した商品がまだ届かないんですが"
)

# 長期記憶の抽出が完了するのを待つ
time.sleep(45)

# 2回目のセッション(翌日など)
reply = chat_with_memory(
    actor_id="user-001",               # 同じ actor_id を使う
    session_id="session-20260502-001", # session_id は新しい
    user_input="昨日の件、その後どうなりましたか?"
)
print(reply)

actor_id が同じであれば、セッションをまたいで長期記憶が引き継がれます。逆に言うと、session_id は会話1回ごとに変えてOKで、ユーザーの同一性は actor_id で管理するという設計です。最初これを逆に理解してて少し混乱しました。

ちょっとハマったこと

実際に触ってみて気になった点をいくつか。

  • 長期記憶の反映タイミングadd_turns() 直後に search_long_term_memories() を呼んでも何も返ってこないことがある。抽出完了まで少し時間がかかるようなので、テストするときは time.sleep(45) くらい入れておくのが無難です
  • Memory Strategy は後付けだと実質意味がない:追加自体はできるが、Strategy を追加する前に保存した会話が長期記憶に入るかは挙動次第っぽいので、本番運用前にきちんと設計しておく必要があります
  • リージョンの制限:利用可能なリージョンに制限がある場合があるので、事前に公式の対応リージョン一覧を確認した上で選ぶのが確実です

正直、まだ理解が浅い部分があって、特に namespace_path の使い分けについては「これで合ってるのか?」という感じです。複数エージェントでメモリを共有したい場合の設計パターンは、もう少し調べてみたいところ。

※この記事にはプロモーションが含まれます

ちなみに、お名前.com レンタルサーバー(WordPressに特化した高速レンタルサーバー。月額990円〜、独自ドメイン実質0円)も気になっています。お名前.com レンタルサーバー

まとめ

AgentCore Memory を使うと、DynamoDB に自分で会話履歴を詰め込む実装より明らかにコードがシンプルになります。特に長期記憶の自動抽出は自前で実装しようとするとそこそこ大変なので、マネージドでやってくれるのは助かります。

次のステップとしては、複数エージェント間での Memory 共有パターンや、カスタム Memory Strategy の実装なども気になるところ。メモリ永続化の仕組みが理解できれば、より複雑な対話フローにも対応できそうです。

📚 シリーズ「Amazon Bedrock AgentCore 入門」(第3回 / 全4回)

← 前回の記事: 前回の記事はこちら

→ 次回の記事: 【第4回】Amazon Bedrock AgentCore 入門 — 本番運用に向けたベストプラクティスとよくあるハマりポイント総まとめ

参考になったらクリックしてもらえると嬉しいです!

Blogmura CloudAWS Ranking
タイトルとURLをコピーしました