# AI 에이전트

Jev·d1-ultra가 쓰는 결정 모델(System One)은 뭐가 다를까?

@Hong2026-10-20

이 글에 담긴 코드와 결과는 직접 실행해 측정한 값을 바탕으로 작성했습니다. 실측은 Hong과 팀원들이 진행했고, 그 결과를 바탕으로 AI가 글을 쓰고 Hong이 검토합니다.

실습 코드 내려받기

질문
고객 문의 분류를 소형 LLM 대신 Jev·d1-ultra 같은 System One 결정 모델(Liquid AI d1-3B)에 맡기면 토큰·형식 실패·확률 보정이 어떻게 달라질까?
답
질문 1개를 10개로 늘리면 결정 모델은 입력 토큰·지연이 약 10~11배, 생성 모델은 출력 토큰 10배·지연 4.35배였어요. 예시 한 줄로 형식 실패는 거의 사라졌고, 환불 여부 확률은 잘 맞았지만 긴급 여부는 어긋났어요.
조건
2026-10-19, llama.cpp 0.7.0-dev(b11718) Docker CPU, d1-3B Q8_0·LFM2.5-1.2B-Instruct Q4_K_M, 직접 쓴 한국어 문의 200건, 질문 1·3·10개

환경

측정일
2026년 10월 19일
실행
Docker Desktop 4.38.0(엔진 27.5.1), 컨테이너는 CPU만 씀(Apple Silicon의 GPU는 컨테이너에서 못 씀), ghcr.io/ggml-org/llama.cpp:server 이미지 다이제스트 sha256:4b7be037…, llama-server --version 0.7.0-dev(build 11718), 서버 한 대씩 -np 1 -t 2 --cache-ram 0 --no-cache-prompt, 컨테이너 cpus 2.5·mem_limit 5g
결정 모델
LiquidAI/d1-3B-GGUF의 d1-3B-Q8_0.gguf(2,874,781,280바이트, sha256 2f0942d5…)
생성 모델
LiquidAI/LFM2.5-1.2B-Instruct-GGUF의 LFM2.5-1.2B-Instruct-Q4_K_M.gguf(730,895,168바이트, sha256 b1b3de11…), temperature 0, 프롬프트 캐시 끔
데이터
직접 쓴 한국어 고객 문의 200건(환불 요청 47건, 긴급 40건, 모호 18건, 카테고리 5종 각 39~41건), sha256 inquiries.jsonl 94e201cb…, questions.json 0b34c455…, README.md a2a9995f…
반복
질문 수별 지연은 3회(호스트의 다른 작업과 CPU를 나눠 쓴 회차는 지연 집계에서 뺌, 결정 모델 질문 3개는 1회만 남음)
목차
  1. 결정 모델이 뭔가: System One, Jev, d1-ultra
  2. 실습 준비
  3. 실습 1: 질문을 1개, 3개, 10개로 늘리면
  4. 실습 2: 정답률과 형식 실패
  5. 실습 3: 확률은 믿을 만한가
  6. 정리

이번 시간에는 고객 문의 분류라는 "판정" 작업을, 글을 쓰는 소형 모델과 글을 쓰지 않는 결정 모델(요즘 Jev와 d1-ultra로 이름을 알린 System One 방식) 두 방식으로 Docker에서 직접 돌려 보며 토큰 비용, 형식 실패, 확률을 얼마나 믿을 수 있는지가 어떻게 다른지 배워가는 시간입니다. 두 모델 다 Liquid AI의 공개 가중치 모델을 골랐어요. 회사가 달라서 생기는 차이가 아니라 "생성이냐 판정이냐"라는 방식 차이만 보고 싶어서요.

여기서 잰 절대 속도는 제 Mac의 Docker CPU 숫자라 다른 컴퓨터에서는 전혀 다르게 나와요. 그래서 결론은 비율과 모양으로만 말할게요.

결정 모델이 뭔가: System One, Jev, d1-ultra

보통 우리가 쓰는 LLM은 글을 만들어서 답하지만, 결정 모델(decision model)은 문의와 질문을 읽은 뒤 질문마다 답의 확률만 돌려줍니다. 이 방식은 TypeSafe라는 회사가 System One이라는 API 규약으로 먼저 정리했고, 이 글에서는 예/아니오 질문인 noul(참일 확률 하나)과 선택지 중 하나를 고르는 choice(선택지마다 확률, 합은 1)를 씁니다.

TypeSafe의 모델 이름이 Jev이고, Liquid AI는 같은 규약으로 d1이라는 결정 모델 계열을 냈어요. 다만 d1-ultra는 API로만 쓸 수 있고 가중치는 비공개라, 이 글에서 직접 돌려 볼 수 있는 건 공개 가중치인 d1-3B뿐이에요.

llama.cpp 서버 문서(tools/server/README.md, 이 글에서 쓴 빌드의 커밋 547208dca 기준)는 "질문은 서로 독립으로 답한다", "usage.input_tokens는 모든 질문의 프롬프트 토큰 합이고 output_tokens는 항상 0"이라고 적어 두었어요. 확률에 대해서는 "They are not guaranteed to be calibrated for your data."라고 씁니다. 보정(calibration)은 모델이 "90%"라고 말한 것들을 모아 보면 실제로 90%쯤 맞는지를 뜻해요.

실습 준비

이 글의 명령은 모두 실습 코드의 decision-model-vs-generation/ 디렉터리에서 실행합니다.

  • compose.yaml: llama.cpp 서버 두 대(d1, gen, 프로필로 한 번에 하나만 띄움)와 Python 클라이언트 app
  • app/: 클라이언트 app의 이미지를 만드는 Dockerfile
  • run.sh: 셸 함수 모음(아래)
  • data/: 문의 200건과 정답 라벨(inquiries.jsonl), 라벨 기준(README.md), 질문 문구와 질문 묶음(questions.json)
  • classify_d1.py: 결정 모델 분류 클라이언트
  • classify_gen.py: 생성 모델 분류 클라이언트
  • common.py: 두 클라이언트가 함께 쓰는 질문·선택지·데이터 읽기 함수
  • report.py: 글의 모든 숫자를 만드는 집계기(out/report.md에 씀)
  • results/: 제 환경에서 나온 집계 결과(report.md)

먼저 명령을 짧게 칠 수 있게 셸 함수를 만들어 둘게요. dc는 프로젝트 이름을 고정한 docker compose, py는 일회용 Python 컨테이너로 스크립트를 실행하고, models는 모델 파일 두 개를 받아요. up d1과 up gen은 서버를 한 대씩 띄우고, fresh는 이전에 돌린 결과 파일을 지우고, down은 서버를 내리고 볼륨까지 지워요. sec는 집계 결과 파일에서 제목이 같은 블록 하나를 꺼내 보여 줘요. 아래 블록은 파일로 만들지 말고 실습 디렉터리에서 연 터미널에 그대로 붙여 넣어 주세요. 이후 명령은 모두 이 터미널에서 실행하고, 터미널을 새로 열면 다시 붙여 넣으시면 됩니다.

dc()    { docker compose -p d1cls "$@"; }

py()    { docker compose -p d1cls run --rm -T app python "$@"; }

fresh() { mkdir -p out && rm -f out/d1-*.jsonl out/gen-*.jsonl out/report.md out/stats-*.txt; }

models() {
  mkdir -p models
  local base=https://www.modelscope.cn/models/LiquidAI
  [ -f models/d1-3B-Q8_0.gguf ] || curl -L -o models/d1-3B-Q8_0.gguf "$base/d1-3B-GGUF/resolve/master/d1-3B-Q8_0.gguf"
  [ -f models/LFM2.5-1.2B-Instruct-Q4_K_M.gguf ] || curl -L -o models/LFM2.5-1.2B-Instruct-Q4_K_M.gguf "$base/LFM2.5-1.2B-Instruct-GGUF/resolve/master/LFM2.5-1.2B-Instruct-Q4_K_M.gguf"
  shasum -a 256 models/*.gguf; wc -c models/*.gguf
}

up()   { dc --profile "$1" up -d --wait "$1"; }

down() { dc --profile d1 --profile gen down -v; }

sec()  { awk -v h="## $1" 'substr($0,1,length(h))==h && substr($0,length(h)+1,1)~/^[: ]?$/{p=1;print;next} /^## /{p=0} p' out/report.md; }

모델은 Hugging Face가 막힌 제 네트워크 때문에 ModelScope 미러에서 받았어요. 받은 파일의 sha256과 크기를 환경 블록과 비교해 보세요.

models
2f0942d5a5f64cf69c3356d2be439b71644b1f0eb3a580912a5a0179eeaba77d  models/d1-3B-Q8_0.gguf
b1b3de114215d9507409a662a501a631095a479a419584e8a2ded6304b19b4f5  models/LFM2.5-1.2B-Instruct-Q4_K_M.gguf
 2874781280 models/d1-3B-Q8_0.gguf
 730895168 models/LFM2.5-1.2B-Instruct-Q4_K_M.gguf
 3605676448 total

서버 설정에서 눈여겨볼 것이 둘 있어요. 하나는 -np 1입니다. llama.cpp의 결정 모델 서버는 슬롯(서버가 동시에 처리하는 요청 자리, 기본값은 자동)이 여러 개면 한 요청의 질문들을 묶어 공통 앞부분을 한 번만 계산해요. 이번에는 "질문마다 독립으로 평가하는 기준선"을 보려고 슬롯을 하나로 두었어요.

다른 하나는 --cache-ram 0 --no-cache-prompt예요. 켜 두면 서버가 바로 앞 요청과 겹치는 앞부분이나 메모리에 남겨 둔 지난 프롬프트를 다시 써서, 질문 수와 상관없이 빨라지는 요청이 생기거든요. 서버 두 대를 같이 띄우면 CPU를 나눠 쓰면서 서로 간섭하니 한 대씩만 띄웁니다.

문의 200건은 제가 직접 한 건씩 썼어요. 라벨은 세 가지예요.

  • refund: 환불을 분명하게 요구하는가. 교환 요청이나 "취소할게요" 같은 조건부 말은 아니오.
  • category: 배송, 결제, 환불교환, 계정, 상품문의 다섯 가지 중 하나.
  • urgent: 24시간 안에 사람이 응대해야 하는가. 이중 결제, 계정 도용 의심, 배송 사고, 식품 변질, 당일 행사용처럼 시간이 지나면 손해가 커지는 경우.

사람마다 라벨이 갈릴 만한 18건은 ambiguous로 표시해 두었어요. 실제 서비스 데이터가 아니라 분포는 실제와 다릅니다.

실습 1: 질문을 1개, 3개, 10개로 늘리면

200건 전부를 질문 수(N) 1개(refund), 3개(refund·urgent·category), 10개(여기에 complaint·legal·repurchase·app_bug·delivery_delay·payment_fail·account_access를 더한 것)로 보내 볼게요. 지연은 요청들을 빠른 순서로 놓았을 때 가운데 값(p50)과 느린 쪽 5%가 시작되는 값(p95)으로 봅니다. 흔들림이 있으니 세 번씩 돌리고, 결정 모델을 먼저 다 돌린 뒤 서버를 내리고 생성 모델을 띄워요.

전부 돌리는 데 제 환경에서는 몇 시간이 걸려요. 직접 돌리지 않고 숫자만 보고 싶으면 실습 코드의 results/report.md에 제 집계 결과를 그대로 넣어 두었어요.

classify_d1.py와 classify_gen.py는 인자로 질문 수, 회차 번호, 선택지를 몇 칸 돌릴지(여기서는 0)를 받아 200건을 차례로 보내고, 응답과 지연을 out/d1-N3-r0-k0.jsonl 같은 파일에 한 줄씩 씁니다. report.py는 이 파일들을 모아 out/report.md를 만들고, 지연은 회차마다 처음 5건을 서버 예열로 보고 뺀 뒤 집계해요. 화면 출력은 길어서 버리고 sec로 필요한 블록만 꺼내 볼게요.

fresh
up d1
for rep in 0 1 2; do for n in 1 3 10; do py classify_d1.py $n $rep 0; done; done
down

up gen
for rep in 0 1 2; do for n in 1 3 10; do py classify_gen.py $n $rep 0; done; done
down

py report.py > /dev/null
for p in d1 gen; do sec "기울기 요약 $p"; done
sec "같은 N에서 생성/결정 p50 지연비"
## 기울기 요약 d1
[
 "N=1 회차 [0, 1] 제외 r2 | 입력 토큰 평균 55.5 출력 토큰 평균 0.0 | p50 900ms (회차별 886~910, 최대/최소 1.03) p95 1153ms",
 "N=3 회차 [0] 제외 r1,r2 | 입력 토큰 평균 186.6 출력 토큰 평균 0.0 | p50 3052ms (회차별 3052~3052, 최대/최소 1.00) p95 3589ms",
 "N=10 회차 [0, 1] 제외 r2 | 입력 토큰 평균 572.4 출력 토큰 평균 0.0 | p50 9704ms (회차별 9184~10439, 최대/최소 1.14) p95 14593ms",
 "N=3/N=1 비: 입력 토큰 3.36 출력 토큰 - p50 3.39 p95 3.11",
 "N=10/N=1 비: 입력 토큰 10.31 출력 토큰 - p50 10.79 p95 12.66"
]
## 기울기 요약 gen
[
 "N=1 회차 [1, 2] 제외 r0 | 입력 토큰 평균 126.1 출력 토큰 평균 7.0 | p50 1518ms (회차별 1498~1540, 최대/최소 1.03) p95 1756ms",
 "N=3 회차 [0, 1] 제외 r2 | 입력 토큰 평균 202.1 출력 토큰 평균 22.9 | p50 2745ms (회차별 2739~2755, 최대/최소 1.01) p95 2960ms",
 "N=10 회차 [0, 1] 제외 r2 | 입력 토큰 평균 419.1 출력 토큰 평균 69.5 | p50 6602ms (회차별 6592~6622, 최대/최소 1.00) p95 11847ms",
 "N=3/N=1 비: 입력 토큰 1.60 출력 토큰 3.28 p50 1.81 p95 1.69 prompt_ms 1.59 predicted_ms 3.84",
 "N=10/N=1 비: 입력 토큰 3.32 출력 토큰 9.97 p50 4.35 p95 6.75 prompt_ms 3.69 predicted_ms 17.61"
]
## 같은 N에서 생성/결정 p50 지연비
{
 "1": 1.6879,
 "3": 0.8996,
 "10": 0.6803
}

"회차" 뒤 대괄호는 집계에 남긴 회차 번호이고, "제외" 뒤는 뺀 회차예요. 뺀 회차는 측정하는 동안 같은 Mac에서 다른 작업이 CPU를 나눠 쓴 회차예요. 같은 질문 수의 가장 빠른 회차보다 p95가 2배 넘게 크거나 p50이 1.5배 넘게 커서, 집계기가 그 기준으로 회차를 빼고 번호를 적어 둡니다.

결정 모델은 질문을 10개로 늘리면 입력 토큰 10.31배에 p50 지연 10.79배가 됐어요. 출력 토큰은 늘 0이고요. 왜 그럴까요? 이 설정에서는 질문마다 문의를 처음부터 다시 읽기 때문이에요. 질문이 10개면 같은 문의를 10번 읽는 셈이라, API로 쓰면 과금 기준이 되는 입력 토큰도 걸리는 시간도 질문 수에 비례합니다.

생성 모델은 질문 10개에서 출력 토큰 9.97배에 지연은 4.35배였어요. 입력 토큰은 질문 문구가 늘어나는 만큼만 늘어서 3.32배고요. 문의 본문은 한 번만 읽고 답 토큰을 순서대로 쓰기 때문에, 질문이 늘어도 다시 읽는 부분이 없어요.

같은 질문 수의 생성/결정 지연비는 질문 1개에서 1.69, 3개에서 0.90, 10개에서 0.68로 줄어요. 두 모델의 크기와 양자화가 달라서 어느 쪽이 빠르다고는 말할 수 없지만, 결정 모델 쪽 기울기가 더 가파르다는 뜻이에요. 다만 이건 슬롯을 하나만 둔 기준선이고, 슬롯이 여러 개인 설정은 이 글에서 재지 않았습니다.

실습 2: 정답률과 형식 실패

같은 결과 파일에서 이번에는 "맞았는가"와 "형식이 깨졌는가"를 볼게요. 정답률은 질문 3개짜리 첫 회차(0번 회차)로 계산합니다. 생성 모델은 프롬프트 끝에 출력 형식 예시 한 줄(출력 형식 예시(값은 바꿔서): {"refund": false})을 붙여 "주어진 키만 가진 JSON 한 줄"로 답하게 했어요.

비교용으로 출력 형식 예시 없이 지시만 준 프롬프트(v1)도 질문 3개로 한 번 돌립니다. 생성 모델 클라이언트는 PROMPT_STYLE 환경 변수로 프롬프트를 고르고, v1이면 결과 파일 이름 끝에 -v1이 붙어요. 환경 변수는 컨테이너 안으로 넘겨야 해서 이 명령만 py 대신 dc run을 그대로 씁니다.

up gen
dc run --rm -T -e PROMPT_STYLE=v1 app python classify_gen.py 3 0 0
down
py report.py > /dev/null
for p in d1 gen; do sec "정답률 $p ambiguous제외=False 파싱된건만=False"; done
for n in 3 1 10; do sec "형식 실패 gen N=$n"; done
sec "형식 실패 gen-v1 N=3"
sec "형식 실패 모양 gen-v1 N=3"
## 정답률 d1 ambiguous제외=False 파싱된건만=False
{
 "refund": {
  "n": 200,
  "correct": 198,
  "acc": 0.99
 },
 "urgent": {
  "n": 200,
  "correct": 103,
  "acc": 0.515
 },
 "category": {
  "n": 200,
  "correct": 175,
  "acc": 0.875
 }
}
## 정답률 gen ambiguous제외=False 파싱된건만=False
{
 "refund": {
  "n": 200,
  "correct": 194,
  "acc": 0.97
 },
 "urgent": {
  "n": 200,
  "correct": 165,
  "acc": 0.825
 },
 "category": {
  "n": 200,
  "correct": 56,
  "acc": 0.28
 }
}
## 형식 실패 gen N=3
{}
관대 파싱으로 살릴 수 있는 건: 0
## 형식 실패 gen N=1
{
 "not_object": 1
}
## 형식 실패 gen N=10
{}
## 형식 실패 gen-v1 N=3 (예시 줄 없이 지시만)
{
 "invalid_json": 155,
 "not_object": 45
}
관대 파싱으로 살릴 수 있는 건: 0
## 형식 실패 모양 gen-v1 N=3
{
 "키: 값 줄 여러 개": 80,
 "한 줄": 75,
 "한 단어 false": 39,
 "한 단어 true": 6
}

refund는 둘 다 잘 맞혀요. 결정 모델 99%, 생성 모델 97%.

urgent는 둘 다 많이 틀리는데 방향이 반대예요. 결정 모델은 200건 중 125건을 긴급이라고 해서 51.5%, 생성 모델은 17건만 긴급이라고 해서 82.5%가 나왔어요. 실제 긴급은 40건이라, 모든 건에 아니오라고만 답해도 80%를 맞히는 문제예요.

category는 결정 모델 87.5%, 생성 모델 28%예요. 생성 모델의 답을 세어 보면 200건 중 184건이 환불교환이에요. 1.2B 모델이 다섯 선택지 중 하나를 고르는 일을 거의 못 한 거죠.

형식 실패는 출력 형식 예시 한 줄을 붙인 뒤로는 질문 1·3·10개 합쳐 600건 중 1건만 깨졌어요. 질문 1개짜리에서 JSON 대신 false 한 단어를 돌려준 건이에요.

예시 줄 없이 지시만 줬을 때(v1)는 200건 중 JSON으로 읽힌 건이 0건이에요. 155건은 키와 값을 줄마다 적거나(80건) refund=true 같은 한 줄로 답했고(75건), 45건은 false(39건)나 true(6건) 한 단어였어요. 결정 모델은 답이 글이 아니라 숫자라서 깨질 형식이 없습니다.

실습 3: 확률은 믿을 만한가

결정 모델의 refund 확률 200건을 크기순으로 늘어놓고 같은 건수로 다섯 구간을 나눈 뒤, 구간마다 "모델이 말한 평균 확률"과 "실제로 환불 요청이었던 비율"을 나란히 놓아요. 둘이 가까우면 보정이 잘 된 거예요.

이 차이의 절댓값을 구간 건수로 가중 평균한 값이 ECE(기대 보정 오차)이고, 확률과 정답(0 또는 1)의 차이를 제곱해 평균한 값이 Brier 점수예요. Brier는 "항상 기저율(환불 비율 23.5%)만 답하는" 기준선과 비교해야 의미가 있어서 그 값도 같이 내고, 1,000번 다시 뽑아 계산한 부트스트랩 95% 구간도 적었어요. AUROC는 실제 참인 건 하나와 아닌 건 하나를 아무렇게나 골랐을 때 참인 건에 더 높은 확률을 준 비율이에요.

집계는 실습 1에서 만든 out/report.md에 다 들어 있어요. refund와 urgent의 같은 건수 구간 블록만 뽑아 볼게요.

for k in refund urgent; do sec "신뢰도 d1 $k ambiguous제외=False count"; sec "Brier d1 $k ambiguous제외=False"; done
## 신뢰도 d1 refund ambiguous제외=False count
{
 "mode": "count",
 "bins": [
  {
   "n": 40,
   "mean_p": 0.0127,
   "observed": 0.0,
   "gap": -0.0127,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.0151,
   "observed": 0.0,
   "gap": -0.0151,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.02,
   "observed": 0.0,
   "gap": -0.02,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.2127,
   "observed": 0.175,
   "gap": -0.0377,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.9667,
   "observed": 1.0,
   "gap": 0.0333,
   "sparse": false
  }
 ],
 "ece": 0.0238
}
## Brier d1 refund ambiguous제외=False: 0.0104 기저율Brier 0.1798 부트스트랩 {"lo95": 0.0027, "hi95": 0.0219} AUROC 0.9981
## 신뢰도 d1 urgent ambiguous제외=False count
{
 "mode": "count",
 "bins": [
  {
   "n": 40,
   "mean_p": 0.3444,
   "observed": 0.05,
   "gap": -0.2944,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.4756,
   "observed": 0.125,
   "gap": -0.3506,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.5518,
   "observed": 0.05,
   "gap": -0.5018,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.6242,
   "observed": 0.275,
   "gap": -0.3492,
   "sparse": false
  },
  {
   "n": 40,
   "mean_p": 0.7439,
   "observed": 0.5,
   "gap": -0.2439,
   "sparse": false
  }
 ],
 "ece": 0.348
}
## Brier d1 urgent ambiguous제외=False: 0.2562 기저율Brier 0.16 부트스트랩 {"lo95": 0.2363, "hi95": 0.2749} AUROC 0.7841

refund는 맨 위 구간이 96.7%라고 했는데 실제 100%, 넷째 구간은 21%라고 했는데 실제 17.5%였어요. ECE가 0.024이고, Brier 점수 0.0104는 기준선 0.1798의 17분의 1 수준이에요. 이 데이터에서는 refund 확률을 그대로 믿고 써도 되겠어요.

urgent는 다섯 구간의 평균 확률이 34%에서 74%까지 올라가는 동안 실제 긴급 비율은 5%에서 50% 사이에 머물러요. ECE가 0.348이고, Brier 점수 0.2562는 긴급 비율 20%만 늘 답하는 기준선 0.16보다 나빠요. 부트스트랩 구간이 0.2363~0.2749로 기준선에 닿지 않으니, 표본이 흔들려서 생긴 차이가 아니에요.

그런데 AUROC는 0.784예요. 확률의 크기는 틀렸지만 순서는 어느 정도 맞는다는 뜻이에요. 제 라벨은 "24시간 안에 사람이 직접 응대해야 하는가"로 꽤 좁게 잡았는데, 모델이 생각하는 긴급은 그보다 넓은 것 같아요. 기준이 사람마다 다른 urgent는 0.5라는 임곗값을 그대로 쓰면 안 되고 자기 데이터로 임곗값을 다시 정해야 해요.

정리

범위: 2026-10-19 Docker CPU의 llama.cpp 0.7.0-dev(b11718)에서 d1-3B Q8_0과 LFM2.5-1.2B-Instruct Q4_K_M을 한 대씩 띄워, 직접 쓴 한국어 문의 200건에 질문 1·3·10개를 보낸 결과입니다. 절대 속도는 제 환경 값이라 결론에 쓰지 않았고, 비율·건수·보정 지표만 적습니다. 3B 모델 하나로 결정 모델 전체를 말할 수 없고, 데이터는 제가 쓴 200건이라 실제 분포와 달라요. d1-ultra와 Jev는 직접 재지 않았고 이 글의 숫자는 그 모델들과 무관합니다.

  • 결정 모델은 출력 토큰이 0이지만 질문마다 문의를 다시 읽어요. 질문을 1개에서 10개로 늘리면 과금 기준인 입력 토큰이 10.31배, 지연은 10.79배가 됐어요. 슬롯을 하나만 둔 기준선이라, 여러 슬롯으로 공통 앞부분을 묶는 설정에서는 지연이 이보다 덜 늘 수 있어요.
  • 생성 모델은 출력 토큰이 질문 수에 비례해 늘고(1개에서 10개로 갈 때 9.97배), 지연은 4.35배로 그보다 덜 늘었어요. 결정 모델이 출력을 안 만든다고 해서 질문이 많을 때도 덜 느려진다고 기대하지 마세요.
  • 생성 모델의 형식 실패는 프롬프트에 출력 예시 한 줄을 넣으면 600건 중 1건으로 줄지만, 예시 없이 지시만 주면 200건 중 200건이 JSON이 아니었어요. 이 정도 크기의 생성 모델로 분류를 시키려면 예시 줄부터 넣어 보세요.
  • 결정 모델의 확률은 질문에 따라 믿을 수 있기도, 없기도 해요. refund는 ECE 0.024에 Brier 0.0104(기저율 0.1798)로 그대로 써도 되지만, urgent는 ECE 0.348에 Brier 0.2562로 기저율 0.16보다 나빴어요. 임곗값으로 쓰기 전에 자기 데이터로 신뢰도 구간을 꼭 그려 보세요.
  • 정답률은 우열이 아니라 방향이 달랐어요. 결정 모델은 urgent를 과하게 긍정했고(51.5%), 생성 모델은 category에서 200건 중 184건을 환불교환이라고 답했어요(28%). 라벨 기준이 사람마다 다른 질문(urgent)은 어느 쪽도 그냥 믿지 마세요.
내 링크 만들기