Bedrock Fine-tuning本番6ヶ月で直面した、データ準備・コスト・精度の現実的な課題

BedrockのFine-tuning機能を実際に本番運用して6ヶ月。ドキュメントに書いてないデータセット準備の落とし穴、想定外のコスト、推論精度の失敗を赤裸々に共有します。

Bedrock Fine-tuningに手を出した時点で気づくべきだった

うちのチームがBedrock Flows本番運用を始めて3ヶ月経った時点で、プロダクト側から「Claude の精度が足りない。カスタムモデル作ってくれ」という依頼が来た。そこから地獄が始まった。

Bedrock Fine-tuningが2025年後半に正式化されて、2026年の今はかなり安定してるんだけど、実際に本番で6ヶ月運用してみると、ドキュメントには書いてない落とし穴だらけなんですよね。データセット準備の手間、思ったより高いコスト、推論時の想定外の遅延…ぜんぶ経験した。

実際にやってみた失敗と工夫を、ありのままに書く。

データセット準備が本当の地獄だった話

まず最初の落とし穴は「準備」。ドキュメントだと「高品質な例100個あれば十分」と書いてあるけど、うちの場合は全然足りなかった。

最初、営業チーム向けの提案生成モデルを作ろうとして、既存のCRM履歴から適当に100件抽出してFine-tuningした。結果は最悪。推論時に「前のモデルより悪くなった」という状況に陥ったんだ。

後で気づいたのは、単なるサンプル数の問題じゃなくて、データの多様性と質が完全に欠落してたということ。営業がCRMに入力したテキストって、実は品質がバラバラ。機械的な記入漏れもあるし、営業個人のくせも強い。そこを無視してFine-tuningすると、モデルが「営業のクセを学習」してしまう。結果は推察の通り、最悪な提案ばかり生成される羽目になった。

結果的に、うちが準備に使った労力は以下の通り。正直、見積もりの3倍は覚悟した方がいい:

  1. データクリーニング(3週間) - 既存データから使えそうなものをフィルタリング。機械的な削除ルールとスポット確認を組み合わせた
  2. 手作業での生成例作成(2週間) - 営業マネージャー3人が理想的なプロンプト・レスポンスのペアを500個作成。一番つらい工程
  3. 品質ラベリング(1週間) - 外部QA業者で品質チェック。これがないと「品質」の定義が主観になる
  4. バージョン管理(進行中) - データセットの変更履歴を追跡。あとで「あの例、なんで入れたんだ?」って問題が出る

ドキュメントでは触れられていないけど、Fine-tuning用のデータセット準備ツール群が2026年でもまだ成熟していない。BigQueryやSageMaker Data Wranglerを組み合わせるしかない状況だ。正直、ここが一番の改善ポイントだと思う。

実装的には、JSONLフォーマットで以下の構成に統一した:

{"prompt": "顧客プロフィール:SaaS企業、年商5億円、現在ERP導入検討中", "completion": " 提案のポイント:\n1. 既存システムとの統合コスト削減\n2. 導入期間3ヶ月を保証\n3. 初期6ヶ月の無料サポート"}
{"prompt": "顧客プロフィール:製造業、工場3拠点、IoT化を進めたい", "completion": " 提案のポイント:\n1. リアルタイム監視ダッシュボード\n2. 既存PLC との互換性確保\n3. オンサイトサポート体制"}

Bedrock側の仕様として、プロンプトと完成例の長さのバランスが重要。うちが最初失敗したのは、promptが短すぎて、「完成例」のバリエーションが多すぎたこと。同じプロンプトに対して異なる回答を学習させるのは避けるべき。1プロンプト = 1ベストな完成例 の原則を守らないと、モデルが混乱して、むしろ性能が落ちる。地味だけど、ここが本当に重要。

コストが想定の3倍になった罠

次の驚きはコスト。公式ドキュメントで「Fine-tuning用の計算コストは通常推論の3倍」と書いてあるけど、実際にはそれより複雑だ。

Bedrock Fine-tuningの課金体系(2026年時点)を整理するとこんな感じ:

項目単価説明
Fine-tuning(1トークン)$0.000075学習データセット全体の処理
推論(1出力トークン)$0.0006カスタムモデルの推論
ストレージ(モデル1個/月)$5.00モデルアーティファクト保管

初回Fine-tuningの試算だと、500個の例×平均800トークンで約40万トークン、単価$0.000075なので $3/回。月に5回新しいバージョンをテストするなら月$15。そして本番推論で月300万トークン出力するなら月$1,800。「あれ、合計で月$1,820?」って計算になる。

実は違うんだ。計算が複雑なのは、データセット内の重複排除ロジックがBlack Boxだから。うちの場合、営業データから生成した例には似たパターンが多かった。Bedrockはこれを検出して「有効トークン数」を圧縮する(ドキュメントより推測)。結果的に実績は月$800程度。まあ、許容範囲か。

でも落とし穴がもう1つある。Fine-tuning後のモデルは、公開モデル(Claude 3.5 Sonnet)より推論が遅い。平均レイテンシが2倍になるケースもあるんですよね。そこからコスト圧力が生まれる:

  • 遅いなら並列度を上げたい→DynamoDBのプロビジョニング容量アップ→コスト増
  • または、キャッシュ層(Redis)を入れる→インフラコスト増

うちはこっちを選んだ。ElastiCacheにRedisを追加して、推論結果の30分キャッシュを実装した。これで実効的には月$2,500程度のコスト増で抑えられたけど、当初見積もりからは+$1,700。予算ヒアリングの段階で「想定と違う」と言われた。

実装アーキテクチャ図

graph TB
    subgraph Client["クライアント層"]
        API["REST API<br/>FastAPI"]
    end
    
    subgraph Inference["推論層"]
        Cache["ElastiCache Redis<br/>30分キャッシュ"]
        Gateway["API Gateway"]
        Lambda["Lambda<br/>推論呼び出し"]
    end
    
    subgraph Bedrock["AWS Bedrock"]
        CustomModel["Fine-tuned Claude<br/>v1.2"]
        PublicModel["Claude 3.5 Sonnet"]
    end
    
    subgraph DataPipeline["データパイプライン"]
        S3Raw["S3 Raw Data<br/>営業CRM抽出"]
        Glue["AWS Glue<br/>クリーニング"]
        S3Train["S3 Training Data<br/>JSONLフォーマット"]
        Bedrock2["Bedrock Fine-tuning<br/>月2回実行"]
    end
    
    subgraph Monitoring["監視・評価"]
        CloudWatch["CloudWatch<br/>レイテンシ監視"]
        Lambda2["Lambda<br/>品質評価"]
        SQS["SQS<br/>評価キュー"]
    end
    
    API -->|キャッシュ確認| Cache
    Cache -->|MISS| Lambda
    Lambda -->|推論リクエスト| Gateway
    Gateway -->|カスタムモデル優先| CustomModel
    Gateway -->|フォールバック| PublicModel
    CustomModel -->|結果キャッシュ| Cache
    PublicModel -->|結果キャッシュ| Cache
    
    S3Raw --> Glue
    Glue --> S3Train
    S3Train --> Bedrock2
    Bedrock2 --> CustomModel
    
    Lambda2 -->|監視対象| CloudWatch
    Lambda -->|品質スコア計算| SQS
    SQS -->|評価実行| Lambda2

推論精度とフォールバックの設計が実は重要

Fine-tuningしたモデルが「必ず元モデルより良い」とは限らない。これが本番運用で一番つらいポイントなんですよね。

うちの営業提案生成モデルの場合:

  • Claude 3.5 Sonnet(オリジナル) - 提案内容は汎用的で安全。だが顧客特性への適応が薄い
  • Fine-tuned Claude - 営業スタイルに合わせた提案が出るけど、たまに営業クセを強く反映して「これはちょっと…」という案が出る

評価指標としては、営業マネージャーが手動で100件抽出して「どっちの提案の方が実際に契約まで行きやすいか」を採点した結果は以下:

  • Fine-tuned:73点
  • オリジナル:68点

「5点差」。統計的に有意か微妙なライン。ただし営業チームの主観としては「Fine-tunedの方が使える」らしい。データに基づく評価より、営業の実感が優先されるのが現実だ。

この曖昧さを処理するために、うちが実装したのはスコアベースのルーティング。正直、この設計があるとないで、本番運用の心の安定度が全然違う:

from enum import Enum
from typing import Literal
import json
import hashlib

class ModelSelection(Enum):
    FINE_TUNED = "fine-tuned-claude-v1.2"
    PUBLIC = "claude-3-5-sonnet"

async def invoke_with_routing(
    prompt: str,
    customer_profile: dict,
    use_fine_tuned: bool = True
) -> dict:
    """
    Fine-tuned と Public を動的にルーティング
    """
    # 1. キャッシュ確認
    cache_key = hashlib.md5(
        f"{prompt}{json.dumps(customer_profile)}".encode()
    ).hexdigest()
    cached = await redis_client.get(f"bedrock:{cache_key}")
    if cached:
        return json.loads(cached)
    
    # 2. モデル選択
    model_id = (
        ModelSelection.FINE_TUNED.value 
        if use_fine_tuned 
        else ModelSelection.PUBLIC.value
    )
    
    # 3. 推論実行
    response = await bedrock_runtime.invoke_model(
        modelId=model_id,
        body=json.dumps({
            "prompt": prompt,
            "max_tokens": 500,
            "temperature": 0.7
        })
    )
    
    result = json.loads(response["body"].read())
    
    # 4. 品質スコア計算(簡易版)
    quality_score = await calculate_quality_score(
        result["completion"],
        customer_profile
    )
    
    # Fine-tunedで品質スコアが低い場合、Public モデルで再実行
    if use_fine_tuned and quality_score < 0.6:
        logger.warning(
            f"Fine-tuned score low ({quality_score}), fallback to public"
        )
        result = await invoke_with_routing(
            prompt,
            customer_profile,
            use_fine_tuned=False
        )
        result["fallback_used"] = True
    
    # 5. キャッシュ保存
    await redis_client.setex(
        f"bedrock:{cache_key}",
        1800,  # 30分
        json.dumps(result)
    )
    
    return result

async def calculate_quality_score(completion: str, context: dict) -> float:
    """
    簡易的な品質スコア計算
    - 顧客セグメント関連キーワードの有無
    - 提案の長さ(短すぎたり長すぎたりしないか)
    - 営業トーンの検出
    """
    score = 0.5  # ベーススコア
    
    # 顧客タイプ別キーワード
    keywords = {
        "saas": ["統合", "導入期間", "サポート"],
        "manufacturing": ["リアルタイム", "監視", "互換性"],
        "retail": ["POS連携", "在庫", "顧客体験"]
    }
    
    if context.get("industry") in keywords:
        for keyword in keywords[context["industry"]]:
            if keyword in completion:
                score += 0.1
    
    # 長さチェック(200-500文字が理想)
    if 200 <= len(completion) <= 500:
        score += 0.2
    
    return min(score, 1.0)

実運用で気づいたのは、フォールバック設計がないと「Fine-tuningしたのに古いモデルを使わせてくれない」という営業からの要望が来ることだ。人間は新しいツールへの期待値が高い。でも現実は「新しい = 必ず良い」ではない。なぜなら、Fine-tuningは営業データの弱点まで学習してしまうから。

本番運用で見えた3つの地味だけど重い課題

1. モデルのドリフト(時間とともに性能が落ちる)

3ヶ月ごとに営業データが更新されるんだけど、古いFine-tuningモデルを使い続けると徐々に性能が落ちる。顧客セグメントが変わったり、提案パターンが進化するからなんですよね。

うちは月1回の定期更新と、四半期ごとの完全再学習をやってる。費用は上がるけど、精度を維持するにはこれが必須。最初のドキュメントには書いてないけど、実運用では「継続的な再学習」が前提になる。

2. 推論レイテンシの予測不可能性

Fine-tuned モデルの推論時間にはバラつきがある。平均500msだけど、たまに2秒かかることもある。公開モデルより安定性が低い。原因は不明。Bedrockのドキュメントにもこれ以上の説明がない。

対策として、タイムアウト設定を短めに(1秒)にして、それを超えたら自動的に公開モデルにフォールバックさせてる。正直、この対策がなかったら本番でオーバーロード状態になってた。

3. A/Bテストがしたくても難しい

Fine-tuningして本番デプロイすると、本当にそれが良かったのか評価するのが困難。理由は「営業の使い方が変わっちゃう」こと。新しいモデルだから営業も工夫して使うようになる。その結果、成約率が上がったのは「モデルのおかげ」なのか「営業の使い方のおかげ」なのか不明確になる。統計的に厳密なA/Bテストを設計するなら、ランダム化が必須だけど、営業から「いや、新しいモデル使いたいんですけど…」という反発が来る。

2026年時点で「やっておけばよかった」こと

実装系だけじゃなく、組織的な準備も大事だった。改めて思う4つのポイント:

  1. データセット管理の自動化 - JSONLファイルを手動で管理してると、バージョン管理が地獄。S3+Glueで自動パイプライン化しておくべきだった。あとから「この例、いつ誰が入れたの?」って問い合わせが来るから

  2. 品質評価メトリクスの事前定義 - Fine-tuning前に「成功の定義」を数値化しておくべき。営業の主観だけに頼ると後から「実は微妙…」ってなる。うちは定義が甘かったから、本番後に「もっと厳密に測定しろ」と言われた

  3. フォールバック設計 - 最初からFine-tuning一本に絞らず、2モデルの共存を前提に設計。予期しない品質低下時の逃げ道が重要。これがないと本番障害時に手の打ちようがない

  4. 推論キャッシュ層 - 後付けするより、最初から入れておいた方が楽。営業提案は同じパターンの繰り返しが多いから、キャッシュ効率が高い。うちは後付けしたから設計をやり直す羽目になった

まとめ

Bedrock Fine-tuningは確かに強力だけど、「単にデータセット用意して学習させればOK」みたいなシンプルなものではない。本番運用6ヶ月で学んだことを整理すると:

  1. データセット準備が7割 - 手作業が半端ない。品質ラベリングなし無理。見積もりの3倍は覚悟した方がいい

  2. コストは想定より複雑 - 学習コスト、推論コスト、推論遅延による間接コスト(キャッシュ層、インフラ増強)を総合で考える必要がある

  3. フォールバック設計は必須 - Fine-tuningモデルが常に最適とは限らない。2モデル共存の仕組みを作っておく

  4. 継続的な再学習が現実的 - 一度やったら終わりではなく、月1回程度の更新が必要。ドキュメントに「セットアンドフォーゲット」みたいな記述があったら信じるな

  5. 評価指標を事前に定義 - 営業の主観に頼ると後から「実は微妙」ってなる。定量的な成功基準がない状態で本番化するな

ぶっちゃけ、いきなり本番にぶち込まずに、まずは内部でのテスト期間(3ヶ月)を作るべき。データ準備だけで2-3ヶ月は見積もっておくのが現実的だ。

IAOSP Top 10の脆弱性対策と同じで、AI/ML基盤でもセキュリティとデータ品質は後付けできない。最初から設計に組み込んでおくことが、後々の心の安定につながる。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事