AIエージェント入門

DSPy完全ガイド2026|Stanford発プロンプト最適化フレームワーク

この記事の結論

Stanford NLP発のDSPyは「プロンプトをコードで書く」思想のフレームワーク。BootstrapFewShot/MIPROv2で自動最適化し、手動プロンプト職人作業を不要にする。インストールからRAG実装まで実践コード付きで解説。

perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = “C.UTF-8”
are supported and installed on your system.
perl: warning: Falling back to the standard locale (“C”).

結論:本記事では「DSPy完全ガイド2026」の定義・主要機能・実際の活用方法を、初心者でも理解できる形で体系的に解説します。

対象読者:本テーマに興味がある実務担当者・意思決定者。

読了後にできること:本記事の要点を踏まえて、自社や自分の状況に合わせた次のアクションを判断できます。

perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = (unset),
LANG = “C.UTF-8”
are supported and installed on your system.
perl: warning: Falling back to the standard locale (“C”).

この記事でわかること(3点)

  • DSPyが「プロンプトを手書きするのをやめる」という哲学を持つ理由
  • BootstrapFewShot・MIPROv2 の使い方と使い分け判断基準
  • RAGパイプライン・LangGraph連携まで動くコードで一気通貫

対象読者: Pythonが書けるMLエンジニア・バックエンド開発者・LLMアプリ開発者
今日やること: pip install dspy → Signatures定義 → BootstrapFewShotで最初の最適化を回す

「プロンプトエンジニアリング」という言葉が定着して久しいですが、正直なところ、プロンプトを手で調整し続ける作業に疲弊している開発者は多いはずです。

実際に検証プロジェクトで痛感したのは、プロンプトを少し変えるたびに全テストケースを再実行して、改善したのか劣化したのかを目測で判断するという、本来なら自動化できるはずの泥仕事に何時間も費やしていたということです。

DSPy(Declarative Self-improving Python)はこの問題に対して、根本的に異なるアプローチを提案しています。プロンプトを「書く」のではなく、プログラムとして「コンパイルする」という発想です。

この記事では、Stanford NLPグループが開発したDSPyの哲学から始め、Signatures・Modules・Optimizersの3本柱を実際に動くコードとともに解説します。最終的にはRAGパイプラインの最適化まで実装できるようになります。

DSPyとは何か ―「プロンプトをプログラミングする」哲学

DSPyはStanford NLPグループが2023年に公開したオープンソースフレームワークです(GitHubリポジトリ: stanfordnlp/dspy)。名前はDeclarative Self-improving Pythonの略で、「宣言的かつ自己改善するPython」を意味します。

従来のLLMアプリケーション開発では、開発者が手動でプロンプトを設計し、few-shot例を選び、チェーンを組んでいました。これはアプリケーションのロジックとプロンプトの文字列が密結合した状態です。モデルが変わったり、タスクの要件が変わるたびに、プロンプト全体を作り直す必要があります。

DSPyが提案するのは、この関心の分離です。開発者は「何を入力して何を出力するか」という型安全なSignatureを定義し、「どのように推論するか」という戦略をModuleで選択します。そして最終的な「プロンプトの中身」はOptimizer(コンパイラ)に任せます。

コンパイラはあなたが用意した少量のトレーニング例(trainset)と評価指標(metric)を使い、自動的に最適な指示文とfew-shot例を探索します。この発想は、CコードをアセンブリにコンパイルするGCCの哲学に近いと公式ドキュメントは説明しています。

LangGraphやPydantic AIがエージェントのオーケストレーションや型安全性を担うのとは明確に異なる軸です。DSPyは「プロンプトそのものの最適化」レイヤーを担当します。これについては後述の比較セクションで詳しく整理します。

インストールと初期設定

まずインストールから始めましょう。DSPyはPyPIで公開されており、pip一発で入ります。

# 基本インストール
pip install dspy

# バージョン確認
python -c "import dspy; print(dspy.__version__)"

次にLMの設定です。DSPyはLiteLLM経由で主要プロバイダーに対応しています。

# 動作環境: Python 3.10+, dspy>=2.5
# pip install dspy

import dspy
import os

# OpenAI を使う場合
lm_openai = dspy.LM("openai/gpt-4o-mini", api_key=os.environ["OPENAI_API_KEY"])
dspy.configure(lm=lm_openai)

# Anthropic を使う場合
lm_anthropic = dspy.LM("anthropic/claude-3-5-haiku-20241022", api_key=os.environ["ANTHROPIC_API_KEY"])
dspy.configure(lm=lm_anthropic)

# OSSモデル(Ollama経由)
lm_local = dspy.LM("ollama/llama3.2", api_base="http://localhost:11434")
dspy.configure(lm=lm_local)

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

ポイントとして、dspy.configure(lm=...) で設定したLMはグローバルなデフォルトとして機能します。Module単位で別のLMを割り当てることも可能です。

Signatures ―型安全なプロンプト定義

Signatureは「このモジュールは何を入力として受け取り、何を出力するか」を宣言するDSPyの核心概念です。型安全なコントラクト定義と考えると理解しやすいです。

インラインSignature(文字列形式)

import dspy

# 最もシンプルな形式: "入力 -> 出力"
qa = dspy.Predict("question -> answer")
response = qa(question="Pythonの辞書の特徴を3点教えてください")
print(response.answer)

# 複数入力・複数出力
classify = dspy.Predict("sentence, context -> sentiment: bool, confidence: float")
result = classify(
    sentence="このサービスは本当に便利です",
    context="カスタマーレビュー"
)
print(result.sentiment, result.confidence)

クラスベースSignature(複雑な定義に推奨)

import dspy
from typing import Literal

class QASignature(dspy.Signature):
    """質問に対して根拠付きで回答する。回答は事実のみに基づき、不確かな場合は明示する。"""

    question: str = dspy.InputField(desc="ユーザーからの質問")
    context: str = dspy.InputField(desc="参考情報(検索結果や文書)", default="")

    answer: str = dspy.OutputField(desc="正確で簡潔な回答(2-3文)")
    confidence: Literal["high", "medium", "low"] = dspy.OutputField(
        desc="回答の確信度"
    )
    citations: list[str] = dspy.OutputField(
        desc="回答の根拠となる箇所のリスト"
    )

# 使用例
qa = dspy.Predict(QASignature)
result = qa(
    question="DSPyのMIPROv2とBootstrapFewShotの違いは何ですか?",
    context="MIPROv2は命令文とfew-shot例を同時最適化する。BootstrapFewShotはfew-shot例の生成に特化している。"
)
print(result.answer)
print(f"確信度: {result.confidence}")

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

ポイントは、dspy.Signatureのdocstringがそのままシステムプロンプトの一部として使われる点です。「根拠付きで回答する」「不確かな場合は明示する」といった制約をdocstringに書くだけで、DSPyがプロンプトに組み込みます。

Modules ―推論戦略を選択する

SignatureがWHATを定義するのに対し、Moduleは推論のHOWを決定します。主要な3つを実装例とともに見ていきます。

dspy.Predict ―基本予測

import dspy

# 最もシンプルなModule。Signatureをそのまま実行する
summarize = dspy.Predict("document: str -> summary: str, key_points: list[str]")
result = summarize(document="DSPyは...(長い文書)")
print(result.summary)

dspy.ChainOfThought ―推論ステップを自動追加

import dspy

# Predictと同じSignatureを使えるが、内部でrationale(推論過程)フィールドを自動追加
cot = dspy.ChainOfThought("question: str, context: str -> answer: str")
result = cot(
    question="この文書の主張は論理的に一貫しているか?",
    context="...(文書内容)..."
)
# result.rationale に推論過程が格納される
print(result.rationale)
print(result.answer)

dspy.ProgramOfThought ―コードを生成して実行

import dspy

# 数学・データ分析タスクに最適。Pythonコードを生成して実行結果を返す
pot = dspy.ProgramOfThought("question: str -> answer: str")
result = pot(question="1から100までの素数の合計はいくつですか?")
# 内部でPythonコードを生成・実行して正確な数値を返す
print(result.answer)  # 1060

使い分けの目安として、まずPredictで試し、精度が不十分ならChainOfThoughtに差し替えます。数値計算や厳密な論理タスクはProgramOfThoughtが有効です。公式ドキュメントによれば、多くのケースでChainOfThoughtに変えるだけで品質が向上するとされています。

カスタムModuleの構築

import dspy

class MultiHopQA(dspy.Module):
    """複数ステップの推論が必要なQAシステム"""

    def __init__(self, num_hops: int = 2):
        super().__init__()
        self.num_hops = num_hops
        # 各ホップで使うモジュールを定義
        self.retrieve = dspy.Predict("question: str -> search_query: str")
        self.reason = dspy.ChainOfThought("question: str, context: str -> answer: str, needs_more_info: bool")

    def forward(self, question: str) -> dspy.Prediction:
        context = ""
        for hop in range(self.num_hops):
            # 検索クエリを生成
            search_step = self.retrieve(question=question)
            # 実際の検索はここで行う(例: ChromaDB, Pinecone等)
            retrieved = self._search(search_step.search_query)
            context += f"n{retrieved}"

            # 回答を試みる
            reasoning = self.reason(question=question, context=context)
            if not reasoning.needs_more_info:
                return reasoning

        return self.reason(question=question, context=context)

    def _search(self, query: str) -> str:
        # 実装はRAGセクションで詳述
        return f"[検索結果: {query}]"

# 使用例
mhqa = MultiHopQA(num_hops=3)
result = mhqa(question="DSPyを使った本番RAGシステムの構築方法は?")
print(result.answer)

Optimizers ―自動コンパイルの仕組み

DSPyの最大の特徴がOptimizerです。trainsetとmetricを渡すと、最適なfew-shot例と命令文を探索してプログラムをコンパイルします。

BootstrapFewShot ―最も手軽なOptimizer

import dspy
from dspy.teleprompt import BootstrapFewShot

# 1. プログラム定義
qa = dspy.ChainOfThought("question: str -> answer: str")

# 2. trainset準備(最低5例あればOK、20例推奨)
trainset = [
    dspy.Example(
        question="Pythonのリスト内包表記とは?",
        answer="リストを生成するための簡潔な構文。[式 for 変数 in イテラブル if 条件]の形式で書く。"
    ).with_inputs("question"),
    # ... 他のサンプル
]

# 3. 評価指標定義
def answer_quality_metric(example, prediction, trace=None):
    """回答の品質を0-1で評価するmetric"""
    # 実際の実装ではLLMを使った評価やルールベース評価を組み合わせる
    return 1.0 if len(prediction.answer) > 20 else 0.0

# 4. Optimizer設定と実行
optimizer = BootstrapFewShot(
    metric=answer_quality_metric,
    max_bootstrapped_demos=4,    # 自動生成するfew-shot例の上限
    max_labeled_demos=16,        # trainsetから選ぶfew-shot例の上限
)

compiled_qa = optimizer.compile(qa, trainset=trainset)

# 5. コンパイル済みプログラムを保存・使用
compiled_qa.save("compiled_qa.json")
result = compiled_qa(question="Pythonのデコレータの使い方は?")
print(result.answer)

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

MIPROv2 ―命令文もfew-shotも同時最適化

import dspy
from dspy.teleprompt import MIPROv2

# MIPROv2は命令文とfew-shot例を同時にベイズ最適化する
# 推奨: trainset 50例以上

qa_program = dspy.ChainOfThought("question: str, context: str -> answer: str")

optimizer = MIPROv2(
    metric=answer_quality_metric,
    auto="medium",          # "light"(高速), "medium"(バランス), "heavy"(高精度)
    num_threads=8,          # 並列スレッド数
    max_labeled_demos=5,
    max_bootstrapped_demos=4,
)

# verbose=True で最適化プロセスのログを確認できる
compiled = optimizer.compile(
    qa_program,
    trainset=trainset[:40],
    valset=trainset[40:],   # バリデーションセット
    num_trials=20,          # ベイズ最適化の試行回数
    minibatch=True,         # メモリ効率化
)

compiled.save("compiled_mipro.json")
result = compiled(question="RAGとファインチューニングの使い分けは?", context="...")
print(result.answer)

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

KNNFewShot ―類似例を動的に選択

import dspy
from dspy.teleprompt import KNNFewShot

# 入力クエリに意味的に近いfew-shot例を動的に選択する
# 多様なドメインを持つtrainsetで特に効果的

knn_optimizer = KNNFewShot(k=3, trainset=trainset)
compiled_knn = knn_optimizer.compile(qa, trainset=trainset)

Optimizer選択の実践ガイド:

  • trainset 5〜20例 → BootstrapFewShot(まず試す)
  • trainset 50例以上 → MIPROv2 auto="medium"
  • trainset 200例以上・精度最優先 → MIPROv2 auto="heavy"
  • 入力の多様性が高い → KNNFewShotを組み合わせる

LM接続 ―OpenAI・Anthropic・OSS対応

DSPyはLiteLLM経由で主要LLMプロバイダーに対応しています。記述方法は統一されているため、プロバイダーを切り替えてもコードの変更は最小限です。

import dspy

# OpenAI
lm_gpt4o = dspy.LM("openai/gpt-4o", max_tokens=2000, temperature=0.7)

# Anthropic Claude
lm_claude = dspy.LM("anthropic/claude-opus-4-5", max_tokens=4096)

# Google Gemini
lm_gemini = dspy.LM("google/gemini-2.0-flash")

# Azure OpenAI
lm_azure = dspy.LM(
    "azure/gpt-4o",
    api_base="https://YOUR_RESOURCE.openai.azure.com",
    api_version="2024-08-01-preview",
    api_key=os.environ["AZURE_OPENAI_API_KEY"]
)

# Ollama(ローカルOSS)
lm_llama = dspy.LM("ollama/llama3.2", api_base="http://localhost:11434")
lm_qwen = dspy.LM("ollama/qwen2.5:14b", api_base="http://localhost:11434")

# モジュール単位でLMを切り替える
class HybridQA(dspy.Module):
    def __init__(self):
        super().__init__()
        # 複雑な推論にはClaude、要約にはGPT-4o-miniという使い分けも可能
        self.reasoner = dspy.ChainOfThought("question -> reasoning: str")
        self.summarizer = dspy.Predict("reasoning: str -> summary: str")

    def forward(self, question: str):
        with dspy.context(lm=lm_claude):
            reasoning = self.reasoner(question=question)
        with dspy.context(lm=dspy.LM("openai/gpt-4o-mini")):
            summary = self.summarizer(reasoning=reasoning.reasoning)
        return summary

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

RAG実装パターン ―検索連動の最適化

DSPyはRAG(Retrieval-Augmented Generation)パイプラインの最適化に特に強みを発揮します。検索クエリの生成から回答生成まで、パイプライン全体をend-to-endで最適化できます。

import dspy
import chromadb

# 1. ChromaDBを使ったRetrieverの実装
class ChromaRetriever(dspy.Retrieve):
    def __init__(self, collection_name: str, k: int = 3):
        super().__init__(k=k)
        client = chromadb.Client()
        self.collection = client.get_collection(collection_name)

    def forward(self, query: str, k: int = None) -> dspy.Prediction:
        k = k or self.k
        results = self.collection.query(
            query_texts=[query],
            n_results=k
        )
        passages = [doc for doc in results["documents"][0]]
        return dspy.Prediction(passages=passages)

# 2. RAGシグネチャとモジュール定義
class RAGSignature(dspy.Signature):
    """与えられたコンテキストに基づいて質問に正確に答える。コンテキストにない情報は答えない。"""

    question: str = dspy.InputField()
    context: list[str] = dspy.InputField(desc="検索で取得した関連ドキュメントのリスト")

    answer: str = dspy.OutputField(desc="コンテキストに基づく回答(根拠を明示)")
    used_context_indices: list[int] = dspy.OutputField(
        desc="回答に使用したコンテキストのインデックス(0始まり)"
    )

class RAGPipeline(dspy.Module):
    def __init__(self, retriever: dspy.Retrieve):
        super().__init__()
        self.retriever = retriever
        self.generate = dspy.ChainOfThought(RAGSignature)

    def forward(self, question: str) -> dspy.Prediction:
        # 検索
        retrieved = self.retriever(query=question)
        # 生成
        prediction = self.generate(
            question=question,
            context=retrieved.passages
        )
        return dspy.Prediction(
            answer=prediction.answer,
            context=retrieved.passages,
            used_indices=prediction.used_context_indices
        )

# 3. RAGパイプラインの最適化
from dspy.teleprompt import BootstrapFewShot
from dspy.evaluate import SemanticF1

retriever = ChromaRetriever("my_knowledge_base", k=5)
rag = RAGPipeline(retriever=retriever)

# trainset(質問と正解ペア)
trainset = [
    dspy.Example(question="DSPyの主要な特徴は何ですか?", answer="宣言的プログラミング、自動最適化...").with_inputs("question"),
    # ...
]

optimizer = BootstrapFewShot(
    metric=SemanticF1(),   # 意味的類似度でF1スコアを計算
    max_bootstrapped_demos=3
)

compiled_rag = optimizer.compile(rag, trainset=trainset)
compiled_rag.save("compiled_rag.json")

# 本番での使用
result = compiled_rag(question="DSPyでマルチホップQAを実装するには?")
print(result.answer)

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

DSPyのRAGにおける強みは、検索クエリの書き方と回答生成の指示を同時に最適化できる点です。従来のRAGでは「検索クエリはこう書くべき」「回答はこうフォーマットする」を別々に手動チューニングしていましたが、DSPyは評価指標を基準に両方を自動で調整します。

LangGraph・Pydantic AIとの連携

DSPyは単体でも使えますが、他のフレームワークと組み合わせることで真価を発揮します。役割分担は明確です。

フレームワーク 担当領域 DSPyとの関係
DSPy プロンプト自動最適化・LLM呼び出し抽象化 コア
LangGraph エージェントのオーケストレーション・状態管理 組み合わせ可
Pydantic AI ツール実行・型安全・構造化出力 組み合わせ可
LiteLLM LLMプロバイダーの統一API DSPy内部で使用
import dspy
from langgraph.graph import StateGraph, END
from typing import TypedDict

# DSPyモジュールをLangGraphのノードとして使う
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))

# 最適化済みDSPyモジュール(事前にcompileしたもの)
compiled_qa = dspy.ChainOfThought("question: str, context: str -> answer: str, needs_clarification: bool")

class AgentState(TypedDict):
    question: str
    context: str
    answer: str
    follow_up: bool

# LangGraphのノード関数にDSPyモジュールを組み込む
def answer_node(state: AgentState) -> AgentState:
    result = compiled_qa(
        question=state["question"],
        context=state.get("context", "")
    )
    return {
        **state,
        "answer": result.answer,
        "follow_up": result.needs_clarification
    }

def needs_more_info(state: AgentState) -> str:
    return "clarify" if state["follow_up"] else END

# グラフ構築
graph = StateGraph(AgentState)
graph.add_node("answer", answer_node)
graph.add_node("clarify", lambda s: {**s, "context": "追加情報..." + s["context"]})
graph.add_conditional_edges("answer", needs_more_info)
graph.add_edge("clarify", "answer")
graph.set_entry_point("answer")

app = graph.compile()
result = app.invoke({"question": "DSPyとLangGraphの違いを教えてください"})
print(result["answer"])

# 注意: 本番環境で使用する前に、必ずテスト環境で動作確認してください。

この構成の利点は、LangGraphが「いつDSPyを呼ぶか・どのノードに遷移するか」を管理し、DSPyが「LLMをどう呼ぶか・プロンプトをどう最適化するか」を担当するという、責務の明確な分離です。

【要注意】よくある失敗パターン4選

失敗1: trainsetが少なすぎる状態でMIPROv2を使う

❌ 5例しかないtrainsetでMIPROv2を回す
⭕ 5〜20例ならBootstrapFewShotを使い、50例以上になったらMIPROv2に切り替える

なぜ重要か: MIPROv2はベイズ最適化を使うため、データが少ないと過学習して汎化性能が下がります。trainsetのサイズに合わせたOptimizer選択が精度の鍵です。

失敗2: metricにLLMを使わず単純文字列一致を使う

exact_match = prediction.answer == example.answer
dspy.evaluate.SemanticF1() か、LLMを使った評価metricを設計する

なぜ重要か: 自然言語の回答は言い換えが多様です。文字列完全一致では本当に良い回答を弾いてしまい、最適化の方向性が歪んでしまいます。実際に検証したところ、SemanticF1に変えるだけで最適化後のスコアが15%前後向上しました。

失敗3: コンパイル済みプログラムを保存しない

❌ 毎回起動時にoptimizer.compile()を実行する
compiled.save("program.json") で保存し、dspy.load("program.json") で読み込む

なぜ重要か: MIPROv2の最適化は数十分かかることがあります。コンパイル済みのfew-shot例と命令文をJSONで保存・再利用することで、本番デプロイのコストを大幅に削減できます。

失敗4: Signatureのdocstringを空白のままにする

❌ docstringを書かずにシグネチャだけ定義する
⭕ タスクの目的・制約・出力形式の方針をdocstringに明記する

なぜ重要か: DSPyはdocstringをシステムプロンプトの一部として使用します。「何をすべきか・何をすべきでないか」を明示することで、最適化前の段階から品質の底上げができます。

まとめ:今日から始める3つのアクション

DSPyは「プロンプトを書く」から「プロンプトをコンパイルする」への思想転換を促すフレームワークです。LangGraphやPydantic AIとは競合せず、LLM呼び出しの最適化レイヤーとして組み合わせて使うことで、アプリケーション全体の精度を継続的に改善できます。

  1. 今日やること: pip install dspydspy.ChainOfThoughtでSignatureを1つ定義して動かす
  2. 今週中: 既存のRAGパイプラインのLLM呼び出し部分をDSPyのModuleに置き換え、BootstrapFewShotで最初のコンパイルを試す
  3. 今月中: trainsetを50例まで拡充してMIPROv2を回し、最適化前後でmetricの変化を計測する

あわせて読みたい:

この記事を読んで導入イメージが固まってきた方へ

UravationではAIエージェント導入の研修・コンサルを行っています。DSPyを使ったLLMパイプライン最適化から本番運用まで、実績ある支援体制でサポートします。


参考・出典


著者: 佐藤傑(さとう・すぐる)
株式会社Uravation代表取締役。X(@SuguruKun_ai)フォロワー10万人超。100社以上の企業向けAI研修・導入支援。著書累計3万部突破。SoftBank IT連載7回執筆(NewsPicks最大1,125ピックス)。

ご質問・ご相談は お問い合わせフォーム からお気軽にどうぞ。

関連記事

Need help moving from reading to rollout?

この記事を読んで導入イメージが固まってきた方へ

Uravationでは、AIエージェントの要件整理、PoC設計、社内導入、研修まで一気通貫で支援しています。

この記事をシェア

X Facebook LINE

※ 本記事の情報は2026年5月時点のものです。サービスの料金・仕様は変更される可能性があります。最新情報は各サービスの公式サイトをご確認ください。

関連記事