自然言語で話しかけることで、店頭の商品を探してそのまま取り置き予約できるショッピングAIのデモアプリを作成しました。今回はAmazon Bedrock AgentCore、Strands Agents SDK、Bedrock Knowledge Bases + S3 Vectorsを組み合わせ、商品検索から、受け取り予約までを 1 つの画面で完結させています。
まず全体構成図を先に出します。
アーキテクチャは大きく2経路に分けました。商品マスタの取得やSQL検索のような軽量APIはAPI Gateway + Lambdaとして応答速度を最優先とし、Bedrock推論が絡むチャットだけをLambda Function URLのRESPONSE_STREAM モード(SSE配信)に寄せました。API Gatewayの29秒ハード制限でLLM推論が切れるのを避けるための分離で、全体は上記の構成図を参照してください。
また、今回の採用技術とその役割、および不採用にした代替を表にまとめます。
| レイヤ | 採用 | 役割 | 不採用代替(理由) |
|---|---|---|---|
| フロント | React 19 / Vite / CloudFront + S3 | SPA 配信 | — |
| 同期 API | API Gateway + Lambda | マスタ取得・SQL 検索 | — |
| ストリーミング AI | Lambda Function URL (RESPONSE_STREAM) | SSE 配信(最大 15 分) | API Gateway: 29 秒ハード制限 |
| エージェント FW | Strands Agents SDK (TS) | ツール呼び出しループ SDK 内包 | Bedrock Agents(GUIベースで構築): CI/CD と相性悪 |
| モデル | Claude Sonnet 4 + Haiku 3 | デフォルト + 速度優先 | — |
| 会話履歴 | AgentCore Memory | stateful 化 | DynamoDB 自前: スキーマ運用発生 |
| RAG ベクトル | Bedrock KB + S3 Vectors(Titan v2 / 1024 次元) | 商品・レビューの意味検索 | OpenSearch Serverless: min 2 OCU ≒ 月 $172 |
| 構造化検索 | Athena + Glue Data Catalog + S3 CSV | SQL 検索 | — |
| 安全装置 | Bedrock Guardrails | PII 匿名化・業務外拒否 | アプリ側 NG ワード: 入出力トレースが残らない、対応範囲に限界あり |
| IaC | AWS CDK (TypeScript) | 開発環境構築 | — |
この構成で意図したことは 4 つあります。同期 API とストリーミング AI の経路分離、AgentCore の部品採用、LLM を呼ばない早期分岐の併設、そして 固定費最小のベクトルストア選定。1 つめはこの節で述べたとおりで、AgentCore の部品採用が次章、残り 2 つが次々章の主題になります。
AgentCore は Runtime / Memory / Gateway / Identity / Observability / Code Interpreter / Browser Tool の 7 コンポーネントで構成されるプラットフォームです。AgentCore と聞くと「Bedrock Agentsの新版」と受け取られがちですが、設計思想がまったく違います。Bedrock Agents がマネジメントコンソール上の GUI 操作で Agent を構築する SaaS 的サービスだったのに対し、AgentCore はコードベースで任意の Agent 実装を載せる PaaS 的サービスです。
今回のプロジェクトでは 7 コンポーネントのうち Memory だけを採用しました。採否は以下のとおりです。
| コンポーネント | 役割 | 本プロジェクト |
|---|---|---|
| Runtime | microVM セッション分離・最長 8 時間 | ❌ 直接DBを検索しに行く機構を残すため Lambda 資産を活用 |
| Memory | 短期 / 長期 / エピソード記憶 | ✅ 短期のみ採用 |
| Gateway | Lambda / OpenAPI を MCP ツール化 | ❌ Strands tool で直実装 |
| Identity | OAuth / IAM 経由の認証 | ❌ 今回は社内デモのため不要 |
| Observability | 分散トレース・メトリクス | ❌ CloudWatch で代替 |
| Code Interpreter | サンドボックス実行 | ❌ 用途なし |
| Browser Tool | マネージドブラウザ自動操作 | ❌ 用途なし |
この採否の判断軸は「検索経路を2経路確保」と「デモ版で開発期間もタイトだったため素早く対応する必要があったこと」の 2 つでした。Runtime を採らなかったのは、Bedrock 経由のエージェント経路に加えて、API Gateway + Lambda から直接 DB にクエリを投げる軽量な検索経路も残しておきたかったためです。Runtime に寄せると microVM ベースに統一する形になりますが、Lambda のまま残すことで 2 経路を並行して運用できる構成を維持しました。Gateway は、ツールが 3 本(search_products / query_database / search_reviews)のスケールでは Strands の tool() 直書きで実装が十分に収まり、限られた開発期間で仕様変更リスクも避けられるため見送りました。Observability も、CloudWatch Logs Insights で type=perf の構造化ログを串刺しする運用で必要な計測が取れており、デモ版の段階で新機能に寄せる必要はありませんでした。
採用した AgentCore Memory によって何が解決されたのかというと、チャットの会話履歴を DynamoDB で自前管理すると、スキーマ設計・ロール列の正規化など、地味な運用工数が必ず発生します。AgentCore Memory はマネージドのイベントストアを提供してくれて、CreateEvent と ListEvents の 2 API だけで済みます。
Strands Agent への差し込みもとても簡単でした。ListEvents API で過去の会話履歴を取得し、昇順にソートし直して HistoryMessage[] 形式に整形したうえで、既存の new Agent({...}) に messages プロパティとして渡すだけです。
ハマりどころは大きく 2 つありました。 1 つめはリソース名の制約です。AgentCore Memory のリソース名(CDK の CfnMemory の name プロパティ)は ^[a-zA-Z][a-zA-Z0-9_]{0,47}$ しか受け付けません。ハイフンが使えないため、慣例で付けた dev-chat-memory が CloudFormation の検証で弾かれたため、アンダースコアに書き換えて解決しています。 2 つめは ListEvents のデフォルト順序です。AWS の list 系 API の慣例で降順(新しい順)が返るため、降順のままLLMに渡してしまうと、AIが過去の会話を逆の時系列で読み取ってしまいます。その結果、最初の数回は偶然うまく会話が成立していても、ターンが進むにつれて文脈の前後関係が破綻し、突然ちぐはぐな回答をし始める原因となります。上のコードで sort を明示しているのはこのためです。
まとめると、既存構成の変更点は messages プロパティ 1 つ、IAM ポリシー 2 行、CDK の CfnMemory 1 リソース で済みます。AgentCore を検証するチームへの最小コストな入り口として、Memory だけ差し込むパターンは再現性が高いと感じています。
続いて、運用面で取り組んだ 2 つの施策について説明します。1 つめは体感レスポンスを改善するための "LLM を呼ばない設計"、2 つめはベクトルストアの常時課金を避けて固定費を約 9 割削減した構成です。
何より先に、段階別の所要時間を計測できる状態を作りました。これが無いと改善のたびに勘で動くことになります。Lambda ハンドラに仕込んだのは、traceId 付きの JSON 1 行ログです。
function logPerf(traceId: string, stage: string, details: Record<string, unknown> = {}) {
console.log(JSON.stringify({ type: "perf", traceId, stage, ...details }));
}
// stage 例: chat_start / bedrock_invoke_start / bedrock_invoke_end /
// kb_retrieve_end / agent_hard_timeout / chat_end
CloudWatch Logs Insights で type=perf クエリ → traceId で 1 リクエストを串刺し → 段階別の p50 / p99 が取れます。この足場のうえで、次の施策を Phase 0〜5 として実施しました。
| Phase | 施策 | 効果 |
|---|---|---|
| 0 | perf JSON ログ + traceId 仕込み | 計測の足場 |
| 1 | maxTokens: 1500 → 800、temperature: 0.2 → 0 |
各推論 5〜15 秒短縮 |
| 2 | systemPrompt にツール使用ルール明記、ツール戻り値圧縮 | 反復回数 5 → 2 回 |
| 3 | 早期分岐(ルールベース) | 該当クエリ 15〜100 秒 → 0.1 秒 |
| 4 | SSE 化 | first-token 18 秒 → 4 秒 |
| 5 | Function URL + RESPONSE_STREAM | 29 秒制限撤廃 |
最大の効果は Phase 3 の早期分岐 でした。ルールベースで答えられるクエリは Bedrock を呼ばないことで応答時間を15〜100秒から 0.1 秒に削減することができました。実装のイメージとしては以下のようになります。
const STORES_LIST_PATTERN = /^(店舗|店|お店)(一覧|教えて|どこ|の場所|を教え)/;
const PRODUCT_ID_PATTERN = /\b([a-z]+-\d{3,})\b/;
export async function tryEarlyResponse(message: string, traceId: string): Promise<EarlyResponse | null> {
if (STORES_LIST_PATTERN.test(message)) { /* /stores を直接返す */ }
const idMatch = message.match(PRODUCT_ID_PATTERN);
if (idMatch) { /* /products から該当 1 件だけ返す */ }
return null; // ここを通らないリクエストだけ Strands Agent へ
}
定型的なパターンに合致するクエリは、モデル経由ではなくルールベースで直接応答することで、応答時間を大きく短縮できます。すべてのクエリを LLM に委ねる必要はなく、判定が確定しているものは事前に分岐させる方針を取っています。 ただし、現在のパターンマッチングは表現の揺れを完全には吸収しきれず、本来は早期分岐に乗せるべき問い合わせを取りこぼしたり、逆に意図の異なる質問を誤って早期応答に振り分けてしまう可能性が残ります。今回はその点を許容したうえで採用していますが、対応する意図(インテント)が増えるほどルールでの正確な振り分けは難しくなり、メンテナンス性も悪化していきます。そのため将来的には、この分岐処理自体を高速・軽量な LLM を利用した「LLM ルーター」など別の方式に置き換え、質問の意図に応じて動的に処理を振り分けるスケーラブルなアーキテクチャへの進化を検討しています。
体感改善のもう一つの軸は Phase 4 のストリーミング UX 設計 です。first-token 18 秒 → 4 秒は、実時間ではなく "待たされた感" を抑えるための施策です。text-delta のチャンクを受信したら即プレースホルダーを解除し、tool-use イベントで「(商品を検索しています…)」のヒントを末尾に追加し、最後に final で AiResponseSchema の全体に差し替えます。チャット型のUIでは、無反応な待ち時間がそのままユーザーの不安につながりやすいため、テキストだけでなくツール呼び出しの進行メタ情報もあわせて配信する構成にしています。
ハードタイムアウトも環境別に設定しています(ローカル 60s / API Gateway 25s / Function URL SSE 50s)。このストリーミング実装ではハマりどころが 2 つありました。1 つめはタイムアウト処理の二重設定です。setTimeout + agent.cancel() の自前打ち切りだけでは Strands の agent.cancel() が cooperative なため Bedrock ストリームが即座に閉じません。過去にモデル側の暴走で Lambda の billed time が伸びた経験から、NodeHttpHandler.requestTimeout を併設して対処しました。2 つめは printer フラグの見落としです。printer: true のまま SSE 配信すると Strands の標準出力と SSE 側で二重にテキストが出てしまうため、Lambda 側では必ず printer: false にします。
ベクトルストアは Bedrock Knowledge Bases から扱える 3 つの選択肢(S3 Vectors / OpenSearch Serverless / Aurora pgvector)を比較して、S3 Vectors を選びました。理由は明確で、OpenSearch Serverless の min 2 OCU ≒ 月 $172 の常時課金 が、「作って止める」デモ検証のサイクルに耐えないからです。 ただし、S3 Vectorsにはコストが安い分、機能面でのトレードオフも存在します。最大の懸念点は、キーワード検索とベクトル検索を組み合わせる 「ハイブリッド検索」に対応していない ことです。 今回のデモではセマンティック検索のみで十分機能しましたが、本番環境において「特定の品番(例:ABC-123)や固有名詞を完全に一致させたい」といった精度要件が厳しくなった場合は、ハイブリッド検索を利用できる OpenSearch Serverless への移行が有力な選択肢になります。
| 観点 | S3 Vectors | OpenSearch Serverless | Aurora pgvector |
|---|---|---|---|
| 最小コスト | 0(従量のみ) | ~$172/月(2 OCU) | ACU 常時課金 |
| レイテンシ | 高め | 低 | 中 |
| ハイブリッド検索 | ✕(セマンティックのみ) | ○ | ○ |
| 管理負担 | 少 | コレクション管理 | VPC / スキーマ管理 |
| デプロイ時間 | 数秒 | 5〜10 分 | 数分 |
| Bedrock KB 対応 | ○ | ○ | ○ |
構成としては、商品用とレビュー用の 2 インデックスを 1 つのベクトルバケットの下に置き、Bedrock Knowledge Base からそれぞれを参照する形にしています。
S3 Vectors にもハマりどころは 2 つありました。1 つめは CSV 1 ファイル投入で混在チャンクです。KB は CSV 全体を 1 ドキュメントとしてチャンク化するため、「ハンマー」で検索すると「水性ペンキ」「防水スプレー」などが上位に混入する事故が起きました。解決策は 1 商品 = 1 ファイル(JSON) に変更することで、KB のベストプラクティスとしても広く言われているとおりでした。2 つめは KBRole の IAM 権限漏れです。s3vectors:GetVectors / ListVectors / QueryVectors / PutVectors / DeleteVectors / GetIndex のどれか 1 つでも抜けていると、KB 作成時点で InvalidRequest でロールバックします。エラーメッセージがやや読みづらいため、IAM の網羅を最初に済ませておくのがおすすめです。
結果として、デモ稼働中の RAG 固定費はほぼ 0 円(アクセス従量のみ)になりました。デプロイ〜削除のサイクルが数秒で回るため検証の回転数も上がっています。「本番データ量がもっと増えたときに OpenSearch Serverless と再比較する」は今後の課題として残しています。
本記事では、AgentCore の7コンポーネントから Memory のみを採用し、Strands Agent に会話履歴を差し込む形でショッピング AI を実装しました。あわせて、早期分岐による定型クエリの高速化(15〜100秒 → 0.1 秒)、S3 Vectors によるベクトルストアの固定費削減(ほぼ 0 円)を組み込んでいます。
AgentCore は全体として導入しなくとも、コンポーネント単位で組み合わせることで十分に機能します。また、AI エージェントであってもすべてのクエリをモデルに委ねる必要はなく、ルールベースで判定できる部分を事前に分岐させることが、応答時間と運用コストの両面で有効でした。
| 技術 | 役割 | 今回の使いどころ |
|---|---|---|
| Amazon Bedrock AgentCore Memory | 会話履歴の短期記憶 | マネージドで 2 API のみ。Strands Agent に 1 行で注入可能 |
| Strands Agents SDK (TypeScript) | エージェントのツール呼び出しループ | ReAct 相当の処理が SDK に内包。ツール定義に集中できる |
| Bedrock Knowledge Bases + S3 Vectors | RAG のセマンティック検索 | 固定費ほぼ 0 円で RAG 基盤を構築 |
| Lambda Function URL (RESPONSE_STREAM) | SSE ストリーミング配信 | API Gateway の 29 秒ハード制限を回避 |
| Bedrock Guardrails | PII 匿名化・業務外拒否 | 入出力トレース付きでチャンク単位評価が可能 |
| Claude Sonnet 4 + Haiku 3 | LLM 推論 | デフォルトは Sonnet 4、速度優先時は Haiku 3 |
Amazon Bedrock AgentCore
Strands Agents
Amazon Bedrock Knowledge Bases / S3 Vectors
Amazon Bedrock Guardrails
Lambda Function URL
AWS CDK