L’appel asynchrone d’outils permet au modèle de continuer à travailler après avoir appelé un outil, sans attendre son résultat. Utilisez-le pour lancer au plus tôt les recherches lentes, répondre aux parties indépendantes d’une demande et transmettre les résultats dès que votre application en dispose.
Un appel de fonction classique suspend le tour du modèle en attendant la réponse de l’outil. Ajoutez async: true à la définition d’une fonction ou d’un outil personnalisé pour permettre au modèle de continuer à travailler après cet appel, avant que votre application ne renvoie le résultat.
C’est toujours votre application qui exécute l’outil. Les outils asynchrones ne transfèrent pas l’exécution à
OpenAI et ne gèrent pas vos tâches en arrière-plan.
Ce fonctionnement diffère du mode en arrière-plan, qui exécute la génération de réponses de manière asynchrone. L’appel asynchrone d’outils permet au modèle de continuer à travailler pendant que votre application exécute un outil.
Lorsqu’une tâche se termine, incluez son résultat dans une requête ultérieure à l’API Responses. Utilisez le call_id d’origine de l’API pour associer le résultat à son appel :
| Type d’outil | Élément d’appel | Élément de sortie |
|---|
| Fonction | function_call | function_call_output |
| Personnalisé | custom_tool_call | custom_tool_call_output |
Ajoutez async: true à la définition de l’outil. Les éléments d’appel correspondants dans response.output incluent 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)
La réponse peut contenir à la fois l’appel asynchrone et une réponse à la demande. Si d’autres tours de conversation ont lieu avant la fin de la tâche, mettez à jour latest_response_id pour poursuivre à partir de la dernière réponse tout en conservant le call_id d’origine de l’outil.
Pour lancer la tâche plus tôt avec le streaming, démarrez-la dès que son élément d’appel complet arrive, tout en continuant à recevoir la réponse.
Un outil d’attente permet au modèle de décider à quel moment il a besoin d’un résultat encore en attente. Par exemple, il peut lancer deux requêtes de recherche de prix, travailler sur une tâche indépendante et attendre seulement lorsqu’il est prêt à comparer les prix.
Ajoutez un argument task_handle à chaque outil asynchrone. Le modèle attribue une référence à chaque appel, et votre application l’associe au call_id d’origine de l’API et à la tâche en cours d’exécution. Veillez à ce que les références restent uniques tout au long de la conversation, y compris pour les tâches terminées et les recherches répétées.
Définissez l’outil d’attente comme une fonction synchrone ordinaire : omettez async ou définissez-le sur false. Son schéma et son comportement sont définis par votre application. wait_for_tasks n’est pas un outil intégré à l’API Responses.
Utilisez ces définitions dans le tableau tools de la requête :
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 lookup_price 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
}
}
]
Enregistrez et démarrez chaque tâche avant de traiter un appel d’attente qui en dépend. Les appels peuvent arriver ensemble ou dans plusieurs réponses. Les éléments de sortie suivants illustrent deux lancements et un appel d’attente qui dépend des deux :
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\"]}"
}
]
Le registre de votre application associe chaque référence à son appel d’origine et à la tâche en cours d’exécution :
| Référence de la tâche | ID de l’appel d’origine | Tâche |
|---|
widget_price_1 | call_widget | Recherche du prix de WIDGET |
gadget_price_1 | call_gadget | Recherche du prix de GADGET |
Conservez le registre pendant toute la conversation pour éviter de réutiliser la référence d’une tâche terminée.
Retrouvez les références demandées dans le registre et attendez uniquement les tâches correspondantes. Renvoyez chaque nouveau résultat disponible avec son call_id d’origine, puis renvoyez le statut avec le call_id propre à l’appel d’attente. Cet ordre permet au modèle de disposer des résultats lorsqu’il reprend son travail.
Par exemple, envoyez ces éléments de sortie dans le tableau input de la requête suivante. Les prix sont donnés à titre d’exemple :
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\"]}"
}
]
Définissez previous_response_id sur l’ID de la dernière réponse et incluez les outils et les instructions dans la requête de continuation. Votre application peut aussi transmettre les résultats au fur et à mesure qu’ils sont disponibles, sans appel d’attente. Utilisez l’outil d’attente uniquement lorsque l’étape suivante du modèle dépend de résultats qui ne sont pas encore arrivés.
L’appel asynchrone d’outils est pris en charge par GPT-6 Astra et les modèles ultérieurs.
L’exécution asynchrone s’applique aux outils de type fonction et aux outils personnalisés exécutés par votre application. Elle ne s’applique pas aux outils intégrés hébergés. Utilisez des appels d’outils directs ; ne configurez pas d’outils asynchrones pour l’appel d’outils par programmation.
En mode multi-agent, ne combinez pas les outils asynchrones avec des appels d’outils parallèles.