Function Calling이 LLM에 제공하는 것은 타입 안전성이지 정확성이 아니다
요약
Function Calling과 구조화된 출력은 LLM 응답을 파싱 가능한 객체로 만들어 개발 편의성을 높이지만, 이는 값의 '정확성'을 보장하지 못합니다. 스키마 유효성 검사는 구문적(syntax) 정확성만 확인할 뿐이며, 의미적 오류를 놓칠 수 있습니다. 특히 문법 제약 디코딩은 모델의 추론 과정을 방해하여 잠재적인 문제점을 야기할 수 있으므로, 구조화된 출력을 인터페이스 계약으로 다루고 자체적인 의미적 테스트가 필요합니다.
핵심 포인트
- 구조화된 출력은 파싱 용이성을 높이나, 값의 진실성(정확성)을 보장하지 못한다.
- 스키마 유효성 검사는 구문 확인일 뿐이며, 의미적 오류를 잡아내지 못한다.
- 문법 제약 디코딩은 모델의 추론 과정을 강제로 제한하여 잠재적인 문제점을 만들 수 있다.
- 구조화된 출력을 안전망이 아닌, 자체 의미적 테스트가 포함된 인터페이스 계약으로 다뤄야 한다.
요약 (TL;DR) — 구조화된 출력과 function calling은 LLM의 응답이 파싱될 것이라고 보장할 뿐, 그 안에 담긴 값들이 진실인지에 대해서는 아무것도 말해주지 않습니다. 스키마 유효성 검사를 성공 지표로 여기는 팀들은 스키마를 준수하지만 의미적으로 잘못된 tool call을 배포하게 되며, 문법 제약 디코딩(grammar-constrained decoding)은 모델이 추론을 마치기도 전에 구조에 강제로 맞추도록 함으로써 이를 더욱 악화시킬 수 있습니다. 해결책은 스키마를 안전망으로 취급하는 것을 멈추고, 자체적인 의미적 테스트가 포함된 인터페이스 계약으로 다루기 시작하는 것입니다.
LLM 에이전트를 배포하는 모든 팀은 결국 같은 안도감을 발견합니다: 모델의 출력을 JSON 스키마나 function-calling spec으로 감싸면, 자유 형식 텍스트 생성의 혼란스러움이 깔끔하고 파싱 가능한 객체로 변한다는 것입니다. 더 이상 환각된 산문을 정규표현식(regex)으로 처리할 필요가 없습니다.
LLM 출력에 첨부된 JSON 스키마는 동일한 작업, 오직 그 작업만을 수행합니다. 이는 refund_amount가 숫자이고, customer_id가 문자열이며, status가 세 가지 열거형(enum) 값 중 하나인지를 확인합니다. 하지만 환불 금액이 올바른지, 고객 ID가 대화 속 실제 고객과 일치하는지, 또는 모델이 실제로 해결하지 않은 티켓에 "resolved"가 정확한 상태였는지 여부는 확인할 수 없습니다. 스키마 유효성 검사(Schema validation)는 정확성을 가장한 구문(syntax) 확인일 뿐입니다.
이것이 중요한 이유는 구조화된 출력(structured output)이 실패 모드(failure mode)를 변화시키지만, 이를 제거하지는 않기 때문입니다. 스키마가 없던 시절에는 오작동한 LLM 응답은 오작동해 보였습니다—잘못된 JSON(malformed JSON), 중괄호 앞에 뜬금없는 문장 등 사람이 로그를 훑어보면 즉시 알아챌 수 있는 것이었습니다. 하지만 스키마 이후의 오작동 응답은 올바른 응답과 구별할 수 없을 정도로 똑같아 보입니다. 이는 파싱됩니다. 유효성 검사를 통과합니다. 그리고 신뢰가 측정 대상이 아니었기 때문에, 결제 API나 데이터베이스 쓰기 작업으로 전송됩니다.
제약 디코딩(Constrained Decoding)은 추론 능력을 준수성(Compliance)과 교환한다
더 깊은 문제는 문법 제약 디코딩(grammar-constrained decoding)에서 나타납니다. 이는 대부분의 "보장된 JSON" 도구의 기반 메커니즘으로, 샘플러(sampler)가 모든 토큰 단계마다 출력을 유효한 스키마 준수 경로에 유지하는 토큰만 방출하도록 제한됩니다. 이것은 정말 영리한 엔지니어링 기술이며, 오작동 출력 자체를 오류 범주에서 제거합니다. 하지만 충분히 논의되지 않은 부작용이 있습니다: 모델이 그 구조물 안에 들어갈 내용을 추론을 완료하기 전에 구조물을 방출하도록 강제한다는 것입니다.
사고의 사슬(Chain-of-thought)은 작동할 때 작동하는 이유는 모델이 답변을 확정하기 전에 중간 토큰들을 펼쳐 놓을 수 있기 때문입니다. 도구 호출에 대한 문법 제약 디코딩(Grammar-constrained decoding)은 종종 그러한 여유 공간을 제거합니다. 첫 번째 토큰은 {여야 하고, 두 번째는 따옴표여야 하며, 키는 고정된 필드 이름 집합 중 하나와 일치해야 합니다. 만약 스키마가 의도적으로 추론 필드를 결정 필드보다 먼저 포함하도록 설계되지 않았다면, '여기서 실제로 어떤 도구가 적용되는지 생각해 볼 시간이' 남아있지 않습니다. 대부분의 스키마는 그렇게 설계되지 않는데, 기능 호출(function-calling) 사양을 작성하는 사람은 디코딩 역학(decoding dynamics)에 대해 생각하기보다는 다운스트림 시스템이 소비해야 할 내용에 대해서만 생각하기 때문입니다.
실질적인 결과는 이렇습니다. 자유 텍스트로 먼저 추론할 수 있도록 허용된다면 올바른 도구와 올바른 인수를 선택할 수 있는 모델이라도, 즉시 확정하도록 강요받으면 잘못된 도구를 선택할 수 있다는 것입니다. 당신은 눈에 보이는 실패(잘못된 형식의 출력)를 감수하고 보이지 않는 실패(완벽하게 유효한 형태 안에 담긴 자신감 있게 틀린 출력)와 맞바꾸었습니다. 아무도 그것이 발생했다는 것을 알아차리지 못한다면, 이것은 나쁜 거래입니다.
모델에게 '가짜'를 학습시키는 재시도 루프 (The Retry Loop That Teaches Models to Fake It)
대부분의 프로덕션 환경의 구조화된 출력 파이프라인에는 복구 루프(repair loop)가 있습니다. 유효성 검사에 실패하면, 오류가 모델에 다시 공급되고, 모델은 다시 시도합니다. 이것은 구문 오류(syntax errors)—닫는 괄호 누락이나 숫자가 아닌 문자열로 지정된 필드—에 대해서는 좋은 공학적 접근 방식입니다. 하지만 의미론적 오류(semantic errors)에 대해서는 훨씬 더 나쁩니다. 왜냐하면 재시도 루프에는 '모델이 유효하지 않은 JSON을 방출했는지'와 '모델이 잘못되었지만 유효한 JSON을 방출했는지'를 구별할 방법이 없고, 오직 전자에 대해서만 작동하기 때문입니다.
모델이 인자 값을 추측하고, 그것을 스키마 승인받아 전송하는 경우, 재시도(retry)를 유발하지 않습니다. 그 추측이 틀렸다는 것을 파이프라인에 알려주는 피드백 신호가 없기 때문입니다. 왜냐하면 '틀림' 자체가 검사되지 않기 때문입니다. 많은 요청을 처리하면서, '스키마 유효성 검사 통과율(schema validation pass rate)'을 신뢰도 지표로 모니터링하는 팀들은 이 수치가 거의 완벽에 가깝게 상승하는 것을 보지만, 실제로 신경 써야 할 부분 — 즉, 도구 호출이 올바른 일을 했는지 여부 — 은 정체되거나 저하되는 것을 목격합니다. 측정 지표와 목표가 조용히 분리되었고, 그 측정 지표는 측정하기 쉽고 목표는 측정하기 비싸기 때문에, 결국 지표가 대시보드를 장악하게 됩니다.
계약(Contract)이 실제로 속해야 할 곳
해결책은 더 엄격한 스키마가 아닙니다. 정규 표현식 패턴(regex patterns), 최소/최대 경계값(min/max bounds), 중첩된 열거형(nested enums)을 추가하여 스키마를 완벽하게 만들 수 있지만, 여전히 형태(shape)만을 제약할 뿐입니다. 해결책은 LLM과 시스템의 나머지 부분 사이에 존재하는 타입이 지정된 인터페이스가 서로 다른 주체가 소유한 두 서비스 간에 존재하는 타입이 지정된 인터페이스와 정확히 같다는 것을 인식하는 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기