Jev: 텍스트 대신 확률을 돌려주는 TypeSafe AI의 결정 모델

Jev: 텍스트 대신 확률을 돌려주는 TypeSafe AI의 결정 모델

LLM을 코드 안의 if 문처럼 써보신 적 있나요? 고객 문의를 담당 부서로 나누는 것처럼 답이 몇 가지로 정해진 판단을 맡기는 일인데요. 프롬프트에 “반드시 JSON으로만 답해”라고 적고 돌아온 문자열을 Zod로 검증하다가 스키마가 어긋나면 다시 요청하는 코드를 한 번쯤 짜보셨을 겁니다. 그렇게 해도 판단 하나에 몇 초씩 걸리고, 가끔은 목록에 없는 부서 이름을 지어내기도 하죠. 😅

9월 15일 TypeSafe AI가 이 문제를 정면으로 겨눈 모델 Jev를 공개했습니다. ChatGPT의 바탕이 된 인간 피드백 강화학습(Reinforcement Learning from Human Feedback, 이하 RLHF)을 OpenAI에서 공동으로 만든 Diogo Almeida가 2년간 비공개로 개발한 모델인데요. Jev는 LLM처럼 자연어를 읽지만 텍스트를 한 글자도 생성하지 않습니다. 대신 우리가 미리 정해둔 선택지 위에 확률 분포를 얹어 돌려주죠. 창업자의 표현으로는 “비정형 상태(state)를 넣으면 타입이 정해진 확률적 결정이 나오는 함수 호출”입니다.

이번 글에서는 세 가지 질문 타입으로 무엇을 물을 수 있는지, 확률과 신뢰도(confidence)를 코드의 분기에 어떻게 쓰는지 살펴보겠습니다. 글에 실린 응답과 수치는 모두 얼리 액세스 계정으로 직접 호출해 받은 것입니다.

텍스트를 생성하지 않는 모델

TypeSafe는 Jev를 System One 모델이라고 부릅니다. 대니얼 카너먼의 책 “생각에 관한 생각”에 나오는 개념에서 따온 이름인데요. 시스템 1은 빠르고 직관적인 판단을, 시스템 2는 느리고 신중한 추론을 맡습니다. 최근 LLM들이 추론 토큰을 길게 늘어놓으며 시스템 2 쪽으로 달려가는 동안, TypeSafe는 반대편인 “1초 안에 내리는 판단”에만 집중하는 모델을 만든 셈입니다.

작동 방식의 차이는 출력에 있습니다. LLM은 토큰을 하나씩 순서대로 생성하는 자기회귀(autoregressive) 방식이라 답이 길어질수록 느려지고, 그 자유로움 때문에 존재하지 않는 값을 지어낼 수도 있습니다. 반면 Jev는 상태와 질문을 받으면 미리 정의된 선택지 각각의 확률을 한 번에 병렬로 계산합니다. 출력이 선택지 밖으로 나갈 수 없으니 타입 오류가 생길 수 없고, 텍스트를 생성하지 않으니 환각(hallucination)이 생길 자리도 없다는 것이 TypeSafe의 설명입니다. 물론 틀린 선택지를 고를 수는 있습니다. 여기서 말하는 “환각이 없다”는 정답을 보장한다는 뜻이 아니라, 스키마 밖의 값이 나오지 않는다는 뜻으로 읽어야 합니다.

훈련 방식도 다릅니다. LLM을 챗봇으로 만든 것이 인간 피드백 강화학습(Reinforcement Learning from Human Feedback, RLHF)이고 수학처럼 검증 가능한 문제로 추론 모델을 만든 것이 검증 가능한 보상 강화학습(Reinforcement Learning with Verifiable Rewards, 이하 RLVR)이라면, TypeSafe는 세 번째 길을 택했습니다. 확률이 실제 정답률과 맞아떨어지도록 보정(calibration)하는 데 초점을 둔 훈련법, 보정된 결정을 위한 강화학습(Reinforcement Learning for Calibrated Decisions, 이하 RLCD)입니다. 보정이 잘 된 모델이라면 0.8이라고 답한 예측을 모아봤을 때 그중 80%가량이 실제로 맞아야 하죠. 사람이 좋아할 답에 맞추면 아첨과 자신만만한 환각을 함께 배우게 되니, 자동화에는 다른 목적 함수가 필요하다는 주장입니다.

LLMJev
최적화 목표사람이 선호하는 응답보정된 확률을 가진 결정
출력자유로운 문자열미리 정한 선택지 위의 확률 분포
샘플링토큰을 하나씩 순서대로모든 답을 한 번에 병렬로
응답 시간3초에서 329초70ms에서 500ms
입력 토큰 가격100만 토큰당 $0.20에서 $10100만 토큰당 $0.042
출력 토큰 가격입력의 5배 안팎무료

표의 수치는 TypeSafe의 발표 글에서 가져왔습니다. 근거와 한계는 뒤에서 따로 살펴보겠습니다.

세 가지 질문: Choice, Score, Noul

Jev에게는 문장을 건네는 대신 질문을 던집니다. TypeSafe는 이 질문 타입을 프리미티브(primitive)라고 부르는데요, 종류는 딱 세 가지고 각각 돌려주는 답의 모양이 다릅니다.

Choice는 정해진 선택지 중 하나를 고르는 질문입니다. 티켓을 담당 부서로 보내거나 문서 종류를 분류할 때 쓰고, 답으로는 고른 선택지(choice)와 선택지마다의 확률(probabilities), 신뢰도(confidence)가 옵니다. 선택지는 한 질문에 최대 255개까지 둘 수 있습니다.

Score는 순서가 있는 단계 위에서 위치를 매기는 질문입니다. 버그 심각도나 고객의 불만 정도처럼 스펙트럼으로 표현되는 판단에 쓰고요. 단계마다 설명을 붙인 목록을 넘기면 기대값(expected value) 형태의 점수(score)가 돌아오는데, 이 값은 1.4처럼 두 단계 사이에 걸칠 수도 있습니다.

Noul은 예/아니요 질문입니다. 답은 “예”일 확률 하나(noul)뿐이고, 0에 가까우면 강한 아니요, 1에 가까우면 강한 예, 0.5 근처면 모델도 확신하지 못한다는 뜻입니다. 셋 중 유일하게 신뢰도 필드가 따로 없는데, 확률 자체가 이미 그 역할을 하기 때문입니다.

어떤 타입을 고를지 헷갈린다면 코드가 그 답을 어떻게 소비할지 생각하면 됩니다. Choice는 switch 문의 각 분기에, Score는 임계값 비교에, Noul은 if 조건에 그대로 대응하니까요. 한 가지 함정은 Noul의 0.5가 “중간 정도”가 아니라 “예인지 아니요인지 반반”이라는 점입니다. “이 지원자가 Python에 강한가?”처럼 정도를 묻는 질문에 Noul을 쓰면 값을 해석할 수 없으니, 정도는 Score로 재고 Noul에는 조건이 분명한 질문만 맡겨야 합니다.

첫 호출: 고객 문의 분류하기

코드를 짜기 전에 콘솔의 플레이그라운드에서 감을 잡을 수 있습니다. 왼쪽에 평가할 내용을 붙여 넣고 가운데에 질문을 쓴 다음 실행하면, 오른쪽에 선택지별 확률 막대와 신뢰도가 바로 뜹니다.

TypeSafe 플레이그라운드에서 고객 문의 하나에 세 질문을 던진 결과. department는 technical 83%, frustration은 1단계 100%, is_urgent는 98% true를 보여준다

세 질문을 한꺼번에 던졌는데도 위쪽에 찍힌 처리 시간은 146ms와 127ms입니다. 질문을 병렬로 평가한다는 말이 어떤 뜻인지 여기서 바로 확인되죠.

API 엔드포인트는 POST /v1/systemone 하나입니다. 평가할 state와 사용할 model, 이름을 붙인 questions 맵을 보내면 같은 이름으로 답이 돌아옵니다.

curl -X POST https://api.typesafe.ai/v1/systemone \
  -H "Authorization: Bearer $TYPESAFE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<'EOF'
{
  "state": "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.",
  "model": "jev-latest",
  "questions": {
    "department": {
      "type": "choice",
      "instructions": "Which team should handle this",
      "criteria": {
        "billing": "Payment or subscription issues",
        "technical": "Bugs or integration problems",
        "sales": "Pricing or account questions"
      }
    },
    "frustration": { "type": "score", "instructions": "How frustrated the customer appears", "criteria": ["Calm, just stating facts", "Frustrated but civil", "Very angry, strong language"] },
    "is_urgent": { "type": "noul", "instructions": "The message conveys urgency or time-sensitivity" }
  }
}
EOF

대기자 명단(waitlist)을 거쳐 받은 계정으로 실제 돌려본 응답입니다.

응답
{
  "model": "jev-1.13.0",
  "answers": {
    "department": {
      "type": "choice",
      "choice": "technical",
      "confidence": 0.73,
      "probabilities": { "sales": 0.0, "billing": 0.18, "technical": 0.82 }
    },
    "frustration": {
      "type": "score",
      "score": 1.0,
      "confidence": 1.0,
      "legend": { "0": "Calm, just stating facts", "1": "...", "2": "..." },
      "probabilities": { "0": 0.0, "1": 1.0, "2": 0.0 }
    },
    "is_urgent": { "type": "noul", "noul": 0.99 }
  },
  "usage": { "input_tokens": 425, "output_tokens": 73 }
}

department는 technical을 골랐지만 billing에도 0.18을 남겨두었습니다. Stripe 연동 문제는 결제와 기술 양쪽에 걸쳐 있으니 그럴 만하죠. 신뢰도 0.73은 이 분포가 얼마나 한쪽으로 몰려 있는지를 한 숫자로 요약한 값입니다. model 필드에 찍힌 jev-1.13.0도 눈여겨볼 만합니다. 요청에는 jev-latest라는 별칭(alias)을 썼는데, 새 버전이 나오면 별칭이 옮겨가면서 같은 요청의 답이 바뀔 수 있으니 임계값을 정교하게 맞춰뒀다면 버전을 고정하는 편이 안전합니다.

같은 요청을 다시 보내면 값이 조금씩 흔들리기도 합니다. 다섯 번 연속 호출해봤더니 technical이 0.82에서 0.85 사이, 신뢰도가 0.73에서 0.78 사이를 오갔거든요. 고른 답은 다섯 번 모두 같았고요. 문서가 말하는 일관성은 “비슷한 입력에 비슷한 답”이지 “같은 입력에 똑같은 비트”가 아니니, 임계값을 소수점 둘째 자리에 딱 붙여 두면 같은 입력에도 분기가 뒤집힐 수 있습니다.

매번 JSON을 손으로 쓸 필요는 없습니다. Python SDK(typesafe-sdk)와 JavaScript SDK(@typesafe-ai/sdk)가 있고, 둘 다 TYPESAFE_API_KEY 환경 변수를 읽어 jev-latest를 기본 모델로 호출합니다. JavaScript SDK는 Node.js 20 이상을 요구하지만 Bun에서도 그대로 동작하고, 설치는 npm으로 해도 됩니다.

bun add @typesafe-ai/sdk  # npm install @typesafe-ai/sdk

같은 요청을 TypeScript로 쓰면 choice(), score(), noul() 헬퍼로 질문을 만들어 systemOne()에 넘기는 형태가 됩니다.

triage.ts
import { choice, noul, score, TypeSafeClient } from "@typesafe-ai/sdk";

const client = new TypeSafeClient(); // TYPESAFE_API_KEY 환경 변수를 읽습니다

const ticket =
  "Hi, I've been trying to connect my Stripe account for 3 days and the integration keeps failing. I'm losing sales. Please help ASAP.";

const { answers, usage } = await client.systemOne({
  state: ticket,
  questions: {
    department: choice("Which team should handle this", {
      billing: "Payment or subscription issues",
      technical: "Bugs or integration problems",
      sales: "Pricing or account questions",
    }),
    frustration: score("How frustrated the customer appears", [
      "Calm, just stating facts",
      "Frustrated but civil",
      "Very angry, strong language",
    ]),
    isUrgent: noul("The message conveys urgency or time-sensitivity"),
  },
});

// answers.department.choice의 타입은 "billing" | "technical" | "sales"
console.log(answers.department.choice, answers.department.confidence);
console.log(answers.frustration.score, answers.isUrgent.noul);

답의 타입이 질문에서 추론된다는 점이 특히 좋습니다. choice()에 넘긴 선택지 키가 그대로 answers.department.choice의 리터럴 유니언(literal union) 타입이 되기 때문에, 뒤에서 === "tecnical"처럼 오타를 내면 실행 전에 tsc가 잡아줍니다. 모델이 선택지 밖의 값을 내지 않는다는 런타임 보장과 컴파일 타임 타입이 맞물리는 셈이죠.

코딩 에이전트와 함께 쓰라고 에이전트 스킬도 제공합니다. 클로드 코드라면 플러그인 마켓플레이스에 typesafe-ai/skills를 추가해 설치하면 되고, 문서 전체가 llms.txt로도 제공되니 에이전트에게 주소만 건네도 됩니다.

확률과 신뢰도로 분기하기

Jev의 답을 코드에 끼워 넣을 때 정작 봐야 할 것은 답 자체가 아니라 그 옆에 붙어오는 신뢰도입니다. 답은 “무엇”을 알려주고, 신뢰도는 “그 답을 믿고 행동해도 되는지”를 알려주니까요.

flowchart TB
    accTitle: Jev를 코드 안에 끼워 넣는 흐름
    accDescr: 코드가 상태와 여러 질문을 한 요청으로 보내면 Jev가 모든 질문을 병렬로 평가해 타입이 정해진 답과 확률, 신뢰도를 돌려준다. 코드는 신뢰도에 따라 자동 처리, 사용자 확인, 사람 검토로 분기한다.

    code["코드: 상태 + 질문 묶음"] --> jev["Jev<br/>모든 질문을 병렬 평가"]
    jev --> answers["타입이 정해진 답<br/>+ 확률 + 신뢰도"]
    answers --> gate{"신뢰도는?"}
    gate -- "높음" --> auto(["자동 처리"])
    gate -- "중간" --> confirm(["사용자에게 확인"])
    gate -- "낮음" --> human(["사람이나 추론 모델로"])

신뢰도는 확률 분포의 모양에서 계산됩니다. 확률이 한 선택지에 몰려 있으면 1에 가깝고, 고르게 퍼져 있으면 0에 가깝습니다. 선택지가 세 개인 Choice라면 가장 큰 확률이 0.9일 때 0.85 정도, 셋이 똑같이 3분의 1씩이면 0이 되는 식이죠.

이 값은 보통 세 구간으로 나눠 씁니다. 높으면 자동으로 처리하고, 중간이면 사용자에게 확인을 받고, 낮으면 사람이나 더 느린 추론 모델에게 넘기는 식인데요. 중요한 건 임계값이 하나가 아니라는 점입니다. 잔액 조회처럼 되돌릴 수 있는 일과 송금 승인처럼 되돌릴 수 없는 일에 같은 기준을 적용할 이유가 없으니까요.

route.ts
const { answers } = await client.systemOne({
  state: userMessage,
  questions: {
    action: choice("What is the user trying to do?", {
      check_balance: "View account balance",
      approve_transfer: "Approve the pending withdrawal request",
      support: "Get help with an issue",
    }),
  },
});

const { choice: action, confidence } = answers.action;

if (confidence < 0.5) {
  // 모델이 확신하지 못하면 추측하지 않고 사람에게 넘깁니다
  routeToHuman(userMessage);
} else if (action === "check_balance") {
  // 낮은 위험: 화면을 잘못 보여줘도 되돌릴 수 있습니다
  showBalance();
} else if (action === "approve_transfer") {
  if (confidence > 0.9) confirmThenExecute();
  else askUserToConfirm(); // 높은 위험인데 신뢰도가 애매하면 한 번 더 묻습니다
} else {
  routeToHuman(userMessage);
}

0.5라는 바닥값은 모델이 스스로 “잘 모르겠다”고 보고한 경우를 걸러내고, 그 위에서는 작업의 위험도에 따라 자동 실행 기준을 다르게 둡니다. 위험 감수 수준을 프롬프트가 아니라 코드가 들고 있는 구조죠. 물론 이게 성립하려면 신뢰도가 실제 정답률과 맞아야 합니다. LLM에게 확신 정도를 물어본 분은 아시겠지만 그 숫자는 대체로 과하게 자신만만하고 같은 입력에도 들쑥날쑥한데, 앞에서 본 RLCD가 존재하는 이유가 바로 이것입니다.

질문은 잘게, 조합은 코드에서

TypeSafe 문서를 관통하는 설계 원칙은 “원자적인 질문을 코드에서 조합하라”는 것입니다. “이 메시지를 분석해서 최선의 대응을 정해줘”는 느린 추론이 필요한 일이라 좋은 질문이 아닙니다. Jev에게는 “이 메시지가 긴급함을 드러내는가?”처럼 아는 사람이 1초 안에 판단할 질문을 여러 개 던져야 하죠. 스타트업 피칭을 평가하고 싶다면 시장 크기, 기술적 실현 가능성, 차별성을 각각 묻고 가중치는 코드에서 정합니다. 우선순위가 바뀌면 프롬프트를 다시 쓰는 대신 숫자만 고치면 되고요.

이 원칙이 실용적인 이유는 질문을 늘리는 비용이 거의 없기 때문입니다. 한 요청에 담긴 질문은 모두 같은 상태를 보고 독립적으로 병렬 평가되니, 하나 더 붙여도 응답 시간은 거의 그대로고 비용은 그 질문의 토큰만큼만 늘어납니다. 그래서 문서는 당장 필요할지 확실하지 않은 질문까지 한 번에 던져두는 추측성 팬아웃(speculative fan-out)을 권합니다. 공식 쿡북(cookbook)에서는 위키백과 문서에 13개 질문을 한 요청으로 묶었을 때 따로 호출할 때보다 12.2배 싸고 10배 빨랐다고 보고하고요.

상태도 문자열 하나일 필요가 없습니다. 티켓과 주문 기록, 환불 정책을 JSON 객체 하나에 담아 보내고 질문에서 `ticket.messages[0].text`처럼 경로로 특정 부분을 가리킬 수 있습니다. 요청당 컨텍스트는 64k 토큰이고, 상태와 가장 긴 질문 하나를 합친 길이는 32k를 넘을 수 없습니다.

세는 일도 이 원칙을 따릅니다. “이 목록에 과일이 몇 개인가?”라고 묻는 대신, 항목마다 “이건 과일인가?”라는 Noul을 던지고 코드에서 더하는 식이죠.

count.ts
const items = [
  "typesafe",
  "apple",
  "california",
  "banana",
  "likes",
  "calibration",
  "orange",
  "vertex",
];

const { answers } = await client.systemOne({
  state: { items },
  questions: Object.fromEntries(
    items.map((_, i) => [
      `item_${i}`,
      noul(`Is \`items[${i}]\` the name of a fruit?`),
    ]),
  ),
});

const fruitCount = Object.values(answers).filter((a) => a.noul > 0.5).length;

판단은 모델이, 산술은 코드가 맡는 분업입니다. 뒤에서 보겠지만 날짜 비교나 숫자 계산도 같은 방식으로 다뤄야 합니다.

속도와 비용, 그리고 그 근거

발표 글에서 가장 눈에 띄는 숫자는 응답 시간 70ms에서 500ms, 입력 토큰 100만 개당 $0.042(10억 개당 $42), 출력 토큰 무료입니다. 같은 수준의 System One 작업에서 LLM보다 40배에서 200배 빠르다고 하고요.

지능 비교를 위해 TypeSafe는 워크플로우 평가라는 방식을 새로 만들었습니다. 정답 레이블을 맞히는 벤치마크가 아니라, 코드로 짜인 동일한 워크플로우를 모든 모델에게 돌리고 가장 크고 비싼 모델(GPT-6 Astra와 Fable 5.1)의 평균 확률에 얼마나 가까운지 재는 방식인데요. 여기서 홈페이지에 걸린 193.6배 빠르고 444.6배 싸다는 수치가 나왔습니다. 다만 TypeSafe 스스로 이 결과가 실제로 얻을 이득의 상한선에 가깝다고 인정합니다. 워크플로우를 자사 팀이 만들었으니 편향이 있을 수 있고, 속도 측정도 서비스가 있는 미국 서부에서 돌린 것이라고 밝혔고요.

그래서 직접 재봤습니다. 캐나다 토론토의 가정용 인터넷에서 앞의 세 질문짜리 요청을 열 번 연속 보냈습니다.

열 번 측정한 왕복 시간
197  253  275  279  296  300  321  334  364  531 (ms)

최소 197ms, 중앙값 298ms, 최대 531ms

발표 글의 70ms에서 500ms와 견주면 대체로 그 범위 안이지만 아래쪽 끝에는 닿지 못합니다. 대륙을 건너는 왕복이 그대로 얹히기 때문인데, 연결을 세우는 데만 25ms에서 113ms가 들었으니 서울에서 호출하면 여기에 더 얹힐 것으로 보는 편이 안전합니다. 그래도 세 질문을 한 번에 묻고 300ms 안에 답을 받는다면 사용자가 기다리는 화면 안에서 쓰기에 무리가 없죠.

속도 제한은 초당 25만 토큰, 분당 요청 1,200건인데 수요가 몰려 서비스가 버거울 정도였다고 하니 예고 없이 바뀔 수 있습니다. 이 속도가 무엇을 가능하게 하는지는 데모가 잘 보여줍니다. Doom 게임 안에서 초당 열 번씩 Jev를 호출해 캐릭터를 움직이는 데모가 시간당 7달러쯤 들고요. 위키백과의 한 문서에서 링크만 타고 목표 문서까지 가는 Wikiracing 데모는 단계마다 수백에서 수천 개 링크 중 하나를 고르는데, 환각이 없다는 점이 이런 고카디널리티(high-cardinality) 선택에서 누적되어 빛을 발합니다.

아직 거친 부분

TypeSafe는 문서에 Jev 1.13의 거친 부분이라는 페이지를 따로 두고 실패 양상을 아홉 가지로 정리해 두었습니다. 제품 문서에서 이렇게 솔직한 목록을 보는 건 드문 일이죠.

첫 번째는 문자 그대로 읽는다는 점입니다. Jev는 우리가 의도한 질문이 아니라 써 놓은 질문에 답합니다. 틀린 답을 보고 “내가 진짜 묻고 싶었던 건…”이라고 설명하게 된다면 그 설명이 바로 지침에서 빠진 절반입니다. 이중 부정이나 여러 단계를 건너야 하는 질문도 정확도를 떨어뜨리고, 상태에 무관한 내용이 많을수록 정확도가 낮아지는 현상(context rot)도 있어 코드에서 먼저 걸러 보내야 합니다. 또 텍스트를 생성하지 않으니 추출이 필요하면 정규식이나 생성 모델로 후보를 뽑은 뒤 Jev에게 고르게 하라고 하고요.

수와 날짜를 못 한다는 항목은 직접 해보니 결이 조금 달랐습니다. 여덟 개짜리 목록에서 과일 개수를 물었더니 정답인 3을 골랐거든요. 그래서 목록을 늘려가며 어디서 무너지는지 봤습니다.

항목 수정답한 번에 물었을 때그때의 신뢰도
8330.82
16550.44
24880.33
3211100.25

답이 서서히 틀리는 게 아니라 신뢰도가 먼저 주저앉고 그다음에 틀립니다. 답만 봤다면 32개에서 처음 배신당했겠지만, 신뢰도를 함께 봤다면 16개 지점에서 이미 이 질문을 코드로 옮기라는 신호를 받습니다. 항목마다 Noul로 물어 코드에서 더한 쪽은 32개에서도 11을 맞혔는데, 속을 보면 monitor를 과일이라 하고 orange를 아니라고 한 오답 둘이 상쇄된 결과였습니다. 합계가 맞았다고 개별 판단이 맞은 건 아니니 세고 끝낼 게 아니라면 개별 확률도 봐야 합니다.

날짜도 비슷했습니다. 형식만 섞인 비교나 분기 경계는 0.01로 단호하게 맞혔지만, “3월 3일 다음 금요일”처럼 한 번 건너뛰는 질문은 0.56, “두 날짜가 45일 넘게 떨어져 있는가”는 실제 간격이 41일인데 0.35가 나왔습니다. 방향은 맞아도 0.5 언저리라 코드가 믿고 분기할 값이 아니죠. 못 한다기보다 모를 때 모른다고 말하는 쪽에 가깝지만, 결론은 같습니다. 산술은 코드로 옮기는 게 맞습니다.

타입을 바꿔 물으면 답도 달라집니다. 문서가 든 예시를 그대로 돌려봤더니 같은 티켓에 “환불을 요청하는가?”가 Noul에서는 0.22, 예/아니요 Choice에서는 0.02로 나왔습니다. 문서에 적힌 0.22와 0.01에 사실상 일치하죠. 질문과 그 부정문을 각각 Noul로 물었을 때 합도 1.12로 1을 넘었고요. 한 질문에서 맞춘 임계값을 다른 타입으로 옮기거나 질문 사이의 산술적 항등식을 기대하면 안 된다는 뜻입니다.

이 밖에 상태를 적대적인 데이터로 취급하지 않아 유도 문장에 답이 흔들릴 수 있고, 입력은 텍스트만 받으며, 고객별 파인튜닝(fine-tuning)이 없어 도메인 지식은 상태와 지침에 담아야 합니다. 공교롭게도 이 두 가지는 뒤이어 Cloudflare가 공개한 결정 모델 Clef가 내세운 차별점입니다. Clef는 이미지를 함께 받고, 자기 데이터로 모델을 다듬는 RL 파인튜닝 플랫폼도 함께 발표했거든요. 바깥의 시선도 비슷합니다. InfoWorld가 인용한 분석가들은 확률만 돌아오고 “왜 그렇게 판단했는지”가 없어 규제 산업에서 감사(audit)가 어렵다는 점, 질문과 임계값과 에스컬레이션(escalation) 경로를 개발자가 모두 설계해야 한다는 부담을 지적합니다. 반대로 보면 TypeSafe는 바로 이 설계 부담을 의도했습니다. 코드가 워크플로우를 소유하고 모델은 좁은 판단만 맡는다는 것이 이 제품의 전제니까요.

한국어로 물으면 얼마나 달라질까

문서는 Jev의 주 훈련 언어가 영어이고 한국어는 정확도가 떨어진다고만 적어두었습니다. 얼마나 떨어지는지는 말해주지 않으니, 뜻이 같은 티켓을 한국어와 영어로 만들어 똑같은 질문(지침은 영어로 고정)을 던져봤습니다.

티켓언어부서신뢰도불만
중복 결제한국어billing1.000.17
중복 결제영어billing1.000.09
API 500 에러한국어technical1.000.53
API 500 에러영어technical1.000.43
가격 문의한국어sales1.000.00
가격 문의영어sales1.000.00
연동 실패한국어technical0.451.00
연동 실패영어technical0.651.00
완곡한 불만한국어technical0.260.05
완곡한 불만영어technical0.740.73

쉬운 판단은 언어를 가리지 않았습니다. 단서가 분명한 앞의 세 티켓은 한국어로 물어도 영어와 똑같이 확률 1.00으로 부서를 골랐거든요.

차이는 애매한 티켓에서 벌어졌습니다. 표 아래쪽 네 줄은 같은 답을 고르고도 신뢰도가 한국어 쪽에서 눈에 띄게 낮습니다. 연동 실패는 0.45 대 0.65, 완곡한 불만은 0.26 대 0.74로 벌어졌죠. 불만 점수는 더 심해서, 영어의 “I’m not happy with the fit”이 0.73인데 한국어의 “사이즈가 안 맞네요”는 0.05였습니다. 번역의 결 차이도 섞여 있겠지만 한국어 쪽이 감정 신호를 훨씬 약하게 읽은 건 분명합니다. 반대로 긴급도를 묻는 Noul은 다섯 쌍 모두 양쪽이 0.01 안팎으로 붙었고요.

그러니 한국어에서 조심할 지점은 “틀린 답이 나온다”가 아니라 “같은 답인데 신뢰도가 낮게 잡힌다”입니다. 영어 기준 임계값을 그대로 쓰면 맞게 분류한 건도 신뢰도 미달로 사람에게 넘어가 처리량만 떨어지죠. 임계값은 언어별로 따로 잡고, 감정이나 어조를 재는 Score는 한국어 표본으로 다시 보정하는 편이 안전합니다.

마치며

Jev는 “AI를 코드 안의 if 문으로 쓰고 싶다”는 오래된 바람에 가장 직접적으로 답하는 시도입니다. 텍스트 생성을 포기한 대가로 스키마 밖의 값이 나올 수 없고, 답마다 보정된 확률과 신뢰도가 붙어 코드가 언제 믿고 언제 사람을 부를지 정할 수 있습니다. 질문은 잘게 쪼개서 모델에게는 1초 안의 판단만 맡기고 산술과 흐름 제어는 코드가 쥐는 분업이 핵심이었죠.

직접 돌려보고 가장 인상 깊었던 건 속도도 가격도 아니고 신뢰도가 무너지는 방식이었습니다. 개수를 세게 했을 때 모델은 틀리기 한참 전부터 신뢰도를 끌어내리며 자기가 헤매고 있다고 말하고 있었거든요. 모델을 코드 안에 들이면서 “틀릴 수도 있다”를 감수하는 대신 “틀릴 것 같다고 미리 말해준다”를 받는 쪽에 가깝다는 느낌이었습니다.

이름의 유래가 이 제품의 야심을 말해줍니다. Jev는 증기기관의 효율이 좋아지자 석탄 소비가 줄기는커녕 폭증했다는 관찰을 남긴 경제학자 William Stanley Jevons에서 따왔습니다. 지능의 비용이 한 자릿수 떨어질 때마다 그만큼 새로운 쓰임새가 열릴 거라는 예측이죠. 맞을지는 더 지켜봐야겠지만, LLM 응답을 파싱하고 검증하고 재시도하는 코드에 지친 분이라면 들여다볼 가치가 충분합니다. 이 글의 실험을 전부 합쳐도 입력 토큰은 2만 개 남짓, 1센트에 못 미쳤으니 부담 없이 자기 데이터로 재보셔도 좋겠습니다.

더 자세한 내용은 TypeSafe AI 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0CCBY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord