ローソンデジタルイノベーション テックブログ

ローソンデジタルイノベーション(LDI)の技術ブログです

Strands Agents SDK × Amazon Bedrock AgentCore で簡単なエージェント型チャットボットを作る

はじめに

記事を開いていただきありがとうございます。LDIコンシューマサービス部 バックエンドユニットのオーです。同部署でAIに関わる活動も行なっています。

今回は「Strands Agents × Amazon Bedrock AgentCore で簡単なエージェント型チャットボットを作る」をテーマに、簡単なエージェント型 AI アプリの作り方について紹介します。

これらの技術は、以前から勉強会やブログ記事などで見かけて気になっていました。そんな中、AWS環境でのtoC向けチャットボット企画が立ち上がり、環境とのフィット感が高いことから採用が決まり、実際に触れる機会がありました。今回は学習も兼ねて、これらを組み合わせて実際に動く簡単なアプリを作ってみました。

この記事が、エージェント型 AI アプリの開発に興味のある方の参考になれば幸いです。

1. 概要

ゴール

Strands AgentsAmazon Bedrock AgentCore を連携させて、シンプルなエージェント型 AI チャットボットを構築します。

具体的には、以下の機能を持つ簡単な株式ポートフォリオ分析チャットボットを実装します。

  • 自分のポートフォリオデータを DB から取得するツール
  • 銘柄ごとにテクニカル分析を行うツール(yfinance 活用)

本記事の株式分析は、エージェントツール連携のデモを目的としたサンプルです。記載の株価・指標は説明用の例示であり、実際の投資判断を推奨するものでも、投資助言を目的としたものでもありません。

本記事の構成

本記事は、以下のパートで構成されています。

  • 実装:上記のエージェント系 AI チャットボットの実装を解説します
  • 動作解説:作ったエージェントが、質問をどう処理し回答を導くのかを解説します
  • 今後さらに深めたいこと:マルチエージェントやセキュリティなど、筆者が今後深めるといいなと思った知識について、軽く紹介します

その中でも、中心となる「実装パート」では、以下の4ステップで解説を進めていきます。

  • Python ベースのチャット UI の準備(Streamlit + FastAPI + PostgreSQL)
  • Strands Agents と Amazon Bedrock のLLMを連携させた基本 AI チャットの実装
  • 会話内容の管理
    • Strands Agents の Session Manager で管理可能
    • 本記事では Amazon Bedrock AgentCore の Memory の短期記憶を活用
  • エージェントの実装
    • 単一エージェントが複数ツールを呼び分ける Tool Use 構成を採用
    • 実装するツールは2つ(ポートフォリオ取得・テクニカル分析)

使う技術

Strands Agents

Amazon が開発するオープンソースのエージェントフレームワークです。 @tool デコレータと Agent クラスを中心にシンプルな API でエージェントを構築できます。 マルチエージェント・Hooks・Interrupts・Guardrails など本格運用に必要な機能も揃っています。

strandsagents.com

github.com

Amazon Bedrock

AWS のマネージド LLM サービスです。 Amazon Nova・Claudeなど複数のモデルを API 経由で利用できます。 Strands Agents の BedrockModel を使うことでモデルの差し替えも容易です。

aws.amazon.com

以下、略称として Bedrock を使います。

Amazon Bedrock AgentCore

エージェント向けのマネージドインフラサービスです。 本記事では AgentCore Memory の短期記憶機能を使い、会話内容をリクエストをまたいで保存します。 ユーザー発言だけでなくツール呼び出し・結果を含む全ターンが保存されるため、文脈を維持した会話が可能になります。

aws.amazon.com

strandsagents.com

以下、略称として AgentCore を使います。

完成イメージ

動作フロー

以下の図は、ユーザーの入力がどのように処理されて回答に至るかの大まかな流れになります。

アプリケーションの動作の流れ

動作画面

実際に動かすとこのような画面になります。

動作画面

2. 実装

それでは、実際に作る過程をお話しします。実装は大きく4つのパートに分かれます。

  • A. Chat UI・API・DB環境の準備
  • B. Strands Agents と Bedrock の基本連携
  • C. 会話内容管理(AgentCore Memory)
  • D. ポートフォリオ取得・テクニカル分析ツールの実装

完成後のディレクトリ構成は次のイメージです。

.
├── backend/
│   ├── __init__.py
│   ├── database.py   # DB接続設定
│   ├── main.py       # FastAPI + Agent ロジック
│   └── models.py     # SQLAlchemy モデル
├── frontend/
│   └── app.py        # Streamlit UI
├── scripts/
│   └── init_db.py    # テーブル作成・シードデータ投入
├── docker-compose.yml
├── pyproject.toml
└── .env              # 環境設定ファイル (※リポジトリやGit履歴には含まない)

A. Chat UI・API・DB環境の準備

事前セットアップ環境について

本パートの実装にあたり、以下の環境をセットアップしています。

各環境の詳細な説明は省略しますが、環境のセットアップについては参考資料を添付しますので、ご参照ください。

Python環境のパッケージ管理には uv を使用します。

コンテナ環境には Docker(Colima)を使用します。

AWS Bedrock および AgentCore Memory については、以下をご参考ください。

依存ライブラリ

依存ライブラリについての詳細な紹介も省略します。各ライブラリの詳細については、ライブラリ名で検索して確認してください。

pyproject.toml に必要なライブラリを定義します。 パッケージ管理には uv を使います。

# pyproject.toml
[project]
name = "agentcore-demo"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
    "streamlit",
    "fastapi",
    "uvicorn[standard]",
    "sqlalchemy",
    "psycopg2-binary",
    "strands-agents",
    "bedrock-agentcore",
    "boto3",
    "yfinance",
    "python-dotenv",
    "httpx",
]

主なライブラリの役割は以下のとおりです。

ライブラリ 役割
streamlit チャット UI (Python基盤Webアプリ制作フレームワーク)
fastapi / uvicorn バックエンド API サーバー
sqlalchemy / psycopg2-binary ORM & PostgreSQL Adapter
strands-agents Strands Agents
bedrock-agentcore AgentCore Memory 連携
boto3 AWS SDK
yfinance 株価データ取得(Yahoo Finance)
DB環境(PostgreSQL)

ポートフォリオデータを管理する PostgreSQL を Docker で起動します。

# docker-compose.yml
services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: agentcore_demo
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  postgres_data:

環境変数は .env ファイルで管理します。以下の内容をコピーして作成し、各値を埋めます。

# .env.example
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/agentcore_demo

# AWS Credentials
AWS_ACCESS_KEY_ID=your_access_key_here
AWS_SECRET_ACCESS_KEY=your_secret_key_here
AWS_SESSION_TOKEN=your_token_here  # 一時クレデンシャルの場合のみ必要
AWS_REGION=us-east-1

# Bedrock
BEDROCK_MODEL_ID=us.amazon.nova-2-lite-v1:0

# AgentCore Memory
AGENTCORE_MEMORY_ID=your_agentcore_memory_id_here  # AWS コンソールで作成したメモリストアの ID
AGENTCORE_MEMORY_REGION=us-east-1

.env や実際のクレデンシャルはリポジトリやGit履歴に含めないことをお勧めします。 また、AI コーディングツールを使う場合は、.env のような機微なファイルを、AIが読めないよう Deny ルールの設定を推奨します(Claude Code であれば .claude/settings.jsondenyList.env を追加)

DB接続の設定は backend/database.py にまとめます。

# backend/database.py
import os
from dotenv import load_dotenv
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker, DeclarativeBase

load_dotenv()

DATABASE_URL = os.environ["DATABASE_URL"]

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)  # リクエストごとにセッションを生成するファクトリ


class Base(DeclarativeBase):  # 全モデルの基底クラス。models.py でインポートして使う
    pass
ポートフォリオテーブルの定義

今回は tickernameavg_price(平均取得単価)・num_holding(保有株数)のシンプルな構成にします。 backend/models.py に定義します。

# backend/models.py
from sqlalchemy import Integer, String, Float
from sqlalchemy.orm import Mapped, mapped_column
from backend.database import Base


class Portfolio(Base):
    __tablename__ = "portfolio"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    ticker: Mapped[str] = mapped_column(String(10), unique=True, nullable=False)  # 例: "AAPL"
    name: Mapped[str] = mapped_column(String(100), nullable=False)                # 例: "Apple Inc."
    avg_price: Mapped[float] = mapped_column(Float, nullable=False)               # 平均取得単価
    num_holding: Mapped[int] = mapped_column(Integer, nullable=False)             # 保有株数

テーブルの作成とシードデータの投入は scripts/init_db.py で行います。

# scripts/init_db.py
from backend.database import engine, SessionLocal
from backend.models import Base, Portfolio

SEED_DATA = [
    {"ticker": "AAPL", "name": "Apple Inc.",      "avg_price": 175.0, "num_holding": 10},
    {"ticker": "MSFT", "name": "Microsoft Corp.", "avg_price": 380.0, "num_holding": 5},
    {"ticker": "GOOGL","name": "Alphabet Inc.",   "avg_price": 140.0, "num_holding": 8},
    {"ticker": "AMZN", "name": "Amazon.com Inc.", "avg_price": 185.0, "num_holding": 6},
    {"ticker": "NVDA", "name": "NVIDIA Corp.",    "avg_price": 800.0, "num_holding": 3},
]


def main():
    Base.metadata.create_all(bind=engine)  # モデル定義からテーブルを自動生成
    db = SessionLocal()
    try:
        for item in SEED_DATA:
            if not db.query(Portfolio).filter_by(ticker=item["ticker"]).first():  # 二重投入を防ぐ
                db.add(Portfolio(**item))
        db.commit()
    finally:
        db.close()


if __name__ == "__main__":
    main()
Streamlit チャット UI

frontend/app.py にチャット画面を実装します。 前述したStreamlitを使うと st.chat_input / st.chat_message API を使うだけで、すぐに LLM チャット風の UI が作れます。

Streamlitの詳細は公式を参照してください。

streamlit.io

まず初期化処理から書きます。

# frontend/app.py
import uuid
import httpx
import streamlit as st

BACKEND_URL = "http://localhost:8000"

st.set_page_config(page_title="Portfolio Advisor", page_icon="📈", layout="wide")

# セッションIDを初期化(ページリロードまで同一セッションを維持)
if "session_id" not in st.session_state:
    st.session_state.session_id = str(uuid.uuid4())
if "messages" not in st.session_state:
    st.session_state.messages = []  # 画面表示用のチャット履歴

続いてメインエリアを実装します。 ユーザーの入力をバックエンドの /chat エンドポイントに対して POST リクエストを行い、受けたレスポンスを表示します。

# frontend/app.py(続き)
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

if prompt := st.chat_input("メッセージを入力してください..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    with st.chat_message("user"):
        st.markdown(prompt)

    with st.chat_message("assistant"):
        with st.spinner("Thinking..."):
            res = httpx.post(
                f"{BACKEND_URL}/chat",
                json={"session_id": st.session_state.session_id, "message": prompt},  # session_id でMemoryを紐づける
                timeout=60,
            )
            response_text = res.json().get("response", "")
        st.markdown(response_text)
        st.session_state.messages.append({"role": "assistant", "content": response_text})

※ Chat UI Libraryの選定についての補足 今回はPython環境でUI実装を簡単にするためにStreamlitを使用しています。

UI側を、Reactで作る場合には、assistant-uiを使用できます。

https://www.assistant-ui.com

TypeScript Fullstackで作る場合には、Strands AgentsのTypeScript版を使用できます。

https://strandsagents.com/docs/user-guide/quickstart/typescript/

B. Strands Agents と Bedrock の基本連携

backend/main.py にエージェントのコアロジックを実装します。

まず環境変数と定数を定義するところから始めます。 BEDROCK_MODEL_ID は Amazon Nova Lite を指定していますが、Claudeなど他のモデルに差し替えることも可能です。

# backend/main.py
import os
from dotenv import load_dotenv
from strands import Agent
from strands.models import BedrockModel

load_dotenv()

MODEL_ID    = os.environ.get("BEDROCK_MODEL_ID", "us.amazon.nova-2-lite-v1:0")  # デフォルトは Amazon Nova Lite
AWS_REGION  = os.environ.get("AWS_REGION", "us-east-1")

# エージェントの役割・行動指針を定義するシステムプロンプト
SYSTEM_PROMPT = """あなたは株式ポートフォリオアドバイザーです。ユーザーのポートフォリオの把握と銘柄分析をサポートします。
利用可能なツールを使ってポートフォリオデータの取得とテクニカル分析を行ってください。
常にユーザーのメッセージと同じ言語で回答してください。"""

Agent の初期化はシンプルです。BedrockModel でモデルを指定し、system_prompt を渡すと基本的な準備は完了です。

Agent はリクエストごとに session_id を受け取って生成する設計にします。 またAgentの初期化はcreate_agent 関数にまとめておくことで、C・D パートで Memory やツールを追加する際もここだけ変更すれば済みます。

# backend/main.py(続き)
def create_agent(session_id: str) -> Agent:
    return Agent(
        model=BedrockModel(model_id=MODEL_ID, region_name=AWS_REGION),
        system_prompt=SYSTEM_PROMPT,
        # session_manager は C パートで追加
        # tools は D パートで追加
    )

この関数を呼び出すだけでエージェントに話しかけられます。

agent = create_agent("session_001")
response = agent("ポートフォリオを見せて")  # 質問・要求メッセージを渡す
print(response)

FastAPI のエンドポイントに組み込むと次のようになります。

# backend/main.py(続き)
from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="AgentCore Demo")

class ChatRequest(BaseModel):
    session_id: str
    message: str

@app.post("/chat")
async def chat(req: ChatRequest):
    agent = create_agent(req.session_id)   # C パートで詳述
    response = agent(req.message)
    return {"response": str(response)}

C. 会話内容の記憶と管理(AgentCore Memory)

Strands Agents は、会話内容を記憶し、話の脈絡を維持した会話を可能にする機能が備えています。AgentCore Memory を使うと、会話内容をクラウド側で保存できます。

※会話内容の管理については、上記の方式以外にもConversation Manager(SlidingWindowConversationManager)機能を利用したIn-Memory形式の簡単な管理方法も提供されています。詳細は以下を参照してください。

https://strandsagents.com/docs/user-guide/concepts/agents/conversation-management

本記事ではAgentCore Memoryを利用した管理を選定しているため、Conversation Managerに対する内容は省略します。

AgentCore Memory の構造

AgentCore Memory の短期記憶(Short-term Memory)は以下の3つのキーで識別されます。

キー 説明
memory_id メモリストアの識別子。AWS コンソールで事前に作成する
actor_id ユーザーの識別子(複数ユーザーを管理する場合に活用)
session_id セッションの識別子。会話ごとに一意の値を割り当てる

memory_id の配下に actor_id(ユーザー)があり、1人のユーザーが複数の session_id(会話)を持てる階層構造になっています。

memory_id(メモリストア)
└── actor_id(ユーザー)
    ├── session_id(会話A)
    ├── session_id(会話B)
    └── ...
AgentCoreMemorySessionManager の初期化

AgentCoreMemoryConfig に上記の3キーを渡し、AgentCoreMemorySessionManager を生成します。 この session_managerAgent のコンストラクタに渡すだけで、会話内容の読み書きが自動的に行われます。

backend/main.py に追記します。

# backend/main.py(続き)
from bedrock_agentcore.memory.integrations.strands.config import AgentCoreMemoryConfig
from bedrock_agentcore.memory.integrations.strands.session_manager import AgentCoreMemorySessionManager

MEMORY_ID     = os.environ["AGENTCORE_MEMORY_ID"]   # AWS コンソールで事前に作成したメモリストアの ID
MEMORY_REGION = os.environ.get("AGENTCORE_MEMORY_REGION", "us-east-1")
ACTOR_ID      = os.environ.get("ACTOR_ID", "user_001")  # 複数ユーザー対応の場合に使うID


def create_agent(session_id: str) -> Agent:
    # AgentCoreMemorySessionManagerの初期化
    config = AgentCoreMemoryConfig(
        memory_id=MEMORY_ID,
        session_id=session_id,  # 会話セッションごとのID
        actor_id=ACTOR_ID, # ユーザーごとのID
    )
    session_manager = AgentCoreMemorySessionManager(config, region_name=MEMORY_REGION)
    
    # session_manager を渡すだけで会話ターンの読み込み・書き戻しが自動化される
    return Agent(
        model=BedrockModel(model_id=MODEL_ID, region_name=AWS_REGION),
        session_manager=session_manager,
        system_prompt=SYSTEM_PROMPT,
        # tools は D パートで実装後に追加
    )

create_agent はリクエストごとに呼び出されます。 Agent は初期化時に session_manager から過去の会話を取得し、 会話終了後に最新のターンを書き戻すため、サーバーを再起動しても文脈が維持されます。

D. ツールの実装

Strands Agents では @tool デコレータを関数に付けるだけで、エージェントが使えるツールとして登録されます。 LLM は関数の docstringType Hinting を読んでツールの使い方を理解するため、 これらをきちんと書いておくことが重要です。

backend/main.py に続けて実装します。

ツール1: ポートフォリオ取得

PostgreSQL からポートフォリオデータを取得するツールです。

# backend/main.py(続き)
from strands import tool
from backend.database import SessionLocal
from backend.models import Portfolio


@tool  # このデコレータで Agent が使えるツールとして登録される
def get_portfolio() -> str:
    """データベースからユーザーの現在の株式ポートフォリオを取得します。"""
    db = SessionLocal()
    try:
        holdings = db.query(Portfolio).all()
        result = [
            {
                "ticker":      h.ticker,
                "name":        h.name,
                "avg_price":   h.avg_price,
                "num_holding": h.num_holding,
            }
            for h in holdings
        ]
        return str(result)  # LLM に渡すため文字列で返す
    finally:
        db.close()

ユーザーが「ポートフォリオを見せて」と入力したとき、エージェントはこのツールを呼び出して DB から一覧を取得し、回答を生成します。

ツール2: テクニカル分析

yfinance で過去90日分の株価を取得し、現在値・前日終値・前日比・移動平均(MA20・MA50)を計算して返します。

# backend/main.py(続き)
import yfinance as yf
from datetime import datetime, timedelta


@tool
def get_technical_analysis(ticker: str) -> str:
    """
    指定した銘柄のテクニカル分析を行います。

    Args:
        ticker: 銘柄コード(例: AAPL, MSFT)
    """
    end   = datetime.today()
    start = end - timedelta(days=90)  # 移動平均の計算に十分な90日分を取得

    stock = yf.Ticker(ticker)
    hist  = stock.history(
        start=start.strftime("%Y-%m-%d"),
        end=end.strftime("%Y-%m-%d"),
    )

    if hist.empty:
        return f"No data found for ticker: {ticker}"

    current_price = round(hist["Close"].iloc[-1], 2)   # 直近の終値
    prev_close    = round(hist["Close"].iloc[-2], 2)   # 前日の終値
    change_pct    = round((current_price - prev_close) / prev_close * 100, 2)
    ma20          = round(hist["Close"].tail(20).mean(), 2)  # 20日移動平均
    ma50          = round(hist["Close"].tail(50).mean(), 2)  # 50日移動平均

    return str({
        "ticker":        ticker,
        "current_price": current_price,
        "prev_close":    prev_close,
        "change_pct":    change_pct,
        "ma20":          ma20,
        "ma50":          ma50,
    })

ユーザーが「AAPLのテクニカル分析をして」と入力すると、エージェントは ticker="AAPL" を引数にこのツールを呼び出し、返却された指標をもとに分析コメントを生成します。

「アップルの株動向は?」「りんご社の株は最近どう?」などある程度の意味の推論にも対応してくれます。

Agent の初期化(完成版)

2つのツールが揃ったところで、create_agentに実装したエージェント初期化の引数として tools に渡して完成です。 B・C パートで組み立ててきた BedrockModelsession_managersystem_prompt とツールを束ねるだけで、エージェントとして動作します。

# backend/main.py(完成版)
def create_agent(session_id: str) -> Agent:
    config = AgentCoreMemoryConfig(
        memory_id=MEMORY_ID,
        session_id=session_id,
        actor_id=ACTOR_ID,
    )
    session_manager = AgentCoreMemorySessionManager(config, region_name=MEMORY_REGION)
    return Agent(
        model=BedrockModel(model_id=MODEL_ID, region_name=AWS_REGION),
        tools=[get_portfolio, get_technical_analysis],  # ← D パートで実装した2つのツール
        session_manager=session_manager,                # ← C パートで設定した Memory 連携
        system_prompt=SYSTEM_PROMPT,
    )

3. 動作解説

実装したシステムが実際にどう動くのかを、エージェント全体の構造と質問ケースごとの挙動に分けて解説します。

動作コマンド

作ったアプリケーションは、以下のコマンドを利用して起動することができます。

# 依存ライブラリのインストール
uv sync

# PostgreSQL 起動
docker-compose up -d

# テーブル作成 + シードデータ投入
uv run python scripts/init_db.py

# FastAPI 起動 (port 8000)
uv run uvicorn backend.main:app --reload --port 8000

# Streamlit 起動 (Chat UI)
uv run streamlit run frontend/app.py

# Streamlit 起動後、接続URLがコンソールに表示 
Uvicorn server started on 0.0.0.0:8501
You can now view your Streamlit app in your browser.

  Local URL: http://localhost:8501

全体構造

今回のシステムは Tool Use 構成を採用しています。 1つのエージェントが会話を管理しながら、必要に応じて複数のツール(関数)を呼び分けます。

AIエージェントの構成

エージェントがすべての思考・判断を担当し、個々のツールはデータ取得・計算を担当します。 ツールの組み合わせ方・呼び出し順序はLLMが動的に推論し決定するイメージです。

質問ケース別の動作イメージ

以下の動作例はすべて実際のログをもとにしています。 ただし、使用するモデルや入力表現によって挙動が変わる場合があります。

ケース1: ポートフォリオ一覧の確認

質問: 「現在のポートフォリオにある銘柄を教えて。」

シンプルな一覧照会です。LLMは get_portfolio 1つで回答できると判断します。

■エージェントの推論過程のイメージ

▶ get_portfolio が必要と判断
▶ get_portfolio を呼び出し
▶ 返却データをもとに回答を生成

■ エージェント処理ログ&回答例

Tool: get_portfolio

Answer: 
現在のポートフォリオにある銘柄は以下の通りです:

1. AAPL - Apple Inc.  (平均取得価格: $175.00、10株)
2. MSFT - Microsoft Corp.  ($380.00、5株)
3. GOOGL - Alphabet Inc.  ($140.00、8株)
4. AMZN - Amazon.com Inc.  ($185.00、6株)
5. NVDA - NVIDIA Corp.  ($800.00、3株)
ケース2: 特定銘柄のテクニカル分析

質問: 「NVDAのテクニカル分析をして。」

銘柄コードが明示されているため、引数を推論してすぐに get_technical_analysis を呼び出します。

■エージェントの推論過程のイメージ

▶ get_technical_analysis が必要と判断
▶ 引数 ticker="NVDA" を推論
▶ get_technical_analysis を呼び出し
▶ 返却データをもとに回答を生成

■ エージェント処理ログ&回答例

Tool: get_technical_analysis(ticker="NVDA")

Answer: 
現在価格: $224.36(前日比 +6.26%)
MA20: $216.75 / MA50: $200.26

→ 現在価格は MA20・MA50 の両方を上回っており、強い上昇トレンドを示しています。
ケース3: 曖昧な呼称からの銘柄特定

質問: 「りんご社の株は最近どう?」

銘柄コードが明示されていないため、LLMはまずポートフォリオを取得して「りんご社=AAPL」を推論してから分析を行います。

■エージェントの推論過程のイメージ

▶ ポートフォリオ内の該当 Ticker を特定するために get_portfolio を呼び出し
▶ AAPL と推論
▶ get_technical_analysis(ticker="AAPL") を呼び出し
▶ 返却データをもとに回答を生成

■ エージェント処理ログ&回答例

Tool #1: get_portfolio
Tool #2: get_technical_analysis(ticker="AAPL")

Answer: 
りんご社(AAPL)の現在株価は $306.31(前日比 -1.84%)。
20日線 ($298.78) を上回っており、中長期的な上昇トレンドは継続中です。
ケース4: 会話内容を活用した文脈引き継ぎ

これは AgentCore Memory の核心的なユースケースです。 以下の2つの質問を連続して投げます。

  • 質問1: Elon Muskさんの会社の銘柄を分析して。
  • 質問2: 公開市場に上場しているものを分析して。

質問1: 「Elon Muskさんの会社の銘柄を分析して。」

ユーザーが「Elon Muskの会社」と言っても、LLMはそれがポートフォリオに含まれているか知りません。 まず get_portfolio でDBを確認し、該当銘柄が存在しないことを確認してから、LLMの学習知識で回答します。

ツールを使わずLLMの学習知識だけで回答することも可能ですが、「利用可能なツールを使って」「ポートフォリオアドバイザー」というシステムプロンプトの指示・ロール定義により、銘柄に関する質問ではまずDBを確認するという行動パターンになっています。

■エージェントの推論過程のイメージ

▶ ポートフォリオに含まれているか不明なので get_portfolio を呼び出しDBを確認
▶ 該当銘柄はDBに存在しなかった
▶ 現在のポートフォリオ情報 + LLMの学習知識で回答を生成

■ エージェント処理ログ&回答例

Tool: get_portfolio

Answer: 
ポートフォリオには AAPL / MSFT / GOOGL / AMZN / NVDA が含まれています。

Elon Musk関連企業の上場銘柄は Tesla Inc. (TSLA) のみです。
SpaceX・The Boring Company・X (旧Twitter) は非公開のため取引不可です。

TSLA をテクニカル分析しますか?

質問2: 「公開市場に上場しているものを分析して。」

この質問単体では「何を分析するか」が分かりません。 AgentCore Memory から前のターンの会話内容をロードすることで、LLMは「上場銘柄=TSLA」という文脈を復元して分析を実行します。

■エージェントの推論過程のイメージ

▶ AgentCore Memory から過去の会話ターンをロードしてLLMに渡す
▶ 前の会話から「上場銘柄=TSLA」と推論
▶ get_technical_analysis(ticker="TSLA") を呼び出し
▶ 返却データをもとに回答を生成

■ エージェント処理ログ&回答例

Tool: get_technical_analysis(ticker="TSLA")

Answer: 
現在価格: $415.88(前日比 -4.57%)
MA20: $422.65 / MA50: $392.51

→ 現在価格は MA20 を下回っており短期的な弱さを示していますが、
  MA50 を上回っており中期的な上昇トレンドは維持されています。
このときの Memory データ

AgentCore Memory の短期記憶には、ユーザー発言だけでなくツール呼び出しとその結果も含む全ターンが保存されます。 これにより、次のリクエスト時にエージェントが会話の文脈をフルに復元できます。

[turn 1] # ユーザーの最初の発言
role: USER
content: "Elon Muskさんの会社の銘柄を分析して。"

[turn 2] # エージェントがポートフォリオ取得ツールを呼び出した記録
role: ASSISTANT
content: [tool_use] get_portfolio

[turn 3] # ツールの返却値。ツール結果はUSERターンとして記録される
role: USER
content: [tool_result] [{'ticker': 'AAPL', ...}, {'ticker': 'MSFT', ...}, ...]

[turn 4] # エージェントの回答
role: ASSISTANT
content: "...分析可能な銘柄はTesla Inc. (TSLA)のみです。Tesla Inc. (TSLA)をテクニカル分析しますか?"

[turn 5] # 「上場しているもの=TSLA」という文脈はMemoryのturn 1〜4から復元される
role: USER
content: "公開市場に上場しているものを分析して。"

[turn 6] # 前の会話からTSLAと推論してツールを呼び出し
role: ASSISTANT
content: [tool_use] get_technical_analysis(ticker="TSLA")

[turn 7] # テクニカル分析ツールの返却値
role: USER
content: [tool_result] {'ticker': 'TSLA', 'current_price': 415.88, ...}

[turn 8] # 取得したデータをもとに分析コメントを生成
role: ASSISTANT
content: "**Tesla Inc. (TSLA) テクニカル分析結果** ..."

まとめ

各ケースを通じて、以下の点が確認できました。

ポイント 説明
ツール選択はLLMが判断 質問の内容に応じて、必要なツールを0〜複数呼び出す
曖昧な表現も解釈可能 「りんご社」「Elon Muskの会社」など自然言語での指定に対応
文脈は Memory で維持 セッションをまたいでも前の会話を参照できる
ツール呼び出しも Memory に記録 ユーザー発言だけでなく、ツール呼び出し・結果も含めた全ターンが保存される

4. 今後さらに深めたいこと

本記事では Strands Agents の基本的な使い方と AgentCore Memory を使った会話内容の記憶と脈絡引き継ぎ、Tool Use による単一エージェント構成を試しました。 ここでは、次のステップとして深掘りしたいと思った、興味深いと感じたトピックをまとめます。

高度なマルチエージェントパターンとAIオーケストレーション

今回は単一エージェントが複数ツールを直接呼び分ける Tool Use 構成を採用しました。 要件が複雑になると、エージェント自体をツールとして別のエージェントに委任する Agents as Tools や、エージェント同士が対等に協力する 協業型 (Peer-to-Peer) 構造が有効になる場面があります。

パターン 特徴 向いているケース
Tool Use(今回) 単一エージェントが複数ツールを直接呼び分ける シンプルな構成・ツール数が少ない場合
Agents as Tools サブエージェントをツールとしてオーケストレーターに登録し、作業を委任する中央集権型 ツールという簡単な構造を維持しつつ、ツールにAIエージェントの機能を持たせたい時
Peer-to-Peer 各エージェントが並列・相互に連携 複数の専門領域を並列処理したい場合 (SwarmやA2Aなど)

Strands Agents では Agent インスタンスをそのまま別の Agenttools に渡すことでマルチエージェント構成を組めます。パターン選択の判断基準については以下が参考になると思います。

dev.classmethod.jp

Hooks と Interrupts

エージェントが自律的に動く分、「どこで何をしているか把握したい」「特定の操作の前に人間の確認を挟みたい」というニーズが出てきます。 Strands Agents はこの2つを HooksInterrupts で実現しています。

Hooks — 開発者がループに介入する

エージェントのライフサイクルに対してコールバックを登録し、モニタリングや動作の変更ができます。

from strands.hooks import BeforeToolCallEvent

def before_tool(event: BeforeToolCallEvent):
    print(f"ツール呼び出し前: {event.tool.tool_name}")

agent.add_hook(BeforeToolCallEvent, before_tool)

利用できる主なイベント:

イベント タイミング
BeforeInvocationEvent エージェント呼び出し前
AfterInvocationEvent エージェント呼び出し後
BeforeToolCallEvent ツール呼び出し前
AfterToolCallEvent ツール呼び出し後
BeforeModelCallEvent LLM呼び出し前
AfterModelCallEvent LLM呼び出し後

strandsagents.com

Interrupts — ユーザーがループに介入する

ツール呼び出し前などに処理を一時停止し、人間の承認を求める Human-in-the-Loop ワークフローを実現します。

from strands.hooks import BeforeToolCallEvent

def require_approval(event: BeforeToolCallEvent):
    event.interrupt(name="approval_required", reason={"tool": event.tool.tool_name})

agent.add_hook(BeforeToolCallEvent, require_approval)

result.stop_reason == "interrupt" を検知してUIで承認ダイアログを出す、といった使い方が典型的です。 今回のポートフォリオ用途でも「売買操作ツールを追加した場合に確認を挟む」といった応用が考えられます。

strandsagents.com

AgentCore のさらなる活用

AgentCore Memory — 長期記憶とユーザー傾向の記憶

今回使った短期記憶(会話セッション内の会話内容の保持)に加え、AgentCore Memory は 長期記憶 もサポートしています。

戦略 内容
Summary Strategy 会話セッションを自動的に要約して蓄積
User Preference Strategy ユーザーの好みや傾向をセッションをまたいで学習
Semantic Strategy 発言から事実情報を抽出して保存

今回は短期記憶のみで「同一セッション内の文脈引き継ぎ」を実現しましたが、長期記憶を組み合わせると「以前の会話でユーザーがリスク許容度が低いと言っていた」などの情報を次のセッションでも活用できるようになります。

strandsagents.com

AgentCore Gateway

大規模なマルチエージェントシステムにおいて、エージェント間のルーティング・認証認可・可用性確保を担うコンポーネントです。 今回のような単一エージェント構成では不要ですが、複数のエージェントサービスを跨ぐ構成になった際に検討が必要になります。

AgentCore Runtime

エージェントのホスティングと可用性を管理するマネージドランタイムです。 今回は FastAPI をローカル/コンテナで動かしましたが、本番運用を考えると AgentCore Runtime を使ったスケーリングや可用性確保の検討が現実的になってきます。

セキュリティとトレーサビリティ

エージェントが自律的に動く分、「何をしたか追跡できること」と「有害な入出力を防ぐこと」の重要性が増します。

Guardrails — コンテンツの境界を定義する

Bedrock の Guardrails を Strands Agents と組み合わせることで、有害コンテンツのフィルタリングやトピック制限を適用できます。

BedrockModel(
    model_id=MODEL_ID,
    guardrail_id="your_guardrail_id",
    guardrail_version="1",
    guardrail_redact_input=True,
    guardrail_redact_output=True,
)

まず shadow mode(ログのみ・ブロックなし)で運用して影響範囲を確認してから有効化するのが実践的です。

strandsagents.com

PII Redaction — 個人情報をログに残さない

エージェントのトレース・ログには、ユーザーの発言内容やツールの返却値がそのまま記録されます。 氏名・メールアドレスなどの個人情報(PII)が含まれる場合は、Presidio や LLM Guard などを使ってログ出力前にマスク処理を挟む必要があります。

# 例: anonymize パイプラインでマスク後にトレース送信
masked_content = anonymizer.anonymize(content)
# → "太郎さんのメール" → "[REDACTED_PERSON]さんのメール"

strandsagents.com

Observability — エージェントの思考を可視化する

Strands Agents は OpenTelemetry ベースのオブザーバビリティをネイティブサポートしています。 モデル呼び出し・ツール実行・トークン使用量などのシグナルを収集でき、Grafana や Datadog などの既存ツールに接続できます。

収集できる主なシグナル:

種別 内容
Traces モデル呼び出し・ツール実行のスパン
Metrics レイテンシ・トークン使用量・エラー率
Logs 推論ステップ・ツール入出力の構造化ログ

エージェントが「なぜそのツールを選んだか」を事後に追えるようになるため、デバッグとコスト管理の両面で有効です。

strandsagents.com

おわりに

本記事では、Strands Agents と AgentCore Memory を使い、ツール連携と会話履歴管理を備えたエージェント型チャットボットの実装例を紹介しました。

実際に作ってみると、思っていたよりも手軽にエージェント型AIチャットボットを構築できました。特にAgentCore Memoryは、短期記憶による会話履歴の管理機能に加え、長期記憶として会話からユーザーの特性を読み取って次回の会話に引き継ぐ機能も備わっており、これらを自前で実装する手間を大きく省いてくれると感じました。

今回はシンプルな構成での実装でしたが、「今後さらに深めたいこと」で紹介させていただいたように、マルチエージェントや Guardrails などの機能も試していきたいと思います。

これから定期的に技術ブログを更新していくので、興味がある方は是非「読者になる」で応援していただけますと幸いです。今後ともローソンデジタルイノベーションをよろしくお願いいたします!