![[アップデート] AgentCore Memory の長期記憶で Strictly Consistent メタデータを使ってみた](https://images.ctfassets.net/ct0aopd36mqt/7M0d5bjsd0K4Et30cVFvB6/5b2095750cc8bf73f04f63ed0d4b3546/AgentCore2.png?w=3840&fm=webp)
[アップデート] AgentCore Memory の長期記憶で Strictly Consistent メタデータを使ってみた
はじめに
こんにちは、スーパーマーケットが大好きなコンサル部の神野(じんの)です。
以前、AgentCore Memory の長期記憶でカスタムメタデータフィルタを使う記事を書きました。
前回の記事では、LLM による抽出(LLM_INFERRED)を使ってイベントの会話内容からメタデータを自動抽出する方法を紹介しました。ただ、LLM による抽出はどうしても非決定的で、厳格な値を付与するのには向いていないといった扱いでした。
そこで本日のアップデートで、AgentCore Memory に Strictly Consistent メタデータが追加されました。
これはアプリケーション側から直接メタデータ値を短期記憶に指定し、LLM を介さずにそのままの値が長期記憶レコードに反映される機能です。What's New では部門スコープの検索、コンプライアンス境界、マルチテナントメモリ実装がユースケースとして挙げられています。
今回は前回の記事との違いにも触れつつ、実際に STRICTLY_CONSISTENT メタデータを試してみます!
前提
| 項目 | バージョン・値 |
|---|---|
| Python | 3.13 |
| boto3 | 1.43.29 |
| botocore | 1.43.29 |
| AWS リージョン | us-east-1 |
| 依存管理 | uv |
uv でプロジェクトを作成し、boto3 をインストールしておきます。
uv init
uv add boto3
以降のコードはすべて uv run で実行しています。
STRICTLY_CONSISTENT メタデータ
LLM_INFERRED との違い
前回の記事で紹介した LLM_INFERRED は、イベントの会話内容を LLM が解析してメタデータ値を抽出する仕組みでした。たとえば「京都に旅行に行きたい」という会話から destination = kyoto を推論する、といった具合です。
今回追加された STRICTLY_CONSISTENT は、短期記憶のイベント作成時にアプリケーション側が metadata パラメータで値を直接指定し、その値が抽出・統合プロセスを経てもそのまま長期記憶レコードに反映されます。LLM による推論を一切介さないため、短期記憶で設定した値がそのまま長期記憶に届くことが保証されます。
| 項目 | LLM_INFERRED | STRICTLY_CONSISTENT |
|---|---|---|
| 値の決定 | LLM が会話内容から推論 | アプリケーションが直接指定 |
| 決定性 | 非決定的(LLM の解釈に依存) | 決定的(指定した値がそのまま反映) |
| 設定上限 | メタデータスキーマの上限内(最大20エントリ) | 最大3キー |
| 対応する型 | STRING / STRINGLIST / NUMBER | STRING のみ |
| 統合動作 | 意味的に類似するレコードは統合されうる | 異なる値のレコードは統合対象が分離される |
| ユースケース | コンテンツの分類・トピック抽出 | 部門スコープ・コンプライアンス境界・マルチテナントメモリ |
表中の「統合動作」について少し補足します。AgentCore Memory の長期記憶には、複数のレコードを1つにまとめる統合(consolidation)プロセスがあります。たとえば同じユーザーの別セッションで「請求書の金額がおかしい」「請求の支払い方法を変更したい」という会話があった場合、意味的に近いのでこれらが1つのレコードに統合されることがあります。
STRICTLY_CONSISTENT を設定すると、この統合が値のグループ単位で分離されます。同じ department=sales を持つイベント同士は一緒に抽出・統合されますが、department=sales のレコードと department=engineering のレコードは内容が似ていても別グループとして扱われ、consolidation の対象が分離されます。
What's New ではマルチテナントメモリやコンプライアンス境界がユースケースとして挙げられていますが、公式ドキュメントのベストプラクティスでは「メタデータだけでテナント分離に頼らないこと」と注意されています。テナント分離には namespace を使い、メタデータはその中での絞り込みとして組み合わせるのが良い形ですかね。色々と深掘りしてみたいです。
対応ストラテジー
STRICTLY_CONSISTENT は以下のストラテジーで利用できます。
- Semantic Memory Strategy
- User Preference Memory Strategy
- Episodic Memory Strategy
Summarization Strategy は対応していない点に注意です。
やってみる
Memory の作成
今回は department(部門)を STRICTLY_CONSISTENT、topic(話題のカテゴリ)を LLM_INFERRED として、1つの Memory に両方の抽出タイプを設定してみます。同じ顧客が複数部門に問い合わせるカスタマーサポートシナリオを想定し、同一 namespace 内で department によるフィルタリングが機能することを確認します。
import boto3
control = boto3.client("bedrock-agentcore-control", region_name="us-east-1")
response = control.create_memory(
name="support_memory_sc",
description="Multi-department support memory with strictly consistent metadata",
eventExpiryDuration=30,
memoryStrategies=[
{
"semanticMemoryStrategy": {
"name": "semantic_strategy",
"namespaceTemplates": ["support/{actorId}/facts"],
"memoryRecordSchema": {
"metadataSchema": [
{
"key": "department",
"type": "STRING",
"extractionType": "STRICTLY_CONSISTENT",
},
{
"key": "topic",
"type": "STRING",
"extractionType": "LLM_INFERRED",
"extractionConfig": {
"llmExtractionConfig": {
"definition": "The support topic category discussed in the conversation",
"llmExtractionInstruction": "LATEST_VALUE",
"validation": {
"stringValidation": {
"allowedValues": [
"billing",
"technical",
"account",
"general",
]
}
},
}
},
},
]
},
}
}
],
indexedKeys=[
{"key": "department", "type": "STRING"},
{"key": "topic", "type": "STRING"},
],
)
memory_id = response["memoryId"]
print(f"Memory ID: {memory_id}")
department キーでは extractionType に STRICTLY_CONSISTENT を指定しています。extractionConfig は不要で、型は STRING に限定されます。一方 topic キーは従来どおり LLM_INFERRED で、definition や allowedValues を設定しています。
前回の記事では department のような決定的に決まるべき値も LLM に抽出させていたので、ここが大きな違いですね。アプリケーション側で制御すべき値とLLMに推論させたい値を明確に分離できます。
イベントの投入
同じ顧客(actorId=customer-001)が異なる部門に問い合わせるシナリオで、部門ごとに異なるメタデータ値を持つイベントを投入してみます。
import boto3
from datetime import datetime, timezone
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
def create_support_event(session_id, department, messages):
payload = []
for role, text in messages:
payload.append(
{"conversational": {"role": role, "content": {"text": text}}}
)
response = client.create_event(
memoryId=memory_id,
actorId="customer-001",
sessionId=session_id,
eventTimestamp=datetime.now(timezone.utc),
payload=payload,
metadata={
"department": {"stringValue": department},
},
)
return response
# 営業部門への問い合わせ(請求関連)
create_support_event(
session_id="session-001",
department="sales",
messages=[
("USER", "先月の請求書の金額が間違っているようです。確認してもらえますか?"),
("ASSISTANT", "承知しました。請求書番号をお教えいただけますか?"),
("USER", "INV-2026-0542 です。本来は50万円のはずが55万円になっています。"),
("ASSISTANT", "確認いたしました。差額の5万円は前月の未払い分が加算されたものです。"),
],
)
# エンジニアリング部門への問い合わせ(技術関連)
create_support_event(
session_id="session-002",
department="engineering",
messages=[
("USER", "本番環境のAPIレスポンスが急に遅くなりました。"),
("ASSISTANT", "どのエンドポイントで遅延が発生していますか?"),
("USER", "/api/v2/reports エンドポイントで、通常200msが5秒以上かかっています。"),
("ASSISTANT", "データベースのコネクションプールが枯渇している可能性があります。接続数を確認してみてください。"),
],
)
# 営業部門への別の問い合わせ(アカウント関連)
create_support_event(
session_id="session-003",
department="sales",
messages=[
("USER", "新しいアカウントを作成したいのですが、手順を教えてください。"),
("ASSISTANT", "管理画面から「新規アカウント作成」を選択し、必要事項を入力してください。承認後にメールが届きます。"),
],
)
print("Events created successfully!")
metadata パラメータの department キーに stringValue で値を直接渡しています。この値が STRICTLY_CONSISTENT として、LLM の推論を介さずにそのまま長期記憶レコードに反映されます。
一方で topic は会話内容から LLM が自動的に推論してくれます。請求の話なら billing、API遅延の話なら technical、アカウント作成の話なら account といった具合ですね。
長期記憶レコードの確認
イベント投入後、しばらく待つと抽出ジョブが走り、長期記憶レコードが生成されます。すべて同じ namespace 内のレコードを確認してみましょう。
import boto3
import json
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
response = client.list_memory_records(
memoryId=memory_id,
namespace="support/customer-001/facts",
)
for record in response.get("memoryRecordSummaries", []):
metadata = record.get("metadata", {})
dept = metadata.get("department", {}).get("stringValue", "N/A")
topic = metadata.get("topic", {}).get("stringValue", "N/A")
print(f"Content: {record['content']}")
print(f"department={dept}, topic={topic}")
print("---")
Content: {'text': '本番環境の /api/v2/reports エンドポイントのAPIレスポンスが急に遅くなり、通常200msのところが5秒以上かかっている。'}
department=engineering, topic=technical
---
Content: {'text': '2026年5月の請求書(INV-2026-0542)の金額が50万円のはずが55万円になっており、差額の5万円は前月の未払い分が加算されたものであると説明された。'}
department=sales, topic=billing
---
Content: {'text': '請求書INV-2026-0542の本来の金額は50万円であると認識していた。'}
department=sales, topic=billing
---
Content: {'text': 'ユーザーは新しいアカウントを作成したいと考えている。'}
department=sales, topic=account
---
同一 namespace 内に department=sales と department=engineering のレコードが共存していますね! department はアプリケーションが渡した値がそのまま反映され、topic は LLM が会話内容から正しく推論しています。
前回の記事では、こういった決定的に決まるべき値(部門名やテナント属性など)も LLM に抽出させていたため、値が揺れたり意図しない値になるリスクがありました。STRICTLY_CONSISTENT ならアプリケーションが指定した値がそのまま入るので安心です。
メタデータフィルタで検索
STRICTLY_CONSISTENT メタデータもフィルタとして利用できます。同一 namespace 内で部門を絞り込んで検索してみます。
import boto3
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
namespace = "support/customer-001/facts"
# 営業部門のレコードのみ検索
response = client.retrieve_memory_records(
memoryId=memory_id,
namespace=namespace,
searchCriteria={
"searchQuery": "問い合わせ内容",
"topK": 10,
"metadataFilters": [
{
"left": {"metadataKey": "department"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "sales"}},
}
],
},
)
print("=== department=sales でフィルタ ===")
for record in response.get("memoryRecordSummaries", []):
metadata = record.get("metadata", {})
dept = metadata.get("department", {}).get("stringValue", "N/A")
topic = metadata.get("topic", {}).get("stringValue", "N/A")
print(f"Score: {record['score']}")
print(f"Content: {record['content']}")
print(f"department={dept}, topic={topic}")
print("---")
# エンジニアリング部門のレコードのみ検索(同じ namespace を指定)
response = client.retrieve_memory_records(
memoryId=memory_id,
namespace=namespace,
searchCriteria={
"searchQuery": "問い合わせ内容",
"topK": 10,
"metadataFilters": [
{
"left": {"metadataKey": "department"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "engineering"}},
}
],
},
)
print("=== department=engineering でフィルタ ===")
for record in response.get("memoryRecordSummaries", []):
metadata = record.get("metadata", {})
dept = metadata.get("department", {}).get("stringValue", "N/A")
topic = metadata.get("topic", {}).get("stringValue", "N/A")
print(f"Score: {record['score']}")
print(f"Content: {record['content']}")
print(f"department={dept}, topic={topic}")
print("---")
metadataFilters は searchCriteria の中に指定します。ListMemoryRecords ではトップレベルですが、RetrieveMemoryRecords では searchCriteria 内に配置する点に注意してください。
同じ namespace を指定しているにもかかわらず、department の値でレコードが正しく絞り込まれます。実際の実行結果は以下のとおりです。
=== department=sales でフィルタ ===
Score: 0.3785945
Content: {'text': '請求書INV-2026-0542の本来の金額は50万円であると認識していた。'}
department=sales, topic=billing
Score: 0.3695521
Content: {'text': '2026年5月の請求書(INV-2026-0542)の金額が50万円のはずが55万円になっており、差額の5万円は前月の未払い分が加算されたものであると説明された。'}
department=sales, topic=billing
Score: 0.36409447
Content: {'text': 'ユーザーは新しいアカウントを作成したいと考えている。'}
department=sales, topic=account
=== department=engineering でフィルタ ===
Score: 0.35761523
Content: {'text': '本番環境の /api/v2/reports エンドポイントのAPIレスポンスが急に遅くなり、通常200msのところが5秒以上かかっている。'}
department=engineering, topic=technical
同一 namespace 内の4件のレコードが、department の値で正しく分離されて返ってきていますね! sales でフィルタすると billing と account のレコード3件、engineering でフィルタすると technical のレコード1件だけが返ります。
両方の抽出タイプを組み合わせたフィルタ
STRICTLY_CONSISTENT と LLM_INFERRED のメタデータを組み合わせてフィルタすることもできます。
import boto3
import json
client = boto3.client("bedrock-agentcore", region_name="us-east-1")
memory_id = "YOUR_MEMORY_ID"
# 営業部門 かつ billing トピックのレコードを検索
response = client.retrieve_memory_records(
memoryId=memory_id,
namespace="support/customer-001/facts",
searchCriteria={
"searchQuery": "問い合わせ内容",
"topK": 10,
"metadataFilters": [
{
"left": {"metadataKey": "department"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "sales"}},
},
{
"left": {"metadataKey": "topic"},
"operator": "EQUALS_TO",
"right": {"metadataValue": {"stringValue": "billing"}},
},
],
},
)
print("=== 営業部門 × billing トピック ===")
for record in response.get("memoryRecordSummaries", []):
print(f"Content: {record['content']}")
print(f"Metadata: {json.dumps(record.get('metadata', {}), ensure_ascii=False)}")
print("---")
department は確実にアプリケーションが指定した値でフィルタでき、topic は LLM が会話内容から判断した分類でフィルタできます。決定的に制御したい軸とLLMに任せたい軸を組み合わせられるのはいいですね。
前回記事との比較
前回の記事で紹介した LLM_INFERRED のみのアプローチと、今回の STRICTLY_CONSISTENT を使うアプローチを比較してみます。
前回の課題と今回の解決
前回の記事では、LLM 抽出で以下の課題がありました。
- allowedValues を絞りすぎると、関係ない会話にも無理やり許可値から選んでしまう
- validation なしだと表記揺れが起きる(tokyo / Tokyo / 東京 など)
- 部門コードのような決定的に決まるべき値も LLM に任せることになる
STRICTLY_CONSISTENT はこれらの課題のうち、特に3番目を解決します。アプリケーションが値を直接制御するので、LLM の気まぐれに左右されません。
使い分けの指針
| 値の性質 | 推奨する extractionType |
|---|---|
| 部門コード、テナント属性、コンプライアンスレベル | STRICTLY_CONSISTENT |
| リージョン、環境名(prod/stg/dev) | STRICTLY_CONSISTENT |
| 会話のトピック、カテゴリ | LLM_INFERRED |
| ユーザーの感情、満足度 | LLM_INFERRED |
ルールとしてはシンプルで、アプリケーション側で値が確定しているものは STRICTLY_CONSISTENT、会話内容から推論が必要なものは LLM_INFERRED を使います。
Memory 作成時の設定比較
前回の記事では department 相当の値もすべて LLM 抽出で設定していました。
{
"key": "department",
"type": "STRING",
- "extractionConfig": {
- "llmExtractionConfig": {
- "definition": "The department this conversation belongs to",
- "llmExtractionInstruction": "LATEST_VALUE",
- "validation": {
- "stringValidation": {
- "allowedValues": ["sales", "engineering", "hr", "unknown"]
- }
- },
- }
- },
+ "extractionType": "STRICTLY_CONSISTENT",
},
STRICTLY_CONSISTENT にすると extractionConfig が不要になり、設定もシンプルになりますね。LLM に「部門を推論して」とお願いする必要がなくなり、代わりにイベント投入時の metadata パラメータで直接値を渡します。
制約事項
STRICTLY_CONSISTENT にはいくつかの制約があります。
| 制約 | 詳細 |
|---|---|
| ストラテジーあたりの最大キー数 | 3 |
| 対応する型 | STRING のみ |
| indexedKeys への宣言 | 必須 |
| extractionConfig | 指定不可(値はイベントから取得) |
| 対応ストラテジー | Semantic / User Preference / Episodic(カスタムオーバーライド含む)。Summary は非対応 |
| 値が未指定の場合 | そのキーはレコードから省略される |
3キーという上限があるので、本当に決定的な制御が必要なキーに絞って使う必要があります。それ以外の分類やカテゴリ付けは LLM_INFERRED に任せるのが良さそうです。
公式ドキュメントに制約やクォータの詳細が記載されています。
おわりに
部門コードやコンプライアンスレベルのような決定的に決まるべき値が LLM の推論結果に依存するのはちょっとリスクがあるので、アプリケーション側から短期記憶に直接値を設定し、そのまま長期記憶に連携できるようになったのはよいですね。What's New でもマルチテナントメモリやコンプライアンス境界がユースケースとして挙げられているとおり、namespace による主体の分離と STRICTLY_CONSISTENT メタデータによるスコーピングを組み合わせることで、より実用的なメモリ設計ができるようになりました。
本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!






