英会話の練習相手が欲しいだけなのに、既存アプリは月額課金が重かったり、会話の自由度が低かったりして「自分の欲しい形」にたどり着けなかった。特に「言い淀んでも待ってくれる」「文法を直しつつ自然に会話を続けてくれる」相手が欲しかった。
この記事では、Amazon Bedrock AgentCore と Strands Agents SDK を使って、AI と音声で英会話練習ができるアプリを個人開発した過程を書く。想定読者は「Bedrock AgentCore を実際に触ってみたい人」と「Strands Agents でエージェントを組んだコードを見たい人」。
自分の環境
| 項目 | バージョン / 内容 |
|---|---|
| Python | 3.12.4 |
| strands-agents | 0.1.6 |
| bedrock-agentcore | 0.1.2 |
| boto3 | 1.34.144 |
| FastAPI | 0.111.0 |
| React Native (Expo) | SDK 51 |
| 利用モデル | Anthropic Claude 3.5 Sonnet (Bedrock 経由) |
| デプロイ先 | AgentCore Runtime |
音声認識・音声合成はスマホ側のネイティブ API(Expo Speech / expo-av)を使い、会話ロジックだけを AgentCore 上のエージェントに任せる構成にした。
実装
1. Strands Agents でエージェントを定義する
会話の役割(英会話コーチ)と、文法チェック用のツールを持たせたエージェントを作る。Strands Agents はツール定義がシンプルで、Python の関数に @tool を付けるだけで済む。
# agent/coach_agent.py
from strands import Agent, tool
from strands.models import BedrockModel
# Bedrock 経由で Claude 3.5 Sonnet を呼び出すモデル定義
model = BedrockModel(
model_id="anthropic.claude-3-5-sonnet-20240620-v1:0",
region_name="us-west-2",
temperature=0.7,
)
@tool
def check_grammar(sentence: str) -> str:
"""
英文の文法チェック結果を返す簡易ツール。
実際の判定はモデル自身の推論に任せ、ここでは前処理のみ行う。
"""
trimmed = sentence.strip()
if not trimmed:
return "空の文章です。もう一度話してみてください。"
if len(trimmed.split()) < 2:
return "短すぎる文章です。もう少し詳しく話してみましょう。"
return f"'{trimmed}' を文法チェック対象として受け付けました。"
SYSTEM_PROMPT = """
あなたはフレンドリーな英会話コーチです。
ユーザーの発話に対して、まず英語で自然に返答し、
その後に簡潔な日本語で文法アドバイスを1文添えてください。
ユーザーが言い淀んでも急かさず、次の話題につながる質問を1つ添えてください。
"""
coach_agent = Agent(
model=model,
tools=[check_grammar],
system_prompt=SYSTEM_PROMPT,
)
def run_conversation_turn(user_utterance: str) -> str:
"""1ターン分の会話を実行し、エージェントの応答テキストを返す"""
response = coach_agent(user_utterance)
return str(response)
@tool デコレータを付けた関数は、エージェントが必要と判断したタイミングで自動的に呼び出される。今回はモデルにほぼ判断を任せる設計にしたため、ツール自体は最小限にした。
2. AgentCore Runtime にデプロイする
Strands のエージェントをそのまま AgentCore にホストする。bedrock-agentcore SDK の BedrockAgentCoreApp でラップし、エントリーポイントを定義する。
# agent/app.py
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from coach_agent import run_conversation_turn
app = BedrockAgentCoreApp()
@app.entrypoint
def invoke(payload: dict) -> dict:
"""
AgentCore Runtime から呼ばれるエントリーポイント。
payload 例: {"user_utterance": "I go to school yesterday."}
"""
user_utterance = payload.get("user_utterance", "")
if not user_utterance:
return {"error": "user_utterance is required"}
reply = run_conversation_turn(user_utterance)
return {"reply": reply}
if __name__ == "__main__":
app.run()
デプロイは agentcore CLI から行う。ローカルでの動作確認とデプロイの流れは以下の通り。
# ローカルでエントリーポイントを起動して疎通確認
python agent/app.py &
curl -X POST http://localhost:8080/invocations \
-H "Content-Type: application/json" \
-d '{"user_utterance": "I go to school yesterday."}'
# 動作確認できたら AgentCore にデプロイ
agentcore configure --entrypoint agent/app.py
agentcore launch
# デプロイ後のエンドポイントを呼び出す(region / agent-id は実際の値に置き換え)
agentcore invoke '{"user_utterance": "Can you help me practice ordering coffee?"}'
agentcore launch は内部でコンテナイメージのビルドと ECR への push、AgentCore Runtime へのデプロイまでを一括で行ってくれる。初回は数分かかるが、2 回目以降はレイヤーキャッシュが効いて短縮される。
3. FastAPI 経由で AgentCore を呼び出す薄いバックエンド
スマホアプリから直接 AgentCore を叩くのではなく、FastAPI を間に挟んで認証とレート制限をかけている。
# backend/main.py
import os
import json
import boto3
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
app = FastAPI(title="English Coach Backend")
AGENT_RUNTIME_ARN = os.environ["AGENTCORE_RUNTIME_ARN"]
client = boto3.client("bedrock-agentcore", region_name="us-west-2")
class TurnRequest(BaseModel):
user_utterance: str
session_id: str
@app.post("/conversation/turn")
async def conversation_turn(req: TurnRequest) -> dict:
if not req.user_utterance.strip():
raise HTTPException(status_code=400, detail="user_utterance is empty")
try:
response = client.invoke_agent_runtime(
agentRuntimeArn=AGENT_RUNTIME_ARN,
runtimeSessionId=req.session_id,
payload=json.dumps({"user_utterance": req.user_utterance}).encode("utf-8"),
)
body = json.loads(response["response"].read())
except Exception as exc:
raise HTTPException(status_code=502, detail=f"agent invocation failed: {exc}")
return {"reply": body.get("reply", "")}
runtimeSessionId にユーザーごとのセッション ID を渡すことで、AgentCore 側が会話の文脈を保持してくれる。フロントエンドは会話開始時に UUID を発行し、以降のリクエストで使い回すだけでよい。
ハマったところ
1. invoke_agent_runtime のレスポンスが StreamingBody で一度しか読めない
最初、レスポンスをログ出力してからパースしようとして body.read() を2回呼び、2回目が空文字になって json.loads が失敗した。
# NG: read() を2回呼ぶと2回目は空になる
raw = response["response"].read()
print(raw) # デバッグ用に1回目の read()
body = json.loads(response["response"].read()) # ここが空文字で失敗する
# OK: 1回だけ read() して変数に保持する
raw = response["response"].read()
print(raw)
body = json.loads(raw)
boto3 の StreamingBody はファイルのようなオブジェクトで、一度読み切るとカーソルが戻らない。エラーメッセージは json.decoder.JSONDecodeError: Expecting value とだけ出るので、原因が読み取り済みストリームだと気づくまで時間がかかった。
2. Strands の @tool 関数の docstring を省略するとツール選択の精度が落ちる
check_grammar を実装した当初、docstring を書かずにいたところ、エージェントがツールをほぼ呼び出さず素の応答だけを返す状態になった。
# NG: docstring がないとツールの用途がモデルに伝わらない
@tool
def check_grammar(sentence: str) -> str:
trimmed = sentence.strip()
...
# OK: docstring に用途と引数の説明を書く
@tool
def check_grammar(sentence: str) -> str:
"""
英文の文法チェック結果を返す簡易ツール。
Args:
sentence: チェック対象のユーザー発話(英文)
"""
trimmed = sentence.strip()
...
Strands はツールの docstring をそのままモデルへのツール説明として渡す仕様になっている。関数名だけでは意図が伝わらず、モデルが「呼ぶべきタイミング」を判断できていなかった。
3. AgentCore のセッション ID を使い回さないと毎回文脈が消える
会話が1ターンごとに前の内容を忘れる不具合が出て、最初は Claude のモデル設定を疑った。実際の原因はフロント側で session_id を毎回 uuid4() で生成し直していたことだった。
# NG: リクエストごとに新しい session_id を発行すると文脈が保持されない
import uuid
session_id = str(uuid.uuid4()) # 毎回呼ばれると意味がない
# OK: 会話開始時に1回だけ発行し、以降は使い回す
# (React Native 側で useState に保持し、conversation 終了までそのまま使う)
AgentCore Runtime は runtimeSessionId をキーに会話状態を保持しているため、ID が毎回変わると新規セッション扱いになり文脈が引き継がれない。FastAPI 側では気づけず、React Native 側の実装を見直して解決した。
結果
| 指標 | 数値 |
|---|---|
| 開発期間(要件定義〜リリース) | 約 6 週間(副業ペース) |
| 1ターンあたりの応答時間(平均) | 約 2.1 秒(Bedrock 呼び出し込み) |
| AgentCore の月間呼び出し回数(自分のテスト利用) | 約 800 回 |
| Bedrock 利用料金(Claude 3.5 Sonnet、1ヶ月) | 数百円程度(自分のテスト範囲) |
| クラッシュ報告(リリース後2週間) | 0 件 |
応答時間 2.1 秒は「間が空きすぎて不自然」と感じるラインの手前で収まっている印象で、体感の会話テンポとしては許容範囲だった。ただし込み入った長文を投げると 3 秒を超えることがあり、ここは今後モデルの max_tokens 調整や会話履歴のトリミングで改善したい。
料金面は個人のテスト利用の範囲なので、公開後にユーザーが増えた場合のコスト試算はまだ検証中。AgentCore Runtime 自体の課金体系(呼び出し回数・実行時間ベース)も踏まえて、今後継続的にモニタリングする予定。
参考リンク
- Amazon Bedrock AgentCore 公式ドキュメント
- Strands Agents SDK 公式ドキュメント
- Amazon Bedrock — Anthropic Claude モデル一覧
- boto3 — bedrock-agentcore クライアントリファレンス
- Expo — expo-av 音声再生ドキュメント
関連リンク
AutoTrader 実装学習キット (FastAPI × React Native)
by ぽん (@pon_freelance)