今日は前回に引き続きDSPyを触っていきます。
前回のLLMを使いやすくするPython製フレームワークDSPyとは?の記事で Optimizer という DSPy の特徴の一つを紹介しました。
DSPy Optimizer とは
前回の記事では、DSPyの基本的な使い方として、SignatureとModuleを使ったシンプルなQ&Aプログラムを紹介しました。dspy.ChainOfThought("question -> answer") と書くだけで、LLMに段階的な推論をさせられるという内容でした。
DSPyが他のLLMフレームワークと一線を画すのは、Optimizer(オプティマイザ)という仕組みです。これは、プロンプトを人間が手作業で調整する代わりに、プログラムが自動的に最適なプロンプトを見つけてくれる機能です。
LLMを使ったアプリケーションの精度を上げるためプロンプトエンジニアリング作業では、例えば「簡潔に答えてください」という指示を「一言で答えてください」に変えてみたり、「ステップバイステップで考えてください」を付け加えたりいろいろな試行錯誤が発生します
時にはお手本を与えることで回答の文体をそろえる方式も試してみたりします。プロンプトの中に「Q: 日本の首都は? A: 東京」のような回答例をいくつか入れて、LLMに「こういう形式で答えてね」と示します。そして、これらの組み合わせを何度もテストして、一番精度が高いものを採用します。
この作業は時間がかかりますし、属人的ですし、モデルを変更するたびにやり直しになります。DSPy Optimizerは、この一連のプロセスを自動化してくれます。
Optimizerの基本的な仕組み
DSPy Optimizerが必要とするものは3つだけです。
1つ目は、最適化したいDSPyプログラムそのものです。例えば dspy.ChainOfThought("question -> answer") のような前回の記事で触れたモジュールです。
2つ目は、少量の学習データです。質問と正解のペアを数件から数十件用意します。数百件の大規模データセットは必要ありません。たとえ回答が「ですます口調」なのか「体言止め」なのかはこの少量の学習データにより自動でDSPyが判別を行い指定してくれるようになります。
3つ目は、評価関数です。「LLMの出力が良いかどうか」を判定する基準です。最もシンプルなのは「正解の文字列が含まれていたらOK」という完全一致の判定ですが、要約や翻訳のようにひとつの正解がないタスクでは、LLM自身に「この回答は適切か?」と採点させる方法もあります。評価関数の設計次第で、Optimizerが目指すゴールが変わります。
この3つを渡すと、Optimizerが内部でLLMを何度も呼び出しながら、最適なプロンプトの構成を探索してくれます。最終的に、最適化済みのプログラムが返ってきます。
BootstrapFewShot
DSPyで最も基本的なOptimizerが BootstrapFewShot です。お手本の事をDSPyでは fewshot と呼びます。名前の通り、お手本を自動的に「ブートストラップ(自力で生み出す)」します。
仕組みはシンプルです。まず、学習データの質問をLLMに解かせます。次に、評価関数で正解できたかどうかを判定します。正解できたケースについて、LLMがどんな推論をしてその答えに至ったかという「トレース(思考の軌跡)」をまるごと記録します。そして、この成功トレースをfew-shotのお手本としてプロンプトに組み込みます。
つまり、「LLMが自分でうまく解けた問題の解き方を、次の問題を解くときのお手本として使い回す」ということです。人間がお手本を考えて書く必要がありません。
実際に BootstrapFewShot を試してみると、最適化後のプロンプトにはLLMが自動生成した推論付きの回答例が挿入されます。これにより、LLMは「こういう形式で、こういう粒度で答えればいいのか」というパターンを学習し、回答の一貫性と精度が向上します。
さっそくやってみる
前回の記事に基づき環境構築及びテストを行った後 optimizer.py というファイルを作成します。
import dspy
# ============================================================
# 1. LMの設定
# ============================================================
# OpenAI を使用
lm = dspy.LM("openai/gpt-4o-mini")
# Anthropic を使う場合はこちらに差し替え
# lm = dspy.LM("anthropic/claude-sonnet-4-20250514")
dspy.configure(lm=lm)
# ============================================================
# 2. 学習データ(正解ペア)を用意する
# DSPy の Optimizer は、この少量データから
# 「良いお手本」を自動選別してプロンプトに組み込む
# ============================================================
trainset = [
dspy.Example(question="日本で一番高い山は?", answer="富士山"),
dspy.Example(question="日本の首都は?", answer="東京"),
dspy.Example(question="太陽系で一番大きい惑星は?", answer="木星"),
dspy.Example(question="水の化学式は?", answer="H2O"),
dspy.Example(question="光の速さはおよそ秒速何キロメートル?", answer="約30万km"),
dspy.Example(question="地球の衛星の名前は?", answer="月"),
dspy.Example(question="日本で一番長い川は?", answer="信濃川"),
]
# with_inputs() で「入力はquestionだけ、answerは正解ラベル」と明示
trainset = [ex.with_inputs("question") for ex in trainset]
# ============================================================
# 3. モジュールを定義する(最適化「前」のベースライン)
# ============================================================
qa = dspy.ChainOfThought("question -> answer")
# ============================================================
# 4. 評価関数を定義する
# Optimizer はこの関数を使って「良い回答かどうか」を判定する
# ============================================================
def metric(example, pred, trace=None):
"""正解が予測に含まれていればTrue"""
return example.answer.lower() in pred.answer.lower()
# ============================================================
# 5. 最適化前の動作を確認
# ============================================================
print("=" * 60)
print("【最適化前】")
print("=" * 60)
test_questions = [
"人体で一番大きい臓器は?",
"世界で一番深い海溝は?",
"日本の国花は?",
]
for q in test_questions:
result = qa(question=q)
print(f" Q: {q}")
print(f" A: {result.answer}")
print()
# ============================================================
# 6. Optimizer で最適化する
# ============================================================
print("=" * 60)
print("【最適化中...】")
print("=" * 60)
optimizer = dspy.BootstrapFewShot(
metric=metric,
max_bootstrapped_demos=3, # LMに解かせて自動生成するお手本の数
max_labeled_demos=2, # trainsetからそのまま使うお手本の数
)
optimized_qa = optimizer.compile(qa, trainset=trainset)
print("最適化完了!\n")
# ============================================================
# 7. 最適化後の動作を確認
# ============================================================
print("=" * 60)
print("【最適化後】")
print("=" * 60)
for q in test_questions:
result = optimized_qa(question=q)
print(f" Q: {q}")
print(f" A: {result.answer}")
print()
# ============================================================
# 8. 最適化で何が起きたかを確認する
# ============================================================
print("=" * 60)
print("【Optimizerが選んだお手本(demos)】")
print("=" * 60)
demos = optimized_qa.predict.demos
print(f" お手本の数: {len(demos)}")
for i, demo in enumerate(demos):
print(f" Demo {i+1}: Q={demo.question} -> A={demo.answer}")
print()
print("これらのお手本がプロンプトにfew-shotとして自動挿入され、")
print("LLMの回答精度が向上します。")
# ============================================================
# 9. 最適化済みモジュールを保存・読み込み(再利用)
# ============================================================
optimized_qa.save("optimized_qa.json")
print("\n最適化結果を optimized_qa.json に保存しました。")
print("次回は以下で読み込めます:")
print(' qa = dspy.ChainOfThought("question -> answer")')
print(' qa.load("optimized_qa.json")')実行すると以下が出力されます。
python optimizer.py
============================================================
【最適化前】
============================================================
Q: 人体で一番大きい臓器は?
A: 皮膚
Q: 世界で一番深い海溝は?
A: マリアナ海溝
Q: 日本の国花は?
A: 日本の国花は桜です。
============================================================
【最適化中...】
============================================================
43%|████████████████████████████████████▍ | 3/7 [00:04<00:06, 1.63s/it]
Bootstrapped 3 full traces after 3 examples for up to 1 rounds, amounting to 3 attempts.
最適化完了!
============================================================
【最適化後】
============================================================
Q: 人体で一番大きい臓器は?
A: 皮膚
Q: 世界で一番深い海溝は?
A: マリアナ海溝
Q: 日本の国花は?
A: 桜
============================================================
【Optimizerが選んだお手本(demos)】
============================================================
お手本の数: 3
Demo 1: Q=日本で一番高い山は? -> A=富士山
Demo 2: Q=日本の首都は? -> A=東京
Demo 3: Q=太陽系で一番大きい惑星は? -> A=木星
これらのお手本がプロンプトにfew-shotとして自動挿入され、
LLMの回答精度が向上します
。
最適化結果を optimized_qa.json に保存しました。
次回は以下で読み込めます:
qa = dspy.ChainOfThought("question -> answer")
qa.load("optimized_qa.json")ポイントは最適化前と後の以下の部分です。
最適化前
Q: 日本の国花は?
A: 日本の国花は桜です。
最適化後
Q: 日本の国花は?
A: 桜出力の文体が変わっています。
以下の部分をOptimizerが学習し出力の文体を自動で調整しています。
============================================================
【Optimizerが選んだお手本(demos)】
============================================================
お手本の数: 3
Demo 1: Q=日本で一番高い山は? -> A=富士山
Demo 2: Q=日本の首都は? -> A=東京
Demo 3: Q=太陽系で一番大きい惑星は? -> A=木星学習に用いられた思考結果は同じディレクトリの`json`ファイルに出力されています。
{
"predict": {
"traces": [],
"train": [],
"demos": [
{
"augmented": true,
"question": "日本で一番高い山は?",
"reasoning": "日本で一番高い山は富士山です。富士山の標高は3,776メートルで、東京からも見える象徴的な山です。",
"answer": "富士山"
},
{
"augmented": true,
"question": "日本の首都は?",
"reasoning": "日本の首都は、政府の機能が集中している都市であり、歴史的にも重要な役割を果たしています。現在の首都
は明治時代に東京に移されて以来、東京が日本の首都として知られています。",
"answer": "東京"
},
{
"augmented": true,
"question": "太陽系で一番大きい惑星は?",
"reasoning": "太陽系で一番大きい惑星は木星です。木星はその質量と体積において他のすべての惑星を上回り、ガス惑星と
して知られています。",
"answer": "木星"
}
],
"signature": {
"instructions": "Given the fields `question`, produce the fields `answer`.",
"fields": [
{
"prefix": "Question:",
"description": "${question}"
},
{
"prefix": "Reasoning: Let's think step by step in order to",
"description": "${reasoning}"
},
{
"prefix": "Answer:",
"description": "${answer}"
}
]
},
"lm": null
},
},
"metadata": {
"dependency_versions": {
"python": "3.12",
"dspy": "3.1.3",
"cloudpickle": "3.1"
}
}
}次回以降の実行ではこの`json`を読み込むだけで最適化されたプロンプトにより出力が生成されるようになります。これは次回の記事で見ていきたいと思います。

