コンテンツにスキップ

DSPy 基本 用語集+学習メモ — 写経で詰まったところ

ex01〜07 を写経しながら「言葉が混乱する」ポイントと「DSPy の思想」を1枚にまとめたメモ。 サンプルの並び・起動方法は README.md、 連載側の応用は 第25回 ../../software-design/25/STUDY_NOTES.md〜 第28回 ../../software-design/28/STUDY_NOTES.md、 上位の写経インデックスは ../README.md を参照。

対応連載回: 第25回(MIPROv2 で枝豆の妖精)/ 第26回(MIPROv2 で日本語RAG)/ 第27回(GEPA で RAG)/ 第28回(GEPA で ReAct)。 このフォルダはその前段として、DSPy の最小構文(Signature / Predict / ChainOfThought / Module / Evaluate / MIPROv2 / ReAct / save・load)を素手で一周する。


0. DSPy の一番大事な思想(これだけは先に)

DSPy はプロンプトを書く道具ではない。プロンプトを「コンパイルさせる」道具である。

普通のプロンプト開発:

人間が文章を試行錯誤で書き直す → 目視で評価 → また書き直す(手作業ループ)

DSPy:

人間は「入出力の型(Signature)」と「評価関数(metric)」だけを書く
→ Optimizer(MIPROv2 / GEPA)が プロンプト本文 と few-shot 例 を自動生成・選別する

PyTorch との対比がそのまま効く:

PyTorch DSPy
nn.Module / forward dspy.Module / forward
nn.Linear(最小の層) dspy.Predict(最小の LLM 呼び出し)
損失関数 loss_fn metric 関数
optimizer.step()重みを更新 optimizer.compile()プロンプトと demoを更新
model.state_dict() を save/load bot.save()/load() で「指示文 + demo」を save/load

最大の誤解の訂正: 「DSPy はプロンプトを書かなくていいフレームワーク」→ 正確には「人間が手で書かない。が、内部では DSPy が必ずプロンプトを組んでいる」。ex01 の dspy.inspect_history(n=1) でその実物(DSPy が裏で生成したプロンプト全文)を覗ける。これを最初に見ておくと「魔法ではない」と腑に落ちる。


1. 全体像:学ぶ順序と DSPy の最適化フロー

サンプルは「下から積み上げる」構成。Signature という最小単位 → それを実行する Predict/ChainOfThought → 複数を束ねる Module → 評価する Example+Evaluate → 自動最適化する MIPROv2 → 応用(ReAct)と永続化(save/load)。

flowchart TD
    A["ex01: Signature + Predict<br/>入出力の型を宣言し1回LLMを叩く"] --> B["ex02: ChainOfThought<br/>reasoning を自動差し込み"]
    B --> C["ex03: Module<br/>Predict を2段に組む (classify→answer)"]
    C --> D["ex04: Example + Evaluate<br/>正解データと metric で平均スコア"]
    D --> E["ex05: MIPROv2.compile<br/>プロンプトと demo を自動最適化"]
    E --> F["ex07: save / load<br/>最適化結果(指示文+demo)を永続化"]
    C --> G["ex06: ReAct<br/>tools付きエージェント"]
    G -.最適化対象にもできる.-> E

DSPy の中核ループ(ex04→ex05 が体現している部分)を時系列で:

sequenceDiagram
    participant U as 人間
    participant Opt as Optimizer (MIPROv2)
    participant Bot as student (Predict/Module)
    participant LM as LM (gpt-4o-mini)
    participant M as metric 関数

    U->>Opt: Signature + trainset + metric を渡す
    loop プロンプト候補を試行
        Opt->>Opt: prompt_model で指示文の候補を生成
        Opt->>Bot: 候補プロンプト + 候補demo を適用
        Bot->>LM: trainset の各例で推論
        LM-->>Bot: prediction
        Bot->>M: (example, prediction) を渡す
        M-->>Opt: スコア
    end
    Opt-->>U: 最良スコアの「指示文+demo」を載せた optimized_bot

ポイントは 「LM の重みは一切変わらない」こと。変わるのは bot が抱える signature(指示文)と demos(few-shot 例)だけ。ex05 の optimized_bot.signatureoptimized_bot.demos を print して、それを直接目で確認するのが写経のクライマックス。


2. サンプル別の要点(原理つき)

ex01 — SignaturePredict(最小構成)

学ぶのは「DSPy の最小単位は文字列プロンプトではなく型宣言だ」ということ。

lm = dspy.LM(model="openai/gpt-4o-mini", temperature=0.0, max_tokens=512)  # ①
dspy.configure(lm=lm)                                                       # ②

class Summarize(dspy.Signature):        # ③
    """与えられた文章を 1 文で要約する。日本語で出力する。"""
    text: str = dspy.InputField(desc="要約対象の文章")
    summary: str = dspy.OutputField(desc="1 文の日本語要約")

summarizer = dspy.Predict(Summarize)    # ④
pred = summarizer(text=sample)          # ⑤
print(pred.summary)                     # ⑥
dspy.inspect_history(n=1)               # ⑦
やってること なぜそうする
LM オブジェクトを作る。"openai/gpt-4o-mini"provider prefix が必須 DSPy 内部は LiteLLM 経由で多プロバイダ対応。prefix が無いと「どのプロバイダか」が決まらず落ちる
dspy.configure でグローバル LM を設定 以降の全 Predict/Module がこの LM を暗黙に使う。PyTorch の device 設定に近い
Signature を継承し、docstring に指示・フィールドに型と desc docstring が「タスク指示プロンプトの核」になる。desc は各フィールドの説明としてプロンプトに展開される
Predict(Signature) で実行可能オブジェクト化 nn.Linear(in, out) に相当。「型」を「呼べる関数」にする
InputField 名をキーワード引数で渡す 位置引数ではなくフィールド名で渡すのが DSPy 流
戻り値 Prediction から OutputField 名でアクセス pred.summary のようにフィールドごとに取れる
直近の LM 呼び出しの実プロンプト全文を表示 「DSPy が裏で何を書いたか」を可視化する最重要デバッグ手段

ex02 — ChainOfThought(reasoning 自動差し込み)

dspy.Predict(Sig)dspy.ChainOfThought(Sig)差し替えるだけで、出力の前に reasoning フィールドが自動で挿入される。Signature 側は一切いじらない。

plain = dspy.Predict(MathProblem)        # answer だけ
cot   = dspy.ChainOfThought(MathProblem) # reasoning → answer
...
c = cot(question=q)
print(c.reasoning)   # ← Signature に書いてないのに取れる
print(c.answer)

原理: ChainOfThoughtPredict のサブクラスで、内部で Signature を「reasoning(思考過程)→ 元の OutputField」という形に動的に拡張してから LLM に渡す。LLM に「まず考えてから答えろ」と強制するので、算数・論理など中間ステップが効くタスクで精度が上がりやすい。出力トークンは増えるのでコスト・レイテンシは上がる。

ex03 — Module(Predict を 2 段に組む)

PyTorch の nn.Module完全同型__init__ でサブモジュール(Predict)を宣言し、forward でデータフローを書く。

class TwoStepQA(dspy.Module):
    def __init__(self):
        super().__init__()
        self.classifier = dspy.Predict(Classify)        # ① サブモジュール宣言
        self.answerer   = dspy.ChainOfThought(Answer)

    def forward(self, question: str) -> dspy.Prediction:
        cls = self.classifier(question=question)        # ② Step1: 分類
        ans = self.answerer(                            # ③ Step2: 分類結果を流す
            question=question, category=cls.category, keywords=cls.keywords,
        )
        return dspy.Prediction(                         # ④ 中間結果ごと返す
            category=cls.category, keywords=cls.keywords,
            reasoning=ans.reasoning, answer=ans.answer,
        )
意図
self.x = dspy.Predict(...) でサブモジュールを「属性」として持つ。これが重要で、属性として宣言された Predict だけが Optimizer の最適化対象として自動的に列挙される(PyTorch の parameters() 自動収集と同じ仕掛け)
② ③ 1段目の出力を2段目の入力に流すだけ。普通の Python
中間結果(category/keywords)も Prediction に詰めて返すと、後段の Evaluate・デバッグで使える

forward を直接呼ばず bot(question=...) と呼ぶのは nn.Module.__call__ と同じ作法(__call__forward に委譲する)。

ex04 — Example + Evaluate(データと metric)

最適化の前提となる「正解データ」と「評価関数」の作り方。

trainset = [
    dspy.Example(prefecture="北海道", capital="札幌市").with_inputs("prefecture"),  # ①
    ...
]
def exact_match(example, prediction, trace=None) -> float:                          # ②
    return 1.0 if example.capital == prediction.capital else 0.0

evaluator = dspy.Evaluate(devset=trainset, metric=exact_match, num_threads=1)       # ③
score = evaluator(bot)                                                              # ④
意図・ハマりどころ
Example(...)入力も正解もまとめて書き、.with_inputs("prefecture") で「どれが入力か」を宣言。これを付け忘れると入力と正解の区別が崩れて評価が破綻する。地味だが最重要
metric は (example, prediction, trace=None) の固定シグネチャ。floatbool を返す。trace は最適化時にだけ渡される(通常評価では None
devset に評価データ、metric に評価関数。num_threads で並列度
evaluator(bot)平均スコアが返る

exact_match(完全一致)と partial_match(「市」「区」を吸収した部分一致)を両方走らせて、metric の設計次第でスコアが変わることを体感するのが狙い。metric は「何を正解とみなすか」の定義そのもので、最適化の方向を決める舵。

ex05 — MIPROv2(自動最適化、写経のクライマックス)

DSPy のフラッグシップ Optimizer。やることは「before スコア → compile → after スコア → 中身を覗く」。

chat_lm   = dspy.LM(model="openai/gpt-4o-mini", temperature=0.0, max_tokens=128)  # 推論用
prompt_lm = dspy.LM(model="openai/gpt-4o-mini", temperature=0.7, max_tokens=512)  # ① 提案生成用
dspy.configure(lm=chat_lm)

optimizer = dspy.MIPROv2(
    metric=accuracy, prompt_model=prompt_lm, auto="light",                        # ②
    max_bootstrapped_demos=2, max_labeled_demos=2,                                # ③
)
optimized_bot = optimizer.compile(student=bot, trainset=trainset)                 # ④

print(optimized_bot.signature)   # ⑤ 最適化後の指示文
print(optimized_bot.demos[:3])   # ⑥ 選ばれた few-shot demo
意図
LM を2種類使う。推論する chat_lm(安い・温度0)と、プロンプト案を創作する prompt_lm(温度0.7で多様性)。役割が違うので分ける
auto="light" は試行回数の最も軽いプリセット(他に medium/heavy)。光熱費と精度のトレードオフ
bootstrapped_demos=モデル自身に解かせて作る demo、labeled_demos=trainset から選ぶ demo。それぞれ最大件数
compile(student=...) が最適化の実行。内部で train/val 分割、プロンプト候補生成、ベイズ最適化的な探索が走る。新しい bot が返る(元の bot は不変=イミュータブル)
⑤ ⑥ 重みではなく signature(指示文)と demos(few-shot)が変わったことを目で確認。これが「DSPy はプロンプトをコンパイルする」の実物

MIPROv2 = Multiprompt Instruction PROposal Optimizer v2。「指示文の候補をいくつも提案 → trainset で評価 → 良い組み合わせを探索」する最適化器(論文)。

ex06 — ReAct(tools 付きエージェント)

dspy.ReAct(Signature, tools=[...]) でツール使用ループ付きエージェントを1行で組む。

def add(a: float, b: float) -> float:
    """2 つの数値を加算して返す。"""   # ← docstring が tool description として LLM に渡る
    return a + b

agent = dspy.ReAct(QA, tools=[add, multiply, lookup_population], max_iters=5)
pred = agent(question=q)
print(pred.answer)
print(len(pred.trajectory))   # 何ステップ Thought→Action→Observation を回したか

原理: 内部で「Thought(考える)→ Action(ツールを呼ぶ)→ Observation(結果を見る)」を max_iters まで反復する ReAct ループ。ツールは普通の Python 関数で、docstring と型ヒントがそのまま tool の説明・引数スキーマになる。LangGraph の create_react_agent と用途は同じだが、DSPy なので ReAct のプロンプト自体も Optimizer で最適化できるのが差分(第28回が GEPA でこれをやる)。pred.trajectory に全ステップの思考・行動・観測が入っており、エージェントのデバッグに直結する。

ex07 — save / load(永続化、LLM 不要)

最適化済み bot が持つ「指示文 + demo」を JSON で保存・復元する。

greeter = dspy.Predict(Greeting)
greeter.demos = [ dspy.Example(...).with_inputs("name"), ... ]  # 最適化済みのつもりの demo
greeter.save(path)              # 人間可読 JSON で保存
loaded = dspy.Predict(Greeting)
loaded.load(path)               # 別インスタンスに復元

要点: JSON に LM 設定は入らない。中身は「指示文と demo だけ」。だから load 後にあらためて dspy.configure(lm=...) で推論 LM を指定する。これが「チューニング側で save → 本番側で load」という運用パターン(第25回 sd_25 と同型)の最小再現。JSON を開くと「これがプロンプトと demo のすべてです」と読めるのが DSPy の透明性。

補足: ex01.py/ex02.py/ex03.py/ex04.py/ex05.py(番号だけのファイル)は写経用の白紙・途中版で、解説付き本体は ex0N_*.py(説明的な名前のほう)。ex01.pyimport dspy の1行だけ、ex02.pyex02_chain_of_thought.py のほぼ写し。読むのは ex0N_*.py のほう


3. 用語集(最重要)

3-1. 一番混乱する:SignatureModule(型 と 構成物)

用語 何か 粒度 具体例 API
Signature タスクの入出力の型 + 指示(docstring)。「何をするか」の宣言だけで、実行能力は持たない 1タスクの仕様 ex01 Summarize、ex03 Classify/Answer class X(dspy.Signature) + InputField/OutputField
Module 複数の Predict/ChainOfThought束ねて1つの処理にした構成物forward でデータフローを書く 複数ステップの組み合わせ ex03 TwoStepQA(classify→answer) class X(dspy.Module) + __init__/forward

判定基準(1文): 「1回の LLM 呼び出しの仕様」なら Signature、「複数の呼び出しを順序立てて組む」なら Module

よくある誤解: 「Signature を継承したら実行できる」→ ✗。Signature は型宣言だけ。実行するには dspy.Predict(Signature) で包む必要がある(型 → 実行体への変換が要る)。

3-2. PredictChainOfThought(実行体の2種)

両方とも「Signature を実行可能にしたもの」。ChainOfThoughtPredict のサブクラス。

用語 出力 内部でやること いつ使う API
Predict Signature の OutputField そのまま Signature をプロンプト化して1回叩く 単純な変換・分類 dspy.Predict(Sig)
ChainOfThought OutputField の前に reasoning を自動追加 Signature を「reasoning→出力」に動的拡張してから叩く 算数・論理・多段推論 dspy.ChainOfThought(Sig)

判定基準(1文): 中間の思考ステップで精度が上がりそうなら ChainOfThought、単純変換なら Predict(出力トークンが増えるので無駄に CoT を使わない)。

3-3. metricEvaluate(採点ルール と 採点係)

用語 何か 具体例 API
metric 1件の (example, prediction) を0〜1(または bool)で採点する関数 ex04 exact_match/partial_match、ex05 accuracy def m(example, prediction, trace=None) -> float
Evaluate metric を devset 全件に適用し平均スコアを出す係。metric を使う側 ex04 evaluator(bot) dspy.Evaluate(devset=, metric=, num_threads=)

判定基準(1文): 「1件をどう採点するか」を書くのが metric、「全件の平均を出す」のが Evaluate。metric は Optimizer にも Evaluate にも同じものを渡す(採点基準は最適化と評価で共通)。

3-4. ExamplePrediction(正解データ と モデル出力)

混同しやすいが向きが逆。

用語 何か 中身 作られ方
Example 人間が用意する正解付きデータ。入力と期待出力を1つに prefecture="北海道", capital="札幌市" dspy.Example(...).with_inputs("入力フィールド名")
Prediction モデルが返した出力。Predict/Module の戻り値 pred.summary / pred.answer / pred.reasoning Predict/Module 呼び出しの返り値

判定基準(1文): 自分で書くのが Example(正解)、モデルから返るのが Prediction(予測)。metric の中で example.capital(正解)と prediction.capital(予測)を比較する、というのが両者の出会う場所。

with_inputs の罠: Example は入力と正解を区別せず全フィールドを持つ。.with_inputs("prefecture") で「prefecture が入力、残り(capital)は正解」と宣言する。付け忘れると capital まで入力扱いになり評価が崩壊する(ex04 のコメントで強調されている)。

3-5. MIPROv2GEPA(2大 Optimizer)

このフォルダで動かすのは MIPROv2 のみ。GEPA は第27〜28回で登場するので、対比だけ載せる。

用語 最適化アプローチ 何を更新するか このフォルダでの登場 連載
MIPROv2 指示文の候補を多数提案し、ベイズ最適化的に良い「指示+demo」の組を探索 指示文 + few-shot demo ex05 で実動 第25, 26回
GEPA 実行の反省(reflection)と進化でプロンプトを改善(遺伝的+自己反省) 主に指示文(プロンプト文面) 未登場(参照のみ) 第27, 28回

判定基準(1文): demo(few-shot 例)も自動で集めて組み合わせたいなら MIPROv2、プロンプト文面そのものを反省ベースで磨きたいなら GEPA。どちらも LM の重みは触らない(プロンプト最適化器)点は共通。

3-6. compile と通常実行(最適化 と 推論)

DSPy で一番「PyTorch 脳」が効くところ。

操作 何が起きるか コスト 結果
通常実行 bot(text=...) Signature をプロンプト化して LLM を1回叩く(推論) 1リクエスト分 Prediction
compile optimizer.compile(student=bot, trainset=) trainset で多数の試行を回し、最良の「指示文+demo」を探す(最適化) 数十〜数百リクエスト(数分・数十円) 新しい最適化済み bot

判定基準(1文): 答えが欲しいだけなら通常実行、プロンプトを良くしたいなら compile。compile は元 bot を壊さず新インスタンスを返す(イミュータブル)ので、before/after を同時に持てる。

3-7. dspy.LM / dspy.configure / inspect_history(基盤の3つ)

用語 役割 注意
dspy.LM(model="openai/gpt-4o-mini") LM ハンドル。内部は LiteLLM 経由 provider prefix(openai/)必須。付け忘れが定番のハマり
dspy.configure(lm=...) グローバル既定 LM を設定 以降の全 Predict/Module が暗黙に使う
dspy.inspect_history(n=1) 直近 n 回の実プロンプト全文を表示 「DSPy が裏で書いたプロンプト」を見る最重要デバッグ

3-8. よくある誤解の訂正(まとめ)

誤解 正しい理解
DSPy はプロンプトを書かなくていい 人間が手で書かないだけ。内部では DSPy が必ず組んでいるinspect_history で見える)
最適化=モデルの重みを更新 重みは不変。指示文 + few-shot demo が更新される
Signature を継承すれば実行できる Signature は型宣言だけ。dspy.Predict() で包んで初めて実行体になる
ChainOfThought は Signature に reasoning を書く 書かない。ChainOfThought自動で差し込む
with_inputs は任意 実質必須。付け忘れると入力/正解の区別が壊れ Evaluate が崩壊
save すれば LM 設定ごと保存される 保存されるのは「指示文+demo」だけ。load 後に dspy.configure(lm=) が要る

4. クイック早見表(迷ったらここ)

困りごと 見る/使うもの
DSPy が裏でどんなプロンプトを書いたか見たい dspy.inspect_history(n=1)(ex01)
1回の LLM 呼び出しを定義したい dspy.Signature + dspy.Predict(ex01)
推論精度を上げたい(中間ステップ系) dspy.ChainOfThought(ex02)
複数ステップを順序立てて組みたい dspy.Module__init__/forward(ex03)
正解データを用意したい dspy.Example(...).with_inputs(...)(ex04)
全件の平均スコアを出したい dspy.Evaluate(devset=, metric=)(ex04)
プロンプトを自動でよくしたい dspy.MIPROv2(...).compile(student=, trainset=)(ex05)
最適化で何が変わったか確認したい optimized_bot.signature.demos(ex05)
ツールを使うエージェントを組みたい dspy.ReAct(Sig, tools=[fn])(ex06)
エージェントの実行履歴を追いたい pred.trajectory(ex06)
最適化結果を別プロセスで使いたい bot.save()/load() + load後に dspy.configure(lm=)(ex07)
LM 呼び出しでいきなり落ちる model に openai/ prefix が付いているか確認

5. 学んだこと(要点)

  • DSPy の核は「プロンプトを書く」から「型と評価を書く」への発想転換。人間は Signature(型+指示)と metric(採点)を書き、プロンプト本文と few-shot は Optimizer が作る。
  • PyTorch との同型を意識すると一気に分かる: Module/forward、Predict≈nn.Linear、compile≈train、save/load≈state_dict。ただし更新されるのは重みではなくプロンプトと demo
  • with_inputs を付け忘れない。Example の入力/正解の境界を決める一行で、忘れると評価が静かに壊れる。
  • metric は最適化の舵。「何を正解とみなすか」を関数で書くので、ex04 のように完全一致と部分一致で結果が変わる。最適化と評価で同じ metric を共有する。
  • 最適化は元 bot を壊さない(イミュータブル)compile は新インスタンスを返すので before/after を並べて検証できる。
  • inspect_history を最初に1回見ておくと「DSPy は魔法ではない、内部でプロンプトを組んでいるだけ」と腑に落ちる。
  • save の JSON は人間可読。「指示文 + demo がプロンプトのすべて」という DSPy の透明性が、ファイルを開くだけで分かる。
  • 番号だけのファイル(ex0N.py)は途中版。解説本体は ex0N_*.py のほうを読む。

6. 拡張アイデア

  1. ex02 の CoT 効果を定量化する: Predict 版と ChainOfThought 版を ex04 の Evaluate にかけ、算数データセット20件で正答率の差を測る。CoT がコスト増に見合うかを数字で出す。
  2. ex05 の auto を振って比較: auto="light"/"medium"/"heavy" でスコアとコスト(呼び出し回数)をプロットし、トレードオフ曲線を描く。max_bootstrapped_demos も振ってみる。
  3. ex03 の Module を最適化対象にする: TwoStepQA をそのまま MIPROv2.compile に渡し、classifier と answerer の両方のプロンプトが同時に最適化されることを inspect_history で確認する(Module 内の各 Predict が独立に最適化される様子の観察)。
  4. ex06 の ReAct を GEPA/MIPROv2 で最適化: tools 付きエージェントの ReAct プロンプトを最適化し、trajectory のステップ数が減る(=無駄なツール呼び出しが減る)かを測る。第28回の予習になる。
  5. LLM-as-a-Judge metric を書く: ex04 の文字列一致 metric を、別の dspy.Predict で「正解かどうかを LLM に判定させる」metric に置き換え、自由記述タスク(要約・QA)でも Evaluate できるようにする。
  6. save した JSON を手で書き換える: ex07 の JSON の demo を編集して load し直し、「プロンプトを直接編集する」古典的アプローチと DSPy の最適化の橋渡しを体感する。

7. 現代版に移植するなら / 注意点

  • .env 直書きは使わない。README は cp .env.sample .env 手順だが、このリポジトリの方針では .env.op + op run --env-file=.env.op -- uv run python ex0N.py を使う(~/.claude/docs/secret-handling.md 参照)。OPENAI_API_KEY を生でファイルに置かない。
  • model 文字列の prefix: dspy.LM(model="openai/gpt-4o-mini") のように LiteLLM 形式の provider prefix を必ず付ける。旧 DSPy(dspy.OpenAI(...) を直接使う書き方)は非推奨で、現行は dspy.LM + dspy.configure に統一されている。
  • dspy のバージョン: pyproject.tomldspy>=2.6.0。API(dspy.LM/MIPROv2/ReAct/Evaluate)はこの世代の書き方。さらに古い記事の dspy.Predict 以外の旧 Teleprompter 名(BootstrapFewShot 等)が出てきたら世代差に注意。
  • ex05 のコスト: auto="light" でも数分・数十円かかる。試すときは trainset を小さく保つ。

8. 記事参照


作成: 2026-06-12 / 最終更新: 2026-06-12