0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LLMのStructured Outputで「format」を分離する:TypeScriptで壊れにくいスキーマ設計

0
Posted at

本稿の背景資料: https://www.aceround.app/ja/blog/ai-interviewer-follow-up-questions/

LLMのStructured Outputで「format」を分離する:TypeScriptで壊れにくいスキーマ設計

LLMにJSONを返させるとき、スキーマを渡しているのにリクエスト自体が400になることがあります。原因がモデルの出力ではなく、スキーマに含めたJSON Schemaのformatキーワードを、互換ゲートウェイが受け付けないことだと、ログを見ても気づきにくいです。

結論はシンプルです。

  • モデルに渡すスキーマは、構文と必須項目だけを表す「受理用」にする
  • モデルから返った値は、アプリケーション境界で「業務用」の厳格なスキーマで検証する
  • 2つのスキーマを同じ名前で使い回さず、テストで境界を固定する

この分離により、ゲートウェイの制約で推論が始まらない問題と、形式が正しくないデータを保存する問題を同時に扱えます。

LLMのStructured Outputを受理用スキーマと業務用スキーマに分ける流れ

まず「どこで失敗したか」を分ける

Structured Outputの失敗は、少なくとも次の3段階に分けてログに残すべきです。

  1. リクエスト検証: ゲートウェイがスキーマを受理したか
  2. JSON解析: モデルの返した文字列をJSONとして解釈できたか
  3. 業務検証: JSONの値がメールアドレスやURLなどの規則を満たすか

format: "email" や format: "uri" は、3番目の業務検証には便利です。しかし、1番目のゲートウェイが対応していない場合、モデルを呼び出す前に拒否されます。つまり「モデルが形式を守れなかった」のではなく、「モデルに到達していない」のです。

受理用と業務用のスキーマを分ける

例として、LLMに採用担当者の連絡先候補を抽出させるケースを考えます。ゲートウェイには形式を付けず、文字列であることだけを要求します。

type ContactCandidate = {
  name: string;
  email: string;
  sourceUrl: string;
};

const modelSchema = {
  type: "object",
  additionalProperties: false,
  properties: {
    name: { type: "string" },
    email: { type: "string" },
    sourceUrl: { type: "string" },
  },
  required: ["name", "email", "sourceUrl"],
} as const;

ここで email と sourceUrl を文字列にしているのは、検証を弱めるためではありません。ゲートウェイとの契約を最小限にするためです。ゲートウェイが認識できるキーワードだけで、LLMから返るオブジェクトの形を固定します。

一方、保存や画面表示に進める前には、厳格な業務検証を行います。

const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
const urlPattern = /^https?:\/\/[^\s]+$/;

function validateContact(value: ContactCandidate): ContactCandidate {
  if (!value.name.trim()) {
    throw new Error("name must not be empty");
  }
  if (!emailPattern.test(value.email)) {
    throw new Error("invalid email");
  }
  if (!urlPattern.test(value.sourceUrl)) {
    throw new Error("invalid sourceUrl");
  }
  return value;
}

実際のプロジェクトでZodなどを使う場合も考え方は同じです。モデル用スキーマでは z.string() を使い、永続化用スキーマでは z.email() や z.url() を使います。モデル用スキーマから生成したJSON Schemaに、対応していない format が混ざっていないことをテストで確認します。

ゲートウェイの制約を小さなテストで再現する

本番のゲートウェイを毎回呼ばなくても、スキーマに未対応キーワードが混入したら検出できます。次のテストは、format を見つけたら失敗する最小の「ゲートウェイ・プローブ」です。

type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue };

function findFormats(value: JsonValue, path = "$"): string[] {
  if (Array.isArray(value)) {
    return value.flatMap((item, index) =>
      findFormats(item, path + "[" + index + "]"),
    );
  }
  if (value && typeof value === "object") {
    return Object.entries(value).flatMap(([key, child]) => {
      const nextPath = path + "." + key;
      return key === "format"
        ? [nextPath + "=" + String(child)]
        : findFormats(child, nextPath);
    });
  }
  return [];
}

function assertGatewaySchema(schema: JsonValue): void {
  const formats = findFormats(schema);
  if (formats.length > 0) {
    throw new Error("unsupported schema formats: " + formats.join(", "));
  }
}

テストでは、受理用スキーマが通り、誤って業務用スキーマを渡した場合だけ失敗することを確認します。

assertGatewaySchema(modelSchema);

const invalidForGateway = {
  ...modelSchema,
  properties: {
    ...modelSchema.properties,
    email: { type: "string", format: "email" },
  },
};

try {
  assertGatewaySchema(invalidForGateway);
  throw new Error("expected the gateway probe to fail");
} catch (error) {
  if (!(error instanceof Error) || !error.message.includes("format")) {
    throw error;
  }
}

このテストは、特定ベンダーのSDKに依存しません。JSON Schemaを生成するライブラリやAI SDKを更新したときにも、実際にゲートウェイへ送る直前の形を検査できます。

ランタイムの処理順序

処理順序を固定すると、エラーの意味が混ざりません。

受理用スキーマでモデルを呼び出し、返却後に厳格検証する時系列

  1. modelSchema を送る前に、format の有無を検査する
  2. ゲートウェイのレスポンスをJSONとして解析する
  3. 解析結果を業務用バリデータへ渡す
  4. 不正なら保存せず、フィールド名と値の出所をログに残す

ここで重要なのは、3番目の検証を省略しないことです。受理用スキーマは「LLMに仕事を始めてもらう」ためのものであり、「データが正しい」と保証するものではありません。

失敗時に残すコンテキスト

実運用では、次の情報を別々に記録すると調査が速くなります。

  • ゲートウェイ拒否: 使用したタスク名、スキーマのハッシュ、HTTPステータス
  • JSON解析失敗: レスポンスID、先頭だけをマスキングした本文、モデル名
  • 業務検証失敗: ユーザーやジョブのID、失敗したフィールド、検証ルール名

メールアドレスやURLの実値をそのままログへ書く必要はありません。値のハッシュやドメインだけに留めると、後から同じ入力を追跡しながら個人情報を増やさずに済みます。

まとめ

Structured Outputを安定させるポイントは、LLMに厳しい正規表現を理解させることではありません。

  • ゲートウェイが受理できる最小スキーマをモデルへ渡す
  • 返却後に業務用スキーマでメール・URL・列挙値を検証する
  • スキーマ生成物を検査するテストをCIに置く
  • 失敗段階ごとにログのコンテキストを分ける

この境界を守れば、ゲートウェイを乗り換えてもモデル呼び出しは壊れにくく、業務データの品質はアプリケーション側で一貫して守れます。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?