Amazon Bedrockのバッチ推論で14,000件の記事要約を効率化してみた

Amazon Bedrockのバッチ推論で14,000件の記事要約を効率化してみた

技術ブログ記事から14,000件の要約テキストを生成。Amazon Bedrockのバッチ推論導入で、コストを50%削減し、処理効率は75倍に向上しました。
2026.01.25

約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のバッチ推論をぜひお試しください。

参考リンク

この記事をシェアする

FacebookHatena blogX

関連記事