DSPy を初めて触ったとき、正直「またフレームワークか」と身構えました。ところが、自分が 3 日かけて詰めたプロンプトと、DSPy の MIPROv2 が 20 分で自動生成したプロンプトを比べると、後者の方が明確に精度が高かったのです。手書きプロンプトでは拾えていなかった「例示の組み合わせ」が、勾配なしの探索で自動的に発見されていました。
このあたりで「プロンプト職人」としての手癖が通用しない世界が来たな、と認めざるを得ませんでした。本記事はその体験を下敷きに、Gemini API と DSPy を組み合わせたプロンプト自動最適化パイプラインを、ゼロから本番投入まで通して解説します。Python の基礎と Gemini API を軽く触った経験がある方であれば、コードをコピーしてそのまま動かせるように構成しています。
本記事で扱うのは、次のような場面に心当たりのある方向けの内容です。プロンプトを毎週手で微調整しているが、品質が頭打ちになってきた。OpenAI や Anthropic から Gemini に乗り換えたが、同じプロンプトを使っても精度が再現されありません。Few-shot 例を手動で選んでいるが、どの組み合わせが最適かは結局勘で決めています。こうした悩みを持っている方にとって、DSPy は「プロンプト工学」から「プロンプト計算」への移行を助けてくれる強力なツールです。
もう一点前置きしておくと、DSPy は魔法ではありません。ラベル付きデータを用意する工程と、評価関数を設計する工程は、これまで通り人間の仕事として残ります。むしろ、ここが丁寧かどうかで結果の質が決まります。ここではこの 2 つの工程にも、実務で詰まりがちなポイントを含めて触れていきます。
プロンプト職人の限界はどこで来るか
私は Gemini Lab の記事制作で、タイトル生成や要約、タグ付けなど、20 個ほどのプロンプトを継続的にメンテナンスしています。最初は gemini-2.5-flash に投げるプロンプトを一行ずつ手で調整していました。A/B テストして良かった方を残す、という典型的な流れです。
このやり方が破綻するのは、だいたい次の 3 つが重なったときです。
入力データの性質が変わったとき(例: 記事のジャンルが増えた)
モデルを切り替えたとき(例: Flash から Pro、または Gemini 2.5 から 3.1)
評価軸を増やしたとき(例: 「正確さ」だけでなく「文字数」「口調」も同時に最適化したい)
手書きで最適化していると、上記のどれかが起こった瞬間、過去の微調整が一気に陳腐化します。しかも変更のたびに再度 A/B を回すコストが発生します。DSPy は、このコストを「評価関数とデータを用意しておけばあとは自動」という形に置き換えてくれます。
DSPy(Declarative Self-improving Language Programs)は Stanford NLP が開発した、LLM を関数のように扱うためのフレームワークです。プロンプトは「書くもの」ではなく「データから最適化されるもの」というのが核となる思想で、PyTorch における nn.Module に近い抽象でプロンプトを定義します。
個人的に腹落ちしたのは、DSPy が「プロンプトエンジニアリング」と「コンパイラ」という 2 つの世界を橋渡ししている点です。プロンプトを人間が書く高級言語と見立て、Optimizer がそれを機械が扱いやすい中間表現に「コンパイル」する、という理解が一番しっくり来ました。この比喩で考えると、「自分でプロンプトを最後まで書こうとする」行為は、C コンパイラが生成するアセンブリを人間が手書きするのに似ています。可能ではありますが、効率が悪いですし、そもそも最適解にたどり着けません。
最初のパイプラインを組んでみる
まずは動く最小構成を作ります。インストールと Gemini への接続までを示します。
# 依存関係。dspy-ai 2.6 以降は Gemini の LiteLLM 経由接続に対応
pip install -U "dspy-ai>=2.6.0" google-generativeai litellm python-dotenv
.env に Gemini の API キーを入れておきます。
# .env
GEMINI_API_KEY = YOUR_GEMINI_API_KEY
DSPy からは、LiteLLM を介して Gemini を呼び出すのが最もシンプルです。モデル名に gemini/ プレフィックスを付けるだけで LiteLLM が Google AI Studio の API を叩いてくれます。
# setup_dspy.py — DSPy と Gemini の接続を確認する最小例
import os
import dspy
from dotenv import load_dotenv
load_dotenv()
# LiteLLM 形式でモデルを指定。temperature は再現性のため 0 にしておく
lm = dspy.LM(
model = "gemini/gemini-2.5-flash" ,
api_key = os.environ[ "GEMINI_API_KEY" ],
temperature = 0.0 ,
max_tokens = 1024 ,
)
dspy.configure( lm = lm)
# 一番単純な Signature(入出力の宣言)
class Translate ( dspy . Signature ):
"""日本語テキストを自然な英語に翻訳する。"""
japanese = dspy.InputField( desc = "翻訳対象の日本語" )
english = dspy.OutputField( desc = "英訳結果" )
predictor = dspy.Predict(Translate)
result = predictor( japanese = "プロンプトを手書きする時代は終わった。" )
print (result.english)
# 期待出力例: "The era of writing prompts by hand is over."
このコードが動けば、DSPy × Gemini の接続は成功です。注目してほしいのは、ここまでの実装に「プロンプト文字列」が一切出てこないことです。DSPy は Signature の docstring とフィールド定義からプロンプトを自動生成しており、モデルへの実際の送信内容はフレームワーク内部で管理されています。
実行中のプロンプトが気になる場合は dspy.inspect_history(n=1) で確認できます。デバッグ時はこれを多用することになるので、早めに馴染んでおくとよいです。
なお、私の環境では temperature=0.0 にしていても、Gemini 2.5 Flash は完全に同じ出力を返さないことがあります。これは Google 側の実装上、確率が最大のトークンが複数並んだ場合に決定的な並び順が保証されないためのようです。再現性を厳密に取りたい場合は、同じ入力で 3〜5 回呼び出して多数決を取る、あるいは seed パラメータを設定する(Gemini 3.1 以降で対応)という方針を考えてください。DSPy の内部でリトライが走るとさらにゆらぎが生じるので、CI で「プロンプトのスナップショットテスト」を書くときはこの挙動を前提に設計してください。
Signature と Module を分けて考える
DSPy の設計が優れているのは、Signature(何を入れて何を出すか) と Module(どうやって推論するか) を綺麗に分離している点です。同じ Signature に対して複数の Module を付け替えられるので、「一旦 Predict で書いて、精度が足りなければ ChainOfThought に差し替える」といった実験が容易になります。
# signature_and_module.py — 同じ Signature を異なる Module で処理する
import dspy
class ClassifyInquiry ( dspy . Signature ):
"""ユーザー問い合わせを「支払い / バグ報告 / 機能要望 / その他」の 4 カテゴリに分類する。"""
inquiry: str = dspy.InputField( desc = "ユーザーからの問い合わせ本文" )
category: str = dspy.OutputField(
desc = "4 カテゴリから 1 つ: payment / bug / feature / other"
)
# 1) Predict: 素直に一発回答を求める。速くて安い
quick_classifier = dspy.Predict(ClassifyInquiry)
# 2) ChainOfThought: 推論理由も出力させる。複雑な問い合わせで精度が上がる
thoughtful_classifier = dspy.ChainOfThought(ClassifyInquiry)
sample = "先週のアップデートから、iPhone のアプリで設定画面を開くとクラッシュします。"
print (quick_classifier( inquiry = sample).category)
# 期待出力: "bug"
out = thoughtful_classifier( inquiry = sample)
print (out.reasoning)
# 期待出力例: "「クラッシュ」という語からアプリの不具合を示唆しているため..."
print (out.category)
# 期待出力: "bug"
実務では、安価な Flash で Predict を回してみて、誤分類が一定の割合を超えたケースだけ ChainOfThought にフォールバックさせる、という二段構成にすることが多いです。全件 ChainOfThought は精度は上がりますが、出力トークンが 3〜5 倍に膨らむのでコストに直撃します。私はこのトレードオフを見誤って、最初の本番投入でトークン費用を 4 倍にしてしまった経験があります。
この二段構成を DSPy で綺麗に書くなら、dspy.Module を継承した自作クラスに両方の分類器を保持し、信頼度に応じて切り替える形が素直です。例えば quick_classifier の結果の出力トークン確率(Gemini 2.5 Pro 以降で取得可能)を参照して、閾値を下回ったら thoughtful_classifier を呼ぶ、という実装です。ここで重要なのは、Module 内のサブモジュールもすべて DSPy の最適化対象になることです。親 Module を compile すれば、quick_classifier と thoughtful_classifier の両方が同時に最適化されます。これは手書きプロンプトでは絶対に再現できない芸当です。
Optimizer で「プロンプトを書かない」に移行する
ここからが DSPy の真骨頂です。Optimizer はラベル付きの少量データを受け取り、モデルに渡すプロンプトと Few-shot 例を自動で探索します。人間が考えるよりも、組み合わせの探索では機械の方がはるかに速いです。
代表的な 2 つを押さえておけば当面は困りません。
BootstrapFewShot : トレーニングデータから少数の成功例を抽出し、Few-shot プロンプトに埋め込む。安価・高速
MIPROv2 : プロンプト文言と Few-shot 例を同時に最適化する上位版。計算コストは高いが精度は最も良い
以下は ClassifyInquiry を BootstrapFewShot で最適化する完全な例です。
# optimize_classifier.py — 20 件の少量データで自動最適化する
import dspy
from dspy.teleprompt import BootstrapFewShot
# 事前に環境変数 GEMINI_API_KEY を設定しておく
dspy.configure(
lm = dspy.LM( model = "gemini/gemini-2.5-flash" , temperature = 0.0 , max_tokens = 256 )
)
class ClassifyInquiry ( dspy . Signature ):
"""ユーザー問い合わせを payment / bug / feature / other のいずれかに分類する。"""
inquiry: str = dspy.InputField()
category: str = dspy.OutputField( desc = "payment, bug, feature, other のいずれか" )
# トレーニング例(実運用では 50〜200 件あると安定する)
trainset = [
dspy.Example( inquiry = "クレジットカードの請求が二重に来ています。" , category = "payment" ).with_inputs( "inquiry" ),
dspy.Example( inquiry = "ダークモードを追加してほしい。" , category = "feature" ).with_inputs( "inquiry" ),
dspy.Example( inquiry = "アプリが起動直後に落ちます。" , category = "bug" ).with_inputs( "inquiry" ),
dspy.Example( inquiry = "退会手続きを教えてください。" , category = "other" ).with_inputs( "inquiry" ),
# ...(省略。現実には 20 件以上)
]
# 評価関数: category が完全一致していれば 1、そうでなければ 0
def exact_match (example, pred, trace = None ):
return pred.category.strip().lower() == example.category.strip().lower()
student = dspy.Predict(ClassifyInquiry)
optimizer = BootstrapFewShot(
metric = exact_match,
max_bootstrapped_demos = 4 , # プロンプトに埋め込む成功例の数
max_labeled_demos = 16 , # ラベル済み例の最大参照数
max_rounds = 1 ,
)
compiled = optimizer.compile( student = student, trainset = trainset)
# 保存して再利用(JSON で保存される)
compiled.save( "compiled_classifier.json" )
# 読み込みと推論
loaded = dspy.Predict(ClassifyInquiry)
loaded.load( "compiled_classifier.json" )
print (loaded( inquiry = "プレミアムプランに戻したいです。" ).category)
# 期待出力: "payment"(請求関連の問い合わせと推論される)
compile の中で何が起きているか、初見ではピンと来ないと思います。ざっくり言うと、student をトレーニング例に対して実行し、exact_match が 1 を返した例だけを Few-shot プロンプトに採用しています。ルール上、どの例も「DSPy 自身が正しく解けた例」なので、Signature と相性が良い形式で保存されます。手書き Few-shot とは異なる論理です。
もっと高度な最適化を試したい場合は MIPROv2 に差し替えるだけで済みます。
from dspy.teleprompt import MIPROv2
optimizer = MIPROv2(
metric = exact_match,
auto = "light" , # "light" / "medium" / "heavy" でコスト調整
num_threads = 4 ,
)
compiled = optimizer.compile( student = student, trainset = trainset, requires_permission_to_run = False )
MIPROv2 はプロンプト本文(instruction)と Few-shot 例の両方を同時に探索するため、BootstrapFewShot と比べて精度は伸びやすい反面、Gemini API 呼び出しが数百回単位で発生します。auto="light" でも 50〜200 回程度の API 呼び出しを覚悟してください。開発時は Flash で最適化し、本番推論だけ Pro に切り替える、というハイブリッド構成が現実的です。
どちらを使うべきかで迷う場合、私は次の順序で判断しています。まず BootstrapFewShot を試し、精度が目標に 5 ポイント以内なら採用します。届かない場合のみ MIPROv2 を回します。MIPROv2 は後述のように料金が結構かかるため、全タスクで常用すると API 費用が月数万円レベルで変動します。「タスクの重要度に応じて Optimizer を選ぶ」という運用が現実的です。
評価関数を LLM-as-a-Judge で自作する
完全一致で測れるタスクばかりではありません。要約・翻訳・FAQ 応答など、自由記述の品質を測るときは LLM-as-a-Judge パターンが有効です。これ自体も DSPy の Signature として表現できます。
# llm_judge.py — 要約品質を Gemini に判定させる評価関数
import dspy
class SummaryJudge ( dspy . Signature ):
"""記事原文と要約を比較し、総合品質を 1〜5 で採点する。"""
article: str = dspy.InputField( desc = "元記事本文" )
summary: str = dspy.InputField( desc = "評価対象の要約" )
rationale: str = dspy.OutputField( desc = "採点の根拠(日本語、100 文字以内)" )
score: int = dspy.OutputField(
desc = "整数で 1〜5。5 が最高、1 が最低。"
)
# 判定は Pro を使う(判断の一貫性が重要なので Flash より精度を優先)
judge_lm = dspy.LM( model = "gemini/gemini-2.5-pro" , temperature = 0.0 , max_tokens = 512 )
judge = dspy.Predict(SummaryJudge)
def summary_metric (example, pred, trace = None ):
with dspy.context( lm = judge_lm):
result = judge( article = example.article, summary = pred.summary)
try :
score = int (result.score)
except ( TypeError , ValueError ):
return 0.0
# Optimizer には 0〜1 の連続値で返すのが扱いやすい
return max ( 0.0 , min ( 1.0 , (score - 1 ) / 4.0 ))
ポイントは dspy.context(lm=judge_lm) で Judge だけ別モデル に切り替えているところです。推論本体(Flash)と判定(Pro)を分けておくと、Flash で数百件最適化する間も、判定側の品質は安定して保てます。DSPy の dspy.context は一時的にグローバル LM をオーバーライドするためのヘルパーで、with ブロックを抜けると元の LM に戻ります。
実務では、Judge が特定の方向にバイアスをかけていないか(例: 長い要約を過大評価していないか)を必ず確認してください。100 件程度の人手評価と Judge のスコア相関を見て、相関係数 0.7 以上が取れていれば実用レベルと判断して良いと思います。
評価の信頼性を高めるコツとして、SummaryJudge の rationale フィールドを必ず読める形で残しておくことを強くおすすめします。Judge がなぜそのスコアを付けたかをログに保存しておけば、あとから「Judge が単に長さで評価していた」といったバイアスを発見できます。私は Judge のスコアと rationale を BigQuery に貯めて、週次でスポットチェックする運用にしています。これだけで Judge の品質が明らかに安定します。
もう一点補足すると、Judge の Signature には「採点基準」を docstring に明記する点が肝心です。「総合品質を 1〜5 で採点」とだけ書くと、Gemini は甘めの採点をしがちです。代わりに「原文に書かれていない情報が含まれていたら 2 点以下、文法ミスがあれば 1 点減点」のような具体的な減点規則を書くと、Judge の評価が急に現実的になります。このあたりの Judge プロンプト設計は、Gemini API プロンプトテンプレート管理の本番システム で統計的に深掘りしているので、Judge を本番に乗せる前に一読しておくと迷いが減ります。
経験則として、単一の Judge よりも、3 つの異なる観点(正確性・文体・簡潔さ)を評価する Judge を個別に定義し、重み付き平均を取る設計の方が、長期的に安定します。理由は単純で、1 つの Judge が何らかのバイアスを持っていても、他の 2 つが相殺してくれるからです。DSPy 的には、metric 関数の中で複数の Judge を呼び出して平均を返すだけなので、実装コストはほとんど増えません。この「Judge のアンサンブル」も、私が実運用で定着させた工夫の一つです。
ケーススタディ: カスタマーサポート分類を 2 週間分のログで仕立てる
ここまでの部品を組み合わせて、現実的なパイプラインを組みます。想定は「過去 2 週間のサポートログ 500 件」を使い、分類精度を 87% → 95% に改善するケースです。
# production_pipeline.py — 実データで最適化し、検証セットで評価する
import json
import random
import dspy
from dspy.teleprompt import MIPROv2
random.seed( 42 )
dspy.configure(
lm = dspy.LM( model = "gemini/gemini-2.5-flash" , temperature = 0.0 , max_tokens = 256 )
)
class ClassifyInquiry ( dspy . Signature ):
"""ユーザー問い合わせを payment / bug / feature / other のいずれかに分類する。"""
inquiry: str = dspy.InputField()
category: str = dspy.OutputField( desc = "payment, bug, feature, other のいずれか" )
# 500 件の実データを読み込む想定
with open ( "support_logs.json" , encoding = "utf-8" ) as f:
raw = json.load(f)
examples = [
dspy.Example( inquiry = r[ "text" ], category = r[ "label" ]).with_inputs( "inquiry" )
for r in raw
]
random.shuffle(examples)
# 7:2:1 に分割(train / val / test)
n = len (examples)
trainset = examples[: int (n * 0.7 )]
valset = examples[ int (n * 0.7 ) : int (n * 0.9 )]
testset = examples[ int (n * 0.9 ) :]
def exact_match (example, pred, trace = None ):
return pred.category.strip().lower() == example.category.strip().lower()
# ベースライン測定
baseline = dspy.Predict(ClassifyInquiry)
baseline_acc = sum ( 1 for e in testset if exact_match(e, baseline( inquiry = e.inquiry))) / len (testset)
print ( f "Baseline accuracy: { baseline_acc :.3f } " )
# 最適化
optimizer = MIPROv2( metric = exact_match, auto = "light" , num_threads = 4 )
compiled = optimizer.compile(
student = dspy.Predict(ClassifyInquiry),
trainset = trainset,
valset = valset,
requires_permission_to_run = False ,
)
# 最適化後の評価
optimized_acc = sum (
1 for e in testset if exact_match(e, compiled( inquiry = e.inquiry))
) / len (testset)
print ( f "Optimized accuracy: { optimized_acc :.3f } " )
compiled.save( "inquiry_classifier_v1.json" )
# 期待出力例:
# Baseline accuracy: 0.874
# Optimized accuracy: 0.954
このスクリプトを一度走らせるだけで、7 ポイント近い精度改善が得られることが多いです。重要なのは、精度改善が「プロンプトを職人的に磨いた結果」ではなく、「データに基づいて自動探索した結果」である点です。データが増えれば compile を再実行するだけで、継続的に改善されていきます。
実データを扱うときに見落としがちなのが「ラベルの質」です。私が以前関わったプロジェクトでは、人手で付けられたラベルに 6% 程度の誤りがあり、それが MIPROv2 を暴走させていました。誤ラベルが Few-shot 例として採用されると、Optimizer は「誤ラベルに忠実な」プロンプトに最適化してしまいます。最低限、ラベルの信頼度を列として持ち、信頼度が閾値未満の例は trainset から除外することを推奨します。データ品質が整っていないと、どのフレームワークを使っても結局うまく行きません。
よくある落とし穴と対処
3 つほど、私が実際に踏んだ落とし穴を共有します。
落とし穴 1: Optimizer が走り切らずにトークンを浪費する
MIPROv2 は内部的に数十〜数百回の推論を行います。途中で 429 Resource Exhausted が返ると、そこまでの進捗が捨てられます。対策は Flash 専用の無料枠に頼らず、従量課金プロジェクトを別で用意して GEMINI_API_KEY を切り替えることです。また num_threads を上げすぎるとレート制限に即ヒットします。num_threads=4 程度が安全です。
落とし穴 2: 評価関数が緩すぎて、何を最適化しているか分からない
return 1.0 if some_keyword in pred.output else 0.0 のような評価関数でも DSPy は「最適化した」と言います。ただし、それはあなたが指定した目的を達成しているだけで、実際のユーザー満足度と一致しないことが多いです。少なくとも metric は複数の観点(完全一致 + 文字数制約 + 禁止語チェック)を合成した関数にし、100 件程度は人手で検証してください。
落とし穴 3: compile 後に Signature を変えると例が壊れる
保存した JSON に記録された Few-shot 例は、当時の Signature と紐付いています。フィールド名を変えたり、型を変えたりすると、読み込み時にエラーになるか、最悪無視されます。Signature を変更したら必ず compile をやり直す運用を徹底してください。バージョン管理には compiled_classifier_v2.json のように明示的なファイル名を使うのが無難です。
追加で、Gemini 固有の注意点もあります。LiteLLM 経由で Gemini を呼ぶと、Structured Output の JSON バリデーションが微妙に緩いケースがあります。具体的には、Signature で score: int と宣言していても、Gemini が "4.0" と小数で返してくるケースがまれにあり、そのまま int にキャストして落ちる、という事象を踏みました。対策は評価関数側で int(float(x)) と二段階でキャストする、あるいは Signature の docstring に「小数ではなく整数で返すこと」と明記することです。この手の細かい挙動は Gemini API プロンプト回帰テスト Pytest ガイド でテスト手法ごと整理してあるので、本番投入前に読んでおくと事故が減ります。
落とし穴 4: 日本語データで Few-shot の順序が精度に効く
英語のベンチマークでは Few-shot 例の順序が精度に与える影響は小さいと言われていますが、日本語データでは違うようです。私の環境で、同じ 4 件の Few-shot を「カテゴリ順」「ランダム」「難易度昇順」の 3 パターンで試したところ、それぞれ 92%・94%・96% と明確に差が出ました。DSPy はデフォルトでランダム順になりますが、最適化時に max_bootstrapped_demos を増やしすぎると、順序の影響を受けやすくなります。経験的には 3〜5 件に抑えて、順序依存を減らす運用が安定します。
落とし穴 5: Gemini Safety Filter が Few-shot 例をブロックする
分類タスクの trainset に、たまたまネガティブな言い回しが含まれていた場合、Gemini の Safety Filter が Few-shot 例ごとブロックすることがあります。ブロックされた例は Optimizer に「失敗例」として解釈され、結果として重要なパターンが学習されません。対策は safety_settings を BLOCK_ONLY_HIGH 以上に緩め、なおかつ trainset から極端な文面を除外することです。私は前処理で NG ワードリストを使って trainset を事前にフィルタリングしています。Safety Filter 関連の挙動は Gemini API セーフティフィルタでレスポンスがブロックされるときの対処 にまとめてあります。
本番パイプラインに組み込むための 3 つの工夫
ローカルで動くのと、本番で回り続けるのは別物です。最後に、私が実務で採用している 3 つの工夫を共有します。
工夫 1: compiled_*.json を Git 管理する
DSPy の save() が出力するのは素の JSON です。プロンプト本文と Few-shot 例が人間可読で入っているため、レビュー可能です。私は prompts/ ディレクトリに配置して通常の PR フローに乗せ、「誰が」「どのデータで」最適化したかをコミットログで追跡しています。こうしておくと、本番で精度が落ちたときに「前バージョンに戻す」が即座にできます。
さらに踏み込んで、compiled_*.json の PR には必ず「最適化前後の精度差」を CI コメントとして自動投稿する仕組みを入れておくと、レビューが格段に楽になります。具体的には GitHub Actions で pytest --benchmark を走らせて、testset の精度を旧版・新版で比較し、差分が 1% 未満なら PR にラベルを付けて自動マージ、1% 以上の変化があれば人間のレビューに回す、という運用です。この仕組みがあると、プロンプト変更の影響が明示的に数字で見えるので、安心して頻繁な再最適化ができます。
工夫 2: 最適化と推論でモデルを分ける
前述の通り、最適化フェーズは Flash、本番推論フェーズは Pro、という構成が費用対効果に優れます。DSPy は最適化時に使ったモデルと推論時のモデルが違っていても動きますが、精度が少し変わることはあるため、最終評価は必ず推論モデルで行ってください。私のワークフローでは、最適化後に testset の評価だけ Pro で走らせて、Pro でも目標精度を超えていることを確認してからデプロイしています。
工夫 3: 月 1 回、実データで compile を再実行する
ユーザーの問い合わせ文面は時間と共に変化します。2 週間前の最適化結果が 3 ヶ月後も最適とは限りません。私は GitHub Actions の workflow_dispatch で月次の再最適化ジョブを組んであり、精度が閾値(例: 93%)を下回ったら Slack に通知が飛ぶようにしています。こうしておくと、プロンプトを「書き続ける」タスクではなく「監視する」タスクに変換できます。
この 3 つの工夫をまとめて運用するために、私は次のようなディレクトリ構成に落ち着いています。app/ には本番コード、prompts/ にバージョン管理された compiled_*.json、training_data/ にラベル済みデータ、.github/workflows/ に月次再最適化ジョブを置く、という構成です。DSPy のコードベース自体は小さくても、このメタデータを丁寧に管理できるかどうかで運用品質が決まります。コードレビュー文化のあるチームなら、prompts/ の PR レビューを専任で回すとさらに安定します。
この運用を踏まえた本番アーキテクチャ全般については Gemini API 本番アーキテクチャパターン集 2026 で詳しく扱っているので、チームへ展開する前に目を通しておくと設計判断が早くなると思います。
コスト試算 — Gemini で DSPy を回すと実際いくらかかるか
DSPy の Optimizer は、思った以上に API を叩きます。導入判断の前に現実的なコスト感を把握しておくのは大事ですので、私が手元で計測した数字を共有します(2026 年 4 月時点の Gemini API 料金ベース、為替 150 円/ドル想定)。
まず BootstrapFewShot で 50 件のトレーニングデータを使った場合、内部の推論回数は 70〜120 回程度に収まります。Gemini 2.5 Flash の入力単価が安価なこともあり、1 回の compile あたり数十円〜百円程度の感覚です。これは毎日実行しても月数千円なので、ほぼ無視できる金額です。
一方 MIPROv2 の auto="medium" は、同じ 50 件でも 300〜600 回の推論が発生します。加えてプロンプト生成用のメタ推論(Gemini 2.5 Pro を使う)も走るため、1 回の compile で数百円〜千数百円になります。auto="heavy" まで上げると、タスクによっては 1 回 5,000 円を超えるケースもありました。月次で再最適化するなら許容できますが、開発中に何度も走らせると目に見えて費用が嵩みます。
対処としては、開発フェーズでは小さいデータセット(20〜30 件)で素早く回し、本番展開前にのみ全データで auto="medium" を一度走らせる 、という流れが費用対効果に優れます。また Gemini にはバッチ推論の仕組みもあり、LiteLLM 経由で有効化すれば通常の推論より 50% ほど安くなります。DSPy 側で明示的にバッチ化する API はまだ安定していませんが、num_threads を増やして並列度を上げるだけでも、実感としては十分に高速です。
もし費用を 1 桁減らしたい場合は、Gemini 2.5 Flash Lite を compile 時の生徒モデルとして使うのも有効です。本番は Flash か Pro に差し替える前提で、Optimizer のループだけ Lite で回すと、同じ探索空間を大幅に安く探索できます。Gemini のモデル階層をうまく使い分けるのが、DSPy をコスト効率良く運用するコツです。
導入の順序 — 今日から始めるならこの順で
ここまで読んで「自分のプロジェクトにも入れたい」と感じた方向けに、推奨の導入順序を示します。順序を間違えると、DSPy の恩恵を感じる前に脱落してしまうので、ここは重要です。
既存プロンプトのうち、最も手戻りが多い 1 つを選ぶ 。サマライズでも分類でも良いです。対象は小さく、評価が明確なものが向いています
評価用データを 30〜50 件ラベル付けする 。これは人間の仕事です。ここで妥協するとあとの最適化の質が決まりません
既存プロンプトを Signature に移植し、ベースライン精度を測る 。ここでは Predict のまま、Few-shot なしで動かします
BootstrapFewShot を一度だけ走らせる 。この時点でほぼ必ず数ポイントの改善が出ます
精度が目標に届かなければ MIPROv2 に切り替える 。ここで届かない場合は、Signature の設計か評価関数に問題がある可能性が高いです
本番投入し、月次の再最適化ジョブを組む 。これで「プロンプトを書き続ける」作業から解放されます
この 6 ステップを 1 週間で回せば、DSPy の価値は実感できるはずです。そして一度経験すると、ほかのプロンプトも DSPy に寄せたくなります。最終的には、プロダクト全体のプロンプトが compiled_*.json に置き換わり、「プロンプトを書く人」は「評価関数を書く人」に変わります。これは個人開発者にとっても、チーム開発にとっても、無視できない生産性のジャンプだと思います。
一点だけ補足すると、最初の 1 つを選ぶときは、可能な限り「評価が数値化しやすいタスク」から始めることをおすすめします。分類・抽出・整形といったタスクは評価が楽で、DSPy の効果も実感しやすいです。逆に、創作性のある要約や長文生成は、初手として選ぶと評価関数の設計に時間を取られて挫折しがちなので、慣れてから取り組むと良いと思います。
DSPy に乗り換えて最も変わったのは、「このプロンプト、もう少し工夫したら精度上がるはず」という終わりのない磨き込みから解放されたことです。評価関数と少量のデータを用意してしまえば、あとは自動最適化が勝手にやってくれるので、人間は評価軸の設計に集中できます。もし今、Gemini API を使ったプロダクトのプロンプトを週末ごとに微調整しているなら、今日のうちに 50 件だけラベル付きデータを用意して、BootstrapFewShot を一度走らせてみてください。手応えが掴めた瞬間から、開発の時間配分がまるごと変わってくるはずです。