非同期ツール呼び出しを使うと、モデルはツールを呼び出した後、その結果を待たずに作業を続けられます。時間のかかる照会を早めに開始し、結果を待つ間に、依頼のうちその結果に依存しない部分に回答できます。アプリケーションは結果が得られた時点でモデルに返します。
通常の関数呼び出しでは、モデルはそのターンの処理を一時停止し、ツールからの応答を待ちます。関数ツールまたはカスタムツールの定義で async: true を指定すると、モデルはツールを呼び出した後、アプリケーションが出力を返す前に作業を続けられます。
非同期でも、ツールを実行するのはアプリケーションです。実行が OpenAI 側に移ることはなく、バックグラウンドジョブの管理もアプリケーションが担います。
バックグラウンドモードでは、レスポンスの生成自体を非同期で実行します。一方、非同期ツール呼び出しでは、アプリケーションがツールを実行している間もモデルが作業を続けられます。
ジョブが完了したら、後続の Responses API リクエストにその出力を含めます。結果を元の呼び出しに対応付けるには、その呼び出しで API が発行した call_id を使います。
| ツールの種類 |
呼び出し項目 |
出力項目 |
| 関数 |
function_call |
function_call_output |
| カスタム |
custom_tool_call |
custom_tool_call_output |
ツール定義に async: true を追加します。response.output 内の対応する呼び出し項目にも async: true が含まれます。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79import OpenAI from "openai";
const client = new OpenAI();
const model = "gpt-6-astra";
const tools = [
{
type: "function",
name: "get_weather",
description: "Read a demo weather snapshot for a city.",
async: true,
strict: true,
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false,
},
},
];
async function getWeather(city) {
const snapshots = {
Paris: {
city: "Paris",
temperature_c: 22,
condition: "Clear",
source: "demo weather snapshot",
},
};
if (typeof city !== "string" || !Object.hasOwn(snapshots, city)) {
throw new Error(`No demo weather snapshot for ${city}.`);
}
return snapshots[city];
}
const instructions =
"Start the weather lookup and answer the independent packing question " +
"without waiting. Use the demo weather result when it arrives; never invent it.";
let response = await client.responses.create({
model,
tools,
instructions,
input:
"Check the demo weather snapshot for Paris. Meanwhile, " +
"list three essentials for any city trip.",
});
const call = response.output.find((item) => item.type === "function_call");
if (!call || call.name !== "get_weather") {
throw new Error("The response did not include a weather call.");
}
const { city } = JSON.parse(call.arguments);
let latestResponseId = response.id;
// Calling an async function starts the application's job immediately.
const job = getWeather(city).catch((error) => ({ error: error.message }));
if (!call.async) {
// Ordinary synchronous calls must finish before the model resumes.
await job;
}
console.log(response.output);
// Independent work or conversation turns can happen here.
// Update latestResponseId after each continuation.
const result = await job;
response = await client.responses.create({
model,
tools,
instructions,
previous_response_id: latestResponseId,
input: [
{
type: "function_call_output",
call_id: call.call_id,
output: JSON.stringify(result),
},
],
});
latestResponseId = response.id;
console.log(response.output);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93import json
from concurrent.futures import ThreadPoolExecutor
from openai import OpenAI
from openai.types.responses import FunctionToolParam
def get_weather(city):
# Demo data. Replace this function with your weather service.
weather = {
"Paris": {
"city": "Paris",
"temperature_c": 22,
"condition": "Clear",
"source": "demo weather snapshot",
}
}
return weather[city]
worker = ThreadPoolExecutor()
def main():
client = OpenAI()
model = "gpt-6-astra"
tools: list[FunctionToolParam] = [
{
"type": "function",
"name": "get_weather",
"description": "Read the demo weather snapshot for a city.",
"async": True,
"strict": True,
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
"additionalProperties": False,
},
},
]
instructions = (
"Start the weather lookup and answer the independent packing "
"question without waiting. Use the actual tool result when it "
"arrives; never invent it. Identify the weather as demo data."
)
response = client.responses.create(
model=model,
tools=tools,
instructions=instructions,
input=(
"Check the demo weather in Paris. Meanwhile, "
"list three essentials for any city trip."
),
)
call = next(item for item in response.output if item.type == "function_call")
arguments = json.loads(call.arguments)
if call.name != "get_weather" or arguments != {"city": "Paris"}:
raise ValueError("Expected a weather lookup for Paris")
latest_response_id = response.id
if call.async_:
job = worker.submit(get_weather, **arguments)
print(response.output_text)
# Independent work or conversation turns can happen here.
# Update latest_response_id after each continuation.
result = job.result()
else:
result = get_weather(**arguments)
response = client.responses.create(
model=model,
tools=tools,
instructions=instructions,
previous_response_id=latest_response_id,
input=[
{
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result),
},
],
)
print(response.output_text)
if __name__ == "__main__":
try:
main()
finally:
worker.shutdown(wait=True)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100package main
import (
"context"
"encoding/json"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
type weatherArguments struct {
City string `json:"city"`
}
type weatherSnapshot struct {
City string `json:"city"`
TemperatureC int `json:"temperature_c"`
Condition string `json:"condition"`
Source string `json:"source"`
}
func getWeather(city string) weatherSnapshot {
// Demo data. Replace this function with your weather service.
if city != "Paris" {
panic("No demo weather snapshot for " + city)
}
return weatherSnapshot{
City: city, TemperatureC: 22, Condition: "Clear", Source: "demo weather snapshot",
}
}
func main() {
client := openai.NewClient()
ctx := context.Background()
tool := responses.ToolParamOfFunction("get_weather", map[string]any{
"type": "object",
"properties": map[string]any{"city": map[string]string{"type": "string"}},
"required": []string{"city"},
"additionalProperties": false,
}, true)
tool.OfFunction.Description = openai.String("Read the demo weather snapshot for a city.")
tool.OfFunction.Async = openai.Bool(true)
tools := []responses.ToolUnionParam{tool}
instructions := "Start the weather lookup and answer the independent packing question " +
"without waiting. Use the actual tool result when it arrives; never invent it. " +
"Identify the weather as demo data."
response, err := client.Responses.New(ctx, responses.ResponseNewParams{
Model: "gpt-6-astra",
Tools: tools,
Instructions: openai.String(instructions),
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Check the demo weather in Paris. Meanwhile, list three essentials for any city trip.")},
})
if err != nil {
panic(err)
}
var call responses.ResponseFunctionToolCall
for _, item := range response.Output {
if item.Type == "function_call" && item.AsFunctionCall().Name == "get_weather" {
call = item.AsFunctionCall()
break
}
}
if call.CallID == "" {
panic("The response did not include a weather call.")
}
var arguments weatherArguments
if err := json.Unmarshal([]byte(call.Arguments), &arguments); err != nil {
panic(err)
}
latestResponseID := response.ID
var result weatherSnapshot
if call.Async {
job := make(chan weatherSnapshot, 1)
go func() { job <- getWeather(arguments.City) }()
fmt.Println(response.OutputText())
// Independent work or conversation turns can happen here.
// Update latestResponseID after each continuation.
result = <-job
} else {
result = getWeather(arguments.City)
}
output, err := json.Marshal(result)
if err != nil {
panic(err)
}
functionOutput := responses.ResponseInputItemParamOfFunctionCallOutput(string(output))
functionOutput.OfFunctionCallOutput.CallID = openai.String(call.CallID)
response, err = client.Responses.New(ctx, responses.ResponseNewParams{
Model: "gpt-6-astra",
Tools: tools,
Instructions: openai.String(instructions),
PreviousResponseID: openai.String(latestResponseID),
Input: responses.ResponseNewParamsInputUnion{OfInputItemList: responses.ResponseInputParam{functionOutput}},
})
if err != nil {
panic(err)
}
fmt.Println(response.OutputText())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.openai.core.JsonValue;
import com.openai.models.responses.FunctionTool;
import com.openai.models.responses.Response;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseFunctionToolCall;
import com.openai.models.responses.ResponseInputItem;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
record WeatherArguments(String city) {}
record WeatherSnapshot(
String city,
@JsonProperty("temperature_c") int temperatureC,
String condition,
String source) {}
static WeatherSnapshot getWeather(String city) {
// Demo data. Replace this function with your weather service.
if (!city.equals("Paris")) {
throw new IllegalArgumentException("No demo weather snapshot for " + city);
}
return new WeatherSnapshot(city, 22, "Clear", "demo weather snapshot");
}
FunctionTool tool =
FunctionTool.builder()
.name("get_weather")
.description("Read the demo weather snapshot for a city.")
.async(true)
.strict(true)
.parameters(
FunctionTool.Parameters.builder()
.putAdditionalProperty("type", JsonValue.from("object"))
.putAdditionalProperty(
"properties", JsonValue.from(Map.of("city", Map.of("type", "string"))))
.putAdditionalProperty("required", JsonValue.from(List.of("city")))
.putAdditionalProperty("additionalProperties", JsonValue.from(false))
.build())
.build();
String instructions =
"Start the weather lookup and answer the independent packing question without waiting. Use the actual tool result when it arrives; never invent it. Identify the weather as demo data.";
Response response =
client
.responses()
.create(
ResponseCreateParams.builder()
.model("gpt-6-astra")
.addTool(tool)
.instructions(instructions)
.input(
"Check the demo weather in Paris. Meanwhile, list three essentials for any city trip.")
.build());
ResponseFunctionToolCall call =
response.output().stream()
.flatMap(item -> item.functionCall().stream())
.filter(item -> item.name().equals("get_weather"))
.findFirst()
.orElseThrow(
() -> new IllegalStateException("The response did not include a weather call."));
WeatherArguments arguments = call.arguments(WeatherArguments.class);
String latestResponseId = response.id();
WeatherSnapshot result;
if (call.async().orElse(false)) {
CompletableFuture<WeatherSnapshot> job =
CompletableFuture.supplyAsync(() -> getWeather(arguments.city()));
System.out.println(response.output());
// Independent work or conversation turns can happen here.
// Update latestResponseId after each continuation.
result = job.join();
} else {
result = getWeather(arguments.city());
}
response =
client
.responses()
.create(
ResponseCreateParams.builder()
.model("gpt-6-astra")
.addTool(tool)
.instructions(instructions)
.previousResponseId(latestResponseId)
.inputOfResponse(
List.of(
ResponseInputItem.ofFunctionCallOutput(
ResponseInputItem.FunctionCallOutput.builder()
.callId(call.callId())
.output(new ObjectMapper().writeValueAsString(result))
.build())))
.build());
response.output().stream()
.flatMap(item -> item.message().stream())
.flatMap(message -> message.content().stream())
.flatMap(content -> content.outputText().stream())
.forEach(text -> System.out.println(text.text()));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70require "json"
require "openai"
def get_weather(city)
# Demo data. Replace this function with your weather service.
raise "No demo weather snapshot for #{city}" unless city == "Paris"
{
city: city,
temperature_c: 22,
condition: "Clear",
source: "demo weather snapshot"
}
end
client = OpenAI::Client.new
tools = [
OpenAI::Models::Responses::FunctionTool.new(
name: "get_weather",
description: "Read the demo weather snapshot for a city.",
async: true,
strict: true,
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
additionalProperties: false
}
)
]
instructions = "Start the weather lookup and answer the independent packing question " \
"without waiting. Use the actual tool result when it arrives; never invent it. " \
"Identify the weather as demo data."
response = client.responses.create(
model: "gpt-6-astra",
tools: tools,
instructions: instructions,
input: "Check the demo weather in Paris. Meanwhile, list three essentials for any city trip."
)
call = response.output.find do |item|
item.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall) && item.name == "get_weather"
end
unless call.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall)
raise "The response did not include a weather call."
end
city = JSON.parse(call.arguments).fetch("city")
latest_response_id = response.id
result = if call.async
job = Thread.new { get_weather(city) }
puts(response.output_text)
# Independent work or conversation turns can happen here.
# Update latest_response_id after each continuation.
job.value
else
get_weather(city)
end
response = client.responses.create(
model: "gpt-6-astra",
tools: tools,
instructions: instructions,
previous_response_id: latest_response_id,
input: [
OpenAI::Models::Responses::ResponseInputItem::FunctionCallOutput.new(
call_id: call.call_id,
output: JSON.generate(result)
)
]
)
puts(response.output_text)
レスポンスには、非同期の呼び出しとモデルの回答の両方が含まれる場合があります。ジョブの完了前に会話が進んだ場合は、latest_response_id を更新して最新のレスポンスから会話を続けます。その際、元のツール呼び出しの call_id は保持してください。
ストリーミングを使ってジョブを早めに開始するには、呼び出し項目の全体を受信した時点でジョブを開始し、レスポンスの残りを引き続き受信します。
待機ツールを使うと、モデルはまだ届いていない結果をいつ待つか判断できます。たとえば、2 件の価格照会を開始した後、価格に依存しない作業を進め、価格を比較する段階で初めて結果を待つことができます。
各非同期ツールに task_handle 引数を追加します。モデルが呼び出しごとにハンドルを割り当て、アプリケーションがそのハンドルを元の API 呼び出しの call_id および実行中のジョブに対応付けます。完了済みのタスクや同じ内容の照会を繰り返す場合も含め、会話全体でハンドルが重複しないようにしてください。
待機ツールは通常の同期関数として定義します。async を省略するか、false に設定してください。スキーマと動作はアプリケーション側で定義します。wait_for_tasks は Responses API の組み込みツールではありません。
リクエストの tools 配列に次の定義を含めます。
1234567891011121314151617181920212223242526272829303132333435[
{
"type": "function",
"name": "lookup_price",
"async": true,
"description": "Look up a product price in the background. Choose a fresh task_handle unique within this conversation, including completed tasks.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"task_handle": { "type": "string" }
},
"required": ["sku", "task_handle"],
"additionalProperties": false
}
},
{
"type": "function",
"name": "wait_for_tasks",
"description": "Wait for selected tasks whose results you need. Pass a nonempty list of distinct task_handles from your earlier async tool calls. Results arrive on their original calls; this tool returns status only. Do not wait again for results that have already arrived.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"task_handles": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["task_handles"],
"additionalProperties": false
}
}
]
各ジョブを登録して開始してから、そのジョブの結果を必要とする待機呼び出しを処理します。これらの呼び出しは、同じレスポンスにまとめて届く場合も、複数のレスポンスに分かれて届く場合もあります。次の出力項目の例では、2 件のジョブを開始し、待機呼び出しで両方の結果を待ちます。
12345678910111213141516171819202122[
{
"type": "function_call",
"name": "lookup_price",
"async": true,
"call_id": "call_widget",
"arguments": "{\"sku\":\"WIDGET\",\"task_handle\":\"widget_price_1\"}"
},
{
"type": "function_call",
"name": "lookup_price",
"async": true,
"call_id": "call_gadget",
"arguments": "{\"sku\":\"GADGET\",\"task_handle\":\"gadget_price_1\"}"
},
{
"type": "function_call",
"name": "wait_for_tasks",
"call_id": "call_wait",
"arguments": "{\"task_handles\":[\"widget_price_1\",\"gadget_price_1\"]}"
}
]
アプリケーションのレジストリで、各ハンドルを元の呼び出しと実行中のジョブに対応付けます。
| タスクハンドル |
元の呼び出し ID |
ジョブ |
widget_price_1 |
call_widget |
WIDGET の価格照会 |
gadget_price_1 |
call_gadget |
GADGET の価格照会 |
完了済みタスクのハンドルを再利用しないよう、会話が終わるまでレジストリを保持してください。
指定されたハンドルに対応するジョブをレジストリから取得し、それらのジョブだけを待ちます。新たに完了した各ジョブの結果を元の呼び出しの call_id で返し、その後、待機呼び出し自体の call_id でステータスを返します。この順序なら、モデルが処理を再開した時点で結果を利用できます。
たとえば、次のリクエストの input 配列に以下の出力項目を含めて送信します。価格は例示用の値です。
1234567891011121314151617[
{
"type": "function_call_output",
"call_id": "call_widget",
"output": "{\"task_handle\":\"widget_price_1\",\"price_cents\":1200,\"currency\":\"USD\"}"
},
{
"type": "function_call_output",
"call_id": "call_gadget",
"output": "{\"task_handle\":\"gadget_price_1\",\"price_cents\":1500,\"currency\":\"USD\"}"
},
{
"type": "function_call_output",
"call_id": "call_wait",
"output": "{\"status\":\"completed\",\"completed_task_handles\":[\"widget_price_1\",\"gadget_price_1\"]}"
}
]
previous_response_id に最新のレスポンス ID を設定し、会話を続けるためのリクエストにツールと指示を含めます。アプリケーションは、待機呼び出しがなくても、結果が得られた時点で返すことができます。待機ツールは、モデルが次の処理に進むために、まだ届いていない結果が必要な場合にのみ使ってください。
非同期ツールを使うと、モデルはユーザーに質問した後も、回答に依存しない作業を続けられます。たとえば、レポートの読者をユーザーに尋ね、回答を待つ間に情報を集め、回答が届いたら読者に合わせてレポートを作成できます。
不足している情報やユーザーの希望を尋ねるには、async: true を指定して関数を定義します。アプリケーションが質問を表示し、ユーザーの回答を受け取ってツールの結果として返します。スキーマと動作はアプリケーション側で定義します。request_user_input_async は Responses API の組み込みツールではありません。
リクエストの tools 配列に、wait_for_tasks とともに次の定義を追加します。
12345678910111213141516{
"type": "function",
"name": "request_user_input_async",
"async": true,
"description": "Ask the user for missing information or a preference. Choose a fresh task_handle unique within this conversation, including completed tasks. Continue independent work while the answer is pending. Use wait_for_tasks when your next step depends on the answer.",
"strict": true,
"parameters": {
"type": "object",
"properties": {
"question": { "type": "string" },
"task_handle": { "type": "string" }
},
"required": ["question", "task_handle"],
"additionalProperties": false
}
}
呼び出し項目の全体を受信したら、質問の task_handle と元の call_id を登録し、ユーザーに質問を表示します。次の出力項目は、レポートの読者を尋ねる例です。
1234567{
"type": "function_call",
"name": "request_user_input_async",
"async": true,
"call_id": "call_audience",
"arguments": "{\"question\":\"Who is the report for: executives or engineers?\",\"task_handle\":\"report_audience_1\"}"
}
待機ツールが使うレジストリで、質問を回答待ちの状態にしておきます。ユーザーが回答する間も、モデルのレスポンスを引き続き受信してください。質問を表示したという通知を返してツール呼び出しを完了させないでください。この呼び出しの結果は、ユーザーの回答です。
ユーザーから回答が届いたら、質問の元の call_id で回答を送信します。たとえば、次のリクエストの input 配列に以下の出力項目を含めます。
1234567[
{
"type": "function_call_output",
"call_id": "call_audience",
"output": "{\"task_handle\":\"report_audience_1\",\"answer\":\"Executives\"}"
}
]
previous_response_id に最新のレスポンス ID を設定し、会話を続けるためのリクエストにツールと指示を含めます。モデルが wait_for_tasks を呼び出して report_audience_1 の結果を待っている場合は、待機ステータスを返す前に回答を返してください。具体的な順序は待機ツールの例を参照してください。
非同期で実行しても、ユーザーが回答するまでレスポンスが開いたままになるわけではありません。回答に依存しない作業は続け、回答が必要な処理に進む前に待機するようモデルに指示してください。アプリケーションで質問を閉じたり回答期限を設定したりできる場合は、回答がなかったことを結果として明示的に返し、モデルが次の対応を判断できるようにします。
GPT-6 Astra 以降のモデルは、非同期ツール呼び出しに対応しています。
非同期実行の対象は、アプリケーションが実行する関数ツールとカスタムツールです。ホスト型の組み込みツールには適用されません。ツールは直接呼び出してください。プログラムによるツール呼び出しで非同期ツールを使用する設定にはしないでください。
マルチエージェントモードでは、非同期ツールと並列ツール呼び出しを併用しないでください。