상태: 초안 (2026-10-27), 실측. 측정은 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.31.0, Qwen3-Reranker-0.6B(재순위용으로 미세 조정된 Qwen3 0.6B 인과 언어 모델)로 했습니다. 공식 문서는 0.32.0(2026-10-18 출시) 기준으로 2026-10-27 에 열어 확인했고, 0.31.0 문서와 차이가 나는 곳은 따로 적었습니다. 2026-10-27 추가. 대화용 Qwen3-0.6B, Qwen3-0.6B-GPTQ-Int8, Qwen3-4B-AWQ 와 vLLM 0.32.0 으로 다시 잰 결과를 3-1절에 더했습니다. 0.31.0 으로 잰 앞의 수치는 그대로 두었습니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. 도구 호출은 정해진 모양의 글이다
- 2. tool_choice 네 가지
- 3. 직접 재 보기
- 3-1. 2026-10-27 재측정: 도구를 아는 모델과 0.32.0
- 4. 운영에서 가져갈 것
- 5. 확인하지 못한 것
- 참고
1. 도구 호출은 정해진 모양의 글이다
에이전트가 날씨 API 를 부르는 장면을 떠올려 봅시다. 모델 안에 「함수 호출」이라는 특별한 출력 장치가 있는 것은 아닙니다. 모델은 여전히 토큰을 하나씩 만들고, 다만 학습 때 「도구를 부를 때는 이런 모양으로 쓴다」를 배웠을 뿐입니다. Qwen 계열은 Hermes 형식을 씁니다.
<tool_call>
{"name": "get_weather", "arguments": {"city": "Seoul", "unit": "c"}}
</tool_call>
서버가 하는 일은 세 가지입니다.
- 도구 목록을 프롬프트에 넣습니다. 요청의
tools를 모델의 채팅 템플릿이 시스템 프롬프트 안에 글로 적어 넣습니다. - 모델이 쓴 글을 고릅니다. 생성된 글에서 위 모양을 찾아 OpenAI 형식의
message.tool_calls로 옮기는 것이 도구 파서입니다. 모델마다 모양이 달라서 파서도hermes,llama3_json,mistral,qwen3_xml처럼 여럿입니다. - 필요하면 모양을 강제합니다.
tool_choice가required이거나 특정 함수 이름이면, 4편의 구조화 출력이 호출의 겉모양을 토큰 단계에서 강제합니다.
켜는 옵션은 다음과 같습니다. --enable-auto-tool-choice 가 없으면 tool_choice="auto" 요청을 받지 않습니다.
vllm serve <모델> --enable-auto-tool-choice --tool-call-parser hermes
2. tool_choice 네 가지
| tool_choice | 호출 의무 | 무엇이 모양을 보장하나 |
|---|---|---|
auto |
없음, 모델이 정함 | 파서뿐. 모델이 모양을 틀리면 글로 남거나 빠짐 |
required |
하나 이상 반드시 | 구조화 출력이 겉모양을 강제 |
이름 지정({"type":"function","function":{"name":...}}) |
그 함수 하나 | 구조화 출력이 겉모양을 강제 |
none |
부르지 않음 | 도구 정의는 기본으로 프롬프트에 남음(--exclude-tools-when-tool-choice-none 으로 뺌) |
공식 문서는 이름 지정 호출에 대해 「파싱 가능한 호출은 보장하지만 질 좋은 호출은 보장하지 않는다」고 적습니다. 이 문장이 이 글의 실측 결과를 그대로 요약합니다.
겉모양과 인자는 따로 묶인다
0.21.0 부터 xgrammar 의 구조 태그(structural tag) 로 도구 호출을 강제하는 길이 들어왔고, 지금 문서는 강제를 두 겹으로 나눕니다.
- 겉봉투(envelope):
<tool_call>같은 표식과 함수 이름.required와 이름 지정에서는 항상 강제합니다. - 인자 스키마: 그 도구에
"strict": true를 달았거나 서버를--tool-strict-level parameter로 띄웠을 때만 강제합니다. 그렇지 않으면 인자는 「JSON 처럼 보이는 것」 정도로만 묶입니다.
서버 쪽 바닥값 --tool-strict-level 은 auto(기본, 요청을 따름), function(모든 요청에서 겉봉투 강제), parameter(모든 도구를 strict 처럼) 셋입니다. 대부분의 에이전트 프레임워크가 strict 를 달지 않으므로, 운영자가 바닥을 올릴 수 있게 만든 것입니다. 환경 변수 VLLM_ENFORCE_STRICT_TOOL_CALLING=false 는 구조 태그를 통째로 끕니다.
3. 직접 재 보기
환경과 방법
- GPU: 8GB급 노트북 GPU(Ada 세대) 한 장, 드라이버 595.99, CUDA 13.0 빌드
- vLLM 0.31.0 공식 컨테이너, PyTorch 2.13.0, xgrammar 0.2.7, llguidance 1.7.6
- 모델: Qwen3-Reranker-0.6B(bf16). 재순위용으로 미세 조정된 모델이라 대화 능력이 거의 없습니다. 채팅으로 물으면 대개 「No.」라고 답합니다. 「도구를 잘 쓰는 모델」이 아니라 「도구를 쓸 줄 모르는 작은 모델」을 서버가 어디까지 붙잡아 주는가를 보는 시험으로 읽어 주십시오.
- 서버:
--enable-auto-tool-choice --tool-call-parser hermes, 나머지는 기본값 - 요청: 「What is the weather in Seoul right now?」, 도구 하나
get_weather(city: string, unit: "c"|"f"), 두 필드 모두 필수,temperature 0.7,max_tokens 200, 시드 0~4 로 다섯 번. 같은 요청을 strict 없이, 그리고strict: true와additionalProperties: false를 달아 보냈습니다. - 「스키마 지킴」은 클라이언트가
arguments를 JSON 으로 읽어city가 문자열이고unit이 c 나 f 인지 확인한 결과입니다.
결과
| tool_choice | strict | tool_calls 가 나온 횟수 | 스키마를 지킨 인자 | 응답 시간(중앙) |
|---|---|---|---|---|
| auto | 없음 | 0/5 | 0/5 | 24.5ms |
| auto | 있음 | 0/5 | 0/5 | 24.6ms |
| required | 없음 | 4/5 | 0/5 | 216.6ms |
| 이름 지정 | 없음 | 4/5 | 0/5 | 216.2ms |
| required | 있음 | 5/5 | 5/5 | 263.1ms |
| 이름 지정 | 있음 | 5/5 | 5/5 | 261.7ms |
원자료의 응답 몇 개를 그대로 옮깁니다.
auto: 다섯 번 모두 본문이 「No.」였고 도구를 부르지 않았습니다.auto에서는 모델이 부르지 않기로 하면 그만입니다.required, strict 없음: 첫 요청은<tool_call>안에"location": "1801, 401, 181, 182, …"처럼 스키마에 없는 필드와 숫자 나열을 200토큰 끝까지 쓰다가 잘려 파서가 tool_calls 로 옮기지 못했습니다(1,180ms). 나머지 넷은{}세 번,{"city": "Hawaii"}한 번이었습니다. 함수 이름은 맞았지만 필수 필드unit은 한 번도 없었습니다.required, strict 있음:{"city": "Baltic", "unit": "c"},{"city": "Berlin", "unit": "c"},{"city": "Hawaii", "unit": "f"},{"city": "none", "unit": "f"},{"city": "New York", "unit": "f"}.
같은 시드에서 required 와 이름 지정의 출력은 글자까지 같았습니다. 도구가 하나뿐이면 두 방식이 같은 문법을 만들기 때문입니다.
서버도 이것을 셉니다. 시험이 끝난 뒤 /metrics 의 vllm:tool_call_parser_invocations_total 은 비스트리밍 채팅 요청에서 outcome="tool_call" 18, outcome="no_tool_call" 12 였습니다. 클라이언트가 센 값(tool_calls 가 나온 응답 18개, 나오지 않은 응답 12개)과 같습니다.
읽는 법
- strict 없는 required 는 「호출이 있다」만 보장했습니다. 함수 이름까지는 맞았지만 인자는 빈 객체이거나 필수 필드가 빠졌습니다. 0.31.0 과 0.32.0 문서 모두 「strict 가 없는 도구는 required 와 이름 지정에서도 넓은 인자 문법만 받는다」고 적고 있고, 실측이 그 문장과 맞았습니다.
- strict 를 켜면 스키마는 지켰지만 뜻은 맞지 않았습니다. 모든 인자가 JSON 으로 읽히고 필수 필드와 열거값을 지켰지만, 질문의 Seoul 은 한 번도 나오지 않았고
"none"같은 값도 나왔습니다. 문법은 「허용되는 토큰」만 남길 뿐, 남은 토큰 중 무엇을 고를지는 여전히 모델이 정합니다. - 시간 차이는 대부분 토큰 수입니다. strict 쪽이 50ms 가량 길었지만
{}대신 두 필드를 다 쓰느라 토큰이 늘어난 몫이 섞여 있어, 문법 검사 자체의 비용으로 읽으면 안 됩니다. 문법 비용만 따로 본 값은 4편에 있습니다.
3-1. 2026-10-27 재측정: 도구를 아는 모델과 0.32.0
앞의 시험은 도구를 모르는 모델로 「서버가 붙잡아 주는 범위」를 봤습니다. 이번에는 도구 호출을 학습한 모델로 내용까지 봤습니다.
- 환경은 같고 vLLM 만 0.32.0 공식 컨테이너로 바꿨습니다. 모델은 대화용 Qwen3-0.6B(bf16), 같은 모델의 GPTQ 8비트판, Qwen3-4B 의 AWQ 4비트판입니다. 사고 과정은
chat_template_kwargs: {"enable_thinking": false}로 껐습니다. - 요청: 「What is the weather in {도시} right now? Answer in celsius.」 도시는 Seoul, Paris, Tokyo, Cairo, Lima, Oslo 여섯 곳. 「맞음」은 인자의 city 에 그 도시가 들어 있고 unit 이 c 인 경우입니다.
| 모델 | tool_choice | strict | 호출이 나온 횟수 | 맞는 도시·단위 | 응답 시간 중앙 |
|---|---|---|---|---|---|
| Qwen3-0.6B | auto | 없음 | 6/6 | 6/6 | 162.1ms |
| Qwen3-0.6B | required | 없음 | 6/6 | 6/6 | 178.0ms |
| Qwen3-0.6B | 이름 지정 | 없음 | 6/6 | 6/6 | 178.1ms |
| Qwen3-0.6B | auto·required·이름 지정 | 있음 | 18/18 | 18/18 | 184.1~185.4ms |
| Qwen3-0.6B GPTQ 8비트 | 여섯 경우 모두 | 없음·있음 | 36/36 | 36/36 | 115.7~140.6ms |
| Qwen3-4B AWQ 4비트 | 여섯 경우 모두 | 없음·있음 | 36/36 | 36/36 | 338.7~362.4ms |
- 도구를 아는 모델은 auto 에서도 매번 불렀고, strict 없이도 인자가 맞았습니다. 재순위 모델이 strict 를 켜야 겨우 스키마를 지키고 그래도 도시를 틀렸던 것과 대조됩니다.
- 0.6B 에서 strict 를 켜면 required·이름 지정은 약 7ms, auto 는 22ms 길어졌습니다. auto 는 strict 가 없으면 문법을 전혀 쓰지 않다가 켜지는 것이라 차이가 더 큽니다. 만든 인자 글은 strict 유무와 관계없이 같았으므로, 이 차이는 문법 마스크의 비용으로 읽을 수 있습니다.
- 같은 0.32.0 컨테이너에서 재순위 모델로 앞의 시험을 그대로 다시 돌린 결과는 0.31.0 과 응답 글자까지 같았습니다(strict 없는 required 의 스키마 준수 0/5, strict 5/5, 도시는 Baltic·Berlin·Hawaii·none·New York).
이 시험은 도구 하나, 짧은 질문 하나라 쉬운 편입니다. 도구가 여럿이거나 인자가 복잡할 때의 정확도는 여전히 미실측입니다.
4. 운영에서 가져갈 것
- 에이전트 서버라면 바닥을 올립니다. 클라이언트가 strict 를 달아 주기를 기대하기보다
--tool-strict-level parameter로 띄우면 모든 도구의 인자가 스키마를 지킵니다. 반대로 스키마가 깊거나 큰 도구가 많으면 문법을 만드는 비용(4편의 첫 요청 지연)이 생기므로, 처음 들어오는 요청의 지연을 함께 봅니다. - 인자는 언제나 신뢰할 수 없는 입력입니다. 스키마를 지킨 인자도
"none"같은 엉뚱한 값을 담았습니다. 도구를 실행하는 쪽에서 값의 범위와 권한을 다시 검사해야 하고, 파일 경로·SQL·셸 명령처럼 위험한 인자는 허용 목록으로만 받아야 합니다. 프롬프트에 들어온 외부 글(웹 페이지, 문서)이 모델을 부추겨 도구를 부르게 만드는 프롬프트 주입도 같은 이유로 막을 수 없다고 보고 설계합니다. - auto 가 조용히 실패하는 것을 지표로 봅니다.
auto에서 모델이 모양을 틀리면 오류가 아니라 평범한 본문으로 돌아옵니다. 위의vllm:tool_call_parser_invocations_total에서no_tool_call의 비율을 대시보드에 두면 모델이나 파서나 채팅 템플릿이 바뀌었을 때 바로 보입니다. 다만 이 지표는 「도구가 필요했는데 안 불렀다」와 「도구가 필요 없었다」를 가르지 못하므로, 애플리케이션 쪽의 기대와 함께 봅니다. - 파서는 모델과 짝입니다. Qwen2.5·Qwen3 는
hermes, Qwen3-Coder 는qwen3_xml, Llama 3.1 은llama3_json처럼 모델 문서가 정한 파서를 씁니다. 0.32.0 에는 체크포인트의response_template을 읽어 모델별 파서 없이 해석하는hf파서가 들어왔지만, 이 파서는 strict·required·이름 지정을 받지 않는다고 문서에 적혀 있습니다(미실측).
5. 확인하지 못한 것
- 도구 호출을 학습한 모델의 정확도는 3-1절에서 도구 하나·짧은 질문으로만 쟀습니다. 도구가 여럿이거나 대화가 길 때는 미실측입니다.
- 여러 도구를 한 번에 부르는 병렬 호출, 스트리밍에서의 tool_calls 조각,
--tool-strict-level서버 옵션은 미실측입니다. - 0.32.0 의
hf파서와parallel_tool_calls=false강제는 미실측입니다.
참고
- vLLM 문서, Tool Calling(0.32.0, 0.31.0): https://docs.vllm.ai/en/latest/features/tool_calling.html
- vLLM 0.21.0 릴리스 노트(구조 태그로 strict 도구 호출): https://github.com/vllm-project/vllm/releases/tag/v0.21.0
- Qwen 문서, Function Calling 과 vLLM: https://qwen.readthedocs.io/en/latest/framework/function_call.html
- 이 시리즈 4편 구조화 출력
- 원자료: 이 시리즈 폴더의 README 「원자료」 절