
AgentCore Memory の短期記憶にカスタムメタデータを付けてフィルタする
はじめに
こんにちは、スーパーマーケットが大好きなコンサル部の神野(じんの)です。
みなさん、GW中もAgentCore触っていますか?私もめちゃくちゃ触っていました。
そんな、AgentCore Memory の短期記憶には、イベントごとに Key-Value 形式のカスタムメタデータを付けられて、ListEvents でそのメタデータをキーで絞り込める仕組みが用意されています。
Event metadata lets you attach additional context information to your short-term memory events as key-value pairs.
結構前から実装されている機能みたいなのですが、最近私はこの機能を知って提供されているんだとびっくりしました。
機能の概要
本題に入る前に、AgentCore Memory の記憶の構造をちょっとだけおさらいしておきます。会話の記憶がそのまま保存される短期記憶と、そこから戦略に基づいて大事な情報を抽出して長期保存しておく長期記憶に分かれています。
| 階層 | 何が入るか | 主な API |
|---|---|---|
| 短期記憶 | 会話イベント(生ログ) | CreateEvent(書き込み) / ListEvents(検索) |
| 長期記憶 | 抽出された情報レコード | RetrieveMemoryRecords(検索) |
短期記憶側で使えるメタデータ周りの機能はこの 2 つです。
CreateEvent(イベントの書き込み)のmetadataパラメータで、Key-Value 形式の独自メタデータを付けられるListEvents(イベントの検索)のfilter.eventMetadataで、そのメタデータをキーに絞り込める
長期記憶側にも metadataFilters というパラメータがあり、こちらは indexedKeys と metadataSchema という別の宣言が必要で、LLM 抽出の挙動など考えどころも多いので別のブログで深掘りします。
今回は短期記憶にポイントを絞って確認してみます!
前提
今回の検証で使用した環境やバージョンです。
| 項目 | バージョン |
|---|---|
| Python | 3.13 |
| boto3 / botocore | 1.43.2 |
| bedrock-agentcore | 1.8.0(公式 Python SDK・Strands カスタムツール例で使用) |
| strands-agents | 1.38.0(Strands カスタムツール例で使用) |
| AWSリージョン | us-east-1 |
uv でセットアップする
依存関係の管理は uv で進めていきます。プロジェクトディレクトリで初期化して、必要なパッケージを追加します。
# プロジェクト初期化
uv init --python 3.13
# 依存パッケージの追加(後半の Strands カスタムツール例を試すなら strands-agents と bedrock-agentcore も)
# uv add すると仮想環境とロックファイルは自動で整います
uv add boto3 strands-agents bedrock-agentcore
以降に登場するスクリプト(create_memory.py など)は uv run create_memory.py のような形で実行できます。
実装
Memory を作る
最小構成の Memory をひとつ用意します。
import boto3
control = boto3.client("bedrock-agentcore-control", region_name="us-east-1")
res = control.create_memory(
name="MetadataFilterBlogShort",
eventExpiryDuration=30,
memoryStrategies=[
{"userPreferenceMemoryStrategy": {
"name": "BlogUserPreferenceShort",
"namespaces": ["/users/{actorId}/preferences"],
}},
],
)
print(res["memory"]["id"])
ストラテジーを 1 つ定義しただけの最小構成です。
作成しておきます。
uv run create_memory.py
MetadataFilterBlogShort1778050726-xxx
返ってきた ID(MetadataFilterBlogShort + suffix)は後続のスクリプトで MEMORY_ID として使います。
イベントにカスタムメタデータを付けて保存する
create_event の metadata 引数に、Key-Value 形式で任意の属性を付けていきます。
import boto3
from datetime import datetime, timezone
# create_memory.py の実行結果(Memory ID)に差し替えてください
MEMORY_ID = "MetadataFilterBlogShort1778050726-xxx"
# アクター(ユーザー識別子)と会話セッション識別子は任意の文字列で OK
ACTOR_ID = "blog-actor-001"
SESSION_ID = "blog-session-001"
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
events = [
("旅行の宿は和室が好きです。京都の和菓子も巡りたい。",
{"category": "travel", "destination": "kyoto", "priority": "high"}),
("出張で東京に行くときは新幹線のグリーン車を選びがちです。",
{"category": "travel", "destination": "tokyo", "priority": "medium"}),
("コーヒーは深煎りのブラックが好みです。",
{"category": "food", "topic": "coffee", "priority": "low"}),
("ホテルは静かなクラブフロアが好きで、朝食は和食派です。",
{"category": "travel", "destination": "kyoto", "priority": "high"}),
]
for text, md in events:
ev = client.create_event(
memoryId=MEMORY_ID, actorId=ACTOR_ID, sessionId=SESSION_ID,
eventTimestamp=datetime.now(timezone.utc),
payload=[{"conversational": {"content": {"text": text}, "role": "USER"}}],
metadata={k: {"stringValue": v} for k, v in md.items()},
)
print("created", ev["event"]["eventId"])
uv run create_events.py
created 0000001778050888398#6d9e2dd3
created 0000001778050889421#54c4a9dc
created 0000001778050889787#1c03c316
created 0000001778050890164#ef034a23
4 件のイベントがそれぞれ独立した eventId で作成されているのが確認できました。
metadata の値が {"stringValue": "..."} という Tagged Union 構造になっているのが少し注意です。ふつうの dict を渡すのではなく、明示的に型を指定する必要がある点だけ注意です。
短期記憶(ListEvents)で絞り込む
ListEvents の filter.eventMetadata でカスタムメタデータを使った絞り込みができるようになっています。
import boto3
# create_events.py で使ったものと同じ ID を指定する
MEMORY_ID = "MetadataFilterBlogShort1778050726-xxx"
ACTOR_ID = "blog-actor-001"
SESSION_ID = "blog-session-001"
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
res = client.list_events(
memoryId=MEMORY_ID, actorId=ACTOR_ID, sessionId=SESSION_ID,
includePayloads=True,
filter={
"eventMetadata": [
{"left": {"metadataKey": "destination"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "kyoto"}}},
]
},
)
for ev in res.get("events", []):
text = ev["payload"][0]["conversational"]["content"]["text"]
print(ev["eventId"], "/", text)
uv run filter_events.py
0000001778050890164#ef034a23 / ホテルは静かなクラブフロアが好きで、朝食は和食派です。
0000001778050888398#6d9e2dd3 / 旅行の宿は和室が好きです。京都の和菓子も巡りたい。
destination=kyoto を付けた 2 件だけが返ってきました!destination=tokyo のグリーン車のイベントや、そもそも destination を付けていないコーヒーのイベントは、ちゃんとフィルタされています。会話ログに属性を切ってサッと絞れるのは使い勝手良さそうですね。
利用可能な演算子は現時点では下記の 3 つです。
| 演算子 | 説明 |
|---|---|
EQUALS_TO |
キーと値が完全一致 |
EXISTS |
そのキーを持つイベントを返す |
NOT_EXISTS |
そのキーを持たないイベントを返す |
複数条件を入れると AND 結合になるので、「category=travel かつ priority=high」のような組み合わせも書けます。
SDK / Strands での扱い
ここまでは boto3 で検証してきました。実際のアプリケーションで使うときは、AgentCore SDK や Strands Agents 経由で動かすケースが多いと思うので、それぞれの対応状況も整理しておきます。
bedrock-agentcore-sdk-python
bedrock-agentcore-sdk-python もメタデータ周りに対応済みで、MemoryClient.create_event の metadata 引数と、MemoryClient.list_events の event_metadata 引数でそのまま使えます。
from bedrock_agentcore.memory import MemoryClient
# create_memory.py の実行結果と任意の識別子
MEMORY_ID = "MetadataFilterBlogShort1778050726-xxx"
ACTOR_ID = "blog-actor-001"
SESSION_ID = "blog-session-001"
client = MemoryClient(region_name="us-east-1")
# 書き込み: metadata を Tagged Union 形式で渡す(最大 15 件)
res = client.create_event(
memory_id=MEMORY_ID,
actor_id=ACTOR_ID,
session_id=SESSION_ID,
messages=[("旅行の宿は和室が好きです。", "USER")],
metadata={"destination": {"stringValue": "kyoto"}},
)
print("created", res["eventId"])
# 検索: event_metadata に boto3 と同じ形式のフィルタを渡す
events = client.list_events(
memory_id=MEMORY_ID,
actor_id=ACTOR_ID,
session_id=SESSION_ID,
event_metadata=[
{"left": {"metadataKey": "destination"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "kyoto"}}},
],
)
print("matched events:", len(events))
uv run agentcore_sdk_example.py
created 0000001778051209068#a0891855
matched events: 1
書き方は boto3 とほぼ同じですね。問題なく実行できました。
Strands Agents の AgentCoreMemorySessionManager
Strands Agents の AgentCoreMemorySessionManager は、AgentCore Memory をセッション永続化レイヤとして透過的に使うためのラッパーです。
サクッとMemoryを使うのに便利ですが、ユーザー定義のイベントメタデータを差し込む引数は今のところ用意されていません。
つまり SessionManager 越しに「会話のターンに priority=high のメタデータを付ける」みたいな使い方は、現状できません・・・
Strands で今回のメタデータフィルタを使いたい場合は、
- SessionManager は使わずに
bedrock-agentcore-sdk-pythonのMemoryClientを直接呼ぶ @toolでカスタムツールを書いて、Agent から呼び出してもらう
上記のような選択肢になりそうです。今回はカスタムツールとして呼び出してみましょう。
Strands カスタムツールで使う
Agent 側からメタデータ付きイベントの記録/取得ができるようにしたい場合は、@tool で薄いラッパーを書いて実装します。bedrock-agentcore-sdk-python の MemorySessionManager を使って実装してみます。
メタデータの値は [a-zA-Z0-9\s._:/=+@-]* のパターンに従う必要があり、日本語の文字列は使用できません。system_prompt で「英数字で渡してね」とお願いすることもできますが LLM の機嫌に依存するので、ここでは Pydantic モデルをツールの引数スキーマそのものとして制約を守るようなやり方にします。
from enum import Enum
from typing import Optional
from bedrock_agentcore.memory import MemorySessionManager
from bedrock_agentcore.memory.constants import ConversationalMessage, MessageRole
from bedrock_agentcore.memory.models.filters import (
EventMetadataFilter,
LeftExpression,
OperatorType,
RightExpression,
StringValue,
)
from pydantic import BaseModel, Field
from strands import tool
# ---- 値域を Enum で固定 ----
class Destination(str, Enum):
"""旅行・出張の目的地。kyoto / tokyo のみ許可。"""
KYOTO = "kyoto"
TOKYO = "tokyo"
class Category(str, Enum):
"""発話のカテゴリ。"""
TRAVEL = "travel"
FOOD = "food"
BUSINESS_TRIP = "business_trip"
class Priority(str, Enum):
HIGH = "high"
MEDIUM = "medium"
LOW = "low"
class EventMetadata(BaseModel):
"""イベントに付けるメタデータ集合。フィールドは固定、値は Enum で値域を制約する。
該当しないフィールドは None / 省略する。
"""
destination: Optional[Destination] = Field(
default=None,
description="旅行・出張の目的地。kyoto か tokyo のみ。該当しない場合は省略する。",
)
category: Optional[Category] = Field(
default=None,
description="発話のカテゴリ。travel / food / business_trip のいずれか。",
)
priority: Optional[Priority] = Field(
default=None,
description="優先度。high / medium / low のいずれか。",
)
def make_memory_tools(
manager: MemorySessionManager, actor_id: str, session_id: str
):
"""memory_id / actor_id / session_id を設定して、Agent 用のツール 2 種を返す。"""
@tool
def remember_user_event(text: str, metadata: EventMetadata) -> str:
"""ユーザー発話をメタデータ付きで AgentCore Memory に記録する。
Args:
text: 発話本文
metadata: イベントに紐付けるメタデータ。EventMetadata のスキーマに従う。
Returns:
作成された event の ID
"""
# None フィールドは AgentCore に送らない
md = {
k: StringValue.build(v)
for k, v in metadata.model_dump().items()
if v is not None
}
event = manager.add_turns(
actor_id=actor_id,
session_id=session_id,
messages=[ConversationalMessage(text=text, role=MessageRole.USER)],
metadata=md,
)
return event.get("eventId") or str(event)
@tool
def search_user_events(
metadata_key: str, metadata_value: Optional[str] = None
) -> list[dict]:
"""メタデータで過去のイベントを絞り込んで取得する。metadata_value 省略時は EXISTS で絞る。
Args:
metadata_key: 絞り込みに使うメタデータのキー
metadata_value: 値で絞る場合の値(省略時はキーの有無だけで絞る)
Returns:
マッチしたイベントのリスト({eventId, text, metadata})
"""
if metadata_value is None:
f = EventMetadataFilter.build_expression(
left_operand=LeftExpression.build(metadata_key),
operator=OperatorType.EXISTS,
)
else:
f = EventMetadataFilter.build_expression(
left_operand=LeftExpression.build(metadata_key),
operator=OperatorType.EQUALS_TO,
right_operand=RightExpression.build(metadata_value),
)
events = manager.list_events(
actor_id=actor_id,
session_id=session_id,
eventMetadata=[f],
include_payload=True,
)
out = []
for ev in events:
text = None
for p in (ev.get("payload") or []):
content = (p.get("conversational") or {}).get("content") or {}
text = content.get("text") or text
meta = ev.get("metadata") or {}
flat = {k: v.get("stringValue") if isinstance(v, dict) else v
for k, v in meta.items()}
out.append({"eventId": ev.get("eventId"), "text": text, "metadata": flat})
return out
return [remember_user_event, search_user_events]
ツールの実装を終えたら、今度はAgentの実装を行います。ここではシンプルに各種設定情報を入れて、
ツールを引数に設定します。
from bedrock_agentcore.memory import MemorySessionManager
from strands import Agent
from strands.models import BedrockModel
from strands_custom_tools import make_memory_tools
# create_memory.py の実行結果と任意の識別子
MEMORY_ID = "MetadataFilterBlogShort1778050726-xxx"
ACTOR_ID = "blog-actor-001"
SESSION_ID = "blog-session-001"
manager = MemorySessionManager(memory_id=MEMORY_ID, region_name="us-east-1")
tools = make_memory_tools(manager, actor_id=ACTOR_ID, session_id=SESSION_ID)
agent = Agent(
model=BedrockModel(
model_id="us.anthropic.claude-sonnet-4-5-20250929-v1:0",
region_name="us-east-1",
),
tools=tools,
system_prompt=(
"ユーザー発話を AgentCore Memory に記録するアシスタントです。"
"発話に応じた適切なメタデータ(category, destination, priority 等)を判断して付与してください。"
"過去の発話が必要なときは search_user_events で絞り込んでから参照してください。"
),
)
動作確認
実際に Agent に会話を送って、メタデータで絞り込めるか確認してみます。destination=kyoto を持つ会話と持たない会話を混ぜて投入し、最後に destination=kyoto で絞ってみます。
from agent_setup import agent
agent("次の発話を記録してください: 「京都の和菓子を巡るのが好きです。」")
agent("次の発話を記録してください: 「東京出張は新幹線のグリーン車が定番です。」")
agent("次の発話を記録してください: 「コーヒーは深煎りのブラックが好みです。」")
agent("次の発話を記録してください: 「京都では和室の宿が一番落ち着きます。」")
agent("destination=kyoto のメタデータが付いている発話だけを取り出してください。")
uv run run.py
最後の検索結果がこちらです。
Tool #5: search_user_events
destination=kyoto のメタデータが付いている発話を取得しました。2件見つかりました:
1. 「京都では和室の宿が一番落ち着きます。」
- カテゴリ: travel
- 目的地: kyoto
2. 「京都の和菓子を巡るのが好きです。」
- カテゴリ: food
- 目的地: kyoto
4 件中、destination=kyoto を持つ 2 件だけがちゃんと返ってきました!Strands Agent からカスタムツール経由で、メタデータフィルタが期待通りに効いていることが確認できました。
おわりに
短期記憶にメタデータ付与して、メタデータのフィルタリングを今回は試してみました!
特定のトピックを抽出したいときに便利そうですね。カテゴリはある程度こちら側で作成しておいて、生成AIには選択してもらう方が無尽蔵にカテゴリが生成されず良さそうな印象です。活用していきたいですね。
本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!
補足: メタデータの制約
公式ドキュメントを見ると、メタデータにはいくつか制約があります。
- 1 イベントあたり最大 15 個まで
- キーは 1〜128 文字
- 値は
stringValueのみ stringValueは最大 256 文字- キー・値ともに使用できる文字に制約があり、日本語などはそのまま入れられない
そのため、メタデータには kyoto、high、travel のような、検索・分類に使う英数字ベースの値を入れるのが良さそうです。










