
JavaScript로 도구를 호출하게 만드는 OpenAI의 Programmatic Tool Calling
요약
OpenAI가 Responses API에 도입한 Programmatic Tool Calling은 모델이 직접 도구를 호출하는 대신 JavaScript 코드를 작성하여 작업을 수행하게 합니다. 이를 통해 불필요한 모델-앱 간의 왕복(Ping-pong)을 줄이고, 토큰 사용량과 레이턴시를 획기적으로 개선할 수 있습니다.
핵심 포인트
- 모델이 직접 함수를 호출하는 대신 실행 가능한 JavaScript 코드를 생성함
- 병렬 처리 및 필터링을 코드 내에서 완결하여 컨텍스트 낭비 방지
- 토큰 비용과 레이턴시를 데이터 양이 아닌 태스크 단계 기준으로 최적화
- output_schema 정의를 통해 생성된 코드의 프로퍼티 참조 신뢰성 확보
재고 API를 10개 상품에 대해 호출하고, 재고가 5개 미만인 것만 추출하여 그 목록을 Slack에 전송한다. 에이전트(Agent)에게 이 작업을 시키면, 10회 분량의 JSON 응답 전체가 모델의 컨텍스트(Context)를 통과한다. 사용하는 것은 「available_units」 수치뿐인데도 말이다. 게다가 왕복할 때마다 모델의 추론(Inference)이 실행되므로, 레이턴시(Latency)와 비용도 왕복 횟수에 비례하여 늘어난다.
이러한 구조적인 낭비에 대해, OpenAI가 7월 9일 Responses API에 도입한 해답이 바로 Programmatic Tool Calling이다. 한마디로 말하자면, 모델이 도구를 직접 호출하게 하는 것을 그만두고, 도구를 호출하는 JavaScript를 작성하게 하는 것이다.
기존의 function calling은 모델이 get_inventory(sku="A")라는 함수 호출을 1개 출력하면, 앱 측에서 이를 실행하고 결과를 모델에 돌려주는 핑퐁(Ping-pong)의 반복이었다. 10개 상품이라면 최소 10번의 왕복이 필요하다.
Programmatic Tool Calling에서 모델은 다음과 같이 작성한다.
const skus = ["A", "B", "C", /* ... */];
const results = await Promise.all(skus.map(s => get_inventory({ sku: s })));
const low = results.filter(r => r.available_units < 5);
그리고 이 프로그램이 OpenAI 측의 런타임(Runtime)에서 실행된다. Promise.all에 의한 병렬 호출도, filter에 의한 필터링도, 루프(Loop)와 조건 분기(Conditional branching)도 모두 모델의 외부에서 완결된다. 모델의 컨텍스트로 돌아오는 것은 최종 결과뿐이다. 왕복 횟수와 토큰(Token) 사용량은 다루는 데이터 양이 아니라 「태스크의 단계(Task steps)」에 의해 결정되게 된다.
OpenAI의 API 변경 로그(Changelog)에는 같은 날 explicit prompt caching controls, persisted reasoning, max reasoning effort, Pro mode가 일괄 추가되었다고 기재되어 있다. GPT-5.6 패밀리(Sol / Terra / Luna)의 투입과 같은 타이밍에 에이전트 실행 주변의 토대(Foundation)가 한 단계 업데이트된 형태가 된다.
설정은 도구 정의(Tool definition)에 추가하기만 하면 된다. 호스트 도구로서 programmatic_tool_calling을 나열하고, 각 도구에 「누가 호출할 수 있는지」를 작성한다.
tools = [
{
"type": "function",
...
allowed_callers 지정은 3가지 방식이 있다.
| 지정 | 동작 |
|---|---|
생략 또는 ["direct"] | 모델이 직접 호출하는 기존 방식 |
["programmatic"] | 생성된 프로그램 내부에서만 호출 가능 |
["direct", "programmatic"] | 둘 다 가능 |
놓치기 쉬운 부분이 output_schema이다. 기존의 function calling에서 필수적인 것은 입력 측의 parameters뿐이었으며, 반환값(Return value)의 스키마는 작성하지 않아도 동작했다. 사람이 읽는 것을 전제로 한 JSON이 반환되면 모델이 해석해 주기 때문이다. 하지만 프로그램을 작성하게 되면 이야기가 달라진다. 모델은 r.available_units라는 프로퍼티 액세스(Property access)를 **실행 전(Before execution)**에 모두 작성해야 하며, 필드명이 다르면 undefined를 잡아 조용히 오류가 발생한다. 문서에서 output_schema를 「생성된 JavaScript에서 신뢰하고 필드를 참조할 수 있도록 만드는 것」으로 정의하고 있는 이유도 이 때문이며, 여기는 사실상의 필수 항목이라고 생각하는 것이 좋다.
호출할 수 있는 도구는 function / custom / mcp / apply_patch와 로컬·호스트 양측의 shell, code_interpreter이다. 기존의 MCP 서버를 그대로 프로그램에서 호출할 수 있다는 점은 실무적으로 매우 유용하다.
이 부분은 냉정하게 읽을 필요가 있다. 프로그램이 실행되는 곳은 매번 깨끗한 격리된 V8 런타임(Isolated V8 runtime)이며, 탑레벨 await는 사용할 수 있지만 그 외에는 상당히 제한되어 있다. 문서에서 명시적으로 「없다」고 기술하고 있는 것들을 나열하자면 Node.js, 패키지 설치, 직접적인 네트워크 액세스, 범용 파일 시스템, 서브프로세스 실행, console, 그리고 실행 간의 상태 유지(State persistence) 등이다.
즉 axios도, fs도, console.log도 없다. 외부 세계와 접촉할 수 있는 유일한 통로는 사용자가 allowed_callers로 허가한 도구뿐이다. 이는 제약인 동시에 설계 그 자체이며, 네트워크도 파일 시스템도 없는 V8은 샌드박스 탈출(Sandbox escape) 면적이 작고, 데이터가 모델의 가중치(Weights)나 로그를 경유하지 않는다. OpenAI가 이 기능을 ZDR(Zero Data Retention) 호환이라고 말할 수 있는 것은, 바로 이 '아무것도 없음'의 반대 급부이다.
디버깅 경험은 솔직히 까다로워진다. console이 없기 때문에, 프로그램 내부에서 어떤 일이 일어났는지 들여다보려면 로그와 유사한 역할을 하는 도구를 직접 하나 만드는 방법밖에 없다. 응답의 output 배열에는 생성된 JavaScript를 포함하는 program 아이템, 프로그램이 발행한 function_call (caller.caller_id가 부모 프로그램의 call_id와 일치하는 것), 그리고 최종 결과인 program_output이 나열되므로, 문제가 발생했을 때는 우선 program의 내용을 읽게 된다. 또한 함수 결과를 반환할 때는 caller 필드를 수정하지 않고 그대로 반환해야 한다. 서비스 측에서 어떤 프로그램을 재개해야 할지 판단할 수 없게 되기 때문이다.
이러한 발상 자체가 OpenAI의 발명은 아니다. Anthropic은 2025년 11월의 Code execution with MCP에서 같은 방향성을 제시했으며, 당시 수치는 150,000 토큰이 2,000 토큰으로 줄어드는 98.7%의 절감이었다. 반면 OpenAI가 이번에 보여주는 것은, 특정 케이스에서 직접적인 도구 호출(Tool calling) 비중 기준 63.5%의 토큰 절감 및 50.1%의 왕복(Round-trip) 절감 수준으로, 차원이 다르다.
이 차이를
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기