TypeSafe Jev 시작하기: TypeScript SDK로 타입 안전한 분류 결정 만들고 confidence로 자동 처리·사람 검토 나누기
Choice·Score·Noul 질문 설계, 라벨 오타를 컴파일 단계에서 잡는 타입 추론, 위험도별 confidence 임계값, 모델 버전 고정과 오류 처리

핵심 요약
- Jev는 글을 생성하지 않고 코드로 정의한 질문에 choice·score·noul 값과 확률만 돌려주는 판단 전용 모델이다.
- TypeScript SDK는 질문 정의에서 답 타입을 추론해, 라벨 오타나 빠진 분기를 tsc가 컴파일 단계에서 잡는다.
- Choice confidence는 (n·p−1)/(n−1)로 계산되며, 행동의 위험도마다 다른 임계값을 둔다.
- jev-latest는 별칭이므로 임계값을 맞춘 버전 ID를 고정하고 응답의 model 필드를 기록한다.
- 영어가 주 학습 언어라 한국어 데이터는 정답셋으로 측정한 뒤 자동 처리에 넣는다.
문의 분류, 리뷰 태깅, 의도 판별처럼 "정해진 보기 중 하나를 고르는" 작업에 LLM을 쓰면, 구조화 출력으로 형식을 묶어도 프롬프트·파싱·재시도 코드가 따라붙는다. 미국의 TypeSafe AI가 2026년 9월 15일 공개한 Jev는 이 작업만 하도록 만든 모델이다. 글을 생성하지 않고, 개발자가 코드로 정의한 질문에 대해 보기 중 하나·루브릭 점수·참일 확률만 돌려준다. 회사는 이런 모델을 "System One 모델"이라고 부른다.
이 글은 소개보다 적용에 집중한다. 쇼핑몰 리뷰를 결함·배송·환불·칭찬·기타로 나누는 분류기를 TypeScript SDK로 만들고, ① 라벨을 타입으로 받아 오타를 컴파일 단계에서 막고 ② confidence 값으로 자동 처리와 사람 검토를 나누고 ③ 운영에 필요한 버전 고정·오류 처리를 붙이는 순서로 진행한다. 아래 예제 코드는 npm에 배포된 @typesafe-ai/sdk 0.7.0의 타입 선언으로 tsc --strict 검사를 통과하는 것을 확인했다(API 호출 결과는 키가 필요해 이 글에서 실행하지 않았다).
LLM 구조화 출력과 무엇이 다른가
같은 분류 문제를 LLM + JSON 스키마로 풀 때와 Jev로 풀 때의 차이를 공식 문서 기준으로 정리하면 다음과 같다. LLM 쪽 방식은 LLM 구조화 출력: JSON 스키마와 Zod로 응답 형식을 보장하고 검증하기에서 다뤘다.
| 항목 | LLM + 구조화 출력 | Jev |
|---|---|---|
| 출력 | 스키마에 맞춘 생성 텍스트(JSON) | 질문별 typed 답: choice, score, noul |
| 확신도 | 기본 제공 없음(별도 기법 필요) | Choice·Score 답마다 probabilities와 0~1 confidence |
| 여러 질문 | 한 응답에 필드를 늘리거나 호출을 나눔 | 한 요청에 질문 여러 개, 서로 독립적으로 병렬 평가 |
| 요금 | 입력+출력 토큰 | 입력 토큰만 과금(100만 토큰당 $0.042), 출력 무료 |
| 할 수 없는 것 | — | 자유 텍스트 생성, 요약, 번역 |
즉 Jev는 LLM을 대체하는 모델이 아니라 "판단" 단계만 떼어 맡기는 부품이다. 요약이나 답변 작성은 여전히 LLM이 하고, 분기·필터·라우팅처럼 결과가 곧바로 if문에 들어가는 지점에 Jev를 둔다. 회사가 발표한 속도·비용 수치(LLM 대비 수십~수백 배)는 자체 측정치이며, 블로그에서도 측정 환경에 따라 과장될 수 있다고 밝히고 있으니 자기 데이터로 다시 재야 한다.
질문 유형 세 가지와 고르는 기준
| 유형 | 쓰는 곳 | 돌려받는 값 | 이 글 예제 |
|---|---|---|---|
choice | 보기 중 하나 고르기(보기 최대 255개) | choice(라벨), probabilities, confidence | 리뷰 주제 |
score | 순서가 있는 단계로 평가 | score(기댓값, 0~단계 수-1 사이 실수), probabilities, confidence | 문제 심각도 |
noul | 문장이 참인지 | noul(참일 확률 0~1), confidence 없음 | 개인정보 포함 여부 |
판단이 "A 또는 B 또는 C"면 choice, "얼마나"면 score, "예/아니오"면 noul이다. 예/아니오를 choice로 만들 수도 있지만 noul이 확률 하나로 더 단순하다. score의 score는 정수가 아니라 단계별 확률의 가중 평균이라 1.5처럼 단계 사이 값이 나온다는 점을 기억해 둔다. 임계값을 정수로 비교하면 의도와 다르게 동작한다.
1단계: 설치와 첫 호출
- TypeSafe 콘솔에서 API 키를 만들고 환경 변수
TYPESAFE_API_KEY에 넣는다. 클라이언트는 이 변수를 자동으로 읽는다. - Node.js 22 이상에서 SDK를 설치하고 ES 모듈로 지정한다. npm 패키지의
engines는>=22이고,.ts파일을 Node로 바로 실행하려면 22.18 이상이 필요하다. - 아래 2단계의 코드를
triage.ts로 저장하고npx tsc --noEmit --strict로 타입 검사부터 통과시킨 뒤 실행한다.
export TYPESAFE_API_KEY="발급받은 키"
npm install @typesafe-ai/sdk
npm pkg set type=module
npm install -D typescript
키를 코드에 직접 넣지 않는다. SDK 기본값은 브라우저에서 클라이언트 생성을 막는데(dangerouslyAllowBrowser: false), 키가 페이지 사용자에게 노출되기 때문이다. 호출은 서버 라우트나 백엔드 작업에서만 한다. .env가 AI 코딩 도구에 읽히지 않게 하는 설정은 시크릿·.env가 AI 코딩 도구 컨텍스트에 들어가지 않게 막는 설정을 참고한다.
2단계: 질문을 코드로 정의하고 결과를 라우팅하기
질문 객체를 const로 한 번 정의해 두면 SDK가 그 정의에서 답의 타입을 추론한다. topic.choice의 타입은 string이 아니라 "defect" | "delivery" | "refund" | "praise" | "other"가 된다. 아래 코드는 주제·심각도·개인정보 여부를 한 요청에 묻고, 결과에 따라 처리 큐를 고른다.
import { choice, noul, score, TypeSafeClient, RateLimitError, UnprocessableEntityError } from "@typesafe-ai/sdk";
const client = new TypeSafeClient({
defaultModel: "jev-1.13.0",
timeout: 5000,
retry: { maxRetries: 3 },
});
const questions = {
topic: choice("What is the customer review mainly about?", {
defect: "The product is broken, faulty, or does not work as described",
delivery: "Shipping delay, wrong address, or damaged packaging",
refund: "The customer asks for a refund, return, or exchange",
praise: "Positive feedback with no request",
other: null,
}),
severity: score("How serious is the problem for the customer?", [
"No problem",
"Minor inconvenience",
"Cannot use the product",
]),
hasPersonalInfo: noul("The review contains a phone number, address, or full name"),
};
type Topic = "defect" | "delivery" | "refund" | "praise" | "other";
type Route = { queue: Topic | "human"; reason: string };
export async function triage(review: string): Promise<Route> {
const { model, answers } = await client.systemOne({ state: { review }, questions });
const { topic, severity, hasPersonalInfo } = answers;
console.log(model, topic.choice, topic.confidence.toFixed(2), severity.score.toFixed(2));
if (hasPersonalInfo.noul > 0.5) return { queue: "human", reason: "pii" };
if (topic.confidence < 0.5) return { queue: "human", reason: "low-confidence" };
switch (topic.choice) {
case "refund":
return topic.confidence >= 0.85
? { queue: "refund", reason: "auto" }
: { queue: "human", reason: "refund-needs-review" };
case "defect":
return severity.score >= 1.5
? { queue: "human", reason: "severe-defect" }
: { queue: "defect", reason: "auto" };
case "delivery":
case "praise":
case "other":
return { queue: topic.choice, reason: "auto" };
default: {
const unreachable: never = topic.choice;
throw new Error(`unknown topic ${unreachable}`);
}
}
}
export async function safeTriage(review: string): Promise<Route> {
try {
return await triage(review);
} catch (err) {
if (err instanceof RateLimitError) return { queue: "human", reason: "rate-limited" };
if (err instanceof UnprocessableEntityError) throw err;
return { queue: "human", reason: "api-error" };
}
}
코드에서 짚을 부분은 다음과 같다.
- 기준(criteria)을 문장으로 적는다.
other: null처럼 설명 없이 둘 수도 있지만, 경계가 애매한 라벨일수록 포함 범위를 적어야 confidence가 안정된다. 라벨 정의서를 쓰는 요령은 LLM으로 텍스트 분류기 만들기와 같다. - "기타" 보기를 둔다. Jev는 보기 밖의 답을 낼 수 없으므로, 기타가 없으면 어디에도 맞지 않는 리뷰가 가장 가까운 라벨로 들어간다.
- 질문 문장은 영어로 썼다. 공식 문서는 영어가 주 학습 언어이고 한국어를 포함한 다른 언어는 정확도가 같지 않다고 밝힌다. 리뷰 본문(state)은 한국어 그대로 보내되, 질문·기준 문장을 영어와 한국어로 각각 만들어 같은 정답셋으로 비교해 보고 정한다.
never로 빠진 분기를 막는다. 나중에 보기에exchange를 추가하면switch의default에서 컴파일 오류가 나서 처리 코드를 빠뜨릴 수 없다.
3단계: 라벨 오타가 컴파일 단계에서 잡히는지 확인
"타입 안전"이 실제로 무엇을 막아 주는지 확인해 보자. 위 코드에서 case "refund":를 case "refunds":로 바꾸고 tsc를 돌리면 다음 오류가 난다(실제 출력).
bad.ts(38,10): error TS2678: Type '"refunds"' is not comparable to type '"delivery" | "other" | "defect" | "refund" | "praise"'.
bad.ts(51,13): error TS2322: Type '"refund"' is not assignable to type 'never'.
LLM 출력 문자열을 비교하는 코드였다면 이 오타는 오류 없이 지나가고, 환불 리뷰가 조용히 다른 큐로 흘러간다. 두 번째 오류는 refund 분기가 사라졌다는 사실까지 알려 준다. 라벨 이름을 바꾸는 리팩터링도 같은 원리로 컴파일러가 수정할 곳을 모두 짚어 준다. 이런 점이 AI가 쓴 코드를 검증할 때 도움이 되는 이유는 TypeScript가 AI 코딩 품질과 유지보수성에 도움이 되는 이유에서 더 다뤘다.
4단계: confidence 임계값을 위험도별로 나누기
Jev의 confidence는 확률 분포에서 계산한 값이다. Choice는 보기 수를 n, 가장 높은 확률을 p라 할 때 (n·p − 1) / (n − 1)로, 한 보기에 확률이 몰리면 1, 고르게 퍼지면 0이다. 보기 5개에서 1위 확률이 0.6이면 confidence는 (5×0.6−1)/4 = 0.5다. "확률 60%"와 "confidence 0.5"가 같은 상황을 가리킨다는 점을 알아 두면 임계값을 정하기 쉽다. Noul에는 confidence가 없으므로 같은 척도가 필요하면 |2p − 1|로 계산한다.
공식 문서가 권하는 방식은 임계값 하나가 아니라 "행동의 위험도"마다 다른 값을 쓰는 것이다. 예제 코드의 기준을 표로 옮기면 다음과 같다.
| 조건 | 처리 | 이유 |
|---|---|---|
| 개인정보 noul > 0.5 | 사람 검토 | 자동 공개·전달 전에 가려야 함 |
| 주제 confidence < 0.5 | 사람 검토 | 모델이 스스로 확신하지 못함 |
| 환불, confidence ≥ 0.85 | 환불 큐 자동 배정 | 돈이 오가는 처리라 기준을 높게 |
| 환불, 0.5~0.85 | 사람 검토 | 잘못 배정하면 되돌리기 번거로움 |
| 결함, 심각도 ≥ 1.5 | 사람 검토 | "사용 불가" 쪽에 가까우면 빠른 대응 필요 |
| 배송·칭찬·기타 | 해당 큐 자동 배정 | 잘못 가도 담당자가 옮기면 됨 |
숫자 0.5·0.85·1.5는 출발점일 뿐이다. 정답이 붙은 리뷰 100~200건을 모아 호출하고, 임계값을 바꿔 가며 "자동 처리 비율"과 "자동 처리 중 틀린 비율"을 함께 본다. 자동 처리 비율이 너무 낮으면 사람 검토 부담이 줄지 않고, 오분류율이 높으면 임계값을 올린다. 측정은 아래처럼 응답의 확률을 그대로 저장해 두면 다시 호출하지 않고 임계값만 바꿔 계산할 수 있다.
// 평가 실행 시: 정답과 함께 원본 확률을 JSON Lines로 저장
const row = {
id, expected,
model, // 예: "jev-1.13.0"
choice: topic.choice,
confidence: topic.confidence,
probabilities: topic.probabilities,
};
appendFileSync("eval.jsonl", JSON.stringify(row) + "\n");
// 임계값별 집계: 저장한 결과만으로 계산
for (const th of [0.5, 0.6, 0.7, 0.85]) {
const auto = rows.filter((r) => r.confidence >= th);
const wrong = auto.filter((r) => r.choice !== r.expected).length;
console.log(th, (auto.length / rows.length).toFixed(2), (wrong / Math.max(auto.length, 1)).toFixed(3));
}
5단계: 운영 전에 붙일 것
모델 버전 고정
jev-latest는 별칭이라 새 버전이 나오면 가리키는 모델이 바뀐다. 문서 확인 시점(2026-10-17) 기준으로 jev-latest는 jev-1.13.0, jev-preview는 10월 14일 나온 jev-1.14.0을 가리킨다. 임계값을 특정 버전으로 맞췄다면 예제처럼 defaultModel에 버전 ID를 직접 적고, 응답의 model 필드를 로그로 남긴다. 새 버전은 저장해 둔 평가셋으로 다시 측정한 뒤 올린다.
제한과 오류
| 항목 | 값 | 대응 |
|---|---|---|
| 컨텍스트 | 요청당 64k, 질문당 32k 토큰 | 긴 리뷰 묶음은 나눠 보낸다 |
| 요청 본문 | 32MiB 초과 시 413 | SDK는 1KiB 이상 본문을 zstd로 압축 |
| 401 | 키 없음·잘못됨 | AuthenticationError |
| 422 | 질문 형식 오류 | UnprocessableEntityError, 재시도하지 말고 코드 수정 |
| 429 | 속도 제한 초과 | RateLimitError, SDK가 자동 재시도 |
SDK 기본 재시도는 최대 2회, 대기 500ms에서 시작해 두 배씩 최대 5초까지 늘리며, 408·429·5xx에만 적용되고 Retry-After 헤더를 따른다. 시도당 타임아웃 기본값은 10초다. 예제는 timeout: 5000, maxRetries: 3으로 바꿨고, 재시도가 끝나도 실패하면 safeTriage가 리뷰를 사람 검토로 보낸다. 판단 모델이 실패했을 때 자동 처리 쪽으로 기본값을 두지 않는 것이 핵심이다.
이런 작업에는 맞지 않는다
- 답이 보기로 정해지지 않는 작업: 요약, 답장 초안, 추출한 값을 문자열로 받는 일은 LLM이 맡는다.
- 한국어 비중이 큰데 평가셋이 없는 경우: 언어별 정확도 차이를 문서가 직접 경고하므로 측정 없이 자동 처리에 넣지 않는다.
- 이미지 판단: SDK에 이미지 입력이 있지만 실험 기능이라 별도 헤더(
TypeSafe-Danger-Zone-Images)가 필요하다. 운영 경로에는 아직 넣지 않는 편이 안전하다.
초보자가 자주 실수하는 포인트
- score 값을 정수로 가정하고 === 2처럼 비교하는 경우
- 기타 보기 없이 모든 입력을 정해진 라벨에 넣게 하는 경우
- jev-latest 별칭을 쓴 채 임계값을 고정해 버전 교체 뒤 동작이 바뀌는 경우
- API 오류 시 자동 처리 쪽을 기본값으로 두는 경우
체크리스트
- 질문 객체를 한 곳에서 const로 정의했는가
- switch에 never 검사를 넣어 보기 추가 시 컴파일 오류가 나는가
- 행동별 confidence 임계값을 정답셋으로 측정했는가
- defaultModel에 버전 ID를 고정하고 응답 model을 로그로 남기는가
- API 키를 서버 쪽 환경 변수로만 다루는가
자주 묻는 질문
Jev는 무료로 써 볼 수 있나요?
콘솔의 Playground에서 상태와 질문을 붙여 넣어 코드 없이 결과를 볼 수 있습니다. API 요금은 입력 토큰 100만 개당 0.042달러이고 출력 토큰은 과금하지 않습니다. 무료 사용량 여부는 콘솔에서 확인해야 합니다.
Python에서도 같은 타입 안전성을 얻을 수 있나요?
Python SDK는 typesafe-sdk 패키지로 설치하고 Choice·Score·Noul 클래스로 질문을 만듭니다. 다만 이 글에서 확인한 라벨 리터럴 타입 추론은 JavaScript/TypeScript SDK 문서에 명시된 기능이므로, Python에서는 Literal 타입과 mypy로 따로 검사를 구성해야 합니다.
Claude Code에서 Jev 연동 코드를 작성하게 할 수 있나요?
TypeSafe가 에이전트 스킬을 배포합니다. Claude Code에서는 claude plugin marketplace add typesafe-ai/skills 뒤 claude plugin install typesafe@typesafe-ai로 설치합니다. 생성된 코드도 이 글의 tsc 검사와 정답셋 측정을 거친 뒤 적용합니다.
참고 자료 · 검증 기준
- TypeSafe — Introducing System One Models and Jev
- TypeSafe Docs — Quick start
- TypeSafe Docs — Confidence
- TypeSafe Docs — Confidence-gated routing
- TypeSafe Docs — Models
- TypeSafe Docs — API reference
- TypeSafe Docs — JavaScript SDK
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 실제 실무 환경 테스트 및 검증된 기술 자료를 바탕으로 작성되었으며, 환경 변화에 따라 지속적으로 갱신됩니다.