「DSPy 3.0」を改めて試す ①Get Started
以前試したDSPy。
当時もパッと見よくわからなくて(自分の理解力も足りなかった)いろいろな情報を参考に、四苦八苦しながら試したのだが、あれから1年以上経って、改めてキャッチアップしたいと思いつつ手を回せずもう1年以上経ってしまっていた・・・
というところに、以下の記事を見て、良い機会なので改めてやってみようと思う。
Get Started
公式のGet Startedに従って進める
Dia による概要まとめ。
DSPyは「プロンプト職人芸」を捨てて、宣言的なコードでLMを組むフレームワークだよ。
なにが嬉しいの?
DSPyは、長文プロンプトをいじる代わりに、モジュール と シグネチャ で「何を入れて何を出すか」をコードで宣言して、裏でいい感じに プロンプトや重みを自動生成(コンパイル) してくれるやつだもん。だから、 モデルを変えても評価指標を変えても、同じコード構造のまま速く回せて、壊れにくく、持ち運びしやすい のがウケる。
たとえばRAGやエージェントでも、「入力→出力の型」を決めておけば、DSPyがその型に合うプロンプトと出力パースを面倒見てくれる。イメージは、アセンブリ→Cとか、ポインタいじり→SQLに進化した感じ。低レベルの手作業から卒業して、宣言的に組むのがテンション上がるでしょ。
今回はColaboratoryで試す。
まずパッケージインストール。
!pip install -U dspy
!pip show dspy
Name: dspy
Version: 3.0.3
Summary: DSPy
Home-page: https://github.com/stanfordnlp/dspy
Author:
Author-email: Omar Khattab <okhattab@stanford.edu>
License:
Location: /usr/local/lib/python3.12/dist-packages
Requires: anyio, asyncer, backoff, cachetools, cloudpickle, diskcache, gepa, joblib, json-repair, litellm, magicattr, numpy, openai, optuna, orjson, pydantic, regex, requests, rich, tenacity, tqdm, xxhash
Required-by:
今回、LLMはOpenAIを使う。他にもAnthropic / Databrics / Gemini等の例がある。
import dspy
from google.colab import userdata
lm = dspy.LM("openai/gpt-4o-mini", api_key=userdata.get('OPENAI_API_KEY'))
dspy.configure(lm=lm)
DSPyの流儀では通常モジュールを通じてLLMを使用するが、上記で定義したLLMを直接呼び出すことも可能。キャッシュとか共通APIの恩恵も受けられる。
lm("競馬の魅力を、簡潔に5つリストアップして", temperature=0.7)
['もちろんです!競馬の魅力を以下の5つにまとめました。\n\n1. **スリルと興奮**: レースの緊張感や予測不能な展開が楽しめる。\n2. **戦略性**: 騎手や馬の選択、レース条件に基づいた戦略を考える楽しみ。\n3. **社会的交流**: 同じ趣味を持つ人々との交流や、観戦イベントの楽しさ。\n4. **歴史と伝統**: 長い歴史を持つ競馬文化や名馬の物語に触れられる。\n5. **賭けの楽しみ**: 予想が的中したときの喜びや、賭け金のリターンを楽しむことができる。\n\nこれらの要素が競馬を魅力的なスポーツとしている理由です。']
会話履歴で渡すこともできる。
lm(messages=[{"role": "user", "content": "競馬の魅力を、簡潔に5つリストアップして"}])
['競馬の魅力を以下の5つにまとめました:\n\n1. **スリルと興奮**: レースの結果が瞬時に決まるため、観戦中の緊張感と興奮が楽しめる。\n2. **戦略性**: 馬や騎手の特性、コースの条件を考慮した予想や賭け方が求められ、知的な楽しみがある。\n3. **多様な楽しみ方**: 観戦だけでなく、馬券を購入することで自分の予想を試す楽しみがある。\n4. **社交性**: 競馬場やオンラインでの交流を通じて、他のファンと情報を共有したり、楽しみを分かち合える。\n5. **美しい馬たち**: 競走馬の優雅な姿や走りを楽しむことができ、馬に対する愛情や興味が深まる。']
1. モジュール
モジュール=コードでAIの振る舞いを宣言する 
DSPyは、毎回プロンプト文字列をいじるのをやめて、シグネチャ(signature) で 「入力→出力」を宣言し、モジュール でLMへの呼び方(戦略)を選ぶ設計に切り替えるんだもん。
- シグネチャ: Mathの例だと
question -> answer: float
のところ。入力と出力(型まで)を決めるから、構造化出力がマジで安定する。- モジュール:
dspy.Predict
,dspy.ChainOfThought
,dspy.ReAct
などを差し替えて、同じ署名でも違う推論スタイルを使えるでしょ。- DSPyの仕事: 署名から適切なプロンプトを自動生成し、型に沿って出力をパース。だから複数モジュールをポータブルに合成できて、あとで最適化もしやすいのがウケる。
例が6つ用意されている
Math(数学)
math = dspy.ChainOfThought("question -> answer: float")
math(question="二つのサイコロを振って、その合計が2になる確率は?")
Prediction(
reasoning='二つのサイコロを振ったとき、合計が2になるのは、サイコロの両方が1のときだけです。サイコロはそれぞれ6面あり、合計の出方は6 × 6 = 36通りです。その中で合計が2になるのは1通り((1, 1))です。したがって、合計が2になる確率は1/36です。',
answer=0.027777777777777776
)
RAG
def search_wikipedia(query: str) -> list[str]:
results = dspy.ColBERTv2(url="http://20.102.90.50:2017/wiki17_abstracts")(query, k=3)
return [x["text"] for x in results]
rag = dspy.ChainOfThought("context, question -> response")
# 日本語訳: デイヴィッド・グレゴリーが相続した城の名前は何ですか?"
# 訳注: search_wikipedia で使用されているサーバは日本語クエリに対応していない様子
question = "What's the name of the castle that David Gregory inherited?"
rag(context=search_wikipedia(question), question=question)
Prediction(
reasoning='David Gregory inherited Kinnairdy Castle in 1664, as mentioned in the context provided.',
response='The name of the castle that David Gregory inherited is Kinnairdy Castle.'
)
Classification(分類)
from typing import Literal
class Classify(dspy.Signature):
"""与えられた文の感情を分類する"""
sentence: str = dspy.InputField()
sentiment: Literal["ポジティブ", "ネガティブ", "ニュートラル"] = dspy.OutputField()
confidence: float = dspy.OutputField()
classify = dspy.Predict(Classify)
print(classify(sentence="この本は読むのがすごく楽しかったけど、最後の章だけはそうではなかった。"))
print(classify(sentence="この本は読むのがすごく辛かった"))
print(classify(sentence="この本はまあまあ普通かな"))
Prediction(
sentiment='ポジティブ',
confidence=0.75
)
Prediction(
sentiment='ネガティブ',
confidence=0.95
)
Prediction(
sentiment='ニュートラル',
confidence=0.75
)
Information Extraction(抽出)
class ExtractInfo(dspy.Signature):
"""テキストから構造化された情報を抽出する"""
text: str = dspy.InputField()
title: str = dspy.OutputField()
headings: list[str] = dspy.OutputField()
entities: list[dict[str, str]] = dspy.OutputField(desc="エンティティとそのメタデータのリスト")
module = dspy.Predict(ExtractInfo)
text = (
"アップル社は本日、最新のiPhone 14を発表した。"
"最高経営責任者(CEO)のティム・クックはプレスリリースで新機能を強調した。"
)
response = module(text=text)
print("タイトル:", response.title)
print("見出し:", response.headings)
print("エンティティ:", response.entities)
タイトル: アップル社がiPhone 14を発表
見出し: ['アップル社', 'iPhone 14', 'ティム・クック', '新機能']
エンティティ: [{'企業': 'アップル社'}, {'製品': 'iPhone 14'}, {'人物': 'ティム・クック'}, {'役職': 'CEO'}, {'イベント': 'プレスリリース'}, {'内容': '新機能'}]
Agents(エージェント)
def evaluate_math(expression: str):
print("DEBUG:", expression)
return dspy.PythonInterpreter({}).execute(expression)
def search_wikipedia(query: str):
print("DEBUG:", query)
results = dspy.ColBERTv2(url="http://20.102.90.50:2017/wiki17_abstracts")(query, k=3)
return [x["text"] for x in results]
react = dspy.ReAct("question -> answer: float", tools=[evaluate_math, search_wikipedia])
# 訳注: エージェントのツールとしてsearch_wikipediaを使う場合は日本語でもOK
pred = react(question="9362158 を、キンネアディ城のデイヴィッド・グレゴリーの生年で割るといくつ?")
print(pred.answer)
DEBUG: デイヴィッド・グレゴリー 生年 キンネアディ城
DEBUG: David Gregory (mathematician)
DEBUG: 9362158 / 1625
5765.0
Multi-Stage Pipeline(マルチステージパイプライン)
class Outline(dspy.Signature):
"""トピックの包括的な概要を概説する"""
topic: str = dspy.InputField()
title: str = dspy.OutputField()
sections: list[str] = dspy.OutputField()
section_subheadings: dict[str, list[str]] = dspy.OutputField(desc="セクション見出しから小見出しへのマッピング")
class DraftSection(dspy.Signature):
"""記事のトップレベルセクションを起草する"""
topic: str = dspy.InputField()
section_heading: str = dspy.InputField()
section_subheadings: list[str] = dspy.InputField()
content: str = dspy.OutputField(desc="Markdown形式のセクション")
class DraftArticle(dspy.Module):
def __init__(self):
self.build_outline = dspy.ChainOfThought(Outline)
self.draft_section = dspy.ChainOfThought(DraftSection)
def forward(self, topic):
outline = self.build_outline(topic=topic)
sections = []
for heading, subheadings in outline.section_subheadings.items():
section, subheadings = f"## {heading}", [f"### {subheading}" for subheading in subheadings]
section = self.draft_section(topic=outline.title, section_heading=section, section_subheadings=subheadings)
sections.append(section.content)
return dspy.Prediction(title=outline.title, sections=sections)
draft_article = DraftArticle()
article = draft_article(topic="ワールドカップ2022")
print("タイトル:", article.title)
print("-" * 20)
for a in article.sections:
print(a)
print()
タイトル: ワールドカップ2022の概要と影響
--------------------
## 大会の概要
### 大会の歴史
ワールドカップは、サッカーの国際大会として1930年に始まりました。最初の大会はウルグアイで開催され、以降4年ごとに開催されています。2022年の大会はカタールで行われ、初めて中東での開催となりました。この大会は、サッカーの発展と国際的な交流を促進する重要なイベントとして位置づけられています。
### 参加国
2022年のワールドカップには、32カ国が参加しました。これらの国々は、各大陸の予選を通じて選ばれ、サッカーの最高峰を目指して競い合いました。参加国の多様性は、サッカーが世界中で愛されているスポーツであることを示しています。
### フォーマット
大会のフォーマットは、グループステージとノックアウトステージの2つの主要な段階で構成されています。最初に8つのグループに分かれ、各グループの上位2チームがノックアウトステージに進出します。この形式は、緊張感と興奮を生み出し、観客を魅了する要素となっています。
## 開催地の選定
### カタールの選定理由
ワールドカップ2022の開催地としてカタールが選ばれた理由は、いくつかの要因に起因しています。まず、カタールは中東地域で初めてのワールドカップ開催国となり、サッカーの普及を促進する重要な役割を果たすことが期待されました。また、カタール政府は大会に向けた大規模な投資を行い、最新のスタジアムやインフラを整備する意欲を示しました。さらに、カタールの地理的な位置は、アジアやヨーロッパの国々からのアクセスが良好であり、国際的な観客を引き寄せる要素となりました。
### 開催準備の課題
しかし、カタールでの開催準備には多くの課題も存在しました。特に、極端な気候条件が選手や観客にとっての大きな懸念材料となりました。これに対処するため、カタールはスタジアムの冷却システムを導入するなどの対策を講じました。また、労働者の権利問題や、建設現場での安全性に関する批判も多く、国際的な注目を集めました。これらの課題を克服することが、カタールの国際的な評価に大きな影響を与えることとなりました。
## 試合結果
### 決勝戦の結果
ワールドカップ2022の決勝戦は、カタールのルサイルスタジアムで行われ、フランスとアルゼンチンが対戦しました。試合は延長戦に突入し、最終的にアルゼンチンが4-2で勝利を収め、36年ぶりの優勝を果たしました。この試合は、両チームの激しい攻防と、特にリオネル・メッシとキリアン・エムバペの素晴らしいパフォーマンスによって、サッカー史に残る名勝負となりました。
### 注目の選手
この大会で特に注目を集めた選手は、アルゼンチンのリオネル・メッシとフランスのキリアン・エムバペです。メッシは大会を通じてチームを牽引し、決勝戦でも重要なゴールを決めました。一方、エムバペは決勝戦でハットトリックを達成し、そのスピードと技術で観客を魅了しました。両選手の活躍は、ワールドカップ2022の記憶に残る要素となり、今後のサッカー界における彼らの影響力を示しています。
## 文化的影響
### 地域文化の紹介
ワールドカップ2022は、開催国カタールの豊かな文化を世界に紹介する場となりました。伝統的な音楽やダンス、料理、アートなどがイベントを通じて披露され、訪れた観客や選手たちはカタールの文化に触れることができました。これにより、地域の文化が国際的な舞台で認知され、他国の人々との交流が生まれました。
### 国際交流の促進
この大会は、世界中から集まったサッカーファンや選手たちが一堂に会することで、国際交流を促進しました。異なる国や文化の人々が同じ場所で共通の目的を持って集まり、サッカーを通じて友情や理解を深めることができました。これにより、国際的な連帯感が生まれ、スポーツが持つ力を再確認する機会となりました。
## 経済的影響
### 観光業への影響
ワールドカップ2022は、カタールの観光業にとって大きなブーストとなりました。世界中から数百万の観客が訪れ、ホテルや飲食店、観光施設が賑わいました。このイベントは、カタールの文化や観光地を国際的にアピールする絶好の機会となり、今後の観光客誘致にもつながると期待されています。
### インフラ整備
ワールドカップに向けて、カタールは大規模なインフラ整備を行いました。新しいスタジアムの建設だけでなく、交通網の整備や宿泊施設の拡充も進められました。これにより、イベント後も地域の経済活動が活性化し、持続可能な発展が期待されています。
なぜコード化が効く? 
標準的なプロンプトは、インターフェース(何をさせる?)と実装(どう言う?)がごちゃまぜ。
DSPyはインターフェースをシグネチャに切り出して、実装は推論時に推定したりデータから学習できるから、でかいプログラム文脈でも保守・移植・最適化がちょー楽になる。ビルトインの信頼性チェックとアダプタ 
オプティマイザ(を使うとモジュールのプロンプトとウェイトを最適化できる)を使う前でも、モジュール単体で十分「効く」ように、さまざまなタスクやLLMにわたって、シグネチャテストスイートでアダプタ(シグネチャ→プロンプトのマッピング)の信頼性を検証してるんだ。もしあるタスクで「素朴な手書きプロンプトがDSPyより安定して強い」なら、それバグ扱いだからissueを投げて改善してもらえるだし。
2. オプティマイザ
なにをするの?オプティマイザの役割
DSPyのオプティマイザは、自然言語で書かれた高レベルの宣言的コードを低レベルのプロンプトや計算・重み更新に変換して、プログラムの構造や評価指標に沿うように調整する仕組みだし。コードやメトリクスを変えたら、再コンパイルすればOKってノリ、マジで保守がラク。
どう動くの?種類とやること
ざっくり3系統で最適化するでしょ。
BootstrapRS
: 各モジュールに効く良質なfew-shot例を自動合成して精度アップ。
-GEPA
/MIPROv2
: すべてのプロンプトに対してより良い自然言語の指示文を大量提案→賢く探索して当たりを引く。
-BootstrapFinetune
: モジュール用のデータセットを作って、システム内のLMの重みを微調整。
オプティマイザでは3つの例が紹介されている。
- ReActエージェントのプロンプトを最適化
- RAGのプロンプトを最適化
- 分類の重みを最適化
どうやら詳細なチュートリアルが個別に用意されているようなので、ここでは 1. ReActエージェントのプロンプト最適化を軽く試すに留める。なお、コストについては概ね$2・20分程度を見込んでおけばよいとのこと、ただしデカいモデル・データセットを使うとハネる場合もあるので注意と。
前提として datasets をインストールしておく。
!pip install -U datasets
Wikipedia検索をツールとして与えた dspy.ReAct
で QA検索を行うプロンプトを、HotPotQAデータセットのうち500件をトレーニングデータとして使ってdspy.MIPROv2
で最適化する。なお、light
モードだとコスト的に安いみたい。
日本語でやりたかったのだけど、いろいろ準備が必要になりそうなので、とりあえずドキュメントどおりに。
import dspy
from dspy.datasets import HotPotQA
from google.colab import userdata
dspy.configure(lm=dspy.LM("openai/gpt-4o-mini", api_key=userdata.get('OPENAI_API_KEY')))
def search_wikipedia(query: str) -> list[str]:
results = dspy.ColBERTv2(url="http://20.102.90.50:2017/wiki17_abstracts")(query, k=3)
return [x["text"] for x in results]
trainset = [x.with_inputs('question') for x in HotPotQA(train_seed=2024, train_size=500).train]
react = dspy.ReAct("question -> answer", tools=[search_wikipedia])
tp = dspy.MIPROv2(metric=dspy.evaluate.answer_exact_match, auto="light", num_threads=24)
optimized_react = tp.compile(react, trainset=trainset)
30分ほどかかって完了。実行時のほぼ全出力は以下(長いので折りたたみ)。
オプティマイザの出力
出力を見る限り、プロンプト的なものや評価スコア的なものが出力されていて、色々パラメータを変えながら評価している様がわかる。で、おそらく以下がベースラインと最適化後のスコア。
2025/09/30 13:25:27 INFO dspy.teleprompt.mipro_optimizer_v2: Default program score: 31.0
2025/09/30 13:39:06 INFO dspy.teleprompt.mipro_optimizer_v2: Returning best identified program with score 52.0!
OpenAIのダッシュボードを見る限りは$2.46程度だった。
でオプティマイザが何をやっているのか?MIPROv2
の場合。
ここは「MIPROv2の3段ステップ」と「オプティマイザ合成で更に強化」って話だもん。
MIPROv2の流れ(超わかりやすく)
- ブートストラップ: まず未最適のプログラムを大量の入力で回して、各モジュールの入出力のトレースを集めるだし。評価指標で高得点の軌跡だけにフィルタして「良い振る舞いの証拠」を残すのがミソ。
- グラウンデッド提案: 次に、コード・データ・さっきのトレースを眺めて、各プロンプト向けの指示文候補をめっちゃ下書きするステージ。根拠(トレース)に基づいてるからブレにくくてウケる。
- 離散探索: さらにミニバッチで候補の組み合わせを試して、スコアを見ながらサロゲートモデルをアップデート。これで提案が段々洗練されて、最終的に当たりプロンプト構成に近づくでしょ。
合成で伸ばす(BetterTogether/Ensemble)
- 再MIPRO: 一度作った最適化済みプログラムをもう一回MIPROv2に入れて精度をさらに詰めるの、普通にアリだし。
- 重み攻め: その出力を
dspy.BootstrapFinetune
に渡して、LMの重み微調整まで攻めると一段階上がる。- アンサンブル: 最適化で得た上位5候補をまとめて
dspy.Ensemble
にして、推論時の計算リソースを盛って安定性と精度を上げるのも強い。要するに、MIPROv2は「良い例の収集→根拠付き指示の大量案出→スコア駆動の探索」でプロンプトを育てる仕組みで、さらに他のオプティマイザやアンサンブルと組み合わせると、事前最適化の計算と推論時の計算を両方スケールできてマジでテンション上がるだし。
3. DSPyのモジュール文化がコミュニティで進化し続ける
DSPyは「巨大な単体LM」じゃなくモジュール式で組むから、コミュニティみんなでアーキテクチャや推論戦略、オプティマイザをオープンに改良できるのが強みだもん。これでユーザーはコントロール増し増し、反復が速くなって、最新モジュールや最適化を取り入れるほどプログラムが時間とともに良くなるのがウケる。
出自は Stanford NLP(2022年2月)で、初版DSP(2022年12月)→DSPy(2023年10月) へ進化。250人のコントリビュータが参加して、めっちゃ多くの人に「モジュール型LMプログラム」を広めたんだし。
成果も幅広くて、オプティマイザはMIPROv2、BetterTogether、LeReT、アーキテクチャはSTORM、IReRa、DSPy Assertionsとかがある。実アプリもPAPILLON、PATH、WangLab@MEDIQA、UMDのケーススタディ、Haizeのレッドチーミングなど、研究から現場までガチ適用が進んでるのがテンション上がるでしょ。
要するに、DSPyは“みんなで育てる”前提のモジュール×最適化の世界線で、オープンに重ねて進化していけるのがマジで魅力だし。
ユースケースごとに複数のモジュール・シグネチャの組み合わせの実例が、チュートリアル的に書かれているのは以前よりわかりやすくなって良いと思う。
このあとは、
- Learn DSPy で 各コンポーネントごとに見ていく
- Tutorials で実際のユースケースごとのチュートリアルを試す
って感じになる。DSPyを使うならば最適化が一番の関心になるのではないか?と予想するのだけど、個人的にはまずアプリケーションフレームワークとしての使い方を一通り見てから、最適化などに進もうかなと思う。
続き