Taste of Tech Topics

Acroquest Technology株式会社のエンジニアが書く技術ブログ

GPT-5の出力形式をCFGを使って強制する

こんにちは。新人エンジニアの飯棲です。

本記事ではGPT-5で新しく導入された新しいパラメータの一つであるCFGについて紹介します。
CFGはLark文法や正規表現によってモデルの回答の出力形式を制限できる便利な機能です。

1. はじめに

GPT-5からはリクエスト時に指定できるパラメータが追加されており、モデルの出力を今までよりも細かく制御することが可能です。
cookbook.openai.com
今回はその中でも、出力のフォーマットを強制できるCFGパラメータを紹介するとともに、CosmosDBのクエリ生成を試してみます。

2. GPT-5の新機能、CFGパラメータについて

1. 何ができるか

Responses APIのtoolsとして、CFGパラメータを指定することができます。
これは、GPT-5の出力形式を、CFG(Context Free Grammar: 文脈自由文法)に沿った形式で強制することのできるパラメータです。
現在はPython構文解析ライブラリであるLark文法、もしくは正規表現として指定し制御することができます。

CFGは、正規表現では扱えない入れ子再帰的な構造も含め表現でき、定義して出力のフォーマットを制御することができます。
例えば、CFGを使うことで、任意の四則演算を表現することができます。
以下は任意の四則演算を表現するLark文法です。

# 任意の四則演算を表現するCFG ex) 1+5-3, (1+9)*(7-4)/6
arith_grammar = textwrap.dedent(r"""
    // ---------- Start ----------
    // 式 (四則演算のみ)。演算子の優先順位: () > * / > + -
    ?start: expr

    // 加減(左結合, 任意個数の項)
    ?expr: expr "+" term   -> add
         | expr "-" term   -> sub
         | term

    // 乗除(左結合, 任意個数の因子)
    ?term: term "*" factor -> mul
         | term "/" factor -> div
         | factor

    // 因子(数値 / 括弧 / 単項マイナス)
    ?factor: NUMBER        -> number
           | "-" factor    -> neg
           | "(" expr ")"

    // ---------- 終端記号 ----------
    // 数字(整数)
    %import common.NUMBER
    %import common.WS
    %ignore WS
""")

2. 注意事項

GPT-5のCFG機能には以下の制限があります。

  • Responses API以外では現状は使えません。
    • Chat Completion APIなどでは利用できません。
  • 正規表現とLarkでそれぞれ以下の構文はサポートされていません。
    • 正規表現でサポートされない例
      1. 先読み ((?=...), (?!...), etc.)
      2. 最短一致量指定子(*?, +?, ??)
    • Larkでサポートされない例
      1. 終端記号の優先順位
      2. templates
      3. %declares
      4. import(import common以外)

例で示すと以下のような構文です。

正規表現でサポートされない例

// 先読み
// 「円」が後ろにあることを確認するための正規表現
YEN_NUMBER: /[0-9]+(?=円)/
YEN: "円"
ex_lookahead: YEN_NUMBER YEN

// 最短一致量指定子
// 最短一致(*?)を使ったタグマッチ
LAZY_TAG: /<.*?>/
ex_lazy: LAZY_TAG

Larkでサポートされない例

// 終端記号の優先順位
WORDNUM.1: /[a-zA-Z0-9]+/
WORD.2: /[a-zA-Z]+/
ex_priority: WORD | WORDNUM

// templates
// Larkのテンプレート構文 {T} を使ったリスト定義
NAME: /[A-Za-z_][A-Za-z_0-9]*/
list{T}: T ("," T)*
ex_templates: list{NAME}

// %declares
// 終端記号を別ファイルや外部から読むことは不可
%declare NUMBER
ex_declare: NUMBER

// import(import common以外)
%import mylib.MYTOKEN
ex_import: MYTOKEN

3. 試してみる

1. 今回やること

今回はAzure CosmosDBのクエリ生成のタスクにCFGパラメータを使います。
CosmosDBのクエリは、基本的な文法はSQLクエリですが、独自の制限がいくつかあり、自由に生成するとしばしばそのまま扱えないクエリになります。
そこで今回は、CosmosDBのクエリを「WHERE」と「ORDER」のみに制限するCFGを試してみます。

2. Lark文法を定義する

CosmosDB のクエリは、SELECT VALUE を使うことで、キーや式の結果を JSON オブジェクトではなく値そのものとして返すことができることを特徴としています。
今回は
SELECT VALUE c.<属性> FROM c WHERE <条件式> ORDER BY <ソート順>
という形式の文章のみを受理するLark文法を記述します。
以下が定義したLark文法です。

import textwrap
cosmos_grammar = textwrap.dedent(r"""
            // ---------- Punctuation & operators ----------
            SP: " "
            COMMA: ","
            SEMI: ";"

            LPAREN: "("
            RPAREN: ")"
            LBRACK: "["
            RBRACK: "]"
            DOT: "."

            PLUS: "+"
            MINUS: "-"
            MUL: "*"
            DIV: "/"

            EQ: "="
            NEQ1: "!="
            NEQ2: "<>"
            LT: "<"
            LTE: "<="
            GT: ">"
            GTE: ">="

            // ---------- Keywords ----------
            SELECT: /(?i)SELECT/
            VALUE:  /(?i)VALUE/
            FROM:   /(?i)FROM/
            WHERE:  /(?i)WHERE/
            ORDER:  /(?i)ORDER/
            BY:     /(?i)BY/
            ASC:    /(?i)ASC/
            DESC:   /(?i)DESC/
            AND:    /(?i)AND/
            OR:     /(?i)OR/
            NOT:    /(?i)NOT/
            TRUE:   /(?i)true/
            FALSE:  /(?i)false/
            NULL:   /(?i)null/

            // ---------- Start (VALUE 必須、WHERE/ORDER BY のみ) ----------
            start: SELECT SP VALUE SP expr SP FROM SP container (SP WHERE SP bool_expr)? (SP ORDER SP BY SP order_list)? SEMI?

            // ---------- Container (コレクション/エイリアス想定) ----------
            container: IDENT

            // ---------- ORDER BY ----------
            order_list: order_term (COMMA SP order_term)*
            order_term: expr (SP (ASC|DESC))?

            // ---------- WHERE (論理式) ----------
            ?bool_expr: or_test
            or_test: and_test (SP OR SP and_test)*
            and_test: not_test (SP AND SP not_test)*
            not_test: (NOT SP)? (comparison | LPAREN bool_expr RPAREN)

            comparison: expr (SP comp_op SP expr)?
            comp_op: EQ | NEQ1 | NEQ2 | LT | LTE | GT | GTE

            // ---------- Expressions ----------
            ?expr: sum
            ?sum: product ((SP? (PLUS|MINUS) SP? ) product)*
            ?product: factor ((SP? (MUL|DIV) SP? ) factor)*
            ?factor: atom

            atom: path
                | NUMBER
                | STRING
                | TRUE
                | FALSE
                | NULL
                | LPAREN expr RPAREN

            // ---------- Field/Property path (e.g. foo.bar / arr[0]) ----------
            path: IDENT (DOT IDENT | LBRACK NUMBER RBRACK)*

            // ---------- Terminals ----------
            IDENT: /[A-Za-z_][A-Za-z0-9_]*/
            NUMBER: /[0-9]+(\.[0-9]+)?/
            STRING: /"([^"\\]|\\.)*"|\'([^\'\\]|\\.)*\'/

    """)

3. CFGに設定してGPT-5を実行する

CFGをResponses APIのパラメータに設定して生成を試してみます。

from openai import OpenAI
client = OpenAI()

sql_prompt_cosmos = (
    "cosmos_grammar を使って、Azure Cosmos DB SQL API のクエリを生成してください。\n"
)

input_requirements = input("要件を入力:")
sql_prompt_cosmos += input_requirements

response_cosmos = client.responses.create(
    model="gpt-5",
    input=sql_prompt_cosmos,
    text={"format": {"type": "text"}},
    tools=[
        {
            "type": "custom",
            "name": "cosmos_grammar",
            "description": "SELECT の直後に VALUE を必ず指定し、WHERE と ORDER BY のみを許可した Cosmos DB SQL API の読み取り専用クエリを実行する。**クエリが文法に厳密に従っているか検証すること。**",
            "format": {
                "type": "grammar",
                "syntax": "lark",
                "definition": cosmos_grammar
            }
        },
    ],
    parallel_tool_calls=False,
)

# grammar ツール出力(文に一致する最初の tool 入力)を取り出して表示
print(response_cosmos.output[1].input)

4. 色々なパターンを試す

これまででCFGの設定は終わったので、実際にいくつかのパターンで文法が強制されるのか見てみましょう。

シンプルなパターン

まずは、シンプルに一つの絞り込み、ソート条件がある以下の文章を試してみます。

  • 入力

ordersテーブルからpriceが1000以上の注文をcreatedAt順に並べる

  • 結果

SELECT VALUE c FROM c WHERE c.price >= 1000 ORDER BY c.createdAt ASC;

問題なく指定した文法に変換できていますね。

複雑なパターン

次は、複数、かつ複雑なクエリが必要な以下の文章を試してみます。

  • 入力

orders テーブルから、createdAt が "2025-01-01" 以降 "2025-06-30" 以前、totalAmount >= 10000、paid = true、priority が"high"もしくは"urgent"を満たす注文を取得し、totalAmount 降順、同値は createdAt 昇順で並べる。

  • 結果

SELECT VALUE orders FROM orders WHERE (orders.createdAt >= "2025-01-01" AND orders.createdAt <= "2025-06-30" AND orders.totalAmount >= 10000 AND orders.paid = true AND (orders.priority = "high" OR orders.priority = "urgent")) ORDER BY orders.totalAmount DESC, orders.createdAt ASC;

これも問題なく変換できています。

逸脱したパターン

ここまではGPT-5の元々の変換能力でも問題なくこなせそうですが、指定した文法から逸脱したクエリを要求するような文章はどうなるでしょうか?
以下の文章では文法に指定していない、SUM()とGROUP BYが本来であれば必要になります。

  • 入力

orders テーブルから、customerごとにpriceの合計を求める。

  • 結果

SELECT VALUE price FROM orders WHERE customer = "Alice";

クエリとしては間違っていますが、これも指定したWHEREとORDER BY以外は使用しないクエリを生成しています。

一方でCFGパラメータを使用しない場合、出力は以下のようになりました。
SELECT VALUE SUM(c.price) SELECT VALUE SUM(c.price) FROM c
定義外のSUM()が使われてしまっていますね。

この場合、「SUM()は使用しないこと」と追記して対処をすることが考えられますが、そのような追記をせずとも、
CFGは使用できるクエリを制限することができます。

4. まとめ

今回はGPT-5のCFGパラメータについて紹介しました。
プロンプトエンジニアリングでの出力整形の精度は高まっている現状ですが、イレギュラーな入力がされる場合でも出力形式を保証できることはCFGの強みだと思います。
クエリ生成やモデルの出力をプログラムに直接渡すような場面で出力形式を完全に制御したい場合に、とても使えそうなパラメータだと思いました。

Acroquest Technologyでは、キャリア採用を行っています。

  • Azure OpenAI/Amazon Bedrock等を使った生成AIソリューションの開発
  • ディープラーニング等を使った自然言語/画像/音声/動画解析の研究開発
  • マイクロサービス、DevOps、最新のOSSクラウドサービスを利用する開発プロジェクト
  • 書籍・雑誌等の執筆や、社内外での技術の発信・共有によるエンジニアとしての成長

少しでも上記に興味を持たれた方は、是非以下のページをご覧ください。

www.wantedly.com