DSPyとInstructorによるLLM構造化出力の検証記事

eyecatch AI関連

「JSONで返してって言ったのに、どこがJSONなんだこれ…」
LLM を触っていて、そんなツッコミを心の中で連発したこと、ありませんか?

  • 余計な自然文がくっついてくる
  • カンマが一個抜けてて json.loads が落ちる
  • key 名がなぜか微妙に違う(titletitel になってるやつ)

正直、この「構造化出力地獄」がイヤで、LLM の実システム適用をためらっているエンジニアはかなり多いはずです。

そんな中で出てきたのが、「DSPy と Instructor を組み合わせて LLM の構造化出力をガチで安定させよう」という検証記事。
これ、ただの「新しい便利ライブラリ紹介」ではなくて、LLMアプリの設計パラダイムを1段シフトさせる話になりつつあると感じています。


一言で言うと:「生SQL時代の終わり」を思い出す

一言で言うと:「生SQL時代の終わり」を思い出す

この記事でやっていることを乱暴に一言で言うと、

「LLM界の ORM を、DSPy + Instructor でやろうとしている」

に近いです。

  • これまで:
  • 「このフォーマットで JSON 出してね」と生のプロンプト(= 生 SQL)を書き、
  • 返ってきたテキストを自前でパースしてバリデーション(= 結果セットを手パース)していた。
  • これから:
  • DSPy で「どんな入出力型なのか」を宣言的に定義し、
  • Instructor に「その型に沿う JSON を強制的に出させる & Pydantic でバリデーション」させる。

生 SQL から ORM に移行したとき、アプリ開発の主戦場が「SQL職人芸」から「スキーマ設計・ドメインモデリング」に移りましたよね。
あれとまったく同じことが、LLM の世界でも起こりつつあると感じます。


何が本当に新しいのか?単なる Instructor の紹介ではないポイント

Instructor 自体はすでに知られていて、

result: Item = client.chat.completions.create(
    model="gpt-4o-mini",
    response_model=Item,  # ← Pydantic
    messages=[...],
)

と書くだけで、Pydantic モデルに沿った構造化出力をしてくれる便利ツールです。
正直、「壊れた JSON 問題」は Instructor 単体でもかなり解決できる

この記事の面白いところは、そこで終わらずに DSPy と組み合わせている点です。

  • DSPy:
  • Signature で入出力を宣言
  • Module でタスクを構成
  • Optimizer で few-shot / プロンプトを自動最適化
  • Instructor:
  • response_model「出てくる JSON の型」を強制

つまり、

DSPy = 推論フローと入出力の「設計・最適化」
Instructor = LLM 出力の「構造と型の保証」

という役割分担で、プロンプト職人芸を極力減らしつつ、構造化出力の安全性を底上げしているわけです。

ぶっちゃけ、ここまでちゃんと「運用を見据えた構造化パイプライン」をコードで示している日本語記事は、まだほとんどありません。


なぜ「わざわざ DSPy × Instructor」なのか?

プロンプトエンジニアリングから「型設計」へのシフト

従来のやり方だと:

  • 「必ず JSON で出して」
  • 「キーは nameprice だけにして」
  • 「前後に余計な説明文をつけないで」

みたいな注意書きだらけのプロンプトを書いて、そのうち誰も触りたくなくなるパターンが多かったと思います。

DSPy × Instructor だと発想が変わります。

  • DSPy
  • class ProductSignature(dspy.Signature):
    • 入力: description: str
    • 出力: name: str, price: float
  • とまず「型」として定義
  • Instructor
  • class Product(BaseModel): name: str; price: float
  • response_model=Product として LLM を叩く

こうなると、開発者が考えるべきなのは、

「どう書けば JSON を壊さないか」ではなく
「どんな型・構造を設計するのがドメイン的に正しいか」

という方向にガラッと変わります。

正直、ここにピンと来ないなら「まだ小さいスクリプトの段階」なんだと思います。
でも、RAG + 複数ステップ + 複数モデルでエージェントっぽいことをやり始めると、型が揺れた瞬間に全部が崩壊します。
その世界線では、このパラダイムシフトはかなり大きいです。

Optimizer × 型バリデーションは、かなり「いやらしい」組み合わせ

DSPy の特徴の一つが、BootstrapFewShot などの Optimizer で「良さげな few-shot / プロンプト」を自動生成・改善できることです。

ここに Instructor の Pydantic バリデーションが加わると、

  • Optimizer:
  • 「このタスクを正しくこなせるプロンプト」を探索
  • Instructor:
  • 「LLM 出力が型を満たしていないもの」を即座に不正とみなし、再試行 or エラー

という形で、「型に沿っていない出力 = 悪いサンプル」としてふるい落とし続けられるわけです。

このループをちゃんと設計できると、

学習データも人間のラベルもなしに、
「動かしながら少しずつ賢くなる構造化 LLM パイプライン」

の現実味が出てきます。
LayerX の記事でも、DSPy を使って「使えば使うほど賢くなる AI 機能」を模索していましたが、その土台としても Instructor との相性はかなり良さそうです。


LangChain アレルギー時代に、なぜこの組み合わせが刺さるのか

コミュニティの雰囲気として、

LANGCHAINを絶対に使わない!!!

という、もはやスローガンに近い感情も出てきていますよね 🤔
背景は単純で、

  • 依存が多い
  • API がデカくて覚えることが多い
  • バージョンごとに挙動が変わりがち
  • 結局「Chain 組んでプロンプト書いてパーサ書く」世界から抜け出せてない

といった「フレームワーク疲れ」です。

その文脈で見ると、今回の DSPy × Instructor という組み合わせは、かなり「いまの空気」にフィットしていると感じます。

LangChain と比べたときの「効いている」ポイント

  • LangChain:
  • 構造化出力は OutputParserPydanticOutputParser でできる
  • ただし、エラー時のリトライやバリデーション戦略は結局自分で書きがち
  • 全体像が「Chain の森」になりやすい
  • DSPy + Instructor:
  • DSPy: SignatureModule による 型付きワークフロー
  • Instructor: response_model 一発で JSON 生成 + バリデーション + リトライ まで面倒を見る
  • API 面がかなりシンプルで、「覚えること」が少ない

「単に構造化出力を安定させたい」のであれば、LangChain をかませる必然性はどんどん薄れていると感じます。
アウトライン系(outlines-dev)や guidance-ai を勧める声が多いのも、結局は「重いフレームワークをかませたくない」という感覚の現れでしょう。


とはいえバラ色ではない:ちゃんと痛い「Gotcha」たち

とはいえバラ色ではない:ちゃんと痛い「Gotcha」たち

こういう話をするとよくあるのが、

「じゃあ全部 DSPy × Instructor にしよう!」

という極端な振り子です。
正直、それはそれで危険です。いくつか本気で懸念しているポイントがあります。

コストは確実に「盛る」方向に振れる

Instructor は「構造が崩れたら内部で再試行」というスタイルです。
つまり、

  • 論理的には 1 回の推論
  • 実際には 2〜3 回 LLM を叩いている

というケースが普通に起こります。

特に、

  • ネストが深い Pydantic モデル
  • フィールド数が多い巨大オブジェクト

なんかを一発で生成させようとすると、一気にエラー率が上がり、リトライ回数も増えがちです。

「自前で JSON 直してた頃より、月のトークン課金が上がったんだけど…」

という現象は普通にありえます。
なので、個人的には:

  • 最初から巨大スキーマを一発で返させない
  • ステップを分解して「小さいスキーマ」を複数回叩く
  • それらを DSPy Module で組む

くらいの設計にしておいた方が、コストと安定性のバランスは取りやすいと思っています。

学習コストは「二重」に乗ってくる

これもぶっちゃけ大きい。

  • DSPy:
  • Signature / Module / Optimizer の概念
  • RAG などと組み合わせたときの設計パターン
  • Instructor:
  • Pydantic でのスキーマ設計
  • 失敗時挙動(例外?再試行?)の理解

両方を理解して初めて「ちゃんとした設計」ができるので、
「とりあえず OpenAI を直で叩いているだけ」のフェーズにはオーバーキルです。

最初からこれで入ると、デバッグ時に

  • LLM の振る舞いが悪いのか
  • Pydantic の型が厳しすぎるのか
  • DSPy Optimizer が変な few-shot を作っているのか

が切り分けにくくて、地味にハマります。

DSL ロックイン問題はそれなりに重い

DSPy も Instructor も軽量なライブラリではありますが、
アーキテクチャとしてガッツリ組み込むとロックインは避けられません。

  • DSPy の Signature ベースの設計は、素の FastAPI + SDK 構成にはそのまま移植しにくい
  • Instructor 前提で書いたコードを「ベンダー独自 SDK + 自前バリデーション」に戻すのもそれなりに手間

なので、長期運用を本気で考えるなら、

「もし DSPy / Instructor を外したくなったとき、どのレイヤーまで書き換える必要があるか?」

を最初から意識して、境界レイヤー(Adapter)をちゃんと切る設計をした方がいいです。
ここは ORM とまったく同じ罠です。


プロダクションで使うか?正直、段階導入が現実解だと思う

じゃあ、自分がプロダクト開発をしていたとして、

「DSPy × Instructor を本番で使うか?」

という問いにどう答えるか。

正直にいうと、

「いきなりコアワークフロー全部は任せない。でも、限定的なタスクでガンガン試す価値はある」

というスタンスです。

こういう順番なら現実的かな、という提案

  1. PoC / プロトタイピング段階
  2. guidance-ai や単純な Instructor 単体で、
    • 「まずは JSON をちゃんと出せるか」
    • 「その JSON でビジネス的に何ができるか」
  3. を軽く検証する

  4. スキーマが固まり始めたタイミング

  5. Pydantic モデルをちゃんと定義し直し、
  6. Instructor に切り替えて構造保証を強める

  7. タスクが複数ステップ化してきたら

  8. DSPy の Signature / Module でワークフローを明示化
  9. 必要に応じて Optimizer をかませて few-shot / プロンプトを自動改善

  10. 本番導入時

  11. 「DSPy / Instructor をまたぐ部分」を一枚ラップしておく
  12. ログやトレースを残して、どこで壊れたかが後から追えるようにする

このくらい「小刻みな導入」にしておけば、

  • コスト爆増
  • デバッグ不能
  • ロックイン地獄

の三点セットをだいぶ避けやすくなります。


最後に:プロンプト職人から「LLM アーキテクト」へ

最後に:プロンプト職人から「LLM アーキテクト」へ

今回の DSPy × Instructor の検証記事は、
単に「便利ツールを2つ組み合わせてみました」という話ではなくて、

「プロンプトを書く人」から「推論と型を設計する人」への役割転換

を実務レベルで示している、という点に価値があると感じています。

  • 生テキストと正規表現で戦っていた時代から、ORM とスキーマ駆動設計に移った
  • 生の LLM 出力と JSON パースで戦っている今から、DSPy × Instructor 的な「型駆動 LLM 開発」に移ろうとしている

この流れ自体は、多分もう止まりません。

ただし、ORM 乱立で痛い目を見たように、
LLM フレームワークも同じ轍を踏みかねない雰囲気があります。
LangChain アレルギーも、その副作用のひとつです。

だからこそ、DSPy や Instructor も、

  • いきなり全面採用しない
  • 役割を絞って導入し、効果とコストをちゃんと測る
  • 「やめるときどうするか」まで含めて設計する

くらいの冷静さを持って付き合うのが良いのでは、と思っています。

構造化出力で毎回イライラしているなら、
まずは小さなタスクを Instructor に置き換えてみる。
そこから「型で LLM を制御する」感覚にハマったら、DSPy でフローごと設計し直す。

そんな順番が、2025 年時点での現実解なんじゃないでしょうか。

コメント

タイトルとURLをコピーしました