論文概要(Abstract)
OPRO(Optimization by PROmpting)は、LLMをオプティマイザとして活用し、自然言語で記述された最適化タスクを解くフレームワークである。各最適化ステップでLLMは過去の解とスコアを含むメタプロンプトから新しい解を生成し、評価して軌跡に追加する。プロンプト最適化への応用で、人手設計プロンプトをGSM8Kで最大+8.4%、Big-Bench Hardタスクで最大+50%上回る性能を達成した。
この記事は Zenn記事: LLMプロンプト管理CI/CD:Langfuse×LaunchDarklyでA/Bテストと安全ロールアウト の深掘りです。
情報源
- arXiv ID: 2309.03409
- URL: https://arxiv.org/abs/2309.03409
- 著者: Chengrun Yang, Xuezhi Wang, Yifeng Lu, Hanxiao Liu, Quoc V. Le, Denny Zhou, Xinyun Chen(Google DeepMind)
- 発表年: 2023
- 分野: cs.LG, cs.AI, cs.CL
背景と動機(Background & Motivation)
プロンプトエンジニアリングは現在、手作業と経験則に頼っている。「Let’s think step by step」のような有名なCoTトリガーでさえ、より良い表現が存在する可能性がある。しかし、自然言語空間での最適化は微分不可能であり、勾配ベースの手法は直接適用できない。
OPROの着眼点は、LLM自身が持つIn-Context Learning能力を最適化に転用することにある。過去の(解, スコア)ペアをコンテキストとして提示すれば、LLMは「何が良い解なのか」のパターンを学習し、より良い解を生成できる。
Zenn記事ではLangfuseによるA/Bテスト結果の記録と分析を紹介しているが、OPROはそのA/Bテストに使うプロンプトバリアントの候補自体を自動生成する技術である。OPROで生成したプロンプト候補をLangfuseのprod-a/prod-bラベルに設定し、本番トラフィックで検証するワークフローが考えられる。
主要な貢献(Key Contributions)
- LLMを汎用オプティマイザとして再定義: 勾配計算なしに自然言語で記述された目的関数を最適化するフレームワークを提案
- 最適化軌跡のIn-Context活用: 過去の(解, スコア)ペアを昇順でメタプロンプトに格納し、LLMが最適化ランドスケープを「理解」する仕組みを実現
- クロスモデル転移の実証: PaLM 2-Lで最適化されたプロンプトがGPT-4上でも有効であることを示した
- 設計知見の体系化: 軌跡の昇順ソートが性能に決定的であることをアブレーションで定量化
技術的詳細(Technical Details)
メタプロンプトの構造
OPROの核心はメタプロンプトの設計にある。3つのパートで構成される:
Part 1: タスク記述 — 最適化対象のタスクを自然言語で説明
Part 2: 最適化軌跡 — 過去の(解, スコア)ペアを昇順(低スコア→高スコア)で提示
Part 3: 生成指示 — LLMに新しい解の生成を指示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Your task is to generate an instruction that achieves
the highest accuracy on the following training examples.
Below are some previous instructions with their scores.
The score indicates how well the instruction performed.
Instruction: "Solve the math problem."
Score: 61
Instruction: "Think step by step."
Score: 63
Instruction: "Let's think step by step."
Score: 65
Instruction: "Let's work through this carefully, step by step."
Score: 68
Generate a new instruction that is different from the above
and achieves a higher score.
Wrap your instruction with <INS> and </INS>.
昇順ソートが性能に決定的な理由: LLMの生成は、コンテキストの末尾に近い例からより強い影響を受ける。最高スコアの解を末尾に配置することで、LLMは改善の方向を理解しやすくなる。
数学的定式化
解空間 $\mathcal{X}$、ブラックボックス目的関数 $f: \mathcal{X} \rightarrow \mathbb{R}$ に対して:
\[x^* = \arg\max_{x \in \mathcal{X}} f(x)\]ステップ $t$ での最適化軌跡 $A^{(t)} = {(x_i, f(x_i))}_{i=1}^{t}$ を用いて:
\[x^{(t+1)} \sim p_\theta(\cdot \mid \text{meta-prompt}(A^{(t)}))\]ここで、
- $x_i$: $i$番目の解(プロンプト文字列)
- $f(x_i)$: 解 $x_i$ のスコア(訓練サブセットでの正解率)
- $p_\theta$: パラメータ $\theta$(固定)を持つLLM
- $\text{meta-prompt}(\cdot)$: 軌跡を自然言語にエンコードする関数
flowchart TD
A[初期化\n簡単なプロンプトで軌跡を初期化] --> B[メタプロンプト構築\nタスク記述 + 最適化軌跡 昇順 + 生成指示]
B --> C[LLMオプティマイザで\nK=8個の候補を並列生成\n温度=1.0]
C --> D[訓練サブセットで\n各候補をスコアリング]
D --> E{スコア改善あり?}
E -->|"改善あり"| F[軌跡に追加\n上位20件をバッファに保持]
E -->|"N回連続で改善なし"| G[収束判定\n処理終了]
F --> H{最大ステップ\nT=200に到達?}
H -->|"未到達"| B
H -->|"到達"| G
G --> I[最高スコアのプロンプトを出力]
アルゴリズム擬似コード
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def opro(task_description: str,
training_examples: list,
llm_optimizer,
llm_scorer,
T: int = 200,
K: int = 8,
buffer_size: int = 20) -> str:
"""OPRO: Optimization by PROmpting
Args:
task_description: タスクの自然言語記述
training_examples: スコアリング用の訓練例
llm_optimizer: 解生成用LLM
llm_scorer: 候補プロンプト評価用LLM
T: 最大最適化ステップ数(論文では200)
K: 1ステップあたりの候補生成数(論文では8)
buffer_size: 軌跡バッファサイズ(論文では20)
Returns:
最高スコアの解(最適化されたプロンプト文字列)
"""
# 初期解で軌跡を初期化
trajectory = initialize_with_simple_prompts()
best_score = float('-inf')
best_solution = None
for t in range(T):
# 昇順ソート(低スコア→高スコア)
trajectory_sorted = sorted(trajectory, key=lambda x: x[1])
# バッファ上位buffer_size件のみ保持
buffer = trajectory_sorted[-buffer_size:]
# メタプロンプト構築
meta_prompt = build_meta_prompt(
task_description=task_description,
trajectory=buffer,
instruction="Generate a new, better solution."
)
# K個の候補解を並列生成(温度T=1.0)
for _ in range(K):
solution = llm_optimizer.generate(
meta_prompt, temperature=1.0
)
solution = extract_between_tags(solution, "<INS>", "</INS>")
# 訓練サブセットでスコアリング
score = evaluate_accuracy(
solution, training_examples, llm_scorer
)
trajectory.append((solution, score))
if score > best_score:
best_score = score
best_solution = solution
# 収束判定
if is_converged(trajectory, window=10):
break
return best_solution
主要な設計選択
複数候補生成(K=8): 各ステップで8個の候補を生成することで、探索の多様性を確保しつつ改善確率を高める。
バッファサイズ(20件): メタプロンプトの長さを管理するため、常に上位20件のみ保持。過去全ての解を含めるとコンテキスト長を超過する。
収束条件: スコア改善がN回連続で閾値未満なら停止。論文では20-30ステップで収束することが多い。
実装のポイント(Implementation)
ハイパーパラメータ
| パラメータ | 値 | 説明 |
|---|---|---|
| K(候補数/ステップ) | 8 | 各ステップで生成する候補解の数 |
| バッファサイズ | 20 | メタプロンプトに含める最大ペア数 |
| T(最大ステップ数) | 200 | プロンプト最適化の最大反復回数 |
| 訓練セットサイズ | ~3.5% of train | GSM8Kで約320例 |
| サンプリング温度 | 1.0 | 多様な候補生成のため高い温度 |
総LLM呼び出し回数: T=200, K=8の場合、1回の最適化で最大1600回のLLM呼び出し。GPT-4で約$50-80のコスト。
Langfuse×OPROの統合パターン
OPROで最適化されたプロンプトをLangfuseのA/Bテスト基盤に流す設計:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
from langfuse import Langfuse
langfuse = Langfuse()
# Step 1: OPROで候補プロンプトを生成
candidate_prompts = opro(
task_description="記事要約タスク",
training_examples=eval_dataset[:320],
llm_optimizer=gpt4o,
llm_scorer=gpt4o,
T=50, # コスト削減のため50ステップ
K=4 # 候補数も削減
)
# Step 2: Top-2をLangfuseのA/Bテストラベルに設定
langfuse.create_prompt(
name="summarizer",
prompt=candidate_prompts[0],
labels=["prod-a"]
)
langfuse.create_prompt(
name="summarizer",
prompt=candidate_prompts[1],
labels=["prod-b"]
)
# Step 3: 本番で500リクエスト/バリアント後に有意差検証
# → LaunchDarklyで勝者プロンプトを100%ロールアウト
実装時の注意点
- 評価データの品質: スコアリング用データの品質がそのまま最適化結果に反映される。ノイズの多いデータでは過適合リスクが高い
- メタプロンプトの言語: 英語メタプロンプトが最も安定。日本語タスクでも、メタプロンプト自体は英語で書くことを推奨
- コスト管理: 本番前のステージング環境で少数ステップ(T=20-30)の実行を推奨
実験結果(Results)
GSM8K(小学校算数)
| 手法 | PaLM 2-L | GPT-3.5 | GPT-4 |
|---|---|---|---|
| “Let’s think step by step” | 71.8% | 78.9% | 92.0% |
| OPRO最良プロンプト | 80.2% | 82.0% | 94.2% |
| 改善幅 | +8.4% | +3.1% | +2.2% |
OPROが発見した代表的なプロンプト:
- “Take a deep breath and work on this problem step-by-step.”
- “Let’s think step by step. Remember to double-check your work.”
Big-Bench Hard(BBH)
| タスク | 人手プロンプト | OPROプロンプト | 改善幅 |
|---|---|---|---|
| Word Sorting | 50% | 75% | +25% |
| Causal Judgment | 54% | 76% | +22% |
| Navigate | 70% | 81% | +11% |
| Object Counting | 72% | 82% | +10% |
| Boolean Expressions | 88% | 95% | +7% |
個別タスクで最大+50%の改善を確認。
アブレーション: 軌跡ソート順の影響
| ソート順 | GSM8K精度 |
|---|---|
| 昇順(低→高) | 80.2% |
| 降順(高→低) | 77.4% |
| ランダム順 | 75.8% |
昇順ソートが2.8ポイント以上の差を生む。LLM生成における位置バイアス(recency bias)を活用した設計。
先行手法との比較
| 手法 | アプローチ | GSM8K (PaLM 2-L) |
|---|---|---|
| Zero-shot “Answer:” | 最小プロンプト | ~60% |
| “Let’s think step by step” | 人手設計CoTトリガー | 71.8% |
| APE(Zhou et al., 2022) | 生成→選択(反復なし) | ~74% |
| APO(Pryzant et al., 2023) | テキスト勾配ベース精練 | ~76% |
| OPRO(本論文) | LLMを最適化器として使用 | 80.2% |
実運用への応用(Practical Applications)
プロンプト改善サイクルの自動化
Zenn記事の3層防御アーキテクチャにOPROを統合する:
- Layer 0(OPRO): プロンプト候補の自動生成
- Layer 1(Promptfoo CI/CD): 候補プロンプトのオフライン評価
- Layer 2(Feature Flag段階ロールアウト): 有望な候補を少数トラフィックで検証
- Layer 3(Langfuse本番監視): 本番メトリクスを次回OPROの訓練データとして還元
クロスモデル転移の活用
OPROで発見されたプロンプトはモデル間で転移する。GPT-4で最適化したプロンプトをClaude 3.5でも使えるため、マルチモデル戦略との相性が良い。LaunchDarklyのAI Configsでモデル×プロンプトの組み合わせを管理できる。
コスト最適化
| 構成 | 呼び出し回数 | 概算コスト |
|---|---|---|
| フル最適化(T=200, K=8) | 1,600回 | $50-80 |
| 軽量最適化(T=50, K=4) | 200回 | $6-10 |
| 週次自動最適化(T=30, K=4) | 120回 | $4-6 |
関連研究(Related Work)
- DSPy (Khattab et al., 2023): パイプライン全体をコンパイルする宣言的フレームワーク。OPROはシングルプロンプトの最適化に特化
- TextGrad (Yuksekgonul et al., 2024): テキスト勾配による自動微分。OPROはより単純な(メタプロンプトのみの)アプローチ
- APE (Zhou et al., 2022): 自動プロンプトエンジニア。OPROは反復的軌跡活用でAPEを上回る
まとめと今後の展望
OPROは「LLM自身がプロンプトを改善する」という直観的かつ強力なアプローチである。メタプロンプトの昇順ソートという単純な設計選択が大きな性能差を生むという知見は、LLMのIn-Context Learning特性の理解に貢献している。
Zenn記事のLangfuse A/Bテスト基盤と組み合わせることで、「プロンプト候補生成(OPRO)→A/Bテスト(Langfuse)→段階ロールアウト(LaunchDarkly)」の自動化パイプラインが実現できる。
参考文献
- arXiv: https://arxiv.org/abs/2309.03409
- Related Zenn article: https://zenn.dev/0h_n0/articles/9fc2f8c4a420e4