👁

AgentCore Runtime SDKを理解しよう

この章では、Amazon Bedrock AgentCore Runtime上にStrands Agents SDKでAIエージェントを実装しデプロイする流れを学び、ハンズオン形式で学習します。これにより、ツール定義・システムプロンプト・モデルパラメータ・会話履歴・ストリーミング応答・エラーハンドリングなど、Strands Agents SDKでエージェントを組み立てる基本が身につきます。

1. 本章の概要

1.1 本章の目的

前章はコンソール操作でテンプレートRuntimeを扱っただけで、独自ロジックは載せていません。本章では、Strands Agents SDK でエージェントコードを書き、AgentCore Runtime にデプロイする流れを扱います。SDKの中心的な要素(ツール定義・モデルパラメータ・会話履歴・ストリーミング)を1つずつ体験することで、以降の応用ハンズオンで自作エージェントを組み立てる土台を作ります。

1.2 本章で学ぶ内容

Strands Agents SDKでのツール定義(@tool + 型ヒント + docstring)、Agentクラスによるエージェントの組み立て、モデルパラメータ(temperature / max_tokens / top_p)の使い方を扱います。あわせて、会話履歴とマルチターン対話(同一 session-id での履歴保持)、ストリーミング応答の書き方、ツール内での例外を安全に扱うエラーハンドリングも押さえます。

1.3 ハンズオンの流れ

AgentCore CLIとStrands Agents SDKで、メール署名を作成するシンプルなAIエージェントを1つ作ってRuntimeへデプロイします。その上でツール定義・システムプロンプト・モデルパラメータ・会話履歴・ストリーミング応答・エラーハンドリングを、コードを書き換えながら1つずつ試します。

1.4 事前準備

必要なツール

この章では、以下のツールを使用します。まだインストールしていない場合は、リンク先の手順に沿って準備をお願いします。

ツール名 関連箇所 理由
AWS CLI AWS CLIのインストール AWSへの認証情報の設定に使用する
Python Pythonのインストール Strandsエージェントのコードを記述・実行するために使用する(Python 3.10以上)
Visual Studio Code Visual Studio Codeのインストール Strandsエージェントのコードを編集するエディタとして使用する
Node.js Node.jsのインストール AgentCore CLI(npm経由で配布)を実行するために使用する
AgentCore CLI AgentCore CLIのインストール Runtimeプロジェクトの作成・ローカル動作確認・デプロイに使用する

必要なアカウント

この章では、以下のアカウントを使用します。まだ用意していない場合は、リンク先の手順に沿って準備をお願いします。

アカウント名 関連箇所 理由
AWSアカウント AWSアカウントの作成 AgentCore Runtimeにエージェントをデプロイする環境として使用する

2. 簡単なAIエージェントの作成

まずは、Runtimeにデプロイして呼び出せる「動くベース」を作ります。メール署名を作成するツールcreate_signature)を1つ持ったシンプルなエージェントを、agentcore create でスケルトンを起こし、ローカル動作確認→Runtimeへのデプロイまで通します。

2.1 Strands Agents SDKとは

Strands Agents SDKは、AWSが公開しているPython向けのエージェントフレームワークです。ツール定義とエージェントの初期化を簡潔に書けるため、Converseのツール呼び出しループを自前で回すコードよりも少ない行数でエージェントを構築できます。Strands自体はAgentCoreに閉じたものではなく、ローカルでも動かせます。

なお、フレームワークとSDKの最新の使い方は AWS Docs — Bedrock AgentCore(AWS公式ドキュメント)Strands Agents(GitHub) に記載があります。

2.2 Runtimeプロジェクトの作成

この節では VSCode でフォルダを開き、AgentCore CLI(内部で Node.js を利用)で agentcore create を実行します。まず、それぞれがインストール済みかをターミナルから確認します。

code --version

以下のように 1.x.x のバージョン番号を含む3行が表示されれば、Visual Studio Code のインストールは確認できています。

1.x.x
xxxxxxxxxxxxxxxx
arm64
node --version

以下のように v20.x.x 以降のバージョン番号が表示されれば、Node.js のインストールは確認できています。

v20.x.x
agentcore --version

以下のように @aws/agentcore/x.x.x の形式でバージョン番号が表示されれば、AgentCore CLI のインストールは確認できています。

@aws/agentcore/x.x.x

表示されないツールがあれば、事前準備の Visual Studio Codeのインストール / Node.jsのインストール / AgentCore CLIのインストール から準備してください。

agentcore createコマンドで、Runtimeプロジェクトのフォルダ構造とスケルトンコードを対話ウィザードから作成します。まず作業用の親フォルダを準備します。

任意の場所に agentcore-runtime-handson フォルダを作成し、Visual Studio Codeの「ファイル」→「フォルダーを開く」から、作成した agentcore-runtime-handson フォルダを開きます。以降の操作は、Visual Studio Codeのターミナルから行います。

agentcore-runtime-handson  ← このフォルダを作成

ターミナルから対話ウィザードを起動します。

agentcore create

ウィザードが順に質問してくるので、以下のように選択・入力します。Project nameWhat would you like to build? を答えると、続けて Add Agent の対話が始まり、Agent name 以降を順に選んでいきます。

質問 選択・入力する値 設定の基準
Project name MyAgent ハンズオン用に分かりやすい名前を付ける
What would you like to build? Agent Runtimeにコードベースのエージェントをデプロイする(Harness はコード不要のマネージド構成、Skip は後からリソースを追加する用途)
Agent name MyAgent プロジェクトと同じ名前で識別しやすくする
Select agent type Create new agent 雛形からエージェントを新規作成する(既存コードの持ち込みや Bedrock Agents からの取り込みではない)
Language Python Strands Agents SDK を使って Python で実装する
Build Direct Code Deploy Docker不要でzipをそのままデプロイできる方式を使う
Protocol HTTP 標準のHTTPエージェントとして動かす(MCPA2AAG-UI は特定プロトコル向け)
Framework Strands Agents SDK 短いコードでツール呼び出しループを書ける推奨フレームワーク
Model Amazon Bedrock Bedrockで提供されるAnthropic Claudeを使う
Memory None 今回は会話履歴のマネージド保存は使わない
Customize advanced settings 何も選択せずEnter VPC接続やカスタム認証など高度な設定は今回は使わない
Review Configuration Y または Enter 表示された内容で確定する

確定すると、以下のようなフォルダ構造でプロジェクトが生成されます。

agentcore-runtime-handson/
└── MyAgent/
    ├── agentcore/
    │   ├── agentcore.json    # プロジェクトとリソースの設定
    │   ├── aws-targets.json  # デプロイ先のAWSアカウント・リージョン
    │   └── cdk/              # CDKインフラ(自動管理)
    └── app/
        └── MyAgent/
            ├── main.py       # エージェントのエントリポイント
            ├── pyproject.toml # Python依存関係
            └── ...

MyAgent/フォルダが生成されていれば、プロジェクトの作成は完了です。

2.3 エージェントコードの記述

生成されたapp/MyAgent/main.pyを、メール署名生成ツールを1つ持った Strands ベースの実装に差し替えます。LambdaによるBedrockの応用操作を身につけようcreate_signature で作ったものと同じ内容を、Strands で書き直す形になります。

完成イメージ

できあがるのは、ユーザから氏名・部署・役職・メールアドレスを受け取り、会社名と代表電話番号を付けた署名テキストを返すエージェントです。たとえば以下のプロンプトを受け取ると、

田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って

次のような署名を返します。

━━━━━━━━━━━━━━━━━━━━━━
【田中太郎】DevOps株式会社 開発部 エンジニア
Email: tanaka@example.com
Tel:   03-1234-5678
━━━━━━━━━━━━━━━━━━━━━━

main.py の書き換え

Visual Studio Codeのエクスプローラーでapp/MyAgent/main.pyを開き、以下の内容で置き換えて保存します。

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel

COMPANY_NAME = "DevOps株式会社"
COMPANY_TEL = "03-1234-5678"
MODEL_ID = "<モデルID>"  # ap-northeast-1で利用可能なClaude Sonnetの推論プロファイルID(例: jp.anthropic.claude-sonnet-4-6)に変更してください

app = BedrockAgentCoreApp()


@tool
def create_signature(name: str, department: str, position: str, email: str) -> str:
    """氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は自動で付与されます。"""
    return (
        "━━━━━━━━━━━━━━━━━━━━━━\n"
        f"【{name}{COMPANY_NAME} {department} {position}\n"
        f"Email: {email}\n"
        f"Tel:   {COMPANY_TEL}\n"
        "━━━━━━━━━━━━━━━━━━━━━━"
    )


agent = Agent(
    model=BedrockModel(model_id=MODEL_ID, region_name="ap-northeast-1"),
    tools=[create_signature],
    system_prompt="あなたはメール署名を作成するアシスタントです。ユーザから受け取った情報をもとに create_signature ツールを呼び出し、結果をそのまま提示してください。",
)


@app.entrypoint
def invoke(payload):
    prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
    result = agent(prompt)
    return {"result": str(result)}


if __name__ == "__main__":
    app.run()

各部の解説

コードを解説します。

COMPANY_NAME = "DevOps株式会社"
COMPANY_TEL = "03-1234-5678"

会社名と代表電話番号は、ユーザからの入力ではなく Runtime 側で持っている固定値として宣言しています。応答にこれらの値が含まれていれば「ツールが実際に呼ばれた証拠」として判断できます。

MODEL_ID = "<モデルID>"  # ap-northeast-1で利用可能なClaude Sonnetの推論プロファイルID(例: jp.anthropic.claude-sonnet-4-6)に変更してください

後述の Agent に渡すClaude モデルの推論プロファイルIDです。プレースホルダの <モデルID> を、実際に Bedrock で有効化しているモデルIDに書き換えてから保存します。

app = BedrockAgentCoreApp()

BedrockAgentCoreApp() は、Runtime が要求する /invocations/ping の HTTP エンドポイントを自動で提供する SDK のクラスで、app として初期化しておきます。

@tool
def create_signature(name: str, department: str, position: str, email: str) -> str:
    """氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は自動で付与されます。"""

@tool デコレータで関数をツールとして登録します。関数の docstring がモデルに提示される description になるため、モデルがツールをいつ使うかの判断材料になります。

    return (
        "━━━━━━━━━━━━━━━━━━━━━━\n"
        f"【{name}】{COMPANY_NAME} {department} {position}\n"
        f"Email: {email}\n"
        f"Tel:   {COMPANY_TEL}\n"
        "━━━━━━━━━━━━━━━━━━━━━━"
    )

ツール本体は、受け取った引数と、冒頭で定義した COMPANY_NAMECOMPANY_TEL を組み合わせて、装飾線付きの署名テキストを返します。

agent = Agent(
    model=BedrockModel(model_id=MODEL_ID, region_name="ap-northeast-1"),
    tools=[create_signature],
    system_prompt="あなたはメール署名を作成するアシスタントです。...",
)

Agent クラスにモデル・ツール一覧・システムプロンプトを渡すだけで、エージェントが組み立てられます。

@app.entrypoint
def invoke(payload):
    prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
    result = agent(prompt)
    return {"result": str(result)}

@app.entrypoint デコレータで Runtime から呼び出されるエントリポイントを宣言しています。payloadpromptagent(prompt) に渡すと、Strands が内部でモデル呼び出しとツール呼び出しのループを回してくれます。

2.4 ローカルでの動作確認

agentcore devは、ローカル用のPython仮想環境を自動作成し、pyproject.tomlで宣言された依存パッケージをインストールし、ローカルサーバを起動して、ブラウザでAgent Inspectorを開きます。

ローカルサーバ上のエージェントは Bedrock のモデルを呼び出すため、事前に AWS 認証情報がどの AWS アカウントと紐付いているかを設定しておく必要があります。事前準備の AWS CLIのインストールaws configure を済ませておき、~/.aws/credentials に対象アカウントの認証情報が保存されている状態にしておきます。この認証情報は続く agentcore deploy でも同じものが使われます。

念のため、Python(agentcore dev が仮想環境作成に使用)と AWS CLI がインストール済みかを確認します。

Windowsの場合:

python --version

以下のように Python 3.x.x のバージョン番号が表示されれば、Python のインストールは確認できています。

Python 3.x.x
aws --version

以下のように aws-cli/2.x.x から始まるバージョン情報が表示されれば、AWS CLI のインストールは確認できています。

aws-cli/2.x.x Python/3.x.x Windows/xx source/exe/AMD64

Macの場合:

python3 --version

以下のように Python 3.x.x のバージョン番号が表示されれば、Python のインストールは確認できています。

Python 3.x.x
aws --version

以下のように aws-cli/2.x.x から始まるバージョン情報が表示されれば、AWS CLI のインストールは確認できています。

aws-cli/2.x.x Python/3.x.x Darwin/xx.x.x source/arm64

プロジェクトフォルダ内で実行する必要があるため、MyAgentフォルダに移動してから起動します。

cd MyAgent
agentcore dev

初回はPython venv作成と依存インストールで数十秒〜数分かかります。完了するとブラウザで Agent Inspector が開き、以下のような画面が表示されます。

チャット欄に以下の質問を入力して、右側の紙飛行機アイコンで送信します。

田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って

エージェントが create_signature ツールを呼び出し、応答に署名が含まれて返ってくれば、ローカルで動く状態です。

create_signature ツール自体は、装飾線と改行で整形された以下のような文字列を返しています。

━━━━━━━━━━━━━━━━━━━━━━
【田中太郎】DevOps株式会社 開発部 エンジニア
Email: tanaka@example.com
Tel:   03-1234-5678
━━━━━━━━━━━━━━━━━━━━━━

一方、Agent Inspector のチャット表示は本文をマークダウンとしてレンダリングするため、行末のシングル \n が改行として反映されず、署名部分が1行に流れて見えます。またモデル側が「田中太郎さんのメール署名が完成しました!」の前置きや「ご確認ください!」の後書きを付けているため、ツールの戻り値がそのままの形で表示されているわけではありません。前置き・後書きの問題は、この後のシステムプロンプトのセクションで整えていきます。

会社名(DevOps株式会社)と代表電話番号(03-1234-5678)はユーザから入力していないのに応答に含まれているため、これが「モデルが自力で生成した文字列ではなく、Runtime 上の create_signature ツールを実際に呼び出して結果を受け取っている」証拠になります。応答下の「1 tool used」バッジからも、ツールが1回呼ばれたことが確認できます。Agent InspectorのTracesタブを開くと、モデル呼び出しとツール呼び出しの流れがタイムライン形式で確認できます。

このあとの各セクションでも agentcore dev は起動したままにしておきます。コードを書き換えて保存するたびに、Agent Inspectorから同じセッションで動作確認できます。

⚠️ ブラウザが自動で開かない場合
agentcore devの出力に表示されるURL(既定はhttp://localhost:8080)を手動でブラウザで開いてください。ポートが既に使用中の場合は、agentcore dev --port 8090のように別ポートを指定できます。
⚠️ 「The provided model identifier is invalid」エラーが出る場合
MODEL_ID に指定した値が、Bedrock で有効なモデル識別子として認識されていない状態です。マネジメントコンソールで ap-northeast-1(東京)の Bedrock を開き、「モデルアクセス」で使いたい Claude Sonnet モデルへのアクセスが付与されていること、および「推論プロファイル」に表示される完全形のID(jp. または apac. で始まり、末尾に -YYYYMMDD-v1:0 が付いた形式)を確認します。そのIDを main.pyMODEL_ID に貼り直して保存し、agentcore dev を再起動してください。

2.5 AgentCore Runtimeへのデプロイ

ローカルで動くことが確認できたら、AgentCore Runtimeへデプロイします。agentcore deployはコードをzipにパッケージング、S3へアップロード、AgentCore Runtimeへ反映、CloudWatchのロギング設定まで、CDK経由でまとめて実行します。Dockerでの手動ビルドやECRの管理は不要です。

agentcore dev は起動したままにしておき、Visual Studio Codeで 別のターミナル を新規に開きます(ターミナルパネル右上の「+」ボタン、または Ctrl+Shift+@)。新しいターミナルで MyAgent フォルダに移動してから、agentcore deploy を実行します。

cd MyAgent
agentcore deploy

初回はCDK bootstrap が走るため数分かかります。全ステップに [done] が付き、以下のように ✓ Deploy to AWS Complete が表示されればデプロイ成功です。

マネジメントコンソールのAgentCore画面を東京リージョンで開き、左メニューの「ランタイム」を選択すると、MyAgent_MyAgent が「準備完了」ステータスで表示されています。

ランタイム名(MyAgent_MyAgent)をクリックすると詳細画面が開き、ランタイム ARNarn:aws:bedrock-agentcore:ap-northeast-1:<AWSアカウントID>:runtime/MyAgent_MyAgent-<ランダム文字列> の形式)と DEFAULT エンドポイントが確認できます。次のセクションでマネジメントコンソールや AgentCore CLI からエージェントを呼び出すときは、内部でこの ARN + エンドポイントへリクエストが届く形になります。

💡 ポイント
Direct Code Deployment(CodeZip)のパッケージには250MB(zip後)/750MB(展開後)の上限があります。上限を超える場合や、独自のシステム依存が必要な場合は、agentcore createのBuild typeでContainerを選ぶか、次章のコンテナデプロイを検討してください。上限は Direct code deployment for Python(AWS公式ドキュメント) に「The maximum size for a .zip deployment package for AgentCore Runtime is 250 MB (zipped) and 750 MB (unzipped).」と記載があります。
⚠️ 「Bootstrap AWS environment failed」エラーが出る場合
この AWS アカウントで過去に CDK を使ったことがあり、CDKToolkit スタックの状態が不整合になっている場合に発生します(詳細エラーは MyAgent/agentcore/.cli/logs/deploy/deploy-*.log に出力されます)。新しいターミナルで、標準の CDK CLI から手動で bootstrap を実行すると解消できるケースが多いです。agentcore deploy の出力に表示される Target: ap-northeast-1:<AWSアカウントID> の数字を控えたうえで、npx cdk bootstrap aws://<AWSアカウントID>/ap-northeast-1 を実行してください。✅ Environment aws://... bootstrapped. と表示されたら、元のターミナルに戻って agentcore deploy を再実行します。

2.6 デプロイしたエージェントの動作確認

デプロイしたエージェントは、マネジメントコンソールのテストUIからも、AgentCore CLI からも呼び出せます。両方を試して、動作確認のパターンを押さえます。

AWSマネジメントコンソールからの確認

先ほどのランタイム詳細画面の 右上「テスト」ボタン をクリックすると、コンソール上のテストUIが開きます。

テスト画面で ランタイムエージェントMyAgent_MyAgentエンドポイントDEFAULT が選択されていることを確認し、入力 欄に以下のJSONを貼り付けて「実行」をクリックします。

{"prompt": "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って"}

出力 欄に {"result": "..."} の形式で、装飾線付きの署名テキストを含む JSON が返れば、AgentCore Runtime 上でエージェントが動作している状態です。

CLIからの確認

同じエージェントを AgentCore CLI からも呼び出してみます。agentcore deploy を実行したターミナル(MyAgent フォルダにいる状態)で、以下を実行します。

agentcore invoke --prompt "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って"

以下のような実行結果が表示されます。

田中太郎さんのメール署名が完成しました!以下をご利用ください。

---

```
━━━━━━━━━━━━━━━━━━━━━━
【田中太郎】DevOps株式会社 開発部 エンジニア
Email: tanaka@example.com
Tel:   03-1234-5678
━━━━━━━━━━━━━━━━━━━━━━
```

---

会社名(DevOps株式会社)と代表電話番号(03-1234-5678)は自動で付与されています。何かご不明な点があればお気軽にどうぞ!


Session: 42560264-c9db-45c5-9237-e3fd45c1e4c7
To resume: agentcore invoke --session-id 42560264-c9db-45c5-9237-e3fd45c1e4c7
Log: <MyAgentプロジェクトのパス>/agentcore/.cli/logs/invoke/invoke-MyAgent-<YYYYMMDD-HHMMSS>.log

会社名と代表電話番号が入った署名が返れば、AgentCore Runtime 上でエージェントが動作している状態です。マネジメントコンソールで対象のエージェントの Observability 画面を開くと、ツール呼び出し・レイテンシ・トークン数などのトレース情報が確認できます。

ここまでで「動くベース」の作成は完了です。ここからは、実務で AI エージェントを設計・実装する際に必ず押さえておきたい SDK の基本文法を、これまでの main.py を土台にしながら6つのセクションで「概要 → コードを書き換え → コードの解説 → 動作確認」の4ステップで1つずつ体感します。1つ目のターミナルでは引き続き agentcore dev が起動しているため、コードを書き換えて保存するたびに Agent Inspector から動作確認できます。

3. ツールの定義と説明文

3.1 概要

エージェントに「メール署名を作る」「天気を取得する」といった具体的な能力を持たせたいとき、Python 関数として実装したツールをエージェントに渡します。ただし、モデルは Python コードを直接読むわけではなく、人間が書いた説明文を通してしか「このツールは何をするもので、いつ呼び出すべきか」を判断できません。説明が曖昧だと、必要なときに呼ばれなかったり、間違った引数で呼ばれたりします。

Strands の @tool デコレータと、関数の型ヒントdocstring を組み合わせて使えば、Python の関数定義そのままがツールの入力スキーマとモデル向けの説明文としてモデルに伝わります。docstring の1文目に「いつ使うべきか」を、Args: セクションに各引数の意味と例を書くと、モデルのツール選択と引数抽出の精度が安定します。

現状の create_signature の docstring は最低限の1行だけなので、「いつ使うべきか」の観点と各引数の意味を補うようにアップデートしてみます。

3.2 コードの書き換え

main.py を、以下の内容で全体を置き換えて保存します。前セクションからの変更は、create_signature の docstring のみです。

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel

COMPANY_NAME = "DevOps株式会社"
COMPANY_TEL = "03-1234-5678"
MODEL_ID = "<モデルID>"  # ap-northeast-1で利用可能なClaude Sonnetの推論プロファイルID(例: jp.anthropic.claude-sonnet-4-6)に変更してください

app = BedrockAgentCoreApp()


@tool
def create_signature(name: str, department: str, position: str, email: str) -> str:
    """ユーザからメール署名の作成を依頼されたときに使ってください。氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は Runtime 側で自動的に付与されるため、ユーザに問い合わせる必要はありません。

    Args:
        name: 氏名(例: '田中太郎')
        department: 部署名(例: '開発部')
        position: 役職(例: 'エンジニア')
        email: メールアドレス(例: 'tanaka@example.com')
    """
    return (
        "━━━━━━━━━━━━━━━━━━━━━━\n"
        f"【{name}{COMPANY_NAME} {department} {position}\n"
        f"Email: {email}\n"
        f"Tel:   {COMPANY_TEL}\n"
        "━━━━━━━━━━━━━━━━━━━━━━"
    )


agent = Agent(
    model=BedrockModel(model_id=MODEL_ID, region_name="ap-northeast-1"),
    tools=[create_signature],
    system_prompt="あなたはメール署名を作成するアシスタントです。ユーザから受け取った情報をもとに create_signature ツールを呼び出し、結果をそのまま提示してください。",
)


@app.entrypoint
def invoke(payload):
    prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
    result = agent(prompt)
    return {"result": str(result)}


if __name__ == "__main__":
    app.run()

3.3 コードの解説

コードを解説します。前セクションからの変更点は create_signature の docstring の1箇所のみなので、その部分を抜粋して見ていきます。

    """ユーザからメール署名の作成を依頼されたときに使ってください。氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は Runtime 側で自動的に付与されるため、ユーザに問い合わせる必要はありません。
    """

docstring の1文目に「ユーザからメール署名の作成を依頼されたときに使ってください」と、ツールを呼ぶべきシーンを明示しました。これにより、モデルは「メール署名を作って」というユーザ発話に対して、迷わず create_signature を選ぶようになります。「会社名と代表電話番号は Runtime 側で自動的に付与されるため、ユーザに問い合わせる必要はありません」と書き添えることで、モデルが「会社名も聞いた方がいいですか?」といった余計な確認質問を返すのも防げます。

    Args:
        name: 氏名(例: '田中太郎')
        department: 部署名(例: '開発部')
        position: 役職(例: 'エンジニア')
        email: メールアドレス(例: 'tanaka@example.com')

Args: セクションには各引数の意味と実例を並べました。特にメールアドレスのような形式が決まっている値では、'tanaka@example.com' のような例を1つ示すことで、モデルがユーザ入力から引数を抽出する際の判断精度が安定します。

3.4 動作確認

agentcore dev は起動したままなので、main.py を保存するとローカルの Python 仮想環境上で自動的に反映されます。Agent Inspectorのチャット欄に、以下の質問を送ってみます。

鈴木花子(営業部 マネージャー、suzuki@example.com)の署名を作成してください

前回と同じ形式の署名が、鈴木花子の情報で組み立てられて返れば、ツール呼び出しが正しく機能しています。Tracesタブで create_signature の引数(name='鈴木花子', department='営業部', position='マネージャー', email='suzuki@example.com')が正しく渡されていることも確認できます。

なお、今回のように 入力の4項目が揃っていて・ツールが1つしかない シンプルなケースでは、前セクションの最小 docstring のままでも同じ結果が返ります。今回強化した docstring が効いてくるのは、ツールが複数あって選択判断が必要入力が曖昧で引数抽出の精度が要るモデルに余計な確認質問をさせたくない といった、実運用で普通に発生する状況です。

エージェントがツールを呼ぶかどうか、どんな引数で呼ぶかは、モデルが ツールに添えられた説明文(今回は docstring)を読んで判断 しています。この原則は Strands の @tool でも、MCP プロトコルでツールを公開する場合でも同じで、モデルはツールごとの description を読んでツール選択と引数抽出を行います。ツールがうまく呼ばれない・引数がずれるといったときは、実装コードよりも先に説明文の書き方を工夫するのが最も効きます。MCP サーバとしてツールを公開する側の話は、後の章 AgentCoreでMCPサーバを作ろう で扱います。

4. エージェントの組み立て

4.1 概要

「メール署名を作る役割で動かしたい」と決めても、単にツールを渡すだけでは、モデルは丁寧な挨拶文を返したり、「以下のとおりです」といった前置きや後書きを付けたり、情報が足りないときに同じ質問を何度も返したりします。エージェントに「どういう役割で、どういう手順で、どういう形式で応答してほしいか」を明示する仕組みが必要になります。

Strands の Agent クラスに渡す system_prompt(システムプロンプト)に、モデルが取るべき手順を箇条書きで書き込めば、エージェントの人格・応答方針・情報不足時の振る舞いを宣言的に指定できます。「単に丁寧に答えて」ではなく「1. 4項目を抽出、2. 揃えばツール呼び出し、3. 結果をそのまま返す」のように手順化するのがコツです。

ここではシステムプロンプトを詳細化して、応答を引き締めてみます。

4.2 コードの書き換え

main.py を、以下の内容で全体を置き換えて保存します。前セクションからの変更は、Agent(...)system_prompt を手順書式にした箇所のみです。

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel

COMPANY_NAME = "DevOps株式会社"
COMPANY_TEL = "03-1234-5678"
MODEL_ID = "<モデルID>"  # ap-northeast-1で利用可能なClaude Sonnetの推論プロファイルID(例: jp.anthropic.claude-sonnet-4-6)に変更してください

app = BedrockAgentCoreApp()


@tool
def create_signature(name: str, department: str, position: str, email: str) -> str:
    """ユーザからメール署名の作成を依頼されたときに使ってください。氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は Runtime 側で自動的に付与されるため、ユーザに問い合わせる必要はありません。

    Args:
        name: 氏名(例: '田中太郎')
        department: 部署名(例: '開発部')
        position: 役職(例: 'エンジニア')
        email: メールアドレス(例: 'tanaka@example.com')
    """
    return (
        "━━━━━━━━━━━━━━━━━━━━━━\n"
        f"【{name}{COMPANY_NAME} {department} {position}\n"
        f"Email: {email}\n"
        f"Tel:   {COMPANY_TEL}\n"
        "━━━━━━━━━━━━━━━━━━━━━━"
    )


agent = Agent(
    model=BedrockModel(model_id=MODEL_ID, region_name="ap-northeast-1"),
    tools=[create_signature],
    system_prompt=(
        "あなたはメール署名を作成するアシスタントです。以下の手順で回答してください。\n"
        "1. ユーザの依頼から氏名・部署名・役職・メールアドレスの4項目を抽出する\n"
        "2. 4項目が揃っていれば create_signature ツールを呼び出す\n"
        "3. ツールの結果をそのまま提示する。挨拶文や『以下のとおりです』などの前置き・後書きは付けない\n"
        "4. 4項目のいずれかが揃わなければ、不足している項目だけを箇条書きでユーザに1度確認する"
    ),
)


@app.entrypoint
def invoke(payload):
    prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
    result = agent(prompt)
    return {"result": str(result)}


if __name__ == "__main__":
    app.run()

4.3 コードの解説

コードを解説します。前セクションからの変更点は Agent(...)system_prompt の1箇所のみなので、その部分を抜粋して見ていきます。

    system_prompt=(
        "あなたはメール署名を作成するアシスタントです。以下の手順で回答してください。\n"
        "1. ユーザの依頼から氏名・部署名・役職・メールアドレスの4項目を抽出する\n"
        "2. 4項目が揃っていれば create_signature ツールを呼び出す\n"
        "3. ツールの結果をそのまま提示する。挨拶文や『以下のとおりです』などの前置き・後書きは付けない\n"
        "4. 4項目のいずれかが揃わなければ、不足している項目だけを箇条書きでユーザに1度確認する"
    ),

システムプロンプトを、モデルが取るべき手順の番号付き箇条書きに書き換えました。手順1・2 でツールの呼び出し条件(4項目が揃えばツールを呼ぶ)を明示し、手順3 で応答フォーマット(ツール結果のまま提示、前置き・後書き禁止)を指定、手順4 で情報不足時の振る舞い(不足項目だけを箇条書きで1度確認する)を指定しています。

このように「エージェントに何をしてほしいか」を手順化してシステムプロンプトに書くと、応答のブレが小さくなり、実運用に耐える振る舞いに近づきます。ツール呼び出しループそのものは Strands が自動で回してくれるので、書き手はこの「振る舞いの宣言」に集中できます。

4.4 動作確認

まず、情報が揃った質問を送ります。

田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って

前回と同じ署名が返るはずですが、応答が 署名だけになり、「以下のとおりです」などの前置きや、「ご確認ください」といった後書きが消えているのが確認できます。

次に、情報が不足している質問を送ります。

田中太郎のメール署名を作って

以下のような、不足項目だけを箇条書きで確認する応答が返れば、システムプロンプトの手順4が効いています。

以下の情報が不足しています。
- 部署名
- 役職
- メールアドレス

以上のように、エージェントに毎回一定の振る舞いをしてほしい(前置き・後書きを抑える/情報不足時の応答を型にはめる/出力フォーマットを固定する、など)ケースでは、システムプロンプトを 番号付きの手順書 として書き下すのが有効です。「丁寧に対応してください」のような曖昧な指示よりも、モデルが取るべきステップを具体的に列挙するほうが、応答の再現性が上がります。

5. モデルパラメータの調整

5.1 概要

メール署名の作成のように「同じ入力には必ず同じ結果が返ってほしい」タスクでは、モデルの応答が呼び出しごとに揺れると、テストや監視が難しくなります。逆に、キャッチコピーやアイデア出しのように「毎回違う切り口の応答がほしい」タスクでは、応答が固定されると使い物になりません。応答の決定性と多様性、あるいは応答の長さを、タスクに応じて調整する必要があります。

BedrockModel のコンストラクタ引数に応答傾向を制御するパラメータを渡せば、この調整ができます。よく使うのは以下の3つです。

パラメータ 意味 使いどころ
temperature 応答のランダム性(0.0〜1.0)。低いほど決定的、高いほど多様 事実回答は低め(0.0〜0.3)、創造的な生成は高め(0.7〜1.0)
max_tokens 応答の最大トークン数 応答が長くなりすぎるのを防ぐ、料金上限をかける
top_p 上位何%の候補トークンから選ぶか(0.0〜1.0) temperature と組み合わせて多様性を絞る

メール署名は事実回答系のタスクなので、temperature=0.0 で応答の決定性を上げ、max_tokens=512 で応答が長くなりすぎるのを防いでみます。

5.2 コードの書き換え

main.py を、以下の内容で全体を置き換えて保存します。前セクションからの変更は、BedrockModel(...)temperature=0.0max_tokens=512 を追加した箇所のみです。

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel

COMPANY_NAME = "DevOps株式会社"
COMPANY_TEL = "03-1234-5678"
MODEL_ID = "<モデルID>"  # ap-northeast-1で利用可能なClaude Sonnetの推論プロファイルID(例: jp.anthropic.claude-sonnet-4-6)に変更してください

app = BedrockAgentCoreApp()


@tool
def create_signature(name: str, department: str, position: str, email: str) -> str:
    """ユーザからメール署名の作成を依頼されたときに使ってください。氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は Runtime 側で自動的に付与されるため、ユーザに問い合わせる必要はありません。

    Args:
        name: 氏名(例: '田中太郎')
        department: 部署名(例: '開発部')
        position: 役職(例: 'エンジニア')
        email: メールアドレス(例: 'tanaka@example.com')
    """
    return (
        "━━━━━━━━━━━━━━━━━━━━━━\n"
        f"【{name}{COMPANY_NAME} {department} {position}\n"
        f"Email: {email}\n"
        f"Tel:   {COMPANY_TEL}\n"
        "━━━━━━━━━━━━━━━━━━━━━━"
    )


agent = Agent(
    model=BedrockModel(
        model_id=MODEL_ID,
        region_name="ap-northeast-1",
        temperature=0.0,
        max_tokens=512,
    ),
    tools=[create_signature],
    system_prompt=(
        "あなたはメール署名を作成するアシスタントです。以下の手順で回答してください。\n"
        "1. ユーザの依頼から氏名・部署名・役職・メールアドレスの4項目を抽出する\n"
        "2. 4項目が揃っていれば create_signature ツールを呼び出す\n"
        "3. ツールの結果をそのまま提示する。挨拶文や『以下のとおりです』などの前置き・後書きは付けない\n"
        "4. 4項目のいずれかが揃わなければ、不足している項目だけを箇条書きでユーザに1度確認する"
    ),
)


@app.entrypoint
def invoke(payload):
    prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
    result = agent(prompt)
    return {"result": str(result)}


if __name__ == "__main__":
    app.run()

5.3 コードの解説

コードを解説します。前セクションからの変更点は BedrockModel(...) の引数に2つのパラメータを追加した箇所のみなので、その部分を抜粋して見ていきます。

    model=BedrockModel(
        model_id=MODEL_ID,
        region_name="ap-northeast-1",
        temperature=0.0,
        max_tokens=512,
    ),

temperature=0.0 は、応答のランダム性を最小化する設定です。同じ入力に対してほぼ同じ応答が返るようになり、テストや監視の観点で扱いやすくなります。max_tokens=512 は、モデルが生成する応答の長さを 512 トークン以内に制限する設定で、ツール呼び出しの往復も含めて応答が長くなりすぎるのを防ぎ、料金の上限もかけられます。

事実回答系のエージェント(メール署名生成、社内FAQ、フォーマット固定の情報整形など)では temperature=0.0 が基本です。逆に、複数の提案を返してほしいエージェント(キャッチコピー作成、アイデア出しなど)では temperature=0.7 くらいに上げて応答の多様性を確保します。

5.4 動作確認

同じ質問を 2回連続 で送ってみます。

田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って

temperature=0.0 にする前は、応答の細かい文言(改行位置や補足の有無)が呼び出しごとに揺れることがありましたが、今回は2回とも完全に同じ署名テキストが返るはずです。Tracesタブで応答トークン数を確認すると、512 以内に収まっていることも確認できます。

ただし、今回のように ツールが固定フォーマットの文字列を返し、システムプロンプトで応答を「結果をそのまま提示」に絞っている ケースでは、モデル側に文言を組み立てる裁量がほとんどないため、実は temperature を変えても結果は元から同じになりがちで、差が可視化されにくいのが実態です。temperature の効果が本領を発揮するのは、Q&A・要約・書き換え・キャッチコピー生成など モデルが応答文言を自由に組み立てるタスク で、そこでは値を下げるほど毎回同じ応答に固定され、上げるほど文言のブレが大きくなります。

以上のように、タスクの性質に応じてモデルパラメータを切り替える(事実回答系は temperature=0.0 で決定性を最優先/創造系は temperature=0.7 前後で多様性を確保/応答長は max_tokens で頭打ちにする)ことで、同じ SDK・同じシステムプロンプトのままエージェントの応答傾向を用途に寄せられます。パラメータは BedrockModel のコンストラクタに渡すだけなので、実運用ではエージェントの目的に応じて値を切り替えていく形になります。

6. 会話履歴とマルチターン対話

6.1 概要

「田中太郎のメール署名を作って」と依頼した直後に「じゃあ、部署を営業部に変えて」と続けたとき、エージェントが前のやりとりを覚えていないと、「何の何を営業部に変えるのか」を毎回聞き返してしまい、対話として成立しません。実運用のチャットボット型エージェントでは、複数ターンにまたがるやりとりを自然に扱える仕組みが必須です。

AgentCore Runtime の runtimeSessionId(CLIの --session-id)を活用すれば、同じセッションIDで呼び続けている限り Runtime 側でセッションの状態が保持され、Agent インスタンスも前のターンのメッセージを覚えた状態で応答できます。セッションIDを変えれば、それぞれ独立した別会話として扱われます。

このセクションは、コードの書き換えは不要です。agentcore invoke の呼び方だけを変えて、履歴が保持されることを体感します。

6.2 前セクションのコードの再利用

コードの書き換えは不要で、前セクションと同じ main.py のまま動作確認に入ります。ここでは、Agent Inspector(ローカル)・AWSマネジメントコンソール・AgentCore CLI の3通りで同じマルチターン挙動を確認していきます。3通り試すことで、実運用で使いそうな各インターフェースが同じセッションIDの仕組みで動くことを体感できます。

6.3 コードの解説

Strands の Agent(...) インスタンスは、同じセッションID内で呼ばれた際に、Strands 内部で会話履歴を保持します。これが AgentCore Runtime の runtimeSessionId と結びつくことで、「同じセッションIDで呼び続ければ、前のターンのやりとりを踏まえた応答が返る」構造になっています。

なお、Runtime のセッション状態は基本的にはセッションの寿命の間だけ保持され、セッションが終わると失われます。「昨日のユーザとの会話内容を明日も引き継ぎたい」「複数セッションをまたいでユーザの嗜好を覚えておきたい」といった セッションをまたぐ永続的なメモリ管理 は、AgentCore Memoryで会話履歴を任せよう で扱う AgentCore Memory に任せる形になります。本章では「同じ session-id で呼び続ければ短期的な履歴は自動で保持される」ことを押さえるところまでにとどめます。

6.4 動作確認

セッションIDによる履歴保持を、Agent Inspector(ローカル)→ 再デプロイ → AWSマネジメントコンソール → AgentCore CLI の順で確認していきます。いずれも同一 session-id で呼び続けると前ターンの情報が使われ、session-id を変えると別会話として扱われることを体感します。

Agent Inspector での確認

Agent Inspector は、同じチャット画面で送信した複数メッセージが自動的に同じセッションID(画面右上に session: xxxxxxxx... として表示される値)でリクエストされます。前セクションから起動したままの agentcore dev の Agent Inspector を開き、以下を順に送信します。

1ターン目:

田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って

田中太郎の署名が返ります。続けて、同じチャットで2ターン目を送信します。

じゃあ、部署を営業部に変えて

田中太郎の営業部版署名 が返れば、Runtime が前ターンの情報(氏名・役職・メールアドレス)を覚えている状態です。もし履歴が保持されていなければ、「何の何を変えるのか」を確認する応答が返るはずです。

画面右上のセッションID(session: d9c3...)が両ターンで共通で、1ターン目に比べて2ターン目の入力トークン数が増えている(例: 989 in1.6k in)ことも、前ターンの履歴が Runtime にまとめて渡されている裏付けになります。

別セッションの挙動も確認します。ブラウザをリロードして新しいセッションを開始し、2ターン目と同じ質問だけを送ります。今度は前の情報を持たないため、「氏名・役職・メールアドレスが不足しています」といった応答が返るはずで、これが「session-id を変えると別会話として扱われる」証拠になります。

AgentCore Runtime への再デプロイ

このあと AgentCore CLI とマネジメントコンソールでも同じ挙動を確認しますが、それらはクラウドの Runtime を呼び出すため、ここまでのセクションで書き換えてきた main.py の内容を先に反映しておきます。agentcore deploy を実行したターミナル(MyAgent フォルダにいる状態)に戻り、以下を実行します。

agentcore deploy

初回と違い、2回目以降は CDK bootstrap もリソース作成もスキップされるため、Deploy to AWS Complete まで数十秒〜1分程度で完了します。

反映されたことは、マネジメントコンソールのランタイム詳細画面でも確認できます。エンドポイント の紐付けが新しい バージョン 2 に切り替わり、バージョン 一覧にも バージョン 2 が追加されています(アップデートのたびに自動でスナップショットが作られるため、旧バージョンもロールバック用に残ります)。

AWSマネジメントコンソールでの確認

再デプロイ後、AgentCore コンソールで対象ランタイムの詳細画面を開き、右上「テスト」ボタンからテストUIを開きます。テスト画面では セッションID を明示的に指定 できるので、それを使ってマルチターンを試します。

セッションID はテストUIが自動で UUID を挿入するので、そのまま使います(AgentCore Runtime はセッションIDに 33 文字以上の文字列を要求するため、実務でも UUID を使うのが一般的です)。入力 欄に以下のJSONを貼って「実行」をクリックします。

{"prompt": "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って"}

出力 欄に、田中太郎の署名を含む {"result": "..."} が返ります。前セクションまでで整えたシステムプロンプトが効いているため、応答は署名のみで、前置き・後書きは付きません。

続いて、セッションID は同じまま入力 欄を以下に書き換えてもう一度「実行」をクリックします。

{"prompt": "じゃあ、部署を営業部に変えて"}

前ターンの情報(氏名・役職・メールアドレス)が保持されているため、部署だけが 営業部 に差し替わった署名が返ります。

対比のため、セッションID の欄に別の UUID を貼り付けて(uuidgen 等で生成、または セッションID を一度クリアして「例を表示」から新しい UUID を取得)、もう一度2ターン目の質問だけを送ります。今度は新規セッションのため、氏名・役職・メールアドレスの不足を返してくるはずです。

AgentCore CLI での確認

同じマルチターン挙動を、AgentCore CLI からも確認できます。agentcore invoke--session-id フラグでセッションIDを指定します。

AgentCore Runtime はセッションIDに 33 文字以上の文字列を要求する ため、以下では UUID を2つ生成し、環境変数 SESSION_1(1ターン目・2ターン目共通で使用)と SESSION_2(別セッション対比用)に控えたうえで、--session-id に渡して実行していきます。

agentcore deploy を実行したターミナル(MyAgent フォルダにいる状態)で、UUID を2つ生成して環境変数に代入します。

Macの場合:

export SESSION_1=$(uuidgen)
export SESSION_2=$(uuidgen)

Windowsの場合(PowerShell):

$env:SESSION_1 = [guid]::NewGuid().ToString()
$env:SESSION_2 = [guid]::NewGuid().ToString()

以降のコマンドは Mac の記法($SESSION_1 / $SESSION_2)で掲載します。Windows PowerShell の場合は $env:SESSION_1 / $env:SESSION_2 に読み替えてください。

まず、SESSION_1 を使って1ターン目を送ります。

agentcore invoke --session-id $SESSION_1 --prompt "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って"

以下のような実行結果が表示されます。

━━━━━━━━━━━━━━━━━━━━━━
【田中太郎】DevOps株式会社 開発部 エンジニア
Email: tanaka@example.com
Tel:   03-1234-5678
━━━━━━━━━━━━━━━━━━━━━━


Session: 25CD8BBC-AE48-4FBC-BC5A-5B1ECB702B6C
To resume: agentcore invoke --session-id 25CD8BBC-AE48-4FBC-BC5A-5B1ECB702B6C
Log: <MyAgentプロジェクトのパス>/agentcore/.cli/logs/invoke/invoke-MyAgent-<YYYYMMDD-HHMMSS>.log

田中太郎(開発部)の署名だけが返り、前セクションまでで整えたシステムプロンプトが効いているため前置き・後書きが付いていません。末尾の Session: に、今回のリクエストで使われたセッションID(SESSION_1 の値)が表示されます。

続けて、同じ SESSION_1 で2ターン目を送ります。

agentcore invoke --session-id $SESSION_1 --prompt "じゃあ、部署を営業部に変えて"

以下のような実行結果が表示されます。

━━━━━━━━━━━━━━━━━━━━━━
【田中太郎】DevOps株式会社 営業部 エンジニア
Email: tanaka@example.com
Tel:   03-1234-5678
━━━━━━━━━━━━━━━━━━━━━━


Session: 25CD8BBC-AE48-4FBC-BC5A-5B1ECB702B6C
To resume: agentcore invoke --session-id 25CD8BBC-AE48-4FBC-BC5A-5B1ECB702B6C
Log: <MyAgentプロジェクトのパス>/agentcore/.cli/logs/invoke/invoke-MyAgent-<YYYYMMDD-HHMMSS>.log

前ターンの情報(田中太郎の氏名・役職・メールアドレス)が保持されているため、部署だけが 営業部 に差し替わった署名が返ります。Session: の値が1ターン目と同じ(例では 25CD8BBC-...)であることも、同一セッション内のやりとりであることを示しています。

対比のため、SESSION_2 に切り替えて同じ2ターン目の質問を送ってみます。

agentcore invoke --session-id $SESSION_2 --prompt "じゃあ、部署を営業部に変えて"

以下のような実行結果が表示されます。

前回の会話履歴がないため、氏名・役職・メールアドレスが確認できません。以下の項目を教えてください。

- 氏名
- 役職
- メールアドレス


Session: DFD3C45B-038C-4A9D-8C7A-8C8A832A1F0D
To resume: agentcore invoke --session-id DFD3C45B-038C-4A9D-8C7A-8C8A832A1F0D
Log: <MyAgentプロジェクトのパス>/agentcore/.cli/logs/invoke/invoke-MyAgent-<YYYYMMDD-HHMMSS>.log

SESSION_2 は新規セッションのため、前の田中太郎の情報を持たない状態です。モデルは「情報が不足しています」の形で確認を返し、Session: の値も別 UUID(例では DFD3C45B-...)に変わっています。これが「session-id を変えると別会話として扱われる」証拠になります。

⚠️ 「Value at 'runtimeSessionId' failed to satisfy constraint」エラーが出る場合
--session-id に指定した値が短すぎる(33 文字未満)ときに発生します。AgentCore Runtime のセッションIDは 33 文字以上必要なので、my-chat-001 のような短い任意文字列は通りません。uuidgen(Mac)や [guid]::NewGuid().ToString()(Windows PowerShell)で生成した UUID をそのまま指定してください。

以上のように、チャットボットのように前ターンのやりとりを踏まえた応答が必要な ケースでは、クライアント側から同じ session-id で呼び続けるだけで、Runtime 側で会話履歴が自動的に保たれます。ユーザやスレッドごとに session-id を分ければ、複数の会話を同じ Runtime で並行して捌けます。Agent Inspector・マネジメントコンソール・AgentCore CLI のいずれから呼び出しても同じ仕組みで動き、コード側の変更なしに呼び出し側の引数だけで完結するのが、Strands + AgentCore Runtime のシンプルな設計のポイントです。

7. ストリーミング応答

7.1 概要

エージェントの応答が数百文字を超える長文になると、送信してから応答が返るまでの数秒〜十数秒の間、ユーザから見ると画面が完全に無反応になります。特にチャットUIでは、この待ち時間が「動いているのか固まったのか分からない」不安に繋がり、体感が大きく悪化します。モデルが生成したテキストを、生成された順に少しずつクライアントへ返す仕組みが必要になります。

Strands の agent.stream_async() と、BedrockAgentCoreApp@app.entrypointyield を組み合わせて使えば、モデルが生成したテキストの断片を1つずつ Runtime のストリーミングエンドポイントに流せるようになります。ChatGPT のように文字が流れて表示される体験が、数行の書き換えで実装できます。

7.2 コードの書き換え

main.py を、以下の内容で全体を置き換えて保存します。前セクションからの変更は、invoke 関数を async def + yield の非同期ジェネレータに書き換えた箇所のみです。

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel

COMPANY_NAME = "DevOps株式会社"
COMPANY_TEL = "03-1234-5678"
MODEL_ID = "<モデルID>"  # ap-northeast-1で利用可能なClaude Sonnetの推論プロファイルID(例: jp.anthropic.claude-sonnet-4-6)に変更してください

app = BedrockAgentCoreApp()


@tool
def create_signature(name: str, department: str, position: str, email: str) -> str:
    """ユーザからメール署名の作成を依頼されたときに使ってください。氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は Runtime 側で自動的に付与されるため、ユーザに問い合わせる必要はありません。

    Args:
        name: 氏名(例: '田中太郎')
        department: 部署名(例: '開発部')
        position: 役職(例: 'エンジニア')
        email: メールアドレス(例: 'tanaka@example.com')
    """
    return (
        "━━━━━━━━━━━━━━━━━━━━━━\n"
        f"【{name}{COMPANY_NAME} {department} {position}\n"
        f"Email: {email}\n"
        f"Tel:   {COMPANY_TEL}\n"
        "━━━━━━━━━━━━━━━━━━━━━━"
    )


agent = Agent(
    model=BedrockModel(
        model_id=MODEL_ID,
        region_name="ap-northeast-1",
        temperature=0.0,
        max_tokens=512,
    ),
    tools=[create_signature],
    system_prompt=(
        "あなたはメール署名を作成するアシスタントです。以下の手順で回答してください。\n"
        "1. ユーザの依頼から氏名・部署名・役職・メールアドレスの4項目を抽出する\n"
        "2. 4項目が揃っていれば create_signature ツールを呼び出す\n"
        "3. ツールの結果をそのまま提示する。挨拶文や『以下のとおりです』などの前置き・後書きは付けない\n"
        "4. 4項目のいずれかが揃わなければ、不足している項目だけを箇条書きでユーザに1度確認する"
    ),
)


@app.entrypoint
async def invoke(payload):
    prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
    async for event in agent.stream_async(prompt):
        if "data" in event:
            yield event["data"]


if __name__ == "__main__":
    app.run()

7.3 コードの解説

コードを解説します。前セクションからの変更点は invoke 関数の1箇所のみなので、その部分を抜粋して見ていきます。

@app.entrypoint
async def invoke(payload):
    prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
    async for event in agent.stream_async(prompt):
        if "data" in event:
            yield event["data"]

async def invoke(payload) で関数を非同期化しています。ストリーミングを扱うには async 関数として宣言するのが必須です。同期版の agent(prompt) が「完了するまで待って全文を1回で返す」のに対し、agent.stream_async(prompt) は「モデルが生成したテキストの断片を1つずつイベントとして返す」非同期ジェネレータになります。

async for event in agent.stream_async(prompt) で、モデルの応答断片が届くたびに event を受け取ります。event には、モデルの生成テキスト以外にもツール呼び出しの開始・終了などのメタ情報が入ってくることがあるため、if "data" in event: で応答テキストの断片が入ったイベントだけをフィルタしています。yield event["data"] でその断片を Runtime のレスポンスストリームに流すと、クライアント側では受信した順に画面へ表示できます。

7.4 動作確認

agentcore dev の Agent Inspector で、これまでと同じ質問を送ってみます。

田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って

これまでは「送信してから数秒無反応→全文が一気に表示」だった応答が、断片が少しずつ画面に流れて表示されるようになれば、ストリーミング応答が有効になっています。

応答が短いメール署名では体感差が小さいので、続けて長めの質問を送ると差が分かりやすいです。

このエージェントの使い方を、初心者向けに詳しく説明してください

このような長めの応答では、断片ごとに文字が流れていく様子がはっきり確認できます。見出し・表・箇条書きなどのマークダウンも、生成された順に上から埋まっていくため、ユーザ側は完成を待たずに読み始められます。

以上のように、チャットUIのように応答が返るまでユーザを待たせたくない ケースでは、agent.stream_async()@app.entrypointyield を組み合わせるだけでストリーミング応答を実装できます。コードの変更は数行で済むわりに体感の待ち時間を大きく短縮できるのが利点で、特に長文回答や複数のツール呼び出しを挟む応答で効果が大きくなります。チャットボット系のエージェントでは、実務ではデフォルトでストリーミングにしておくと扱いやすい方針です。

8. エラーハンドリング

8.1 概要

実運用のエージェントでは、ユーザから不正な入力が来る(メールアドレスの形式が壊れている、必須情報が抜けている等)、外部APIが遅延・エラーを返す、モデル呼び出し自体が失敗する、といったエラーが必ず発生します。ツール内で例外を投げっぱなしにすると、Runtime に伝播して500系エラーになり、クライアント側には「何かが失敗した」しか届かず、ユーザは何を直せばよいか分からない状態になります。

これを避けるには、ツール内で try/except して、エラーの説明文を戻り値として返すのが基本パターンです。モデルは戻り値を「ツールの結果」として受け取り、「メールアドレスの形式が正しくないので、name@example.com のような形式で提供してください」といった、ユーザ向けの自然な応答へ組み立て直してくれます。@app.entrypoint 側にも同じ考え方で try/except を入れておくと、想定外の例外に対してもクライアントへ意味のあるレスポンスを返せます。

現状の create_signature は、無効なメールアドレスが渡されても検証せず、そのまま署名に埋め込んでしまいます。ここでは入力検証と例外処理を加えて、不正入力に対しても意味のある応答を返す形に改善します。

8.2 コードの書き換え

main.py を、以下の内容で全体を置き換えて保存します。前セクションからの変更は、create_signature にメールアドレス形式チェックと try/except を追加、invoke にも try/except を追加した2箇所です。

from bedrock_agentcore.runtime import BedrockAgentCoreApp
from strands import Agent, tool
from strands.models import BedrockModel

COMPANY_NAME = "DevOps株式会社"
COMPANY_TEL = "03-1234-5678"
MODEL_ID = "<モデルID>"  # ap-northeast-1で利用可能なClaude Sonnetの推論プロファイルID(例: jp.anthropic.claude-sonnet-4-6)に変更してください

app = BedrockAgentCoreApp()


@tool
def create_signature(name: str, department: str, position: str, email: str) -> str:
    """ユーザからメール署名の作成を依頼されたときに使ってください。氏名・部署名・役職・メールアドレスを受け取り、所定のフォーマットのメール署名を返します。会社名と代表電話番号は Runtime 側で自動的に付与されるため、ユーザに問い合わせる必要はありません。

    Args:
        name: 氏名(例: '田中太郎')
        department: 部署名(例: '開発部')
        position: 役職(例: 'エンジニア')
        email: メールアドレス(例: 'tanaka@example.com')
    """
    try:
        if "@" not in email or "." not in email.split("@")[-1]:
            return f"エラー: メールアドレス '{email}' の形式が正しくありません。'name@example.com' のような形式で指定してください。"
        return (
            "━━━━━━━━━━━━━━━━━━━━━━\n"
            f"【{name}{COMPANY_NAME} {department} {position}\n"
            f"Email: {email}\n"
            f"Tel:   {COMPANY_TEL}\n"
            "━━━━━━━━━━━━━━━━━━━━━━"
        )
    except Exception as e:
        return f"メール署名の作成中に予期しないエラーが発生しました: {e}"


agent = Agent(
    model=BedrockModel(
        model_id=MODEL_ID,
        region_name="ap-northeast-1",
        temperature=0.0,
        max_tokens=512,
    ),
    tools=[create_signature],
    system_prompt=(
        "あなたはメール署名を作成するアシスタントです。以下の手順で回答してください。\n"
        "1. ユーザの依頼から氏名・部署名・役職・メールアドレスの4項目を抽出する\n"
        "2. 4項目が揃っていれば create_signature ツールを呼び出す\n"
        "3. ツールの結果をそのまま提示する。挨拶文や『以下のとおりです』などの前置き・後書きは付けない\n"
        "4. 4項目のいずれかが揃わなければ、不足している項目だけを箇条書きでユーザに1度確認する"
    ),
)


@app.entrypoint
async def invoke(payload):
    try:
        prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
        async for event in agent.stream_async(prompt):
            if "data" in event:
                yield event["data"]
    except Exception as e:
        yield f"エージェント実行中にエラーが発生しました: {e}"


if __name__ == "__main__":
    app.run()

8.3 コードの解説

コードを解説します。前セクションからの変更点は create_signatureinvoke の2箇所なので、順に抜粋して見ていきます。

    try:
        if "@" not in email or "." not in email.split("@")[-1]:
            return f"エラー: メールアドレス '{email}' の形式が正しくありません。'name@example.com' のような形式で指定してください。"
        return (
            "━━━━━━━━━━━━━━━━━━━━━━\n"
            f"【{name}】{COMPANY_NAME} {department} {position}\n"
            f"Email: {email}\n"
            f"Tel:   {COMPANY_TEL}\n"
            "━━━━━━━━━━━━━━━━━━━━━━"
        )
    except Exception as e:
        return f"メール署名の作成中に予期しないエラーが発生しました: {e}"

create_signature の中で、メールアドレスの簡易的な形式チェック(@ が含まれる、@ 以降にドットが含まれる)を入れています。不正な場合はエラーの説明文を戻り値として返します。ここで例外を投げずに戻り値として返すのが重要で、モデルは戻り値を「ツールの結果」として受け取り、「メールアドレスの形式が正しくないので、name@example.com のような形式で提供してください」のようにユーザ向けの自然な応答へ組み立て直してくれます。全体を try/except Exception で包むことで、想定外の例外に対してもエラー内容を戻り値として返し、CloudWatch Logs に例外だけが残ってクライアントが無反応、といった事態を避けられます。

@app.entrypoint
async def invoke(payload):
    try:
        prompt = payload.get("prompt", "田中太郎(開発部 エンジニア、tanaka@example.com)のメール署名を作って")
        async for event in agent.stream_async(prompt):
            if "data" in event:
                yield event["data"]
    except Exception as e:
        yield f"エージェント実行中にエラーが発生しました: {e}"

invoke 側も同じ考え方で、try/except Exception で全体を包んで、想定外の例外が起きても yield でエラーメッセージをクライアントに返す形にしています。ストリーミング応答なので、yield でエラー文字列を1つ流せば、クライアント側にはそれが応答の一部として届きます。

なお、temperature=0.0 と手順化されたシステムプロンプトを持つ 近年の高度なモデルでは、ツールに不正な引数を渡す前にモデル側で気付いてユーザに確認を返すことが多く、今回追加したツール側の try/except や検証コードが実際に発火する場面は限定的です。ここでの try/except は「モデルの判断がずれたとき」「システムプロンプトを差し替えたとき」「想定外の値や外部連携の失敗が起きたとき」に備えた 保険(多層防御) としての意味合いが強いため、本ハンズオンでは動作確認は省略します。

以上のように、ユーザ入力の不正や外部連携の失敗が想定されるエージェント では、ツール内で例外を投げっぱなしにせず、try/except でエラーの説明文を戻り値として返す実装にしておくのが基本です。モデルが賢く手前で捌いてくれる現代のエージェントでも、想定外の入力・障害への備えとして欠かせない要素で、エラー文字列をモデルが自然文に整えてユーザに返す構造にしておけば、対話が破綻せずユーザに次のアクションを促せる形になります。@app.entrypoint 側にも同じ try/except を入れておけば、想定外の例外にもクライアントへ意味のあるレスポンスを返せます。

9. 不要リソースの削除

AgentCore Runtimeは稼働時間ベースで課金される要素を含むため、ハンズオン後は忘れずに削除します。AgentCore CLIにはremove alldeployを組み合わせた片付け手順があります。まずagentcore.jsonのリソース定義を空の状態にします。

agentcore remove all

以下のような実行結果が表示されます。

  Reset AgentCore Schemas

  AgentCore schemas reset successfully

  Your source code has not been modified.


  Next: Run agentcore deploy to deploy changes to aws

  Esc back · Ctrl+C quit

AgentCore schemas reset successfully が表示されれば、agentcore.json のリソース定義がリセットされた状態です。Your source code has not been modified. のとおりソースコード自体はそのまま残るので、あとで手を加えて再デプロイする流れも取れます。

この時点ではまだAWS上のリソースは残っています。続いてデプロイを実行すると、AgentCore CLIが「空になったconfigとAWS上の実リソース」を比較し、差分としてリソースをまとめて破棄します。

agentcore deploy

以下のような実行結果が表示されます。

  AgentCore Deploy

  Project: MyAgent
  Target: ap-northeast-1:<AWSアカウントID>

  [done]    Validate project
  [done]    Check dependencies
  [done]    Sync CDK dependencies
  [done]    Build CDK project
  [done]    Synthesize CloudFormation
  [done]    Check stack status
  [done]    Computing diff changes...
  [done]    Publish assets

  ╭────────────────────────────────────────────────╮
  │ ✓ Deploy to AWS Complete                       │
  │                                                │
  │ [████████████████████] 6/6                     │
  ╰────────────────────────────────────────────────╯

  Log: agentcore/.cli/logs/deploy/deploy-<YYYYMMDD-HHMMSS>.log

  Next: Run agentcore add to add an agent, or agentcore status to view deployment status

  Esc back · Ctrl+C quit

✓ Deploy to AWS Complete が表示されれば AWS 側の片付けは完了です。末尾の Next: Run agentcore add to add an agent, ... のヒントは、設定を空にしてデプロイした結果、対象エージェントが無くなった状態であることを示しています。

念のためマネジメントコンソールの AgentCore 「ランタイム」画面を東京リージョンで開くと、ランタイムリソース (0)・「エージェントが見つかりません」と表示され、実リソースが破棄されたことも確認できます。

ローカルのagentcore-runtime-handson/フォルダは、必要に応じて手動で削除してください。

10. まとめ

この章では、Strands Agents SDK と AgentCore CLI を学びつつ、実際にメール署名エージェントをAgentCore Runtimeにデプロイして扱いを確かめました。

  • AgentCore Runtimeは、ローカルにコードを書いて zip 化 → S3 → Runtime更新 の流れで開発する
  • AgentCore CLI(agentcore create / dev / deploy / invoke / remove)により、その流れを1コマンドずつに自動化できる
  • agentcore create の対話ウィザードで、Framework・Model provider・Build typeを選ぶだけでプロジェクトのスケルトンを生成できる
  • agentcore dev(Agent Inspector)と agentcore deploy / agentcore invoke により、ローカル→クラウド→呼び出しの流れを短いコマンドで扱える
  • ツールの定義は @tool + 型ヒント + docstring の3点セットで、docstring と Args: セクションがモデルへの description になる
  • エージェントの組み立ては Agent(model, tools, system_prompt) で宣言的に書け、システムプロンプトを手順書式にすると振る舞いが安定する
  • モデルパラメータ(temperature / max_tokens / top_p)により、応答の傾向と長さをコントロールできる(事実回答系は temperature=0.0 が基本)
  • 会話履歴は、同じ session-id で呼び続ければ Runtime 側が自動で保持する
  • ストリーミング応答は agent.stream_async() + yield で書け、長めの応答が必要な場面で受け取り側の体感を改善できる
  • エラーハンドリングは、ツール内で try/except して意味のあるメッセージを戻り値として返すのが基本である
  • Docker不要の Direct Code Deployment(CodeZip)でデプロイでき、zipサイズ制限や独自依存が必要な場合はコンテナデプロイに切り替えられる
  • 稼働時間ベースの課金要素を含むため、不要になったら agentcore remove all + agentcore deploy で片付ける

次の章では、AgentCore Runtime にコンテナデプロイする流れをハンズオン形式で体験します。

この教材は役に立ちましたか?

いいねをたくさんいただけると、制作者の励みになり、より多くのセクションが作れるようになります。

感想を一言(任意)

いただいたコメントは次の制作のヒントになります。ぜひお気軽にご投稿ください。

このコメントは他の受講生には公開されません。DevOps Camp運営が、教材改善のために確認します。

0 / 2000