Definition:Declarative Self-improving Python
DSPyとは、大規模言語モデル(Large Language Model, LLM)を利用するアプリケーションを、自然言語によるプロンプトの記述ではなく、宣言的なプログラムとして構築するためのPythonフレームワークである。DSPyでは、開発者は入力と出力の関係や処理の流れを記述し、実際にモデルへ与えられるプロンプトの生成や最適化はシステムが自動的に行う。このため、従来のプロンプトエンジニアリングに依存した開発手法から脱却し、ソフトウェア工学的な方法でLLMアプリケーションを構築することを目的としている。
従来のLLM利用では、「どのようなプロンプトを書くか」が性能を大きく左右していた。しかしプロンプトは自然言語で記述されるため再利用性や保守性に乏しく、モデルの変更やデータセットの変更によって性能が大きく変動する問題があった。DSPyはこの問題に対し、プロンプトそのものを実装対象から切り離し、プロンプトを最適化対象として扱うことで、より再現性の高い開発手法を提供している。
2022年以降、GPT-3.5やGPT-4などの高性能な大規模言語モデルが普及すると、多くの研究者や開発者がプロンプトエンジニアリングによってシステムを構築するようになった。しかし実際には、少数の模範例を追加したり指示文の表現を変更したりするだけで性能が大きく変化することが知られており、プロンプトの調整作業は試行錯誤に依存し、体系的な開発手法が存在しなかった。
こうした状況の中で、Stanford NLPを中心とする研究グループは、プロンプトをソフトウェアの一部として手動管理するのではなく、自動的に生成・最適化する枠組みを提案した。研究は2022年2月に開始され、前身にあたる DSP(Demonstrate-Search-Predict)として2022年12月に最初のバージョンが公開された。その後2023年10月に DSPy へと発展した。
DSPyの根底にある思想は、ニューラルネットワークにおいて人間が重みを直接設計しないのと同様に、LLMシステムにおいても人間がプロンプトを直接設計する必要はないというものである。
初期のDSPyはFew-shot例の自動生成を主な目的としていたが、2023年2月にはプログラムの重みを最適化するためのコンパイルという概念が導入され(これはStanfordのAlpacaプロジェクト開始より前、GPT-4の初回リリースより約1ヶ月前のことであった)、その後は検索拡張生成(RAG)、マルチステップ推論、エージェントシステムなどを含む複雑なLLMパイプライン全体を最適化する方向へと発展した。現在では単なるプロンプト生成ツールではなく、LLMアプリケーション全体をプログラムとして記述・最適化するためのフレームワークとして位置付けられている。
DSPyの中心的な考え方は、LLM呼び出しを関数として扱うことである。
例えば質問応答システムを考えると、入力として質問が与えられ、出力として回答が得られる。この関係は数学的には、\[f : X \to Y\]
という写像として表現できる。
従来の開発手法では、この関数を実現するためのプロンプトを人間が設計していた。しかし、DSPyでは、関数の入出力仕様のみを定義し、どのようなプロンプトを用いるべきかはオプティマイザが決定する。
この考え方は機械学習におけるモデル学習と類似している。ニューラルネットワークの利用者は通常、重み行列を直接指定するのではなく、モデル構造と目的関数を定義し、学習アルゴリズムに最適化を委ねる。DSPyは同様に、開発者が処理構造と評価指標を定義し、プロンプト最適化をシステムに委ねるのである。
DSPyではまずシグネチャ(Signature)と呼ばれる仕組みによって入出力の関係を定義する。シグネチャは関数型言語における型宣言に相当し、入力フィールドと出力フィールドから構成される。例えば質問応答システムであれば、入力として質問を受け取り、出力として回答を返すという関係を定義する。
その上で、各種モジュールを用いて推論方法を指定する。Predict は単純な一回の推論を行う最も基本的なモジュールである。ChainOfThought は中間推論過程を生成してから最終回答を出力する。これは近年の推論モデルで広く利用されている思考連鎖(Chain of Thought)を抽象化したものである。ReAct は推論(Reasoning)と行動(Acting)を交互に繰り返すモジュールで、外部ツールの呼び出しを組み合わせながら問題を解くエージェント的な処理を実現する。ツール利用を伴う複雑なタスクに適している。
DSPyにおけるコンパイルとは、宣言的に記述されたLLMプログラムを最適化済みの推論パイプラインへ変換することを指す。一般的なプログラミング言語のコンパイルがソースコードを機械語へ変換するのに対し、DSPyのコンパイルはプロンプトの内容そのものを評価データに基づいて最適化するという点で異なる。
コンパイルを担うオプティマイザ(旧称:テレプロンプター)には複数の種類が用意されている。
BootstrapFewShot は、Few-shot例を自動生成してプロンプトに組み込む最も基本的なオプティマイザである。
COPRO(Cooperative Prompt Optimization) は、各ステップの指示文を自動生成・改良し、座標上昇法(ヒルクライミング)で最適化するオプティマイザである。
MIPROv2(Multiprompt Instruction PRoposal Optimizer Version 2) は、指示文とFew-shot例の両方をベイズ最適化によって同時に探索する高度なオプティマイザである。タスクの特性を踏まえた候補指示文を自動的に提案し、評価指標を最大化する最適な組み合わせを効率的に探索する。
さらに、BootstrapFinetune を用いることで、プロンプト最適化にとどまらず、LLM自体のファインチューニングまで自動化することもできる。
DSPyの特徴は、プロンプトを静的な文字列として扱わず、学習可能なパラメータとして扱う点にある。
従来のプロンプトエンジニアリングでは、
「あなたは優秀な数学教師です」
と書くか、
「あなたは経験豊富な数学教師です」
と書くかを人間が試行錯誤して決定していた。
DSPyではこれらの指示文やFew-shot例を探索空間として扱い、評価指標を最大化するよう自動的に最適化する。
その結果として、モデルの変更やタスクの変更が発生した場合でも、再コンパイルによって新しい環境へ適応できる。これはソフトウェアの移植性や保守性を大きく向上させる。
以下はDSPyによる最も単純な質問応答システムの例である。
まず使用するLLM(ここではGPT-4o-mini)を設定。
lm = dspy.LM("openai/gpt-4o-mini", api_key="...")
dspy.configure(lm=lm)「質問を受け取り、回答を返す」という入出力の仕様(シグネチャ)を定義します。プロンプトの中身はまだ決めていません。
class QA(dspy.Signature):
question = dspy.InputField()
answer = dspy.OutputField()
Predictモジュールにシグネチャを渡して呼び出す。DSPyが自動的にプロンプトを組み立ててLLMに送り、result.answerに回答が返ってくる。
qa = dspy.Predict(QA)
result = qa(question="フランスの首都はどこですか?")
コード1との違いは dspy.Predict の代わりに dspy.ChainOfThought を使う点。
ChainOfThought を使うと、LLMは最終回答を出す前に「まず考える」ステップを踏みます。内部的には「途中の思考過程(rationale)」も生成されており、result.rationale として取り出せます。計算問題や複数ステップの推論が必要な質問に有効。
外部の知識ベース(ベクトルDBなど)から、質問に関連する文書を3件取得するモジュールを用意。
self.retrieve = dspy.Retrieve(k=3)
「質問」と「取得した文書」を受け取り、「回答」を生成するモジュールを用意。
self.generate = dspy.ChainOfThought("question, context -> answer")forwardメソッドで上記2つを組み合わせます。まず検索して文書を取ってきて、それを文脈としてLLMに渡して回答を生成する、という2段階のパイプライン。
def forward(self, question):
docs = self.retrieve(question)
return self.generate(question=question, context=docs)
「正解と予測が一致しているか」を判定する評価指標を定義。これがコンパイルの目標となる。
def exact_match(example, prediction, trace=None):
return example.answer == prediction.answer
compileを呼ぶと、訓練データ(train_data)を使って評価指標が最大になるようなプロンプト(指示文・Few-shot例の組み合わせ)をベイズ最適化で自動探索し、最適化済みプログラムを返す。
optimizer = MIPROv2(metric=exact_match, auto="medium")
optimized_program = optimizer.compile(
dspy.ChainOfThought(QA),
trainset=train_data
)
コード1〜3で「構造だけ」定義したプログラムに対して、「どんなプロンプトが最も性能が出るか」を自動で見つけ出す例。コード1〜3が「設計図を書く」作業なら、コード4は「その設計図をチューニングする」作業に相当する。
Mathematics is the language with which God has written the universe.