はじめに:プロンプト職人は廃業する運命なのか
前回の記事
『プロンプトで祈るのはもうやめる。Outlines / Guidance で LLM の出力を 100% 制御する技術』
では、LLMの 出力(Output) を論理的に制御する方法を解説しました。
多くの反響をいただき、現場のエンジニアがいかに
LLMの不安定な挙動に悩まされているか を再確認しました。
出力は制御できた。では入力は?
出力は制御できるようになりました。
しかし、入力(Input)=プロンプト はどうでしょうか?
- 「もう少し丁寧に考えて」
- 「ステップ・バイ・ステップで」
- 「あなたは優秀なアシスタントです」
相変わらず、私たちは
文字列の魔術(プロンプトエンジニアリング) に時間を費やしています。
- モデルのバージョンが変わるたびに微調整
- Few-shot の例を手作業で入れ替え
- なぜ効いたのか分からないまま運用
これは本当に エンジニアリング と呼べるのでしょうか?
本記事では、スタンフォード大学発のライブラリ DSPy を紹介します。
DSPy は、
プロンプトを「手書き」するのをやめ、
評価指標に基づいて コンパイル(自動最適化) する
という、LLM開発の前提をひっくり返す技術です。
DSPy とは:LLMアプリを「PyTorch」のように書く
DSPy(Declarative Self-improving Language Programs)の核心は、
次の思想にあります。
プロンプトは文字列ではなく、最適化可能なパラメータである
従来の開発
- 長大なプロンプト文字列を f-string で結合
- 試行錯誤と勘に頼った調整
- モデルが変わると全部やり直し
DSPy の開発
- 入出力の型(Signature)を定義
- ロジック(Module)を書く
- データと評価関数を与えて Compile
これはまさに、
PyTorch でニューラルネットを定義して学習させる感覚
で、LLMアプリを書くという体験です。
仕組み:なぜ「コンパイル」で賢くなるのか?
DSPy の中核にあるのが
Teleprompter(Optimizer) という概念です。
構成要素
-
Signature
入出力の定義(例:質問 → 回答) -
Dataset
少量の教師データ(質問と正解) -
Metric
良し悪しを判定する評価関数 -
Compile
Optimizer が LLM を何度も叩いて最適化
Compile 中に行われていること
Optimizer は、以下を 自動探索 します。
- タスクに最適な Instruction(指示文)
- 問いに最も効果的な Few-shot 例示(Demo)
- 有効な Chain of Thought(思考ステップ)
人間が
「どの例を見せれば賢くなるかな……」
と悩んでいた部分を、
アルゴリズムが総当たりで最適化 してくれるわけです。
実践:DSPy で RAG の精度を上げる
今回は、単純な QA タスク(RAG 想定)で
DSPy がどのようにプロンプトを自動生成するかを見ていきます。
1. 環境構築
pip install dspy-ai
2. モジュール定義
ここでは dspy.ChainOfThought を使います。
これだけで「推論してから回答せよ」というロジックになります。
プロンプトを書く必要はありません。
import dspy
# LLMの設定(OpenAI / Local)
# turbo = dspy.OpenAI(model='gpt-3.5-turbo')
# dspy.settings.configure(lm=turbo)
# ローカルLLMの場合は HFClientVLLM などを使用
# 1. Signature(入出力定義)
class GenerateAnswer(dspy.Signature):
"""質問に対して、文脈を踏まえて論理的に回答する"""
context = dspy.InputField(desc="回答の根拠となる情報")
question = dspy.InputField(desc="ユーザーからの質問")
answer = dspy.OutputField(desc="短く簡潔な回答")
# 2. Module(処理ロジック)
class RAG(dspy.Module):
def __init__(self):
super().__init__()
self.generate_answer = dspy.ChainOfThought(GenerateAnswer)
def forward(self, context, question):
return self.generate_answer(context=context, question=question)
3. コンパイル(最適化)の実行
ここが DSPy のハイライト です。
今回は BootstrapFewShot を使います。
これは、
成功した推論プロセスだけを Few-shot として残す
最適化手法です。
from dspy.teleprompt import BootstrapFewShot
# 教師データ
trainset = [
dspy.Example(
context="Pythonは1991年にグイド・ヴァン・ロッサムによって開発された。",
question="Pythonの開発者は誰?",
answer="グイド・ヴァン・ロッサム"
).with_inputs('context', 'question'),
# ... 数件追加 ...
]
# 評価関数(簡易的に完全一致)
def validate_answer(example, pred, trace=None):
return example.answer == pred.answer
# コンパイル!
teleprompter = BootstrapFewShot(
metric=validate_answer,
max_labeled_demos=4
)
compiled_rag = teleprompter.compile(
RAG(),
trainset=trainset
)
検証結果:何が変わったのか?
pred = compiled_rag(context="...", question="...")
dspy.inspect_history(n=1)
Before(最適化前)
- 単純な
質問: ... 文脈: ... → 回答:
という素っ気ないプロンプト
After(最適化後)
- DSPy が自動選定した 成功した推論パターン が
Few-shot として複数挿入 - Instruction 部分も自動で微調整
DSPy は、こちらが一切指示していないにも関わらず、
「このタイプの質問は、この思考ステップを踏めば正解できる」
というパターンを発見し、
それをプロンプトにハードコードしてくれました。
精度比較(例)
手書きプロンプト(Zero-shot) : 正答率 60%
DSPy(BootstrapFewShot) : 正答率 85%
※ タスク・データによって変動します
まとめ:LLM開発のパラダイムシフト
プロンプトエンジニアリングは重要ですが、
人間が手作業でやるには限界 があります。
DSPy がもたらす変化:
-
モジュール化
ロジックとプロンプトの分離 -
移植性
モデル変更時は compile し直すだけ -
性能向上
人間が思いつかない Few-shot を発見
最強の組み合わせ
- 入力:DSPy で最適化
- 出力:Outlines で制御
これで私たちは、
- お祈りから解放され
- 作文からも解放され
- 真の意味での Software Engineering 2.0
に集中できるはずです。