비동기 도구 호출

비동기 도구 호출 (Async tool calling)

비동기 도구 호출(Async tool calling)을 사용하면 도구의 결과를 기다리지 않고, 모델이 도구를 호출한 뒤에도 계속 작업할 수 있어요. 느린 조회 요청을 일찍 시작하고, 요청의 독립적인 부분에 답하며, 결과가 준비되었을 때 제공하는 데 사용하세요.

출처: 문서

본문

비동기 도구의 작동 방식

일반적인 함수 호출은 모델의 차례를 멈추고 도구 응답을 기다려요. 함수나 커스텀 도구 정의에 async: true를 설정하면, 애플리케이션이 출력을 반환하기 전에 모델이 그 호출을 발행한 뒤에도 계속 작업을 이어갈 수 있어요.

애플리케이션은 여전히 도구를 실행해요. 비동기 도구는 실행을 OpenAI로 옮기거나 백그라운드 작업을 관리하지 않아요.

이는 응답 생성을 비동기로 실행하는 백그라운드 모드와는 달라요. 비동기 도구 호출은 애플리케이션이 도구를 실행하는 동안 모델이 계속 작업하게 해요.

작업이 끝나면 그 출력을 나중의 Responses 요청에 포함하세요. 원래 API call_id를 사용해 결과를 그 호출에 연결하세요:

도구 유형 호출 항목 (Call item) 출력 항목 (Output item)
Function function_call function_call_output
Custom custom_tool_call custom_tool_call_output

비동기 도구 호출하기

도구 정의에 async: true를 추가하세요. response.output의 해당 호출 항목에는 async: true가 포함돼요.

백그라운드에서 날씨 조회를 실행하기

import 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);
import 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)
package 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())
}
import 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()));
require "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)

응답에는 비동기 호출과 답변이 함께 포함될 수 있어요. 작업이 끝나기 전에 다른 대화 턴이 일어나면, 원래 도구 call_id를 유지하면서 latest_response_id를 갱신해 가장 최근 응답에서 이어가세요.

스트리밍으로 더 일찍 발송하려면, 응답을 계속 소비하면서 완전한 호출 항목이 도착했을 때 작업을 시작하세요.

대기(wait) 도구 추가하기

대기 도구를 사용하면 모델이 보류 중인 결과가 필요해졌을 때를 직접 고를 수 있어요. 예를 들어 가격 조회 두 개를 시작하고, 독립적인 작업을 하다가, 가격을 비교할 준비가 되었을 때만 대기할 수 있어요.

각 비동기 도구에 task_handle 인자를 추가하세요. 모델은 각 호출에 핸들을 할당하고, 애플리케이션은 그 핸들을 원래 API call_id와 실행 중인 작업에 연결해요. 완료된 작업과 반복 조회 요청을 포함해 대화 전체에서 핸들을 고유하게 유지하세요.

대기 도구는 async를 생략하거나 false로 설정한 일반 동기 함수로 정의하세요. 그 스키마와 동작은 애플리케이션에 속해요. wait_for_tasks는 내장된 Responses 도구가 아니에요.

요청의 tools 배열에 이 정의들을 사용하세요:

[
  {
    "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
    }
  }
]

각 작업 등록하기

의존하는 대기를 처리하기 전에 각 발송을 등록하고 시작하세요. 호출은 함께 또는 여러 응답에 걸쳐 도착할 수 있어요. 아래 예시 출력 항목은 두 개의 발송과 둘 다에 의존하는 대기를 보여줘요:

[
  {
    "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\"]}"
  }
]

애플리케이션의 레지스트리는 각 핸들을 원래 호출과 실행 중인 작업에 연결해요:

작업 핸들 (Task handle) 원래 호출 ID (Original call ID) 작업 (Job)
widget_price_1 call_widget WIDGET 가격 조회
gadget_price_1 call_gadget GADGET 가격 조회

완료된 작업의 핸들 재사용을 막으려면 레지스트리를 대화 전체에 걸쳐 유지하세요.

대기 상태보다 먼저 결과 전달하기

요청된 핸들을 레지스트리에서 찾아 그 작업들만 기다리세요. 새로 완료된 각 결과를 원래 call_id로 반환하고, 그다음 대기 호출의 자체 call_id로 상태를 반환하세요. 이 순서 덕분에 모델이 재개할 때 결과를 받아요.

예를 들어 다음 요청의 input 배열에 이 출력 항목들을 보내세요. 가격은 예시일 뿐이에요:

[
  {
    "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 도구가 아니에요.

wait_for_tasks와 함께 요청의 tools 배열에 이 정의를 추가하세요:

{
  "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를 등록하고 사용자에게 질문을 표시하세요. 다음 예시 출력 항목은 보고서의 대상을 묻습니다:

{
  "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 배열에 이 출력 항목을 포함하세요:

[
  {
    "type": "function_call_output",
    "call_id": "call_audience",
    "output": "{\"task_handle\":\"report_audience_1\",\"answer\":\"Executives\"}"
  }
]

previous_response_id를 가장 최근 응답 ID로 설정하고, 연속 요청에 도구와 지시사항을 포함하세요. 모델이 report_audience_1에 대해 wait_for_tasks를 호출했다면, 대기 도구 예시처럼 대기 상태보다 먼저 답변을 반환하세요.

비동기 실행은 사용자가 답할 때까지 응답을 열어두지 않아요. 모델에게 독립적인 작업을 계속하고, 답변이 필요한 단계에서는 대기하라고 지시하세요. 애플리케이션이 질문을 닫거나 타임아웃하도록 지원한다면, 모델이 어떻게 진행할지 결정할 수 있도록 명시적인 "답변 없음" 결과를 반환하세요.

호환성

비동기 도구 호출은 GPT-6 Astra 및 이후 모델에서 지원돼요.

비동기 실행은 애플리케이션이 실행하는 함수 및 커스텀 도구에 적용돼요. 호스팅된 내장 도구에는 적용되지 않아요. 직접 도구 호출을 사용하세요. 프로그래매틱 도구 호출에는 비동기 도구를 구성하지 마세요.

멀티 에이전트 모드에서는 비동기 도구를 병렬 도구 호출과 함께 사용하지 마세요.

더 알아보기