はじめに
本記事では、OpenAI Responses API の CFG(文法制約)機能を GPT-5 で試した実験ログと、そのために用意したツール群を紹介します。対象リポジトリは本ブログ直下のコード(llm-playground-cfg)です。
Response API の CFG Function Calling とは
従来の function calling では JSON Schema でツールの呼び出す関数の引数を定義していましたが、GPT-5 からは文法制約(CFG や正規表現)を使って、より複雑な構造を持つ出力を生成できるようになりました。これにより、特定の文法に従った出力をモデルに要求することが可能になります。
想定ユースケース:
- 四則演算の式を生成するツール
- SQL クエリの生成
- 独自 DSL のフロー定義
今回試したこと
- GPT-5 の Responses API で、カスタムツールに Lark 構文を渡す「文法制約」を試しました。
- まずは四則演算の式だけを生成するツールを作り、出力された式をローカルで Lark でパースし、安全な評価器で値を検証しました。
- CLI コマンドで単発の検証と一括スイート実行を用意し、結果を Markdown レポートに保存するようにしました。
- 複数モデル(gpt-5 / gpt-5-mini / gpt-5-nano)で同一ケースを比較し、実行時間も記録しました。
なお、今回のリポジトリの実装コードの9割以上は Codex CLI の GPT-5 が書いてくれました。API の仕様書だけ与えたら文法の定義も含めて自動生成してくれました。生成に使用したプロンプトは以下から確認できます。
四則演算の Lark 文法
今回は以下のような Lark 文法を定義しました。これは四則演算の基本的な構文を表現しています。
start: expr
?expr: term ( ("+"|"-") term )*
?term: factor ( ("*"|"/") factor )*
?factor: INT | "(" expr ")"
%import common.INT
%import common.WS_INLINE
%ignore WS_INLINE
この文法を Responses API のツール定義に組み込んで、モデルに「式だけを出して」と依頼します。
結果
セットアップ
- 依存管理: uv
- Python: 3.12
- 主要ライブラリ: openai, lark, rich, pytest
- API キー:
.env
にOPENAI_API_KEY
を設定します(値の露出は避ける)
uv sync
# .env に OPENAI_API_KEY=... を設定
export OPENAI_API_KEY=sk-...
単発実行(CLI)
# 簡単な疎通
uv run python -m cli ping
# 文法制約で式を生成→Lark でパース→安全評価→比較
uv run python -m cli cfg-math --prompt "add four plus four" --expect 8
実行例(抜粋):
╭────── CFG Math Validation ───────╮
│ Prompt add four plus four │
│ Expression 4+4 │
│ Parsed yes │
│ Value 8 │
│ Expected 8 │
│ Check pass │
╰──────────────────────────────────╯
一括スイートとレポート
複数ケースをまとめて実行し、Markdown で結果を保存します。モデルはデフォルトで gpt-5,gpt-5-mini,gpt-5-nano
の 3 つを比較。
uv run python -m cli cfg-math-suite
# or モデルを限定
uv run python -m cli cfg-math-suite --models gpt-5,gpt-5-mini
出力例(抜粋): docs/experiments/cfg-math/run-YYYYMMDD-HHMMSS.md
# | Model | Prompt | Expression | Parsed | Value | Expected | Check | Time (s) |
---|---|---|---|---|---|---|---|---|
1 | gpt-5 | add four plus four | 4+4 |
yes | 8 | 8 | pass | 10.84 |
2 | gpt-5 | seven times three plus one | 7*3+1 |
yes | 22 | 22 | pass | 15.92 |
3 | gpt-5-mini | add four plus four | 4+4 |
yes | 8 | 8 | pass | 7.87 |
4 | gpt-5-mini | seven times three plus one | 7*3+1 |
yes | 22 | 22 | pass | 5.47 |
5 | gpt-5-nano | add four plus four | 4+4 |
yes | 8 | 8 | pass | 8.20 |
6 | gpt-5-nano | seven times three plus one | 7*3+1 |
yes | 22 | 22 | pass | 8.57 |
- すべてのモデルで正しい式と値を返しました。
- 実行時間はケースやタイミングでばらつきあり。
- gpt-5 は 10-15 秒程度
- gpt-5-mini は 6-11 秒程度
- gpt-5-nano は 7-8 秒程度
詳細なレポートは以下から確認できます。
難しい問題のサンプル
より複雑な問題でも正しく式を生成できるか確認しました。
ある村にはリンゴ農家が4軒あります。
1軒目は120個、2軒目は150個のリンゴを収穫しました。
3軒目は1軒目と2軒目の合計の半分を収穫し、4軒目は3軒目より60個多く収穫しました。
村祭りの日、次のことが起こりました:
- 村全体のリンゴの総収穫数から、5分の1を来場者に配りました。
- 残りのリンゴを4軒の農家で均等に分けました。
- 均等分配されたあとの各農家の手持ちから、2箱分(1箱12個)を市場に出しました。
このとき、市場に出した後に農家の手元に残っているリンゴの合計は何個になりますか?
人間でも暗算は難しいですね。(この問題は ChatGPT の GPT-5 に作ってもらいました。)
実行すると gpt-5, gpt-5-mini, gpt-5-nano それぞれでの出力と評価を行い、結果を標準出力に表示します。
$ ./scripts/test_math.sh
╭──────────────────────────────────────────── CFG Math Validation ────────────────────────────────────────────╮
│ Prompt ある村にはリンゴ農家が4軒あります。 │
│ 1軒目は120個、2軒目は150個のリンゴを収穫しました。 │
│ 3軒目は1軒目と2軒目の合計の半分を収穫し、4軒目は3軒目より60個多く収穫しました。 │
│ 村祭りの日、次のことが起こりました: │
│ │
│ 村全体のリンゴの総収穫数から、5分の1を来場者に配りました。 │
│ 残りのリンゴを4軒の農家で均等に分けました。 │
│ 均等分配されたあとの各農家の手持ちから、2箱分(1箱12個)を市場に出しました。 │
│ このとき、市場に出した後に農家の手元に残っているリンゴの合計は何個になりますか? │
│ Expression (120+150+((120+150)/2)+(((120+150)/2)+60))-(120+150+((120+150)/2)+(((120+150)/2)+60))/5-2*12… │
│ Parsed yes │
│ Value 384 │
│ Expected 384 │
│ Check pass │
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭─────────────────────────────────────── CFG Math Validation ────────────────────────────────────────╮
│ Prompt ある村にはリンゴ農家が4軒あります。 │
│ 1軒目は120個、2軒目は150個のリンゴを収穫しました。 │
│ 3軒目は1軒目と2軒目の合計の半分を収穫し、4軒目は3軒目より60個多く収穫しました。 │
│ 村祭りの日、次のことが起こりました: │
│ │
│ 村全体のリンゴの総収穫数から、5分の1を来場者に配りました。 │
│ 残りのリンゴを4軒の農家で均等に分けました。 │
│ 均等分配されたあとの各農家の手持ちから、2箱分(1箱12個)を市場に出しました。 │
│ このとき、市場に出した後に農家の手元に残っているリンゴの合計は何個になりますか? │
│ Expression (120+150+(120+150)/2+(120+150)/2+60)-(120+150+(120+150)/2+(120+150)/2+60)/5-4*(2*12) │
│ Parsed yes │
│ Value 384 │
│ Expected 384 │
│ Check pass │
╰────────────────────────────────────────────────────────────────────────────────────────────────────╯
╭───────────────────────────────────── CFG Math Validation ──────────────────────────────────────╮
│ Prompt ある村にはリンゴ農家が4軒あります。 │
│ 1軒目は120個、2軒目は150個のリンゴを収穫しました。 │
│ 3軒目は1軒目と2軒目の合計の半分を収穫し、4軒目は3軒目より60個多く収穫しました。 │
│ 村祭りの日、次のことが起こりました: │
│ │
│ 村全体のリンゴの総収穫数から、5分の1を来場者に配りました。 │
│ 残りのリンゴを4軒の農家で均等に分けました。 │
│ 均等分配されたあとの各農家の手持ちから、2箱分(1箱12個)を市場に出しました。 │
│ このとき、市場に出した後に農家の手元に残っているリンゴの合計は何個になりますか? │
│ Expression (120 + 150 + (120 + 150)/2 + (120 + 150)/2 + 60) * 4 / 5 - 96 │
│ Parsed yes │
│ Value 384 │
│ Expected 384 │
│ Check pass │
╰────────────────────────────────────────────────────────────────────────────────────────────────╯
このように、複雑な文脈でも正しい式を生成し、パース・評価できることを確認しました。
mini/nano モデルでも同様に成功しています。
今後の予定
- CFG を用いた他ドメインの検証
- SQL(最小サブセット)
- KQL(最小サブセット)
- OData($filter サブセット)
- 独自 DSL(Mini Flow DSL)
- 正規表現(住所・アクセスログ)の制約生成
- スイートの拡張(難問・異常系、モデル別の比較指標追加、総時間/平均時間の集計)
以上、GPT-5 の CFG function calling を四則演算で試した第 1 回でした。次回以降は SQL/KQL/OData/DSL など、より実務寄りの文法制約に踏み込みます。
Part2: