프로그램을 위한 AI 판단 모델, Jev 정리
TypeSafe가 올린 Jev 소개 글을 읽으면서 처음 든 생각은 "결국 AI한테 물어보고 JSON으로 받는 것 아닌가"였다.
GPT에게도 이렇게 시킬 수 있기 때문이다.
이 사용자가 이탈할 가능성을 분석하고
low, medium, high 중 하나로 답해줘.그러면 이런 결과를 기대하게 된다.
{
"result": "high"
}그런데도 Jev라는 모델을 따로 만든 이유가 뭔지 궁금했다. 답은 사람을 위한 AI와 프로그램을 위한 AI의 차이에 있었다.
사람이 읽을 답변과 프로그램이 쓸 판단은 쓰임이 달랐다
GPT나 Claude 같은 범용 LLM은 사람과 대화하려고 만든 모델이다. "이 사용자가 이탈할 가능성이 높은 이유를 설명해줘"라고 요청하면 "최근 로그인 횟수가 감소했고 구매 활동도 없기 때문에 이탈 가능성이 높은 것으로 판단됩니다" 같은 자연어를 만들어준다.
사람에게는 유용하지만 프로그램 입장에서는 번거롭다. 쇼핑몰에서 이탈 가능성이 높은 사용자에게 쿠폰을 보내는 기능을 만든다면, 필요한 건 긴 설명이 아니라 high라는 값 하나다. 그래야 코드에서 이렇게 쓴다.
if (churnLevel === "high") {
sendCoupon();
}Jev가 파고든 문제가 여기였다. AI가 사람에게 답변하는 대신, 프로그램이 그대로 쓸 판단 결과를 돌려주게 만든다.
Jev는 AI가 들어간 함수에 가까웠다
기존 LLM을 쓸 때는 const answer = await askGPT(prompt)처럼 호출하고, 돌아오는 건 "이 사용자는 이탈 가능성이 높습니다" 같은 문자열이다. Jev는 개념적으로 const decision = await jev(input)에 가깝고, 돌아오는 건 Decision(TypeSafe가 쓰는 용어로, 프로그램이 곧바로 사용하는 판단 결과)이다.
{
type: "choice",
choice: "high",
probabilities: { low: 0.05, medium: 0.08, high: 0.87 }
}요청할 때는 자연어 질문만 던지는 게 아니라 무엇을 판단할지와 어떤 결과 중 하나를 고를지를 같이 정의한다. 이탈 가능성을 판단한다면 가능한 결과부터 정한다.
type ChurnLevel =
| "low"
| "medium"
| "high";그리고 판단 대상이 될 사용자 데이터를 준비한다.
const user = {
loginCount: 2,
recentPurchase: false,
cartAbandonment: 3,
};JS SDK(@typesafe-ai/sdk)에서는 두 가지가 하나의 요청으로 묶인다.
import { choice, TypeSafeClient } from "@typesafe-ai/sdk";
const client = new TypeSafeClient();
const response = await client.systemOne({
state: user,
questions: {
churnLevel: choice("이 사용자의 이탈 가능성은?", {
low: null,
medium: null,
high: null,
}),
},
});
console.log(response.answers.churnLevel.choice);판단 대상 데이터는 state로 넘기고, 물어볼 내용은 questions 맵에 내가 정한 id(churnLevel)를 키로 달아둔다. 답도 같은 id 아래(answers.churnLevel)로 돌아온다. 선택지는 배열이 아니라 선택지 이름을 키로 갖는 맵이고, 값 자리에는 그 선택지가 무엇을 뜻하는지 설명을 넣을 수 있다.
선택지는 내가 정하고, 그중 무엇인지는 Jev가 고른다
선택지를 low, medium, high로 정의해뒀다면 Jev가 돌려줄 수 있는 값은 그 셋뿐이다. "매우 높음", "위험해 보임", "아마 high" 같은 값을 새로 만들어내는 일은 없다. 정해둔 선택지 밖의 값이 나오는 것은 구조적으로 불가능하다고 TypeSafe는 설명한다. 여기서 typed decision(결과 타입이 미리 고정된 판단)이라는 말이 나온다.
반대로 내가 규칙을 직접 쓰지 않는 부분도 있다. 로그인 3회 이하이고 구매가 0회이고 장바구니 이탈이 2회 이상이면 high로 본다는 식의 기준을 코드로 박아둘 필요는 없었다. 그 판단은 데이터를 보고 Jev가 한다.
choice, probabilities, confidence는 서로 다른 값이었다
실제로 돌아오는 응답은 이런 모양이다.
{
"model": "jev-latest",
"answers": {
"churnLevel": {
"type": "choice",
"choice": "high",
"probabilities": { "low": 0.05, "medium": 0.08, "high": 0.87 }
}
}
}세 필드는 각각 다른 값이다.
choice: 확률이 가장 높은 선택지 하나. 여기서는high다.probabilities: 선택지 전체에 대한 확률 분포. 모든 값을 더하면 1이 된다.confidence: 같은 응답에 함께 담겨 오는 값으로, 분포가 얼마나 한쪽으로 쏠려 있는지를 0~1 값 하나로 압축한probabilities와는 별개 필드다. 한 선택지에 확률이 전부 몰리면, 그러니까probabilities가{ "returns": 1.0, "shipping": 0.0, "billing": 0.0 }이면confidence는1.0이다. 계산식 자체는 공개돼 있지 않다. 분포를 직접 계산하지 않고도 임계값 하나로 분기하라고 만들어둔 값이다.
이 확률값들은 내가 정해둔 상수가 아니라 모델이 입력을 보고 만들어낸 결과다. 선택지 전체에 대한 판단 분포가 probabilities로 돌아오고, 그중 가장 높은 high가 choice로 돌아온다.
0.87을 "87% 확신"으로 읽으면 안 됐다
0.87을 "Jev가 자기 자신을 87% 믿는다"로 읽으면 정확하지 않다. TypeSafe가 강조하는 개념은 calibrated probability, 보정된 확률이다. 모델이 90%라고 말한 판단들이 실제로도 90% 정도 맞도록 확률을 맞춰둔다는 뜻이다. Jev가 100개의 판단에 대해 90% 확률이라고 했다면 실제로도 90개쯤 맞고 10개쯤 틀려야 한다. 다만 calibration은 여러 판단을 묶어서 재는 지표다. 개별 답 하나가 맞는다는 보장은 아니다.
사람이 읽는 답변이라면 "아마 그렇습니다" 정도로 말해도 사람이 다시 판단하면 그만이다. 프로그램의 워크플로에 AI 판단이 들어가는 순간 이야기가 달라진다.
if (result.confidence > 0.9) {
automaticallyProcess();
}임계값 분기에 쓰는 단일 수치는 분포 전체인 probabilities가 아니라 confidence다. AI가 확률 0.95라고 말했는데 실제 정확도가 60%라면 이런 코드는 위험하다. 반대로 0.95라고 말한 결과들이 실제로도 그만큼 맞아준다면 자동 처리와 사람 검토를 갈라놓을 수 있다.
if (result.confidence > 0.9) {
automaticallyProcess();
} else {
requestHumanReview();
}경계를 0.9로 둘지 0.5로 둘지는 고정값이 아니라, 그 동작이 틀렸을 때 감당해야 하는 위험에 따라 정한다. Jev가 확률을 전면에 내세우는 이유도 AI의 판단을 프로그램의 자동화 로직에 연결하기 위해서다.
GPT에게 JSON으로 답하라고 시키는 것과는 다른 이야기였다
가장 걸렸던 의문이 이거였다. GPT에게 JSON으로 답하라고 하면 똑같은 걸 얻을 수 있지 않나.
다음 사용자가 이탈할 가능성을
low, medium, high 중 하나로 판단하고
JSON으로 반환해줘.이렇게 시키면 GPT도 값과 확률이 담긴 JSON을 돌려준다.
{
"value": "high",
"probability": 0.87
}겉보기 형태만 놓고 보면 Jev의 응답과 크게 달라 보이지 않는다. 하지만 일반 LLM은 문자열 생성이 중심이고 Structured Output(출력을 정해진 스키마에 맞춰 받는 기능)은 그 위에 얹힌 제약이다. Jev는 판단 자체를 중심에 두고 설계해서, 타입이 고정된 결과와 보정된 확률을 같이 돌려준다. JSON이라는 형태는 Jev가 풀려는 문제가 아니었다.
Jev는 AI가 판단하는 if문에 가까웠다
if (user.age < 14) block() 같은 코드는 내가 기준을 직접 쓴다. 그런데 코드로 조건을 다 적기 어려운 판단들이 있다.
- 이 리뷰가 악성 리뷰인가
- 이 고객이 이탈할 것 같은가
- 이 문의는 환불 요청인가
- 이 이미지가 특정 정책을 위반하는가
- 이 요청은 어떤 워크플로로 보내야 하는가
이런 판단에 규칙을 일일이 써두는 건 현실적이지 않다. 일반 코드 사이에 AI 판단을 하나 끼워 넣는 쪽이 낫다. Jev를 AI가 판단하는 if문으로 보면 어디에 쓰는지 분명해진다.
고객 문의 처리로 옮겨보면 이렇다. 사용자가 "지난주에 주문한 상품을 환불하고 싶어요"라고 입력하면 백엔드가 Jev에 판단을 요청한다. 가능한 결과는 미리 정해둔다.
type Intent =
| "refund"
| "delivery"
| "account"
| "other";refund가 0.97로 돌아오면 백엔드는 그대로 분기한다.
if (response.answers.intent.choice === "refund") {
routeToRefundWorkflow();
}역할이 겹치는 줄 알았는데 겨냥하는 곳이 달랐다
| GPT / Claude | Jev | |
|---|---|---|
| 주요 대상 | 사람 | 프로그램 |
| 주요 목적 | 답변 생성 | 의사결정 |
| 출력 | 자연어 중심 | Decision 중심 |
| 대화 | 적합 | 하지 않음 (문자열 생성 자체를 포기했다) |
| 코드 생성 | 적합 | 하지 않음 |
| 분류 | 가능 | 주요 사용 사례 |
| 워크플로 분기 | 가능 | 주요 사용 사례 |
| Structured Output | 지원 | 핵심적인 설계 방향 |
| 확률 보정 | 일반적인 핵심 기능은 아님 | 중요한 개념 |
| 빠른 반복 판단 | 상대적으로 비효율적일 수 있음 | 주요 목표 |
Jev에 자연어가 들어갈 수는 있다. "지난주에 주문한 상품 환불하고 싶어요"를 그대로 넘겨도 된다. 다만 그 문장은 사람과 대화하려고 넣는 게 아니라 애플리케이션의 다음 동작을 고르기 위해 넣는다. 입력은 자연어여도 Jev를 쓰는 주체는 프로그램이다.
애플리케이션 전체가 아니라 판단이 필요한 지점 하나를 맡는다
AI가 애플리케이션을 통째로 대신하는 그림이 아니었다. 판단이 필요한 지점 하나를 맡고, 나머지 분기는 여전히 일반 코드가 한다.
모델 구조까지는 아직 들여다보지 않았다. 요청하는 방식 자체가 달랐다. "AI야 답을 만들어줘"가 아니라 "이 상황에서 정해진 선택지 중 어떤 걸 골라야 하는지 판단해줘"로 묻는다.