Closed9

メモ: GPT-5の新しいパラメータとツール

kun432kun432
kun432kun432

https://cookbook.openai.com/examples/gpt-5/gpt-5_new_params_and_tools

Diaによる、まず全体の要約

ウケる、めっちゃ新機能盛りだくさんじゃん!GPT-5の新しいパラメータとツールについて、テンション上げて説明するね!

ざっくり言うと…

GPT-5は、開発者がモデルの返答を細かくコントロールできる新しいパラメータやツールが追加されたんだよ。出力の長さやスタイル、フォーマットまで自由自在って感じ!

主な新機能まとめ

  1. Verbosity Parameter(冗長度パラメータ)
    • モデルの返答の長さを「low(短め)」「medium(普通)」「high(長め)」で調整できる!
    • 例えば詩やコードの説明を、サクッと短くもできるし、めっちゃ詳しくもできる。
    • プロンプトを何回も書き直さなくていいから、超ラク。
  2. Free-Form Function Calling(自由形式の関数呼び出し)
    • PythonやSQL、シェルコマンドとかを生のテキストでツールに渡せる!
    • JSONラップしなくていいから、自然なコードやクエリをそのまま実行できるのがウケる。
  3. Context-Free Grammar (CFG)(文脈自由文法)
    • モデルの出力を、指定した文法(プログラミング言語とか)に絶対合わせるようにできる!
    • 例えばSQLの方言(MS SQLとPostgreSQL)ごとに文法を分けて、間違った構文を出さないようにできるんだよ。マジで便利。
  4. Minimal Reasoning(最小限の推論)
    • 推論トークンをほぼ使わず、超高速で返答できるモード!
    • 例えば「このレビューの感情は?→positive」みたいな、説明不要なタスクに最適。
    • 速度重視のときはこれ一択でしょ。

使い方のイメージ

  • 冗長度パラメータで、返答の長さを一発で調整できるから、用途に合わせてサクサク切り替えられる。
  • コード生成やSQLクエリみたいな、フォーマット厳守の出力もCFGでガッチリ制御できる。
  • ツール呼び出しも、ラップなしで生テキスト渡せるから、外部環境との連携が超スムーズ。
  • 最小限推論は、分類や抽出みたいな「説明いらん」系のタスクで爆速。

まとめ

開発者が「こうしてほしい!」って思った通りに、GPT-5の返答を細かくコントロールできるようになったってこと!マジでノリがいいし、使い勝手爆上がりだし、ウチもテンション上がるわ~。

kun432kun432

目次

はじめに

事前準備

1. 冗長度パラメータ(Verbosity Parameter)

  • 1.1 概要
  • 1.2 コーディング用途での冗長度パラメータの使い方
  • 1.3 まとめ

2. 自由形式の関数呼び出し(Free‑Form Function Calling)

  • 2.1 概要
  • 2.2 クイックスタート例 – 円の面積を計算する
  • 2.3 ミニベンチマーク – 3言語で配列をソート
  • 2.4 まとめ

3. 文脈自由文法(CFG:Context‑Free Grammar)

  • 3.1 概要
  • 3.2 文法の基本
  • 3.3 例 – SQL方言:MS SQL vs PostgreSQL
  • 3.4 特定のSQL方言を生成する
  • 3.5 例 – 正規表現CFG構文
  • 3.5 ベストプラクティス
  • 3.6 まとめ

4. 最小限の推論(Minimal Reasoning)

  • 4.1 概要
  • 4.2 まとめ
kun432kun432

はじめに

  • GPT-5シリーズでは、モデルの応答をより細かく制御できる新しい開発者向けコントロールを導入した。
  • 出力の長さやスタイルの調整、厳密なフォーマットの強制まで、より自由度の高いコントロールが可能

最新機能の概要

No 機能 概要 値 / 使用方法
1 冗長度パラメータ(Verbosity Parameter) モデルの返答をより簡潔または詳細にするようにヒントを与えることができる。プロンプトを書き直す代わりに、このパラメータを使って安定した出力を維持できる。 - low: 簡潔なUX、最小限の文章。
- medium(デフォルト): バランスの取れた詳細。
- high: 冗長で、監査や教育、引き継ぎに最適。
2 自由形式の関数呼び出し(Free-Form Function Calling) PythonスクリプトからSQLクエリまで、あらゆる生テキストのペイロードをJSONラップなしでカスタムツールに直接生成できる。外部ランタイムとの連携がより柔軟になる。例:
- コードサンドボックス(Python、C++、Java、…)
- SQLデータベース
- シェル環境
- コンフィグジェネレーター
構造化されたJSONが不要で、対象ツールにとってプレーンテキストの方が自然な場合に使用。
3 文脈自由文法(CFG: Context-Free Grammar) 言語内で有効な文字列を定義する一連の生成規則。各規則は非終端記号を終端記号や他の非終端記号に書き換えるが、周囲の文脈には依存しない。OpenAIツールで、プログラミング言語やカスタムフォーマットの構文に出力を制約するのに有用。 モデルが文法で受け入れられる有効な文字列のみを出力するよう契約として使用。
4 最小限の推論(Minimal Reasoning) GPT-5をほとんど推論トークンを使わずに実行し、レイテンシを最小化し、最初のトークンの表示までの時間を短縮する。説明が不要な決定的で軽量なタスク(抽出、フォーマット、短い書き換え、単純な分類)に最適。指定がない場合、推論の努力はデフォルトでmediumになる。 reasoning_effortminimal に設定。複数ステップの計画やツールを多用するワークフローには避けるべき。

対応モデル

  • gpt-5
  • gpt-5-mini
  • gpt-5-nano

対応APIエンドポイント

  • Responses API
  • Chat Completions API

注: GPT-5シリーズのモデルで最高のパフォーマンスを得るためには、Responses APIの使用を推奨。

kun432kun432

事前準備

OpenAIのCookbookはGitHub上でipynbとして公開されているので、Colaboratory等で実行することができる。

https://github.com/openai/openai-cookbook/tree/main/examples/gpt-5/gpt-5_new_params_and_tools.ipynb

以下リンクでColaboratoryで開ける。自分のノートブックとして複製して使用することをオススメ。

https://colab.research.google.com/github/openai/openai-cookbook/blob/main/examples/gpt-5/gpt-5_new_params_and_tools.ipynb

以後はColaboratoty上で実行。

OpenAI SDKをインストール。pandasも併せてインストールされる。

!pip install --quiet --upgrade openai pandas && \
echo -n "openai " && pip show openai | grep '^Version:' | cut -d' ' -f2 && \
echo -n "pandas " && pip show pandas | grep '^Version:' | cut -d' ' -f2
出力
openai 1.99.4
pandas 2.3.1

OpenAIのAPIキーを環境変数にセットする。ColaboratoryのシークレットにOpenAI APIキーを登録しておけば、以下のようにして読み込める。

from google.colab import userdata
import os

os.environ['OPENAI_API_KEY'] = userdata.get('OPENAI_API_KEY')
kun432kun432

1. 冗長度パラメータ(Verbosity Parameter)

1.1 概要

冗長度(verbosity)パラメータを使えば、返答の長さや詳細さについてのヒントをモデルに与える。

設定可能な値は以下:

  • low: 簡潔なUX、最小限の文章。サクッと短くまとめたいとき。
  • medium(デフォルト): バランスの取れた詳細。通常はこれで十分。
  • high: 冗長で、監査・教育・引き継ぎなどに最適。細かい説明が欲しいとき。

プロンプトは書き換えず固定にして、verbosityパラメータを変えて違いを見るサンプルコード。Responses APIが使用されている。

from openai import OpenAI
import pandas as pd
from IPython.display import display

client = OpenAI()

question = "少年と彼の初めてのペットの犬について詩を書いて。"

data = []

for verbosity in ["low", "medium", "high"]:
    response = client.responses.create(
        model="gpt-5-mini",
        input=question,
        text={"verbosity": verbosity}
    )

    # テキストを抽出
    # 注: ドキュメントどおりだとエラーになるので修正
    #     (response.output_textを使うほうがいいと思う)
    output_text = ""
    for item in response.output:
        if hasattr(item, "content") and item.content is not None:
            for content in item.content:
                if hasattr(content, "text") and content.text is not None:
                    for text in content.text:
                        output_text += text

    usage = response.usage
    data.append({
        "verbosity": verbosity,
        "サンプル出力": output_text,
        "出力トークン": usage.output_tokens
    })

# データフレームを作成
df = pd.DataFrame(data)

# ヘッダーを中央寄せして表示
pd.set_option('display.max_colwidth', None)
styled_df = df.style.set_table_styles(
    [
        {'selector': 'th', 'props': [('text-align', 'center')]},  # 絡むヘッダを中央寄せ
        {'selector': 'td', 'props': [('text-align', 'left')]}     # テーブルセルを左寄せ
    ]
)

display(styled_df)

verbosityパラメータが高くになるにつれて、出力量も増えているのがわかる。

1.2 コーディング用途での冗長度パラメータの使い方

冗長度パラメータは、生成されるコードの長さや複雑性、また付随する説明の深さなどに影響する。
例えば、「100万個のランダムな数字のリストをソートするPythonプログラムを生成して。」というプロンプトに対して、異なる冗長度パラメータをセットしてみる。

from openai import OpenAI

client = OpenAI()

prompt = "100万個のランダムな数字のリストをソートするPythonプログラムを生成して。"

def ask_with_verbosity(verbosity: str, question: str):
    response = client.responses.create(
        model="gpt-5-mini",
        input=question,
        text={
            "verbosity": verbosity
        }
    )

    # アシスタントのテキスト生成を抽出
    output_text = ""
    for item in response.output:
        if hasattr(item, "content") and item.content is not None:
            for content in item.content:
                if hasattr(content, "text") and content.text is not None:
                    for text in content.text:
                        output_text += text

    # トークン使用状況の詳細
    usage = response.usage

    print("--------------------------------")
    print(f"Verbosity: {verbosity}")
    print("Output:")
    print(output_text)
    print("Tokens => input: {} | output: {}".format(
        usage.input_tokens, usage.output_tokens
    ))

# 使い方:
ask_with_verbosity("low", prompt)

low の場合

ask_with_verbosity("low", prompt)
出力
--------------------------------
Verbosity: low
Output:
以下は100万個のランダムな数を生成してソートする簡単なPythonプログラムです。

```python
import random
import time

N = 1_000_000
random.seed(0)

# ランダムな浮動小数点数を生成
data = [random.random() for _ in range(N)]

# ソート(インプレース)
start = time.perf_counter()
data.sort()
elapsed = time.perf_counter() - start

print(f"Sorted {N} numbers in {elapsed:.3f} seconds")
print("First 10:", data[:10])
```

実行: python3 your_script.py
Tokens => input: 30 | output: 594

medium の場合

ask_with_verbosity("medium", prompt)
出力
--------------------------------
Verbosity: medium
Output:
以下は、100万個のランダムな数を生成してソートするシンプルな Python プログラム例です(組み込みの list.sort() を使用)。実行時間も計測します。

1) 純粋な Python(random + list.sort)
```python
import random
import time

def main():
    N = 1_000_000
    random.seed(42)  # 再現性が欲しい場合に設定
    print("生成中...")
    arr = [random.random() for _ in range(N)]  # 0.0〜1.0 の浮動小数点
    print("ソート開始...")
    t0 = time.perf_counter()
    arr.sort()  # Timsort(O(n log n))
    t1 = time.perf_counter()
    print(f"ソート完了: {t1 - t0:.4f} 秒")
    print("最小10:", arr[:10])
    print("最大10:", arr[-10:])

if __name__ == "__main__":
    main()
```

2) より高速にしたい場合(NumPy を使うとメモリと速度の点で有利になることがあります)
```python
import numpy as np
import time

def main():
    N = 1_000_000
    rng = np.random.default_rng(42)
    print("生成中...")
    arr = rng.random(N)  # NumPy 配列(float64)
    print("ソート開始...")
    t0 = time.perf_counter()
    arr.sort()  # NumPy の in-place ソート(Quicksort など)
    t1 = time.perf_counter()
    print(f"ソート完了: {t1 - t0:.4f} 秒")
    print("最小10:", arr[:10])
    print("最大10:", arr[-10:])

if __name__ == "__main__":
    main()
```

注意点:
- Python の float はオブジェクトなので list にするとオーバーヘッドが大きくなります(1,000,000 要素でも数十MB程度消費)。NumPy の場合はメモリ効率が良いです。
- 真に巨大なデータ(メモリに乗らない場合)は外部ソート(チャンクに分けて部分ソート→マージ)やデータベース/分散処理を検討してください。
Tokens => input: 30 | output: 1241

high の場合

ask_with_verbosity("high", prompt)
出力
--------------------------------
Verbosity: high
Output:
以下は「100万個のランダムな数字のリストをソートするPythonプログラム」です。まずは最もシンプルで実用的な方法(組み込みの list.sort() を使う)を示します。その後、時間計測や NumPy を使った高速生成・ソート、さらにデータがメモリに乗らない場合の外部ソート(外部マージソート)のサンプルも説明します。

注意:
- Pythonの list.sort() は Timsort(安定で高速)で、1,000,000 個程度の要素なら普通にメモリに収まります(目安:Python の int は各オブジェクトで約28 bytes、ポインタが 8 bytes/要素のオーバーヘッド等あり、合計数十MB程度)。環境によって時間は変わります(数百ミリ秒〜数秒程度が多い)。
- 大きなデータ(メモリに乗らない)場合は後半の外部ソートの方法を使ってください。

1) シンプルなプログラム(組み込み sort)
以下をファイル(例: sort_one_million.py)として保存して実行してください。

# sort_one_million.py
import random
import time

def main():
    N = 1_000_000  # 100万
    seed = 12345   # 再現性が欲しい場合はシードを設定(任意)
    random.seed(seed)

    print(f"Generating {N:,} random floats...")
    t0 = time.perf_counter()
    data = [random.random() for _ in range(N)]  # 0.0 <= x < 1.0 の float
    t1 = time.perf_counter()
    print(f"Generation done in {t1 - t0:.3f} s")

    print("Sorting ...")
    t0 = time.perf_counter()
    data.sort()  # インプレースでソート(Timsort)
    t1 = time.perf_counter()
    print(f"Sorted {N:,} items in {t1 - t0:.3f} s")

    # 簡単な検証(O(N))
    ok = all(data[i] <= data[i+1] for i in range(len(data)-1))
    print("Verified sorted:" , ok)
    print("Min:", data[0], "Max:", data[-1])

if __name__ == "__main__":
    main()

実行例:
$ python3 sort_one_million.py
Generating 1,000,000 random floats...
Generation done in 0.30 s
Sorting ...
Sorted 1,000,000 items in 0.80 s
Verified sorted: True
Min: 1.617... Max: 0.999...

(時間は環境に依存します)

2) NumPy を使う場合(大量の乱数生成で速く、ソートも高速)
NumPy がインストールされていると、大量の乱数生成は速く、配列ベースのソート(numpy.sort)はメモリ効率が良いことがあります。NumPy をインストールしていれば次のようにできます。

# sort_with_numpy.py
import time
try:
    import numpy as np
except ImportError:
    raise SystemExit("NumPy が必要です: pip install numpy")

def main():
    N = 1_000_000
    print(f"Generating {N:,} random floats with NumPy...")
    t0 = time.perf_counter()
    a = np.random.random(N)  # ndarray of floats
    t1 = time.perf_counter()
    print(f"Generation done in {t1 - t0:.3f} s")

    print("Sorting with numpy.sort() ...")
    t0 = time.perf_counter()
    b = np.sort(a)  # returns sorted copy
    t1 = time.perf_counter()
    print(f"Sorted {N:,} items in {t1 - t0:.3f} s")

    # 簡単な検証
    print("Min:", b[0], "Max:", b[-1])
    print("Is sorted (sample check):", (b[:-1] <= b[1:]).all())

if __name__ == "__main__":
    main()

3) メモリに乗らない場合:外部マージソート(chunkごとにソートしてマージ)
もしデータが非常に大きくメモリに乗らない場合は、次のような外部ソート戦略が有効です:
- 元データをチャンク(例えば 100k 行ずつ)に分ける。
- 各チャンクをメモリ内でソートして一時ファイルに書く。
- 最後に heapq.merge を使って複数ソート済みファイルをマージ(ストリームマージ)する。

簡易サンプル(整数をテキスト行に保存する想定):

# external_sort_example.py
import random
import os
import tempfile
import heapq

def write_sorted_chunk(numbers, dirpath):
    numbers.sort()
    fd, path = tempfile.mkstemp(dir=dirpath, text=True)
    os.close(fd)
    with open(path, "w") as f:
        for n in numbers:
            f.write(f"{n}\n")
    return path

def iter_file_numbers(path):
    with open(path) as f:
        for line in f:
            yield int(line.strip())

def external_sort_generate_and_sort(total=10**6, chunk_size=100_000):
    # 例: total が非常に大きい場合にチャンクごとにソートして一時ファイルへ
    tmpdir = tempfile.mkdtemp()
    chunk_paths = []
    remaining = total
    while remaining > 0:
        size = min(chunk_size, remaining)
        chunk = [random.randint(0, 10**9) for _ in range(size)]
        path = write_sorted_chunk(chunk, tmpdir)
        chunk_paths.append(path)
        remaining -= size
        print(f"Written chunk {path}, {size} items")
    # マージして最終出力(ここでは stdout に書くかわりにファイルに)
    out_path = os.path.join(tmpdir, "merged.txt")
    with open(out_path, "w") as out_f:
        iterators = [iter_file_numbers(p) for p in chunk_paths]
        for val in heapq.merge(*iterators):
            out_f.write(f"{val}\n")
    print("Merged into", out_path)
    # 後片付け(必要であれば)
    # for p in chunk_paths: os.remove(p)
    # os.remove(out_path)
    # os.rmdir(tmpdir)

if __name__ == "__main__":
    external_sort_generate_and_sort(total=200_000, chunk_size=50_000)
    # 上の total/ chunk_size を変更してテストしてください。

外部ソートはディスクI/Oが支配的なので、チャンクサイズの調整や一時ファイルの配置(SSDが速い)などでパフォーマンスが変わります。

補足とアドバイス
- list.sort() はほとんどの場合最速で簡単なので、まずはこれを使うのが良いです。
- 大量生成がボトルネックなら NumPy を検討してください(生成が圧倒的に速い)。
- データが本当に大きくメモリに乗らない場合は外部ソート/データベース/分散処理(Spark 等)を検討してください。
- ソートの時間計測は time.perf_counter() を使うと良いです。
- メモリ使用量に敏感なら、整数でも Python の int はオーバーヘッドが大きいので、numpy の int32/float32 などを使うとメモリ効率が上がります。

必要なら、用途に合わせて(整数か浮動小数点か、メモリに収まるか否か、速度重視かメモリ重視か)サンプルを調整してファイルを作成します。どの形式(int/float)、どの範囲/分布の乱数を使いたいか、また実行環境(メモリ量、NumPyの有無)を教えてください。最適化したスクリプトを用意します。
Tokens => input: 30 | output: 2959

1.3 まとめ

新しい冗長度パラメータは、プロンプト自体を変更せずに、出力の長さと深さの両方を確実にスケールさせることができる。 これにより、正確さや推論の質を維持しながら、返答のボリュームを調整できる。

kun432kun432

2. 自由形式の関数呼び出し(Free‑Form Function Calling)

2.1 概要

GPT‑5は、PythonスクリプトやSQLクエリなど、あらゆるプレーンテキストのペイロードをカスタムツールに直接送信できる「自由形式の関数呼び出し(Free-Form Function Calling)」をサポートしている。

新しいツールタイプ "type": "custom"を使えば、データをJSONでラップすることなく、そのまま外部ランタイムに渡すことができ、より柔軟な連携が可能になる。

  • コードサンドボックス(Python、C++、Javaなど)
  • SQLデータベース
  • シェル環境
  • コンフィグジェネレーター

注意: カスタムツールタイプでは、並列ツール呼び出しをサポートしていない。

2.2 クイックスタートのサンプル ― 円の面積を計算する

円の面積を計算するシンプルなPythonコードを生成するコード。このコードでは、モデルは自由形式のツール呼び出しを使って結果を出力する。

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-5-mini",
    input="code_exec ツールを使用して、'strawberry' に含まれる 'r' の文字数に等しい半径を持つ円の面積を計算して。",
    text={"format": {"type": "text"}},
    tools=[
        {
            "type": "custom",
            "name": "code_exec",
            "description": "任意のPythonコードを実行する",
        }
    ]
)
print(response.output)

出力結果。読みやすさのために適宜改行を入れている。

[
    ResponseReasoningItem(
        id='rs_689616fc19fc8194b56d1ef99d297018088abb66addf7573',
        summary=[],
        type='reasoning',
        content=None,
        encrypted_content=None,
        status=None
    ),
    ResponseCustomToolCall(
        call_id='call_vQTTLJN8XC46qZrK4LRSZmMD',
        input="# Python code to count 'r' in 'strawberry' and compute area of circle with that radius\nimport math\ns = 'strawberry'\nr = s.count('r')\narea = math.pi * (r ** 2)\nr, area\n",
        name='code_exec',
        type='custom_tool_call',
        id='ctc_689616fea70c8194905f39220d53043c088abb66addf7573',
        status='completed'
    )
]

モデルは、生のPythonコードを含むtool callを出力する。あとはこのコードをサーバー側で実行して、表示された結果を次の `responses.create コールで返せばよい。

2.3 ミニベンチマーク ― 3言語で配列をソート

自由形式のツール呼び出しの使い方を示すために、GPT‑5に以下のことを投げてみる:

  • 固定された配列を10回ソートするPython、C++、Javaのコードを生成する。
  • 各イテレーションでかかった時間(ミリ秒単位)だけをコード内で出力する。
  • 3つの関数すべてを呼び出したら、そこで終了する。
from openai import OpenAI
from typing import List, Optional

MODEL_NAME = "gpt-5"

# モデルの実行ごとに渡されるツール
# これらのツールを一度だけ定義すれば、設定を一箇所に集中させることができる
TOOLS = [
    {
        "type": "custom",
        "name": "code_exec_python",
        "description": "Pythonコードを実行する。",
    },
    {
        "type": "custom",
        "name": "code_exec_cpp",
        "description": "C++コードを実行する。",
    },
    {
        "type": "custom",
        "name": "code_exec_java",
        "description": "Javaコードを実行する。",
    },
]

client = OpenAI()

def create_response(
    input_messages: List[dict],
    previous_response_id: Optional[str] = None,
):
    """ ``client.responses.create``のラッパー関数。

    Parameters
    ----------
    input_messages: List[dict]
        モデルに与えられる実行中の会話履歴。
    previous_response_id: str | None
        *前回*の呼び出し時の ``response.id`` を渡して、モデルが会話のスレッドを保持できるようにする。
        最初のリクエストでは省略可能。
    """
    kwargs = {
        "model": MODEL_NAME,
        "input": input_messages,
        "text": {"format": {"type": "text"}},
        "tools": TOOLS,
    }
    if previous_response_id:
        kwargs["previous_response_id"] = previous_response_id

    return client.responses.create(**kwargs)

# 再帰
def run_conversation(
    input_messages: List[dict],
    previous_response_id: Optional[str] = None,
):

    response = create_response(input_messages, previous_response_id)

    # ``response.output`` は、要素 0 がモデルメッセージとなっているリストであることが期待される。
    # 要素 1(存在する場合)はツール呼び出しを表す。モデルがツール呼び出しを完了すると、その要素は省略される。
    tool_call = response.output[1] if len(response.output) > 1 else None

    if tool_call and tool_call.type == "custom_tool_call":
        print("--- ツール名 ---")
        print(tool_call.name)
        print("--- ツール呼び出し引数(生成されたコード) ---")
        print(tool_call.input)

        # モデルがスレッドを継続できるように、合成された「ツール結果」を追加する。
        
        input_messages.append(
            {
                "type": "function_call_output",
                "call_id": tool_call.call_id,
                "output": "done", # <-- ツール呼び出しの実行結果で置き換える
            }
        )

        # 更新された会話で再帰処理を行い、応答IDを追跡してモデルが前のターンを認識できる
        # ようにする。
        return run_conversation(input_messages, previous_response_id=response.id)
    else:
        # 基本ケース: 追加のツール呼び出しなし - return
        return


prompt = """
数値の配列を10回ソートするコードを、3つの言語(C++、Python、Java)を使って、code_exec 関数群を使用して作成してください。

**必ず次の3つの関数を正確に1回ずつ呼び出してください**: code_exec_python code_exec_cpp および code_exec_java
これらの関数は、各言語で配列をソートするためのツールである。各言語でこれらの3つの関数を1回ずつ呼び出したら、処理を停止してください。

配列のソートに要した時間をミリ秒単位で表示してください

[448, 986, 255, 884, 632, 623, 246, 439, 936, 925, 644, 159, 777, 986, 706, 723, 534, 862, 195, 686, 846, 880, 970, 276, 613, 736, 329, 622, 870, 284, 945, 708, 267, 327, 678, 807, 687, 890, 907, 645, 364, 333, 385, 262, 730, 603, 945, 358, 923, 930, 761, 504, 870, 561, 517, 928, 994, 949, 233, 137, 670, 555, 149, 870, 997, 809, 180, 498, 914, 508, 411, 378, 394, 368, 766, 486, 757, 319, 338, 159, 585, 934, 654, 194, 542, 188, 934, 163, 889, 736, 792, 737, 667, 772, 198, 971, 459, 402, 989, 949]
"""

# 初期の開発者メッセージ
messages = [
    {
        "role": "developer",
        "content": prompt,
    }
]

run_conversation(messages)

結果

出力
--- ツール名 ---
code_exec_python
--- ツール呼び出し引数(生成されたコード) ---
# Python: Sort the array 10 times and measure the elapsed time in milliseconds
import time

arr = [448, 986, 255, 884, 632, 623, 246, 439, 936, 925, 644, 159, 777, 986, 706, 723, 534, 862, 195, 686, 846, 880, 970, 276, 613, 736, 329, 622, 870, 284, 945, 708, 267, 327, 678, 807, 687, 890, 907, 645, 364, 333, 385, 262, 730, 603, 945, 358, 923, 930, 761, 504, 870, 561, 517, 928, 994, 949, 233, 137, 670, 555, 149, 870, 997, 809, 180, 498, 914, 508, 411, 378, 394, 368, 766, 486, 757, 319, 338, 159, 585, 934, 654, 194, 542, 188, 934, 163, 889, 736, 792, 737, 667, 772, 198, 971, 459, 402, 989, 949]

start = time.perf_counter()
for _ in range(10):
    v = arr[:]  # copy
    v.sort()
end = time.perf_counter()

ms = (end - start) * 1000.0
print(f"Python: {ms:.3f} ms")
--- ツール名 ---
code_exec_cpp
--- ツール呼び出し引数(生成されたコード) ---
// C++: Sort the array 10 times and measure the elapsed time in milliseconds
#include <iostream>
#include <vector>
#include <algorithm>
#include <chrono>

int main() {
    std::vector<int> arr = {448, 986, 255, 884, 632, 623, 246, 439, 936, 925, 644, 159, 777, 986, 706, 723, 534, 862, 195, 686, 846, 880, 970, 276, 613, 736, 329, 622, 870, 284, 945, 708, 267, 327, 678, 807, 687, 890, 907, 645, 364, 333, 385, 262, 730, 603, 945, 358, 923, 930, 761, 504, 870, 561, 517, 928, 994, 949, 233, 137, 670, 555, 149, 870, 997, 809, 180, 498, 914, 508, 411, 378, 394, 368, 766, 486, 757, 319, 338, 159, 585, 934, 654, 194, 542, 188, 934, 163, 889, 736, 792, 737, 667, 772, 198, 971, 459, 402, 989, 949};

    auto start = std::chrono::steady_clock::now();
    for (int i = 0; i < 10; ++i) {
        auto v = arr; // copy
        std::sort(v.begin(), v.end());
    }
    auto end = std::chrono::steady_clock::now();

    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
    std::cout << "C++: " << ms << " ms" << std::endl;
    return 0;
}
--- ツール名 ---
code_exec_java
--- ツール呼び出し引数(生成されたコード) ---
// Java: Sort the array 10 times and measure the elapsed time in milliseconds
import java.util.Arrays;

public class Main {
    public static void main(String[] args) {
        int[] arr = new int[]{448, 986, 255, 884, 632, 623, 246, 439, 936, 925, 644, 159, 777, 986, 706, 723, 534, 862, 195, 686, 846, 880, 970, 276, 613, 736, 329, 622, 870, 284, 945, 708, 267, 327, 678, 807, 687, 890, 907, 645, 364, 333, 385, 262, 730, 603, 945, 358, 923, 930, 761, 504, 870, 561, 517, 928, 994, 949, 233, 137, 670, 555, 149, 870, 997, 809, 180, 498, 914, 508, 411, 378, 394, 368, 766, 486, 757, 319, 338, 159, 585, 934, 654, 194, 542, 188, 934, 163, 889, 736, 792, 737, 667, 772, 198, 971, 459, 402, 989, 949};

        long start = System.nanoTime();
        for (int i = 0; i < 10; i++) {
            int[] v = Arrays.copyOf(arr, arr.length); // copy
            Arrays.sort(v);
        }
        long end = System.nanoTime();

        long ms = (end - start) / 1_000_000L;
        System.out.println("Java: " + ms + " ms");
    }
}

モデルは、同じアルゴリズムについて、Python、C++、Javaの3つのコードブロックを出力している。
関数呼び出しの出力は、モデルへの入力として連鎖的に渡され、すべての関数が一度ずつ呼び出されるまでモデルが処理を続けられるようになっている。

2.4 まとめ

GPT-5の自由形式ツール呼び出し(Free-form tool calling)を使うと、PythonスクリプトやSQLクエリ、設定ファイルなどの生テキストのペイロードを、JSONでラップせずにカスタムツールへ直接送信できる。 これにより、外部ランタイムとの連携がより柔軟になり、ツールが期待する正確なフォーマットでコードやテキストを生成することが可能になる。構造化されたJSONが不要で、自然なテキスト出力の方が使いやすい場合に最適。

kun432kun432

3. 文脈自由文法(CFG:Context‑Free Grammar)

3.1 概要

Diaでまずフラットに翻訳してもらった

文脈自由文法(Context‑Free Grammar, CFG)は、ある言語に属する文字列を定義する生成規則の集合。各規則は、非終端記号を終端記号(リテラルなトークン)や他の非終端記号の並びに書き換えますが、周囲の文脈には依存しない。このため「文脈自由」と呼ばれる。CFGは、ほとんどのプログラミング言語の構文を表現でき、OpenAIのカスタムツールでは、モデルが文法で許可された文字列だけを出力するように強制する制約として機能する。

最後の部分ね。現時点で自分は、出力ルールを構文で縛るもの、Structured Outputのより自由で制御性が高いもの、という風に認識した。実際に使いながら見ていくのが良さそう。

3.2 文法の基本

対応している文法構文

モデルのサンプリングを制約するために、内部的には LLGuidance を使用している。

サポートしていないLarkの機能

  • 正規表現での検索範囲の指定((?=...)(?!...)、など)
  • 正規表現における非貪欲修飾子(*?+???
  • 終端記号の優先順位、テンプレート、%declares、%import(%import commonを除く)。

終端記号(Terminals)と規則(Rules)、そして貪欲な字句解析(Greedy Lexing)**

概念 ポイント
終端記号(UPPER) 字句解析器(lexer)が最初にマッチさせる。最長一致が優先される。
規則(lower) 終端記号を組み合わせる。テキストのトークン化の仕方には影響を与えられない。
貪欲な字句解析 複数の終端記号にまたがる自由なテキストを「形作ろう」としないこと。コントロールを失う原因になる。

正しいパターン設計と間違ったパターン設計の例

1つの終端記号で、アンカー(目印)間の自由テキストをまとめて扱う

start: SENTENCE
SENTENCE: /[A-Za-z, ](the hero|a dragon)[A-Za-z, ](fought|saved)[A-Za-z, ](a treasure|the kingdom)[A-Za-z, ]./

自由テキストを複数の終端記号や規則に分割しようとする

start: sentence
sentence: /[A-Za-z, ]+/ subject /[A-Za-z, ]+/ verb /[A-Za-z, ]+/ object /[A-Za-z, ]+/

3.3 例 ― SQL方言:MS SQL vs PostgreSQL

CFGを使って複数のSQL方言ツールを構築するための標準的なリファレンスとして、サンプルコードが記載されている。このコードでは以下を示している:

  • TOP句とLIMIT句の意味を持つ、2つの独立した文法(mssql_grammar_definitionpostgres_grammar_definition)を定義
  • 1つのスクリプトで、プロンプト作成・ツール呼び出し・結果の検証を行う。
  • アシスタントの応答を並べて比較する方法

異なるSQL方言ごとにLARK文法を定義

import textwrap

# ----------------- MS SQL 方言の文法 -----------------
mssql_grammar = textwrap.dedent(r"""
            // ---------- Punctuation & operators ----------
            SP: " "
            COMMA: ","
            GT: ">"
            EQ: "="
            SEMI: ";"

            // ---------- Start ----------
            start: "SELECT" SP "TOP" SP NUMBER SP select_list SP "FROM" SP table SP "WHERE" SP amount_filter SP "AND" SP date_filter SP "ORDER" SP "BY" SP sort_cols SEMI

            // ---------- Projections ----------
            select_list: column (COMMA SP column)*
            column: IDENTIFIER

            // ---------- Tables ----------
            table: IDENTIFIER

            // ---------- Filters ----------
            amount_filter: "total_amount" SP GT SP NUMBER
            date_filter: "order_date" SP GT SP DATE

            // ---------- Sorting ----------
            sort_cols: "order_date" SP "DESC"

            // ---------- Terminals ----------
            IDENTIFIER: /[A-Za-z_][A-Za-z0-9_]*/
            NUMBER: /[0-9]+/
            DATE: /'[0-9]{4}-[0-9]{2}-[0-9]{2}'/
    """)

# ----------------- PostgreSQL 方言の文法 -----------------
postgres_grammar = textwrap.dedent(r"""
            // ---------- Punctuation & operators ----------
            SP: " "
            COMMA: ","
            GT: ">"
            EQ: "="
            SEMI: ";"

            // ---------- Start ----------
            start: "SELECT" SP select_list SP "FROM" SP table SP "WHERE" SP amount_filter SP "AND" SP date_filter SP "ORDER" SP "BY" SP sort_cols SP "LIMIT" SP NUMBER SEMI

            // ---------- Projections ----------
            select_list: column (COMMA SP column)*
            column: IDENTIFIER

            // ---------- Tables ----------
            table: IDENTIFIER

            // ---------- Filters ----------
            amount_filter: "total_amount" SP GT SP NUMBER
            date_filter: "order_date" SP GT SP DATE

            // ---------- Sorting ----------
            sort_cols: "order_date" SP "DESC"

            // ---------- Terminals ----------
            IDENTIFIER: /[A-Za-z_][A-Za-z0-9_]*/
            NUMBER: /[0-9]+/
            DATE: /'[0-9]{4}-[0-9]{2}-[0-9]{2}'/
    """)

3.4 特定のSQL方言を生成する

プロンプトを定義、関数を呼び出して、MS SQL方言のクエリを生成してみる。

from openai import OpenAI

client = OpenAI()

sql_prompt_mssql = (
    "mssql_grammarを使って、Microsoft SQL Server用のクエリを生成してください。"
    "各顧客ごとに最新の注文5件を取得し、customer_id、order_id、order_date、total_amount を表示する。"
    "total_amount が500より大きく、order_date が '2025-01-01' 以降のものを対象とする。"
)

response_mssql = client.responses.create(
    model="gpt-5",
    input=sql_prompt_mssql,
    text={"format": {"type": "text"}},
    tools=[
        {
            "type": "custom",
            "name": "mssql_grammar",
            "description": "SELECT文のTOP句と基本的なWHERE/ORDER BYのみを使った、Microsoft SQL Server用の読み取り専用クエリを実行する。**クエリが文法に従っているか十分に考慮すること。**",
            "format": {
                "type": "grammar",
                "syntax": "lark",
                "definition": mssql_grammar
            }
        },
    ],
    parallel_tool_calls=False
)

print("--- MS SQL クエリ ---")
print(response_mssql.output[1].input)

結果。みやすさのため少し改行を入れている。

出力
--- MS SQL クエリ ---
SELECT TOP 5 customer_id, order_id, order_date, total_amount
FROM orders
WHERE total_amount > 500 AND order_date > '2025-01-01' ORDER BY order_date DESC;

次にPostgreSQL方言のSQLクエリを生成。

sql_prompt_pg = (
    "postgres_grammar を使って、PostgreSQL用のクエリを生成してください。"
    "各顧客ごとに最新の注文5件を取得し、customer_id、order_id、order_date、total_amount を表示する。"
    "total_amount が500より大きく、order_date が '2025-01-01' 以降のものを対象とする。"
)

response_pg = client.responses.create(
    model="gpt-5",
    input=sql_prompt_pg,
    text={"format": {"type": "text"}},
    tools=[
        {
            "type": "custom",
            "name": "postgres_grammar",
            "description": "SELECT文のLIMIT句と基本的なWHERE/ORDER BYのみを使った、PostgreSQL用の読み取り専用クエリを実行する。**クエリが文法に従っているか十分に考慮すること。**",
            "format": {
                "type": "grammar",
                "syntax": "lark",
                "definition": postgres_grammar
            }
        },
    ],
    parallel_tool_calls=False,
)

print("--- PG SQL クエリ ---")
print(response_pg.output[1].input)

結果。みやすさのため少し改行を入れている。

出力
--- PG SQL クエリ ---
SELECT customer_id, order_id, order_date, total_amount
FROM orders
WHERE total_amount > 500 AND order_date > '2025-01-01' ORDER BY order_date DESC
LIMIT 5;

出力は、論理的には同じクエリだが、書き方(構文)が異なることを示している。
方言ごとに異なる文法を用意することで、モデルがその方言に合った有効なSQL文だけを生成できるようになる。

方言 生成されるクエリ例 主な違い
MS SQL Server SELECT TOP 5 customer_id, … ORDER BY order_date DESC; TOP N 句がカラムリストの前に来る
PostgreSQL SELECT customer_id, … ORDER BY order_date DESC LIMIT 5; LIMIT NORDER BY の後に来る

3.5 例 ― 正規表現CFG構文

次に、正規表現(Regex)CFG構文を使って、自由形式ツール呼び出しの出力を特定のタイムスタンプ形式に制約するサンプル。

from openai import OpenAI

client = OpenAI()

timestamp_grammar_definition = r"^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]) (?:[01]\d|2[0-3]):[0-5]\d$"

timestamp_prompt = (
    "timestamp_grammarを使って、2025年8月7日10時のタイムスタンプを保存してください。"
)

response_mssql = client.responses.create(
    model="gpt-5",
    input=timestamp_prompt,
    text={"format": {"type": "text"}},
    tools=[
        {
            "type": "custom",
            "name": "timestamp_grammar",
            "description": "日付+時刻(24時間表記)のタイムスタンプを保存する。",
            "format": {
                "type": "grammar",
                "syntax": "regex",
                "definition": timestamp_grammar_definition
            }
        },
    ],
    parallel_tool_calls=False
)

print("--- タイムスタンプ ---")
print(response_mssql.output[1].input)

結果

出力
--- タイムスタンプ ---
2025-08-07 10:00

3.5 ベストプラクティス

Lark文法は完璧に仕上げるのが難しいことがある。シンプルな文法であれば安定して動作する一方で、複雑な文法を使う場合は、モデルが「おかしな出力」(文法から外れたもの)にならないように、文法定義そのものやプロンプト、ツールの説明を何度も調整する必要が出てくる場合がある。

  • 終端記号は範囲を限定する
    /.*\./ より /[^.\n]{0,10}*\./ のように、内容(否定文字クラス)や長さ({M,N})で制限する。
  • ワイルドカード(.)より明示的な文字クラスを使う
    [A-Za-z0-9_] みたいに、何でもOKな . は避ける。
  • 空白も明示的に扱う
    グローバルな %ignore の代わりに、SP = " " のようにルールとして明示する。
  • ツールの説明をしっかり書く
    モデルに「このCFGはこういう形式しか受け付けない」ということを正確に伝えて、文法に従うように強く指示する。

トラブルシューティング

  • 文法が複雑すぎてAPIに拒否される場合
    • ルールや終端記号をシンプルにする。%ignore.* を削除する。
  • 予期しないトークンが出る場合
    • 終端記号が重複していないか、貪欲な字句解析になっていないか確認する。
  • モデルが「おかしな出力」(長すぎ・繰り返し・文法はあっているが意味をなさない)を出す場合
    • 文法をもっと厳しくする
    • プロンプトやツール説明を工夫する(few-shotの例を追加したり、文法を守るよう強調する)
    • reasoning effort を高くする。 medium から highに。

参考となるリソース

3.6 まとめ

3.6 まとめ

GPT-5の文脈自由文法(CFG)サポートを使うことで、モデルの出力を事前に定義した構文に厳密に制約でき、有効な文字列だけが生成されるようにすることができる。これは、プログラミング言語のルールやカスタムフォーマットを強制したい場合に特に有用で、後処理やエラーを減らすのに役立つ。正確な文法と明確なツール説明を提供することで、モデルの出力を確実に目的の構造に収めることができる。

kun432kun432

4. 最小限の推論(Minimal Reasoning)

4.1 概要

4.1 概要

GPT-5は新たに「最小限の推論(minimal reasoning effort)」をサポートしている。このモードを使うと、モデルは推論トークンをほとんど、または全く出力しなくなる。これは、開発者が「ユーザーに最初のトークンが表示されるまでの時間」をとにかく速くしたい場合に設計されている。
※reasoning effort が指定されていない場合、デフォルト値は medium になる。

少し時間などを計測するようにしてみた。

from openai import OpenAI
import time
import json

client = OpenAI()

prompt = "レビューの感情を positive・neutral・negative のいずれかで分類し、1語だけ返してください。"

start_time = time.perf_counter()

response = client.responses.create(
    model="gpt-5",
    input= [
        {
            'role': 'developer',
            'content': prompt
        },
        {
            'role': 'user',
            'content': 'そのレストランの料理は素晴らしかった!みんなにおすすめします。'
        }
    ],
    reasoning = {"effort": "minimal"},
)

# モデルのテキスト出力を抽出
output_text = ""
for item in response.output:
    if hasattr(item, "content") and item.content is not None:
        for content in item.content:
            if hasattr(content, "text") and content.text is not None:
                for text in content.text:
                    output_text += text

end_time = time.perf_counter()

# トークン消費の詳細
usage = response.usage

print("--------------------------------")
print("出力:")
print(output_text)
print("--------------------------------")
print("トークン消費:")
print(json.dumps(usage.model_dump(), indent=2))
print("--------------------------------")
print("実行時間:")
print("{:.5f} 秒".format(end_time - start_time))
出力
--------------------------------
出力:
positive
--------------------------------
トークン消費:
{
  "input_tokens": 54,
  "input_tokens_details": {
    "cached_tokens": 0
  },
  "output_tokens": 7,
  "output_tokens_details": {
    "reasoning_tokens": 0
  },
  "total_tokens": 61
}
--------------------------------
実行時間:
1.28887 秒

コメントアウト、つまりデフォルトのmediumにしてみる。

(snip)
    #reasoning = {"effort": "minimal"},
(snip)
出力
--------------------------------
出力:
positive
--------------------------------
トークン消費:
{
  "input_tokens": 54,
  "input_tokens_details": {
    "cached_tokens": 0
  },
  "output_tokens": 135,
  "output_tokens_details": {
    "reasoning_tokens": 128
  },
  "total_tokens": 189
}
--------------------------------
実行時間:
11.20310 秒

4.2 まとめ

最小限の推論(minimal reasoning)を使うと、GPT-5は推論トークンをほとんど、または全く使わずに動作し、レイテンシ(応答までの時間)を最小化し、最初のトークン表示までの速度を上げることができる。 説明が不要な決定的で軽量なタスク(抽出、フォーマット、短い書き換え、単純な分類)に最適。
reasoning effort を指定しない場合はmediumがデフォルトなので、「速さ重視」のときはminimalを明示的に設定するのがオススメ。


余談

このminimal reasoning effort、自分はストリーミング時のTTFTが気になったので少し計測してみたけども、GPT-4.1やGPT-4o、Gemini-Flash-Liteなどに比べると、GPT-5は遅いという印象(自分が試した限りだと、GPT-5 nanoでも600msを切ることがなかった。上記にリストアップしたモデルの軽量バージョンだと500msを切ってくる)。ただ、最初のチャンク以降の生成は非常に高速。

このあたりは、Reasoningモデルらしいという気もするし、minimalでもいくらかはReasoningするるのかなぁという気もするし、まだモデルリリース直後で安定していないのかも?とか。まあ推測の範囲を出ないのである程度落ち着いてみてから再度計測してみたい。

このスクラップは27日前にクローズされました