Pythonで堅牢なLLM構造化出力の検証
「雰囲気」に頼る解析をやめ、契約を検証せよ。
ほとんどのLLM「構造化出力」チュートリアルは、本気度にかけるものです。 それらは、JSONを丁寧な口調でリクエストし、モデルが適切に動作することを祈る方法を教えます。 それでは検証ではありません。 それは単に括弧で囲まれた楽観主義にすぎません。
OpenAIの公式ドキュメントでも、この区別は明確にされています。JSONモードでは有効なJSONが得られますが、構造化出力(Structured Outputs)ではスキーマへの準拠が強制的に適用されます。OpenAIは、可能な限りJSONモードの代わりに構造化出力を使用することを推奨しています。

それでも、ペイロードが信頼できることを意味するわけではありません。JSON Schemaは構造と許可される値を定義し、PydanticはPythonで型付き検証を提供しますが、OpenAIは明示的に、スキーマが有効なレスポンスでも誤った値を含む可能性があることに注意を喚起しています。さらに、拒否(refusals)や不完全な出力により、期待した形状を回避してしまう可能性があります。本番環境では、構造化出力の検証はトグルスイッチではなくパイプラインです。この境界線は、スループット、リトライ、スケジューラの制限に関するLLMパフォーマンスエンジニアリングハブのより広範な文脈内でも存在する必要があります。
構造化出力の検証は契約である
LLMにおける構造化出力の検証とは、事前に回答の形状を定義し、可能な限りモデルがその形状を生成하도록制約し、その後、アプリケーションがそれを信頼する前に結果を再度検証することを意味します。実用的な観点からは、ペイロードがデータベース、UI、キュー、またはダウンストリームサービスに触れる前に、必須フィールド、型、列挙型、閉じたオブジェクト形状、およびドメインルールをチェックすることを意味します。JSON Schemaはまさにこの種構造的検証のために存在し、PydanticはPythonの型ヒントに対して信頼できないデータを検証するために構築されており、Pythonのjsonschemaライブラリはインスタンスをスキーマに対して直接検証する方法を提供します。
また、2つの一般的なユースケースの間には明確な分割があります。モデルが構造化された形式でユーザーに回答する場合は、構造化レスポンス形式を使用します。モデルがアプリケーションのツールや関数を呼び出す場合は、関数呼び出しを使用します。OpenAIのドキュメントはこの区別を明確に述べており、関数呼び出しについては、引数が関数スキーマに確実に準拠するようにstrict: trueを有効にすることを推奨しています。
私の強い意見はシンプルです。すべての構造化されたLLMレスポンスをAPIの境界線として扱ってください。プロンプトではなく契約の観点から考え始めると、アーキテクチャはよりクリーンになり、バグのコストは低くなり、「なぜモデルは本番環境で新しいフィールドを発明してしまったのか」という問題はほぼ消滅します。これが「LLMの構造化出力の検証とは何か」という問いに対する真の答えであり、「モデルに丁寧にJSONを頼む」ことよりもはるかに優れた答えです。
JSONモードは検証ではない
この記事から1つだけ覚えておくべきことがあるなら、それはこれです。JSONモードはスキーマ検証ではありません。OpenAIのヘルプセンターでは、JSONモードは出力が特定のスキーマと一致することを保証するものではなく、有効なJSONであり、エラーなしに解析されることだけを保証すると述べています。構造化出力のガイドでも、より明確な表現で同じことが述べられています。JSONモードも構造化出力も有効なJSONを生成できますが、スキーマへの準拠を強制するのは構造化出力だけです。
この違いは、人々が認める以上に重要です。OpenAIは構造化出力のリリース記事で、構造化出力を使用した場合、gpt-4o-2024-08-06は複雑なJSONスキーマの評価で100%を達成したのに対し、gpt-4-0613は40%未満にとどまったと報告しています。これらの数値を普遍的な真実として扱う必要はありませんが、より広い意味でのポイントが見えます。スキーマの強制により、失敗の表面積が「何が起こってもおかしくない」状態から「契約ははるかに厳密である」状態へと変化します。
それでもなお、エッジケースは存在し、それを無視することは、おもちゃのデモがページャーの当直(Pager Duty)の呼び出しに変わる原因となります。OpenAIは、モデルが安全でないリクエストを拒否できることをドキュメント化しており、その拒否は通常のスキーマパスの外に表現されます。また、max_output_tokensに達した場合やコンテンツフィルタによる中断など、不完全なレスポンスについてもドキュメント化しています。したがって、「JSONモードは信頼できるLLM出力に十分か」というFAQへの答えは、短いものと長いものの2つあります。短い答えは「いいえ」です。長い答えは、厳格な構造化出力であっても、明示的な障害処理が必要だということです。
構造化出力が依然として壊れる場所
スキーマの強制は問題を縮小します。しかし、それを消去するわけではありません。実際のトラフィックでは、プロンプトの文言とはほとんど関係のない理由で、壊れたり驚かされたりするペイロードが依然として見られます。
設計すべき失敗の形状
モデルとクライアントは詳細について意見が一致しません。JSONの前後に追加の文章が付けられたり、ペイロードの周りにMarkdownのフェンスブロックが付けられたり、名前は有効だが引数がPydanticモデルと一致しないJSONであるツール呼び出しが返されたりすることがあります。ストリーミングはこれを悪化させます。なぜなら、未完成のバッファを検証してしまう可能性があるからです。防御的なコードは、「バイト線がすでにモデルと一致している」という前提ではなく、「文字列が入力され、内部にJSONがあるかもしれない」という前提を想定すべきです。
プロバイダーとAPIの違い
すべてのホストが同じ構造化出力の表面を提供しているわけではありません。あるスタックではスキーマにバインドされた完成度の高い補完を提供し、別のスタックではJSON構文のみを保証し、ローカルランタイムはホストされたAPIに遅れをとっている場合があります。これが、FAQ「PythonでLLMのJSONを検証するには?」が、存在する場合はプロバイダー側の強制から始め、最終的にはPython側の検証で終わる理由の1つです。ベンダーの比較のより広い視点については、主要なLLMプロバイダー間の構造化出力比較参照してください。ローカルでモデルを実行する場合、Ollamaでの抽出後などのPythonとGoでのOllamaによる構造化LLM出力で線フォーマットを正規化した後、同じ検証パイプラインが適用されます。ランタイムがまだJSONを奇妙なプレフィックスや推論トレースでラップしている場合、Ollama GPT-OSS構造化出力の問題で説明されている同じクラスのパーサーの失敗を期待してください。
実際に機能するPythonスタック
私の推奨事項は、あえて退屈なものです。まず、モデルプロバイダーが構造的契約を強制できる場合は、それに任せます。次に、PydanticでPythonで返されたペイロードを検証します。第三に、スキーマだけでは証明できない事実に対して、明示的なビジネスルールの検証を使用します。第四に、プレイグラウンドのスクリーンショットを指差して終わりにするのではなく、フィクスチャと敵対的例で契約をテストします。OpenAIの構造化出力ドキュメント、Pydanticのバリデーターモデル、Pythonのjsonschemaツール、およびOpenAI自身の構造化出力評価例は、すべてその方向を指しています。
PydanticはPythonにとって適切な重心です。出力を通常のPython型としてモデル化し、model_json_schema()でJSON Schemaを生成し、model_validate_json()で生JSONを検証できます。Pydanticのドキュメントでは、model_validate_json()はjson.loads(...)してから検証するよりも一般的により良い方法であると述べています。なぜなら、その2段階のプロセスはPythonで余分な解析作業を追加するからです。
リポジトリにスタンドアロンのスキーマファイルを保持する場合、またはCIがモデルコードとは独立してフィクスチャペイロードを検証したい場合は、Pythonのjsonschemaパッケージがjsonschema.validate(...)で最もシンプルな契約チェックを提供します。pre-commitでそれを使いたい場合は、check-jsonschemaはjsonschemaに基づいて構築されたCLIおよびpre-commitフックとして具体的に存在します。これは、スキーマ変更をコード変更と同様にレビューしたいチームにとって非常に良い適合です。
フレームワークは配管を減らすことができますが、実際の検証の必要性をなくすわけではありません。LangChainは現在、プロバイダーがサポートする場合にプロバイダーネイティブの構造化出力を自動選択し、そうでない場合はツール戦略にフォールバックします。Instructorは、モデル呼び出しの上にPydanticレスポンスモデル、検証、リトライ、マルチプロバイダーサポートをレイヤーします。Guardrailsはバリデーターと入出力ガードレイヤーに焦点を当てています。いずれも有用なツールです。しかし、スキーマとビジネスルールは依然としてあなたに属します。より高レベルのライブラリを選択する場合、Python向けのBAML vs Instructor比較はこの記事の有用な補足となります。
最小限のOpenAIとPydanticの例
最も小さく本番環境に値する例には、いくつかの譲れない要素があります。可能な限り閉じた列挙型値のセットを使用します。追加のキーを禁止します。フィールドの説明を追加して、スキーマが人間に理解可能であり、モデルにより読み取りやすいようにします。ルートオブジェクトは明示的で退屈なものに保ちます。OpenAIは明確な名前と、重要なキーのタイトルおよび説明を推奨しており、JSON Schemaはenumを使用して値を制限し、Pydanticはextra="forbid"でオブジェクト形状を閉じることができます。
from typing import Literal
from openai import OpenAI
from pydantic import BaseModel, ConfigDict, Field
class TicketClassification(BaseModel):
model_config = ConfigDict(extra="forbid")
category: Literal["billing", "bug", "how_to", "abuse"] = Field(
description="Support ticket category."
)
priority: Literal["low", "medium", "high"] = Field(
description="Operational urgency."
)
needs_human: bool = Field(
description="Whether a human should review the case."
)
summary: str = Field(
description="A one sentence summary of the issue."
)
client = OpenAI()
response = client.responses.parse(
model="gpt-4o-2024-08-06",
input=[
{
"role": "system",
"content": "Classify support tickets. Return only the structured result.",
},
{
"role": "user",
"content": "Customer reports duplicate charges after refreshing checkout.",
},
],
text_format=TicketClassification,
)
result = response.output_parsed
print(result.model_dump())
この例では、見落としやすく、かつ絶対に気にする価値のある2つの詳細があります。Pydantic側のextra="forbid"は、OpenAIの関数呼び出しドキュメントにおける厳格なツールスキーマの要件でもあるadditionalProperties: falseというJSON Schemaの概念を反映しています。また、列挙型は装飾的なものではありません。それは、モデルがコードが理解できない値を発明するのを止める最も簡単な方法の1つです。
OpenAI Python SDKは、text_formatとしてPydanticモデルを供給したclient.responses.parse(...)をサポートしており、解析されたオブジェクトはresponse.output_parsedで返されます。同じSDKはclient.chat.completions.parse(...)もサポートしており、解析されたオブジェクトはmessage.parsedに存在します。最小限の接着剤で直接構造化データ抽出を行いたい場合、これらのヘルパーが最もクリーンな出発点です。
解析、正規化、そして検証
構造化出力とmodel_validate_jsonは、スタックがエンドツーエンドで整っている場合、多くの解析の痛みを取り除きます。プレーンなチャットテキストを返すプロバイダー、JSONをフェンスで囲むモデル、または生のコミット文字列を保存するロギングパスをサポートする瞬間、Pydanticが実行される前にテキストを辞書に変える1つのチョークポイントが必要です。
import json
def parse_json_from_llm_text(text: str) -> dict:
cleaned = text.strip()
if cleaned.startswith("```"):
cleaned = cleaned.split("\n", 1)[1]
cleaned = cleaned.rsplit("```", 1)[0].strip()
# Common "Sure, here is the JSON:" prefix before the object.
if not cleaned.startswith("{") and "{" in cleaned and "}" in cleaned:
start = cleaned.find("{")
end = cleaned.rfind("}")
if end > start:
cleaned = cleaned[start : end + 1]
return json.loads(cleaned)
ticket_dict = parse_json_from_llm_text(raw_completion_text)
ticket = TicketClassification.model_validate(ticket_dict)
このヘルパーは意図的に退屈です。ペイロードが依然として単一のトップレベルオブジェクトである場合、フェンスされた"json ... “ブロックと、前置きの自然言語の前文を処理します。それは完全なJSON抽出子ではありません。モデルが文字列値内に中括弧をネストする場合、ナイーブなスライスが壊れる可能性があり、適切な修正は通常、より厳格なプロンプティング、スキーマにバインドされた補完、または専用のパーサーライブラリです。
ストリーミング補完
チャットトークンをストリーミングする場合、json.loadsまたはmodel_validate_jsonを各デルタで実行しないでください。APIが完了したメッセージを報告するまで(クライアントでストリームの終了またはfinish_reasonを確認し)、バッファリングします。テキストを連結し、1回だけ解析します。同じルールは、ツール呼び出しの引数がチャンクで到着する場合にも適用されます。引数文字列が完了した後にのみ検証します。
chunks: list[str] = []
for chunk in completion_stream:
delta = chunk.choices[0].delta.content or ""
chunks.append(delta)
raw_completion_text = "".join(chunks)
ticket = TicketClassification.model_validate_json(raw_completion_text)
JSONの周りにフェンスや雑談を期待する場合、raw_completion_textをまずparse_json_from_llm_textに渡すことができます。
プレーン文字列解析を所有した後、次の制約はしばしばPythonではなく、プロバイダーのJSON Schema方言と、リモートAPIが実際に受け入れるものになります。
Pythonで巧妙になる前に(プロバイダーのスキーマ制限)
スキーマジェネレーターの出力を盲信してAPIにダンプし、すべてのJSON Schema機能がサポートされていると仮定しないでください。OpenAIはJSON Schemaのサブセットをサポートし、構造化出力ではすべてのフィールドが必須であることを要求し、ルートがトップレベルのanyOfではなくオブジェクトであることを要求し、ネストの深さと総プロパティ数の制限をドキュメント化しています。プロバイダー向けのスキーマをシンプルに保ちます。それは妥協ではありません。それは良いエンジニアリングです。
プロバイダーに依存しない検証パスが必要、または保存されたフィクスチャとモックを検証したい場合は、Pydanticとjsonschemaの組み合わせは依然として素晴らしいものです。
from jsonschema import validate as validate_json
schema = TicketClassification.model_json_schema()
payload = {
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "Checkout duplicates charges after refresh.",
}
validate_json(instance=payload, schema=schema)
ticket = TicketClassification.model_validate(payload)
print(ticket)
このパターンは、モデルプロバイダーがネイティブな構造化出力の強制を提供しないテスト、契約フィクスチャ、および統合において特に便利です。ただし、ローカルで生成されたスキーマは、特定のプロバイダーがサポートするサブセットよりも広範である可能性があるため、「ローカルで有効」は自動的に「すべてのLLM APIで受け入れられる」ことを意味しないことを覚えておいてください。また、一部のプロバイダーはスキーマアーティファクトを前処理およびキャッシュするため、新しいスキーマの最初のリクエストはウォームリクエストよりも遅くなる場合があります。
ツール呼び出しは2番目の契約
関数またはツール呼び出しは、構造化出力のもう1つの主要な形状です。モデルは名前を選択し、あなたが管理するJSON Schemaと一致するはずの引数を渡します。OpenAIは、引数がそのスキーマと整列するように、ツール定義でstrict: trueを推奨しています。エージェント中心のスタックでは、悪質なサンプリングはすぐに無効なツールJSONに変わります。QwenとGemma向けのエージェント推論パラメータリファレンスを使用して、マルチステップ作業とサンプリング設定を整列させます。
以下のスニペットは、すでにプロバイダーのツール呼び出しオブジェクトをname文字列とarguments辞書にマッピングしていることを前提としています。例えば、チャット補完でtool_calls[].functionを解析することにより(JSON文字列引数はまずjson.loads)。dispatch_toolはその正規化の後のステップです。
Pythonでは2つの実践的なルールが役立ちます。まず、実行をルーティングする前に、ツール名を明示的な許可リストに対して検証します。次に、アドホックなキーアクセスではなく、テストで使用するのと同じPydanticモデルで引数辞書を検証します。避けている障害モードは、「有効なJSON引数、しかし起動したツールにとって間違った形状」であり、これは文字列チェックをすり抜けます。
from typing import Any, Callable
from pydantic import BaseModel
ToolHandler = Callable[[dict[str, Any]], str]
def dispatch_tool(
*,
name: str,
arguments: dict[str, Any],
handlers: dict[str, tuple[type[BaseModel], ToolHandler]],
) -> str:
if name not in handlers:
raise ValueError(f"unsupported tool {name}")
model_cls, handler = handlers[name]
validated = model_cls.model_validate(arguments)
return handler(validated.model_dump())
handlers: dict[str, tuple[type[BaseModel], ToolHandler]] = {
"classify_ticket": (
TicketClassification,
lambda data: f"queued as {data['category']}",
),
}
このパターンは、ルーティングと検証を1つの場所に保ちます。実際のハンドラーはよりリッチになりますが、分割は同じままにするべきです:許可された名前、型付き引数、そして副作用。
スキーマ検証は依然としてビジネスルールを必要とする
有効なオブジェクトは、正しいオブジェクトと同じではありません。OpenAIはこれを直接的に述べています。構造化出力は、JSONオブジェクトの値内のミスを防止しません。それが、FAQ「なぜスキーマ検証とビジネスルール検証の両方が重要なのか」に blunt(直接的な)答えがある理由です。なぜなら、レスポンスはスキーマと完全に一致しながらも、ビジネスに損害を与える方法で誤っている可能性があるからです。
現実的な例を挙げます。構造は有効ですが、価格ロジックは依然として意味不明である可能性があります。
from decimal import Decimal
from typing import Literal
from typing_extensions import Self
from pydantic import BaseModel, ConfigDict, Field, model_validator
class Offer(BaseModel):
model_config = ConfigDict(extra="forbid")
currency: Literal["USD", "EUR", "GBP"]
amount: Decimal = Field(gt=0)
original_amount: Decimal | None
discounted: bool
@model_validator(mode="after")
def check_discount_logic(self) -> Self:
if self.discounted:
if self.original_amount is None:
raise ValueError(
"original_amount is required when discounted is true"
)
if self.original_amount <= self.amount:
raise ValueError(
"original_amount must be greater than amount"
)
return self
このバリデーターは、実際のシステムでスキーマだけでは苦手とすることを実行します。それは、モデル全体が解析された後のクロスフィールドセマンティクスをチェックします。Pydanticのmodel_validatorは、まさにこの種の全体オブジェクト検証のために存在します。デフォルトのないDecimal | Noneフィールドに注目してください。それはフィールドが存在し続ける一方でnullを許可し、厳格な構造化出力下でのオプションのような値に対するOpenAIのドキュメント化されたパターンに一致します。
検証の失敗を自動的にモデルにフィードバックさせたい場合、InstructorはPydanticの上にある実用的なレイヤーです。そのドキュメントでは、検証エラーがキャプチャされ、フィードバックとしてフォーマットされ、モデルに再試行を促すために使用されるリトライループを説明しています。
import instructor
retrying_client = instructor.from_provider("openai/gpt-4o", max_retries=2)
offer = retrying_client.create(
response_model=Offer,
messages=[
{
"role": "user",
"content": (
"Extract the offer from this text. "
"Was 49.00 USD, now 19.00 USD."
),
}
],
)
これは、私が喜んで推奨する数少ない利便性の1つです。実際の検証エラーに連動した自動リトライは有用です。サイレントな強制はそうではありません。Instructorのモデルレイヤー、リトライドキュメント、および検証ドキュメントはすべて同じアイデアに傾いており、それは正しいことです。
フレームワークなしで同じアイデアを実装できます。ループは小さく、モデルに問いかけ、Pydanticで検証し、検証が失敗した場合、エラー詳細をフォローアップのユーザーメッセージで返し、修正されたJSONのみを要求します。試行回数を制限し、最終的な失敗をログに記録し、呼び出し元に制御されたエラーを表示します。すでにresponses.parseや他のスキーマバインドヘルパーに依存している場合、このパスを稀にしか行使しないかもしれません。それでも、JSONモード、古いチャットエンドポイント、または生文字列をあなたに渡す任意のゲートウェイには重要です。
from openai import OpenAI
from pydantic import ValidationError
client = OpenAI()
messages = [
{"role": "system", "content": "Return only JSON that matches the ticket schema."},
{"role": "user", "content": "Customer reports duplicate charges after refreshing checkout."},
]
ticket: TicketClassification | None = None
for attempt in range(2):
completion = client.chat.completions.create(
model="gpt-4o-2024-08-06",
messages=messages,
response_format={"type": "json_object"},
)
raw_text = completion.choices[0].message.content or ""
try:
ticket = TicketClassification.model_validate_json(raw_text)
break
except ValidationError as exc:
messages.append(
{
"role": "user",
"content": f"Validation failed with {exc.errors()}. Return corrected JSON only.",
}
)
else:
raise RuntimeError("exhausted structured output retries")
assert ticket is not None
実際のサービスでは、トレーシングIDを添付し、ログ内の顧客テキストを削除し、回復可能な検証エラーと拒否または不完全なレスポンスを区別します。重要な部分は、リトライが一般的な「再試行」メッセージではなく、実際のバリデーター出力によって駆動されることです。
テスト、リトライ、そして閉じて失敗する
LLM検証が失敗したときに何が起こるべきでしょうか。肩をすくむことではありません。ペイロードを拒否し、失敗をログに記録し、タスクがリトライに値する場合は限定された試行でリトライし、受け入れられそうに見えているものにゴミを正規化するのではなく、閉じて失敗します。これもまた、多くのチームがプロバイダーのドキュメントがそれらのパスが存在すると教えているにもかかわらず、拒否と不完全な出力を明示的に処理することを忘れる場所です。
OpenAIのResponses APIの場合、障害処理は二番煎じではなく、第一級のコードであるべきです。変数はcompletionではなく、client.responses.createまたはparseからのresponseです。
if response.status == "incomplete":
raise RuntimeError(response.incomplete_details.reason)
content = response.output[0].content[0]
if content.type == "refusal":
raise RuntimeError(content.refusal)
それは防御的な過剰設計ではありません。それはドキュメント化された障害モードと直接整列しています。モデルが拒否する場合、あなたはスキーマ有効なペイロードを持っていません。レスポンスが不完全な場合、あなたはスキーマ有効なペイロードを持っていません。両方を制御フロー内の明示的なブランチとして扱ってください。
また、モデル呼び出し自体の外で契約をテストする必要があります。
import pytest
from jsonschema import validate as validate_json
from pydantic import ValidationError
def test_ticket_fixture_matches_schema():
payload = {
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "Checkout duplicates charges after refresh.",
}
validate_json(instance=payload, schema=TicketClassification.model_json_schema())
def test_discount_logic_rejects_broken_offer():
with pytest.raises(ValidationError):
Offer.model_validate(
{
"currency": "USD",
"amount": "19.00",
"original_amount": "10.00",
"discounted": True,
}
)
def test_ticket_rejects_unknown_category_string():
with pytest.raises(ValidationError):
TicketClassification.model_validate(
{
"category": "refund",
"priority": "high",
"needs_human": True,
"summary": "Customer wants a refund.",
}
)
def test_ticket_rejects_extra_keys():
with pytest.raises(ValidationError):
TicketClassification.model_validate(
{
"category": "bug",
"priority": "high",
"needs_human": True,
"summary": "Broken flow.",
"severity": "critical",
}
)
これは、PythonにおけるLLM出力検証のための正しい形状のテスト戦略です。契約内のすべてのフィールドが行使されるように、jsonschemaでゴールデンフィクスチャを検証します。Pydanticでセマンティクスを検証し、次に違法な列挙文字列、禁止された追加キー、およびあなたが気にするクロスフィールドの矛盾などの敵対的ケースを追加します。実際のモデル出力のスナップショットを作成する場合、PIIをスクラブし、それらを回帰フィクスチャとして扱います。
あなたのチームがOpenAIスタックに生きている場合、Evals APIには、機械可读形式に依存するタスクのテストと反復のために特化した構造化出力評価レシピも含まれています。また、リポジトリに生スキーマファイルを保持している場合、check-jsonschemaをCIまたはpre-commitに接続します。バイブスを運ぶのではなく、契約を運んでください。
後であなたを救う本番環境チェック
検証が失敗した場合、FAQの答えは直接的です。ペイロードを拒否し、なぜ失敗したかをログに記録し、タスクがもう1回試すに値する場合にターゲットを絞ったフィードバックでリトライし、データをキューに強制するのではなく、閉じて失敗します。
短い運用チェックリストは、チームが反復インシデントを避けるのを助けます。
- プロバイダーに送信したJSON Schemaのバージョンまたはハッシュをログに記録し、障害を正確に再現できるようにします。
- ログ内のモデル入力と出力を削除します。構造化ログは、顧客テキストを漏洩する場合は無意味です。
- 拒否率、不完全なレスポンス率、検証失敗率、および修復成功率のカウンターまたはメトリックを発行します。そこにスパイクがあれば、モデルまたはプロンプトの変更が配信されたときを推測するよりも優れています。
より広範なLLMシステムの可視化ガイダンスは、カウンターが存在する1度、それらのシグナルをダッシュボード、トレース、およびSLOレビューに接続するのに役立ちます。
ベストプラクティスは複雑ではありません。可能な限りプロバイダー側の構造化出力または厳格なツールスキーマを使用します。必要な場合は生テキストを正規化します。PydanticでPythonで契約をミラーリングします。スキーマが証明できないものに対してビジネスルールの検証を追加します。拒否と不完全なレスポンスを通常のブランチとして処理します。契約がデモで止まり、ソフトウェアになるまでテストします。それより少ないものは、単にプロンプトエンジニアリングのコスプレです。