Amazon Bedrockのバッチ推論で14,000件の記事要約を効率化してみた
約6万件の技術ブログ記事、インデックスやSEO対策に利用する記事の要約テキストの作成にLLMを利用する機会がありました。
当初はオンデマンドでBedrockを利用していましたが、コストと時間の課題が顕在化。
途中でAmazon Bedrockのバッチ推論への切り替えを試み、処理コストを50%削減。処理効率やスループットを改善できたので、その内容を紹介します。
バッチ推論を選んだ理由
オンデマンド実行だとコストが高く(1万件あたり約$40)、処理時間も長い(約11件/分 → 1万件あたり約15時間)という課題がありました。リアルタイム処理は不要だったため、バッチ推論への切り替えを検討しました。
Amazon Bedrockのバッチ推論には以下の特徴があります:
- コスト: オンデマンドの50%
- 処理時間: 24時間以内に完了(自動スケール)
大量データを一括処理し、リアルタイム応答が不要な場合に最適です。
今回はClaude Haiku 4.5(2025年10月リリース)をus-west-2(Oregon)リージョンで利用しました。
事前準備
データ準備
CMSから入手した記事レコードを処理し、要約生成に必要な情報を抽出しました。
クリーニング内容:
- HTMLタグ除去(BeautifulSoup使用)
- コードブロック除去(正規表現)
- 画像リンク除去
- Markdown記法の正規化
AWS環境準備
1. S3バケット作成
aws s3 mb s3://my-bedrock-batch-bucket --region us-west-2
バケット構成:
s3://my-bedrock-batch-bucket/
├── bedrock-batch/input/ # 入力JSONL
└── bedrock-batch/output/ # 出力結果
2. IAMロール作成
信頼ポリシー (trust-policy.json):
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Service": "bedrock.amazonaws.com"
},
"Action": "sts:AssumeRole"
}]
}
権限ポリシー (permissions-policy.json):
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bedrock-batch-bucket/*"
},
{
"Effect": "Allow",
"Action": [
"bedrock:InvokeModel"
],
"Resource": "*"
}
]
}
ロール作成コマンド:
# ロール作成
aws iam create-role \
--role-name BedrockBatchInferenceRole \
--assume-role-policy-document file://trust-policy.json
# 権限付与
aws iam put-role-policy \
--role-name BedrockBatchInferenceRole \
--policy-name BedrockBatchS3Access \
--policy-document file://permissions-policy.json
よくあるミス: IAM権限不足
バッチ推論では権限エラーが、実行を開始した10-15分後に判明します。特にクロスリージョン推論を使用する場合、推論プロファイルのARNが含まれるため、リソース指定が複雑になりがちです。
今回は bedrock:InvokeModel のリソースを "*" に設定することで、権限不足に起因するエラーを回避しています。
リソースを絞る必要がある場合には、以下に注意してください:
- モデルARN(
arn:aws:bedrock:*::foundation-model/*) - Inference ProfileのARN(
arn:aws:bedrock:*:ACCOUNT_ID:inference-profile/*) - Cross-Region Inference Profileの場合、リージョンやアカウントIDの指定が複雑
最小権限の原則に則った設定で利用する場合、バッチ推論の最小課金単位(1,000件)でテスト実行することを推奨します。
バッチ入力JSONL形式
バッチ推論の入力はJSONL(JSON Lines)形式です。1行に1つのJSONオブジェクトを記述します。
形式例:
{
"recordId": "wp-post-12345",
"modelInput": {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 4096,
"temperature": 0.1,
"system": "あなたは技術ブログの要約専門家です。記事から日本語と英語の要約を生成し、JSON形式で出力してください。",
"messages": [
{
"role": "user",
"content": "タイトル: Amazon Bedrockの使い方\n\n記事:\n本文テキスト..."
}
]
}
}
JSONL作成スクリプト(簡略版):
import json
import boto3
from bs4 import BeautifulSoup
import re
def clean_text(text):
"""HTMLタグやMarkdownを除去し、バッチ投入用に整形"""
soup = BeautifulSoup(text, 'html.parser')
text = soup.get_text()
text = re.sub(r'```[\s\S]*?```', '', text)
text = re.sub(r'\s+', ' ', text).strip()
return text[:50000] # 1件あたり50,000文字以内に制限
# 記事データ読み込み
articles = load_articles() # CMSから取得した記事データ
# JSONL作成
with open('batch_input.jsonl', 'w', encoding='utf-8') as f:
for article in articles:
record = {
"recordId": article['id'],
"modelInput": {
"anthropic_version": "bedrock-2023-05-31",
"max_tokens": 4096,
"temperature": 0.1,
"system": "あなたは技術ブログの要約専門家です。記事から日本語と英語の要約を生成し、JSON形式で出力してください。",
"messages": [{
"role": "user",
"content": f"タイトル: {article['title']}\n\n記事:\n{clean_text(article['content'])}"
}]
}
}
f.write(json.dumps(record, ensure_ascii=False) + '\n')
print(f"✓ JSONL作成完了: {len(articles)}件")
バッチサイズの調整
バッチ推論にはAWSのクォータ(制限値)があります。これに収まるよう、バッチサイズを調整しました。
AWS Bedrockのクォータ
| 項目 | 制限値 |
|---|---|
| 最小レコード数 | 1,000件 |
| 最大レコード数 | 50,000件 |
| 最大ファイルサイズ | 200MB |
| 最大ジョブサイズ | 1GB |
最適なバッチサイズの算出
対象記事は約15,000件です。テストで1,000件を実行後、残りをクォータ範囲内で一括実行する計画を立てました。
実測値から1記事あたりの平均サイズが6.2KBであることが分かりました。
- 1,000件 = 約6.2MB
- 14,000件 = 約87MB
- クォータ上限: 50,000件、200MB
14,000件は両方のクォータ範囲内だったため、1バッチで処理することにしました。
投入前検証
バッチ投入前に、クォータ制限に抵触しないことを確認することを推奨します。エラー発覚は実行開始から10分程度を経た後になる場合があり、時間ロスの要因になります。今回は次のような検証スクリプトを用意しました。
検証スクリプト (validate_batch_input.py):
import json
from pathlib import Path
MIN_RECORDS = 1000
MAX_RECORDS = 50000
MAX_FILE_SIZE_MB = 200
def validate_batch_input(file_path):
path = Path(file_path)
# ファイルサイズチェック
size_mb = path.stat().st_size / (1024 * 1024)
print(f"ファイルサイズ: {size_mb:.2f} MB (制限: {MAX_FILE_SIZE_MB} MB)")
assert size_mb <= MAX_FILE_SIZE_MB, "ファイルサイズ超過"
# レコード数チェック
record_count = 0
with open(path, 'r', encoding='utf-8') as f:
for line in f:
record = json.loads(line)
assert 'recordId' in record, "recordId がありません"
assert 'modelInput' in record, "modelInput がありません"
record_count += 1
print(f"レコード数: {record_count:,}件 (最小: {MIN_RECORDS:,}件、最大: {MAX_RECORDS:,}件)")
assert MIN_RECORDS <= record_count <= MAX_RECORDS, "レコード数範囲外"
print(f"✓ 検証完了: 投入可能")
return True
# 実行
validate_batch_input('batch_input.jsonl')
実行結果:
ファイルサイズ: 87.78 MB (制限: 200 MB)
✓ ファイルサイズ OK
レコード数: 14,108件 (最小: 1,000件、最大: 50,000件)
✓ レコード数 OK
✓ 形式チェック OK
✓ 必須フィールド OK
✓ 検証完了: 投入可能
100件テスト実行
本番実行前に、100件でテストを実施し、出力形式やエラーハンドリングを確認しました。
バッチジョブ投入
投入スクリプト (run_batch_job.py):
import boto3
from datetime import datetime
S3_BUCKET = 'my-bedrock-batch-bucket'
# Cross-Region Inference Profile経由のモデルID
# 大量リクエスト時のスロットリング回避のため、特定リージョンに固定せず米国リージョン全体(us.)を使用
MODEL_ID = 'us.anthropic.claude-haiku-4-5-20251001-v1:0'
ROLE_ARN = 'arn:aws:iam::ACCOUNT_ID:role/BedrockBatchInferenceRole'
# S3アップロード
s3 = boto3.client('s3', region_name='us-west-2')
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
s3_key = f"bedrock-batch/input/batch_{timestamp}.jsonl"
s3.upload_file('batch_input.jsonl', S3_BUCKET, s3_key)
input_uri = f"s3://{S3_BUCKET}/{s3_key}"
print(f"✓ S3アップロード完了: {input_uri}")
# バッチジョブ作成
bedrock = boto3.client('bedrock', region_name='us-west-2')
job_name = f"blog-summary-batch-{timestamp.replace('_', '')}"
response = bedrock.create_model_invocation_job(
jobName=job_name,
roleArn=ROLE_ARN,
modelId=MODEL_ID,
inputDataConfig={
's3InputDataConfig': {
's3Uri': input_uri
}
},
outputDataConfig={
's3OutputDataConfig': {
's3Uri': f"s3://{S3_BUCKET}/bedrock-batch/output/"
}
}
)
job_arn = response['jobArn']
job_id = job_arn.split('/')[-1]
print(f"✓ バッチジョブ投入完了")
print(f"ジョブ名: {job_name}")
print(f"ジョブID: {job_id}")
print(f"ARN: {job_arn}")
実行結果:
✓ S3アップロード完了: s3://my-bedrock-batch-bucket/bedrock-batch/input/batch_20260125_083843.jsonl
✓ バッチジョブ投入完了
ジョブ名: blog-summary-batch-20260125083843
ジョブID: 3utuwhgemacy
ARN: arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/3utuwhgemacy
ジョブ監視
# ステータス確認
aws bedrock get-model-invocation-job \
--region us-west-2 \
--job-identifier 3utuwhgemacy
テスト結果
- 処理時間: 約10分
- 成功率: 100%(100件すべて成功)
- 入力トークン: 255,324
- 出力トークン: 60,592
テストは正常に完了し、日本語と英語の要約が正しく生成されていることを確認しました。
注意: 100件のテストでも、最小課金単位の1,000件分の料金が発生しました。先行テストを実施する場合、1,000件単位で実施することをお勧めします。
出力サンプル:
{
"recordId": "wp-post-898154",
"modelOutput": {
"content": [{
"type": "text",
"text": "{\"title_en\":\"Creating Multiple Amazon WorkSpaces for Users in a Single Directory\",\"summary_ja\":\"Simple ADディレクトリを1つ作成し、複数のユーザー名を登録。同じディレクトリ内で異なるWorkSpaces 2つを作成...\",\"summary_en\":\"Create a single Simple AD directory with multiple user accounts. Deploy two WorkSpaces in the same directory...\"}"
}],
"usage": {
"input_tokens": 1281,
"output_tokens": 529
}
}
}
注意: 出力の modelOutput.content[0].text はJSON形式の文字列です。プログラムで利用する際は、この文字列を再度 json.loads() でパースする必要があります。
本番実行(14,108件)
テストが成功したので、残り14,108件を本番実行しました。
データ準備
テスト済み100件を除外し、残り約14,000件のJSONLを作成しました。
バッチ投入
残り約14,000件のJSONLを作成後、テストと同じスクリプトで投入しました。スクリプトは入力ファイル名(batch_input.jsonl)を参照するため、ファイルを差し替えるだけで本番実行できます。
# 残り約14,000件のJSONL作成(テスト済み100件を除外)
python3 create_remaining_batch.py
# 件数確認
wc -l batch_input.jsonl
# 14108 batch_input.jsonl
# 同じスクリプトで投入
python3 run_batch_job.py
実行結果:
✓ S3アップロード完了: s3://my-bedrock-batch-bucket/bedrock-batch/input/batch_remaining_20260125_090512.jsonl
✓ バッチジョブ投入完了
ジョブ名: blog-summary-remaining-20260125090512
ジョブID: siasz3eopo31
ARN: arn:aws:bedrock:us-west-2:123456789012:model-invocation-job/siasz3eopo31
処理結果
- 処理時間: 約17分
- 成功率: 100%(14,108件すべて成功)
- 入力トークン: 28,385,402
- 出力トークン: 8,044,345
manifest.json:
{
"totalRecordCount": 14108,
"processedRecordCount": 14108,
"successRecordCount": 14108,
"errorRecordCount": 0,
"inputTokenCount": 28385402,
"outputTokenCount": 8044345
}
約17分で14,000件以上の処理が完了しました。エラーは0件で、すべての記事の要約生成に成功しました。
注: 出力結果は指定したS3パスの直下ではなく、ジョブID配下に保存されます。manifest.json.out も同じ階層に生成されます。
s3://my-bedrock-batch-bucket/bedrock-batch/output/
├── 3utuwhgemacy/ # ジョブID
│ ├── batch_20260125_083843.jsonl.out
│ └── manifest.json.out
└── siasz3eopo31/ # ジョブID
├── batch_remaining_20260125_090512.jsonl.out
└── manifest.json.out
結果とメリット
コスト比較
| 方式 | 入力コスト | 出力コスト | 合計 | 削減額 |
|---|---|---|---|---|
| オンデマンド | $22.71 | $32.18 | $54.89 | - |
| バッチ | $11.35 | $16.09 | $27.44 | $27.44 (50%) |
トークン単価(Haiku 4.5):
- オンデマンド: 入力$0.80/1M、出力$4.00/1M
- バッチ: 入力$0.40/1M、出力$2.00/1M
約15,000件の処理で、$27.44のコスト削減 を実現できました。
処理時間
バッチ推論:
- 処理時間: 約17分(14,108件)
- スループット: 約830件/分
オンデマンド(実測値ベース):
- スループット: 約11件/分(Haiku 4.5実測値)
- 想定処理時間: 約21時間
今回の場合、バッチ推論によりスループットが約75倍に向上し、約21時間の時間短縮を実現しました。
注: バッチ推論の処理時間は、リソースの可用性や並行ジョブ数により変動します。今回は比較的空いていた可能性があり、17分という高速な処理となりました。
まとめ
Amazon Bedrockのバッチ推論を使って、約15,000件の記事要約を効率的に生成できました。コスト50%削減、処理時間も大幅に短縮できました。
1,000件以上のデータをLLMで一括処理する場合は、Amazon Bedrockのバッチ推論をぜひお試しください。








