결정 모델(Jev)의 쓸모는 정답률보다 모를 때 물러서는 방식에 있다
글을 쓰지 않고 판단만 돌려주는 모델이 나왔습니다. TypeSafe의 jev입니다.
에이전트 안의 LLM 호출 중에서 이런 모델로 옮겨 앉힐 수 있는 자리가 어디인지 훑어보고, 가장 전형적인 분류 과제 하나에서 추론형 LLM과 나란히 돌려 봤습니다.정답률은 LLM에 조금 못 미쳤습니다.
대신 모를 때 물러서는 모양이 좋았고, 같은 입력에는 같은 답을 냈습니다.
그래서 LLM을 대신하는 자리보다는 LLM 앞에 세우는 자리가 맞다고 봤습니다.

글을 쓰지 않는 모델
에이전트를 만들다 보면 모델 호출의 상당수가 사실은 글을 쓰는 일이 아니더라고요. 이 요청이 환불인지 재예약인지, 두 이름이 같은 회사인지, 이 문장이 정책에 걸리는지를 묻는 것들입니다. 답은 보기 몇 개 중 하나인데도 LLM에 JSON으로 달라고 부탁하고, 파싱하고, 보기 밖의 답이 오면 버리는 코드를 매번 짜게 되죠. jev는 바로 이 자리를 노린 모델입니다. 텍스트를 생성하지 않고, 판단할 내용(state)과 질문을 받아 타입이 정해진 답과 확률을 돌려줍니다[1]. 스스로를 System One 모델이라고 부르는데, 빠르고 직관적인 사고와 느리고 숙고하는 사고를 나눈 카너먼의 구분에서 빠른 쪽을 맡겠다는 뜻입니다[2].
질문 종류는 셋입니다.
| 종류 | 묻는 것 | 돌려주는 것 |
|---|---|---|
| Choice | 보기 중 하나를 고르기 | 고른 보기, 보기별 확률, 확신도 |
| Score | 순서 있는 등급 중 어디쯤인가 | 점수, 등급별 확률, 확신도 |
| Noul | 예인가 아니오인가 | 예일 확률(0-1) |
한 요청에 질문 여러 개를 같은 입력에 대해 한꺼번에 물을 수 있고, 답은 항상 우리가 준 보기 안에서만 나옵니다. 가격은 입력 100만 토큰당 0.042달러이고 출력은 받지 않습니다[3]. 한계도 문서에 솔직하게 적혀 있습니다. 텍스트만 받고, 영어가 기본이라 한중일 문자를 포함한 다른 언어는 처리는 하지만 같은 수준은 아니라고 합니다. 셈과 개수 세기에 약하고, 이중 부정이나 돌려 말한 조건은 덜 믿을 만하고, 판단과 무관한 내용이 입력에 많이 섞이면 정확도가 떨어진다고 해요[3]. 결국에는 좁고 닫힌 질문에, 짧은 입력을 보고 빠르게 답하는 모델입니다.
LLM 호출을 다시 세어 보면
새 모델이 나오면 저는 먼저 지금 돌아가는 에이전트의 LLM 호출을 하나씩 꺼내 봅니다. 이번에도 그렇게 했는데, 꺼내 놓고 보니 모양이 크게 셋으로 갈리더라고요. 기준은 네 가지였습니다.
보기가 닫혀 있는가, 셈이 필요 없는가, 텍스트만으로 되는가, 도구를 쓰며 여러 단계를 밟지 않고 한 번에 판단하는가.
잘 맞는 자리는 분류와 라우팅, 이름이나 엔티티가 같은 대상인지 보는 매칭, 어떤 규칙이 이 입력에 정말 적용되는지 다시 보는 확인이었습니다. 셋 다 지금은 LLM을 한 번 불러 보기 중 하나나 예/아니오를 받는 구조라 그대로 옮겨 앉힐 수 있어요. 가드레일처럼 입력이나 출력을 거르는 자리도 같은 모양입니다.
조건부인 자리도 있었습니다. 등급을 매기는 평가는 Score로 물을 수는 있지만, 판단에 참고할 사례나 기준을 입력에 같이 넣어야 해서 입력이 길어집니다. 무엇을 제외하느냐가 핵심인 판단, 예를 들어 이 대상에 포함되지만 이런 경우는 뺀다는 식의 조건은 jev 문서가 스스로 약하다고 밝힌 영역이라 좀 불안합니다.
맞지 않는 자리는 분명했어요. 도구를 부르며 근거를 찾아가는 다단계 추론, 이미지나 스캔 문서 판독, 숫자를 읽고 합을 맞추는 일, 사람이 읽을 문장을 쓰는 일입니다. 결국에는 에이전트가 하던 일 가운데 사실상 분류였던 것들만 추려 낸 셈이죠. 추려 보면 생각보다 그 몫이 작지 않습니다.
무엇으로 재 봤나
시험 과제는 가장 전형적인 모양으로 골랐습니다. 짧은 텍스트 한 줄을 받아 열한 개 카테고리 중 하나로 나누는 분류입니다. 입력에는 분류 대상 설명과 함께 원래 붙어 있던 라벨 같은 메타데이터가 들어 있는데, 실무 데이터라 이 메타데이터가 내용과 어긋나는 경우가 되게 많습니다. 그래서 메타데이터는 참고만 하고 내용을 보고 판단해야 하죠. 카테고리 정의도 서로 조금씩 겹칩니다. 외부 강연료는 강연 카테고리에도, 외부 위탁 카테고리에도 걸칠 수 있는 식이에요.
이 과제의 원래 구조는 흔한 세 단입니다. 규칙으로 가를 수 있으면 코드가 바로 정하고, 못 가른 것만 LLM에 묻고, LLM도 모르면 메타데이터 기반의 결정론적 기본값으로 내려갑니다. LLM은 GPT-6 계열의 추론 모델이고, 확신이 없으면 null을 내도록 지시해 뒀습니다. 결국에는 두 번째 단을 jev로 바꿀 수 있느냐가 이번 질문이었습니다.
실제 데이터에서 중복을 뺀 120건을 뽑고, 정답 라벨은 한 건씩 직접 달았습니다. 여기서 좀 고민이 됐는데, 애매한 건을 답 하나로 못 박으면 채점이 제 취향을 재는 일이 됩니다. 그래서 정답을 허용 답의 목록으로 두고, 여러 카테고리가 섞였거나 내용이 부족한 건은 판단불가도 정답으로 인정했습니다. 정답이 하나로 확정되는 건은 66개였고, 이것들을 확정 건으로 따로 집계했습니다.
비교 대상은 세 갈래입니다. 원래 구조 그대로의 GPT-6, 판단불가를 보기에 넣은 jev Choice, 카테고리마다 예/아니오를 따로 묻는 jev Noul입니다. jev는 질문과 보기 설명을 한국어로 준 판과 영어로 준 판을 따로 돌렸고, 같은 입력을 두 번씩 돌려 반복했을 때 같은 답이 나오는지도 봤습니다. 호출은 pydantic-ai의 TypeSafe 연동으로 했습니다. 출력 타입의 필드 설명이 질문이 되고, 보기마다 붙인 설명이 판단 기준이 되는 방식이에요[4].
숫자부터
| 방식 | 정확도(전체) | 정확도(확정 건) | 답한 비율 | 답한 것 중 정답 | 반복 일치 | 건당 지연 |
|---|---|---|---|---|---|---|
| GPT-6 단독 | 96.7-99.2% | 95.5-98.5% | 75-88% | 100% | 80.0% | 0.9초 |
| jev Choice 한국어 | 90.0-91.7% | 98.5% | 90-92% | 91-93% | 97.5-99.2% | 0.2초 |
| jev Choice 영어 | 85.0% | 95.5% | 91-92% | 84% | 99.2% | 0.2초 |
| jev Noul 한국어 | 94.2-95.8% | 97.0-98.5% | 69-73% | 98-99% | 96.7% | 0.2초 |
| jev Noul 영어 | 90.0% | 86.4% | 72% | 94% | 99.2% | 0.2초 |
범위는 두 회차의 값이고, GPT-6의 지연은 여러 건을 묶어 한 번 부른 시간을 건수로 나눈 값입니다[5].
GPT-6는 답한 것 중에는 하나도 틀리지 않았습니다. 대신 많게는 넷 중 하나꼴로 판단을 미뤘고, 미룬 것 중에는 사람이 보면 바로 답이 나오는 쉬운 건도 있었어요. jev Choice는 거의 다 답했는데 그만큼 틀린 답도 냈고, Noul은 GPT-6보다 조금 더 미루는 대신 답한 것의 98-99%를 맞혔습니다. 확정 건만 놓고 보면 세 방식이 거의 같습니다. 결국에는 차이가 쉬운 건이 아니라 애매한 건을 어떻게 다루느냐에서 났습니다.
Choice는 확신도와 같이 봐야 한다
Choice의 오답을 열어 보니 거의 다 애매한 건이었습니다. 두 카테고리의 정의에 모두 걸치거나, 어느 정의에도 딱 맞지 않는 것들이에요. 사람이 봐도 답 하나로 떨어지지 않습니다. 판단불가라는 보기를 줬는데도 잘 고르지 않더라고요. 보기 설명이 조금이라도 걸리는 카테고리가 있으면 그쪽으로 확률이 몰렸습니다.
대신 확신도는 쓸 만했습니다. 한국어 Choice 1회차에서 확신도 0.9 이상인 63건은 전부 맞았고, 0.5 미만인 22건은 64%만 맞았습니다.
같은 1회차에서 확신도 0.5 미만을 판단불가로 돌리면 정확도 95.0%, 답한 비율 73%, 답한 것 중 정답 97.7%가 됩니다. GPT-6와 비슷한 성향이 되는 겁니다.
Noul로 바꿔 물었더니 보류가 저절로 나왔다
Noul은 보기 하나하나에 대해 이 항목이 이 카테고리에 해당하는가를 따로 묻습니다. 카테고리가 여섯 개면 질문 여섯 개가 한 요청에 같이 갑니다. 저는 예일 확률이 가장 높은 카테고리를 답으로 삼고, 그 확률이 0.5에 못 미치면 판단불가로 뒀습니다.
결과는 확연히 달랐습니다. 한국어 Noul 1회차의 오답 7건 중 6건이 판단 보류였고, 엉뚱한 답은 1건뿐이었어요. 그 1건도 단가와 횟수만 있고 무엇에 대한 건지 적혀 있지 않은 항목입니다. 보류한 건들은 메타데이터가 내용과 어긋나 있거나 카테고리 경계에 걸친 것들이었는데, 원래 구조에서는 이런 건이 결정론적 기본값으로 넘어가니 피해가 크지 않습니다. Choice에서 틀리던 애매한 건들은 Noul에서 대부분 맞거나 보류로 바뀌었고요.
왜 이런 차이가 날까 생각해 보면 구조 때문입니다. Choice는 보기끼리 확률을 나눠 가져서 합이 1이 되니, 어딘가에는 몰리게 돼 있죠. Noul은 보기마다 따로 묻기 때문에 모든 보기에 아니오라고 할 수 있어요. 어느 보기에도 확신이 없는 건은 모든 확률이 낮게 나오고, 그게 곧 보류 신호가 됩니다. 모르는 것을 모른다고 말할 자리가 질문 구조 안에 있는 겁니다.
반대로 기대했다가 안 된 것도 있었습니다. 예가 둘 이상 나온 건이 10개였는데, 이걸로 여러 카테고리가 섞인 건을 가려낼 수 있지 않을까 했거든요. 실제로는 카테고리 정의가 서로 겹쳐서 두 곳에 다 예가 나온 경우가 대부분이었고, 진짜 섞인 건은 1건뿐이었습니다. 멀티 라벨 탐지기로 쓰려면 카테고리 정의부터 서로 배타적으로 다듬어야 하는 겁니다. 비용은 질문 수만큼 늘어서 Noul의 건당 입력 토큰이 Choice의 1.4배쯤이었지만, 120건 전체를 돌려도 1센트가 안 됩니다. 결국에는 판단불가를 보기로 주는 것보다 보기마다 따로 묻는 쪽이 보류를 더 정직하게 끌어냈습니다.
한국어로 묻는 게 나았다
문서가 영어를 권하길래 질문과 보기 설명을 영어로 옮긴 판도 돌렸습니다. 판단할 입력 자체는 한국어 그대로 두고요. 결과는 Choice와 Noul 모두 한국어 판이 나았습니다.
영어 Choice는 같은 성격의 애매한 건들을 한 카테고리로 통째로 몰았고, 영어 Noul은 한국어 판이 맞힌 쉬운 건 몇 개를 보류하거나 이웃 카테고리로 틀렸어요. 입력은 한국어인데 질문만 영어면 모델이 두 언어 사이를 한 번 더 건너야 하니, 그 사이에서 뜻이 좀 새는 것 같습니다. 과제 하나에서 본 결과라 일반화하긴 이르지만, 한국어 입력을 다룬다면 질문도 한국어로 먼저 재 보는 게 맞겠습니다.
같은 입력에 같은 답이 나오는가
개인적으로 이번에 제일 크게 본 숫자는 이것입니다. 같은 120건을 두 번 돌렸을 때 같은 답을 낸 비율이 GPT-6는 80%, jev는 97-99%였습니다.
GPT-6의 흔들림은 대부분 답과 보류 사이에서 일어났습니다. 한 번은 카테고리를 답하고 다음에는 모르겠다고 하는 식입니다. 정확도로만 보면 둘 다 틀린 건 아니니 괜찮아 보이는데, 판정이 뒤로 이어지는 파이프라인에서는 사정이 좀 다릅니다. 사용자가 입력 일부만 고쳐서 다시 돌렸는데 손대지 않은 항목의 분류가 바뀌면, 그 뒤에 적용되는 처리와 결과 화면이 통째로 달라집니다. 사용자는 자기가 고친 것 때문인지 모델 때문인지 알 수가 없죠. 이런 흔들림을 막으려고 이전 결과를 재사용하는 조건을 따로 설계하는 팀이 많은데, 앞단의 모델 자체가 안정적이면 그 설계가 훨씬 단순해집니다.
판정 파이프라인에서 일관성은 정확도만큼 비싼 속성이다. 이번 시험에서 제일 오래 남는 건 이 문장일 것 같습니다.
폴백은 어떻게 짤 것인가
jev Noul이 보류한 건만 GPT-6에 넘기는 폴백을 이미 받아 둔 결과로 조합해 봤습니다. 따로 다시 돌린 게 아니라 같은 회차의 두 결과를 건마다 이어 붙인 계산입니다. 두 회차 모두 확정 건 정확도가 100%였고, 전체 정확도는 98.3-99.2%로 GPT-6 단독과 같거나 조금 나았습니다. GPT-6를 부르는 건은 120개 중 33-37개로, 3할 정도로 줄었고요. 둘 다 답한 75건에서는 73건이 같은 답이었으니, 앞단의 판정이 뒷단과 크게 어긋나지도 않았습니다.
그런데 조합한 흐름의 반복 일치율은 84.2%였습니다. jev 단독의 96.7%에서 뚝 떨어진 거죠. LLM으로 넘긴 3할에서 LLM의 흔들림이 그대로 따라 들어온 겁니다. jev 단독일 때는 두 회차 모두 보류한 33건도 같은 답으로 셉니다. 폴백을 붙이면 이 33건이 LLM으로 넘어가는데, LLM이 두 회차에 같은 답을 낸 건 그중 16건뿐이었어요. 전체 120건에서는 80%가 같았는데, jev가 넘긴 어려운 건에서는 절반 아래로 떨어진 겁니다. LLM은 앞단이 어려워서 넘긴 바로 그 건들에서 가장 많이 흔들렸습니다. 정확도를 얻는 대신 일관성을 일부 내준 셈이라, 어디까지 LLM에 넘길지는 무엇을 더 아끼느냐로 정해야 합니다. 뒤에 결정론적 기본값이 하나 더 있는 구조라면, jev가 보류한 건을 LLM 없이 바로 기본값이나 사람 검토로 보내는 구성도 따져 볼 만합니다. 정확도는 조금 내주더라도 결과는 매번 같아지니까요.
폴백을 짜다가 하나 헛짚은 게 있었습니다. pydantic-ai에는 FallbackModel이 있고, jev 뒤에 LLM을 붙여 두면 확신 없는 결정을 넘겨준다는 설명이 있어서 확신도 기준만 주면 되는 줄 알았어요. 코드를 열어 보니 넘기는 기준인 decision_route_threshold는 도구가 붙어 있거나 출력 타입이 여러 개일 때, 어느 경로로 갈지 고르는 질문에만 걸리더라고요[6]. 출력 타입이 하나면 경로 질문 자체가 없어서, 필드의 확신도가 아무리 낮아도 넘어가지 않습니다. 그래서 확신도와 보류를 앱 코드에서 읽어 직접 분기했습니다. 결국에는 라이브러리의 폴백은 API 실패와 경로 선택용이고, 답이 불확실한 경우는 우리 코드가 다뤄야 합니다.
삽질에서 건진 것
같은 걸 해 보실 분을 위해 막혔던 것들만 짧게 적어 둡니다.
- 보기마다 설명을 붙이려면 Enum 멤버에 docstring을 달라고 안내돼 있는데, 허용 보기가 입력마다 다르면 동적으로 만들기가 어렵습니다. 필드 스키마를 const와 description 쌍의 목록(anyOf)으로 직접 주면 같은 효과가 납니다. Noul로 보기마다 묻는 형태는 dict[str, bool] 필드의 propertyNames에 같은 목록을 주면 됩니다.
- 실행 결과의 usage는 메서드가 아니라 속성입니다. 확신도와 보기별 확률은 응답의 provider_details 아래에 필드 이름을 키로 들어옵니다.
- Noul의 확신도는 확률이 아니라 경계 0.5에서 얼마나 떨어졌는지를 0-1로 늘린 값입니다. 보기별 예 확률이 필요하면 probabilities 쪽을 읽어야 합니다.
결국에는 어디에 둘 것인가
정리하면 jev를 넣는다고 정확도가 오르지는 않았습니다. 이 과제에서는 추론형 LLM이 1-3%p 앞섰고, 원래 묶어서 한 번 부르던 호출이라 비용 차이도 크지 않았습니다. 그래도 넣을 이유는 남는데, 그게 모를 때 물러서는 모양과 반복의 안정성입니다. 보기마다 따로 묻는 Noul을 앞단에 세워 확실한 것만 가르고, 나머지를 LLM이나 결정론적 기본값으로 넘기면 LLM 호출이 3할로 줄고, 넘기지 않은 7할은 매번 같은 답을 받습니다.
그래서 저는 결정 모델을 LLM을 대신하는 모델이 아니라 LLM 앞에 서는 모델로 보기로 했습니다. 호출량이 큰 라우팅이나 가드레일이라면 비용과 지연의 이득까지 더해질 거고, 이름 매칭이나 해당 여부 확인처럼 원래 예/아니오인 질문은 Noul에 더 자연스럽게 맞을 것 같습니다. 데이터가 밖으로 나가는 문제를 먼저 풀어야 한다는 것도 그대로고요. 판단만 하는 모델이 쓸모 있어지는 순간은 정답을 많이 맞힐 때가 아니라, 모르는 걸 모른다고 말하고 그 말을 매번 똑같이 할 때더라고요.
주석
[1] TypeSafe 문서, Introduction. 텍스트 생성 없이 타입이 정해진 질문에 구조화된 답을 돌려주는 System One 모델로 소개하고, Choice, Score, Noul 세 가지 질문을 한 요청에서 섞어 쓸 수 있다고 설명합니다. https://docs.typesafe.ai/introduction
[2] Daniel Kahneman, Thinking, Fast and Slow (2011). 빠르고 자동적인 System 1과 느리고 숙고하는 System 2를 구분합니다.
[3] TypeSafe 문서, Model jaggedness: jev-1.13. 셈과 개수 세기, 숫자 보정, 이중 부정과 간접 조건, 판단과 무관한 내용이 많은 입력에서의 약점을 스스로 정리해 둔 페이지입니다. https://docs.typesafe.ai/model-jaggedness/jev-1.13
[4] Pydantic AI 문서, TypeSafe (Jev). 설치, 모델 이름, 출력 타입의 필드와 보기 설명이 질문으로 바뀌는 방식, provider_details의 확신도가 정리돼 있습니다. 이번 시험은 pydantic-ai-slim 2.52.0으로 했습니다. https://pydantic.dev/docs/ai/models/typesafe/
[5] 시험 조건. 실제 데이터 120건(정답이 하나로 확정되는 건 66개), 정답은 사람이 단 허용 답 목록, 방식마다 2회 반복. GPT-6는 원래 구조와 같은 프롬프트와 추론 강도로 입력 묶음마다 한 번 호출했고, jev는 동시 8요청으로 건마다 호출했습니다. jev의 건당 입력 토큰은 Choice 약 710, Noul 약 1,010이었습니다.
[6] pydantic-ai-slim 2.52.0의 pydantic_ai/models/decision.py. DecisionModel 설명에 경로 질문은 도구가 붙었거나 출력 타입이 여럿일 때 추가된다고 적혀 있고, 확신이 기준 미만인 경로 선택만 UnsureRoute로 넘깁니다.