프로그래밍 방식의 도구 호출을 사용하면 모델이 도구 호출을 조율하는 JavaScript를 작성하고 실행할 수 있습니다. 프로그램은 여러 도구를 병렬로 호출하고, 반복문과 조건문을 사용하며, 중간 결과를 호스팅 런타임에 보관할 수 있습니다. 서로 관련된 도구를 순서대로 호출하거나 대용량 도구 출력을 처리한 뒤 결과를 반환해야 하는 작업에 유용합니다.
Responses API에서는 애플리케이션이 프로그래밍 방식의 도구 호출을 허용할지 결정합니다. 또한 이 기능을 지원하는 각 도구에 대해 모델의 직접 호출을 허용할지, 프로그램을 통한 호출을 허용할지, 두 방식 모두 허용할지 지정합니다. 클라이언트가 담당하는 도구 호출은 계속 애플리케이션에서 실행합니다. Agents API에서는 프로그래밍 방식의 도구 호출이 기본으로 활성화되며, API가 에이전트 루프를 관리합니다.
프로그래밍 방식의 도구 호출을 활성화하기 전에 모델 페이지를 확인하세요.
OpenAI는 생성된 프로그램마다 격리된 V8 런타임을 새로 만들어 그 안에서 프로그램을 실행합니다. 런타임은 최상위 await를 사용하는 JavaScript를 지원합니다. Node.js, 패키지 설치, 직접 네트워크 액세스, 범용 파일 시스템, 하위 프로세스 실행, 콘솔은 제공하지 않으며, 프로그램 실행 간에 JavaScript 상태를 유지하지 않습니다. 프로그램은 요청에서 활성화한 도구를 통해서만 외부 시스템과 상호 작용할 수 있으며, text(...) 또는 image(...)로 결과를 출력할 수 있습니다.
Responses API 요청에서 프로그래밍 방식의 도구 호출은 영구 코드 실행 컨테이너 없이도 데이터 비보관(ZDR) 워크플로우를 지원합니다. ZDR은 조직 또는 프로젝트에서 활성화해야 합니다. store: false를 설정하면 상태를 저장하지 않고 요청을 이어갈 수 있지만, 이 설정만으로 ZDR이 활성화되지는 않습니다. ZDR 적용 가능 여부와 데이터 보관 방식은 모델, 도구, 타사 서비스를 포함한 요청 전체에 따라 달라집니다. 자세한 내용은 데이터 관리를 참고하세요.
작업 단계의 제어 흐름을 예측할 수 있고 코드로 결과를 간결하게 구조화해 반환할 수 있다면 프로그래밍 방식의 도구 호출을 사용하세요. 호출 한 번으로 충분하거나, 결과가 나올 때마다 모델이 새로 판단해야 하거나, 작업에 승인이 필요하거나 인용 또는 원본 산출물을 보존해야 한다면 직접 도구 호출을 사용하세요.
| 작업 유형 |
권장 방식 |
| 단일 조회 또는 작업 |
직접 도구 호출을 사용하세요. |
| 코드로 필터링, 조인, 순위 지정, 중복 제거, 집계 또는 검증이 가능한 여러 결과 |
프로그램이 결과를 간결하게 구조화해 반환할 수 있다면 프로그래밍 방식의 도구 호출을 사용하세요. |
| 데이터 흐름을 예측할 수 있고 앞선 호출 결과에 의존하는 호출 |
코드로 후속 호출의 인수를 도출할 수 있고 한도와 실패 시 동작이 명확하다면 프로그래밍 방식의 도구 호출을 사용하세요. |
| 결과에 따라 달라지는 검색 또는 의미 평가 |
모델이 각 결과를 바탕으로 다음 결정을 내려야 한다면 직접 도구 호출을 사용하세요. |
| 쓰기 작업 또는 승인 여부가 중요한 작업 |
어디까지 권한이 부여되었는지 명확히 하려면 기본적으로 직접 도구 호출을 사용하세요. |
| 최종 인용 또는 원본 산출물 검증 |
프로그램이 원본 출력을 보존하고 모든 필수 항목을 검증하는 경우를 제외하고는 직접 도구 호출을 사용하세요. |
Responses API에서는 programmatic_tool_calling 호스팅 도구를 요청에 추가하세요. 그런 다음 이 기능을 지원하는 도구 중 프로그램에서 호출할 각 도구에 allowed_callers를 설정하세요.
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[
{
"type": "function",
"name": "get_inventory",
"description": "Return an object with sku (string) and available_units (number).",
"parameters": {
"type": "object",
"properties": {
"sku": { "type": "string" }
},
"required": ["sku"],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {
"sku": { "type": "string" },
"available_units": { "type": "number" }
},
"required": ["sku", "available_units"],
"additionalProperties": false
},
"allowed_callers": ["programmatic"]
},
{
"type": "programmatic_tool_calling"
}
]
allowed_callers는 모델이 도구를 호출할 수 있는 방식을 지정합니다.
| 값 |
동작 |
생략 또는 ["direct"] |
모델이 도구를 직접 호출할 수 있습니다. |
["programmatic"] |
program 항목 안의 코드만 도구를 호출할 수 있습니다. |
["direct", "programmatic"] |
모델이 도구를 직접 호출하거나 프로그램에서 호출할 수 있습니다. |
parameters는 함수 인수를 설명합니다. 함수가 예측 가능한 구조의 데이터를 반환한다면 output_schema로 function_call_output.output 문자열에 인코딩된 JSON 객체의 구조를 설명하세요. 생성된 JavaScript가 반환된 필드를 안정적으로 사용할 수 있도록 두 스키마를 모두 정의하세요.
다음 도구 유형은 allowed_callers: ["programmatic"] 설정을 지원합니다.
function 및 custom
mcp
apply_patch
- 로컬 및 호스팅
shell
code_interpreter
MCP 도구에서는 도구의 require_approval 정책에 따라 호출을 승인할 때까지 프로그램이 일시 중지될 수 있습니다.
OpenAI에서 호스팅하는 도구를 프로그램에서 활성화하기 전에 해당 도구의 데이터 보관 및 보안 지침을 검토하세요.
도구 검색은 생성된 JavaScript 외부에서 최상위 Responses API 도구로 실행됩니다. defer_loading: true로 설정한 함수 도구, 사용자 정의 도구, MCP 도구는 처음에는 프로그램에서 사용할 수 없습니다. 모델이 검색 조건에 맞는 도구를 로드하면 이후에 시작하는 프로그램에서 tools.*로 해당 도구를 호출할 수 있습니다. 단, 해당 도구의 allowed_callers에 "programmatic"이 포함되어 있어야 합니다. 이미 실행 중인 프로그램은 도구 검색을 호출할 수 없으므로, 모델은 로드를 미룬 도구가 필요한 프로그램을 시작하기 전에 해당 도구를 로드해야 합니다.
애플리케이션이 모델의 직접 함수 호출과 프로그램을 통한 함수 호출을 모두 허용한다면 각 호출 방식을 사용할 워크플로우 단계를 지정하세요. “프로그래밍 방식의 도구 호출을 효율적으로 사용하세요” 같은 일반적인 지침만으로는 어느 단계까지 해당 방식을 사용해야 하는지 알 수 없습니다. 다음과 같이 지침을 작성할 수 있습니다.
<tool_orchestration>
[bounded stage]에서는 프로그래밍 방식의 도구 호출을 사용하고, [eligible tools]만 호출하세요.
서로 독립적인 호출은 안전한 경우 동시에 실행하세요. 도구의 입력 및 출력 필드는 문서에 명시된 것만 사용하세요.
중간 결과를 처리해 필요한 내용만 남긴 뒤, [program result shape] 형식에 정확히 맞춰 출력하세요.
최종 답변에 필요한 근거도 포함하세요.
[condition]이 충족되면 중단하세요. 일시적인 오류로 호출이 실패하면 최대 [R]회 재시도하세요.
완료된 호출을 반복하거나 사이드 이펙트가 발생하는 작업을 수행하지 마세요.
필수 결과가 여전히 누락되어 있으면 실패를 명확히 나타내는 구조화된 결과를 반환하세요.
[semantic judgment, approval, or final validation]에는 직접 도구 호출을 사용하세요.
</tool_orchestration>
이 템플릿을 적용한 예시는 다음과 같습니다.
<tool_orchestration>
프로그래밍 방식의 도구 호출로 sku_123의 재고량과 수요량을 비교하세요.
get_inventory와 get_demand만 사용하고 두 호출을 동시에 실행하세요.
도구의 입력 및 출력 필드는 문서에 명시된 것만 사용하세요.
중간 결과를 처리해 필요한 내용만 남긴 뒤, sku, available_units, requested_units, shortage_units 필드가 있는 JSON 객체를 정확히 하나만 출력하세요.
shortage_units의 값은 max(requested_units - available_units, 0)으로 계산하세요.
계산 근거로 available_units와 requested_units를 포함하세요.
두 도구의 결과에 각각 필수 필드가 모두 포함되면 중단하세요. 일시적인 오류로 호출이 실패하면 최대 1회 재시도하세요.
완료된 호출을 반복하거나 사이드 이펙트가 발생하는 작업을 수행하지 마세요.
필수 결과가 여전히 누락되어 있으면 실패를 명확히 나타내는 구조화된 결과를 반환하세요.
재고를 변경하는 작업은 실행 전에 반드시 승인을 받으세요. 직접 도구 호출은 이 승인을 받을 때만 사용하세요.
</tool_orchestration>
두 방식이 모두 필요한 워크플로우에서는 한 방식에서 다른 방식으로 넘기는 지점을 하나만 정하세요. 호출 경로를 번갈아 사용하거나 같은 작업을 반복하지 마세요. 안전한 대체 경로가 있다면 한 번만 정의하고 재시도 횟수를 제한하세요.
각 API 호출은 기존과 동일한 표준 Responses API 객체를 반환합니다. 프로그래밍 방식의 도구 호출을 위해 별도의 응답 래퍼를 사용하지 않습니다. 모델이 프로그래밍 방식의 도구 호출을 사용하면 응답의 output 배열에 다음 항목이 포함될 수 있습니다.
program 항목에는 생성된 JavaScript, call_id, 프로그램을 재개하거나 재실행하는 데 사용하는 불투명한 식별 값인 fingerprint가 포함됩니다.
function_call 항목은 프로그램에서 요청한 함수 호출을 나타냅니다. 이 항목에는 애플리케이션이 함수 결과를 반환할 때 사용하는 고유한 call_id가 있습니다. caller.caller_id는 프로그램의 call_id와 일치합니다.
program_output 항목에는 프로그램의 최종 결과와 상태가 포함됩니다. 이 항목의 call_id는 프로그램의 call_id와 일치하며, status 값은 completed 또는 incomplete입니다.
이 항목들은 response.output에서 각각 독립된 최상위 항목이며, 항목 간의 실행 관계는 caller 필드에 기록됩니다.
예를 들어 애플리케이션이 get_inventory와 get_demand를 실행하는 동안 프로그램은 일시 중지될 수 있습니다.
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[
{
"type": "program",
"id": "prog_123",
"call_id": "call_prog_123",
"code": "const [stock, demand] = await Promise.all([tools.get_inventory({ sku: 'sku_123' }), tools.get_demand({ sku: 'sku_123' })]); text(JSON.stringify({ sku: stock.sku, available_units: stock.available_units, requested_units: demand.requested_units, shortage_units: Math.max(demand.requested_units - stock.available_units, 0) }));",
"fingerprint": "opaque_replay_state"
},
{
"type": "function_call",
"id": "fc_123",
"call_id": "call_inventory_123",
"name": "get_inventory",
"arguments": "{\"sku\":\"sku_123\"}",
"caller": {
"type": "program",
"caller_id": "call_prog_123"
}
},
{
"type": "function_call",
"id": "fc_456",
"call_id": "call_demand_123",
"name": "get_demand",
"arguments": "{\"sku\":\"sku_123\"}",
"caller": {
"type": "program",
"caller_id": "call_prog_123"
}
}
]
이 예시에서는 response.output의 관련 항목만 보여 주고, 이를 감싸는 표준 Responses 객체는 생략합니다. 애플리케이션이 중첩 함수 호출의 결과를 반환하면 이후 응답에 완전한 program_output 항목이 포함될 수 있습니다.
1
2
3
4
5
6
7{
"type": "program_output",
"id": "prog_out_123",
"call_id": "call_prog_123",
"result": "{\"sku\":\"sku_123\",\"available_units\":42,\"requested_units\":31,\"shortage_units\":0}",
"status": "completed"
}
program_output.result의 JSON 문자열은 지침에서 지정한 프로그램 결과 형식을 따릅니다. 이 문자열을 담는 program_output 항목은 위에 나온 API 규격을 따릅니다. 두 형식은 별도로 정의됩니다. 최종 message는 프로그램 출력과 함께 도착하거나 이후 응답에 포함될 수 있으므로, 해당 메시지를 받을 때까지 계속 진행하세요.
모델이 생성한 JavaScript는 OpenAI가 호스팅 런타임에서 실행합니다. 애플리케이션은 응답에 포함된 클라이언트 담당 함수 호출을 실행하며, 생성된 JavaScript를 실행하지 않습니다.
함수 결과는 function_call_output으로 반환하세요. 함수 호출의 caller 값을 변경하지 말고 그대로 복사하세요. 서비스는 이 값으로 재개할 프로그램을 식별합니다.
프로그램은 클라이언트가 담당하는 도구를 호출할 때마다 일시 중지될 수 있습니다. 응답에 어시스턴트의 최종 메시지가 포함될 때까지 다음 절차를 따르세요.
- 호스팅 도구와 프로그래밍 방식의 호출을 허용하는 함수를 포함해 요청을 보내세요.
- 응답에 포함된 클라이언트 담당 함수 호출을 모두 실행하세요.
- 원래의
call_id와 caller를 포함해 각 함수 결과를 반환하세요.
- 응답이 불완전하면 먼저 처리한 뒤 계속 진행하세요.
- 응답에 대기 중인
function_call 항목도 최종 message 항목도 없다면 해당 응답에서 이어가세요. store: false를 사용하면 응답의 출력 항목을 다시 보내세요. 저장된 응답이라면 previous_response_id를 사용하세요.
- 응답에 최종
message 항목이 포함되면 중단하세요. response.output_text 또는 메시지에 담긴 거절 내용을 확인하세요.
다음 예시는 store: false를 사용하면서 모든 응답 항목을 보존하고 각 함수 결과를 프로그램에 반환합니다.
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
100
101
102
103
104
105
106
107
108
109
110
111
112import OpenAI from "openai";
import { toResponseInputItems } from "openai/lib/responses/ResponseInputItems";
const client = new OpenAI();
const implementations = {
get_inventory: async ({ sku }) => ({ sku, available_units: 42 }),
get_demand: async ({ sku }) => ({ sku, requested_units: 31 }),
};
const tools = [
{
type: "function",
name: "get_inventory",
description:
"Return an object with sku (string) and available_units (number).",
parameters: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
additionalProperties: false,
},
output_schema: {
type: "object",
properties: {
sku: { type: "string" },
available_units: { type: "number" },
},
required: ["sku", "available_units"],
additionalProperties: false,
},
allowed_callers: ["programmatic"],
strict: true,
},
{
type: "function",
name: "get_demand",
description:
"Return an object with sku (string) and requested_units (number).",
parameters: {
type: "object",
properties: { sku: { type: "string" } },
required: ["sku"],
additionalProperties: false,
},
output_schema: {
type: "object",
properties: {
sku: { type: "string" },
requested_units: { type: "number" },
},
required: ["sku", "requested_units"],
additionalProperties: false,
},
allowed_callers: ["programmatic"],
strict: true,
},
{ type: "programmatic_tool_calling" },
];
const input = [
{
role: "user",
content: "Compare inventory with demand for sku_123.",
},
];
while (true) {
const response = await client.responses.create({
model: "gpt-6-astra",
store: false,
input,
tools,
});
if (response.status !== "completed") {
throw new Error(`Response ended with status ${response.status}`);
}
// Preserve replayable output, including program and reasoning items.
input.push(...toResponseInputItems(response.output));
const calls = response.output.filter((item) => item.type === "function_call");
if (calls.length === 0) {
const message = response.output.find((item) => item.type === "message");
if (message) {
const refusal = message.content.find((part) => part.type === "refusal");
console.log(response.output_text || refusal?.refusal || "");
break;
}
continue;
}
const outputs = await Promise.all(
calls.map(async (call) => {
const run = implementations[call.name];
if (!run) throw new Error(`Unknown tool: ${call.name}`);
const result = await run(JSON.parse(call.arguments));
return {
type: "function_call_output",
call_id: call.call_id,
output: JSON.stringify(result),
// Preserve caller so the runtime can resume the correct program.
caller: call.caller,
};
})
);
input.push(...outputs);
}
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117import json
from openai import OpenAI
client = OpenAI()
model = "gpt-6-astra"
def get_inventory(sku):
return {"sku": sku, "available_units": 42}
def get_demand(sku):
return {"sku": sku, "requested_units": 31}
implementations = {
"get_inventory": get_inventory,
"get_demand": get_demand,
}
tools = [
{
"type": "function",
"name": "get_inventory",
"description": "Return an object with sku (string) and available_units (number).",
"parameters": {
"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"],
"additionalProperties": False,
},
"output_schema": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"available_units": {"type": "number"},
},
"required": ["sku", "available_units"],
"additionalProperties": False,
},
"allowed_callers": ["programmatic"],
},
{
"type": "function",
"name": "get_demand",
"description": "Return an object with sku (string) and requested_units (number).",
"parameters": {
"type": "object",
"properties": {"sku": {"type": "string"}},
"required": ["sku"],
"additionalProperties": False,
},
"output_schema": {
"type": "object",
"properties": {
"sku": {"type": "string"},
"requested_units": {"type": "number"},
},
"required": ["sku", "requested_units"],
"additionalProperties": False,
},
"allowed_callers": ["programmatic"],
},
{"type": "programmatic_tool_calling"},
]
input_items = [
{
"role": "user",
"content": "Compare inventory with demand for sku_123.",
}
]
while True:
response = client.responses.create(
model=model,
store=False,
input=input_items,
tools=tools,
)
if response.status != "completed":
raise RuntimeError(f"Response ended with status {response.status}")
# Preserve every output item, including program and reasoning items.
input_items.extend(item.model_dump(exclude_none=True) for item in response.output)
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
message = next(
(item for item in response.output if item.type == "message"), None
)
if message:
refusal = next(
(part.refusal for part in message.content if part.type == "refusal"),
"",
)
print(response.output_text or refusal)
break
continue
for call in calls:
run = implementations.get(call.name)
if run is None:
raise ValueError(f"Unknown tool: {call.name}")
result = run(**json.loads(call.arguments))
input_items.append(
{
"type": "function_call_output",
"call_id": call.call_id,
"output": json.dumps(result),
# Preserve caller so the runtime can resume the correct program.
"caller": call.caller.model_dump() if call.caller else None,
}
)
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179package main
import (
"context"
"encoding/json"
"fmt"
"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/responses"
)
type toolArguments struct {
SKU string `json:"sku"`
}
func main() {
client := openai.NewClient()
input := responses.ResponseInputParam{
responses.ResponseInputItemParamOfMessage(
"Compare inventory with demand for sku_123.",
responses.EasyInputMessageRoleUser,
),
}
tools := []responses.ToolUnionParam{
functionTool(
"get_inventory",
"Return an object with sku (string) and available_units (number).",
"available_units",
),
functionTool(
"get_demand",
"Return an object with sku (string) and requested_units (number).",
"requested_units",
),
programmaticTool(),
}
for {
response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{
Model: "gpt-6-astra",
Store: openai.Bool(false),
Input: responses.ResponseNewParamsInputUnion{OfInputItemList: input},
Tools: tools,
})
if err != nil {
panic(err)
}
if response.Status != "completed" {
panic(fmt.Errorf("response ended with status %s", response.Status))
}
// Preserve every output item, including program and reasoning items.
input = append(input, outputAsInput(response.Output)...)
calls := functionCalls(response.Output)
if len(calls) == 0 {
if text, ok := finalMessageText(response); ok {
fmt.Println(text)
break
}
continue
}
for _, call := range calls {
result, err := runTool(call.Name, call.Arguments)
if err != nil {
panic(err)
}
output, err := json.Marshal(result)
if err != nil {
panic(err)
}
toolOutput := responses.ResponseInputItemParamOfFunctionCallOutput(string(output))
toolOutput.OfFunctionCallOutput.CallID = openai.String(call.CallID)
caller := call.Caller.AsProgram()
if caller.CallerID == "" {
panic("function call is missing its program caller")
}
// Preserve caller so the runtime can resume the correct program.
toolOutput.OfFunctionCallOutput.Caller.OfProgram =
&responses.ResponseInputItemFunctionCallOutputCallerProgramParam{
CallerID: caller.CallerID,
}
input = append(input, toolOutput)
}
}
}
func functionTool(name, description, resultField string) responses.ToolUnionParam {
parameters := map[string]any{
"type": "object",
"properties": map[string]any{
"sku": map[string]any{"type": "string"},
},
"required": []string{"sku"},
"additionalProperties": false,
}
outputSchema := map[string]any{
"type": "object",
"properties": map[string]any{
"sku": map[string]any{"type": "string"},
resultField: map[string]any{"type": "number"},
},
"required": []string{"sku", resultField},
"additionalProperties": false,
}
tool := responses.ToolParamOfFunction(name, parameters, true)
tool.OfFunction.Description = openai.String(description)
tool.OfFunction.AllowedCallers = []string{"programmatic"}
tool.OfFunction.OutputSchema = outputSchema
return tool
}
func programmaticTool() responses.ToolUnionParam {
tool := responses.NewToolProgrammaticToolCallingParam()
return responses.ToolUnionParam{OfProgrammaticToolCalling: &tool}
}
func outputAsInput(
output []responses.ResponseOutputItemUnion,
) []responses.ResponseInputItemUnionParam {
input := make([]responses.ResponseInputItemUnionParam, 0, len(output))
for _, item := range output {
var converted responses.ResponseInputItemUnion
if err := json.Unmarshal([]byte(item.RawJSON()), &converted); err != nil {
panic(err)
}
input = append(input, converted.ToParam())
}
return input
}
func functionCalls(
output []responses.ResponseOutputItemUnion,
) []responses.ResponseFunctionToolCall {
calls := make([]responses.ResponseFunctionToolCall, 0)
for _, item := range output {
if item.Type == "function_call" {
calls = append(calls, item.AsFunctionCall())
}
}
return calls
}
func finalMessageText(response *responses.Response) (string, bool) {
for _, item := range response.Output {
if item.Type != "message" {
continue
}
text := response.OutputText()
if text != "" {
return text, true
}
for _, content := range item.AsMessage().Content {
if content.Type == "refusal" {
return content.AsRefusal().Refusal, true
}
}
return "", true
}
return "", false
}
func runTool(name, argumentsJSON string) (map[string]any, error) {
var arguments toolArguments
if err := json.Unmarshal([]byte(argumentsJSON), &arguments); err != nil {
return nil, fmt.Errorf("parse %s arguments: %w", name, err)
}
switch name {
case "get_inventory":
return map[string]any{"sku": arguments.SKU, "available_units": 42}, nil
case "get_demand":
return map[string]any{"sku": arguments.SKU, "requested_units": 31}, nil
default:
return nil, fmt.Errorf("unknown tool: %s", name)
}
}
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
100
101
102
103
104
105
106
107
108
109
110
111
112import com.fasterxml.jackson.databind.ObjectMapper;
import com.openai.client.okhttp.OpenAIOkHttpClient;
import com.openai.core.JsonValue;
import com.openai.models.responses.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
var json = new ObjectMapper();
var tools =
List.of(
Tool.ofFunction(function("get_inventory", "available_units")),
Tool.ofFunction(function("get_demand", "requested_units")),
Tool.ofProgrammaticToolCalling());
var input = new ArrayList<ResponseInputItem>();
input.add(
ResponseInputItem.ofEasyInputMessage(
EasyInputMessage.builder()
.role(EasyInputMessage.Role.USER)
.content("Compare inventory with demand for sku_123.")
.build()));
while (true) {
var response =
client
.responses()
.create(
ResponseCreateParams.builder()
.model("gpt-6-astra")
.store(false)
.inputOfResponse(input)
.tools(tools)
.build());
if (!response.status().orElseThrow().equals(ResponseStatus.COMPLETED)) {
throw new IllegalStateException("Response ended with status " + response.status());
}
// Preserve every replayable output item, including program and reasoning items.
response.output().stream()
.map(item -> JsonValue.from(item).convert(ResponseInputItem.class))
.forEach(input::add);
var calls = response.output().stream().flatMap(item -> item.functionCall().stream()).toList();
if (calls.isEmpty()) {
var messages = response.output().stream().flatMap(item -> item.message().stream()).toList();
if (!messages.isEmpty()) {
messages.forEach(
message ->
message
.content()
.forEach(
content -> {
content.outputText().ifPresent(text -> System.out.println(text.text()));
content
.refusal()
.ifPresent(refusal -> System.out.println(refusal.refusal()));
}));
break;
}
continue;
}
for (var call : calls) {
String sku = json.readTree(call.arguments()).get("sku").asText();
var result =
switch (call.name()) {
case "get_inventory" -> Map.of("sku", sku, "available_units", 42);
case "get_demand" -> Map.of("sku", sku, "requested_units", 31);
default -> throw new IllegalArgumentException("Unknown tool: " + call.name());
};
var output =
ResponseInputItem.FunctionCallOutput.builder()
.callId(call.callId())
.output(json.writeValueAsString(result));
// Preserve caller so the runtime can resume the correct program.
call.caller()
.ifPresent(
caller ->
output.caller(
JsonValue.from(caller)
.convert(ResponseInputItem.FunctionCallOutput.Caller.class)));
input.add(ResponseInputItem.ofFunctionCallOutput(output.build()));
}
}
private static FunctionTool function(String name, String outputField) {
var parameters =
Map.of(
"type",
"object",
"properties",
Map.of("sku", Map.of("type", "string")),
"required",
List.of("sku"),
"additionalProperties",
false);
var outputSchema =
Map.of(
"type",
"object",
"properties",
Map.of("sku", Map.of("type", "string"), outputField, Map.of("type", "number")),
"required",
List.of("sku", outputField),
"additionalProperties",
false);
return FunctionTool.builder()
.name(name)
.description("Return an object with sku (string) and " + outputField + " (number).")
.parameters(JsonValue.from(parameters).convert(FunctionTool.Parameters.class))
.outputSchema(JsonValue.from(outputSchema).convert(FunctionTool.OutputSchema.class))
.allowedCallers(List.of(FunctionTool.AllowedCaller.PROGRAMMATIC))
.strict(true)
.build();
}
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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125require "json"
require "openai"
client = OpenAI::Client.new
def get_inventory(sku:)
{
sku: sku,
available_units: 42
}
end
def get_demand(sku:)
{
sku: sku,
requested_units: 31
}
end
implementations = {
"get_inventory" => method(:get_inventory),
"get_demand" => method(:get_demand)
}
tools = [
{
type: :function,
name: "get_inventory",
description: "Return an object with sku (string) and available_units (number).",
parameters: {
type: :object,
properties: { sku: { type: :string } },
required: ["sku"],
additionalProperties: false
},
output_schema: {
type: :object,
properties: {
sku: { type: :string },
available_units: { type: :number }
},
required: %w[sku available_units],
additionalProperties: false
},
allowed_callers: [:programmatic],
strict: true
},
{
type: :function,
name: "get_demand",
description: "Return an object with sku (string) and requested_units (number).",
parameters: {
type: :object,
properties: { sku: { type: :string } },
required: ["sku"],
additionalProperties: false
},
output_schema: {
type: :object,
properties: {
sku: { type: :string },
requested_units: { type: :number }
},
required: %w[sku requested_units],
additionalProperties: false
},
allowed_callers: [:programmatic],
strict: true
},
{ type: :programmatic_tool_calling }
]
input = [
{
role: :user,
content: "Compare inventory with demand for sku_123."
}
]
loop do
response = client.responses.create(
model: "gpt-6-astra",
store: false,
input: input,
tools: tools
)
unless response.status == OpenAI::Responses::ResponseStatus::COMPLETED
raise "Response ended with status #{response.status}"
end
# Preserve every output item, including program and reasoning items.
input.concat(response.output)
calls = response.output.grep(OpenAI::Models::Responses::ResponseFunctionToolCall)
if calls.empty?
message = response.output.find do |item|
item.is_a?(OpenAI::Models::Responses::ResponseOutputMessage)
end
next unless message.is_a?(OpenAI::Models::Responses::ResponseOutputMessage)
refusal = message.content.find do |content|
content.is_a?(OpenAI::Models::Responses::ResponseOutputRefusal)
end
text = response.output_text
if text.empty? &&
refusal.is_a?(OpenAI::Models::Responses::ResponseOutputRefusal)
text = refusal.refusal
end
puts(text)
break
end
calls.each do |call|
implementation = implementations.fetch(call.name) do
raise ArgumentError, "Unknown tool: #{call.name}"
end
result = implementation.call(**JSON.parse(call.arguments, symbolize_names: true))
output = {
type: :function_call_output,
call_id: call.call_id,
output: JSON.generate(result)
}
# Preserve caller so the runtime can resume the correct program.
output[:caller] = call.caller_.to_h if call.caller_
input << output
end
end
응답을 저장하면 이전 응답 항목을 모두 다시 보내는 대신 previous_response_id를 사용해 이어갈 수 있습니다. 새 function_call_output 항목들을 다음 입력으로 보내세요. store: false를 사용하는 경우에는 program, 추론, 함수 호출, 함수 호출 출력, program_output 항목을 하나도 빠뜨리지 않고 전체 시퀀스를 원래 순서대로 다시 보내세요.
상태를 저장하지 않는 추론 모델 요청에서는 반환된 추론 항목을 모두 다시 보내세요. 각 항목에는 기본적으로 encrypted_content가 포함됩니다. 상태를 저장하지 않고 대화를 이어가는 일반적인 방법은 대화 상태를 참고하세요.
- JavaScript가 설명문을 파싱하지 않고도 확인할 수 있도록 간결하고 구조화된 데이터를 반환하세요.
output_schema로 각 도구가 반환할 필드와 타입을 정의하고, 오류 발생 시 동작을 문서화하세요. 반환 형식을 미리 알 수 없다면 모델이 결과를 확인할 수 있도록 해당 도구는 직접 호출만 허용하세요.
- 프로그램 결과의 정확한 형식과 반드시 포함할 근거를 정의하세요. 프로그램이 유효한 결과를 생성할 수 없으면 실패를 명확히 나타내는 구조화된 결과를 반환하세요.
- 가능하면 함수 호출이 멱등성을 갖도록 설계하세요. 재시도하거나 재실행할 때 안전하지 않은 사이드 이펙트가 반복되어서는 안 됩니다.
- 호스팅된 프로그램에서 시작한 호출이라도 애플리케이션에서 호출마다 인수와 권한을 확인하세요.
- 모델이 도구를 올바르게 조합할 수 있도록 각 도구에 구체적인 이름과 설명을 지정하세요.
- 호출 주체에 관계없이 영향이 큰 작업을 실행하기 전에 애플리케이션 수준에서 승인을 받도록 하세요.
프로그래밍 방식의 도구 호출을 사용하면 모델 컨텍스트에 추가되는 중간 도구 출력의 양을 줄일 수 있지만, 그 효과는 작업과 도구 응답에 따라 달라집니다. 먼저 직접 도구 호출로 기준값을 구한 다음 대표적인 작업에서 두 방식을 비교하세요.
효율을 측정하기 전에 최종 답변이 충족해야 할 품질 기준과 반드시 제시해야 할 근거를 정하세요. 토큰 사용량과 도구 호출 횟수를 평가할 때 답변의 정확성, 완전성, 필요한 근거를 얼마나 충실히 제시했는지도 함께 살펴보세요. 효율을 위해 품질 저하를 허용한다면 어떤 부분을 어느 정도까지 허용하는지 명시하세요.
다음 항목을 측정하세요.
- 최종 답변의 정확성, 완전성, 필요한 근거를 제시한 정도
- 입력 토큰 수와 총 토큰 수, 전체 처리 지연 시간, 비용
- 모델 턴 수, 도구 호출 횟수, 재시도 횟수, 실패 후 복구 동작
- 안전성 평가 결과, 특히 사이드 이펙트와 승인 요건에 관한 결과
- 실제 호출 경로가 해당 워크플로우 단계에 지정한 경로와 일치하는지 여부
Agents API에서는 프로그래밍 방식의 도구 호출이 기본으로 활성화되며, OpenAI가 관리하는 에이전트 하네스에서 실행됩니다. 하네스는 에이전트에 exec 도구를 제공하며, 생성된 JavaScript에서 에이전트의 기존 도구를 사용할 수 있도록 합니다. 이 도구들을 명령줄 프로그램으로 래핑하거나 샌드박스에 설치할 필요는 없습니다.
프로그래밍 방식의 도구 호출을 비활성화하려면 agent.tools에 다음 항목을 추가하세요.
1234{
"type": "programmatic_tool_calling",
"enabled": false
}
이 항목이나 항목의 enabled 필드를 생략하면 프로그래밍 방식의 도구 호출은 활성 상태로 유지됩니다. 타입만 지정한 항목인 { "type": "programmatic_tool_calling" }을 사용해도 활성 상태가 유지됩니다. 위에서 설명한 allowed_callers 설정과 응답을 이어가는 루프는 Responses API 연동에 해당합니다.
프로그래밍 방식의 도구 호출은 environment.type을 none으로 설정한 대화 전용 세션에서도 작동합니다. Bash, 실행기 MCP, 그 밖에 샌드박스에서 실행되는 도구에는 여전히 실행 환경이 필요합니다.
JavaScript로 도구 호출을 조율해도 도구의 실행 위치는 바뀌지 않습니다. 셸 호출은 샌드박스에서 명령어를 실행하며, JavaScript 런타임 자체가 시스템 프로세스를 시작하지는 않습니다. 실행기 MCP는 계속 샌드박스를 사용하고, 함수 도구는 계속 애플리케이션 서버를 호출합니다. 에이전트는 도구 결과를 처리한 뒤 어떤 내용을 모델 컨텍스트에 포함할지 결정합니다.
위의 호출 경로 지침에 따라 코드를 사용할 워크플로우 단계를 지정하세요. Agents API 설정과 호출 처리 방법은 함수 및 MCP 연결 가이드를 따르세요.
- 함수 호출을 사용해 클라이언트에서 실행할 함수를 정의하세요.
- 도구 검색을 사용해 크기가 큰 도구 정의는 모델에 필요할 때 로드하세요.
- 대화 상태 가이드를 참고해 응답을 저장하는 경우와 상태를 저장하지 않는 경우에 Responses API 요청을 이어가세요.
- 저장 방식을 선택하기 전에 데이터 관리를 검토하세요.