はじめに
Amazon Bedrock AgentCore考察記事第6弾。
Amazon Bedrock AgentCore Runtime, Gateway, Identityとこれまで考察をしてきて、今回はMemoryに焦点を当てて考察をする。
今回も、Terraform AWS ProviderはAmazon Bedrock AgentCore未対応のままなので、Terraformのexternalデータソースを使って構築をしていく。externalデータソースの概要は過去の記事を参考にしていただきたい。
また、エージェントアプリと、それに接続するクライアントアプリケーションは基本的に上記記事のものをベースにする。
Amazon Bedrock AgentCore Memoryについて
Amazon Bedrock AgentCore Runtime上で動作するエージェントとの会話(というか一般的なLLMの仕様だが)は何も制御しないと、基本的に一問一答となり、過去の会話の記録(コンテキスト)は引き継がれない。
LLMに対して、messagesの属性で過去の会話の記録を渡すことでコンテキストを引き継ぐことが可能だが、一時記録領域がないと、Amazon Bedrock AgentCore Runtimeはサーバレスであるため、クライアントアプリケーションの間でコンテキストのやり取りをする必要が出てきて管理が煩雑になる。
図の方が分かりやすいため、AWS公式のDeveloper Guideのイメージを拝借しておく。
詳細はリンク先を参照していただきたい。
この煩雑さを解消してくれるのがAmazon Bedrock AgentCore Memoryだ。マネージドに会話記録を格納するためのストレージを用意してくれる。
また、ストレージには2種類ある。
- セッション単位の記録を保存するShort-term memory
- セッションを跨いで長期的な情報を保存するLong-term memory
Long-term memoryにはさらに3種類の方式があるが、今回の記事では過去の会話の要約を記録しておく「Summary」で検証を行う。
周辺リソースの構築
今回、設定変更が必要になる周辺リソースはIAMのみだ。
IAM
現在Amazon Bedrock AgentCore RuntimeにアタッチしているIAMロールに以下のポリシーを追加しよう。
最小権限構成にする場合は以下のようにする。
statement {
effect = "Allow"
actions = [
"bedrock-agentcore:CreateEvent",
"bedrock-agentcore:ListEvents",
"bedrock-agentcore:ListMemoryRecords", # Long-term memoryのみで使用
]
resources = [
data.external.bedrock_agentcore_memory.result.memory_arn,
]
}
Amazon Bedrock AgentCore Memoryの構築
Amazon Bedrock AgentCore Memoryに関連するリソースはmemoryのみだ。
※この後、eventという概念が登場するが、これはリソースという扱いではなさそう
Amazon Bedrock AgentCore Memory
なぜか、memoryのリソースは他のリソースと異なり、list_memoriesのAPIでnameを取得することができない。このため、Pythonスクリプト内では、list_memoriesで取得したmemory単位にループを組み、idを元にさらにget_memoryを呼び出して、指定された名前のリソースが存在するかをチェックしている。
namespacesの名前は何でもよいが、{actorId}と{sessionId}が無いとエラーになる。
また、何でもよいと言いつつ、Long-term memoryを検索する際に、階層で情報を取得できるため、それなりに設計はしておいた方が良い。たとえば、{actorId}より上位に{sessionId}入れてしまうと、Actorによる検索ができなくなる。
今回、Long-term memoryを指定してもしなくてもリソースを作れるようにしたかったため、「指定されなかったパラメータをcreate_memoryに渡さない」という実装をした。
**({"description": description} if description else {}),
と書いてある部分が該当の実装であるため、これが不要な場合は、この行の部分はパラメータありきにするか、行を削除して問題ない。
data "external" "bedrock_agentcore_memory" {
program = ["python3", "./bedrock_agentcore_memory.py"]
query = {
aws_region = data.aws_region.current.region
memory_name = local.bac_memory_name
description = "Example Memory"
# encryption_key_arn = ""
# memory_execution_role_arn = ""
event_expiry_duration = 7
memory_strategies = jsonencode(
[
{
summaryMemoryStrategy = {
name = "Summary"
description = "Example Summarization of conversation history."
namespaces = ["/example/{actorId}/{sessionId}"]
}
}
]
)
}
}
"""
Create Amazon Bedrock AgentCore Memory if it doesn't exist yet.
"""
import boto3
import botocore
import json
import sys
import time
def main():
params = json.load(sys.stdin)
aws_region = params["aws_region"]
memory_name = params["memory_name"]
description = None
if params.get("description"):
description = params.get("description")
encryption_key_arn = None
if params.get("encryption_key_arn"):
encryption_key_arn = params.get("encryption_key_arn")
memory_execution_role_arn = None
if params.get("memory_execution_role_arn"):
memory_execution_role_arn = params.get("memory_execution_role_arn")
event_expiry_duration = int(params["event_expiry_duration"])
memory_strategies = None
if params.get("memory_strategies"):
memory_strategies = json.loads(params["memory_strategies"])
bac = boto3.client("bedrock-agentcore-control", region_name=aws_region)
try:
paginator = bac.get_paginator("list_memories")
for page in paginator.paginate():
for memory in page.get("memories", []):
get_result = bac.get_memory(memoryId=memory.get("id"))
print(
json.dumps(
{
"memory_id": get_result["memory"]["id"],
"memory_arn": get_result["memory"]["arn"],
}
)
)
sys.exit(0)
except botocore.exceptions.ClientError as e:
sys.stderr.write(json.dumps({"error": "list_memories/get_memory() failed."}))
sys.exit(1)
except RuntimeError as e:
sys.stderr.write(json.dumps({"error": str(e)}))
sys.exit(1)
try:
create_result = bac.create_memory(
name=memory_name,
**({"description": description} if description else {}),
**({"encryptionKeyArn": encryption_key_arn} if encryption_key_arn else {}),
**({"memoryExecutionRoleArn": memory_execution_role_arn} if memory_execution_role_arn else {}),
eventExpiryDuration=event_expiry_duration,
**({"memoryStrategies": memory_strategies} if memory_strategies else {}),
)
except botocore.exceptions.ClientError as e:
sys.stderr.write(json.dumps({"error": f"create_memory() failed {str(e)}"}))
sys.exit(1)
except RuntimeError as e:
sys.stderr.write(json.dumps({"error": str(e)}))
sys.exit(1)
try:
while True:
get_result = bac.get_memory(memoryId=create_result["memory"]["id"])
if get_result["memory"]["status"] == "ACTIVE":
break
elif get_result["memory"]["status"] in ("FAILED"):
raise RuntimeError(f"invalid status {get_result["memory"]["status"]}.")
time.sleep(1)
except botocore.exceptions.ClientError as e:
sys.stderr.write(json.dumps({"error": f"get_memory() failed {str(e)}"}))
sys.exit(1)
except RuntimeError as e:
sys.stderr.write(json.dumps({"error": str(e)}))
sys.exit(1)
print(
json.dumps(
{
"memory_id": create_result["memory"]["id"],
"memory_arn": create_result["memory"]["arn"],
}
)
)
if __name__ == "__main__":
main()
Destroy用のterraform_data
今回も、externalデータソースでリソースを作成しているため、terraform destroyでリソースの自動削除がされない。
以下のようにして削除されるようにしておこう。
resource "terraform_data" "destroy_bedrock_agentcore_memory" {
input = {
aws_region = data.aws_region.current.region
memory_id = data.external.bedrock_agentcore_memory.result.memory_id
}
provisioner "local-exec" {
when = destroy
command = <<-EOT
aws bedrock-agentcore-control delete-memory \
--region ${self.input.aws_region} \
--memory-id ${self.input.memory_id}
EOT
}
}
エージェントアプリの実装
さて、これをデプロイする前に、Amazon Bedrock AgentCoreのエージェントアプリではメモリアクセス時にメモリIDが必要になるため、Dockerfileに以下の環境変数を追加しておこう。
Dockerfile
ENV AWS_BEDROCK_AGENTCORE_MEMORY_ID=<払い出したメモリID>
いちいち作るたびに変更するのが面倒な場合は、Dockerfileをテンプレート化しておいて、Terraformで作成するようにしよう。
# (前略)
ENV DOCKER_CONTAINER=1
ENV AWS_REGION=${aws_region}
ENV AWS_DEFAULT_REGION=${aws_region}
+ ENV AWS_BEDROCK_AGENTCORE_MEMORY_ID=${aws_bedrock_agentcore_memory_id}
# (後略)
resource "local_file" "agents_dockerfile" {
filename = "../script/agents/Dockerfile"
content = templatefile("${path.module}/template_file/script/agents/Dockerfile.tmpl", {
aws_region = data.aws_region.current.region
aws_bedrock_agentcore_memory_id = data.external.bedrock_agentcore_memory.result.memory_id
})
}
エージェントアプリ
今回、ちゃんと仕組みを理解したかったのでAmazon Bedrock AgentCore SDKを使わずに作った。そのため、SDKを利用するケースと比べると冗長なコードになっている。
それぞれの関数の役割
-
_add_memory_event()
Amazon Bedrock AgentCore Memoryに1つの会話履歴を追加する -
add_user_memory_event()
promptで渡す内容をmemory_event用に正規化する -
add_assistant_memory_event()
LLMが返してきたresult.messageをmemory_event用に正規化する -
get_memory()
Long-term memoryの内容をLLMのmessageに渡せるよう正規化する -
long_term_memory_tool()
LLMにtoolsとしてLong-term memoryを渡せるように設定する
create_eventのAPIで作成する会話履歴は、eventTimestampでdatetime型を渡せるため、これを使ってソートをすれば正確に会話履歴を渡すことができる……と思いきや、せっかくPythonのdatetime型はマイクロ秒精度で情報を持つことができるにもかかわらず、このeventTimestampは秒までの精度しかない(APIで取得した情報の精度を確認した)。同秒で情報を作ると、正しくソートが行えなくなってしまうのだ。
今回は、この後のモジュール呼び出し元で、LLMの呼び出し前後でuser, assistantの情報を渡すようにして、秒がずれるようにした。ただ、本当は、一貫性の観点から、LLMの呼び出しが成功したときのみuserの履歴を渡したいところである(このために1秒のsleepを入れるのはナンセンスである……)。
Actorの概念
Amazon Bedrock AgentCore Memoryの素晴らしい点として、Actorの概念によりマルチテナントの実装が簡素化されるという点だ。ActorによりNamespaceが分かれるため、テナント単位の複数のメモリを作る必要がない。今回、例として"example_user"を渡しているが、ここには、たとえば、Amazon Bedrock AgentCore Identityで取得したuser_id的なもの(一意であることが分かっていればなんでも良い)を設定することで、追加のコーディングすることなくマルチテナントを実装できる。
import boto3
import os
from datetime import datetime, timezone
from strands_tools.agent_core_memory import AgentCoreMemoryToolProvider
_bac = boto3.client("bedrock-agentcore")
def _add_memory_event(session_id: str, payload: dict):
"""
Add a memory event to the Amazon Bedrock AgentCore Memory.
"""
response = _bac.create_event(
memoryId=os.environ["AWS_BEDROCK_AGENTCORE_MEMORY_ID"],
actorId="example_user",
sessionId=session_id,
eventTimestamp=datetime.now(timezone.utc),
payload=payload,
)
def add_user_memory_event(session_id: str, user_message: str):
"""
Add a user memory event to the Amazon Bedrock AgentCore Memory.
"""
_add_memory_event(
session_id=session_id,
payload=[
{
"conversational": {
"content": {"text": user_message},
"role": "USER",
}
}
],
)
def add_assistant_memory_event(session_id: str, payload: dict):
"""
Add an assistant memory event to the Amazon Bedrock AgentCore Memory.
"""
_add_memory_event(
session_id=session_id,
payload=[
{
"conversational": {
"content": payload["content"][0],
"role": payload["role"].upper(),
}
}
],
)
def get_memory(session_id: str):
"""
Get memory events from the specified memory.
"""
payload = []
paginator = _bac.get_paginator("list_events")
for page in paginator.paginate(
memoryId=os.environ["AWS_BEDROCK_AGENTCORE_MEMORY_ID"],
sessionId=session_id,
actorId="example_user",
includePayloads=True,
maxResults=10,
):
for event in page.get("events", []):
payload.append(
{
"role": event["payload"][0]["conversational"]["role"].lower(),
"content": [event["payload"][0]["conversational"]["content"]],
"event_time_stamp": event["eventTimestamp"],
}
)
return sorted(payload, key=lambda x: x["event_time_stamp"])
def long_term_memory_tool(session_id):
return AgentCoreMemoryToolProvider(
region=os.environ["AWS_REGION"],
memory_id=os.environ["AWS_BEDROCK_AGENTCORE_MEMORY_ID"],
actor_id="example_user",
session_id=session_id,
namespace="/example/example_user",
).tools
続いて、呼び出し元である。
とはいえ、たいして難しいことはない。
今回、main()の処理はほとんど触らなくて良いようにモジュール化をしている。
# (前略)
tools = tools + long_term_memory_tool(session_id)
agent = Agent(
model=bedrock_model,
system_prompt="<任意のプロンプト>",
messages=get_memory(session_id),
tools=tools,
)
try:
user_message = payload.get("prompt", "こんにちは")
add_user_memory_event(session_id, user_message)
result = agent(user_message)
add_assistant_memory_event(session_id, result.message)
except Exception as e:
result.massage = f"agent() error: {e}"
return {"result": result.message}
いざ、動かす!
さて、それではterraform applyして、クライアントのアプリケーションを動かそう。
Input your prompt> Amazon Auroraのdb.r5.xlargeインスタンスを3日間ずっと使う場合の料金を知りたい
Agent Response: Amazon Aurora のdb.r5.xlargeインスタンスを3日間(72時間)使用した場合の料金をお答えします。
## Amazon Aurora db.r5.xlarge インスタンス料金(東京リージョン)
**基本インスタンス料金(3日間=72時間):**
### Aurora MySQL
- **標準版**: $0.70/時間 × 72時間 = **$50.40 USD**
- **IO最適化版**: $0.91/時間 × 72時間 = **$65.52 USD**
### Aurora PostgreSQL
- **標準版**: $0.70/時間 × 72時間 = **$50.40 USD**
- **IO最適化版**: $0.91/時間 × 72時間 = **$65.52 USD**
### 日本円換算(1USD=150円として計算)
- **標準版**: 約**7,560円**
- **IO最適化版**: 約**9,828円**
## 注意事項
1. **この料金はインスタンス料金のみ**です
2. **別途発生する料金**:
- Aurora ストレージ料金(使用量に応じて)
- I/O料金(IO最適化版を使用しない場合)
- バックアップストレージ料金
- データ転送料金(該当する場合)
3. **IO最適化版**は、I/O料金が含まれているため、I/O使用量が多い場合はお得になります
4. **標準版**は基本料金は安いですが、I/O料金が別途発生します
実際の総費用は、ストレージ使用量やI/O使用量によって変動しますので、具体的な使用パターンがあれば、より詳細な見積もりも可能です。
Input your prompt> もう1サイズ大きなインスタンスタイプについても教えてください
Agent Response: ## Amazon Aurora インスタンス料金比較(東京リージョン)
### db.r5.xlarge(元のサイズ)vs db.r5.2xlarge(1つ上のサイズ)
| インスタンスタイプ | エンジン | バージョン | 時間料金 | 3日間(72時間)料金 | 日本円(3日間) |
|---|---|---|---|---|---|
| **db.r5.xlarge** | Aurora MySQL | 標準版 | $0.70/時間 | **$50.40** | **約7,560円** |
| **db.r5.xlarge** | Aurora MySQL | IO最適化版 | $0.91/時間 | **$65.52** | **約9,828円** |
| **db.r5.2xlarge** | Aurora MySQL | 標準版 | $1.40/時間 | **$100.80** | **約15,120円** |
| **db.r5.2xlarge** | Aurora MySQL | IO最適化版 | $1.82/時間 | **$131.04** | **約19,656円** |
## サイズアップによる影響
### 料金の比較
- **db.r5.2xlarge は db.r5.xlarge の ちょうど2倍の料金**
- 標準版:$50.40 → $100.80(+$50.40)
- IO最適化版:$65.52 → $131.04(+$65.52)
### スペックの比較
- **vCPU**: 4コア → 8コア(2倍)
- **メモリ**: 32 GiB → 64 GiB(2倍)
- **ネットワーク性能**: 最大10 Gbps → 最大10 Gbps(同じ)
## 選択の指針
- **CPU/メモリ使用量が高い場合**: db.r5.2xlarge を検討
- **コスト重視の場合**: db.r5.xlarge で十分かを確認
- **IO使用量が多い場合**: IO最適化版を検討
※日本円は1USD=150円で計算。実際の料金にはストレージ、I/O、バックアップ料金が別途発生します。
Input your prompt>
ちゃんと1つ前の会話が引き継がれた!
さらに、一度セッションを切ってから以下のように聞いてみよう。
Input your prompt> これまでの会話って何を話してましたっけ?たぶん、このセッションの前から話していたと思うのだけど。
Agent Response: 過去の会話履歴を確認できました!記録された内容を見ると、これまでに以下のような話をしていたようですね:
## これまでの会話の概要
### 主な話題:
1. **日本語での挨拶と自己紹介**
- あなたから「こんにちは」のご挨拶
- 私がAWSファイナンシャルの専門家として自己紹介
2. **AWS料金に関するサービス提供**
- EC2インスタンスの最適化
- 月額料金の計算(USD・日本円両方)
- リージョン間の価格比較
- コスト最適化のアドバイス
3. **Amazon Aurora PostgreSQLの料金についての具体的な質問**
- db.r5.xlargeインスタンスを3日間連続使用した場合の料金
- 東京リージョンでの詳細な価格計算を提供
- 次のサイズ(db.r5.2xlarge)との比較も提供
4. **簡単な計算問題**
- 1+1+1=3の計算
- その結果に2をかけた場合(3×2=6)の計算
### 提供したサービス例:
- EC2インスタンスの推奨
- 月額使用料の詳細計算
- ストレージ・ネットワークコストの説明
- パフォーマンス要件に基づく最適化提案
今回も引き続き、AWS料金の最適化や具体的なインスタンス選択についてお手伝いできますので、何かご質問がありましたらお気軽にお申し付けください!
今回の記事を書く前に実験で聞いていた「簡単な計算問題」なんかもバッチリ残っている!
ログを見るとTool #1: agent_core_memoryの履歴があるため、たしかに設定したtoolsを使ってLong-term memoryにアクセスしたようだ。
また、CLIでもLong-term memoryに情報が格納されていることを確認可能だ。
※上記した通り、{sessionId}を指定せず、{actorId}であるexample_userまでの階層を指定することで、複数のセッションにまたがった履歴を取得することができた。
$ aws bedrock-agentcore list-memory-records --memory-id <今回作ったAmazon Bedrock AgentCore MemoryのID> --namespace /example/example_user
とすると、
{
"memoryRecordSummaries": [
{
"memoryRecordId": "mem-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"content": {
"text": "<summary>\n <topic name=\"Previous Conversation Recap\">\n The user asked in Japanese what they had been discussing previously. The assistant responded by providing a summary of their past conversations, which included: greeti
ngs and self-introductions in Japanese, AWS pricing services (EC2 instance optimization, monthly fee calculations in USD and Japanese yen, region price comparisons, and cost optimization advice), specific questions about Amazon Aurora PostgreSQL pricing (calculating costs for a db.r5.xlarge instance used for 3 days in the Tokyo region, with comparisons to db.r5.2xlarge), and some simple math calculations (1+1+1=3 and 3×2=6). The assistant identified itself as an AWS Financial specialist and offered continued assistance with AWS pricing optimization and instance selection.\n </topic>\n</summary>"
},
"memoryStrategyId": "Summary-xxxxxxxxxx",
"namespaces": [
"/example/example_user/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
],
"createdAt": "2025-08-31T13:14:34+09:00"
},
# (後続あり)
]
}
といった感じでサマライズされた情報が出力される。
これで、継続的な会話をするエージェントアプリを作れるようになったぞ!
