비동기 도구 호출을 사용하면 모델이 도구를 호출한 뒤 결과를 기다리지 않고 작업을 계속할 수 있습니다. 시간이 오래 걸리는 조회를 미리 시작하고, 조회 결과 없이도 답할 수 있는 부분에는 먼저 답하고, 애플리케이션에서 결과가 준비되면 전달하세요.
일반적인 함수 호출에서는 도구가 응답할 때까지 모델의 턴이 일시 중단됩니다. 함수나 사용자 정의 도구의 정의에 async: true를 설정하면 모델은 도구를 호출한 뒤 애플리케이션이 출력을 반환하기 전에도 작업을 계속할 수 있습니다.
비동기 도구도 애플리케이션에서 직접 실행합니다. OpenAI가 도구 실행을 대신하거나
백그라운드 작업을 관리하지 않습니다.
백그라운드 모드에서는 응답 생성 자체가 비동기로 진행됩니다. 비동기 도구 호출에서는 애플리케이션이 도구를 실행하는 동안 모델이 작업을 계속합니다.
작업이 끝나면 이후 Responses 요청에 해당 출력을 포함하세요. 원래 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는 그대로 유지하세요.
스트리밍을 사용하면 작업을 더 일찍 시작할 수 있습니다. 전체 호출 항목을 수신하는 즉시 작업을 시작하고 나머지 응답도 계속 수신하세요.
대기 도구를 사용하면 모델이 아직 도착하지 않은 결과를 언제 기다릴지 결정할 수 있습니다. 예를 들어 가격 조회 두 건을 시작한 뒤 다른 작업을 진행하다가 가격을 비교할 때만 결과를 기다릴 수 있습니다.
각 비동기 도구에 task_handle 인수를 추가하세요. 모델은 호출마다 핸들을 할당하고, 애플리케이션은 이 핸들을 원래 API 호출의 call_id 및 실행 중인 작업과 연결합니다. 이미 완료된 작업이나 반복된 조회 요청까지 포함해 대화 전체에서 핸들이 중복되지 않도록 하세요.
대기 도구는 일반 동기 함수로 정의하세요. async를 생략하거나 false로 설정하면 됩니다. 이 도구의 스키마와 동작은 애플리케이션에서 정의합니다. wait_for_tasks는 Responses의 기본 제공 도구가 아닙니다.
요청의 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
}
}
]
대기 호출을 처리하기 전에 해당 호출이 기다리는 작업을 각각 등록하고 시작하세요. 호출은 한 응답에 함께 도착할 수도 있고 여러 응답에 나뉘어 도착할 수도 있습니다. 다음 출력 항목 예시에는 작업을 시작하는 호출 두 건과 두 작업을 모두 기다리는 대기 호출 한 건이 있습니다.
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의 기본 제공 도구가 아닙니다.
요청의 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 및 이후 모델은 비동기 도구 호출을 지원합니다.
비동기 실행은 애플리케이션에서 실행하는 함수 도구와 사용자 정의 도구에 적용됩니다. 호스팅되는 기본 제공 도구에는 적용되지 않습니다. 비동기 도구는 직접 호출하세요. 프로그래밍 방식의 도구 호출에 사용하도록 구성하지 마세요.
멀티 에이전트 모드에서는 비동기 도구와 병렬 도구 호출을 함께 사용하지 마세요.