【AWS】コラム

Bedrock と Step Functions で作る自動プロンプト改良ループ

作成者: ゼネックコミュニケーション|Sep 4, 2026, 2:48:05 AM

 こんにちは~ 浮田です。  

プロンプトのチューニング、毎回「ちょっと言い回しを変えては試す」の繰り返しで、正直しんどいなと思っていました。

プロンプトは LLM に読ませるための文章なんだから、どう書けば伝わるかは LLM 自身が一番よく知っているのでは? 

つまり自分のことは自分でやれと。

人間は採点と環境づくりだけ引き受けて、改良は本人にやらせる。

この「生成→評価→選抜→反復」のプロンプト自動育成ループを Step Functions で回してみたのが今回の記事です。

 

■  TL;DR

  • LLM 自身にプロンプトを改良させ、正解率で採点し、良いものを残して反復する「プロンプト自動育成」ループを AWS Step Functions で構築した
  • 題材は 書類画像の自動仕分け(請求書/領収書/見積書/納品書/発注書/契約書/その他)。合成書類ジェネレータで正解ラベル付きデータも自動生成
  • 判定モデルを Amazon Nova Lite、改良モデルを Claude Sonnet 4.6 に分離。難化データで 正解率 5割前後 → 80%前後 の成長を確認。さらに同一条件でフル育成を10回試行し、最終スコアが 80.4±1.4% に収束する再現性まで検証した
  • 一番の学びは技術構成そのものより、「モデル選定が全て」「反復改良の価値はピーク更新ではなく初回の外れを救済する保険」「採点系の temperature は 0 に」「出力上限切れで改良案が無言で握りつぶされる罠」 といった運用知見だった

 

   1. プロンプト自動育成とは  

Claude や Nova のような LLM は、プロンプトの書き方ひとつで精度が大きく変わります。良いプロンプトを人力で試行錯誤するのは大変なので、これを自動化したい。

学術的には OPRO / PromptBreeder / DSPy などの自動プロンプト最適化と同じ発想で、流れはこうです。

初期プロンプト → 変種を生成 → 評価データで採点 → 上位を選抜 → 改善案を生成 → …(収束 or 規定回数まで繰り返す)→ 最良プロンプトを保存

この「生成 → 評価 → 選抜 → 反復」のループを Step Functions のステートマシンで回すのが本記事の主題です。

 

■  2. なぜ Step Functions なのか  

このループに必要な要素は、そのまま Step Functions の得意分野でした。

やりたいこと

Step Functions の機能

複数の変種を並列評価

Map ステート(同時実行数も制御可)

目標正解率まで繰り返す

Choice ステート + ループ

LLM 呼び出しの一時失敗に耐える

Retry / Catch(指数バックオフ)

数十分単位の長時間実行

Standard ワークフロー

世代ごとの進捗を可視化

実行グラフ・実行履歴

 

■  3. アーキテクチャ  

Step Functions (Standard / JSONata)

① Init データセット(S3)を読み込み、育成用(dev+fewshot)と検証用(test)に分割

② Generate Claude Sonnet で判定プロンプトを N 個生成(誤分類を弱点フィードバック)

③ Evaluate Map(並列) 各変種で育成セットの画像を分類し、正解率を算出

④ Select 正解率が最良の変種を選抜し DynamoDB に履歴・混同行列を保存

⑤ CheckDone 規定世代 or 目標正解率に到達まで ② へループ

⑥ TestEval 育成済みベストを「未使用の test セット」で採点(過学習チェック)

⑦ Finalize 最良プロンプトと dev/test スコアを S3 / DynamoDB に保存


  • Amazon Bedrock: 判定プロンプト生成(Sonnet)+ 画像分類の実行(Nova/Haiku など、Converse API で差し替え可能)
  • DynamoDB: 世代ごとのプロンプト・正解率・誤分類傾向の履歴
  • S3: 書類画像データセットと最終成果物
  • Lambda (Python) / CDK (TypeScript)

インフラは CDK 1 スタックに集約。

ステートマシン定義(ASL/JSONata)は外部 JSON にし、CDK の definitionSubstitutions で Lambda ARN を注入しています。

 

 4. 題材:書類仕分け × 合成データ    

書類タイプ分類を選んだ理由は、正解ラベルがあり「正解率」で客観採点できるから。

しかも請求書・領収書・見積書は見た目がそっくり(品目表+合計金額)で、判定プロンプトの書き方で精度が変わるので育成のしがいがあるのでは?と考えたからです。

手元に書類がなかったので、Pillow の合成ジェネレータで各クラスの画像と正解ラベルを生成しました。

タイトル文言のゆらぎ(請求書 / ご請求書 / INVOICE)、決め手キーワードのランダム表示、スキャン風劣化(傾き・影・低解像度・ノイズ)を入れ、後半では「難化版」として英語タイトル多用や引っかけ書類も追加しました。

 

  5. 評価:正解率 + dev/test 分割(LLM-as-Judge 不要)  

各変種プロンプトで画像を分類し、出力ラベルが正解と一致するか数えるだけ。

採点にLLMを使わないので採点基準のブレがありません(ただし判定モデル自身の出力ゆらぎは別問題で、後述の temperature: 0 が必要でした)。

さらに dataset.jsonsplit(fewshot/dev/test)で育成用と検証用を分割。育成ループは dev だけを使い、test は最後の 1 回だけ採点します。devScoretestScore の差(overfitGap)で過学習を可視化できます。

 

   6. 実装  

判定モデルは Converse API でモデル非依存に

Bedrock の Converse API は Claude でも Nova でも同じコードで呼べるので、判定モデルを環境変数ひとつで差し替えられます。

採点は決定的に、生成は創造的に

当初 temperature を指定し忘れており、同じプロンプト・同じ画像でも判定結果が毎回揺れていました。採点側は temperature: 0 で決定的にし、多様な変種が欲しい改良側(Sonnet)はデフォルトのまま——という書き分けが重要です。

誤分類を次世代へフィードバック

評価で集めた混同行列(どのクラスをどう間違えたか)を Generate に渡し、「ここの見分けを強化せよ」と改良方向を誘導します。

 ループ状態は JSONata の変数($generation / $history / $best / $testset)で持ち回し、収束判定は Choice ステートで実装しています。  

 

   7. 試行錯誤の記録 

 ここからが実際に動かして分かったことです。

  7-1. Claude Sonnet は優秀すぎて「一発満点」 

 最初、判定も改良も Claude Sonnet 4.6 で回したところ、第1世代でいきなり dev/test 100%

目標スコア到達で即終了しました。技術的には成功ですが、「世代ごとに伸びる」絵が出ない。モデルが強すぎると育成のドラマが生まれないという最初の学びです。

実務だと、すぐに正解を出してくれるのはいいことなんですが、今回は成長が見たかったので、判定役を非力なモデルに下げ、改良役は賢いモデルのままという役割分担にしました。
判定 = Amazon Nova Lite(安い・小型)、改良 = Claude Sonnet 4.6。  

  7-2. ハマったバグ:出力上限切れで改良案が「無言で握りつぶされる」  

Nova Lite に切り替えて回すと、正解率が 49→47→44→51% と横ばいで揺れるだけ。最終プロンプトを見ると、なぜかこの画像が何の書類か答えてください。」という雑な初期プロンプトのままでした。

DynamoDB を調べると、各世代の変種が 1個だけ・全部シード。generate(改良役)が毎回フォールバックに落ちていたのです。

原因は:

Sonnet が 3 つの長大なプロンプトを生成 → max_tokens=3000途中で切れて JSON が壊れる → パース失敗 → variants = [seed] にフォールバック

つまり改良案が一度も採用されず、正解率の揺れは Nova の非決定性だけでした

エラーも出ず静かに握りつぶされていたので発見が遅れました。

対策:

  1. max_tokens を 8000 に拡大(途中切れ防止)
  2. JSON 抽出を堅牢化(```json フェンス除去・型チェック)
  3. フォールバック時に警告ログ(CloudWatch で検知可能に)
  4. 各変種を「簡潔に」指示(小型モデルにも効く+切れにくい)

教訓: LLM 出力のパースは「静かに失敗」しやすい。フォールバックは必ずログを吐かせる。

 7-3. 直したら育った:49% → 70%、プロンプトが「決定木」に進化  

 バグを直すと、ちゃんと育成が機能しました。  

雑なシード「この画像が何の書類か答えてください。」から始まり、Sonnet が自力で 優先順位付きの決定木を編み出しました。

シード → [クラス定義+出力形式を導入] → 65% → [決定木化] → 70%

育った最終プロンプトの一部:

書類画像を7クラスに分類し、ラベル名1語のみ出力せよ。

### 各クラスの識別ロジック(上から順に評価し、最初に一致したクラスで確定)

① 契約書: 「第○条」などの条文番号 AND(「甲」または「乙」)が存在する → 契約書

② 領収書: タイトルに「領収」がある、または「確かに受領」「但し」がある → 領収書

 ※金額が書いてあっても受領証明があれば請求書でなく領収書
...

判定順序が戦略的(紛らわしい契約書・領収書を先に、汎用的な請求書を後に)で、「Xに見えてもYがあればY」という打ち消しルールまで含む。

これは前世代の混同行列をフィードバックした効果がそのまま反映されたものです。

人間のプロンプトエンジニアが書くような内容に、自動で到達しました。

 7-4. どのモデルが一番「育つ」か  

同じシード vs 同じ育成済みプロンプトを各モデルで比較しました。

モデル

素→育成後

コメント

Haiku 4.5

84→100 (+16)

伸びる&満点。伸びしろ少

Sonnet 4.6

88→100 (+12)

元から高く伸びしろ少

Nova Lite

39→54 (+14)

伸びるが頭打ち低い

Nova Pro

25→41 (+16)

伸び幅大だが到達点が低い

Nova 2 Lite

52→54 (+2)

ほぼ伸びない

注意点: このプロンプトは Nova Lite 用に育てたもの。各モデルで個別に育成すれば結果は変わります。また素スコアの低さは「能力」より「出力形式」の影響も大きい(自由回答だとラベル抽出に失敗する)。

 7-5. 頭打ちは突破できるか? → プロンプト系レバーは全滅  

Nova Lite の 70% の壁を、プロンプトの工夫で破れるか試しました。結果はすべて逆効果

試み

結果

情報を盛る(詳細な注意書き追加)

70% → 58% ↓

CoT(考えてからラベル出力)

62.5% → 46.4% ↓

few-shot(正解例画像を同梱)

60.7% → 41.1% ↓

短くしすぎ(キーワード表だけ)

58% → 44% ↓

共通する法則: 小型モデルに「処理する材料」を増やすほど崩れる。

Nova Lite は複数画像や長い推論を捌けず、追加情報がノイズになる。制約してシンプルに使うのが最善で、勝ったのは「キーワード+優先順位+打ち消しルールの簡潔な決定木」でした。

つまり 7-3 で育成が自力で見つけたスタイルが、実は最適だったわけです。

※この表の数値は temperature 未指定(既定値)時代の測定で、±数ポイントのノイズを含みます。ただし全レバーが2桁劣化しており、結論は覆りません。

 7-6. 難化データ × Nova で「大きな成長曲線」— 10回回して分かった本当の価値  

もっと大きな成長を見たくて、データを難化(英語タイトル多用・低解像度・引っかけ書類)し、Nova Lite で 6 世代回しました。

雑なシード(素の正解率は5割前後)から 80% 前後まで育ち、これまでで最大の成長です。

ただ、1回の実行で「成長した」と言い切るのは危険です。

実際、最初の1回は「dev = test = 80.4%(過学習ゼロ)」という出来すぎた結果で、危うくそのまま成果として書くところでした。

そこで判定を temperature=0 で決定化した上で、同一条件でフル育成を10回実行して分布を見ました。

指標

10回試行の結果

第1世代ベスト

平均 76.2%(67.0〜81.2% とばらつく)

最終 devScore

平均 80.4%(全回 77.7〜82.1% に収束、標準偏差 1.4)

testScore

平均 74.7%(62.5〜80.4%)

overfitGap

平均 5.7pt(0.0〜15.2)

ここから見えたことが3つあります。

  1. 第1世代は「ガチャ」。Sonnet が最初に書くプロンプトの出来は 67〜81% と大きくばらつく
  2. 反復はピークを上げないが、外れを救済する。第1世代 67.0% の回は反復が +14.2pt 引き上げて 81.2% に到達(10回中6回で第2世代以降のベスト更新が発生)。結果、どのスタートでも最終スコアは 80% 前後の狭い帯に収束した。**育成ループの実証された価値は「最高値の更新」ではなく「初回の外れを救済し、着地点を保証する保険」**です
  3. 82% の壁は10回とも破れず。頭打ちがノイズではなく Nova Lite の能力上限であることが、分布として確定した

なお「過学習ゼロ」の回は10回中1回だけで、gap は平均 5.7pt、最悪 15.2pt。dev がほぼ同じでも test は大きく揺れるため、「dev で選んだベストが汎化でも最良」とは限らない

というのが次の改善余地です。

ちなみに判定役を Haiku 4.5 に替えて同じ難化データで5回試行すると、5回すべて第1世代で dev/test 100%(目標到達で早期終了)。強いモデルには育成の出番すらないので、成長曲線を見せたいなら「ちょうど良い能力のモデル × 難しめのタスク」が要る、という結論です。

 

 8. 得られた知見    

  1. モデル選定が全て。強すぎるモデルは育成の余地がなく(即満点)、弱すぎると天井が低い。「育成が効く」=伸びしろと指示追従の両立するモデルを選ぶ話に帰着する。
  2. 小型モデルは情報を盛るほど劣化する。CoT も few-shot も詳細化も逆効果。簡潔な決定木が精度高かった。
  3. LLM 出力のパースは静かに失敗する。出力上限切れ・JSON 崩れでフォールバックが握りつぶす。必ずログを吐かせ、max_tokens に余裕を。
  4. dev/test 分割で過学習を可視化。育成データだけ高くて test が低ければ過学習。10回試行の gap は平均 5.7pt(0〜15.2)で、dev が同点でも汎化には差が出る。
  5. 採点系の temperature は 0 に。評価スコアが揺れる正体は判定モデルの temperature 未指定だった。生成側(変種づくり)は多様性のため高いままで良いが、採点側は決定的にする。この1行で「複数回測って平均」が丸ごと不要になった
  6. 累積ベスト保持には「更新マージン」を。ノイズや僅差の入れ替わりで良いプロンプトが捨てられないよう、過去ベストを明確に(+2pt超)上回ったときだけ更新する。当たりを引いた世代が、後続の凡庸な世代に上書きされなくなる
  7. 反復改良は「保険」として効く。第1世代の出来は 67〜81% とばらつくが、6世代のループがどのスタートも 80% 前後に収束させる。ピーク更新より分散の圧縮が実証された価値(10回試行)

 

 9. ハマりどころ  

  • スロットリング: 判定モデルの同時実行上限に注意。MaxConcurrency と適応リトライ(Config(retries={"mode":"adaptive"}))で吸収
  • BucketDeployment の OOM: 画像 168 枚(PNG 59MB)で、S3 アップロード用 Lambda が既定 128MB でメモリ不足に。JPEG 化(→9.7MB)+ memoryLimit 拡張で解決。デプロイが 50 分ハングして焦った、、。 OOM は「エラーメッセージが出る」のではなく「沈黙やハングとして現れる」ってあるあるなんですね。知らなかったです。
  • 推論プロファイル ID: 東京リージョンは jp. / apac. プレフィックスが必要(apac.amazon.nova-lite-v1:0 など)。aws bedrock list-inference-profiles で確認
  • JSONata の表記: ASL の "QueryLanguage""JSONata"(大文字 JSONATA はデプロイ時エラー)

 

 10. おわりに  

「生成 → 評価 → 選抜 → 反復」というプロンプト最適化のループは、Step Functions の Map / Choice / Retry / 変数にきれいに対応しました。ワークフロー基盤としての相性は非常に良いです。

ただ、実際に回して面白かったのは技術構成そのものより、「どのモデルをどう使うか」で結果が激変するという運用のリアルでした。

プロンプト自動化は「賢いプロンプトを書く技術」であると同時に、「最適化が効くモデル・タスク・評価設計を選ぶ技術」でもあるというのが、一連の試行錯誤から得た実感です。

「自分のことは自分でやれ」とプロンプト改良を LLM に丸投げした結果、たしかに改良はやってくれました。ただし、誰に(モデル選定)・何を(タスク)・どう採点するか(評価設計)を決めるのは、最後まで人間の仕でした。