본문 바로가기
AI Agent/Tool-Use (Tool Calling)

Native Tool Calling

by AteN 2025. 5. 28.

네이티브 도구 호출 완전 정리, 호출을 텍스트에서 떼어내면

요약

  • 네이티브 도구 호출은 "어떤 함수를 어떤 인자로 부를지"를 자유 텍스트가 아니라 별도의 구조화된 필드로 내보내는 방식입니다. 핵심은 한 문장입니다. 호출을 텍스트에서 떼어내 구조로 만든다.
  • 기본 단위는 왕복입니다. 앱이 스키마를 넘기고, 모델이 구조화 호출을 만들고, 앱이 실제 실행하고, 결과를 다시 모델에 되돌립니다. 모델 호출 2번 사이에 우리 코드의 실행 1번이 낍니다.
  • 가장 흔한 오해. 네이티브가 더 똑똑한 게 아닙니다. 네이티브는 인터페이스 구현 방식이지 추론력 자체가 아닙니다.
  • 두 번째 오해. 네이티브라고 인자 JSON이 항상 맞는 건 아닙니다. 보장하려면 제약 디코딩(strict)이 따로 필요하고, 이건 병렬 호출과 상충합니다.
  • 제공사마다 이름과 중첩이 다릅니다. 개념은 같지만 그대로 이식하면 안 되고 어댑터가 필요합니다.
  • 오픈 모델에서는 chat template의 도구 토큰과 런타임 파서 조합이 곧 네이티브인데, 포맷 표준에 합의가 없습니다.

1장. "네이티브"가 무엇을 가리키나

1.1 도구 호출 자체부터

LLM 은 텍스트만 만든다
   날씨를 진짜로 조회하거나
   파일을 쓰거나
   계산기를 돌릴 수는 없다

그래서 도구 호출이 필요하다
   모델이 "이런 함수를, 이런 인자로 불러줘"라고 지시서를 만들면
   실제 실행은 우리 코드가 대신 한다

   이게 에이전트 루프의 '행동' 단계다

1.2 두 갈래

"네이티브"는 이 지시서를 다루는 방식을 가리킵니다.

프롬프트 기반 (non-native)
   시스템 프롬프트에 "너는 이런 도구를 쓸 수 있어..."라고 글로 설명
   모델이 자유 텍스트 안에 JSON 이나 의사코드를 섞어 뱉으면
   우리가 정규식과 휴리스틱으로 긁어낸다

   초기 ReAct 와 구버전 프레임워크가 이랬다

네이티브
   도구를 API 의 tools 파라미터(스키마)로 넘기고
   모델이 훈련된 능력으로 별도의 구조화 필드에 호출을 담아 내면
   런타임이 그걸 파싱해 정식 객체로 돌려준다

1.3 네이티브 = 모델 능력 + 플랫폼 인터페이스

모델 능력       도구 포맷을 파인튜닝으로 학습한 것
플랫폼 인터페이스  1급 구조로 API 에 통합된 것

   둘이 결합된 형태가 "네이티브"다

핵심은 하나입니다. 호출을 텍스트에서 떼어내 구조로 만든다.

1.4 흔한 오해 둘

① "네이티브면 모델이 알아서 다 한다"
   아니다

   함수 선택과 인자 정확도는  모델 능력
   구조 안정성과 파싱은       런타임 책임

   둘을 분리해서 봐야 원인을 찾을 수 있다

② "네이티브 = 더 똑똑"
   네이티브는 인터페이스 구현 방식일 뿐 추론력이 아니다

1.5 주방의 주문 전표

손님(앱) 이 메뉴판(tools 스키마) 을 요리사에게 건넨다

요리사(모델) 는 말로 "음... 파스타 좋을 것 같은데요"라고 떠들지 않는다
   대신 주문 전표(tool_calls) 에 메뉴명과 옵션(인자)을 또박또박 적어 낸다

주방(내 런타임) 이 그 전표대로 실제 요리(실행)를 하고
완성 접시(도구 결과) 를 다시 요리사에게 보여준다

요리사는 그 접시를 보고 다음 판단을 한다

프롬프트 기반은 요리사가 손님과의 대화 속에 주문을 흘리듯 섞어 말하는 격입니다. 주방이 알아듣기 애매합니다. 네이티브는 전표라는 별도 양식을 강제해 이 모호함을 없앱니다.


2장. 한 번의 호출은 모델 호출 2번

2.1 여섯 단계

① 도구 정의 전달
   요청에 tools (이름 + 설명 + JSON Schema) 와
   tool_choice (auto / 강제 / 금지 / 특정 도구) 를 실어 보낸다

② 모델 결정
   그냥 답할지 도구를 부를지 정한다
   부르기로 하면 자연어 대신 구조화 호출을 출력한다

③ 런타임 파싱
   API 나 self-host 파서가 모델 출력에서 호출을 떼어내 객체로 돌려준다

④ 실행
   내 코드가 그 함수를 실제로 실행한다
   모델은 실행하지 않는다

⑤ 결과 주입
   실행 결과를 도구 결과 메시지로 대화에 도로 넣는다

⑥ 반복 또는 종결
   모델이 결과를 보고 다음 호출을 하거나 최종 답을 낸다

⑥이 반복되면 그게 에이전트 루프입니다.

2.2 흐름

사용자: 서울 날씨 어때?
   │
   ▼
앱: messages + tools 스키마 전송
   │
   ▼
모델 호출 ①   tool_choice: auto
   │
   ├─ 도구 불필요 ──► 바로 자연어 답변, finish_reason: stop
   │
   └─ 도구 필요
        │
        ▼
     구조화 호출 반환
     tool_calls: get_weather(city=서울)
     finish_reason: tool_calls
        │
        ▼
     앱이 실제 get_weather 실행 ──► {temp:24, cond:맑음}
        │
        ▼
     결과를 tool 메시지로 messages 에 추가
        │
        ▼
     모델 호출 ②
        │
        ▼
     최종 답: 서울은 24도, 맑습니다, finish_reason: stop

"모델이 실행하지 않는다"를 기억하면 설계가 안 꼬입니다. 모델은 지시서만 쓰고, 권한과 실행은 전부 우리 쪽에 있습니다.


3장. 날씨 도구 한 바퀴

OpenAI Chat Completions 포맷으로 왕복을 따라갑니다.

3.1 도구 결정 요청

POST /v1/chat/completions
{
  "model": "...",
  "tool_choice": "auto",
  "messages": [ {"role": "user", "content": "서울 지금 날씨 어때?"} ],
  "tools": [ {"type": "function", "function": {
      "name": "get_weather",
      "description": "도시의 현재 날씨를 조회한다.",
      "parameters": {
        "type": "object",
        "properties": {"city": {"type": "string"}},
        "required": ["city"],
        "additionalProperties": false
      }}} ]
}

3.2 모델이 구조화 호출을 반환

여기가 네이티브의 본질입니다.

{"choices": [{"finish_reason": "tool_calls", "message": {
   "content": null,
   "tool_calls": [{"id": "call_1", "type": "function",
     "function": {"name": "get_weather", "arguments": "{\"city\":\"서울\"}"}}]}}]}

세 가지를 눈여겨봅니다.

content 가 null 이다        자연어 답이 아니다
tool_calls 필드에 담긴다     텍스트와 분리된 별도 구조
arguments 는 JSON 문자열이다  객체가 아니라 문자열이다. 파싱이 필요하다
finish_reason 이 tool_calls   왜 멈췄는지가 명시된다

3.3 런타임이 실행

앱이 arguments를 파싱해 실제 get_weather("서울")을 호출하고 {"temp":24,"cond":"맑음"}을 얻습니다.

3.4 결과를 대화에 도로 넣고 재호출

assistant의 tool_calls와, 같은 id를 가진 tool 역할 결과 메시지를 이어붙입니다.

"messages": [
  {"role": "user", "content": "서울 지금 날씨 어때?"},
  {"role": "assistant", "tool_calls": [{"id": "call_1", "type": "function",
     "function": {"name": "get_weather", "arguments": "{\"city\":\"서울\"}"}}]},
  {"role": "tool", "tool_call_id": "call_1", "content": "{\"temp\":24,\"cond\":\"맑음\"}"}
]

id 매칭이 중요합니다. 병렬 호출에서 결과가 뒤섞이면 모델이 엉뚱한 해석을 합니다.

3.5 최종 답

"서울은 지금 24도, 맑습니다."가 나오고 finish_reasonstop이 됩니다. 도구가 더 필요하면 3.2에서 3.4가 반복됩니다.

3.6 변형 셋

① 병렬 호출
   "서울이랑 부산 날씨"면 모델이 tool_calls 에 2개를 한 번에 담는다
   런타임이 각각 실행해 tool 메시지 2개로 되돌린다

② 제공사 차이
   Anthropic 은 도구를 input_schema 로 정의하고
   호출은 assistant 응답의 content 블록 안에 tool_use 로 온다
   결과는 같은 tool_use_id 를 가진 tool_result 블록으로 되돌린다
   개념은 같고 이름과 중첩만 다르다

③ 어디서 깨지나
   3.2 의 arguments JSON 이 스키마를 어기면 3.3 실행이 실패한다
   이걸 생성 시점에 막는 게 5장의 제약 디코딩이다

4장. 제공사별 차이

4.1 비교표

개념(스키마 전달 → 구조화 호출 → 결과 반환)은 같지만 필드 이름과 구조가 다릅니다.

OpenAI Anthropic Google Gemini
도구 정의 tools[].function (name, parameters) tools[] (name, input_schema) Tool{functionDeclarations[]}
호출 표현 message.tool_calls[] (id, name, arguments는 JSON 문자열) tool_use content 블록 (id, name, input) functionCall part (name, args 객체)
결과 반환 role:"tool" 메시지 (tool_call_id) tool_result 블록 (tool_use_id 매칭) functionResponse part
강제와 선택 tool_choice: auto, required, none, {name} tool_choice: auto, any, tool, none mode: AUTO, ANY, NONE
병렬 parallel_tool_calls content에 여러 tool_use 블록 여러 functionCall part
스키마 강제 strict: true strict: true responseSchema 등

4.2 구조적으로 가장 다른 지점

Anthropic 은 호출을 텍스트와 같은 content 배열의 블록으로 둔다

   즉 한 응답 안에
   텍스트 블록과 tool_use 블록이 교차할 수 있다

   OpenAI 는 content 와 tool_calls 가 별도 필드다

   이 차이 때문에 어댑터가 필요하다

4.3 같은 회사 안에서도 이름이 다르다

OpenAI 의 최신 Responses API 는
같은 개념을 function_call, call_id, function_call_output 으로 부른다

   3장 예시의 Chat Completions 형태와 필드명이 다르다
   문서를 볼 때 어느 API 기준인지 확인해야 한다

5장. 오픈 모델에서의 네이티브

5.1 무엇이 네이티브를 만드나

self-host 에서 네이티브는
chat template 의 도구 토큰 + 런타임의 tool-call 파서 조합이다

chat template
   토크나이저에 든 Jinja2 템플릿
   역할(system/user/assistant/tool)과 도구 정의를
   모델이 학습한 토큰열로 직렬화한다

   네이티브 여부는 사실상
   "이 템플릿에 도구용 토큰이 있나"로 갈린다

5.2 vLLM의 파서 선택

--tool-call-parser hermes --enable-auto-tool-choice

제공되는 파서에는 hermes, mistral, llama3_json, granite, xlam, deepseek_v3 등이 있고 대부분 JSON 기반입니다.

5.3 포맷 표준에 합의가 없다

이게 오픈 모델에서 가장 큰 문제입니다.

모델 계열 호출 표식
Hermes 계열 <tool_call>...</tool_call> 태그
Qwen2.5 chat template이 Hermes식이라 hermes 파서를 재사용
Mistral [TOOL_CALLS] 토큰
Llama 3.1 `<
Llama 3.2 특수 토큰 없음

마지막 줄이 합의 부재의 단적인 예입니다. 같은 회사의 연속 버전인데 표식 방식이 다릅니다.

5.4 vLLM 문서가 직접 적어둔 함정

Llama 의 소형 모델은 호출 포맷을 자주 틀린다
Mistral 7B 는 병렬 호출을 제대로 못 만든다

소형 모델일수록 네이티브 출력만 믿으면 안 되고 런타임 보강이 필요합니다.


6장. 스키마를 어기지 못하게

6.1 왜 필요한가

네이티브라도 인자 JSON은 깨질 수 있습니다. 이를 생성 시점에 막는 게 제약 디코딩입니다.

문법(grammar)이나 JSON Schema 로
스키마에 맞는 토큰만 샘플링한다

   틀린 JSON 을 만들고 나서 고치는 게 아니라
   애초에 못 만들게 한다

6.2 제공사별 방법

OpenAI strict: true
   Structured Outputs 로 인자가 스키마를 보장한다

   요건이 있다
     additionalProperties: false
     모든 properties 를 required 로

Anthropic strict: true
   커스텀 도구 정의에 붙이면 호출이 스키마와 정확히 일치하도록 강제한다

self-host
   vLLM 과 llama.cpp 의 grammar(GBNF)나 guided JSON 으로 같은 효과

6.3 실무 함정, strict와 병렬은 양립하지 않는다

놓치기 쉬운 제약입니다.

OpenAI 에서 strict 보장은
병렬 호출과 동시에 적용되지 않는다

   신뢰성(strict) 과 병렬 중 하나를 골라야 한다
   보통 신뢰성을 택한다

6.4 스키마를 너무 빡빡하게 잡으면

숫자 범위, 정규식, 유니코드 제약을 촘촘히 걸면
모델을 부자연스러운 토큰으로 몰아
내용 품질이 저하될 수 있다

   형식은 강제하되 값은 느슨하게

7장. 오류를 나눠 보기

7.1 다섯 단계로 분리

네이티브는 모델과 플랫폼이 얽혀 있어, 오류가 나면 원인을 나눠 봐야 합니다.

① 잘못된 도구 선택     모델 능력 문제
② 인자 형식 오류       모델 능력 + 제약 디코딩으로 보완 가능
③ 런타임 파싱 실패     파서와 템플릿 문제
④ 도구 실행 실패       내 코드나 외부 API 문제
⑤ 결과 해석 오류       모델 능력 문제

평가도 이 단계로 분리하는 게 맞습니다. "도구 호출 정확도 몇 %"라는 단일 숫자는 어디를 고쳐야 하는지 알려주지 않습니다.

7.2 자주 하는 실수 넷

① strict 와 병렬을 동시에 기대
   OpenAI 에선 양립하지 않는다. 하나를 고른다

② 소형 모델 네이티브 출력만 믿고 validator 생략
   포맷이 흔들린다. 검증과 정규화를 반드시 붙인다

③ tool_choice 미설정
   불필요한 호출이나 누락이 생긴다
   auto / required / 특정 도구를 상황에 맞게 지정한다

④ 제공사 간 포맷을 그대로 이식
   이름과 중첩과 결과 반환 방식이 다르다
   특히 Anthropic 의 content 블록 모델

8장. 다른 갈래와 층위

8.1 코드로 행동하기

네이티브
   JSON 한 덩어리로 한 함수를 부른다

CodeAct
   모델이 아예 실행 가능한 파이썬 코드를 써서
   여러 도구를 묶어 부른다

논문은 이 방식이 제어 흐름과 변수와 도구 조합에서 더 유연해, 17개 LLM에서 기존 방식 대비 최대 20% 높은 성공률을 냈다고 보고합니다.

둘은 대립이 아니라 스펙트럼의 양 끝입니다.

한 번에 한 호출   ←────────────→   코드로 여러 호출을 오케스트레이션

최근 상용 API의 programmatic tool calling도 이 접점을 넓히고 있습니다.

8.2 MCP와의 층위 차이

자주 혼동되는 지점입니다.

네이티브 도구 호출   "어떻게 부르나"
MCP                 "도구를 어디서 가져오나"

   서로 대체가 아니라 층위가 다르다
   MCP 로 가져온 도구를 네이티브 호출로 부른다

8.3 온디바이스에서의 선택

4B 급 소형 모델
   네이티브 tool_calls 를 신뢰하되 런타임 보강이 정답이다
   스키마 validator, 인자 정규화, 위험 게이트를 덧댄다

더 나아간 설계
   네이티브를 본류가 아니라 폴백으로만 둔다

   플래너가 텍스트 JSON 계획으로 도구와 인자를 한 콜에 확정해
   결정적으로 실행하고
   인자를 미리 못 채우는 스텝에서만
   네이티브 tool_choice: auto 를 발동한다

LLM 재호출 병목을 줄이려는 선택이고, 8.1의 CodeAct나 계획 기반 접근과 닿아 있습니다.


9장. 발전사와 정리

9.1 계보

시기 발전 비고
2022에서 2023 프롬프트 기반 도구 사용(ReAct), 자가학습 도구화(Toolformer) 자유 텍스트 파싱
2023 OpenAI function calling 발표 네이티브의 시작
2023에서 2024 Anthropic tool_use, Gemini function calling, 오픈 모델 chat template 도구 토큰 제공사 확산
2024 Structured Outputs strict, vLLM 다중 파서, CodeAct 신뢰성과 self-host와 코드 액션
2025 이후 병렬, 스트리밍, hosted 도구, MCP로 도구 연결 표준화 에이전트 본격화

9.2 핵심 셋

① 네이티브 도구 호출은 함수 호출을
   자유 텍스트에서 떼어내 구조화 필드로 만드는 것이다

② 기본 단위는 왕복이다
   스키마 전달 → 구조화 호출 → 실제 실행 → 결과 반환
   모델 호출 2번 사이에 우리 코드의 실행 1번이 낀다

③ 네이티브라고 인자 JSON 이 항상 맞진 않는다
   보장하려면 strict 나 제약 디코딩이 따로 필요하고
   이건 병렬 호출과 상충한다

9.3 남는 질문

  • 소형 온디바이스 모델에서 네이티브 출력의 실패율은 얼마나 되나. validator와 제약 디코딩으로 어디까지 메울 수 있는지 정량 비교가 필요합니다.
  • 오픈 모델의 포맷 표준이 수렴할까. 지금은 계열마다 다르고 같은 회사 안에서도 버전마다 다릅니다.
  • strict와 병렬을 함께 쓸 수 있게 될까. 지금은 택일인데, 병렬이 필요한 워크로드에서는 아쉬운 제약입니다.

9.4 용어 정리

용어 한 줄 뜻
네이티브 도구 호출 모델 훈련과 API 1급 구조로 도구 호출을 처리하는 방식
프롬프트 기반 도구 호출 프롬프트로 도구를 설명하고 자유 텍스트에서 호출을 파싱하는 방식
tool_calls / tool_use / functionCall OpenAI / Anthropic / Gemini의 구조화 호출 표현
tool 역할 / tool_result / functionResponse 도구 실행 결과를 모델에 되돌리는 메시지와 블록
tool_choice 도구 호출을 자동, 강제, 금지, 특정 지정하는 제어
parallel tool calls 한 턴에 여러 도구를 동시에 호출하는 것
chat template 역할과 도구를 모델 학습 토큰열로 직렬화하는 Jinja2 템플릿
tool-call parser 모델 출력에서 호출을 구조로 추출하는 런타임 컴포넌트
제약 디코딩 문법이나 JSON Schema로 스키마에 맞는 토큰만 생성하는 것
Structured Outputs / strict 인자가 스키마를 보장하게 하는 기능
CodeAct 액션을 JSON이 아니라 실행 가능한 코드로 표현하는 방식
BFCL 함수호출 능력을 재는 대표 리더보드

9.5 참고자료

'AI Agent > Tool-Use (Tool Calling)' 카테고리의 다른 글

MCP 보안  (0) 2026.02.13
MCP (Model Context Protocol)  (0) 2026.01.29

댓글