본문으로 건너뛰기
18/3030개 중 18장

Tool Calling과 Structured Outputs: 깨지지 않는 계약

24번 호출, 깨진 JSON 0건, 쓸 수 있는 날짜 2개. 더 나은 설명을 붙인 같은 endpoint와 schema로도 고칠 수 없는 것.

이 페이지에서

모델에 항공편 검색 도구를 주고 Madrid에서 Berlin으로 가는 항공편을 찾아 달라고 요청해 보세요. 돌아오는 결과는 이렇습니다:

TEXT
<tool_call>
{"name": "search_flights",
 "arguments": {"from": "Madrid", "to": "Berlin", "date": "3rd October 2026"}}
</tool_call>

JSON은 유효합니다. 도구 이름도 맞습니다. 모든 필수 필드가 있습니다. 그런데 이 호출은 쓸모가 없습니다. 어떤 항공편 API도 공항 코드가 필요한 곳에 "Madrid"를, 날짜가 필요한 곳에 "3rd October 2026"를 받지 않습니다.

그 간극 — 구문적으로는 완벽하지만 의미적으로는 쓸 수 없는 상태 — 이 이 장의 주제입니다. 그리고 먼저 분명히 해야 할 것은 이것이 JSON 문제가 아니라는 점입니다. 이 도구로 24번 요청하는 동안 모델은 24개의 유효한 tool call과 깨진 JSON 0건을 만들었습니다. 모두가 디버깅하는 그 부분에서 단 한 번도 실패하지 않았습니다.

모델은 아무것도 실행하지 않습니다

섹션 링크: 모델은 아무것도 실행하지 않습니다

메커니즘에 들어가기 전에, 가장 많은 혼란을 막아 주는 문장부터 말하겠습니다. tool call은 요청이지 행동이 아닙니다.

모델은 이 인수로 search_flights를 호출하고 싶습니다라고 말하는 구조화된 메시지를 내보냅니다. 그리고 멈춥니다. 당신의 코드는 그 메시지를 받고, 그것을 승인할지 결정하고, 호출할 것을 호출한 뒤, 결과를 또 다른 메시지로 돌려보냅니다. 모델은 당신의 데이터베이스를 건드린 적도, HTTP 요청을 만든 적도, 자격 증명을 가진 적도 없습니다.

30장의 agent 보안에 관한 모든 것은 이 구분에서 나오며, 23장의 agent 설계에 관한 모든 것도 마찬가지입니다. 모델은 제안하고, 당신의 코드가 결정합니다. 그리고 모든 보장은 코드에 있습니다.

따라서 용어를 걷어낸 도구는 두 가지입니다:

스키마. 함수의 이름, 역할, 받는 인수와 그 타입 및 제약을 설명하는 JSON Schema입니다. 이것이 prompt에 들어가며, 모델이 실제로 보는 유일한 것입니다.

endpoint. 그 인수를 받아 무언가를 반환하는 당신 코드 안의 함수입니다. 모델은 이것을 보지 못하고, 어떤 언어로 되어 있는지도 모르며, 데이터베이스 쿼리와 하드코딩된 문자열을 구분할 수도 없습니다.

요청과 함께 스키마를 보냅니다

섹션 링크: 요청과 함께 스키마를 보냅니다

도구 정의는 prompt에 들어가며, 모델이 학습한 형식으로 직렬화됩니다. 이것들은 모든 호출마다 token 비용을 발생시킵니다. 이 사실은 이 장의 뒤에서 숫자로 다시 돌아옵니다.

모델은 텍스트 대신 호출로 답합니다

섹션 링크: 모델은 텍스트 대신 호출로 답합니다

응답에는 산문 대신 구조화된 요청이 들어 있고, API는 그 사실을 나타내는 종료 이유를 보고합니다. 이 이유가 중요합니다. 사용자에게 답을 보여 주는 대신 도구를 실행해야 한다는 것을 코드가 아는 방법이기 때문입니다.

당신의 코드가 실행합니다 — 또는 거부합니다

섹션 링크: 당신의 코드가 실행합니다 — 또는 거부합니다

이 단계에는 모델이 없습니다. 스키마에 맞춰 인수를 검증하고, 이 호출자가 이것을 할 수 있는지 결정한 뒤 실행합니다.

결과를 메시지로 다시 보냅니다

섹션 링크: 결과를 메시지로 다시 보냅니다

결과는 대화의 또 다른 턴이 되며, 이를 위해 예약된 역할에 담깁니다. 모델은 그것을 다른 context처럼 읽습니다.

모델이 답하거나, 다른 도구를 요청합니다

섹션 링크: 모델이 답하거나, 다른 도구를 요청합니다

이것이 23장의 루프이며, 하나의 요청이 열두 번의 왕복으로 바뀔 수 있는 이유입니다.

이 중 어떤 것도 emergent가 아닙니다. 11장에서 보았듯 tool calling은 학습된 행동입니다:1 post-training 동안 모델은 바로 이런 형태의 대화를 수천 개 보았습니다. 그래서 형식이 모델마다 다르고, 비슷한 크기의 모델 사이에서도 신뢰성이 크게 달라지며, 모델이 한 번도 본 적 없는 도구를 호출할 수 있습니다. 형태는 학습되었고, 구체적인 도구는 당신의 prompt에서 오기 때문입니다.

나쁜 스키마의 비용, 측정해 보기

섹션 링크: 나쁜 스키마의 비용, 측정해 보기

대부분의 사람이 처음 쓰는 도구는 이렇습니다. 여기서 틀린 것은 없습니다. 다만 얇을 뿐입니다:

tools/badFlights.tsTS
{
  name: "search_flights",
  description: "Search for flights.",
  parameters: {
    type: "object",
    properties: {
      from: { type: "string", description: "Airport." },   
      to:   { type: "string", description: "Airport." },   
      date: { type: "string", description: "The date." },  
    },
    required: ["from", "to", "date"],
  },
}

24번의 요청, 여섯 도시 쌍과 네 가지 날짜 표현 방식("the 3rd of next month", "next Friday", "15 December", "tomorrow")을 교차했고, 결과가 재현되도록 greedy decoding을 사용했습니다:

tool calledbroken JSONdate in ISOairports as IATAeverything correct
위 스키마24/2402/244/241/24

마지막 세 열을 보기 전에 처음 두 열을 읽어 보세요. 모델은 매번 올바른 도구를 호출하고, 매번 올바른 형식의 JSON을 만듭니다. 실패는 전적으로 에 있으며, 그 값들은 사용할 수 없습니다. MAD 대신 "Madrid", 2026-10-03 대신 "3rd October 2026"가 들어갑니다.

이 점을 강조할 가치가 있습니다. 무언가 깨졌을 때 어디를 봐야 하는지를 결정하기 때문입니다. 본능적으로 JSON 파서를 추가하고 재시도하거나, 모델에게 유효한 JSON을 더 강하게 요구하게 됩니다. 하지만 둘 다 여기서 실제로 일어난 일을 해결하지 못합니다.

이제 설명만 바꿔 봅니다

섹션 링크: 이제 설명만 바꿔 봅니다

같은 endpoint입니다. 뒤에 있는 코드도 같습니다. 같은 모델, 같은 prompts, 같은 decoding입니다. 바뀐 것은 스키마 안의 텍스트뿐입니다:

tools/goodFlights.tsTS
{
  name: "search_flights",
  description: "Search scheduled flights between two airports on a given day.",
  parameters: {
    type: "object",
    properties: {
      from: {
        type: "string",
        description: "Departure airport as a three-letter IATA code, e.g. MAD for Madrid. Never a city name.",   
        pattern: "^[A-Z]{3}$",
      },
      to: { /* same */ },
      date: {
        type: "string",
        description: "Departure date as an ISO 8601 calendar date, YYYY-MM-DD. Resolve relative dates against today before calling.",   
        format: "date",
        pattern: "^\\d{4}-\\d{2}-\\d{2}$",
      },
    },
    required: ["from", "to", "date"],
  },
}
date FORMATdate VALUEairport FORMATairport VALUE
얇은 스키마2/241/244/244/24
설명이 있는 스키마24/2412/2416/248/24

날짜 형식은 24개 중 2개에서 24개 중 24개로 갑니다. 코드도 건드리지 않고 재시도 로직도 없이, 텍스트 변경만으로 완벽해졌습니다. 이 장에서 운영 습관 하나만 가져간다면 바로 이것입니다. 도구가 잘못 호출될 때, 수정점은 거의 항상 설명에 있으며, 그것이 시스템에서 가장 저렴한 수정입니다.

이제 두 번째 열을 읽어 보세요. 더 중요한 절반입니다.

스키마는 형태를 제약합니다. 지식을 공급할 수는 없습니다.

섹션 링크: 스키마는 형태를 제약합니다. 지식을 공급할 수는 없습니다.

날짜는 24번 중 24번 ISO 형식입니다. 하지만 맞는 날인 것은 24번 중 12번입니다.

즉 이제 호출의 절반은 완벽하게 형식화된, 그러나 틀린 날짜를 담고 있습니다. 설명은 모델에게 어떤 형태를 만들지 알려 주었고, 모델은 그것을 흠잡을 데 없이 만들었습니다. 하지만 "next Friday"를 2026-09-11로 바꾸려면 오늘 날짜를 알고 달력 계산을 해야 하며, 설명을 아무리 많이 써도 그 지식을 공급할 수 없습니다. 공항도 마찬가지입니다. 형식은 4에서 16으로 올랐지만 값은 4에서 8로만 올랐습니다. MAD라고 쓰려면 Madrid의 공항이 MAD라는 사실을 알아야 하기 때문입니다.

그 구분이 이 장의 하중을 지탱하는 핵심 아이디어입니다:

스키마는 형태에 관한 계약입니다. 모델 출력이 파싱 가능하고, 타입이 있으며, 일관되게 만들 수 있습니다. 하지만 그것을 으로 만들 수는 없습니다. 좋은 스키마 뒤에도 남는 모든 실패 모드는 형식 실패가 아니라 지식 실패입니다.

둘은 서로 다른 수정이 필요하며, 이를 혼동하면 몇 주를 낭비합니다. 형식 실패는 설명이나 아래의 constrained decoding으로 고칩니다. 지식 실패는 지식을 prompt에 넣어서 고칩니다. 시스템 메시지에 현재 날짜를 넣거나, 모델이 먼저 호출하는 두 번째 도구로 공항 조회를 제공하거나, 집합이 열거할 만큼 작다면 스키마에 enum을 넣는 식입니다. 이 세 가지의 공통점에 주목하세요. 문제를 모델의 기억에서 꺼내 입력으로 옮긴다는 점입니다. 이것이 24장 전체의 내용입니다.

Structured outputs, 그리고 "constrained decoding"이 실제로 의미하는 것

섹션 링크: Structured outputs, 그리고 "constrained decoding"이 실제로 의미하는 것

위의 모든 것은 여전히 모델이 올바른 형태를 선택한다는 데 의존합니다. 더 강한 보장이 가능하며, 이것이 17장에서 얻는 가장 큰 보상입니다.

생성이 어떻게 작동하는지 떠올려 봅시다. 매 단계마다 모델은 어휘의 모든 token에 대해 logit을 만들고, sampler가 하나를 고릅니다. Constrained decoding은 그 사이에 한 단계를 끼워 넣습니다. 당신의 JSON Schema에서 파생된 grammar가 주어지면, 다음에 합법적으로 올 수 있는 token들을 계산하고, 나머지 모든 token의 logits를 음의 무한대로 설정한 뒤, sampler가 남은 것 중에서 선택하게 합니다.

스키마가 다음 항목이 반드시 {여야 한다고 말하면, {가 아닌 모든 token의 확률은 0입니다. "낮다"가 아니라 0입니다. 잘못된 token이 sampling 전에 분포에서 제거되었기 때문에 모델은 유효하지 않은 JSON을 내보낼 수 없습니다.

이것이 "structured outputs", "JSON mode", "guided generation"의 내부에서 일어나는 일이며, 그 두 가지 속성을 설명합니다. grammar가 표현할 수 있는 것 — 타입, 필수 필드, enum, 중첩 — 에 대해서는 보장이 완전합니다. 정중하게 요청하는 것이 아니라 기계적으로 강제되기 때문입니다. 그리고 이것은 내용에 대해서는 아무 말도 하지 않습니다. grammar는 "date"가 날짜 패턴에 맞는 문자열이 되도록 강제할 수 있지만, 그것이 맞는 날이 되도록 강제할 수는 없습니다. 앞 절과 같은 벽에 반대편에서 도달한 것입니다.

실무적인 메모 두 가지가 있습니다. 공짜가 아닙니다. 매 단계마다 마스크를 계산해야 하고, 복잡한 grammar는 측정 가능한 지연 시간을 유발합니다. 그리고 모델이 하는 일을 바꿉니다. 선호하는 token에서 밀려난 모델은 완벽한 구조를 만들면서도 더 나쁜 내용을 만들 수 있습니다. 그래서 단순한 형태에는 "잘 요청하고 검증하기"가 여전히 합리적인 기본값이며, constrained decoding은 형태가 복잡하거나 소비자가 엄격할 때 비용을 정당화합니다.

부작용, 그리고 중요한 단 하나의 속성

섹션 링크: 부작용, 그리고 중요한 단 하나의 속성

14장은 timeout 뒤 재시도가 이어져 답 하나에 두 번의 생성 비용이 청구되는 상황을 측정했습니다. 도구에서는 같은 실패가 더 나빠집니다. 도구는 실제로 무언가를 수 있기 때문입니다.

당신의 코드가 charge_card를 호출하고, timeout이 나고, 재시도하면 두 번 청구됩니다. 모델은 이 모든 일이 일어났다는 사실을 전혀 모릅니다. 모델은 도구 결과 하나만 봅니다. 수정은 어떤 분산 시스템에서나 같은 것이며 모델의 문제가 아닙니다. 호출에 키를 부여해 작업을 idempotent하게 만들고, 두 번째 실행이 첫 번째 실행을 인식해 일을 다시 하는 대신 그 결과를 반환하게 해야 합니다.

여기서 나오는 설계 규칙은 분명하게 말할 가치가 있습니다. 도구 카탈로그에서 읽기와 쓰기를 분리하세요. 읽기는 자유롭게 재시도하고, 병렬로 실행하고, 캐시할 수 있습니다. 쓰기는 그럴 수 없으며, 키와 권한 검사, 그리고 — 사용자가 발생 전에 알고 싶어 할 만한 모든 것에 대해 — 요청과 행동 사이에 사람을 두는 승인 단계를 가져야 합니다. 그 승인 단계는 예의가 아닙니다. prompt injection과 실제 결과 사이를 가로막는 몇 안 되는 장치 중 하나이며, 30장이 측정하듯 그중 가장 약한 장치이기도 합니다.

도구가 몇 개가 되면 성능이 떨어질까요?

섹션 링크: 도구가 몇 개가 되면 성능이 떨어질까요?

속설은 많은 도구를 로드하면 모델이 잘못 선택한다고 말합니다. 반복하기보다 측정할 가치가 있습니다. 그래서 같은 24개 요청에 항공편 도구와 점점 늘어나는 다른 도구들을 함께 넣었습니다. 일부러 헷갈리게 만든 세 가지(기차 시간표, 페리 노선, 버스 노선)도 포함했습니다.

tools loadedprompt tokenschose search_flightsdate in ISO
135324/2424/24
573024/2424/24
101,19321/2421/24
202,11924/2424/24

선택은 악화되지 않았습니다. 도구가 20개이고 그중 3개가 그럴듯하게 헷갈릴 수 있었지만, 5억 parameter 모델은 24번 중 24번 올바른 것을 골랐습니다. 10개에서 보이는 하락은 다른 도구 이름을 댄 세 번의 호출이며, 20개로 가면 사라집니다.

이것은 부정 결과이며, 그렇게 보고해야 합니다. 이 작업에서, 이 도구들로는, "도구가 너무 많다"가 문제가 아니었습니다. 단조롭게, 그리고 6배로 커진 것은 prompt입니다. 353 tokens에서 2,119 tokens로 늘었고, 대화의 모든 요청에서, 영원히, 도구 사용 여부와 무관하게 비용으로 지불됩니다.

따라서 속설의 정직한 버전은 정확도가 아니라 비용과 context에 관한 것입니다. 도구 20개는 모든 메시지에 붙는 영구 세금이며, 16장은 이미 영구 prefix가 40턴에 걸쳐 청구서에 무엇을 하는지 보여 주었습니다. 사람들이 많은 도구가 품질을 해친다고 보고할 때, 메커니즘은 대개 정의들이 중요한 context를 밀어냈다는 것입니다. 이는 18장의 옷을 입은 24장 문제입니다. 정말로 서로 거의 중복되는 도구들은 실제 문제이기도 하며, 그 해결책은 도구를 줄이는 것이 아니라 더 나은 설명과 namespace입니다. 시스템별로 prefix를 붙여(crm.search_customer, billing.search_customer) 두 팀에서 합쳐진 두 카탈로그가 충돌하지 않게 하고, 모델이 구별할 수 있는 단서를 갖게 하세요.

세 종류의 도구, 그리고 다음 부분을 여는 하나

섹션 링크: 세 종류의 도구, 그리고 다음 부분을 여는 하나

도구가 세상에 무엇을 하는지에 따라 나누면 도움이 됩니다. 각 경우에 필요한 엔지니어링이 다르기 때문입니다.

데이터 도구는 읽습니다. 검색, 가져오기, 쿼리입니다. 재시도 가능하고, 병렬화 가능하며, 캐시 가능합니다. 유용한 것을 반환하지 못하는 방식으로 실패하며, 주된 위험은 신뢰할 수 없는 텍스트를 context로 가져온다는 점입니다. 이것이 30장 전체의 공격 표면입니다.

행동 도구는 씁니다. 전송, 생성, 청구, 삭제입니다. 키 없이는 재시도할 수 없고, 안전하게 병렬화할 수 없으며, 승인 흐름이 존재하는 이유입니다.

오케스트레이션 도구는 다른 모델을 호출합니다. 구현이 또 다른 agent인 도구입니다. 자신만의 prompt, 자신만의 도구, 자신만의 루프를 가지고 있습니다. 그리고 호출하는 모델에게는 앞의 두 종류와 정확히 똑같이 보입니다. 모델이 보는 것은 언제나 스키마와 endpoint뿐이기 때문입니다.

그 세 번째 종류는 단순한 호기심거리가 아닙니다. 25장의 agent-as-a-tool 절반을 뒷받침하는 메커니즘입니다. 다른 topology인 handoff는 대화를 넘겨주고 다시 가져오지 않습니다. 그리고 이것은 이 장의 인터페이스가 충분히 좁아서 전체 agent 하나가 그 뒤에 들어갈 수 있기 때문에 작동합니다.

이제 당신에게는 무언가를 요청할 수 있는 모델과, 그 요청을 파싱 가능하게 만드는 계약이 있습니다. 아직 없는 것은 prompt에 들어가는 것 너머로 모델이 요청할 대상입니다.

프로덕션에서 가장 흔한 도구는 압도적으로, 모델이 학습 중 보지 못한 텍스트 뭉치에 대한 검색입니다. 당신의 문서, 티켓, 계약서 같은 것들입니다. 이것은 해결된 문제처럼 들립니다. embedding하고, 가장 가까운 이웃을 찾고, 붙여 넣으면 됩니다. 하지만 해결되지 않은 부분들이 답의 신뢰성을 결정합니다. embedding 전에 텍스트를 어떻게 자를지, 어떤 유사도 threshold가 모릅니다를 의미할 만큼 낮은지, 독자가 확인할 수 있도록 인용을 주장에 어떻게 붙일지입니다.

19장은 retrieval입니다. 그리고 틀린 답이 호기심거리를 멈추고 책임이 되기 시작하는 장입니다.


이 장의 측정값은 Qwen/Qwen2.5-0.5B-Instruct에서 greedy decoding으로 얻었습니다. 여섯 도시 쌍과 네 가지 날짜 표현을 교차한 24개의 생성 요청을 대상으로 했고, 도구 정의에는 모델 자체의 chat template를 사용했습니다. 결과는 정확히 재현되며, 작은 모델입니다. 따라서 형식/값 분리를 현재 모델이 무엇을 하는지에 대한 benchmark가 아니라 메커니즘의 시연으로 읽어야 합니다. frontier 모델은 "next Friday"를 훨씬 더 자주 올바르게 해결합니다. 그래도 스키마가 그것을 하게 만들 수는 없으며, 일반화되는 부분은 바로 그것입니다.

위에서 사용한 JSON Schema vocabulary(type, properties, required, pattern, format, enum)는 provider 문서가 지명하는 JSON Schema draft에 규정되어 있습니다. 유용한 부분집합은 작고 provider 전반에서 같습니다. 실제로 존재하는 차이 — 어떤 keywords가 단지 모델에 전달되는 것이 아니라 constrained decoding으로 강제되는지 — 는 가정하기보다 provider의 structured-output 가이드에서 읽어 볼 가치가 있습니다.

기법으로서 constrained decoding에 대해서는 guidance 스타일 라이브러리들과 outlines 프로젝트가 grammar-to-logit-mask 구성을 17장의 sampler에 직접 대응되는 방식으로 문서화합니다. 그리고 왕복 자체에 대해서는 가장 명확한 명세가 튜토리얼이 아니라 프로토콜입니다. 26장은 그것을 한 줄씩 읽습니다.

  1. Ouyang, L. et al. Training language models to follow instructions with human feedback. arXiv:2203.02155 (2022). post-training 레시피를 표준으로 만든 논문입니다. tool call의 형태는 답변의 형태와 똑같이, 시연을 통해 거기서 학습됩니다.


제작자

David Vicente Campos

NeuraLIA Labs 창립자 & MyRealFood 공동 창립자

저는 레온 대학교 출신의 컴퓨터 엔지니어입니다. MyRealFood를 공동 창업해 수백만 명이 더 건강하게 먹기 위해 사용해 온 앱을 CTO로서 만들었고, NeuraLIA Labs를 설립해 그곳에서 AI 제품을 만들고 있습니다. 여기서는 제가 그 과정에서 이해해야 했던 것들에 대해, 누군가 제게 설명해줬으면 했던 방식으로 쓰고 있습니다.

저자 더 알아보기

NeuraLIA Labs에서 발행합니다.

새 글을 받은편지함에서 받아보세요

AI 뉴스, 가이드, 제품 업데이트 — 읽을 만한 소식이 있을 때 짧은 이메일로 보내드려요.

메시지가 편하다면 같은 글을 여기서도 받아보세요:WhatsApp 커뮤니티 (새 탭에서 열림)Telegram 채널 (새 탭에서 열림)

강좌 목차

Abstract software decision engine with branching paths, probability nodes, and glowing gates.
jev13분 읽기

Jev AI 모델은 글쓰기가 아니라 결정을 위해 만들어졌다

TypeSafe AI의 Jev가 주목받는 이유는 소프트웨어 지능을 확률 문제로 다루기 때문입니다. 올바른 분기를 선택하고, 신뢰도를 붙이며, 코드에 필요한 것이 결정일 때 LLM에 텍스트 작성을 맡기는 비용을 피합니다.

이제 모델 선택은 LIA에게 맡기세요

모든 AI 모델을 한곳에서. 오늘 무료로 시작하세요.