🦁

VLLMでデプロイしたLLMにうまくJSONを書かせる方法

に公開

はじめに

LLMを組み合わせたシステム開発をしていると、LLMにJSONを吐かせたくなる場面があるはず。
OpenAIのAPIなんかはかなりよく出力してくれる。(APIがJSONをうまく吐けるように作られているらしい。OpenAIすごい。)
でもローカルデプロイしているLLMだと何度かプロンプトで調整してみるも、
思った通りのフィールド名にならなかったりして匙を投げたくなった!
なんて経験をした人もいるのではないでしょうか。
だが、VLLMを使うと割とその辺をうまく吸収してくれる制約機能があって、
試してみたら気軽にできて効果的だったので記事にまとめることにしてみました。

手順

手順は下の通り。
(1) VLLM サーバを起動
(2) 出したい構造化出力の仕様を決める
(3) 出力させる

また下のページが参考になります。

VLLMのGithubリポジトリの解説
https://github.com/vllm-project/vllm/blob/main/docs/features/structured_outputs.md

(1)VLLMサーバを起動する

VLLMライブラリのバージョンによるらしいが、私が使ったバージョン(0.15.0)だと

vllm serve (model名)
# 例
vllm serve meta-llama/Llama-3-8B-Instruct

のように、特別なオプションなしでOK。

実際のところ、これで起動するとバックエンド指定をよしなにやってくれているんだとか。
Tokenizer によってはバックエンドを明示指定するのが有効らしく、vllmバージョンによって対応しているバックエンドが違うため、そこはvllmの公式Githubで利用できるものを確認する必要がある。

# xgrammarで指定する場合
vllm serve meta-llama/Llama-3-8B-Instruct --structured-outputs-config.backend xgrammar

(2)出力させたい構造化出力の仕様(スキーマ)を決める

自分が必要なJSONがどんな形か考えながら、仕様を決めましょう。
たとえば私はこんなJSONを書きたいと思っているとします。

{
    "name": "田中太郎",
    "age": 28,
    "score": 87.5
}

なので、仕様(スキーマ)はこんな感じになります。

schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer"},
        "score": {"type": "number"}
    },
    "required": ["name", "age", "score"]
}

とはいっても面倒だと思うので、例を書いたら
ChatGPTなりClaudeなりと相談しつつ書いてしまえば早いと思います。

もっというと、(3)に書くテストコードと一緒に、JSONの例を渡してお願いしちゃうのが一番早いかも。

(3)リクエストを送る

curl でテストしてもいいのですが、システムの中に組み込む使い方が中心だと思うので、Pythonでの使い方を書いておきます。

import openai
import json

client = openai.OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")

# ここで(2)のschemaを書く
schema = {
    "type": "object",
    "properties": {
        "name": {"type": "string"},
        "age": {"type": "integer"},
        "score": {"type": "number"}
    },
    "required": ["name", "age", "score"]
}

response = client.chat.completions.create(
    model="meta-llama/Llama-3-8B-Instruct",
    messages=[{"role": "user", "content": "ユーザー情報を生成して"}],
    extra_body={"structured_outputs": {"json": schema}}  # この行が重要
)

あとはシステムに組み込むだけですが、生の出力とParse可能かどうかを確認したい場合は上のコードに下を追記してください。

raw = response.choices[0].message.content
print("=== 生出力 ===")
print(raw)

print("\n=== JSON検証 ===")
try:
    parsed = json.loads(raw)
    print("✅ パース成功")
    print(parsed)
except json.JSONDecodeError as e:
    print(f"❌ パース失敗: {e}")

まとめ

やってみるとプロンプトで苦労するよりもほしいJSONがすぐに手に入るし、
Logitを調整して~なんてやるよりもずっと簡単だと思いました。
今後必要になるときは積極的に使っていきたいです。

Discussion