この記事は NewsPicks Advent Calendar 2025 の11日目の記事です。
はじめに
こんにちは!ソーシャル経済メディア「NewsPicks」エンジニアの平岡(@Dhira24kobe)です!
今回、週次で数千件のデータを処理するAI分析基盤を構築する機会があり、Amazon Bedrockのバッチ推論を採用しました。
この記事では、Bedrockバッチ推論を選定する際に意識すべきポイントと、実際に運用してわかった課題・解決策を紹介します。
バッチ推論の導入を検討している方の参考になれば幸いです!
※ 本記事は2025年12月時点の情報です。Bedrockは頻繁にアップデートされるため、最新の仕様は公式ドキュメントをご確認ください。
Amazon Bedrockのバッチ推論とは
Amazon Bedrockのバッチ推論は、大量のリクエストを非同期でまとめて処理する機能です。
JSONLファイルをS3に配置してジョブを開始すると、AWS側がスケジューリングと実行を管理します。オンデマンド推論と比較して以下の特徴があります。
| 方式 | 特徴 | 懸念点 |
|---|---|---|
| オンデマンド推論 | シンプル、即時実行 | クォータ制限、コスト高 |
| バッチ推論 | コスト50%割引、スロットリング対策不要 | 非同期、最小100件制約 |
オンデマンド推論は実装がシンプルですが、数千件を処理する場合はクォータ(リクエスト数/分)などを意識する必要があります。リトライやスロットリング対策の実装が必要になり、複雑さが増します。
一方、バッチ推論はAWS側がスケジューリングを管理するため、スロットリングを意識した実装が不要です。コストも50%削減できるため、リアルタイム性が不要なユースケースに適しています。
なぜバッチ推論を選んだのか
今回のAI分析基盤には、以下の要件がありました。
- 週次で数千件のデータを処理
- 日曜日に実行し、月曜日までに完了すればOK
- LLMによる推論の中でtoolの使用が必要なく、1つのプロンプトに対する回答を得られれば良い
リアルタイム性が不要で、まとまった件数を処理するユースケースです。
NewsPicksではインフラ基盤としてAWSを全面的に採用しているため、今回もAWSサービスに限定して検討しました。上記の要件とバッチ推論の特徴を照らし合わせた結果、バッチ推論が最適と判断しました。
全体アーキテクチャ
今回のシステムは、AWS公式サンプル「Bedrock Batch Orchestrator」を参考に構築しました。
① Worker: RDSから集計データを取得し、Bedrock用のJSONL形式に変換してS3にアップロード
② SQS → EventBridge Pipes: JSONLの準備完了を通知し、Step Functionsワークフローを起動
③ ListInputFiles Lambda: S3から処理対象のJSONLファイル一覧を取得
④ Map(並列処理): 各ファイルに対してバッチ推論ジョブを並列実行
⑤ StartBatchInference Lambda: Bedrockバッチ推論ジョブを開始し、taskTokenをDynamoDBに保存
⑥ Bedrock: バッチ推論を実行し、結果をS3に出力
⑦ EventBridge → SyncJobStatus Lambda: ジョブ変更イベントを検知し、Step Functionsにコールバック
⑧ SaveToRds Lambda: 全ファイルの処理完了後、結果をRDSに保存(エラーが発生した場合はSlackに通知)
ポイントは、Map状態による並列処理とコールバックパターンの組み合わせです。複数のJSONLファイルを同時に処理しつつ、各ジョブの完了を非同期で待機できます。(コールバックパターンを採用した理由は「課題2」セクションで詳しく説明します)
コールバックパターンの詳細
Bedrockのバッチ推論は数時間かかることがあります。単純なポーリング(定期的にステータスを確認)では無駄なAPI呼び出しが発生するため、Step Functionsのコールバックパターンを採用しました。
並列処理内のサービスの補足を以下に記載します。
StartBatchInference Lambda
-
Bedrock CreateModelInvocationJob APIでバッチ推論ジョブを開始 - Step Functionsから渡された
taskTokenを、ジョブIDをキーとしてDynamoDBに保存 - Lambdaは即座に終了(ジョブ完了を待たない)
Step FunctionsはWAIT_FOR_TASK_TOKENの設定により、コールバックされるまで待機状態になります。
なぜDynamoDBが必要か
EventBridgeから呼び出されるSyncJobStatus Lambdaは、Step Functionsのコンテキスト外で実行されます。 Step Functionsに完了を通知するにはtaskTokenが必要ですが、EventBridgeのイベントには含まれていません。
そのため、StartBatchInference実行時にtaskTokenをDynamoDBへ保存しておく必要があります。
SyncJobStatus Lambda
EventBridgeがBedrockジョブの状態変更(Completed、Failedなど)を検知すると、このLambdaがトリガーされます。
- イベントからジョブIDとステータスを取得
- DynamoDBからtaskTokenを取得
- 3つバリデーション処理(後述)
- ステータスに応じて
SendTaskSuccessまたはSendTaskFailureを呼び出し
これにより、Step Functionsの待機が解除され、次のステップへ進みます。
リトライ機構
リトライを行う方法としては2パターンを想定して設計しています。
- Map処理内でエラーが発生し、
SendTaskFailureが送られた時 - 手動でStep Functionsを実行する時
基本的には、前者のMap処理内でエラーが起きた場合に自動で4回まで同じJSONLファイルに対してリトライを行うようにStep Functionsで定義しています。
Map処理内でエラーが起きる原因として挙げられるのは、バッチ推論ジョブ内でのエラー、バリデーションのエラー(「課題3」セクションで詳細を記載)です。
これ以外のパターンとして考えられるのは、
- Map処理の前段階で失敗した
- Map処理は成功したが、RDSへの保存でエラーが起きた
- 4回とも自動リトライが失敗した
このような時はSlackに通知が来て、手動でリトライを実行するような運用になっています。手動で実行した際に、成功していたJSONLファイルに対してももう一度バッチ処理が行われてしまったら、コストが無駄になってしまいます。
そうならないために、Map処理前のListInputFiles LambdaでDynamo DBからS3ファイルのURIを使用して、対象のJSONLファイルのジョブのステータスがCompletedの状態で保存されていた場合はスキップするという処理が入っています。
ぶつかった3つの課題
実際に実装を進めていくにあたって、Amazon Bedrockのバッチ推論を本番運用していこうとした時に、出会った大きな課題点の3つを紹介しようと思います。
課題1: 最小100レコード制約
課題の内容
Bedrockバッチ推論には「1ジョブあたり最小100レコード」という制約があります。99レコード以下のJSONLファイルでジョブを開始すると、バリデーションエラーで失敗します。
週次で数千件を処理するため1,000件単位でバッチを分割しますが、最終バッチが100件未満になる可能性がありました。
解決策: 前バッチとのマージ
この問題に対し、100件未満の端数が発生した場合は前のバッチとマージするというアプローチを採用しました。
具体的な処理フローは以下のとおりです。
- レコードを1,000件単位でJSONLファイルに分割
- 最終バッチのレコード数を確認
- 100件以上 → そのままアップロード
- 100件未満 & 前バッチあり → 前バッチをS3からダウンロードしてマージ
- 100件未満 & 前バッチなし → エラー(全体で100件未満のためバッチ推論不可)
| データ件数 | 最終バッチ | 対応 | 結果 |
|---|---|---|---|
| 2,350件 | 350件 | そのままアップロード | ✅ |
| 2,050件 | 50件 | 前バッチとマージ | ✅ |
| 80件 | 80件 | マージ先なし | ❌ |
なぜ1,000件単位なのか
バッチサイズを1,000件に設定した理由は、並列処理の効率とマージ時のオーバーヘッドのバランスです。
並列処理: Step FunctionsのMap状態で複数ジョブを同時実行できる
マージ頻度: 端数が100件未満になる確率を抑えつつ、マージ時のファイルサイズを適切に保つ
バッチサイズが大きすぎると並列性が下がり、小さすぎるとマージ頻度が増えます。運用データを分析した結果、1,000件が適切なバランスでした。
課題2: 長い待ち時間への対処
課題の内容
Bedrockバッチ推論は、ジョブ開始から完了まで数時間かかることがあります。
単純なポーリング(定期的にステータスを確認)で待機する方法もありますが、以下の問題があります。
- 無駄なAPI呼び出しが発生する
- ポーリング間隔が長いと完了検知が遅れる
- ポーリングのたびに状態遷移が発生し、Step Functionsのコストが増加する
解決策: コールバックパターン
この問題に対し、Step Functionsの コールバックパターン(Wait for Task Token) を採用しました。
ポーリングとコールバックの違いは以下のとおりです。
コールバックパターンの実装詳細は「全体アーキテクチャ」セクションで説明したとおり、DynamoDBを使ってtaskTokenを管理しています。
効果
- API呼び出し回数: ジョブ開始時と完了時の2回のみ
- Step Functions: 待機中は課金されない
- 完了検知: EventBridgeがリアルタイムで検知
課題3: 出力品質の担保
課題の内容
バッチ推論では、数千件のリクエストを一括で処理します。しかし、すべてのレコードが正常に処理されるとは限りません。
- Bedrockの推論自体が失敗するケース
- Guardrailによってコンテンツがブロックされるケース
- LLMの出力が期待したJSON形式になっていないケース
オンデマンド推論であれば、失敗したリクエストをその場でリトライできます。しかしバッチ推論では、ジョブ完了後にまとめて結果を確認するため、どのレコードが失敗したかを正確に把握する仕組みが必要です。
解決策: 3段階バリデーション
失敗の検知と適切なリトライを実現するため、3段階のバリデーションをSyncJobStatus Lambdaに実装しました。
| 段階 | 検証内容 | 失敗時の対応 |
|---|---|---|
| 1. Manifest | 推論レベルの失敗を検知 | リトライ |
| 2. JSON | 出力形式の異常を検知 | リトライ |
| 3. Guardrail | 不適切なコンテンツを検知 | リトライ |
1. Manifestバリデーション
Bedrockはバッチ推論完了時にmanifest.json.outを出力します。このファイルには処理結果のサマリーが含まれています。
{
"totalRecordCount": 1000,
"processedRecordCount": 1000,
"successRecordCount": 998,
"errorRecordCount": 2
}
errorRecordCountが1件でもあれば、ジョブを失敗として扱い、Step Functionsのリトライを実行します(リトライはバッチ推論ジョブ単位)。
2. JSONバリデーション
LLMの出力が期待したJSON形式になっているか検証します。
- JSON形式としてパース可能か
- 必須フィールドが存在するか
形式が不正な場合はジョブを失敗として扱い、リトライを試みます。LLMの出力は確率的なため、リトライで正常な形式が得られることが多いです。
3. Guardrailバリデーション
ここがバリデーションの中で特色がある部分だと思います。
やっていることとしてはBedrockのGuardrailsを使って、出力内容が適切かチェックします。不適切なコンテンツがブロックされた場合は、ジョブを失敗として扱い、リトライを試みます。
BedrockのGuardrailは基本的には、InvokeModelやConverseではパラメータに指定することで利用できますが、バッチ推論ではそのようなパラメータを指定することができないので、使用することができません。
この問題を解決するために使用したのが、GuardrailsのApplyGuardrailです。
以下の記事を参考にさせていただきました。
このBedrockのAPIはLLMと関係なく、APIに渡された文字列に対してGuardrailのフィルタを適用することができるものです。
今回のユースケースとしては、バッチ推論によって出力されたJSONLファイルから出力結果を1レコードずつ取得して、ApplyGuardrail APIの入力に入れて呼び出しました。
これにより、出力される文章の品質担保するフィルタをバッチ推論に入れることができました。
(入力はユーザーからの入力を使用することはないので、フィルタしていません)
効果
- 失敗したジョブを自動でリトライ
- 不適切なコンテンツの混入を防止
- 問題発生時の原因特定が容易
予想外だったこと
バッチ推論は50%のコスト削減が魅力ですが、実装中に想定外のコスト増加を経験しました。
プロンプトキャッシュが使えなくなった
当初(2025/09/25)、JSONLファイル内でシステムプロンプトキャッシュ(cache_control)を指定して問題なく実行できていました。同一バッチ内でシステムプロンプトが共通であればキャッシュが効き、入力トークンのコストが大幅に削減されるはずでした。
{
"recordId": "CALL0000001",
"modelInput": {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 1024,
"system": [
{
"type": "text",
"text": "システムプロンプト...",
"cache_control": { // システムプロンプト設定
"type": "ephemeral"
}
}
],
"messages": [...]
}
}
しかし、その後の検証で、バッチ推論ではプロンプトキャッシュが使用できない仕様であることが判明しました。AWSサポートに確認したところ、もともとキャッシュが効いていた状態が想定外の動作だったとのことでした。
本番導入前にはコスト面の検証を十分に行い、不明点があればAWSサポートに相談しながら進めることをお勧めします。
まとめ
今回はAmazon Bedrockのバッチ推論を本番運用することについて書きました。
Bedrockのバッチ推論は大規模AI処理に関してはコストを単純に半分に抑えられる素晴らしいサービスです。
今後バッチ推論を利用して大量のデータをAI分析することが当たり前の世の中になっていくと考えており、さまざまな手段の中から、Amazon Bedrockを選定検討の参考にしていただければと思います。
※ 書ききれなかった部分ではありますが、本番運用していくにあたって監視・評価に関しても考える必要があります。
