3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Structured Outputs の仕組み JSONスキーマのガイダンス

Last updated at Posted at 2025-04-03

Structured Outputs を用いることで LLM(Large Language Model)の出力を厳密に JSONスキーマに準拠させられますが、逐次的にテキストを生成する LLM で、どうすればそのようなことを実現できるのか。その仕組みを Hugging Face の Guidance を参考に調査しました。

結論からいうと有限状態マシン(FSM: Finite State Machine、有限オートマトンともいう)を用いて LLM が予測する次トークンの候補から JSONスキーマに準拠するものだけを選抜します。

例えば、"ACC" または "ABC" のいずれかのテキストだけを生成したい場合、その有限状態マシンは次のような状態遷移図で表せます。

ここで、0 から 4 の数値の箱は状態を、状態から次の状態へ遷移する矢印はその状態で入力可能な文字を表します。例えば初期状態0は文字 A だけを許容し、A が入力されたら状態1へ遷移します。状態1 は B 又は C を許容し、C が入力されたら状態2へ、B が入力されたら状態3へ遷移します。

さて、文字 A をLLM へ入力すると次の文字(次トークン)を次表のように予測したとします。

次トークン 確率
A 0.1
B 0.2
C 0.2
D 0.5

このままだと A の次の文字は D の可能性が高いですが、有限状態マシンに従うと A の次は B または C しか許容されません。なのでそれ以外の文字 A, D を候補から除外し残りの候補から次の文字を選択します(次表)。

次のトークン 確率
A 0
B 0.2
C 0.2
D 0

この操作を繰り返すことで有限状態マシンに従うテキストを生成します。すなわち JSONスキーマに基づく有限状態マシンを用いれば、JSONスキーマに準拠したテキストを生成できるということです。

有限状態マシンは正規表現から作成できるので、JSONスキーマを正規表現へ変換できればその正規表現から JSONスキーマの有限状態マシンを作成することができます。

以降では、Python にてパッケージ outlines-coreinteregular を用いて JSONスキーマの有限状態マシンを作成します。outlines-core で JSONスキーマを正規表現へ変換し、interegular でその正規表現の有限状態マシンを作成します(次図)。

実行環境

実行環境: Google Colab

バージョン

  • Python 3.11.11
  • outlines-core 0.1.26
  • interegular 0.3.3

outlines-coreをインストールすると依存関係で interegular もインストールされます。

pip install -q outlines-core

正規表現の有限状態マシン作成

JSONスキーマへ取り掛かる前に正規表現 A(B|C)C の有限状態マシンを作成してみます。

import outlines_core
import interegular

# 正規表現から有限状態マシンを作成
fsm = interegular.parse_pattern(r'A(B|C)C').to_fsm()

print(fsm)

実行結果

  name final? A B C anything_else 
----------------------------------
* 0    False  1                   
  1    False    2 3               
  2    False      4               
  3    False      4               
  4    True                       

ここで name は状態を表します。 * は初期状態を表し、final? は最終状態であるか否かを表します。A B C はその文字を受理した場合の遷移先です。

冒頭の有限状態マシンの状態遷移図はこれを図にしたものです。

有限状態マシンで逐次評価

次の関数 accepts は、有限状態マシンを用いて入力 inputs を1文字ずつ読み取り逐次的に評価するコード例です。

def accepts(fsm, inputs):
    anything_else = interegular.fsm.anything_else

    state = fsm.initial
    for symbol in inputs:
        print(symbol, end='')
        if anything_else in fsm.alphabet and not symbol in fsm.alphabet:
            print(" is anything_else", end='')
            symbol = anything_else
        transition = fsm.alphabet[symbol]

        # Missing transition = transition to dead state
        if not (state in fsm.map and transition in fsm.map[state]):
            print()
            return False

        post_state = fsm.map[state][transition]
        print(f" {state} -> {post_state}", end='')
        state = post_state
        print()

    return state in fsm.finals


print(accepts(fsm, 'ACC'))
print()
print(accepts(fsm, 'ABC'))
print()
print(accepts(fsm, 'AAC'))

このコードは interegular の accepts 関数 に少し手を加えたものです。

実行結果

A 0 -> 1
C 1 -> 3
C 3 -> 4
True

A 0 -> 1
B 1 -> 2
C 2 -> 4
True

A 0 -> 1
A
False

ここで有限状態マシンに合致する "ACC" と "ABC" は戻り値が True ですが、合致しない "AAC" は2文字目の "A" で False となっていることが分かります。

雰囲気が分かったので次は、JSONスキーマの有限状態マシンを作成します。

JSONスキーマを正規表現へ変換

パッケージ outlines-core で JSONスキーマを正規表現へ変換します。

まず、Pydantic のモデル User を定義しその JSONスキーマを生成します。

import json
from pydantic import BaseModel

# Pydantic モデル
class User(BaseModel):
    id: int
    name: str

# JSONスキーマを生成
schema = json.dumps(User.model_json_schema(), indent=2)

print(schema)

実行結果:JSONスキーマ

{
  "properties": {
    "id": {
      "title": "Id",
      "type": "integer"
    },
    "name": {
      "title": "Name",
      "type": "string"
    }
  },
  "required": [
    "id",
    "name"
  ],
  "title": "User",
  "type": "object"
}

outlines-core の build_regex_from_schema 関数を使って JSONスキーマを正規表現へ変換します。

from outlines_core.fsm.json_schema import build_regex_from_schema

# JSONスキーマを正規表現へ変換
regex_str = build_regex_from_schema(schema, whitespace_pattern=None)

print(regex_str)

実行結果:JSONスキーマの正規表現

\{[ ]?"id"[ ]?:[ ]?(-)?(0|[1-9][0-9]*)[ ]?,[ ]?"name"[ ]?:[ ]?"([^"\\\x00-\x1F\x7F-\x9F]|\\["\\])*"[ ]?\}

正規表現から有限状態マシンを作成

パッケージ interegular で JSONスキーマの正規表現から有限状態マシンを作成します。

JSONスキーマの正規表現を用いて有限状態マシンを作成

# 正規表現から有限状態マシンを作成
schema_fsm = interegular.parse_pattern(regex_str).to_fsm()

状態遷移表を確認します。

print(schema_fsm)

次は、状態遷移表からの抜粋です。

  name final?     "  ,  -  0  1  2  3  4  5  6  7  8  9  :  \\ a  d  e  i  m  n  {  } anything_else 
-----------------------------------------------------------------------------------------------------
* 0    False                                                                     1                               
  1    False   2  3                                                                                              
  2    False      3                                                                                              
  3    False                                                            4                                        
  4    False                                                      5                                              
  5    False      6                                                                                              
  6    False   7                                         8                                                       
  7    False                                             8                                                       
  8    False   10       12 11 9  9  9  9  9  9  9  9  9                                                          
  9    False   15    14    13 13 13 13 13 13 13 13 13 13                                                         
  10   False            12 11 9  9  9  9  9  9  9  9  9                                                          
  11   False   15    14                                                                                          
  12   False               11 9  9  9  9  9  9  9  9  9                                                          
  13   False   15    14    13 13 13 13 13 13 13 13 13 13                                                         
  14   False   16 17                                                                                             
  15   False         14                                                                                          
  16   False      17                                                                                             
  17   False                                                                  18                                 
  18   False                                                   19                                                
  19   False                                                               20                                    
  20   False                                                         21                                          
  21   False      22                                                                                             
  22   False   23                                        24                                                      
  23   False                                             24                                                      
  24   False   25 26                                                                                             
  25   False      26                                                                                             
  26   False   27 29 27 27 27 27 27 27 27 27 27 27 27 27 27 28 27 27 27 27 27 27 27 27 27            
  27   False   27 29 27 27 27 27 27 27 27 27 27 27 27 27 27 28 27 27 27 27 27 27 27 27 27            
  28   False      30                                        30                                                   
  29   False   31                                                                   32                           
  30   False   27 29 27 27 27 27 27 27 27 27 27 27 27 27 27 28 27 27 27 27 27 27 27 27 27            
  31   False                                                                        32                           
  32   True                                                                                                      

有限状態マシンでJSONを評価

JSON文字列が JSONスキーマに準拠するかどうかを有限状態マシンで評価します。

# 有限状態マシンでJSON文字列を評価
accepts(schema_fsm, '{"id": 123, "name": "アリス"}')

実行結果

{ 0 -> 1
" 1 -> 3
i 3 -> 4
d 4 -> 5
" 5 -> 6
: 6 -> 8
  8 -> 10
1 10 -> 9
2 9 -> 13
3 13 -> 13
, 13 -> 14
  14 -> 16
" 16 -> 17
n 17 -> 18
a 18 -> 19
m 19 -> 20
e 20 -> 21
" 21 -> 22
: 22 -> 24
  24 -> 25
" 25 -> 26
ア is anything_else 26 -> 27
リ is anything_else 27 -> 27
ス is anything_else 27 -> 27
" 27 -> 29
} 29 -> 32
True

True なのでJSONスキーマに準拠しています。

JSONスキーマに準拠しない場合は、その時点で False が返ります。

accepts(schema_fsm, '{"foo": "bar"}')

実行結果

{ 0 -> 1
" 1 -> 3
f is anything_else
False

以上です。
最後までお読みいただきありがとうございます。

参考文献

ドキュメント Hugging Face Guidance

ドキュメント Outlines

パッケージ outlines-core

パッケージ interegular

3
1
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
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?