함수 호출: Responses API와 Chat Completions 비교
프로토콜 수준에서 함수 정의, 호출 ID, 결과 메시지, 인자 스트리밍, 권한, 멱등성과 경로 호환성을 비교합니다.

Responses와 Chat Completions는 모두 애플리케이션에 함수 실행을 요청할 수 있지만 도구 호출 흐름을 표현하는 방식이 다릅니다. Chat Completions는 tools[].function 아래에 정의를 두고 message.tool_calls를 반환하며 role: "tool" 메시지로 결과를 받습니다. Responses는 평면적인 함수 정의, 형식이 지정된 function_call 출력 항목, call_id로 연결된 function_call_output을 사용합니다.
모델이 애플리케이션 함수를 직접 실행하는 것은 아닙니다. 애플리케이션 코드가 제안된 인수를 검증하고 권한을 확인한 뒤 정확히 실행하고, 결과를 모델에 반환하며, 요청을 재시도할 때 중복 부작용을 막아야 합니다.
도구 호출 흐름의 네 단계
1. 허용된 함수와 인자 스키마를 선언한다
2. 모델이 제안한 하나 이상의 함수 호출을 받는다
3. 애플리케이션에서 검증, 권한 확인, 실행을 수행한다
4. 각 호출의 상관관계 ID를 사용해 결과를 반환한다
네 번째 단계 이후에야 모델이 도구 결과에 근거한 답을 만들 수 있습니다. 다른 도구가 필요하면 새 호출 ID로 반복합니다. OpenAI Function Calling 가이드도 이를 다단계 교환으로 설명합니다.
전송 규약 비교하기
| 항목 | Responses API | Chat Completions |
|---|---|---|
| 함수 정의 | 평면적인 tools[] 항목 |
tools[].function 아래에 중첩 |
| 모델이 제안한 호출 | 형식이 지정된 function_call 항목 |
어시스턴트 메시지의 tool_calls[] |
| 연관 ID | call_id |
id, 결과에는 tool_call_id |
| 함수 이름 | function_call.name |
tool_calls[].function.name |
| 인수 | function_call.arguments의 JSON 문자열 |
tool_calls[].function.arguments의 JSON 문자열 |
| 도구 결과 | function_call_output 항목 |
role: "tool" 메시지 |
| 스트리밍 | 형식이 지정된 인수 증분 이벤트 | delta.tool_calls[] 조각 |
| 최종 텍스트 | 출력 항목 / SDK 보조 기능 | choices[0].message.content |
배열 위치로 호출을 연결하면 안 됩니다. 병렬 호출과 스트리밍 조각은 완료 순서가 달라질 수 있으므로 명시적인 호출 ID를 안정적인 연관 키로 사용합니다.
엄격한 함수 스키마 정의하기
읽기 전용 배송 조회 함수의 인수 스키마입니다.
{
"type": "object",
"properties": {
"order_id": {
"type": "string",
"pattern": "^ORDER-[0-9]{4}$"
}
},
"required": ["order_id"],
"additionalProperties": false
}
strict: true는 지원 모델에 스키마 준수를 요청하지만 애플리케이션의 파싱과 검증은 여전히 필요합니다. pattern과 같은 기능은 제공자마다 다를 수 있으므로 Structured Outputs와 같은 호환성 검증을 적용합니다.
Responses API로 호출 흐름 구현하기
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.MODELFLARE_API_KEY,
baseURL: "https://modelflare.dev/v1",
});
const model = process.env.MODEL_ID;
if (!model) throw new Error("MODEL_ID가 필요합니다");
const orders = new Map([
["ORDER-1001", { status: "in_transit", eta: "2026-08-06" }],
]);
const tools = [
{
type: "function",
name: "get_delivery_status",
description: "주문의 현재 배송 상태를 반환합니다.",
parameters: {
type: "object",
properties: {
order_id: { type: "string", pattern: "^ORDER-[0-9]{4}$" },
},
required: ["order_id"],
additionalProperties: false,
},
strict: true,
},
];
function executeTool(name, rawArguments) {
if (name !== "get_delivery_status") {
throw new Error(`허용되지 않은 도구: ${name}`);
}
const args = JSON.parse(rawArguments);
if (!/^ORDER-[0-9]{4}$/.test(args.order_id)) {
throw new Error("order_id가 올바르지 않습니다");
}
return orders.get(args.order_id) ?? { status: "not_found" };
}
const first = await client.responses.create({
model,
input: "ORDER-1001 배송 상태를 알려 주세요.",
tools,
parallel_tool_calls: false,
});
const calls = first.output.filter((item) => item.type === "function_call");
if (calls.length === 0) {
console.log(first.output_text);
process.exit(0);
}
const toolOutputs = calls.map((call) => ({
type: "function_call_output",
call_id: call.call_id,
output: JSON.stringify(executeTool(call.name, call.arguments)),
}));
const final = await client.responses.create({
model,
input: [...first.output, ...toolOutputs],
tools,
parallel_tool_calls: false,
});
console.log(final.output_text);
모델 출력의 call.call_id와 결과의 call_id가 정확히 일치해야 합니다. 무상태 교환에서는 앞선 출력 항목과 함수 결과를 함께 반환합니다. 모든 OpenAI 호환 경로가 응답 상태를 저장한다고 가정하지 마십시오.
Chat Completions로 호출 흐름 구현하기
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.MODELFLARE_API_KEY,
baseURL: "https://modelflare.dev/v1",
});
const model = process.env.MODEL_ID;
if (!model) throw new Error("MODEL_ID가 필요합니다");
const orders = new Map([
["ORDER-1001", { status: "in_transit", eta: "2026-08-06" }],
]);
const tools = [
{
type: "function",
function: {
name: "get_delivery_status",
description: "주문의 현재 배송 상태를 반환합니다.",
parameters: {
type: "object",
properties: {
order_id: { type: "string", pattern: "^ORDER-[0-9]{4}$" },
},
required: ["order_id"],
additionalProperties: false,
},
strict: true,
},
},
];
function executeTool(name, rawArguments) {
if (name !== "get_delivery_status") {
throw new Error(`허용되지 않은 도구: ${name}`);
}
const args = JSON.parse(rawArguments);
if (!/^ORDER-[0-9]{4}$/.test(args.order_id)) {
throw new Error("order_id가 올바르지 않습니다");
}
return orders.get(args.order_id) ?? { status: "not_found" };
}
const messages = [
{ role: "user", content: "ORDER-1001 배송 상태를 알려 주세요." },
];
const first = await client.chat.completions.create({
model,
messages,
tools,
parallel_tool_calls: false,
});
const assistant = first.choices[0].message;
const calls = assistant.tool_calls ?? [];
if (calls.length === 0) {
console.log(assistant.content);
process.exit(0);
}
messages.push(assistant);
for (const call of calls) {
const result = executeTool(call.function.name, call.function.arguments);
messages.push({
role: "tool",
tool_call_id: call.id,
content: JSON.stringify(result),
});
}
const final = await client.chat.completions.create({
model,
messages,
tools,
parallel_tool_calls: false,
});
console.log(final.choices[0].message.content);
도구 호출이 포함된 어시스턴트 메시지를 먼저 messages에 추가한 뒤 결과를 넣습니다. 원본 메시지를 누락하거나 맞지 않는 tool_call_id를 반환하면 대화 기록이 유효하지 않습니다.
스트리밍 인수를 조각으로 다루기
{"order는 잘못된 JSON이 아니라 아직 완성되지 않은 JSON입니다. 호출별 버퍼를 만들고 각 증분을 올바른 버퍼에 추가한 뒤 인수 완료 또는 응답 완료 신호를 기다립니다. 완성된 문자열을 한 번만 파싱하고 검증 후 실행합니다. Responses는 형식이 지정된 이벤트를, Chat Completions는 choices[].delta.tool_calls[]를 사용합니다. 첫 조각에서 도구를 실행하면 안 됩니다. 자세한 경계는 AI API 스트리밍 가이드를 참고하십시오.
도구 실행을 안전하고 멱등성 있게 만들기
- 등록된 함수 이름만 허용합니다.
- 크기 제한 내에서 파싱하고 완전한 스키마를 검증합니다.
- 현재 사용자 또는 작업 부하가 리소스를 사용할 권한이 있는지 확인합니다.
- 읽기 전용 도구와 부작용이 있는 도구를 분리합니다.
- 모델로 돌려보낼 결과에서 비밀 정보를 제거합니다.
- 기한을 설정하고 하위 서비스 재시도 횟수를 제한합니다.
- 원문 대신 안전한 작업 참조를 기록합니다.
부작용이 있는 도구는 안정적인 애플리케이션 요청과 호출 ID에서 멱등 키를 만들고 결과를 먼저 저장해야 합니다. 네트워크 재시도가 같은 호출을 다시 보내면 두 번째 환불, 메시지, 배포, 데이터베이스 변경 대신 저장된 결과를 반환합니다.
모델이 생성한 함수 이름과 인수는 신뢰할 수 없는 입력입니다. 스키마가 유효해도 권한이나 소유권을 부여하지 않으며 트랜잭션 경계를 대체하지 못합니다.
여러 호출의 처리 방식 결정하기
예시는 parallel_tool_calls: false로 상태 기계를 단순하게 유지합니다. 병렬 호출을 켜면:
- 완료 순서가 아니라 호출 ID로 결과를 연결합니다.
- 응답당 호출 수와 애플리케이션 동시 실행 수를 제한합니다.
- 하나의 실패가 다른 결과에 미치는 영향을 정의합니다.
- 수락한 모든 호출에 결과 하나를 반환합니다.
- 순서가 중요한 부작용은 직렬로 실행합니다.
독립적인 읽기 작업에서는 지연 시간을 줄일 수 있지만 권한 확인, 순서 제어, 재시도, 부분 실패 처리가 복잡해집니다.
Modelflare 호환성 경계 이해하기
Modelflare는 적격 OpenAI/Codex 경로에서 엄격한 함수 정의를 보존하고, 지원되는 함수 호출 형식을 Responses와 Chat Completions 사이에서 변환합니다. 이 범위는 Responses의 전체 도구 기능보다 의도적으로 좁습니다.
애플리케이션이 정의한 함수 도구는 두 형식으로 표현할 수 있습니다. 제공자가 실행하는 검색, 코드 실행 등의 호스팅 도구는 같은 개념이 아니며 Chat Completions로 자동 변환된다고 가정할 수 없습니다. 다른 OpenAI 호환 모델 계열에서는 별도 검증 전까지 Chat Completions를 변환 없이 전달하며, tools, strict, 병렬 호출, 스트리밍 인수, tool_choice 지원 여부는 상위 제공자 계약이 결정합니다.
| 테스트 | 필요한 증거 |
|---|---|
| 읽기 전용 호출 | 함수 이름, 파싱된 인수, 호출 ID, 결과 연결, 최종 텍스트 |
| 잘못된 인수 | 도구 미실행과 명시적인 검증 실패 |
| 알 수 없는 함수 | 애플리케이션 허용 목록에서 거절 |
| 도구가 필요 없는 경로 | 허위 호출 없는 정상 최종 텍스트 |
| 스트리밍 호출 | 완전한 인수와 올바른 호출 ID |
| 반복 요청 | 한 번의 부작용 또는 캐시된 결과 |
| 여러 호출 | 완료 순서와 무관한 연관 관계 |
| 대체 경로 | 동일한 모델, 프로토콜, 스키마, 결과 계약 |
안정적인 AI API 라우팅으로 경로 변경을 명시적으로 관리하고 AI API 키 보안으로 작업 부하와 권한을 분리하십시오.
애플리케이션 계약에 따라 형식 선택하기
형식이 지정된 출력 항목, Responses 이벤트 모델, 상태 연속성 또는 검증된 Responses 기능이 필요하면 Responses를 사용합니다. 안정된 메시지 기록을 이미 보유하고 제공자의 도구 계약이 검증되었다면 Chat Completions가 적합합니다. 더 넓은 비교는 Responses API와 Chat Completions를 참고하십시오.
두 형식의 구현 기준은 같습니다. 명시적인 도구 허용 목록, 엄격한 인수 스키마, 애플리케이션 측 검증, 실행 전 권한 확인, 호출 ID 연관, 부작용의 멱등성, 완전한 결과 반환이 필요합니다. 엔드포인트 이름이 아니라 이러한 통제가 작업 흐름을 신뢰할 수 있게 만듭니다.