BEACHSIDE BLOG

Azure と GitHub と C# が好きなエンジニアの個人メモ ( ・ㅂ・)و ̑̑

要約: Better performance from reasoning models using the Responses API (OpenAI Cookbook)

Responses API 推奨になって結構経ちますが、使う理由のおさらいとして OpenAI Cookbook の以下記事をざっくり要約しました (+ Overview は私の気分で箇条書きにがっつりまとめちゃいましたが)。

原文は 2025年5月の記事です。 なので原文内のサンプルコードは、最近っぽくするのに以下の変更を加えて、それに合わせて文章も多少変えてます。

  • Azure OpenAI の endpoint への接続
  • 新しい v1 endpoint を利用。
  • Azure へ接続するが client の class は OpenAI ではなく AzureOpenAI を利用。
    • v1 endpoint の利用から Azure 接続時も OpenAI class も利用可能になりましたね。
    • しかし Entra ID 認証するなら、現時点では結局 AzureOpenAI が必要なので。
    • といっても今回 Entra ID ではなく API key での認証のコードにしてますが。
  • 実行結果に関する文章は、今回使ったコードの結果に基づいて変更 (主にトークン数など)。

Overview

  • Responses APIと最新の推論モデルの利点:
    • 高度なインテリジェンス、低コスト、効率的なトークン使用を実現。
    • 推論の要約へのアクセスやホストされたツール使用が可能。
    • 柔軟性とパフォーマンス向上のための機能強化にも対応できる。
  • 新モデル「o3」と「o4-mini」:
    • 推論機能とエージェント的なツール使用を組み合わせることに優れている。
    • Responses API を最大限に活用することで、これらのモデルのパフォーマンスをさらに向上させられる。
  • Responses APIの主な特徴:
    • Completions APIに似ているが、改善点や機能が追加されている。
    • モデルに以前の推論項目へのアクセスを許可することで、インテリジェンスを最大化しコストを最小化できる。
    • レスポンスの暗号化されたコンテンツも展開しており、ステートフルな方法でAPIを使用できない人にも役立つ。

How Reasoning Models work (推論モデルはどのように機能するか)

Responses APIがどのように役立つかを詳しく説明する前に、推論モデルがどのように機能するかを簡単に復習しましょう。

o3やo4-miniのようなモデルは、問題を段階的に分解し、推論を符号化する内部的な思考の連鎖を生成します。安全のため、これらの推論トークンは要約された形式でのみユーザーに公開されます。 多段階の会話では、推論トークンは各ターンの後に破棄されますが、各ステップからの入力および出力トークンは次のステップに供給されます。

図: https://platform.openai.com/docs/guides/reasoning?api-mode=responses#how-reasoning-works より引用。

生成されるレスポンスのオブジェクトをみてみましょう。

import os
from dotenv import load_dotenv
from openai import AsyncAzureOpenAI

# 必要な環境変数はロードしておいてます。
load_dotenv(override=True)

azure_client = AsyncAzureOpenAI(
    api_key=os.environ["AIF_API_KEY"],
    base_url=os.environ["AIF_ENDPOINT_V1"], # https://xxxxxxxxxx.azure.com/openai/v1
    api_version="preview"
)

response = await azure_client.responses.create(
    model="o4-mini",
    input="私にジョークを面白い言ってください"
)

print(response.model_dump_json(indent=2))

print したレスポンスはこんな感じ。

{
  "id": "resp_689f28582c54819eaab8f1385b6703e605fced5ca6aea09b",
  "created_at": 1755261016.0,
  "error": null,
  "incomplete_details": null,
  "instructions": null,
  "metadata": {},
  "model": "o4-mini",
  "object": "response",
  "output": [
    {
      "id": "rs_689f285b9024819ebe896dc48cc7935405fced5ca6aea09b",
      "summary": [],
      "type": "reasoning",
      "content": null,
      "encrypted_content": null,
      "status": null
    },
    {
      "id": "msg_689f288ea858819e960de6f672e19da705fced5ca6aea09b",
      "content": [
        {
          "annotations": [],
          "text": "ある日、先生がクラスで言いました。\n\n「宿題を“やってきた”人、手を挙げなさい!」\n\n教室中でみんな手を挙げると、先生が続けて、\n\n「じゃあ“持ってきた”人は?」\n\nすると…誰も手を挙げない!\n\n先生がニヤリと一言——  \n「やってきただけで、持ってはきてないからね!」",
          "type": "output_text",
          "logprobs": null
        }
      ],
      "role": "assistant",
      "status": "completed",
      "type": "message"
    }
  ],
  "parallel_tool_calls": true,
  "temperature": 1.0,
  "tool_choice": "auto",
  "tools": [],
  "top_p": 1.0,
  "background": false,
  "max_output_tokens": null,
  "max_tool_calls": null,
  "previous_response_id": null,
  "prompt": null,
  "prompt_cache_key": null,
  "reasoning": {
    "effort": "medium",
    "generate_summary": null,
    "summary": null
  },
  "safety_identifier": null,
  "service_tier": "default",
  "status": "completed",
  "text": {
    "format": {
      "type": "text"
    },
    "verbosity": null
  },
  "top_logprobs": null,
  "truncation": "disabled",
  "usage": {
    "input_tokens": 17,
    "input_tokens_details": {
      "cached_tokens": 0
    },
    "output_tokens": 3051,
    "output_tokens_details": {
      "reasoning_tokens": 2944
    },
    "total_tokens": 3068
  },
  "user": null,
  "content_filters": null,
  "store": true
}

応答オブジェクトの JSON ダンプから、output_text に加えて、モデルが reasoning も生成していることがわかります。このアイテムはモデルの内部の推論トークンを表し、ID(ここでは例として rs_689f285b9024819ebe896dc48cc7935405fced5ca6aea09b)として公開されます。Responses API はステートフルであるため、これらの推論トークンは保持されます。将来の応答が同じ推論アイテムにアクセスできるように、後続のメッセージにその ID を含めるだけで済みます。多段階の会話で previous_response_id を使用すると、モデルは以前に生成されたすべての推論アイテムに自動的にアクセスできるようになります。

モデルが生成した推論トークン (reasoning_token) の数も確認できます。たとえば、17個の入力トークンで、応答には3051個の出力トークン (output_tokens) が含まれ、そのうち2944個は最終的なアシスタントメッセージには表示されない推論トークンです。

あれ、図では以前のターンの推論は破棄されると示されていたのでは?それなら、後のターンでわざわざそれを返す意味は何なのでしょうか?

通常の多段階の会話では、推論のアイテムやトークンを含める必要はありません。モデルはそれらがなくても最良の出力を生成するように訓練されているからです。しかし、ツール使用が関わる場合は状況が変わります。ターンに関数呼び出しが含まれる場合(API外での追加の往復が必要になる可能性があります)、previous_response_id を介して、または推論アイテムを明示的に input に追加することで、推論アイテムを含める必要があります。関数呼び出しの簡単な例で、これがどのように機能するか見てみましょう。

import requests

def get_weather(latitude, longitude):
    response = requests.get(f"https://api.open-meteo.com/v1/forecast?latitude={latitude}&longitude={longitude}&current=temperature_2m,wind_speed_10m&hourly=temperature_2m,relative_humidity_2m,wind_speed_10m")
    data = response.json()
    return data['current']['temperature_2m']


tools = [{
    "type": "function",
    "name": "get_weather",
    "description": "Get current temperature for provided coordinates in celsius.",
    "parameters": {
        "type": "object",
        "properties": {
            "latitude": {"type": "number"},
            "longitude": {"type": "number"}
        },
        "required": ["latitude", "longitude"],
        "additionalProperties": False
    },
    "strict": True
}]

context = [{"role": "user", "content": "今日の東京の天気はどうですか?"}]

response = await azure_client.responses.create(
    model="o4-mini",
    input=context,
    tools=tools,
)

print(response.output)

結果:

[ResponseReasoningItem(id='rs_689f2f147ec48192ac8d7de83c199e27078ee12268125a2b', summary=[], type='reasoning', content=None, encrypted_content=None, status=None),
ResponseFunctionToolCall(arguments='{"latitude":35.6895,"longitude":139.6917}', call_id='call_SEnPgcT7dVU80068NUJiu3MN', name='get_weather', type='function_call', id='fc_689f2f1883d0819292777ee2b221aae3078ee12268125a2b', status='completed')]

推論の結果、o4-mini モデルはより多くの情報が必要だと判断し、それを得るための関数を呼び出します。私たちはその関数を呼び出し、その出力をモデルに戻すことができます。重要なのは、モデルのインテリジェンスを最大限に引き出すために、出力全体を次のターンのコンテキストに再度追加することで、推論のアイテムを含めるべきだということです。

import json

context += response.output # 回答をコンテキストに追加する(推論アイテムを含む)
print(context)


tool_call = response.output[1]
args = json.loads(tool_call.arguments)


# calling the function
result = get_weather(args["latitude"], args["longitude"]) 

context.append({                               
    "type": "function_call_output",
    "call_id": tool_call.call_id,
    "output": str(result)
})

# 追加された関数呼び出しの出力を伴って、再びAPIを呼び出しています。
# これは別のAPI呼び出しですが、会話の中では単一のターンと見なされることに注意してください。
response_2 = await azure_client.responses.create(
    model="o4-mini",
    input=context,
    tools=tools,
)

print(response_2.output_text)

結果は以下:

現在の東京の気温は約25.9℃です。比較的暖かい陽気ですが、降水状況や雲の量などはわかりませんので、お出かけ前に最新の天気予報をご確認ください。

この簡単な例では、推論アイテムの有無にかかわらずモデルがうまく機能する可能性が高いため、その利点は明確ではないかもしれませんが、当社自身のテストではそうではないことがわかりました。SWE-benchのようなより厳格なベンチマークでは、推論アイテムを含めることで、同じプロンプトと設定で約3%の改善が見られました。

Caching

上記で示したように、推論モデルは reasoning tokens (推論トークン) と completion tokens の両方を生成し、APIはこれらを異なる方法で扱います。この区別は、キャッシュの仕組みに影響し、パフォーマンスとレイテンシの両方に影響を与えます。以下の図はこれらの概念を示しています。

図: https://cookbook.openai.com/examples/responses_api/reasoning_items#how-reasoning-models-work より引用。

Turn 2では、モデルは以前のターンの推論アイテムを再利用しないため、Turn 1からの推論アイテムは無視され、削除されます。結果として、図の4番目のAPI呼び出しは、それらの推論アイテムがプロンプトから欠落しているため、完全なキャッシュヒットを達成できません。しかし、それらを含めることは無害です。APIは単に、現在のターンに関連のない推論アイテムを破棄するだけです。キャッシュは1024トークンより長いプロンプトにのみ影響することに留意してください。当社のテストでは、Completions APIからResponses APIに切り替えることで、キャッシュ利用率が40%から80%に向上しました。キャッシュ利用率が高くなると、コストが削減され(たとえば、o4-mini のキャッシュされた入力トークンは、キャッシュされていないトークンよりも75%安価です)、レイテンシが改善されます。

Encrypted Reasoning Items (暗号化された推論アイテム)

ZDR (Zero Data Retention, ゼロデータ保持) の要件を持つ組織など、一部の組織はコンプライアンスやデータ保持ポリシーのため、Responses API をステートフルな方法で使用できません。これらのケースをサポートするために、OpenAI は暗号化された推論アイテムを提供しており、ワークフローをステートレスに保ちながらも、推論アイテムの恩恵を受けられるようにしています。

暗号化された推論アイテムを使用するには:

  • API呼び出しの include フィールドに ["reasoning.encrypted_content"] を追加します。
  • APIは推論トークンの暗号化されたバージョンを返します。これは、通常の推論アイテムと同様に、以降のリクエストで再度渡すことができます。

ZDR の組織の場合、OpenAI は store=false を自動的に強制します。リクエストに encrypted_content が含まれている場合、それはメモリ内で復号化され(ディスクには決して書き込まれず)、次の応答を生成するために使用された後、安全に破棄されます。新しい推論トークンは即座に暗号化されて返されるため、中間状態が永続化されることはありません。

context = [{"role": "user", "content": "今日の東京の天気はどうですか?"}]

response = await azure_client.responses.create(
    model="o4-mini",
    input=context,
    tools=tools,
    store=False,
    include=["reasoning.encrypted_content"] # 回答の中で暗号化された chain of thought が返されます。
)

print(response.output) 

結果:

[ResponseReasoningItem(id='rs_689f36df0df08191977ba3626601a66902672609e1d93279', summary=[], type='reasoning', content=None, encrypted_content='gAAAAABonzbi0GdjVCUTsB_DAbixkKYv_ZFAtE8W1V9uzI_c7tX6ZkpP1v-7O5afms9-MHQ3yYLeK0cZoZzp2UYW4uVyZVo4_G2Qfy0E52P5KUHojzfF_5TIhkDsSCyjIwQVeoWf5lThMIiUjYQPgGpaSE_uBGSn-EL9DuiJi5bKlZKTjo05OxCPYVD1V4pKJ6n4cIh7C4l-o54yKoankiOZGbhuIfYKCHzmYS2yL5KxF9PRkiS8wmjnQqmJiqf6sh_hHDLFmoA7RwirHOH1HfD8n9q6KwJhPHY7UEd6YMm0lg9ofZL4zOGwenPWCEYKa6RFb28cj8kWTyUWFHCI6c5KVREvz9w73dhkz3tDiWSabtJ9Dgr7UEznt4a1NyCF08n49Bja27WXplUfJnpw9NxZ7VJuI_QEYSd0ZU0C0h_veMyojHVn8CaNvfDFLdR4nAuuGaz9J8uUDzdsEWHwxv5l6Jvwwur_PIJlqxVBlFCw09YpUf0IAunMFnocPOz4-jWj3WwxT5ZaTL5ZGYD56_cn2qGG3kySOrBhfKWNjWupTvl5pZDe6phqDBFcV419YxIVXgvjFGY8TZSDZeJkvZ49ujeAHOh6iVAPabhdR0NMPfWqlRYKCqESUQ705qLrngHHFi7-SNP8kpA03Ma6i3vv7sYNzlwG8PF8JN7kvxrWOTgAtRH4qvoohAhVH_nXkO1_Xm3IyZ12huyu6gKEZ2nW1VLsVRD4qfKKqMwQv4Xu5rQdcD8VJ44ZhJVyrSncE5fQJ4zJI6MX_HkJ2fM_4cjSMfVnQln_M4tIpIzS9-_J2QzlxszMFj2fLRfkqD4RpOW0WedkiPV7n5d2CXCPuFMMsPuO3oe2l0U3r9gAG2aTNSLW30ONYzB5w-GdnAjcEVilWY8ByApoqvHq7YdSGa0qpgHXR6wzC_P-H9lequy5Feugaf6wpULoiaMKxbnGp1WbiqZXZkPJfSP_0M9pgTmsQsTicWoGvROxNUMt_RDlHyreFM4pT7BrpMDC4JohpADjKrKOsSDZDoqf18qI3GpQ1Fejk3346Z5r-n8ajy4aGrhth6i5rW1xvP6BUIseBKrLhHd0NdCLVEUFjquoAuZ_KCVx2NElJYnHPzOHIQpGKbsIKEaLDfWZ45P_EpSRwJCcHemZkYT2JK1CixQbM_0Su63PhkayWnw8d4wna4vMivnBaCW81Ipc2qX2-RB4FawRkxUnFHCMIS6UL1z7RIg_hk3JwvxB7VGgKKbUHGrD9B8fqw51NArFEaEKXWWP6dgNHhUA6mEYXGYFJpnbk8vuWgRnlDVoIZt7MDLTJxxXtkn7cNoQFZCamX0TzOK4fW3VACXtj-mZpgtmWnux-g4vYey-oKPZGv9YC5RrWKteiDJmj2aTfjhYQExCKMLkT1horTH6vqrbqNuTaBA25CRTXKmuArgaJGbYyIwDnI58ypVQy0BLf0F7Egr_a-RzzCwxitsTyh1hstTq04VSxdK-xAGI8ocf8GCqRjSfm7LSFk1qJq3H9mC7PrSR-EGw_heu5k7JD0JM3gks_k5W4M6fYUnmjRuWTo6fUE5bORC-jSOTDlTSDvEaxD99743-IYd1ipOiRCPrC5Wj_YFDP4tnrr7DI2J25t1t9BkgRK9DsEo57XLidi_FSHdApQ1OMsJCifZ1N0YRD936bfziuJ8ujhyuSanRCKDJvNExXdB4SSXJz7FXmis4407lyPaf76FTeWgZqGLP34MWZg5Q-rkZtRANXr7eA-x3DdRhYps5TD6-NEnjAtCVsLJ41Z9w7G4L1vvF9yFBqjGUSiLLOWsWtliTtQlN9w==', status=None), ResponseFunctionToolCall(arguments='{"latitude":34.6937,"longitude":135.5023}', call_id='call_wS9mBcfjGClyHuKbYICDu8n3', name='get_weather', type='function_call', id='fc_689f36e240248191b5264e6420100fae02672609e1d93279', status='completed')]

include=["reasoning.encrypted_content"] を設定すると、返される推論アイテムに encrypted_content フィールドが表示されるようになります。この暗号化されたコンテンツは、モデルの推論状態を表し、OpenAI がデータを保持することなく、完全にクライアント側で永続 化されます。その後、以前に推論アイテムで行ったのと同様に、これを再度渡すことができます。

context += response.output
tool_call = response.output[1]
args = json.loads(tool_call.arguments)



result = 20 #mocking the result of the function call

context.append({                               
    "type": "function_call_output",
    "call_id": tool_call.call_id,
    "output": str(result)
})

response_3 = await azure_client.responses.create(
    model="o4-mini",
    input=context,
    tools=tools,
    store=False,
    include=["reasoning.encrypted_content"]
)

print(response_3.output_text)

結果:

今日の大阪の天気は晴れで、気温は約20℃です。過ごしやすい陽気になっています。夜にかけては多少冷え込む可能性がありますので、薄手の上着があると安心です。

include フィールドを単純に変更するだけで、暗号化された推論アイテムを再度渡し、モデルのパフォーマンスをインテリジェンス、コスト、レイテンシの面で向上させることができます。 これで、OpenAI の最新の推論モデルを最大限に活用するための知識が十分に身についたはずです!

Reasoning Summaries (推論の要約)

Responses API のもう一つの便利な機能は、推論の要約をサポートしていることです。生のchain of thought のトークンは公開していませんが、ユーザーはその summary (要約) にアクセスできます。

response = await azure_client.responses.create(
    model="o4-mini",
    input="光合成と細胞呼吸の主な違いは何ですか?",
    reasoning={
        "effort": "high", # 深く推論させるために high をセット
        "summary": "auto"
    })

# response から最初の推論の要約テキストを抽出。
first_reasoning_item = response.output[0]
first_summary_text = first_reasoning_item.summary[0].text if first_reasoning_item.summary else None
print("First reasoning summary text:\n", first_summary_text)

結果:

First reasoning summary text:
 **Comparing Photosynthesis and Cellular Respiration**

The user is looking to understand the main differences between photosynthesis and cellular respiration and has requested specific details. I can organize the differences into a bullet list or a comparison table while noting key aspects like purpose, location, light dependence, and reactions. 

Here’s a possible structure: 
1. Purpose: Energy fixation vs. Energy release
2. Location: Chloroplasts vs. Mitochondria
3. Light requirement: Light-dependent vs. Non-light dependent
4. Reaction equations: Different reactants and products
5. Oxygen/Carbon Dioxide: Oxygen production vs. Consumption

I’ll summarize these clearly in Japanese.

reasoning summary のテキストにより、ユーザーはモデルの思考プロセスを垣間見ることができます。たとえば、複数の関数呼び出しを伴う会話中に、ユーザーは最終的なアシスタントメッセージを待つことなく、どの関数が呼び出されたか、そして各呼び出しの背後にある推論の両方を見ることができます。これにより、アプリケーションのユーザーエクスペリエンスに透明性と双方向性が加わります。

Conclusion

OpenAI Responses APIと最新の推論モデルを活用することで、アプリケーションのより高度なインテリジェンス、透明性の向上、および効率化を実現できます。推論の要約、コンプライアンスのための暗号化された推論アイテムの利用、コストとレイテンシの最適化など、これらのツールは、より堅牢でインタラクティブなAIエクスペリエンスを構築することを可能にします。

Happy coding!