함수 호출 심화(Function Calling Deep Dive) — OpenAI, Anthropic, Gemini
세 최전선 제공자(Provider)는 2024년에 같은 도구 호출 루프(Tool-Call Loop)로 수렴했지만, 그 외의 거의 모든 표면은 서로 갈라졌습니다. OpenAI는 tools와 tool_calls를 사용합니다. Anthropic은 tool_use와 tool_result 블록(Block)을 사용합니다. Gemini는 functionDeclarations와 고유 식별자 상관관계(Unique-id Correlation)를 사용합니다. 이 강의는 세 제공자를 나란히 비교해서, 한 제공자에서 잘 돌던 코드가 다른 제공자로 이식될 때 배관(Plumbing) 차이 때문에 깨지지 않도록 도와줍니다.
유형: Build
언어: Python (표준 라이브러리, 스키마 변환기)
선수 지식: Phase 13 · 01 (도구 인터페이스)
예상 시간: 약 75분
학습 목표
- OpenAI, Anthropic, Gemini의 함수 호출(Function-Calling) 페이로드(Payload)가 선언(Declaration), 호출(Call), 결과(Result)에서 어떻게 다른지 세 가지 형태 차이를 말로 설명합니다.
- 하나의 도구 선언을 세 제공자 형식으로 모두 변환하고, 엄격 모드(Strict-Mode) 제약이 어디에서 달라지는지 예측합니다.
- 각 제공자의
tool_choice를 사용해서 도구 호출을 강제하거나 금지하거나, 모델이 자동으로 고르게 합니다.
- 제공자별 고정 한도(Hard Limit), 즉 도구 개수, 스키마 깊이(Schema Depth), 인자 길이(Argument Length)와, 한도를 어겼을 때 나오는 오류 시그니처(Error Signature)를 압니다.
문제
함수 호출 요청의 형태는 제공자마다 다릅니다. 2026년 프로덕션 스택의 구체적인 예시는 다음과 같습니다.
OpenAI Chat Completions / Responses API. tools: [{type: "function", function: {name, description, parameters, strict}}]를 전달합니다. 모델 응답에는 choices[0].message.tool_calls: [{id, type: "function", function: {name, arguments}}]가 들어 있고, 여기서 arguments는 직접 파싱(Parsing)해야 하는 JSON 문자열입니다. 엄격 모드(strict: true)는 제약 디코딩(Constrained Decoding)을 통해 스키마 준수를 강제합니다.
Anthropic Messages API. tools: [{name, description, input_schema}]를 전달합니다. 응답은 content: [{type: "text"}, {type: "tool_use", id, name, input}] 형태로 돌아옵니다. input은 이미 파싱된 객체이며 문자열이 아닙니다. 결과를 보낼 때는 {type: "tool_result", tool_use_id, content} 블록이 들어 있는 새 user 메시지로 응답합니다.
Google Gemini API. tools: [{functionDeclarations: [{name, description, parameters}]}]를 전달합니다. 선언이 functionDeclarations 아래에 중첩되어 있습니다. 응답은 candidates[0].content.parts: [{functionCall: {name, args, id}}]로 도착합니다. Gemini 3 이상에서 id는 병렬 호출 상관관계(Parallel-Call Correlation)를 위해 고유하게 부여됩니다. 결과는 {functionResponse: {name, id, response}} 형태로 회신합니다.
루프 구조는 같습니다. 하지만 필드 이름, 중첩 위치, 문자열 대 객체 관례, 상관관계 기제(Correlation Mechanism)가 모두 다릅니다. OpenAI 위에서 날씨 에이전트(Weather Agent)를 만든 팀은, 같은 코드를 Anthropic으로 옮기는 데 배관 수정만 이틀, Gemini로 옮기는 데 또 하루를 씁니다.
이 강의는 세 형식을 하나의 정규 도구 선언(Canonical Tool Declaration)으로 통합하고, 가장자리(Edge)에서 분기 라우팅하는 변환기(Translator)를 만듭니다. Phase 13 · 17은 같은 패턴을 LLM 게이트웨이(Gateway)로 일반화합니다.
개념
공통 구조
모든 제공자에는 다섯 가지가 필요합니다.
- 도구 목록(Tool List). 도구별 이름, 설명, 입력 스키마.
- 도구 선택(Tool Choice). 특정 도구를 강제하거나, 도구 사용을 금지하거나, 모델이 결정하게 합니다.
- 호출 방출(Call Emission). 도구 이름과 인자를 담은 구조화된 출력입니다.
- 호출 식별자(Call Id). 결과를 올바른 호출과 짝지어 줍니다. 병렬 호출에서 특히 중요합니다.
- 결과 주입(Result Injection). 결과를 원래 호출과 다시 이어 주는 메시지 또는 블록입니다.
필드별 형태 차이
| 관점 | OpenAI | Anthropic | Gemini |
|---|
| 선언 봉투(Envelope) | {type: "function", function: {...}} | {name, description, input_schema} | {functionDeclarations: [{...}]} |
| 스키마 필드 | parameters | input_schema | parameters |
| 응답 컨테이너 | 어시스턴트 메시지의 tool_calls[] | tool_use 타입의 content[] | functionCall 타입의 parts[] |
| 인자(Arguments) 타입 | 문자열화된 JSON | 파싱된 객체 | 파싱된 객체 |
| 식별자 형식 | call_... (OpenAI 생성) | toolu_... (Anthropic) | UUID (Gemini 3+) |
| 결과 블록 | 역할 tool, tool_call_id | tool_result, tool_use_id를 가진 user | 일치하는 id를 가진 functionResponse |
| 특정 도구 강제 | tool_choice: {type: "function", function: {name}} | tool_choice: {type: "tool", name} | tool_config: {function_calling_config: {mode: "ANY"}} |
| 도구 금지 | tool_choice: "none" | tool_choice: {type: "none"} | mode: "NONE" |
| 엄격 스키마(Strict Schema) | strict: true | 스키마 자체가 계약(항상 적용) | 요청 수준의 responseSchema |
실제로 부딪히는 한도
- OpenAI. 요청당 도구 128개. 스키마 깊이 5. 인자 문자열은 8192 바이트 이하. 엄격 모드는
$ref를 허용하지 않고, 범위가 겹치는 oneOf/anyOf/allOf를 허용하지 않으며, 모든 속성(Property)을 required에 나열해야 합니다.
- Anthropic. 요청당 도구 64개. 스키마 깊이는 사실상 제한이 없지만 실용적 한계는 10 정도입니다. 엄격 모드 플래그(Flag)는 따로 없습니다. 스키마는 계약이며 모델은 대체로 준수합니다.
- Gemini. 요청당 함수 64개. 스키마 타입은 OpenAPI 3.0 부분집합(Subset)입니다. JSON Schema 2020-12와 약간 차이가 있습니다. Gemini 3부터는 병렬 호출에 고유 식별자(Unique Id)가 붙습니다.
모든 제공자가 이름만 다르게 세 가지 모드를 지원합니다.
- 자동(Auto). 모델이 도구 사용과 텍스트 응답 중 선택합니다. 기본값입니다.
- 필수 / 임의(Required / Any). 모델이 최소한 하나의 도구를 호출해야 합니다.
- 금지(None). 모델이 도구를 호출하면 안 됩니다.
각 제공자에 고유한 모드도 있습니다.
- OpenAI. 특정 도구 이름을 강제할 수 있습니다.
- Anthropic. 특정 도구 이름을 강제할 수 있고,
disable_parallel_tool_use 플래그가 단일 호출과 다중 호출을 분리합니다.
- Gemini.
mode: "VALIDATED"는 모델 의도와 관계없이 모든 응답을 스키마 검증기(Schema Validator)로 통과시킵니다.
병렬 호출
OpenAI의 parallel_tool_calls: true는 기본값이며, 하나의 어시스턴트 메시지 안에서 여러 호출을 한 번에 방출합니다. 호스트는 모두 실행한 뒤, tool_call_id 별 항목이 들어 있는 일괄 도구 역할 메시지(Batched Tool-Role Message)로 답합니다. Anthropic은 과거에는 단일 호출 중심이었지만, Claude 3.5 기준 기본값인 disable_parallel_tool_use: false가 다중 호출을 가능하게 합니다. Gemini 2는 병렬 호출은 허용했지만 안정된 식별자(Stable Id)가 없었습니다. Gemini 3는 UUID를 추가해서 순서가 뒤섞인 응답도 깔끔히 연결할 수 있게 했습니다.
스트리밍(Streaming)
세 제공자 모두 스트리밍 도구 호출(Streamed Tool Call)을 지원합니다. 전송 형식(Wire Format)은 다릅니다.
- OpenAI.
tool_calls[i].function.arguments의 델타 청크(Delta Chunk)가 조금씩 도착합니다. finish_reason: "tool_calls"가 올 때까지 누적합니다.
- Anthropic.
block-start / block-delta / block-stop 이벤트(Event)가 옵니다. input_json_delta 청크가 부분 인자를 담습니다.
- Gemini. Gemini 3에서 새로 추가된
streamFunctionCallArguments가 functionCallId와 함께 청크를 방출해서, 여러 병렬 호출이 섞여 도착해도 처리할 수 있습니다.
Phase 13 · 03은 병렬 호출과 스트리밍 재조립(Reassembly)을 깊게 다룹니다. 이 강의는 선언과 단일 호출 형태에 집중합니다.
오류와 복구
잘못된 인자(Argument) 오류도 제공자마다 다른 모습으로 드러납니다.
- OpenAI(비엄격, Non-strict). 모델이
arguments: "{bad json}"을 반환하고 JSON 파싱이 실패합니다. 오류 메시지를 넣어 다시 호출합니다.
- OpenAI(엄격, Strict). 디코딩 중에 검증(Validation)이 일어나므로 잘못된 JSON은 나올 수 없지만, 거절(Refusal) 블록은 나올 수 있습니다.
- Anthropic.
input에 예상하지 못한 필드가 섞여 들어올 수 있습니다. 스키마는 권고(Advisory) 성격이 강하므로 서버 측 검증이 필요합니다.
- Gemini. OpenAPI 3.0의 묘한 차이(Quirk)가 있습니다. 객체 필드의
enum이 조용히 무시될 수 있으므로 직접 검증해야 합니다.
변환기 패턴
코드 안에서 사용하는 정규 도구 선언은 다음과 같은 형태입니다. 구체적인 형태는 여러분이 정합니다.
Tool(
name="get_weather",
description="Use when ...",
input_schema={"type": "object", "properties": {...}, "required": [...]},
strict=True,
)
작은 함수 세 개가 이를 세 제공자 형태로 변환합니다. code/main.py의 하니스(Harness)가 정확히 이 일을 하고, 가짜 도구 호출을 각 제공자의 응답 형태로 왕복(Round-trip)시켜 보여 줍니다. 네트워크는 필요 없습니다. 이 강의는 HTTP 통신이 아니라 형태(Shape)를 가르치기 때문입니다.
프로덕션 팀은 이런 변환기를 AbstractToolset(Pydantic AI), UniversalToolNode(LangGraph), BaseTool(LlamaIndex) 같은 추상화로 감쌉니다. Phase 13 · 17은 세 제공자 앞단에 OpenAI 형태 API를 노출하는 게이트웨이를 출시합니다.
사용해보기
code/main.py는 하나의 정규 Tool 데이터 클래스와, OpenAI / Anthropic / Gemini 선언 JSON을 만드는 변환기 세 개를 정의합니다. 그다음에 손으로 만든 제공자별 응답을 같은 정규 호출 객체(Canonical Call Object)로 파싱해서, 표면 아래의 의미(Semantics)가 같다는 것을 보여 줍니다. 실행한 뒤 세 선언을 나란히 비교해 봅니다.
볼 지점은 다음과 같습니다.
- 세 선언 블록은 봉투(Envelope)와 필드 이름만 다릅니다.
- 세 응답 블록은 호출이 어디에 자리하는지가 다릅니다. 최상위
tool_calls, content[] 블록, parts[] 항목 순으로 위치가 옮겨 갑니다.
- 하나의
canonical_call() 함수가 세 응답 형태 모두에서 {id, name, args}를 추출합니다.
산출물 만들기
이 강의는 outputs/skill-provider-portability-audit.md를 만듭니다. 한 제공자에 맞춰 만든 함수 호출 통합(Integration)이 주어지면, 이 스킬(Skill)은 이식성 감사(Portability Audit)를 만듭니다. 어떤 제공자별 한도에 의존하는지, 어떤 필드 이름을 바꿔야 하는지, 다른 제공자로 옮길 때 무엇이 깨지는지를 점검합니다.
연습문제
-
(쉬움) code/main.py를 실행하고, 세 제공자 선언 JSON이 모두 같은 기반 Tool 객체를 직렬화한 결과인지 확인합니다. 정규 도구에 enum 매개변수를 추가하고, Gemini 변환기만 OpenAPI 묘한 차이를 별도로 처리해야 하는지 확인합니다.
-
(중간) 각 제공자에 대해 ListToolsResponse 파서를 추가해서, 모델이 list_tools나 디스커버리 호출(Discovery Call) 뒤에 반환하는 도구 목록을 추출합니다. OpenAI에는 이 기능이 기본 제공되지 않는다는 비대칭을 기록합니다.
-
(중간) tool_choice 변환을 구현합니다. 정규 ToolChoice(mode="force", tool_name="x")를 세 제공자 형태로 사상(Mapping)합니다. 그 뒤 mode="any"와 mode="none"도 사상합니다. 강의의 차이 비교 표와 대조해 봅니다.
-
(중간) 세 제공자 중 하나를 골라 함수 호출 가이드를 끝까지 읽습니다. 해당 스키마 사양에서 다른 두 제공자가 지원하지 않는 필드 하나를 찾습니다. 후보는 OpenAI strict, Anthropic disable_parallel_tool_use, Gemini function_calling_config.allowed_function_names입니다.
-
(어려움) 테스트 벡터를 작성합니다. 선언된 스키마를 위반하는 인자를 가진 도구 호출입니다. 각 제공자의 검증기 또는 Lesson 01의 표준 라이브러리 검증기를 대리(Proxy)로 사용해서 어떤 오류가 나는지 기록합니다. 프로덕션에서 엄격성(Strictness) 관점으로 어떤 제공자를 쓸지 문서화합니다.
핵심 용어
| 용어 | 흔한 설명 | 실제 의미 |
|---|
| 함수 호출(Function Calling) | "도구 사용" | 구조화된 도구 호출을 방출하기 위한 제공자 수준 API |
| 도구 선언(Tool Declaration) | "도구 스펙" | 이름 + 설명 + JSON Schema 입력 페이로드 |
tool_choice | "강제 / 금지" | 자동 / 필수 / 금지 / 특정 이름 모드 |
| 엄격 모드(Strict Mode) | "스키마 강제(Schema Enforcement)" | 스키마와 일치하도록 디코딩을 제약하는 OpenAI 플래그 |
tool_use 블록 | "Anthropic의 호출 형태" | id, name, input을 담는 인라인 콘텐츠 블록 |
functionCall 파트 | "Gemini의 호출 형태" | name, args, id를 담은 parts[] 항목 |
| 문자열형 인자(Arguments-as-string) | "문자열화된 JSON" | OpenAI는 인자를 객체가 아니라 JSON 문자열로 반환한다 |
| 병렬 도구 호출(Parallel Tool Calls) | "한 턴(Turn) 안의 팬아웃(Fan-out)" | 하나의 어시스턴트 메시지 안에 여러 도구 호출 |
| 거절(Refusal) | "모델의 거절" | 엄격 모드에서 호출 대신 등장하는 거절 블록 |
| OpenAPI 3.0 부분집합 | "Gemini 스키마의 묘한 차이" | Gemini가 사용하는, JSON Schema와 유사하지만 미세하게 다른 방언(Dialect) |
더 읽을거리