構造化出力を安定してLLMにさせたいなら「instructor」はいかが?

Daichi Kugimiya

2025.9.29

はじめに:LLMの出力制御の課題

「このAIアプリ、ユーザーにいくつか選択肢を提示して、選んでもらう機能が必要だな…」

生成AIを使ったアプリケーション開発では、LLMから構造化されたデータを安定して取得することが重要な課題の一つだと感じています。

従来は、プロンプトに「JSON形式で返してください」とお願いしても、返ってくるレスポンスは必ずしも期待通りではありませんでした。

よくある問題:

  • "description": "お肌に良い" で終わるはずが ...お肌に良い", のようにカンマが余分についている
  • レスポンスに必須のはずのキー rating がない
  • 数値を期待したのに "5" のように文字列で返ってくる

これらの不完全なレスポンスを処理するため、大量のエラーハンドリングコードが必要でした。「もしこのキーがなかったら…」「もしパースに失敗したら…」そんなコードの複雑さに悩まされていた方も多いのではないでしょうか。

そこで今回は、TypeScriptとZodを使ってLLMの出力を型安全に扱えるinstructor-js をご紹介します。このライブラリを使うことで、構造化された信頼性の高いレスポンスを簡単に取得できるようになると思います。

instructor-jsとは?

instructor-jsは、一言でいうと 「OpenAI APIのレスポンスを、Zodで定義した型通りに確実に出力させてくれるライブラリ」 です。

その魔法のタネは、OpenAIのTool Calling機能にあります。instructorは、私たちがZodで定義したスキーマ(=型の定義)をTool Callingの形式に変換し、LLMに「この道具(スキーマ)を使って回答してください」と指示します。これにより、LLMは指定された構造に従わざるを得なくなり、結果として私たちは欲しい形のデータを確実に手に入れることができるのです。

実践!おすすめの温泉情報を取得してみる

百聞は一見にしかず。instructorを使って「おすすめの温泉を3つ教えて」とLLMに聞き、その情報をきれいな配列で受け取ってみましょう。

1. 準備:ライブラリのインストールと設定

まずは必要なライブラリをインストールします。

npm install openai zod instructor-js

次に、OpenAIクライアントを初期化し、instructorで「パッチ」を当てて拡張します。これはinstructorを使う上での決まり文句のようなものです。

import OpenAI from "openai";
import Instructor from "instructor-js";

// 通常のOpenAIクライアントを初期化
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY,
});

// instructorでクライアントを拡張!
export const instructor = Instructor({
  client: openai,
  mode: "TOOLS", // Tool Callingモードを指定
});

2. 型定義:Zodで欲しいレスポンスの形を決める

次に、LLMに返してほしいデータの「型」をZodで定義します。今回は、個々の温泉情報と、その配列を持つレスポンス全体の型を定義します。

import { z } from "zod";

// 温泉一つ分の情報スキーマ
const OnsenSchema = z.object({
  name: z.string().describe("温泉の名前"),
  prefecture: z.string().describe("温泉がある都道府県"),
  features: z
    .array(z.string())
    .describe("温泉の特徴を説明する短いキーワードの配列"),
  rating: z
    .number()
    .min(1)
    .max(5)
    .describe("1から5段階のおすすめ度"),
});

// レスポンス全体のスキーマ
export const OnsenResponseSchema = z.object({
  onsenList: z.array(OnsenSchema).describe("おすすめの温泉のリスト"),
});

// TypeScriptの型も推論できる
export type OnsenResponse = z.infer<typeof OnsenResponseSchema>;

describe を追加することで、LLMが各フィールドの意味をより正確に理解し、期待通りのデータを生成しやすくなります。

3. 実行:instructorでAPIを呼び出す

準備は整いました。実際にinstructorを使ってAPIを呼び出してみましょう。

import { instructor } from "./lib/openai";
import { OnsenResponseSchema, OnsenResponse } from "./schemas";

async function getOnsenRecommendations(): Promise<OnsenResponse> {
  console.log("おすすめの温泉情報を取得します...");

  const response = await instructor.chat.completions.create({
    model: "gpt-4o",
    messages: [
      {
        role: "user",
        content: "箱根周辺でおすすめの温泉を3つ教えてください。",
      },
    ],
    response_model: {
      schema: OnsenResponseSchema,
      name: "OnsenRecommendations",
    },
    max_retries: 3, // ここが重要!
  });

  return response;
}

getOnsenRecommendations().then((data) => {
  console.log("取得成功!");
  console.log(JSON.stringify(data, null, 2));

  // 型が保証されているので、そのままプロパティにアクセスできる!
  console.log("\\n--- おすすめNo.1 ---");
  console.log(`名前: ${data.onsenList[0].name}`);
  console.log(`場所: ${data.onsenList[0].prefecture}`);
  console.log(`評価: ★${data.onsenList[0].rating}`);
});

response_modelに先ほど定義したZodスキーマを指定し、max_retriesを設定するだけで準備は完了です。

実行結果の例:

{
  "onsenList": [
    {
      "name": "箱根湯寮",
      "prefecture": "神奈川県",
      "features": [
        "日帰り利用可能",
        "古民家風の趣",
        "19室の貸切個室露天風呂"
      ],
      "rating": 5
    },
    {
      "name": "天山湯治郷",
      "prefecture": "神奈川県",
      "features": [
        "野趣あふれる露天風呂",
        "自家源泉かけ流し",
        "休憩処が充実"
      ],
      "rating": 4
    },
    {
      "name": "龍宮殿本館",
      "prefecture": "神奈川県",
      "features": [
        "芦ノ湖と富士山の絶景",
        "国登録有形文化財",
        "日帰り入浴プランあり"
      ],
      "rating": 5
    }
  ]
}

--- おすすめNo.1 ---
名前: 箱根湯寮
場所: 神奈川県
評価: ★5

一切の型変換やパース処理を書かずに、完璧に型付けされたオブジェクトを取得できました。data.onsenList[0].name のように、エディタの補完も効いて開発体験が良いと感じます。

instructorの真価:自動リトライ機能

このライブラリの真の価値は、単なる型変換にとどまらないと考えています。

特に注目すべきは、max_retriesで設定した自動リトライ機能だと思います。これは単なる再実行ではありません。

もし最初の試行でLLMがratingフィールドを忘れたとします。すると…

  1. instructorがレスポンスを受け取る。
  2. Zodが「ratingフィールドは必須なのに存在しません!」という検証エラーを出す。
  3. instructorがそのエラーをキャッチする。
  4. instructorはLLMに対し、「前回の回答はこのエラーで失敗したよ。次はratingを必ず含めて回答してね」 という追加の指示と共に、2回目のリクエストを自動で送る。

この自己修正(Self-Correction)のループにより、開発者は複雑なバリデーション実装から解放されると感じています。コードはクリーンなままで、システムの信頼性も向上すると思います。

まとめ

instructor-jsは、LLMからのレスポンスという「不確実なもの」を、「確実な型付きデータ」に変換してくれる強力なライブラリです。

主なメリット:

  • 型安全性: Zodスキーマにより、コンパイル時とランタイムの両方で型安全性を確保
  • 自動リトライ: 失敗時の自己修正機能により、信頼性の高い出力を実現
  • 開発効率: 複雑なエラーハンドリングが不要になり、ビジネスロジックに集中可能

LLMを使った開発で構造化データの取得に課題を感じている方は、ぜひinstructor-jsを試してみてください。開発体験が大幅に向上するのではないかと思います。

参考リンク

ブログ一覧へ戻る

お気軽にお問い合わせください

SREの設計・技術支援から、
SRE運用内で使用する
ツールの導入など、
SRE全般についてご支援しています。

資料請求・お問い合わせ