🛠️

DSPyのプロンプト最適化は何をしているのか

に公開

この記事はTimee Product Advent Calendar 2025 17日目の記事です。

はじめに

LLMにより様々なタスクの解決ができるようになりましたが、その振る舞いを制御するには、やはり適切なプロンプト設計が重要です。一方で、プロンプトを書く作業は非常に属人的であり、「なぜそのプロンプトで上手くいくのか」が曖昧になりがちなため、改善の指針が持ちにくいという課題があります。

DSPyはこのような課題を解決するために提案された、プロンプトの最適化を行うためのフレームワークです。DSPyでは、プロンプトやワークフローを宣言的に定義し、それらをデータに基づいて自動的に最適化します。

ところで、初めて「プロンプト最適化」という言葉を聞いたとき、私はこんな疑問を持ちました。

  • 「プロンプト最適化は、何をどう最適化しているのか?」
  • 「無限に近い離散空間の探索をどのように実現しているのか?」

本記事では、このような疑問を整理することを目的として、DSPyの基本的なコンセプトと代表的な最適化アルゴリズムの仕組みについて、原論文の内容を参照しつつ解説します。

なお、この記事では実践的な使用例についての解説は行いません。詳細な使い方については、既に他の優れた記事があるので、そちらを参照してください。

https://zenn.dev/cybernetics/articles/39fb763aca746c
https://zenn.dev/chips0711/articles/a82f3755b364bd

コンセプト

DSPy (Declarative Self-improving Python) は、Stanford NLPによって開発されたLLMプログラム最適化フレームワークです。Declarativeという名前の通り、LLMプログラム全体を構造化されたモジュールの組み合わせとして宣言的に記述します。

DSPyのコンセプトは次の2つに集約されます。

  • 「What」(何を解かせるか)を人間が宣言的に記述
  • 「How」(どう指示するか)はデータによる評価を元に自動で最適化

このコンセプトの下、自由度が高い「プロンプトの記述」という作業を、プログラマブルなものにしています。以下では、DSPyの基本的なコンポーネントについて概観しつつ、上記のコンセプトがフレームワークの中でどのように表現されているかを見ていきます。

コアコンポーネント

Module

DSPyでは、ワークフローを構成する再利用可能なコンポーネントをModuleとして抽象化しています。複数のモジュールを組み合わせることで、発展的なLLMパイプラインを構築することができます。

最も基本的なモジュールがPredictです。これは単一のLLM呼び出しを抽象化したものであり、多くのDSPyモジュールはこのPredictに内部的に依存しています。

モジュールをインスタンス化する際には、以下のように「何を受け取り、何を返すか」を表現するSignatureを指定します。Signatureは、そのモジュールのI/Oに関する取り決めです。このように「How」ではなく「What」をベースとした宣言的な記述が、まさにプロンプトエンジニアリングをプログラマブルにするための仕掛けです。

summarize = dspy.Predict("document -> summary")
result = summarize(document="some long document")
print(result.summary)  # summarized content

Signatureの定義

Signatureの定義には、上記のインライン形式の簡潔な書き方に加え、クラスをベースにした記述方法もあります。Signatureを継承する形で、より柔軟な定義が可能です。

class SentimentAnalysis(dspy.Signature):
    """Classify sentiment polarity (positive / negative / neutral)."""
    text: str = dspy.InputField(desc="The text to analyze")
    label: Literal["positive", "negative", "neutral"] = dspy.OutputField(
        desc="Predicted sentiment label"
    )

analyze_sentiment = dspy.Predict(SentimentAnalysis)

result = analyze_sentiment(text="I love programming! It's so much fun and rewarding.")
print(result.label)  # positive

Signatureを定義する際、フィールド名は任意の命名が可能ですが、タスクを的確に表現した簡潔な命名を行うことが推奨されています。例えば、要約タスクであれば、"document -> summary"などと表現することが考えられます。

Moduleの定義

DSPyではPredictの他にも、様々なプロンプトテクニックを抽象化した組み込みのモジュールが提供されています。代表的なモジュールはChainOfThoughtです。多くの場合、PredictChainOfThoughtに置き換えることで、推論の品質を向上させることができます。

込み入ったロジックを持つモジュールは、以下のように自身でカスタマイズして定義することも可能です(ドキュメントにも言及がありますが、実装は全体的にPyTorchのインターフェースを意識しているようです)。

class SummarizeAndClassify(dspy.Module):
    """Summarize text first, then classify sentiment of the summary."""
    def __init__(self):
        super().__init__()
        self.summarize = dspy.Predict("text -> summary")
        self.classify = dspy.Predict("summary -> sentiment")

    def forward(self, text: str):
        summary = self.summarize(text=text).summary
        result = self.classify(summary=summary)
        return dspy.Prediction(summary=summary, sentiment=result.sentiment)

Optimizer (Teleprompter)

それぞれのモジュールは、内部的に学習可能なパラメータを持っています。典型的には、Few-Shot例やInstructionなどがそれにあたります。DSPyでは、Optimizer(Teleprompterとも呼ばれます)を通してこれらのパラメータを探索し、プログラム全体を最適化します。

プログラムの最適化を行うにあたり、Optimizerには次の3つの材料を渡す必要があります。

  • 最適化する対象のプログラム
  • 学習データ(+ 検証データ)
  • 評価指標(Metric

DSPyでは、推論するサンプル一つ一つをExampleというオブジェクトとして表現し、それらを束ねたものを、学習・評価を行うためのデータセットとします。

trainset = [
    dspy.Example(question="question 1", answer="answer 1").with_inputs("question"),
    dspy.Example(question="question 2", answer="answer 2").with_inputs("question"),
    dspy.Example(question="question 3", answer="answer 3").with_inputs("question"),
    ...
]

予測結果の良し悪しを測るための評価指標(Metric)は、入力と対応する予測値を受け取り、数値(float, int, bool)を返す関数として定義します。

def validate_answer(example, pred, trace=None):
    return example.answer.lower() == pred.answer.lower()

このように定義したデータセットとMetricをOptimizerに渡し、compileメソッドを呼ぶことで、自動的にプログラムが最適化されます。以下は、最もシンプルなOptimizerであるBootstrapFewShotにより最適化を行う例です。

from dspy.teleprompt import BootstrapFewShot

qa = dspy.Predict("question -> answer")
teleprompter = BootstrapFewShot(metric=validate_answer)
optimized = teleprompter.compile(qa, trainset=trainset)

以上、DSPyの基本的な使い方について概観しました。

  • LLMによる推論パイプラインをModuleとして定義し
  • そのパラメータ(Few-Shot例やInstruction)をOptimizerを利用して最適化する

というシンプルな手続きに沿って、プロンプトの最適化を行えることがわかりました。

ここからは、実際にどのような方法でプロンプトの最適化がなされているのか、その内部の仕組みについて深堀りしていきます。

Optimizerのアルゴリズム

本記事では、代表的なOptimizerとして以下の4つのアルゴリズムの中身について紹介します。

アルゴリズム 概要 最適化する対象 コスト
BootstrapFewShot よいFew-Shot例の選択 Few-shot例
BootstrapFewShotWithRandomSearch 最適なFew-Shot例の探索 Few-Shot例
MIPROv2 ベイズ最適化による最適なプロンプトの組み合わせの探索 Instruction (+ Few-Shot例)
GEPA 遺伝的アルゴリズムによる最適なプロンプトの探索 Instruction

BootstrapFewShot

BootstrapFewShotは最もシンプルなOptimizerの一つで、よいFew-Shot例の選択を自動で行います。

公式ドキュメントではデータが非常に少ない(10前後)場合には、このBootstrapFewShotから試すことを推奨しています。

BootstrapFewShotWithRandomSearch

BootstrapFewShotはシンプルなFew-Shot例の選択だけを目的としていましたが、BootstrapFewShotWithRandomSearchはもう一歩進んで、「どのFew-Shot例をプロンプトに組み込むとスコアがよくなるか」という観点から最適化を行います。

公式ドキュメントでは50サンプル程度データが集まる場合に使うことを推奨しています。


Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programsより引用

MIPROv2

MIPRO (Multiprompt Instruction PRoposal Optimizer) では、さらに発展して、Few-Shot例だけでなくInstructionもあわせた最適化を行います(だんだんプロンプト最適化っぽくなってきました)。各推論器に対してFew-Shot例とInstructionを複数生成した上で、ベイズ最適化 (TPE) [1]を用いて、その中から最適な組み合わせを探索します。

MIPROは、Few-Shot例を使わないプロンプト(=Zero-Shot推論)の最適化にも利用できるため、上記の2つよりもさらに汎用的です。公式ドキュメントによると、200サンプル程度の十分なデータがあり、かつ最適化に一定のコストがかけられる場合に有用なオプションであるとされています。


Optimizing Instructions and Demonstrations for Multi-Stage Language Model Programsより引用

GEPA

GEPA (GEnetic-PAreto) は、遺伝的アルゴリズムをベースとした比較的新しいアルゴリズムです。提案された論文では、複数のベンチマークに対して既存のMIPROよりもさらに高いパフォーマンスを出すことが報告されています。

GEPAは、プログラムの候補集合 (Candidate Pool) に対して、次のような遺伝的操作を繰り返すことによって、さらに良い解の探索を行います。

  • 変異 (Mutation) :プログラムの推論結果をコンテキストに入れ、LLMによりさらに良いプロンプトを提案させる
  • 交叉 (Crossover) :2つのプログラムのモジュールを部分的に組み替えて新たな候補を生成する

GEPAの大きな特徴は候補集合の持ち方です。(名前にもあるように)検証データに対するスコアがパレート最適であるような候補集合を保持します。

パレート最適とは「全てのサンプルにおいて自分よりも高いスコアを持つ解」を持たないような解のことです。例えば、検証データの各サンプルについてタスクの成功・失敗が以下のようなテーブルで表されるとき、Program AとProgram Bはパレート最適ですが、Program CはProgram Aに全てのスコアで負けているためパレート最適ではありません。したがって、Program Cは候補集合から除外されます。

検証データ Program A Program B Program C
sample 1 × ×
sample 2
sample 3 × ×
sample 4 × ×
sample 5 ×

このように、最適な一つではなく「少なくとも他のものよりは悪くはない」という緩やかな条件を満たす解を候補として広く持つことによって、局所最適に陥ることを避けながら「探索と活用」を実現しています[2]

GEPAも汎用的なOptimizerであり、複数のモジュールを組み合わせた複雑なプログラムに対しても最適化を行うことが可能です。アルゴリズムの詳細は、以下の通りです。


GEPA: Reflective Prompt Evolution Can Outperform Reinforcement Learningより引用

おわりに

以上、DSPyの基本的なコンセプトと主要な最適化アルゴリズムについて紹介しました。コンセプトとしても興味深く、プロンプト最適化というプロセスに対する解像度が上がりました。

一方で、プロンプト最適化の恩恵を受けるためには、土台となる「信頼できるデータセット」と、最適化を適切な結果に導くための「適切な評価指標設計」がより一層重要であり、実際にはこの辺りに多くの試行錯誤が必要になりそうだとも感じました。

いずれにせよ、DSPyを含むプロンプト最適化の技術は、今後LLMを活用したアプリケーション設計において有効なアプローチの一つとなりそうなので、これからも継続的にキャッチアップしていければと思います。

参考資料

ドキュメント

https://dspy.ai/

GitHub

https://github.com/stanfordnlp/dspy
https://github.com/gepa-ai/gepa

論文

https://arxiv.org/abs/2310.03714
https://arxiv.org/abs/2406.11695
https://arxiv.org/abs/2507.19457

We’re Hiring!

タイミーではデータサイエンティストをはじめ、一緒に働くメンバーを募集しています!
カジュアル面談も行なっておりますので、興味のある方はぜひお気軽にお申し込みください!

https://product-recruit.timee.co.jp/

脚注
  1. 内部実装ではOptunaを利用しています(参考)。 ↩︎

  2. 厳密には、検証データのサンプルのいずれかに対して最も高いスコアを達成するサンプルを保持する仕様になっています。そのため実際には、定義上パレート最適ではある(=優越するプログラムが存在しない)ものの、候補集合から除外される場合があります(参考)。 ↩︎

Discussion