GPT-5のCFG Function Callingは、2025年8月7日のGPT-5公開と同時にOpenAI APIへ入ったcustom tool("type": "custom")に、文脈自由文法(Context-Free Grammar)で入力の形を縛る機能です。モデルはJSONではなくプレーンテキストでツールを呼び、生成は指定したLark文法か正規表現で制約されます。ただし、出力上限などで応答が途中で止まる(status が incomplete になる)ことはあるため、実行前に完了状態と入力の構文を確かめます。この記事では、Responses APIでの定義形、custom_tool_call_output で結果を戻すまでの往復、OpenAIのLarkで使えない記法、Chat Completionsとの形の違い、GPT-6系へ移るときの確認点を、公式ドキュメントと手元での実行結果をもとに整理します。
まとめ
- CFG Function Callingは、custom tool の
formatに{"type": "grammar", "syntax": "lark" または "regex", "definition": 文法}を渡す機能。文法で縛られるのはcustom toolの入力文字列で、出力上限などで応答が途中で止まった場合は文法どおりに終わっていないことがある。 - モデルは
custom_tool_call(call_idとinput文字列)を返す。実行結果はcustom_tool_call_outputに同じcall_idを付けて戻す。function tool のfunction_call_outputとは型名が違う。 - LarkはLLGuidanceで解釈される派生版。先読み・後読み、最短一致(
*?など)、終端記号の優先度、テンプレート、%import common以外のimport、%declareは使えない。正規表現はRustのregexクレートの構文。 - Chat Completionsでも使えるが、定義が
custom.format.grammar.{syntax, definition}と1段深い。GPT-6.1 SolはChat Completionsでのツール呼び出しに対応していないため、新規実装はResponses APIで書く。 - 文法が保証するのは形だけ。手元の検証では、日付の正規表現は「2026-02-30」を通した。意味の検証、権限の制御、読み取り専用の接続は受け側で別に持つ。
gpt-5-2025-08-07などGPT-5のスナップショットは2026年12月11日にAPIから外れる。custom tool の書き方はGPT-6系の公式例でも同じ。
CFG Function Callingの位置づけ|JSONを介さないcustom tool
OpenAIのツール呼び出しには、JSON Schemaで引数を定義するfunction toolと、GPT-5で追加されたcustom toolの2種類があります。custom tool は引数の構造を定義せず、モデルが任意の文字列をそのままツールへ渡します。OpenAIは追加の理由を、長い入力をJSONに入れると引用符・バックスラッシュ・改行のエスケープを1つでも誤れば壊れるため、と説明しています。数百行のコードや5ページの報告書を渡すツールで効く設計です。開発者向けのGPT-5発表では、SWE-bench VerifiedでJSONのツールをcustom toolに置き換えても、GPT-5のスコアは「ほぼ同じ」でした。
custom tool に format を付けないと、入力は制約のない自由テキストになります。ここにLark文法か正規表現を渡すと、モデルのサンプリング自体が文法で制限されます。出力を後から検査して弾く仕組みではなく、文法に合わないトークンを生成の段階で選ばせない仕組みです。これがCFG Function Callingです。
function tool(JSON Schema・strict)との違い
| 項目 | function tool | custom tool+CFG |
|---|---|---|
| 定義 | "type": "function"+parameters |
"type": "custom"+format |
| モデルが返す形 | JSON文字列(arguments) |
プレーンテキスト(input) |
| 形の保証 | strict: true でスキーマどおり |
Lark文法または正規表現どおり |
| 呼び出しアイテム | function_call |
custom_tool_call |
| 結果を戻すアイテム | function_call_output |
custom_tool_call_output |
| 向く入力 | 名前付きの引数が数個 | SQL・DSL・コード・数式など |
引数がキーと値の組で表せるなら、function tool と strict: true で足ります。OpenAIは現行ガイドでstrictを常に有効にするよう推奨しています。custom tool+CFGを選ぶのは、SQLや独自DSLのように入力そのものが言語で、JSONの文字列フィールドに押し込むと構文を保証できない場合です。Function Callingの往復全体や strict の条件はFunction Callingの仕組みとOpenAI APIでの実装、JSON Schemaで出力そのものを縛る方法はLLM構造化出力の解説で扱っています。
ツール定義の書き方|format・syntax・definition
Responses APIでは、tools 配列に次の形でcustom toolを渡します。format を省くと既定の自由テキストになり、{"type": "text"} を明示しても同じです。
{
"type": "custom",
"name": "run_readonly_sql",
"description": "注文DBに読み取り専用のSELECT文を1つ渡して実行する。文法どおりの1文だけを出力する。",
"format": {
"type": "grammar",
"syntax": "lark",
"definition": "start: select_stmt\n..."
}
}
モデルが返す output 配列には、推論モデルなら reasoning アイテムに続いて次のような呼び出しが入ります。公式ガイドのLark例では、「4足す4」という指示に対して input が "4 + 4" になっています。
{
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_pmlLjmvG33KJdyVdC4MVdk5N",
"name": "math_exp",
"input": "4 + 4"
}
特定のcustom toolを必ず呼ばせたいときは、tool_choice に {"type": "custom", "name": "run_readonly_sql"} を渡します。ツール一覧は変えずに使えるツールだけを絞る {"type": "allowed_tools", "mode": "auto", "tools": [...]} も使えます。公式ガイドは、ツール一覧を毎回同じにしておくとプロンプトキャッシュが効きやすいことを理由に挙げています。
並列呼び出しには注意が必要です。GPT-5公開時のOpenAI Cookbook(現在はアーカイブ扱い)は「custom tool type does NOT support parallel tool calling」と明記しています。2026年10月時点の公式ガイドの並列呼び出しの節はfunctionの並列だけを説明していて、custom toolには触れていません。1ターンに1回の呼び出しを前提に組み、複数の文を1回の input に詰めたい場合は文法側で区切りを定義してください。
Lark文法の書き方と非対応の記法
OpenAIのLarkは、Pythonの構文解析ライブラリLarkの記法をもとにした派生版です。サンプリングの制約にはOSSの LLGuidance(guidance-ai)を使っているとFunction callingの公式ガイドに書かれています。そのため、Lark本体で通る文法がAPIで通るとは限りません。
終端記号と規則の役割分担
大文字の終端記号(terminal)は字句解析器が最長一致で先に切り出し、小文字の規則(rule)は切り出された記号を組み合わせるだけです。規則で自由文の境界を決めようとしても、字句解析器は従いません。公式ガイドの「正しい例と誤った例」を、手元で動く形に縮めると次のとおりです。
# 良い例:自由文を1つの終端記号(大文字)にまとめ、長さに上限を付ける
start: SENTENCE
SENTENCE: /[A-Za-z, ]{0,40}(fought|saved|found|lost)[A-Za-z, ]{0,40}\./
# 悪い例:規則(小文字)で自由文を区切ろうとする。字句解析は最長一致で先に走るので境界が守られない
start: sentence
sentence: /[A-Za-z, ]+/ verb /[A-Za-z, ]+/
verb: "fought" | "saved" | "found" | "lost"
良い例をLLGuidance 1.9.1でコンパイルし、トークン列を流し込んで確かめました。「the hero saved the kingdom.」は受理され、動詞の候補に無い「the hero ate a cake.」は拒否されました。悪い例の方をPythonのLark 1.3.1に読ませると、「the hero saved the kingdom」すら解析に失敗しました。先頭の /[A-Za-z, ]+/ が文末まで飲み込み、verb に渡す文字が残らないためです。終端記号の正規表現には {0,40} のような上限を付けます。公式は .+ のような無制限の繰り返しを避けるよう求めていて、出力が長く繰り返しになる「分布外(out of distribution)」の挙動を防ぐためだと説明しています。
OpenAIのLarkで使えない記法
| 非対応の記法 | 例 | 置き換え方 |
|---|---|---|
| 先読み・後読み | (?=a) (?!a) |
文字クラスと選択に展開 |
| 最短一致 | *? +? ?? |
否定文字クラス+上限付き繰り返し |
| 終端記号の優先度 | TERM.2 |
終端記号どうしが重ならないように書く |
| テンプレート | _separated{x, sep} |
規則を展開して書く |
| import | %import(common以外) |
%import common.INT などに限る |
| 宣言 | %declare |
使わない |
手元でLLGuidanceに先読みを含む文法を渡すと、「look-around, including look-ahead and look-behind, is not supported」というエラーでコンパイルが止まりました。一方、最短一致の /.+?\./ はLLGuidance 1.9.1では通りました。OpenAIのガイドはこれを非対応と明記しているので、ローカルのLLGuidanceで通ったことはAPIで通る根拠になりません。手元の確認は「明らかに通らない記法を早く見つける」用途に留め、最終確認は実際のAPI呼び出しで行ってください。
空白と複雑さの扱い
Lark本体でよく使う %ignore WS で空白を読み飛ばす書き方は、OpenAIのガイドが避けるよう求めています。無制限の読み飛ばしは文法を複雑にしすぎてAPIに拒否される原因になり、モデルが分布外に出る原因にもなるためです。空白は SP: " " のような終端記号で、許す位置にだけ明示的に置きます。APIが「文法が複雑すぎる」と返したときの対処として、公式は規則と終端記号を減らすこと、無制限の %ignore を外すことを挙げています。モデルが文法上は正しいが意味のおかしい出力を続ける場合は、文法を締める、ツールの説明文に文法の意図と数例を書く、reasoning.effort を上げる、の3つが公式の対処です。
正規表現(regex)形式の使いどころと限界
日付、ID、金額のように1行で終わる形式なら、Larkより "syntax": "regex" の方が短く書けます。構文はPythonの re ではなくRustのregexクレートで、先読み・後読みと最短一致は使えません。パターンは1行で書き、改行は \n で表します。/.../ で囲む必要はありません。正規表現の方言差は正規表現の基本記号と言語ごとの方言で整理しています。
限界も手元で確認しました。^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$ をLLGuidanceに渡すと、「2026-13-01」と「2026/10/02」は拒否しましたが、存在しない日付の「2026-02-30」は受理しました。正規表現が保証するのは文字の並びであって、暦として正しいかどうかではありません。受け取った側で日付型に変換し、失敗したらエラーとして返す処理は省けません。
Pythonでの実装|読み取り専用SQLツールの往復
CFGが効く典型例として、自然文から注文DBへのSELECT文を作らせるツールを組みます。文法でテーブル名・列名・比較演算子を許可リストにし、LIMIT を必須にします。コードはopenai(Python SDK)3.23.0とPython 3.12で書いています。openai 3.xはPython 3.10以上が必要です。
文法とツール定義・往復の処理
実行にはopenai 3.23.0とlark 1.3.1、環境変数 OPENAI_API_KEY、読み取り対象のSQLite DBが必要です。DBには文法で許可したテーブル(orders など)と列を用意しておきます。呼び出しは ask("出荷済みの注文IDと金額", "shop.db", OpenAI()) です。
import os
import sqlite3
from lark import Lark
from lark.exceptions import LarkError
from openai import OpenAI
SQL_GRAMMAR = r"""
start: select_stmt
select_stmt: "SELECT " columns " FROM " TABLE where? limit ";"
columns: COLUMN (", " COLUMN)*
where: " WHERE " COLUMN " " OP " " value
limit: " LIMIT " LIMIT_N
value: NUMBER | STRING
TABLE: "orders" | "customers"
COLUMN: "id" | "customer_id" | "status" | "total" | "created_at"
OP: "=" | ">" | "<" | ">=" | "<="
NUMBER: /[0-9]{1,9}/
STRING: /'[A-Za-z0-9_-]{1,32}'/
LIMIT_N: /[1-9][0-9]{0,2}/
"""
TOOLS = [{
"type": "custom",
"name": "run_readonly_sql",
"description": "注文DBに読み取り専用のSELECT文を1つ渡して実行する。文法どおりの1文だけを出力する。",
"format": {"type": "grammar", "syntax": "lark", "definition": SQL_GRAMMAR},
}]
parser = Lark(SQL_GRAMMAR, start="start") # 受け側でも同じ文法で検証する
def run_sql(sql: str, db_path: str) -> str:
try:
parser.parse(sql)
except LarkError as e:
return f"ERROR: grammar mismatch: {e.__class__.__name__}"
con = sqlite3.connect(f"file:{db_path}?mode=ro", uri=True) # 読み取り専用で開く
try:
return repr(con.execute(sql).fetchall())
except sqlite3.Error as e:
return f"ERROR: database execution failed: {e.__class__.__name__}"
finally:
con.close()
def ask(question: str, db_path: str, client: OpenAI) -> str:
model = os.environ.get("OPENAI_MODEL", "gpt-6.1-sol")
items = [{"role": "user", "content": question}]
resp = client.responses.create(model=model, input=items, tools=TOOLS)
if resp.status != "completed": # 途中で止まった応答のinputは実行しない
raise RuntimeError(f"response {resp.status}: {resp.incomplete_details}")
items += resp.output # reasoning アイテムも含めて返す
for item in resp.output:
if item.type == "custom_tool_call":
items.append({
"type": "custom_tool_call_output",
"call_id": item.call_id,
"output": run_sql(item.input, db_path),
})
final = client.responses.create(
model=model, input=items, tools=TOOLS, tool_choice="none"
)
return final.output_text
押さえる点は4つです。1つ目は、1回目の応答の output を丸ごと次の入力に足すことです。推論モデルでは reasoning アイテムも一緒に戻す必要があると公式ガイドに書かれています。2つ目は、結果を custom_tool_call_output で戻すことです。output は文字列のほか、テキスト・画像・ファイルの配列も受け付けます。3つ目は、APIに渡したのと同じ文法をPythonのLark 1.3.1にも読ませ、受け取ったSQLをもう一度構文検査してから実行していることです。4つ目は、2回目の呼び出しを tool_choice="none" にして、ツール結果を受けた最終回答だけを返させていることです。続けてツールを呼ばせたい場合は、回数に上限を付けたループに書き換えます。
APIキーを使わず、SDKのHTTP層をモックに差し替えて往復を流しました。1回目のリクエストには "format": {"type": "grammar", "syntax": "lark", ...} が送られ、2回目の入力は user→reasoning→custom_tool_call→custom_tool_call_output の順に並びました。結果の中身は [(1, 3200), (3, 800)] です。読み取り専用で開いた接続に DELETE を投げると「attempt to write a readonly database」で失敗することも確かめています。
文法が受理・拒否する文の実測
同じSQL文法をLLGuidance 1.9.1でコンパイルし、トークナイザ o200k_base で分割した文を流し込んだ結果です。
| 入力 | 結果 | 理由 |
|---|---|---|
SELECT id, total FROM orders WHERE status = 'shipped' LIMIT 50; |
受理 | 全要素が許可リスト内 |
SELECT id FROM orders; |
拒否 | LIMIT必須 |
DELETE FROM orders; |
拒否 | SELECT以外 |
SELECT id FROM users LIMIT 10; |
拒否 | 許可外のテーブル |
... WHERE status = 'x' OR '1'='1' LIMIT 10; |
拒否 | ORが文法に無い |
SELECT id FROM orders LIMIT 5000; |
拒否 | LIMITは3桁まで |
APIでは、拒否される形のトークンはそもそも生成されません。モデルは文法の範囲内で別の書き方を探すことになります。この表は「文法が意図どおりの範囲を切り出せているか」を確かめるためのもので、APIを呼ぶ前に手元で回せます。
Chat Completionsで使う場合の形の違い
custom tool はChat Completionsでも定義できますが、フィールドの入れ子が1段深く、Responses APIのコードをそのまま移すと動きません。APIリファレンスとSDKの型定義(ChatCompletionCustomToolParam)で確認した形は次のとおりです。
resp = client.chat.completions.create(
model=MODEL, # 既存コードのモデルIDのまま
messages=[{"role": "user", "content": "2026年10月2日を保存して"}],
tool_choice={"type": "custom", "custom": {"name": "save_date"}},
tools=[{
"type": "custom",
"custom": {
"name": "save_date",
"description": "YYYY-MM-DD形式の日付を1つ保存する",
"format": {
"type": "grammar",
"grammar": {"syntax": "regex", "definition": r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$"},
},
},
}],
)
call = resp.choices[0].message.tool_calls[0]
print(call.type, call.custom.input) # custom 2026-10-02
| 項目 | Responses API | Chat Completions |
|---|---|---|
| 定義の位置 | ツール直下に name と format |
custom の下に入れる |
| 文法の位置 | format.syntax format.definition |
format.grammar.syntax format.grammar.definition |
| 呼び出しの受け取り | custom_tool_call の input |
tool_calls[].custom.input |
| 結果の戻し方 | custom_tool_call_output |
role: "tool" と tool_call_id |
モデル側の対応も確認が必要です。GPT-5の公開時、custom toolの対応エンドポイントにはResponses APIとChat Completionsの両方が挙がっていました。一方、2026年10月時点のGPT-6.1 Solのモデルページでは、同モデルは「Chat Completions is supported without tool calling」、GPT-6 LunaはChat Completionsでの関数呼び出しが reasoning_effort を none にしたときに限られます。Chat Completionsで書いた既存コードは、モデルを上げた時点でツールが呼べなくなる可能性があります。新しく書くならResponses APIを選んでください。両APIの全体的な違いはOpenAI Responses APIとChat Completions APIの違いにまとめています。
GPT-5からGPT-6系への移行で確認すること
OpenAIの廃止予定一覧では、gpt-5-2025-08-07・gpt-5-mini-2025-08-07・gpt-5-nano-2025-08-07 が2026年12月11日にAPIから外れます。推奨の移行先はそれぞれ gpt-5.6-sol・gpt-5.6-terra・gpt-5.6-luna です。2026年10月時点の最新はGPT-6系(gpt-6-astra・gpt-6.1-sol・gpt-6-luna)で、公式ガイドのcustom toolとCFGの例は gpt-6-astra で書かれています。定義の形はGPT-5の頃と変わっていません。GPT-6系の料金と提供経路はGPT-6 Astraの解説を参照してください。
- モデルIDを外に出す:上のコードのように環境変数で渡しておくと、スナップショットの廃止で書き換える箇所が1つで済む。
- Chat Completionsのツール呼び出し:GPT-6.1 Solでは使えない。移行前にResponses APIへ書き換える。
- reasoning.effort の値:GPT-5の
minimalはGPT-6.1 Solでは使えない(low以上)。文法上は正しいが意味の誤った出力が続くときの対処として公式が挙げる「effortを上げる」は、GPT-6.1 Solではxhigh・maxまで選べる。 - 非同期のツール実行:GPT-6ではfunctionとcustom toolに
async: trueを付けると、ツールの実行中もモデルが他の作業を進め、結果は元のcall_idで後から返せる。
CFGで守れること・守れないこと
CFGは「形の保証」であって「正しさの保証」ではありません。上のSQL文法は DELETE も許可外のテーブルも生成させませんが、status = 'shiped' のような綴り違いや、ユーザーに見せてはいけない別顧客の注文を絞り込む条件は、文法上はどれも正しい文です。公式ガイドも、文法上は正しく意味が誤った出力を「分布外」の症状として挙げています。
したがって、CFGをSQLインジェクション対策の代わりにしてはいけません。読み取り専用の接続、行単位の権限、受け側での再パース、件数の上限は、文法とは別の層で持つべきです。許可リストで入力を絞る考え方そのものは入力バリデーションの許可リスト設計と同じで、CFGはその許可リストを生成側にも効かせる追加の層と考えるのが正確です。
CFGを使わない方がよい場面もはっきりしています。
- 引数が数個のキーと値で済む:function tool と
strict: trueの方が書きやすく、受け側もJSONとして読める。 - SQLの全構文を許したい:文法が大きくなるほど「複雑すぎる」で拒否されやすく、公式も単純な文法ほど安定すると書いている。許す構文を業務で必要な範囲に削れないなら、CFGは向かない。
- 長い自由文が主体:章立てだけを縛りたいなら、JSON Schemaで見出しと本文のフィールドを分ける構造化出力の方が扱いやすい。
よくある質問
CFGとは何の略で、どういう意味ですか?
Context-Free Grammar(文脈自由文法)の略です。各規則が前後の文脈に関係なく、記号を別の記号の並びに書き換える規則の集合で、多くのプログラミング言語の構文はこれで表せます。OpenAI APIでは、custom toolの入力が満たすべき形を決める「契約」として使います。
GPT-5でJSON出力を強制したい場合もCFGを使うべきですか?
JSONなら、CFGではなく構造化出力(text.format のJSON Schema)か、function toolの strict: true を使ってください。CFGはJSONで表しにくいSQL・DSL・数式・コードのような入力を縛るための機能です。
PythonでCFGを使うのにLarkのインストールは必要ですか?
API呼び出しだけなら不要です。文法は文字列として definition に渡すだけで、解釈はOpenAI側のLLGuidanceが行います。受け取った文字列を同じ文法で再検査したい場合に、Pythonの lark パッケージを入れます。
parallel_tool_calls を有効にすればcustom toolを並列に呼べますか?
GPT-5公開時のCookbookは、custom toolは並列呼び出しに対応しないと明記しています。現行の公式ガイドも並列呼び出しをfunctionについてだけ説明しているため、custom toolは1ターン1回の前提で設計するのが安全です。
gpt-5-mini や gpt-5-nano でもCFGは使えますか?
公開時の対応モデルは gpt-5・gpt-5-mini・gpt-5-nano の3つでした。ただし各スナップショットは2026年12月11日にAPIから外れるため、これから組むなら移行先(gpt-5.6-terra・gpt-5.6-luna)かGPT-6系で動作を確かめてください。