vLLMのStructured OutputでJSONパース失敗率を改善してみた
X-tech推進本部 川端AI活用の一環で、過去のコードレビュー指摘をナレッジ化するプロジェクトに取り組んでいます。
このプロジェクトでは、蓄積したレビューコメントをLLMでJSON形式に正規化し、その出力結果をクラスタリングする処理を構築しました。
具体的には、システムプロンプトで出力フォーマットを厳密に定義し、以下のようなJSONを返す構成です。
{
"tags": ["css"],
"summary": "transitionに変数を使用していない",
"canonical": "use css variable",
"reason": "transitionは`var(--TRANSITION)`をご使用お願いします"
}
後段のクラスタリング処理はJSONが前提のため、パース失敗が起きると該当コメントがそのまま欠損になります。本記事では、この失敗をvLLMのStructured Outputsによって解消した方法を紹介します。
※ 記事執筆時点ではvLLM v0.21.0で動作確認を行っています。
問題:プロンプト指示だけでは5%程度が落ちる
700件以上のコメントに対して、プロンプト指示だけでクラスタリングをしようとしたところ、以下の結果になりました。
| 項目 | 件数 | 割合 |
|---|---|---|
| 総コメント数 | 727 | 100% |
| JSONパース成功 | 689 | 94.8% |
| JSONパース失敗 | 38 | 5.2% |
38件が後段処理に届きません。リトライで拾う方法もありますが、そもそも失敗させたくないと考え、根本的な解決策を探しました。
解決策:vLLMのStructured Output
このプロジェクトでは推論サーバーにvLLMを使用しており、vLLMが提供するStructured Output機能で根本的に解決できるのでは、と試してみました。
Structured Outputとは
vLLMが提供する機能で、response_format.json_schema パラメータにJSON Schemaを渡すことで、Constrained Decoding(制約付き復号)が有効になります。
通常のサンプリングではLLMはあらゆるトークンを生成できますが、Constrained Decodingではスキーマに合致するトークンだけに候補を絞って生成します。これにより、JSONスキーマに沿った出力を生成しやすくなります。
実装
1. JSON Schemaを定数として定義する
import群の直後にPython dictとして定義しておきます(Pydantic modelを利用する方法もありますが、今回はこちらで)。
REVIEW_THREAD_SCHEMA: dict = {
"type": "object",
"properties": {
"tags": {
"type": "array",
"items": {
"type": "string",
"enum": ["a11y", "css", "html", "javascript", "performance",
"coding", "design", "security", "other"],
},
},
"summary": {"type": "string"},
"canonical": {"type": "string"},
"reason": {"type": "string"},
},
"required": ["tags", "summary", "canonical", "reason"],
}
tagsはenumで選択肢を制限しています。
2. APIリクエストにresponse_formatを追加する
def call_llm(messages: list) -> dict:
body = {
"model": MODEL_NAME,
"messages": messages,
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "review_thread",
"schema": REVIEW_THREAD_SCHEMA,
},
},
}
return body
結果
プロンプト指示だけのケースと同じコメントに対して、改めて検証しました。
| 指標 | 導入前 | 導入後 |
|---|---|---|
| 総コメント数 | 727 | 727 |
| 成功率 | 94.8% | 100.0% |
| 失敗件数 | 38 | 0 |
失敗が0件になりました。リトライ処理も不要になります。
Tips:バックエンドの選択
Structured Outputを使う際は、サーバー側のバックエンド設定も確認しておくとよいです。
--structured-outputs-config.backend フラグとは
Constrained Decodingの処理エンジンは、vllm serve起動時の--structured-outputs-config.backendフラグで切り替えられます。
vllm serve <model> --structured-outputs-config.backend xgrammar
ソースコードを確認した限り指定できるバックエンドは以下のとおりです。
| バックエンド | 特徴 |
|---|---|
| xgrammar | GPU上で文法処理を行う新しい実装。高速 |
| guidance | 事前計算なしで安定した処理速度 |
| outlines | 実績のある従来実装。幅広いスキーマに対応 |
| LMFE | Python re互換のregexに対応。 |
デフォルト(auto)の挙動
デフォルトはautoで、vLLMが利用可能なバックエンドの中から自動で選択します。執筆時ではxgrammar→guidance→outlinesの優先順位で選択されます。LMFEは明示的なフラグ指定のみ利用可能、公式のドキュメントにも言及がない状態でした。
基本的にはデフォルトのままで問題ありませんが、挙動を固定したい場合(再現性の確保、特定バックエンドのチューニング等)は明示的に指定するのがよいでしょう。
まとめ
「JSONで返して」とプロンプトに書くだけでは、どうしても数%の失敗が残ります。vLLMのStructured Outputを使えば、推論エンジン側でスキーマ準拠を強制できるため、この問題を根本から解消できます。
JSONを前提とした後段処理がある場合や、できるだけパース成功率を上げたい場合は、Structured Outputを使うことが有効だと感じました。