Prompt Optimizing with GEPA and Databricks for 90x cheaper inference | by AI on Databricks | Mediumの翻訳です。
本書は著者が手動で翻訳したものであり内容の正確性を保証するものではありません。正確な内容に関しては原文を参照ください。
著者: Austin Choi, Delivery Solutions Architect
Databricks Mosaic AI研究チームが素晴らしい記事を公開しました: 自動化プロンプト最適化による90倍安価な最先端企業エージェントの構築
彼らは、こちらのGEPAの論文をベースとして、コストパフォーマンスの価値を飛躍的に向上させる膨大なパフォーマンス向上を得ることができるようにプロンプトを最適化できると主張しています。そして、実行は簡単です!
どんどん生成AIアプリケーションが本格運用に投入されるに従い、我々はお客様のエージェントの本番運用投入後に何をすべきかと言う質問をかず多くいただくようになっています。こちらは、いくつか共通するトレンドです:
- 大規模かつ最先端のモデルを使用するコストは高すぎて、いかなるROIもほとんど不可能になっている
- スピードが最も重要な場合に、大規模なモデルの推論時間が長くなってしまう
- お客様はモデルをホスティングすることを好むが、大規模モデルのホスティングのコストは許容できない
- 大規模モデルにおける繰り返し開発は高価である
- 自身のモデルに対してコントロールを維持したい
Databricksにおいて、我々のMosaic AI研究チームもまた、小規模な言語モデルを活用することの価値を認識しています。このブログ記事では、論文の知見を検証するために厳密なベンチマークを行いました。彼らは、最適化された小規模な言語モデルを用いて、最先端モデルに匹敵しながらも90倍安価な推論を達成できる可能性があることを発見しました。
実現は不可能に聞こえるかもしれませんが、(小規模なものでも)すべてのLLMのパラメーター空間は膨大であり、かつて我々が取り扱ったことのないものであることを思い出してください。現在においても、我々はこれらのブラックボックスのニューラルネットワークの意思決定プロセスを理解しようとし続けています。そのため、最も最適なプロンプトに到達するまでに、我々がトライできる信じられないほどのプロンプトの組み合わせが存在しています。現在のプロンプトエンジニアリングは、極端に手作業で脆いものです。さらに、これらのプロンプトは、特定のモデルを念頭に置いて大規模に開発され、モデルの切り替えや移行を困難なものにしています。あなたのエージェントや生成AIアプリケーションのパフォーマンスが劣化した場合、このプロセスを再度繰り返さなくてはなりません。このようなことから、我々はクイックにテストを行い、最適なプロンプトを発見する自動化され、客観的な手段を必要としています。
GEPAのようなプロンプトオプティマイザをいつ使うべきか?
以下のようなシナリオのすべてあるいは一部において、プロンプトオプティマイザを使用することを考えてみましょう。
小規模な言語モデル
GEPAはパフォーマンス改善において二桁パーセントの向上を示しました。これは、多くの場合において、最先端のモデルや自身より大規模なモデルに匹敵あるいは打ち負かすのに十分です。後半のデモでは、GPT-OSS 20BがどのようにしてパフォーマンスでGPT-OSS 120Bを打ちまかし、Claude 4 Sonnetに並ぶのかをデモンストレーションします。
GEPAによるパフォーマンスゲインによって、小規模なモデルを活用できるようになります。これによって、
- コストの改善: 比較するモデルに応じて、同じパフォーマンスレベルで最大90倍安価な水路コストを達成できます。
- レイテンシーの改善: 小規模なモデルは本質的にタスクをより迅速に完了します。
- ホスティングの余裕: 膨大なトラフィックに対応する際、小規模なモデルはパフォーマンスを犠牲にすることなしに、トラフィックの増加に対してスケールする余地を提供します。また、いかなるレート制限の問題を回避できます。
プロプライエタリ言語モデルのチューニング
よりコスト効率の高いチューニング加えて、プロプライエタリモデルがすでに有している高いパフォーマンスを改善するために、プロプライエタリモデルを活用、最適化することができます。これらの最適化されたプロンプトは、他の生成AIアプリケーションやモデルにロードできるように、あなたが所有することになります。他のモデルに対してこのプロセスを繰り返して、ご自身の資産として管理できます!
自動化されたプロンプトエンジニアリング
プロンプトを手動で維持するのは現実的ではありません。多くの経験あるエンジニアが知っているように、テキストの文字列の維持は脆弱なものです。few-shotプロンプトやステップの追加を通じたプロンプトへのテキストの追加は、この問題を悪化させるだけです。
プロンプトオプティマイザによって、あなたのデータを用いてあなた特有のユースケースにプロンプトをチューニングできる自動化プロセスを実行することができます。GEPAの利点の一つは最適化のスピードです。軽量なワークロードは約15分であり、異なる複数のパラメーターをテストし、ユースケースに最適なプロンプトを特定するために、複数の実験ランを実行することができます。
時間と共に、入力データ自身のようにいくつかの要素が変化したことで、あなたのエージェントやアプリケーションがかつてのようなパフォーマンスを発揮しなくなる「ドリフト」のようなものに遭遇するかもしれません。アプリケションが本番運用レベルに復帰するように、ドリフトが起きたら常にプロンプトオプティマイザの実行をトリガーすることができます。
あなたのデータとフィードバックをベースにする
我々のデータ、文言、フィードバックにより立脚するように、我々はLLMのファインチューニングにトライしました。これは、高コストで、時間を浪費し、実装が複雑であることがわかりました。また、大規模モデルをホスティングする必要が出てくるので、大規模モデルを使用する際の同じ問題に苦しむことになります。このため、あなたがあなたのデータとうまく動作するモデルを手にしていたとしても、依然としてコストを看過することはできません。
別の問題は、ファインチューニングを適切に実行するには高品質なデータが必要になるということです。高品質データ、高品質データを準備するリソースの欠如によって、多くのトレーニングランは失敗します。しかし、ファインチューニングはあなたのモデルのパフォーマンス改善を行うための一貫性のある方法であることは明らかです。
GEPAはこのすべてを美しく対応します。プロンプトを更新するだけであり、モデル自身の重みを変更するよりも負荷が低いものとなります。そして、GEPAは大規模なデータを必要とせず、自身を改善するために自然言語を使用するという事実のおかげで、大きな改善を見るには約15の例で十分です。
しかし、ファインチューニングとGEPAの改善を組み合わせたらどうなるのでしょうか?両方の世界のベストを手に入れられるのでしょうか?
ファインチューニングとプロンプト最適化を組み合わせることで、最大のパフォーマンス改善を得ることができるということを、研究ブログとGEPAの研究論文で気づくことでしょう。小規模モデルでこのアプローチを撮ることで、大きな改善を得ることができます。
安価で簡単な繰り返しの開発
前のセクションで述べたように、GEPAの主なゴールは単にプロンプトを更新することなので、あなたのリソースを要求しません。モデルを用いて、プロンプトの教育、テストを行うためにいくつかのLLM呼び出しを行いますが、テストの大部分は最適化される小規模な言語モデルを用いて行われるのでこれですべてです。平均では、一つの軽量なGEPAの実行は15分から20分であり、実行コストは$10程度です。
GEPAとは?
ここで、GEPAのようなプロンプトオプティマイザの出番です。GEPAはトライアンドエラーからハイレベルのルールを学習するために自然言語の内省を使用するオプティマイザなので、Genetic Paretoの略となっています。研究論文では:
いかなるAIシステムには1つ以上のLLMプロンプトが含まれているので、GEPAは問題を診断し、プロンプトの更新を提案、テストし、自身の試行におけるパレートフロンティア(複数の目標間でトレードオフが存在する場合に、これ以上どの目標も他の目標を犠牲にせずには改善できない最適な解の集合)から補完的な教訓を組み合わせるために、システムレベルでの軌跡をサンプリングし、自然言語でそれらを内省します。GEPAの設計の結果として、多くの場合いくつかのロールアウトであっても、大きな品質のゲインを得ることができます。4つのタスクにおいて、GEPAはGPROを平均10%、最大で20%上回るパフォーマンスを示しています。また、GEPAは2つのLLMにおいて、先進的なプロンプトオプティマイザMIPROv2を10%以上上回るパフォーマンスを示しており、コード最適化における推論時検索戦略として有望な結果を示しています。
簡単に言えば、GEPAはあなたのプロンプトを改善するために、あなたの評価結果と自然言語の両方を使うことができるのです!GEPAは理想的なプロンプトを特定するために自然言語のフィードバックと結果を組み合わせるので、GEPAが使用するリファレンスとしていくつかのデータポイントだけが必要となります。GEPAは最終的にユースケースに最適なプロンプトを特定するために、これらのプロンプトのリストを維持するためにパレートフロンティアを使用します。
また、この論文の結果は上述したMosaic AIリサーチチームのブログ記事でも検証されており、パフォーマンスの改善においてGEPAがベストのパフォーマンスを示したと結論づけています。
なぜDatabricks上で?
GEPAを使用する際の主要コンポーネントの一つが、あなたの例示データと自然言語のフィードバックの両方を連携させる能力です。しかし、あなたのGEPAエクスペリメントが間違いなく行う数百のテストに対して直接フィードバックを書くことは現実出来ではないことを我々全員が知っています。
あなたの生成AIアプリケーションを評価し、フィードバックを提供するように特別にチューニングされた機能であるスコアラーとジャッジにようこそ。もっとも重要なことは、ユースケースに応じてスコアラーとジャッジを調整できるということです。Databricksは、お客様が自身のエージェントを評価する際に使用できる評価機能の提供に多大なリソースを投資しています。詳細はこちらのブログをご覧ください。
Databricksでは以下を活用することができます:
定義済みジャッジ
これらのジャッジは、評価のために設計された特定のメトリックを使用できるようにDatabricksですぐに利用できます。ジャッジのいくつかは:
- 正しさ
- クエリーに対する適切性
- 収集の適切性
こちらで、定義済みのジャッジの完全なリストを確認できます。
カスタムジャッジ
あなたが探しているものと正確にアラインするように、ご自身のジャッジを定義できます。これらは、プロンプトやエクスペクテーションを通じて調整できます。詳細はこちらをご覧ください。
カスタムコードベースのジャッジ
あなたの評価においてより多くの柔軟性や一貫性を必要とする場合、いくつかのコードに基づくスコアラーをいつでも定義することができます。これによって、求めている特定のメトリクスを計算したり、評価に使用するであろう他のツールを活用することができます。
詳細はこちらをご覧ください。
参照し、GEPAに対して自然言語のフィードバックを提供するために、これらのジャッジやスコアラーを活用する必要があります。ジャッジやスコアラーは、GEPAのオプティマイザがあなたのユースケースに近づくようにガイドする、よりターゲットされたフィードバックを提供できるので、LLMによって生成されたノイズの削減において多大なる改善を示します。
DatabricksとMLflowの密連携
我々はGEPAとその他のプロンプトオプティマイザをDatabricksに組み込みました。DSPyを通じて、あるいは、MLflowのプロンプト最適化機能(GEPAはMLflow 3.5.0に含まれています)を用いることで、簡単にMLflowで最適化ランを追跡することができます。
DSPyはMLflowとDatabricksで完全にサポートされています。エクスペリメント、プロンプトレジストリなどのような、プロダクションレディにするためのMLflowのすべてのツールを活用することができます!
DSPy: GEPAを使う最も簡単な方法
GEPAを使い始める最も簡単な方法はDSPyを使うことです!DSPyは、その構造化された入出力のシグネチャフォーマットによって、GEPAはネイティブにこのフレームワークに組み込むことができるので、生成AIの開発を構造化、モジュール化するように設計された軽量かつ宣言型のフレームワークです。また、エージェント開発の維持、理解を非常に簡単なものにします。
GEPAのデモ
このデモでは、分類エージェントを作成するために、DSPyとDatabricksのジャッジを使用します。様々なLLMがデータをどれだけ適切に分類しているのかをテストするために、HuggingFace PubMed Classificationデータセットを使用します。そして、小規模言語モデルの一つを最適化し、最先端モデルと比較します。
完全なコードはこちらです: https://github.com/hmoazam/databricks-dspy-cookbook/blob/main/dspy_hackathon/03_Advanced_DSPy_Optimizer_GEPA.py
訳者註
こちらでコードを実際に動かしています。
ステップ 1: データのセットアップ
はじめに、使用するデータをダウンロード、セットアップします:
import numpy as np
import pandas as pd
from dspy.datasets.dataset import Dataset
from pandas import StringDtype
def read_data_and_subset_to_categories() -> tuple[pd.DataFrame]:
"""
Read the pubmed-text-classification-cased dataset. Docs can be found in the url below:
https://huggingface.co/datasets/ml4pubmed/pubmed-text-classification-cased/resolve/main/{}.csv
"""
# Read train/test split
file_path = "https://huggingface.co/datasets/ml4pubmed/pubmed-text-classification-cased/resolve/main/{}.csv"
train = pd.read_csv(file_path.format("train"))
test = pd.read_csv(file_path.format("test"))
train.drop('description_cln', axis=1, inplace=True)
test.drop('description_cln', axis=1, inplace=True)
return train, test
class CSVDataset(Dataset):
def __init__(
self, n_train_per_label: int = 40, n_test_per_label: int = 20, *args, **kwargs
) -> None:
super().__init__(*args, **kwargs)
self.n_train_per_label = n_train_per_label
self.n_test_per_label = n_test_per_label
self._create_train_test_split_and_ensure_labels()
def _create_train_test_split_and_ensure_labels(self) -> None:
"""Perform a train/test split that ensure labels in `test` are also in `train`."""
# Read the data
train_df, test_df = read_data_and_subset_to_categories()
train_df = train_df.astype(StringDtype())
test_df = test_df.astype(StringDtype())
# Sample for each label
train_samples_df = pd.concat([
group.sample(n=self.n_train_per_label, random_state=1)
for _, group in train_df.groupby('target')
])
test_samples_df = pd.concat([
group.sample(n=self.n_test_per_label, random_state=1)
for _, group in test_df.groupby('target')
])
# Set DSPy class variables
self._train = train_samples_df.to_dict(orient="records")
self._test = test_samples_df.to_dict(orient="records")
# Sample a train/test split from the pubmed-text-classification-cased dataset
dataset = CSVDataset(n_train_per_label=3, n_test_per_label=10)
# Create train and test sets containing DSPy examples
train_dataset = [example.with_inputs("description") for example in dataset.train]
test_dataset = [example.with_inputs("description") for example in dataset.test]
print(f"train dataset size: \n {len(train_dataset)}")
print(f"test dataset size: \n {len(test_dataset)}")
print(f"Train labels: \n {set([example.target for example in dataset.train])}")
print(f"Sample entry: \n {train_dataset[0]}")
train_datasetに15個の例のみを設定していることに気づくことでしょう。これは、小規模なデータに対してGEPAの能力をデモンストレーションするためです。
ステップ 2: 分類エージェントのセットアップ
このデータに対して分類タスクを実行するために、分類エージェントを作成するためにDSPyを使います。
from typing import Literal
import mlflow
import dspy
# turning on autologging traces
mlflow.dspy.autolog(
log_evals=True,
log_compiles=True,
log_traces_from_compile=True
)
# Create a signature for the DSPy module
class TextClassificationSignature(dspy.Signature):
description: str = dspy.InputField()
target: Literal[
'CONCLUSIONS', 'RESULTS', 'METHODS', 'OBJECTIVE', 'BACKGROUND'
] = dspy.OutputField()
class TextClassifier(dspy.Module):
"""
Classifies medical texts into a previously defined set of categories.
"""
def __init__(self, lm_name: str):
super().__init__()
# Define the language model
self.lm = dspy.LM(model=f"databricks/{lm_name}", max_tokens = 25000, cache=False, reasoning_effort="medium")
# Define the prediction strategy
self.generate_classification = dspy.Predict(TextClassificationSignature)
def forward(self, description: str):
"""Returns the predcited category of the description text provided"""
with dspy.context(lm=self.lm):
return self.generate_classification(description=description)
ステップ 3: 評価関数のセットアップ
これは、この分類タスクを我々のLLMがどれだけうまく行っているのかを特定するために使用する関数です。この関数が呼び出されるたびに精度のスコアを取得します。
ここでは、分類結果とジャッジからのフィードバックを収集するために、dspy.Predictionを使用していることに注意してください。Databricksが提供する構築済みCorrectnessジャッジを使用します!
import time
from databricks.agents.evals import judges
def validate_classification_with_feedback(example, prediction, trace=None, pred_name=None, pred_trace=None) -> bool:
"""
Uses Databricks AI judges to validate the prediction and return score (1.0 = corract, 0.0 = incorrect) plus feedback.
"""
# Call correctness judge
judgement = judges.correctness(
request=example.description,
response=prediction.target,
expected_response=example.target
)
# obtain score from judgement (1.0 = correct, 0.0 = incorrect)
if judgement and judgement.value:
score = int(judgement.value.name == "YES")
else:
# if no judgement, fallback to comparing prediction to expected
score = int(example.target == prediction.target)
# obtain feedback from judgement
if judgement and judgement.rationale:
feedback = judgement.rationale
else:
# if no judgement, do not provide feedback
feedback = None
return dspy.Prediction(score=score, feedback=feedback)
def check_accuracy(classifier, test_data: pd.DataFrame = test_dataset) -> float:
"""
Checks the accuracy of the classifier on the test data.
"""
scores = []
for example in test_data:
prediction = classifier(description=example["description"])
score = validate_classification_with_feedback(example, prediction).score
scores.append(score)
return np.mean(scores)
ステップ 4: テストの時間です!
楽しいパートの時間です!最適化されていないシナリオにおいて、GPT-OSS 20B、GPT-OSS 120B、Claude 4がどれだけうまく動いているのかを見てみましょう。
我々はDSPyを使っているので、必要なのは使用するモデルの文字列を変更することだけです。別のプロバイダーのモデルを使いたい場合には、文字列を切り替えます。新しいプロバイダーのためにコードのリファクタリングする必要はありません!
GPT-OSS 20B:
低めではありますが、小規模モデルでは期待通りです。
GPT-OSS 120B:
GPT-OSS 20Bから10%ポイントの改善です。これも、OpenAIで公開されているモデルカードからすれば期待通りです。
Claude 4 Sonnet:
GPT-OSS 20Bから18%ポイントの大きな改善です。これもですが、Claudeを考えれば期待通りです。
テイクアウェイ
この時点では、Claudeははるかに高性能であり、それには正当な理由があります。これは最先端のフロンティアモデルです。しかし、GPT-OSS 20Bを使うよりもおおよそ60倍から75倍高価です(プロバイダーに依存します)。このサイズでのパフォーマンスの差は基本的に期待通りです。
ステップ 5: GEPAの実行!
ステップ1で作成した15行のトレーニングセットを用いて、GPT-OSS 20BでGEPAを実行しましょう。また、以下を指定する必要があります:
- 教師LLM。次のベストステップが何であるのかを評価するために、これはClaudeのようによりパワフルなLLMであるべきです。
- 学生LLM。この場合は小規模なLLM、GPT-OSS 20Bです。
- 新規プロンプトを生成するために、教師LLMが出力をレビューする評価関数。
- 分類エージェント自身。
- スレッディング、シード、最適化レベルのような様々なハイパーパラメーター。
これをエクスペリメントとしてこれを追跡するために、MLflowでこのランをラップします。これによって、これらのLLMが行うすべてを格納、追跡することができ、GEPAがどのように動作するのかに関する可視性を提供し、すべてが適切に動作するようにします。
small_lm_name = "databricks-gpt-oss-20b"
reflection_lm_name = "databricks-claude-sonnet-4"
gepa = dspy.GEPA(
metric=validate_classification_with_feedback,
auto="light",
reflection_minibatch_size=15,
reflection_lm=dspy.LM(f"databricks/{reflection_lm_name}", max_tokens=25000),
num_threads=16,
seed=1
)
with mlflow.start_run(run_name=f"gepa_{id}"):
compiled_gepa = gepa.compile(
TextClassifier(lm_name=small_lm_name),
trainset=train_dataset, #reminder: Only passing 15 training sets!
)
compiled_gepa.save(f"compiled_gepa_{id}.json")
この新規プロンプトをjsonファイルとして保存できます!モデルの重みと同じように、任意のDSPyシグネチャでこのjsonファイルを使用することができます。このため、開発環境でアプリケーションを最適化し、アプリケーションに干渉することなしに、このjsonファイルをプロモーションすることができます。
ステップ 6: 最適化されたアプリケーションのテスト!
いよいよ本番です!GEPAがどれだけうまくやったのかを見てみましょう!GPT-OSS 20Bのみを再実行します。
ワオ!GEPAはGPT-OSS 20Bで+14%改善し、GPT-OSS 120Bを打ちまかし、Claude Sonnet 4に4%で迫る勢いです!
これは、GPT-OSS 120Bよりもこのプログラムはうまく実行することができ、コストは20倍低く、83%小規模なものとなっています!あるいは、コスト削減を考えれば、このパフォーマンスの違いが許容可能な場合、60倍安価な推論のために、Claude Sonnet 4よりもGPT-OSS 20Bを使うことができます!
まとめ
単なるプロンプトの最適化によって、GPT-OSS 20Bが非常に高いパフォーマンスを示すことを目撃しました。これは、小規模な言語モデルがフロンティアモデルの能力に近づくにつれて、生成AI活用の選択肢が大幅に広がります。
以前に、コスト、レイテンシー、開発ライフサイクル、その他の許容できない出費によって、プロダクションに到達できなかったのであれば、Databricksのスコアラー/ジャッジとGEPAを活用することを検討し、MLflowであなたのランを管理しましょう!あなたが、MLエンジニアなのであれば、これら全ては馴染みのあることに感じるに違いありません!
GEPA、DSPy、Databricksに関するさらなるインテグレーションや記事を楽しみにしてください!プロンプトオプティマイザは、あなたの生成AIアプリケーションの長期的なデプロイメントを成功させるには不可欠なものになるでしょう。







