함수 호출
함수 호출 (Function calling)
이번엔 모델이 자기 학습 데이터 밖의 정보에 접근하고 외부 시스템과 소통할 수 있게 해주는 강력한 기능, 함수 호출을 배워볼게요. 함수 호출은 **툴 호출(tool calling)**이라고도 불러요. 이 글에서는 모델을 여러분의 앱이 제공하는 데이터와 액션에 연결하는 방법을 살펴볼 거예요. JSON 스키마로 정의하는 함수 툴과, 자유 형식 텍스트 입력·출력을 다루는 커스텀 툴 두 가지를 모두 다룰게요.
출처: 공식문서
Agents API 세션을 쓴다면 Functions를 통해 함수를 등록하고 세션의 액션 요청을 처리해요. 이 글의 예시들은 Responses API와 Chat Completions 통합을 기준으로 보여드릴게요.
여러분의 앱에 함수가 많거나 스키마가 크다면, 함수 호출을 툴 검색(tool search)과 함께 사용해 자주 안 쓰는 툴은 지연 로드하고 모델이 필요로 할 때만 불러올 수 있어요. tool_search는 gpt-5.4 이후 모델에서만 지원돼요.
GPT-6 Astra는 툴 호출을 위해 Responses API가 필요해요. 아래 Chat Completions 예시는 호환성을 위해 GPT-5.6을 사용해요. 기존 통합을 업데이트하고 싶다면 마이그레이션 가이드를 참고하세요.
작동 원리 (How it works)
툴 호출은 여러분의 애플리케이션과 모델 사이에서 일어나는 다단계 대화예요. 먼저 툴 호출에 쓰이는 핵심 용어 몇 가지를 정리하고, 실제 예시로 감을 잡아볼게요.
툴 — 모델에게 주는 기능
**함수(function)**나 **툴(tool)**은 모델이 접근할 수 있다고 알려주는 하나의 기능 조각을 추상적으로 가리켜요. 모델이 프롬프트에 응답을 생성하다 보면, 프롬프트의 지시를 따르기 위해 툴이 제공하는 데이터나 기능이 필요하다고 판단할 수 있어요.
예를 들어 모델에게 이런 툴을 줄 수 있어요:
- 특정 위치의 오늘 날씨 가져오기
- 주어진 사용자 ID의 계정 정보 접근하기
- 분실 주문에 대한 환불 처리하기
프롬프트에 응답하는 과정에서 모델이 알거나 할 수 있길 바라는 것이라면 무엇이든 툴로 줄 수 있어요.
프롬프트와 함께 API 요청을 만들 때, 모델이 사용을 고려할 수 있는 툴 목록을 함께 넣을 수 있어요. 예를 들어 전 세계 어딘가의 현재 날씨에 대한 질문에 답할 수 있게 하려면, location을 인자로 받는 get_weather 툴을 제공하면 되죠.
툴 호출 — 모델이 툴 사용을 요청
**함수 호출(function call)**이나 **툴 호출(tool call)**은 모델이 프롬프트를 살펴본 뒤, 프롬프트의 지시를 따르기 위해 우리가 제공한 툴 중 하나를 호출해야 한다고 판단했을 때 받을 수 있는 특별한 종류의 응답이에요.
API 요청에서 "파리 날씨가 어때?" 같은 프롬프트를 받으면, 모델은 location 인자에 Paris를 넣은 get_weather 툴에 대한 툴 호출로 응답할 수 있어요.
툴 호출 출력 — 모델을 위해 우리가 만드는 출력
**함수 호출 출력(function call output)**이나 **툴 호출 출력(tool call output)**은 모델의 툴 호출에서 받은 입력을 사용해 툴이 생성하는 응답이에요. 툴 호출 출력은 구조화된 JSON이나 평문 텍스트일 수 있고, 특정 모델 툴 호출을 가리키는 참조(곧 나올 예시에서 call_id로 참조)를 담아야 해요.
날씨 예시를 완성해 보면:
- 모델은
location을 인자로 받는get_weather툴에 접근할 수 있어요. - "파리 날씨가 어때?" 같은 프롬프트에 응답으로 모델은
Paris라는 값을 가진location인자를 담은 툴 호출을 반환해요. - 툴 호출 출력은 JSON 객체(예:
{"temperature": "25", "unit": "C"}— 현재 온도가 25도임을 나타냄), 이미지 내용, 또는 파일 내용을 반환할 수 있어요.
그다음 툴 정의, 원래 프롬프트, 모델의 툴 호출, 툴 호출 출력을 전부 다시 모델에게 보내서 다음과 같은 텍스트 응답을 받아요:
오늘 파리의 날씨는 25C입니다.
함수 대 툴 (Functions versus tools)
- 함수는 JSON 스키마로 정의되는 특정 종류의 툴이에요. 함수 정의 덕분에 모델이 여러분의 애플리케이션에 데이터를 전달할 수 있고, 여러분의 코드는 모델이 제안한 데이터에 접근하거나 액션을 취할 수 있어요.
- 함수 툴에 더해, 이 글에서 다루는 커스텀 툴은 자유 형식 텍스트 입력·출력으로 동작해요.
- OpenAI 플랫폼의 일부인 빌트인 툴도 있어요. 이런 툴은 모델이 웹 검색, 코드 실행을 하게 하고 MCP 서버의 기능에 접근하게 하는 등 다양한 일을 할 수 있게 해줘요.
툴 호출 흐름 (The tool calling flow)
툴 호출은 OpenAI API를 통해 여러분의 애플리케이션과 모델 사이에서 일어나는 다단계 대화예요. 툴 호출 흐름은 크게 다섯 단계로 나눌 수 있어요:
- 모델이 호출할 수 있는 툴과 함께 요청을 만든다
- 모델로부터 툴 호출을 받는다
- 툴 호출의 입력으로 애플리케이션 쪽에서 코드를 실행한다
- 툴 출력과 함께 두 번째 요청을 모델에게 보낸다
- 모델로부터 최종 응답을 받는다 (또는 더 많은 툴 호출)

Responses를 사용하면 애플리케이션이 작업이 요구하는 만큼 이 흐름을 계속 이어갈 수 있어요. 그 주변의 반복적인 오케스트레이션을 패키징한 프레임워크를 원한다면, Responses API가 Agents SDK와 어떻게 비교되는지를 참고하세요.
함수 툴 예시 (Function tool example)
별자리별 오늘의 운세를 가져오는 get_horoscope 함수를 위한 종단간 툴 호출 흐름을 살펴볼게요.
완전한 툴 호출 예시
import OpenAI from "openai";
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";
const openai = new OpenAI();
// 1. 모델이 호출할 수 있는 툴 목록 정의
/** @type {OpenAI.Responses.Tool[]} */
const tools = [
{
type: "function",
name: "get_horoscope",
description: "Get today's horoscope for an astrological sign.",
parameters: {
type: "object",
properties: {
sign: {
type: "string",
description: "An astrological sign like Taurus or Aquarius",
},
},
required: ["sign"],
additionalProperties: false,
},
strict: true,
},
];
function getHoroscope(sign) {
return `${sign}: Next Tuesday you will befriend a baby otter.`;
}
// 시간이 지나며 계속 추가할 실행 중인 입력 목록 생성
/** @type {OpenAI.Responses.ResponseInput} */
let input = [
{ role: "user", content: "What is my horoscope? I am an Aquarius." },
];
// 2. 정의된 툴과 함께 모델에게 프롬프트
let response = await openai.responses.create({
model: "gpt-6-astra",
tools,
input,
});
// 다음 턴을 위해 모델 출력 보존
input.push(...toResponseInputItems(response.output));
for (const item of response.output) {
if (item.type !== "function_call") continue;
if (item.name === "get_horoscope") {
// 3. get_horoscope 함수 로직 실행
const { sign } = JSON.parse(item.arguments);
const horoscope = getHoroscope(sign);
// 4. 함수 호출 결과를 모델에 제공
input.push({
type: "function_call_output",
call_id: item.call_id,
output: horoscope,
});
}
}
console.log("Final input:");
console.log(JSON.stringify(input, null, 2));
response = await openai.responses.create({
model: "gpt-6-astra",
instructions: "Respond only with a horoscope generated by a tool.",
tools,
input,
});
// 5. 모델은 응답을 줄 수 있어야 한다
console.log("Final output:");
console.log(response.output_text);
from openai import OpenAI
import json
client = OpenAI()
# 1. 모델이 호출할 수 있는 툴 목록 정의
tools = [
{
"type": "function",
"name": "get_horoscope",
"description": "Get today's horoscope for an astrological sign.",
"parameters": {
"type": "object",
"properties": {
"sign": {
"type": "string",
"description": "An astrological sign like Taurus or Aquarius",
},
},
"required": ["sign"],
},
}
]
def get_horoscope(sign):
return f"{sign}: Next Tuesday you will befriend a baby otter."
# 시간이 지나며 계속 추가할 실행 중인 입력 목록 생성
input_list = [{"role": "user", "content": "What is my horoscope? I am an Aquarius."}]
# 2. 정의된 툴과 함께 모델에게 프롬프트
response = client.responses.create(
model="gpt-6-astra",
tools=tools,
input=input_list,
)
# 후속 요청을 위해 함수 호출 출력 저장
input_list += response.output
for item in response.output:
if item.type == "function_call":
if item.name == "get_horoscope":
# 3. get_horoscope 함수 로직 실행
sign = json.loads(item.arguments)["sign"]
horoscope = get_horoscope(sign)
# 4. 함수 호출 결과를 모델에 제공
input_list.append(
{
"type": "function_call_output",
"call_id": item.call_id,
"output": horoscope,
}
)
print("Final input:")
print(input_list)
response = client.responses.create(
model="gpt-6-astra",
instructions="Respond only with a horoscope generated by a tool.",
tools=tools,
input=input_list,
)
# 5. 모델은 응답을 줄 수 있어야 한다
print("Final output:")
print(response.model_dump_json(indent=2))
print("\n" + response.output_text)
package main
import (
"context"
"encoding/json"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
func main() {
client := openai.NewClient()
tool := horoscopeResponseTool()
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("What is my horoscope? I am an Aquarius.")},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
var functionOutput responses.ResponseInputItemUnionParam
for _, output := range response.Output {
if output.Type != "function_call" {
continue
}
call := output.AsFunctionCall()
if call.Name != "get_horoscope" {
continue
}
var arguments struct {
Sign string `json:"sign"`
}
if err := json.Unmarshal([]byte(call.Arguments), &arguments); err != nil {
panic(err)
}
functionOutput = responses.ResponseInputItemParamOfFunctionCallOutput(getHoroscope(arguments.Sign))
functionOutput.OfFunctionCallOutput.CallID = openai.String(call.CallID)
}
if functionOutput.OfFunctionCallOutput == nil {
panic("the model did not call get_horoscope")
}
response, err = client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
PreviousResponseID: openai.String(response.ID),
Instructions: openai.String("Respond only with a horoscope generated by a tool."),
Input: responses.ResponseNewParamsInputUnion{OfInputItemList: responses.ResponseInputParam{functionOutput}},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
fmt.Println(response.OutputText())
}
func horoscopeResponseTool() responses.ToolUnionParam {
parameters := map[string]any{
"type": "object",
"properties": map[string]any{
"sign": map[string]any{"type": "string", "description": "An astrological sign like Taurus or Aquarius"},
},
"required": []string{"sign"},
"additionalProperties": false,
}
tool := responses.ToolParamOfFunction("get_horoscope", parameters, true)
tool.OfFunction.Description = openai.String("Get today's horoscope for an astrological sign.")
return tool
}
func getHoroscope(sign string) string {
return fmt.Sprintf("%s: Next Tuesday you will befriend a baby otter.", sign)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.models.responses.FunctionTool;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseInputItem;
import java.util.List;
import java.util.Map;
FunctionTool horoscope =
FunctionTool.builder()
.name("get_horoscope")
.description("Get today's horoscope for an astrological sign.")
.parameters(
FunctionTool.Parameters.builder()
.putAdditionalProperty("type", JsonValue.from("object"))
.putAdditionalProperty(
"properties",
JsonValue.from(
Map.of(
"sign",
Map.of(
"type", "string",
"description",
"An astrological sign like Taurus or Aquarius"))))
.putAdditionalProperty("required", JsonValue.from(List.of("sign")))
.putAdditionalProperty("additionalProperties", JsonValue.from(false))
.build())
.strict(true)
.build();
var firstResponse =
client
.responses()
.create(
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("What is my horoscope? I am an Aquarius.")
.addTool(horoscope)
.build());
var functionCall =
firstResponse.output().stream()
.flatMap(item -> item.functionCall().stream())
.filter(call -> call.name().equals("get_horoscope"))
.findFirst()
.orElseThrow(() -> new IllegalStateException("The model did not call get_horoscope"));
record HoroscopeArguments(String sign) {}
String sign = functionCall.arguments(HoroscopeArguments.class).sign();
ResponseCreateParams followUp =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.instructions("Respond only with a horoscope generated by a tool.")
.previousResponseId(firstResponse.id())
.inputOfResponse(
List.of(
ResponseInputItem.ofFunctionCallOutput(
ResponseInputItem.FunctionCallOutput.builder()
.callId(functionCall.callId())
.output(sign + ": Embrace an unexpected opportunity today.")
.build())))
.addTool(horoscope)
.build();
client.responses().create(followUp).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"
client = OpenAI::Client.new
tools = [
{
type: :function,
name: "get_horoscope",
description: "Get today's horoscope for an astrological sign.",
parameters: {
type: :object,
properties: { sign: { type: :string } },
required: ["sign"],
additionalProperties: false
},
strict: true
}
]
first_response = client.responses.create(
model: "gpt-6-astra",
input: "What is my horoscope? I am an Aquarius.",
tools: tools
)
function_call = first_response.output.find do |item|
item.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall) &&
item.name == "get_horoscope"
end
unless function_call.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall)
raise "The model did not call get_horoscope"
end
arguments = JSON.parse(function_call.arguments, symbolize_names: true)
sign = arguments.fetch(:sign)
response = client.responses.create(
model: "gpt-6-astra",
previous_response_id: first_response.id,
input: [
{
type: :function_call_output,
call_id: function_call.call_id,
output: "#{sign}: Embrace an unexpected opportunity today."
}
],
tools: tools
)
puts(response.output_text)
참고로 GPT-5나 o4-mini 같은 추론 모델의 경우, 툴 호출과 함께 돌아오는 응답의 추론 항목을 툴 호출 출력과 함께 다시 전달해야 해요.
함수 정의하기 (Defining functions)
함수는 보통 각 API 요청의 tools 파라미터에 선언해요. 툴 검색을 쓰면 애플리케이션이 지연 함수를 상호작용 중에 나중에 로드할 수도 있어요. 어느 쪽이든 각 호출 가능한 함수는 동일한 스키마 형태를 사용해요. 함수 정의는 다음 속성들을 가져요:
| 필드 | 설명 |
|---|---|
type |
항상 function이어야 해요 |
name |
함수의 이름 (예: get_weather) |
description |
함수를 언제·어떻게 사용하는지에 대한 세부 사항 |
parameters |
함수의 입력 인자를 정의하는 JSON 스키마 |
strict |
함수 호출에 엄격 모드(strict mode)를 적용할지 여부 |
다음은 get_weather 함수의 정의 예시예요:
{
"type": "function",
"name": "get_weather",
"description": "Retrieves current weather for the given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country e.g. Bogotá, Colombia"
},
"units": {
"type": "string",
"enum": ["celsius", "fahrenheit"],
"description": "Units the temperature will be returned in."
}
},
"required": ["location", "units"],
"additionalProperties": false
},
"strict": true
}
parameters가 JSON 스키마로 정의되기 때문에, 속성 타입, enum, 설명, 중첩 객체, 재귀 객체 같은 풍부한 기능을 활용할 수 있어요.
네임스페이스 정의하기 (Defining namespaces)
네임스페이스를 사용해 crm, billing, shipping 같은 도메인별로 관련 툴을 묶을 수 있어요. 네임스페이스는 비슷한 툴을 정리하는 데 도움을 주고, 모델이 서로 다른 시스템이나 목적을 위한 툴(예: CRM용 검색 툴 하나, 지원 티켓팅 시스템용 검색 툴 하나) 중에서 선택해야 할 때 특히 유용해요.
{
"type": "namespace",
"name": "crm",
"description": "CRM tools for customer lookup and order management.",
"tools": [
{
"type": "function",
"name": "get_customer_profile",
"description": "Fetch a customer profile by customer ID.",
"parameters": {
"type": "object",
"properties": {
"customer_id": { "type": "string" }
},
"required": ["customer_id"],
"additionalProperties": false
}
},
{
"type": "function",
"name": "list_open_orders",
"description": "List open orders for a customer ID.",
"defer_loading": true,
"parameters": {
"type": "object",
"properties": {
"customer_id": { "type": "string" }
},
"required": ["customer_id"],
"additionalProperties": false
}
}
]
}
툴 검색 (Tool search)
대규모 툴 생태계에 모델을 노출해야 한다면, tool_search로 일부 또는 모든 툴의 로드를 지연할 수 있어요. tool_search 툴은 모델이 관련 툴을 검색해서 모델 컨텍스트에 추가한 뒤 사용하게 해줘요. gpt-5.4 이후 모델에서만 지원돼요. 자세한 내용은 툴 검색 가이드를 참고하세요.
함수 정의 모범 사례 (Best practices for defining functions)
-
명확하고 상세한 함수 이름, 파라미터 설명, 지시사항을 작성하세요.
- 함수의 목적과 각 파라미터(및 그 형식), 출력이 무엇을 나타내는지 명시적으로 설명하세요.
- 시스템 프롬프트로 각 함수를 언제(그리고 언제 쓰지 말아야) 할지 설명하세요. 일반적으로 모델에게 정확히 무엇을 할지 말해주는 편이 좋아요.
- 예시와 엣지 케이스를 포함하세요. 특히 반복되는 실패를 고치는 데 유용해요. (참고: 예시 추가는 추론 모델의 성능을 해칠 수 있어요.)
- 지연 툴은 상세 안내를 함수 설명에 두고, 네임스페이스 설명은 간결하게 유지하세요. 네임스페이스는 모델이 무엇을 로드할지 고르는 데, 함수 설명은 로드된 툴을 올바르게 사용하는 데 도움을 줘요.
-
소프트웨어 공학 모범 사례를 적용하세요.
- 함수를 예측 가능하고 직관적으로 만드세요. (최소 놀람 원칙)
- enum과 객체 구조를 사용해 잘못된 상태를 방지하세요. 예를 들어
toggle_light(on: bool, off: bool)은 잘못된 호출을 허용해요. - 인턴 테스트를 통과하세요. 인턴/사람이 모델에게 준 것만으로 함수를 올바르게 사용할 수 있을까요? (아니라면 어떤 질문을 하게 될까요? 답을 프롬프트에 추가하세요.)
-
모델의 부담을 덜고 가능하면 코드를 사용하세요.
- 이미 아는 인자를 모델이 채우게 하지 마세요. 예를 들어 이전 메뉴에서 이미
order_id를 알고 있다면,order_id파라미터를 넣지 말고 파라미터 없는submit_refund()를 정의하고order_id는 코드에서 전달하세요. - 항상 순서대로 호출되는 함수는 결합하세요. 예를 들어 항상
query_location()다음에mark_location()을 호출한다면, 마킹 로직을 쿼리 함수 호출로 옮기세요.
- 이미 아는 인자를 모델이 채우게 하지 마세요. 예를 들어 이전 메뉴에서 이미
-
정확도를 높이기 위해 처음에 사용 가능한 함수 수를 적게 유지하세요.
- 서로 다른 함수 개수로 성능을 평가하세요.
- 한 턴 시작 시 20개 미만의 함수를 노출하는 것을 목표로 하세요. 다만 이는 엄격한 규칙이 아닌 부드러운 제안이에요.
- 툴 검색을 사용해 크거나 드물게 쓰는 툴 표면의 일부를 미리 다 노출하는 대신 지연하세요.
-
OpenAI 리소스를 활용하세요.
- Playground에서 함수 스키마를 생성하고 반복하세요. (Playground)
- 함수가 많거나 어려운 작업에는 파인튜닝으로 함수 호출 정확도를 높이는 것을 고려하세요. (쿡북)
토큰 사용량 (Token Usage)
내부적으로 함수는 모델이 훈련받은 문법으로 시스템 메시지에 주입돼요. 즉 호출 가능한 함수 정의는 모델의 컨텍스트 제한에 포함되고 입력 토큰으로 청구돼요. 토큰 제한에 부딪힌다면, 처음에 로드하는 함수 수를 제한하거나, 설명을 최대한 줄이거나, 지연 툴이 필요할 때만 로드되도록 툴 검색을 사용하는 것을 제안해요.
툴 스펙에 정의된 함수가 많다면 파인튜닝을 사용해 사용되는 토큰 수를 줄이는 것도 가능해요.
함수 호출 처리하기 (Handling function calls)
모델이 함수를 호출하면, 여러분은 그 함수를 실행하고 결과를 반환해야 해요. 모델 응답에는 0개, 1개 또는 여러 개의 호출이 포함될 수 있으므로, 여러 개가 있다고 가정하는 것이 모범 사례예요.
응답의 output 배열에는 type 값이 function_call인 항목이 포함돼요. 각 항목에는 call_id(나중에 함수 결과를 제출하는 데 사용), name, JSON으로 인코딩된 arguments가 있어요.
여러 함수 호출이 포함된 응답 예시
[
{
"id": "fc_12345xyz",
"call_id": "call_12345xyz",
"type": "function_call",
"name": "get_weather",
"arguments": "{\"location\":\"Paris, France\"}"
},
{
"id": "fc_67890abc",
"call_id": "call_67890abc",
"type": "function_call",
"name": "get_weather",
"arguments": "{\"location\":\"Bogotá, Colombia\"}"
},
{
"id": "fc_99999def",
"call_id": "call_99999def",
"type": "function_call",
"name": "send_email",
"arguments": "{\"to\":\"[email protected]\",\"body\":\"Hi bob\"}"
}
]
툴 검색을 쓰고 있다면, function_call 앞에 tool_search_call과 tool_search_output 항목도 볼 수 있어요. 함수가 로드되면 여기에서 보여주는 것과 같은 방식으로 함수 호출을 처리하면 돼요.
함수 호출 실행 및 결과 추가
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";
input.push(...toResponseInputItems(response.output));
for (const toolCall of response.output) {
if (toolCall.type !== "function_call") {
continue;
}
const name = toolCall.name;
const args = JSON.parse(toolCall.arguments);
const result = await callFunction(name, args);
input.push({
type: "function_call_output",
call_id: toolCall.call_id,
output: result.toString(),
});
}
input_messages += response.output
for tool_call in response.output:
if tool_call.type != "function_call":
continue
name = tool_call.name
args = json.loads(tool_call.arguments)
result = call_function(name, args)
input_messages.append(
{
"type": "function_call_output",
"call_id": tool_call.call_id,
"output": json.dumps(result),
}
)
input = append(input, responseOutputAsInput(response.Output)...)
for _, output := range response.Output {
if output.Type != "function_call" {
continue
}
toolCall := output.AsFunctionCall()
var arguments functionArguments
if err := json.Unmarshal([]byte(toolCall.Arguments), &arguments); err != nil {
panic(err)
}
result, err := callFunction(toolCall.Name, arguments)
if err != nil {
panic(err)
}
toolOutput := responses.ResponseInputItemParamOfFunctionCallOutput(result)
toolOutput.OfFunctionCallOutput.CallID = openai.String(toolCall.CallID)
input = append(input, toolOutput)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.models.responses.EasyInputMessage;
import com.openai.models.responses.FunctionTool;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseInputItem;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
response.output().stream()
.map(item -> JsonValue.from(item).convert(ResponseInputItem.class))
.forEach(input::add);
response.output().stream()
.flatMap(item -> item.functionCall().stream())
.forEach(
call -> {
String result;
if (call.name().equals("get_weather")) {
record Coordinates(double latitude, double longitude) {}
Coordinates coordinates = call.arguments(Coordinates.class);
result =
JsonValue.from(
Map.of(
"latitude", coordinates.latitude(),
"longitude", coordinates.longitude(),
"temperature_c", 18))
.toString();
} else if (call.name().equals("send_email")) {
record Email(String to, String body) {}
Email message = call.arguments(Email.class);
result = JsonValue.from(Map.of("to", message.to(), "status", "sent")).toString();
} else {
throw new IllegalArgumentException("Unknown function: " + call.name());
}
var output =
ResponseInputItem.ofFunctionCallOutput(
ResponseInputItem.FunctionCallOutput.builder()
.callId(call.callId())
.output(result)
.build());
input.add(output);
System.out.println(call.callId() + " " + result);
});
input.concat(response.output)
response.output.each do |tool_call|
next unless tool_call.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall)
arguments = JSON.parse(tool_call.arguments)
result = call_function(tool_call.name, arguments)
input << {
type: :function_call_output,
call_id: tool_call.call_id,
output: JSON.generate(result)
}
end
위 예시에서 각 호출을 라우팅하는 가상의 call_function이 있다고 가정했어요. 가능한 구현은 다음과 같아요:
함수 호출 실행 및 결과 추가
const callFunction = async (name, args) => {
if (name === "get_weather") {
return getWeather(args.latitude, args.longitude);
}
if (name === "send_email") {
return sendEmail(args.to, args.body);
}
throw new Error(`Unknown function: ${name}`);
};
def call_function(name, args):
if name == "get_weather":
return get_weather(**args)
if name == "send_email":
return send_email(**args)
raise ValueError(f"Unknown function: {name}")
func callFunction(name string, arguments functionArguments) (string, error) {
switch name {
case "get_weather":
return getWeather(arguments.Location), nil
case "send_email":
return sendEmail(arguments.To, arguments.Body), nil
default:
return "", fmt.Errorf("unknown function: %s", name)
}
}
def call_function(name, arguments)
case name
when "get_weather"
FunctionCallingExample.get_weather(
arguments.fetch("latitude"),
arguments.fetch("longitude")
)
when "send_email"
FunctionCallingExample.send_email(
arguments.fetch("to"),
arguments.fetch("body")
)
else
raise ArgumentError, "Unknown function: #{name}"
end
end
결과 포맷팅 (Formatting results)
function_call_output 메시지에 전달하는 결과는 보통 문자열이어야 하고, 형식은 여러분의 몫이에요(JSON, 오류 코드, 평문 등). 모델은 그 문자열을 필요에 따라 해석해요.
이미지나 파일을 반환하는 함수라면 문자열 대신 이미지·파일 객체 배열을 전달할 수 있어요.
함수에 반환 값이 없다면(예: send_email), "success"처럼 성공·실패를 나타내는 문자열을 반환하세요.
결과를 응답에 통합하기 (Incorporating results into response)
결과를 input에 추가한 뒤, 모델에 다시 보내 최종 응답을 받을 수 있어요.
모델에 결과 다시 보내기
const response = await openai.responses.create({
model: "gpt-6-astra",
input,
tools,
});
response = client.responses.create(
model="gpt-6-astra",
input=input_messages,
tools=responses_tools,
)
print(response.output_text)
response, err = client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfInputItemList: input},
Tools: tools,
})
if err != nil {
panic(err)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.models.responses.EasyInputMessage;
import com.openai.models.responses.FunctionTool;
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;
FunctionTool weather =
FunctionTool.builder()
.name("get_weather")
.description("Get the weather for a city.")
.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())
.strict(true)
.build();
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.inputOfResponse(
List.of(
ResponseInputItem.ofEasyInputMessage(
EasyInputMessage.builder()
.role(EasyInputMessage.Role.USER)
.content("What is the weather like in Paris?")
.build()),
ResponseInputItem.ofFunctionCall(
ResponseFunctionToolCall.builder()
.callId("call_weather")
.name("get_weather")
.arguments("{\"city\":\"Paris\"}")
.build()),
ResponseInputItem.ofFunctionCallOutput(
ResponseInputItem.FunctionCallOutput.builder()
.callId("call_weather")
.output("{\"city\":\"Paris\",\"temperature_c\":18}")
.build())))
.addTool(weather)
.build();
client.responses().create(params).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 "openai"
client = OpenAI::Client.new
input = [
{
role: :user,
content: "What is the weather like in Paris?"
},
{
type: :function_call,
call_id: "call_weather",
name: "get_weather",
arguments: '{"city":"Paris"}'
},
{
type: :function_call_output,
call_id: "call_weather",
output: '{"city":"Paris","temperature_c":18}'
}
]
tools = [
{
type: :function,
name: "get_weather",
description: "Get the weather for a city",
parameters: {
type: :object,
properties: { city: { type: :string } },
required: ["city"],
additionalProperties: false
},
strict: true
}
]
response = client.responses.create(
model: "gpt-6-astra",
input: input,
tools: tools
)
puts(response.output_text)
최종 응답
"It's about 15°C in Paris, 18°C in Bogotá, and I've sent that email to Bob."
추가 설정 (Additional configurations)
툴 선택 (Tool choice)
기본적으로 모델은 언제, 몇 개의 툴을 사용할지 결정해요. tool_choice 파라미터로 특정 동작을 강제할 수 있어요.
- Auto: (기본값) 0개, 1개, 또는 여러 함수를 호출해요.
tool_choice: "auto" - Required: 하나 이상의 함수를 호출해요.
tool_choice: "required" - Forced Function: 정확히 하나의 특정 함수를 호출해요.
tool_choice: {"type": "function", "name": "get_weather"} - Allowed tools: 모델이 만들 수 있는 툴 호출을 모델에게 제공된 툴의 부분집합으로 제한해요.
allowed_tools를 쓸 때
모델 요청 전반에서 툴의 일부만 사용 가능하게 하고 싶은데 전달하는 툴 목록은 바꾸고 싶지 않을 때 allowed_tools 목록을 설정할 수 있어요. 이렇게 하면 프롬프트 캐싱으로 얻는 절약을 최대화할 수 있어요.
"tool_choice": {
"type": "allowed_tools",
"mode": "auto",
"tools": [
{ "type": "function", "name": "get_weather" },
{ "type": "function", "name": "search_docs" }
]
}
tool_choice를 "none"으로 설정해 함수를 전달하지 않은 것과 같은 동작을 흉내낼 수도 있어요.
툴 검색을 사용할 때 tool_choice는 턴에서 현재 호출 가능한 툴에 여전히 적용돼요. 이는 툴 일부를 로드한 뒤 그 부분집합으로 모델을 제한하고 싶을 때 가장 유용해요.
병렬 함수 호출 (Parallel function calling)
GPT-5부터 시작하는 지원 모델에서는 빌트인 툴도 함께 사용 가능할 때 함수를 병렬로 호출할 수 있어요. 빌트인 툴은 병렬 함수 호출 배치에 포함될 수 없어요.
모델은 한 턴에서 여러 함수를 호출하기로 선택할 수 있어요. parallel_tool_calls를 false로 설정하면 정확히 0개 또는 1개의 툴만 호출되도록 방지할 수 있어요.
참고: 현재 파인튜닝된 모델을 사용하고 모델이 한 턴에서 여러 함수를 호출하면, 그 호출들에 대해서는 엄격 모드가 해제돼요.
gpt-4.1-nano-2025-04-14 참고: 이 gpt-4.1-nano 스냅샷은 병렬 툴 호출이 활성화된 경우 때때로 같은 툴에 대해 여러 툴 호출을 포함할 수 있어요. 이 스냅샷을 사용할 때는 이 기능을 비활성화할 것을 권장해요.
엄격 모드 (Strict mode)
strict를 true로 설정하면 함수 호출이 최선 노력(best effort)이 아니라 함수 스키마를 안정적으로 따르도록 보장해요. 엄격 모드는 항상 켜는 것을 권장해요.
내부적으로 엄격 모드는 구조화된 출력 기능을 활용하기 때문에 몇 가지 요구사항이 생겨요:
parameters의 각 객체에 대해additionalProperties를false로 설정해야 해요.properties의 모든 필드를required로 표시해야 해요.
선택적 필드는 type 옵션으로 null을 추가해 표시할 수 있어요 (아래 예시 참고).
strict: true를 보내면서 스키마가 위 요구사항을 충족하지 못하면, 놓친 제약 조건에 대한 세부 사항과 함께 요청이 거부돼요. strict를 생략하면 기본값은 API에 따라 달라져요: Responses 요청은 가능할 때 스키마를 엄격 모드로 정규화하려 시도하고, 스키마가 엄격 모드와 호환될 수 없으면 비엄격(non-strict) 최선 노력 함수 호출로 폴백해요. 폴백이 일어나면 응답 툴이 strict: false를 보여줘요. Chat Completions 요청은 기본적으로 비엄격으로 유지돼요. Responses에서 엄격 모드를 꺼두고 비엄격 최선 노력 함수 호출을 유지하려면 명시적으로 strict: false를 설정하세요.
엄격 모드 활성화
{
"type": "function",
"name": "get_weather",
"description": "Retrieves current weather for the given location.",
//highlight-start
"strict": true,
//highlight-end
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country e.g. Bogotá, Colombia"
},
"units": {
//highlight-start
"type": ["string", "null"],
//highlight-end
"enum": ["celsius", "fahrenheit"],
"description": "Units the temperature will be returned in."
}
},
//highlight-start
"required": ["location", "units"],
"additionalProperties": false
//highlight-end
}
}
엄격 모드 비활성화
{
"type": "function",
"name": "get_weather",
"description": "Retrieves current weather for the given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country e.g. Bogotá, Colombia"
},
"units": {
//highlight-start
"type": "string",
//highlight-end
"enum": ["celsius", "fahrenheit"],
"description": "Units the temperature will be returned in."
}
},
//highlight-start
"required": ["location"],
//highlight-end
}
}
Playground에서 생성된 모든 스키마는 엄격 모드가 활성화되어 있어요.
엄격 모드를 켜는 것을 권장하지만 몇 가지 제약이 있어요:
- JSON 스키마의 일부 기능은 지원되지 않아요. (지원 스키마 참고)
특히 파인튜닝된 모델에 대해서는:
- 스키마는 첫 요청에서 추가 처리를 거치고(그 후 캐시됨) 이로 인해 요청 간 스키마가 달라지면 지연 시간이 높아질 수 있어요.
- 스키마는 성능을 위해 캐시되어 제로 데이터 보존 대상이 아니에요.
스트리밍 (Streaming)
스트리밍을 사용하면 모델이 인자를 채우면서 어떤 함수가 호출되는지 보여주고, 심지어 인자를 실시간으로 표시하며 진행 상황을 보여줄 수 있어요.
스트리밍 함수 호출은 일반 응답 스트리밍과 매우 비슷해요: stream을 true로 설정하고 서로 다른 event 객체를 받으면 돼요.
스트리밍 함수 호출
import { OpenAI } from "openai";
const openai = new OpenAI();
/** @type {OpenAI.Responses.Tool[]} */
const 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,
},
];
const stream = await openai.responses.create({
model: "gpt-6-astra",
input: [{ role: "user", content: "What's the weather like in Paris today?" }],
tools,
stream: true,
store: true,
});
for await (const event of stream) {
console.log(event);
}
from openai import OpenAI
client = OpenAI()
tools = [
{
"type": "function",
"name": "get_weather",
"description": "Get current temperature for a given location.",
"parameters": {
"type": "object",
"properties": {
"location": {
"type": "string",
"description": "City and country e.g. Bogotá, Colombia",
}
},
"required": ["location"],
"additionalProperties": False,
},
}
]
stream = client.responses.create(
model="gpt-6-astra",
input=[{"role": "user", "content": "What's the weather like in Paris today?"}],
tools=tools,
stream=True,
)
for event in stream:
print(event)
package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
func main() {
client := openai.NewClient()
parameters := map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{"type": "string", "description": "City and country e.g. Bogotá, Colombia"},
},
"required": []string{"location"},
"additionalProperties": false,
}
tool := responses.ToolParamOfFunction("get_weather", parameters, true)
stream := client.Responses.NewStreaming(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("What's the weather like in Paris today?")},
Tools: []responses.ToolUnionParam{tool},
})
for stream.Next() {
fmt.Println(stream.Current().Type)
}
if err := stream.Err(); err != nil {
panic(err)
}
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.core.http.StreamResponse;
import com.openai.models.responses.FunctionTool;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseStreamEvent;
import java.util.List;
import java.util.Map;
FunctionTool weather =
FunctionTool.builder()
.name("get_weather")
.description("Get the weather for a city.")
.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())
.strict(true)
.build();
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("What is the weather in Paris?")
.addTool(weather)
.build();
try (StreamResponse<ResponseStreamEvent> stream = client.responses().createStreaming(params)) {
stream.stream()
.forEach(
event -> {
System.out.println(event);
event
.outputItemAdded()
.ifPresent(added -> System.out.println("response.output_item.added: " + added));
event
.functionCallArgumentsDelta()
.ifPresent(
delta ->
System.out.println("response.function_call_arguments.delta: " + delta));
});
}
require "openai"
client = OpenAI::Client.new
stream = client.responses.stream(
model: "gpt-6-astra",
input: "What is the weather in Paris?",
tools: [
{
type: :function,
name: "get_weather",
description: "Get the weather for a city",
parameters: {
type: :object,
properties: { city: { type: :string } },
required: ["city"],
additionalProperties: false
},
strict: true
}
]
)
stream.each { |event| puts(event.type) }
출력 이벤트
{"type":"response.output_item.added","response_id":"resp_1234xyz","output_index":0,"item":{"type":"function_call","id":"fc_1234xyz","call_id":"call_1234xyz","name":"get_weather","arguments":""}}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"{\""}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"location"}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"\":\""}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"Paris"}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":","}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":" France"}
{"type":"response.function_call_arguments.delta","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"delta":"\""}
{"type":"response.function_call_arguments.done","response_id":"resp_1234xyz","item_id":"fc_1234xyz","output_index":0,"arguments":"{\"location\":\"Paris, France\"}"}
{"type":"response.output_item.done","response_id":"resp_1234xyz","output_index":0,"item":{"type":"function_call","id":"fc_1234xyz","call_id":"call_1234xyz","name":"get_weather","arguments":"{\"location\":\"Paris, France\"}"}}
하지만 청크를 하나의 content 문자열로 합치는 대신, 청크를 인코딩된 arguments JSON 객체로 합치게 돼요.
모델이 하나 이상의 함수를 호출하면 각 함수 호출에 대해 response.output_item.added 타입의 이벤트가 방출되고, 다음 필드가 포함돼요:
| 필드 | 설명 |
|---|---|
response_id |
함수 호출이 속한 응답의 id |
output_index |
응답에서 출력 항목의 인덱스. 응답 내 개별 함수 호출을 나타내요. |
item |
진행 중인 함수 호출 항목. name, arguments, id 필드 포함 |
그 후 response.function_call_arguments.delta 타입의 일련의 이벤트를 받게 되는데, 여기에는 arguments 필드의 delta가 포함돼요. 이 이벤트들은 다음 필드를 가져요:
| 필드 | 설명 |
|---|---|
response_id |
함수 호출이 속한 응답의 id |
item_id |
delta가 속한 함수 호출 항목의 id |
output_index |
응답에서 출력 항목의 인덱스. 응답 내 개별 함수 호출을 나타내요. |
delta |
arguments 필드의 델타. |
아래는 delta들을 최종 tool_call 객체로 합치는 방법을 보여주는 코드 스니펫이에요.
tool_call 델타 축적
const finalToolCalls = {};
for await (const event of stream) {
if (
event.type === "response.output_item.added" &&
event.item.type === "function_call"
) {
finalToolCalls[event.output_index] = event.item;
} else if (event.type === "response.function_call_arguments.delta") {
const index = event.output_index;
if (finalToolCalls[index]) {
finalToolCalls[index].arguments += event.delta;
}
}
}
final_tool_calls = {}
for event in stream:
if event.type == "response.output_item.added":
final_tool_calls[event.output_index] = event.item
elif event.type == "response.function_call_arguments.delta":
index = event.output_index
if final_tool_calls[index]:
final_tool_calls[index].arguments += event.delta
package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
func main() {
client := openai.NewClient()
parameters := map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{"type": "string"},
},
"required": []string{"location"},
"additionalProperties": false,
}
tool := responses.ToolParamOfFunction("get_weather", parameters, true)
stream := client.Responses.NewStreaming(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("What's the weather like in Paris today?"),
},
Tools: []responses.ToolUnionParam{tool},
})
finalToolCalls := map[int64]responses.ResponseFunctionToolCall{}
for stream.Next() {
event := stream.Current()
if event.Type == "response.output_item.added" && event.Item.Type == "function_call" {
finalToolCalls[event.OutputIndex] = event.Item.AsFunctionCall()
}
if event.Type == "response.function_call_arguments.delta" {
finalToolCall, ok := finalToolCalls[event.OutputIndex]
if !ok {
continue
}
finalToolCall.Arguments += event.Delta
finalToolCalls[event.OutputIndex] = finalToolCall
}
}
if err := stream.Err(); err != nil {
panic(err)
}
fmt.Println(finalToolCalls)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.core.http.StreamResponse;
import com.openai.models.responses.FunctionTool;
import com.openai.models.responses.ResponseCreateParams;
import com.openai.models.responses.ResponseFunctionToolCall;
import com.openai.models.responses.ResponseStreamEvent;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
FunctionTool weather =
FunctionTool.builder()
.name("get_weather")
.description("Get the weather for a city.")
.parameters(
FunctionTool.Parameters.builder()
.putAdditionalProperty("type", JsonValue.from("object"))
.putAdditionalProperty(
"properties", JsonValue.from(Map.of("location", Map.of("type", "string"))))
.putAdditionalProperty("required", JsonValue.from(List.of("location")))
.putAdditionalProperty("additionalProperties", JsonValue.from(false))
.build())
.strict(true)
.build();
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("What is the weather in Paris?")
.addTool(weather)
.build();
Map<Long, ResponseFunctionToolCall> toolCalls = new LinkedHashMap<>();
try (StreamResponse<ResponseStreamEvent> stream = client.responses().createStreaming(params)) {
stream.stream()
.forEach(
event -> {
event
.outputItemAdded()
.ifPresent(
added ->
added
.item()
.functionCall()
.ifPresent(call -> toolCalls.put(added.outputIndex(), call)));
event
.functionCallArgumentsDelta()
.ifPresent(
delta ->
toolCalls.computeIfPresent(
delta.outputIndex(),
(ignored, call) ->
call.toBuilder()
.arguments(call.arguments() + delta.delta())
.build()));
});
}
toolCalls.values().forEach(System.out::println);
require "openai"
client = OpenAI::Client.new
stream = client.responses.stream(
model: "gpt-6-astra",
input: "What is the weather in Paris?",
tools: [
{
type: :function,
name: "get_weather",
parameters: {
type: :object,
properties: { location: { type: :string } },
required: ["location"],
additionalProperties: false
},
strict: true
}
]
)
final_tool_calls = {}
stream.each do |event|
case event
when OpenAI::Models::Responses::ResponseOutputItemAddedEvent
item = event.item
next unless item.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall)
final_tool_calls[event.output_index] = {
id: item.id,
call_id: item.call_id,
name: item.name,
type: item.type,
arguments: item.arguments.dup
}
when OpenAI::Models::Responses::ResponseFunctionCallArgumentsDeltaEvent
tool_call = final_tool_calls[event.output_index]
tool_call[:arguments] << event.delta if tool_call
end
end
puts(final_tool_calls.sort.to_h.values)
축적된 final_tool_calls[0]
{
"type": "function_call",
"id": "fc_1234xyz",
"call_id": "call_2345abc",
"name": "get_weather",
"arguments": "{\"location\":\"Paris, France\"}"
}
모델이 함수 호출을 끝내면 response.function_call_arguments.done 타입의 이벤트가 방출돼요. 이 이벤트는 다음 필드를 포함한 전체 함수 호출을 담아요:
| 필드 | 설명 |
|---|---|
response_id |
함수 호출이 속한 응답의 id |
output_index |
응답에서 출력 항목의 인덱스. 응답 내 개별 함수 호출을 나타내요. |
item |
name, arguments, id 필드를 포함한 함수 호출 항목. |
커스텀 툴 (Custom tools)
커스텀 툴은 JSON 스키마로 구동되는 함수 툴과 거의 같은 방식으로 동작해요. 다만 모델에게 툴에 필요한 입력에 대한 명시적 지시를 주는 대신, 모델이 임의의 문자열을 툴의 입력으로 전달해요. 이는 응답을 불필요하게 JSON으로 감싸는 것을 피하거나, 응답에 커스텀 문법을 적용할 때 유용해요 (아래에서 더 자세히 다룰게요).
다음 코드 샘플은 Python 코드를 담은 텍스트 문자열을 응답으로 받을 것으로 기대하는 커스텀 툴을 만드는 모습을 보여줘요.
커스텀 툴 호출 예시
import OpenAI from "openai";
const client = new OpenAI();
const response = await client.responses.create({
model: "gpt-6-astra",
input: "Use the code_exec tool to print hello world to the console.",
tools: [
{
type: "custom",
name: "code_exec",
description: "Executes arbitrary Python code.",
},
],
});
console.log(response.output);
from openai import OpenAI
client = OpenAI()
response = client.responses.create(
model="gpt-6-astra",
input="Use the code_exec tool to print hello world to the console.",
tools=[
{
"type": "custom",
"name": "code_exec",
"description": "Executes arbitrary Python code.",
}
],
)
print(response.output)
package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
func main() {
client := openai.NewClient()
tool := responses.ToolParamOfCustom("code_exec")
tool.OfCustom.Description = openai.String("Executes arbitrary Python code.")
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Use the code_exec tool to print hello world to the console.")},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
fmt.Println(response.Output)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.responses.CustomTool;
import com.openai.models.responses.ResponseCreateParams;
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("Use code_exec to print hello world.")
.addTool(
CustomTool.builder()
.name("code_exec")
.description("Executes arbitrary Python code.")
.build())
.build();
client.responses().create(params).output().forEach(System.out::println);
require "openai"
client = OpenAI::Client.new
response = client.responses.create(
model: "gpt-6-astra",
input: "Use code_exec to print hello world.",
tools: [
{
type: :custom,
name: "code_exec",
description: "Executes arbitrary Python code."
}
]
)
puts(response.output)
이전과 마찬가지로 output 배열에는 모델이 생성한 툴 호출이 포함돼요. 다만 이번에는 툴 호출 입력이 평문 텍스트로 주어져요.
[
{
"id": "rs_6890e972fa7c819ca8bc561526b989170694874912ae0ea6",
"type": "reasoning",
"content": [],
"summary": []
},
{
"id": "ctc_6890e975e86c819c9338825b3e1994810694874912ae0ea6",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_aGiFQkRWSWAIsMQ19fKqxUgb",
"input": "print(\"hello world\")",
"name": "code_exec"
}
]
컨텍스트 자유 문법 (Context-free grammars)
컨텍스트 자유 문법(CFG)은 주어진 형식으로 유효한 텍스트를 만드는 방법을 정의하는 규칙 집합이에요. 커스텀 툴의 경우, 커스텀 툴에 대한 모델의 텍스트 입력을 제약하는 CFG를 제공할 수 있어요.
커스텀 툴을 구성할 때 grammar 파라미터로 커스텀 CFG를 제공할 수 있어요. 현재 문법을 정의할 때 lark와 regex 두 가지 형태의 CFG 구문을 지원해요.
Lark CFG
Lark 컨텍스트 자유 문법 예시
import OpenAI from "openai";
const client = new OpenAI();
const grammar = `
start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT
`;
const response = await client.responses.create({
model: "gpt-6-astra",
input: "Use the math_exp tool to add four plus four.",
tools: [
{
type: "custom",
name: "math_exp",
description: "Creates valid mathematical expressions",
format: {
type: "grammar",
syntax: "lark",
definition: grammar,
},
},
],
});
console.log(response.output);
from openai import OpenAI
client = OpenAI()
grammar = """
start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT
"""
response = client.responses.create(
model="gpt-6-astra",
input="Use the math_exp tool to add four plus four.",
tools=[
{
"type": "custom",
"name": "math_exp",
"description": "Creates valid mathematical expressions",
"format": {
"type": "grammar",
"syntax": "lark",
"definition": grammar,
},
}
],
)
print(response.output)
package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
"github.com/openai/openai-go/v3/shared"
)
func main() {
client := openai.NewClient()
grammar := `start: expr
expr: term (SP ADD SP term)* -> add
| term
term: factor (SP MUL SP factor)* -> mul
| factor
factor: INT
SP: " "
ADD: "+"
MUL: "*"
%import common.INT`
tool := responses.ToolParamOfCustom("math_exp")
tool.OfCustom.Description = openai.String("Creates valid mathematical expressions")
tool.OfCustom.Format = shared.CustomToolInputFormatParamOfGrammar(grammar, "lark")
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Use the math_exp tool to add four plus four.")},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
fmt.Println(response.Output)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.CustomToolInputFormat;
import com.openai.models.responses.CustomTool;
import com.openai.models.responses.ResponseCreateParams;
String grammar =
"""
start: expr
expr: term (SP ADD SP term)*
term: INT
SP: " "
ADD: "+"
%import common.INT
""";
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("Use math_exp to add four plus four.")
.addTool(
CustomTool.builder()
.name("math_exp")
.description("Creates valid mathematical expressions.")
.format(
CustomToolInputFormat.Grammar.builder()
.syntax(CustomToolInputFormat.Grammar.Syntax.LARK)
.definition(grammar)
.build())
.build())
.build();
client.responses().create(params).output().forEach(System.out::println);
require "openai"
client = OpenAI::Client.new
grammar = <<~LARK
start: expr
expr: term (SP ADD SP term)*
term: INT
SP: " "
ADD: "+"
%import common.INT
LARK
response = client.responses.create(
model: "gpt-6-astra",
input: "Use math_exp to add four plus four.",
tools: [
{
type: :custom,
name: "math_exp",
description: "Creates valid mathematical expressions.",
format: {
type: :grammar,
syntax: :lark,
definition: grammar
}
}
]
)
puts(response.output)
커스텀 툴의 출력은 여러분이 정의한 Lark CFG를 따라야 해요:
[
{
"id": "rs_6890ed2b6374819dbbff5353e6664ef103f4db9848be4829",
"type": "reasoning",
"content": [],
"summary": []
},
{
"id": "ctc_6890ed2f32e8819daa62bef772b8c15503f4db9848be4829",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_pmlLjmvG33KJdyVdC4MVdk5N",
"input": "4 + 4",
"name": "math_exp"
}
]
문법은 Lark의 변형을 사용해 지정돼요. 모델 샘플링은 LLGuidance로 제약돼요. 몇몇 Lark 기능은 지원되지 않아요:
- 렉서 정규식의 Lookaround
- 렉서 정규식의 게으른 수량자(
*?,+?,??) - 터미널 우선순위
- 템플릿
- 임포트(빌트인
%importcommon 제외) %declare
커스텀 문법 실험에는 Lark IDE를 사용하는 것을 권장해요.
문법 복잡도 제한 (Limit grammar complexity)
문법을 툴에 필요한 규칙과 패턴으로만 제한하세요. 문법이 너무 복잡하면 OpenAI API가 오류를 반환할 수 있으니, API에서 사용하기 전에 원하는 문법이 호환되는지 확인해야 해요.
Lark 문법은 완벽하게 만들기 까다로울 수 있어요. 덜 복잡한 문법이 가장 안정적으로 동작하지만, 복잡한 문법은 문법 정의 자체, 프롬프트, 툴 설명을 반복하면서 모델이 분포 밖으로 벗어나지 않게 해야 해요.
올바른 패턴 대 잘못된 패턴 (Correct versus incorrect patterns)
올바른 (단일, 경계 있는 터미널):
start: SENTENCE
SENTENCE: /[A-Za-z, ]*(the hero|a dragon|an old man|the princess)[A-Za-z, ]*(fought|saved|found|lost)[A-Za-z, ]*(a treasure|the kingdom|a secret|his way)[A-Za-z, ]*\./
이렇게 하지 마세요 (규칙/터미널에 걸쳐 분할). 이는 규칙이 터미널 사이의 자유 텍스트를 분할하려 시도해요. 렉서는 자유 텍스트 조각을 탐욕스럽게 매칭해서 제어할 수 없게 돼요:
start: sentence
sentence: /[A-Za-z, ]+/ subject /[A-Za-z, ]+/ verb /[A-Za-z, ]+/ object /[A-Za-z, ]+/
소문자 규칙은 터미널이 입력에서 어떻게 잘리는지에 영향을 주지 않아요 — 터미널 정의만 영향을 줘요. "앵커 사이의 자유 텍스트"가 필요하면, 그것을 하나의 거대한 정규식 터미널로 만들어 렉서가 의도한 구조로 정확히 한 번 매칭하게 하세요.
터미널 대 규칙 (Terminals versus rules)
Lark는 렉서 토큰에 터미널을(관례상 UPPERCASE), 파서 생성물에 규칙을(관례상 lowercase) 사용해요. 지원되는 부분집합 안에 머물고 예기치 않은 상황을 피하는 가장 실용적인 방법은 문법을 명시적으로 유지하고 불필요한 복잡성을 피하며, 터미널과 규칙을 관심사 분리가 명확하게 사용하는 거예요.
터미널이 사용하는 정규식 구문은 Python의 re 모듈이 아니라 Rust regex crate 구문이에요.
핵심 아이디어와 모범 사례 (Key ideas and best practices)
렉서는 파서보다 먼저 실행된다
터미널은 CFG 규칙 로직이 적용되기 전에 렉서가 (탐욕적으로 / 가장 긴 매칭 우선) 매칭해요. 터미널을 여러 규칙으로 분할해 "모양"을 만들려 하면, 렉서는 그 규칙들이 아닌 터미널 정규식에만 안내될 수 있어요.
자유 형식 범위에서 텍스트를 잘라낼 때는 터미널 하나를 선호하라
임의 텍스트에 내장된 패턴(예: 앵커 사이에 "아무거나" 있는 자연어)을 인식해야 한다면, 그것을 단일 터미널로 표현하세요. 자유 텍스트 터미널을 파서 규칙과 교차시키려 하지 마세요. 탐욕적 렉서가 의도한 경계를 존중하지 않아 모델이 분포 밖으로 벗어날 가능성이 매우 높아요.
규칙을 사용해 개별 토큰을 조합하라
규칙은 명시적으로 구분된 터미널(숫자, 키워드, 구두점)을 더 큰 구조로 결합할 때 이상적이에요. 두 터미널 사이의 "사이에 있는 것"을 제약하는 데는 적합한 도구가 아니에요.
터미널을 집중적이고, 경계가 있으며, 자족적으로 유지하라
명시적 문자 클래스와 경계 있는 수량자({0,10} — 모든 곳에 무제한 *가 아니라)를 선호하세요. "마침표까지의 아무 텍스트"가 필요하면, /.+\./보다 /[^.\n]{0,10}*\./ 같은 것을 선호해 폭주하는 성장을 피하세요.
규칙을 토큰 결합에 사용하고 정규식 내부를 제어하는 데는 사용하지 마라
좋은 규칙 사용 예시:
start: expr
NUMBER: /[0-9]+/
PLUS: "+"
MINUS: "-"
expr: term (("+"|"-") term)*
term: NUMBER
공백을 명시적으로 처리하라
무제한적인 %ignore 지시문에 의존하지 마세요. 경계 없는 ignore 지시문을 사용하면 문법이 너무 복잡해지거나 모델이 분포 밖으로 벗어날 수 있어요. 공백이 허용되는 곳에 명시적 터미널을 사용하는 것을 선호하세요.
문제 해결 (Troubleshooting)
- API가 문법을 너무 복잡하다며 거부하면, 규칙과 터미널을 단순화하고 경계 없는
%ignore를 제거하세요. - 커스텀 툴이 예상치 못한 토큰으로 호출되면, 터미널이 겹치지 않는지 확인하고 탐욕적 렉서를 확인하세요.
- 모델이 "분포 밖(out-of-distribution)"으로 표류할 때(과도하게 길거나 반복적인 출력을 만드는 것으로 나타나는데, 구문상 유효하지만 의미상 틀린 경우):
- 문법을 조이세요.
- 프롬프트를 반복하세요 (few-shot 예시 추가) 그리고 툴 설명을 반복하세요 (문법을 설명하고 모델이 추론하고 따르도록 지시).
- 더 높은 추론 노력으로 실험해 보세요 (예: medium에서 high로 올리기).
Regex CFG
Regex 컨텍스트 자유 문법 예시
import OpenAI from "openai";
const client = new OpenAI();
const grammar =
"^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\\s+(?P<day>\\d{1,2})(?:st|nd|rd|th)?\\s+(?P<year>\\d{4})\\s+at\\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$";
const response = await client.responses.create({
model: "gpt-6-astra",
input:
"Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.",
tools: [
{
type: "custom",
name: "timestamp",
description: "Saves a timestamp in date + time in 24-hr format.",
format: {
type: "grammar",
syntax: "regex",
definition: grammar,
},
},
],
});
console.log(response.output);
from openai import OpenAI
client = OpenAI()
grammar = r"^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\s+(?P<day>\d{1,2})(?:st|nd|rd|th)?\s+(?P<year>\d{4})\s+at\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$"
response = client.responses.create(
model="gpt-6-astra",
input="Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.",
tools=[
{
"type": "custom",
"name": "timestamp",
"description": "Saves a timestamp in date + time in 24-hr format.",
"format": {
"type": "grammar",
"syntax": "regex",
"definition": grammar,
},
}
],
)
print(response.output)
package main
import (
"context"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
"github.com/openai/openai-go/v3/shared"
)
func main() {
client := openai.NewClient()
grammar := `^(?P<month>January|February|March|April|May|June|July|August|September|October|November|December)\s+(?P<day>\d{1,2})(?:st|nd|rd|th)?\s+(?P<year>\d{4})\s+at\s+(?P<hour>0?[1-9]|1[0-2])(?P<ampm>AM|PM)$`
tool := responses.ToolParamOfCustom("timestamp")
tool.OfCustom.Description = openai.String("Saves a timestamp in date and time format.")
tool.OfCustom.Format = shared.CustomToolInputFormatParamOfGrammar(grammar, "regex")
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String("Use the timestamp tool to save a timestamp for August 7th 2025 at 10AM.")},
Tools: []responses.ToolUnionParam{tool},
})
if err != nil {
panic(err)
}
fmt.Println(response.Output)
}
import com.openai.client.OpenAIClient;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.models.CustomToolInputFormat;
import com.openai.models.responses.CustomTool;
import com.openai.models.responses.ResponseCreateParams;
String grammar =
"^(January|February|March|April|May|June|July|August|September|October|November|December) "
+ "\\d{1,2}(st|nd|rd|th)? \\d{4} at (0?[1-9]|1[0-2])(AM|PM)$";
ResponseCreateParams params =
ResponseCreateParams.builder()
.model("gpt-6-astra")
.input("Use timestamp to save August 7th 2025 at 10AM.")
.addTool(
CustomTool.builder()
.name("timestamp")
.description("Saves a timestamp in date and time format.")
.format(
CustomToolInputFormat.Grammar.builder()
.syntax(CustomToolInputFormat.Grammar.Syntax.REGEX)
.definition(grammar)
.build())
.build())
.build();
client.responses().create(params).output().forEach(System.out::println);
require "openai"
client = OpenAI::Client.new
grammar = "^(January|February|March|April|May|June|July|August|September|October|November|December) \\d{1,2}(st|nd|rd|th)? \\d{4} at (0?[1-9]|1[0-2])(AM|PM)$"
response = client.responses.create(
model: "gpt-6-astra",
input: "Use timestamp to save August 7th 2025 at 10AM.",
tools: [
{
type: :custom,
name: "timestamp",
description: "Saves a timestamp in date and time format.",
format: {
type: :grammar,
syntax: :regex,
definition: grammar
}
}
]
)
puts(response.output)
툴의 출력은 여러분이 정의한 Regex CFG를 따라야 해요:
[
{
"id": "rs_6894f7a3dd4c81a1823a723a00bfa8710d7962f622d1c260",
"type": "reasoning",
"content": [],
"summary": []
},
{
"id": "ctc_6894f7ad7fb881a1bffa1f377393b1a40d7962f622d1c260",
"type": "custom_tool_call",
"status": "completed",
"call_id": "call_8m4XCnYvEmFlzHgDHbaOCFlK",
"input": "August 7th 2025 at 10AM",
"name": "timestamp"
}
]
Lark 구문과 마찬가지로, 정규식도 Python의 re 모듈이 아니라 Rust regex crate 구문을 사용해요.
몇 가지 Regex 기능은 지원되지 않아요:
- Lookaround
- 게으른 수량자(
*?,+?,??)
핵심 아이디어와 모범 사례
패턴은 한 줄에 있어야 한다
입력에서 개행을 매칭해야 한다면 이스케이프 시퀀스 \n을 사용하세요. 패턴이 여러 줄에 걸치게 하는 verbose/extended 모드는 사용하지 마세요.
정규식을 평범한 패턴 문자열로 제공하라
패턴을 //로 감싸지 마세요.