Decisions API가 0.62를 줬다. 다음은?
OpenAI Decisions API의 0.62는 모델의 추정치이고, 환불해 줄지는 OpenAI도, 비슷한 모델 Jev를 내놓은 TypeSafe도 애플리케이션에 맡깁니다. 임계값을 금액과 고객 이력까지 함께 보는 비즈니스 규칙으로 관리하고 모델이 준 숫자를 결정마다 저장해 두면, 모델을 다시 부르지 않고 새 임계값을 시험할 수 있습니다.
OpenAI Decisions API가 환불 요청에 준 0.62는 「제조 불량이다」 같은 문장이 참일 가능성을 모델이 추정한 값이고, 환불해 줄지는 OpenAI도, 비슷한 모델 Jev를 내놓은 TypeSafe도 애플리케이션에 맡깁니다. 「0.7 이상이면 환불」 같은 임계값을 금액과 고객 이력까지 함께 보는 비즈니스 규칙으로 관리하고, 모델이 준 숫자를 결정마다 저장해 두면, 임계값을 바꿀 때 모델을 다시 부르지 않고 지난 환불 요청으로 시험할 수 있습니다.
제조가 불량이면 물건을 돌려받지 않고 환불해 주고, 쓰다 닳았거나 사용자 부주의로 망가졌으면 일반 반품 절차로 보내는 온라인 주방용품 상점이 있다고 가정해 봅니다. $60짜리 블렌더를 산 고객이 「두 번밖에 안 썼는데 용기에 금이 갔어요」라며 환불을 요청했습니다.
상점의 애플리케이션은 고객의 메시지와 주문 기록을 POST /v1/decisions로 보내면서 문장이 참일 확률을 묻는 predicate 질문 유형으로 요청합니다. 지침(instructions)은 「메시지에 적힌 문제가 정상적인 마모나 오용이 아니라 제조 불량인가요?」입니다. 응답은 이렇습니다.
{
"model": "gpt-6-luna",
"answers": [
{ "type": "predicate", "name": "defect", "probability": 0.62 }
],
"usage": { ... }
}
응답의 probability 0.62는 이 글을 위해 정한 값입니다. 0.62라면 고객의 메시지를 제조 불량으로도 오용으로도 볼 수 있습니다.
OpenAI 가이드는 질문의 답을 보고 실제로 처리하는 일은 애플리케이션이 해야 한다고 못박습니다. 임계값은 사람이 정답을 달아 둔 예시를 놓고, 잘못 답한 경우(오탐)나 놓친 경우(미탐)의 비용을 따져 선택해야 한다고 안내합니다. TypeSafe도 LLM(대규모 언어 모델) 가드레일 쿡북에 「TypeSafe supplies the assessment; your application owns the decision.」이라고 적었습니다. 평가는 TypeSafe가 하되 결정은 애플리케이션이 내려야 한다는 뜻입니다. 고객의 블렌더 환불 요청을 바로 들어줄지, 사람(운영자)에게 넘길지, 일반 반품으로 보낼지는 애플리케이션을 운영하는 온라인 주방용품 상점이 정해야 합니다.
OpenAI Decisions API와 TypeSafe Jev가 돌려주는 것
TypeSafe는 Jev를 대표 모델로 내놓은 회사입니다. 보통의 LLM API는 사람이 읽을 글을 내도록 만들어져 있어 애플리케이션이 그 글을 해석해야 합니다. Jev는 Decisions API와 마찬가지로 글을 쓰지 않고, 숫자 하나나 보기 가운데 하나, 등급 점수 하나처럼 코드가 바로 쓸 수 있는 답만 돌려줍니다. 애플리케이션은 이 값을 if 문에 그대로 넣으면 됩니다. TypeSafe 문서는 임계값을 정하는 법과 숫자를 읽는 법을 OpenAI 문서보다 자세히 다뤄서 이 글에서 자주 인용합니다. OpenAI의 predicate처럼 예/아니요를 묻는 질문 유형을 TypeSafe는 Noul이라고 부릅니다. 두 API를 나란히 놓으면 이렇습니다.
| OpenAI Decisions API | TypeSafe Jev | |
|---|---|---|
| 호출 주소 | POST /v1/decisions | POST /v1/systemone |
| 모델 이름 | gpt-6-luna 하나만 지원 | jev-1.14.0 또는 별칭 jev-latest |
| 예/아니요 질문 | predicate, probability를 돌려줌 | Noul, noul을 돌려줌 |
| 보기에서 하나 고르기 (담당 부서 등) | choice | Choice |
| 순서 있는 등급 매기기 (심각도 등) | score | Score |
| 입력 | 텍스트, 이미지, JSON | 텍스트와 JSON. 이미지는 별도 헤더가 필요하고, TypeSafe는 아직 운영 환경용이라고 밝히지 않음 |
| 요청당 질문 수 | 1~400개 | 정해진 개수 없음. 기본 한도는 요청당 64k 토큰 |
| 입력 토큰 100만 개당 가격 | $0.10 (캐시 미적용) | $0.042 |
| 출력 토큰 | 무료 | 무료 |
| 문서에 적힌 속도 | Responses API보다 약 10배 빠름 | 대부분의 호출이 약 100ms |
같은 0.62라도 어느 필드에 담겼느냐에 따라 뜻이 다릅니다.
| 필드 | 나오는 곳 | 0.62의 뜻 |
|---|---|---|
probability(predicate), noul | Decisions API, Jev | 문장이 참일 가능성을 모델이 추정한 값 |
score | 두 API의 score 질문 | 등급에 0, 1, 2처럼 번호를 매기고, 모델이 등급마다 준 확률을 그 번호에 곱해 더한 값. 확률이 0.1, 0.7, 0.2이면 0 × 0.1 + 1 × 0.7 + 2 × 0.2 = 1.1. score가 0.62이면 0번과 1번 등급 사이라는 뜻이고, 확률이 아님 |
confidence | 두 API의 choice와 score 답 | TypeSafe에서는 확률이 한 보기에 몰릴수록 1에, 고르게 퍼질수록 0에 가까운 값(공식). OpenAI 가이드와 API 레퍼런스에는 정의가 없음 |
이 글의 0.62는 표 첫째 줄의 probability입니다. predicate와 Noul 답에는 confidence가 없어서 애플리케이션이 볼 숫자는 이 값 하나입니다. Noul 문서는 「예」의 임계값을 0.7, 「아니요」의 임계값을 0.3으로 잡고 시작하되 그 사이 값이 나온 건은 사람에게 보내도 된다고 안내합니다. 이 안내대로라면 0.62를 받은 요청은 「예」도 「아니요」도 아니어서 사람 검토로 보낼 수 있습니다.
임계값 하나로는 모자라는 이유
그런데 0.62를 받은 요청을 사람의 검토로 보내는 것이 늘 좋은 선택은 아닙니다. 자동 환불과 사람의 검토 가운데 어느 쪽이 더 효율적인 선택인지는 검토 비용과 견준 상대적인 환불 금액에 따라 달라지기 때문에 계산으로 알아볼 수 있습니다. 만약에 검토 비용이 한 건에 $6 들고, 사람의 검토는 늘 맞는 검토이고, 자동 환불이 잘못 나간다면 물건을 돌려받지는 못하니 환불한 금액을 모두 잃는다고 가정해 봅니다.
제조 불량이면 어차피 환불해 주므로, 자동 환불에서 손해가 나는 경우는 제조 불량이 아닌데 환불을 해 줬을 때뿐입니다. AI 모델이 준 확률이 제조 불량일 가능성이니 반대로 제조 불량이 아닐 가능성은 1 - AI 모델이 준 확률입니다. 이 가능성에 환불 금액을 곱한 값이 자동 환불 한 건에서 평균적으로 잃는 금액, 곧 기대 손실입니다. 사람 검토로 보내게 된다면 이 손실 대신에 검토 비용이 듭니다. 그래서 (1 - AI 모델이 준 확률) × 금액이 검토 비용보다 작을 때는 자동 환불이 더 효율적입니다.
$15 블렌더와 $60 블렌더의 제조 불량일 확률 0.62를 각각 이 공식에 넣어, 나란히 비교해 봅시다. 제조 불량이 아닐 가능성은 1 - 0.62 = 0.38입니다.
| 환불 요청 | 기대 손실 | 검토 비용($6)과 비교 | 더 효율적인 방법 |
|---|---|---|---|
| $15 블렌더 | 0.38 × $15 = $5.70 | $0.30 적음 | 자동 환불 |
| $60 블렌더 | 0.38 × $60 = $22.80 | 네 배 가까이 많음 | 사람 검토 |
제조 불량 확률이 똑같이 0.62여도 $15 블렌더 환불은 사람 검토를 거치지 않고 바로 환불하는 편이, $60 블렌더 환불은 $6의 검토 비용을 들여 사람이 검토하는 편이 더 효율적입니다. 0.38 × 금액이 검토 비용과 같아지는 금액(손익분기)은 $6 ÷ 0.38 = $15.79입니다. 제조 불량 확률이 0.62일 때 환불 금액이 $15.79보다 작으면 자동 환불이, 더 크다면 사람 검토가 더 효율적입니다.
공식을 거꾸로 쓰면 금액마다의 임계값이 나옵니다. 기대 손실이 검토 비용과 같아지는 확률을 구하면 되니, 임계값은 1 - (검토 비용 ÷ 금액)입니다. 만약 $60 환불을 자동으로 처리하려면 확률이 1 - ($6 ÷ $60) = 0.90은 돼야 하고, $15 환불이면 1 - ($6 ÷ $15) = 0.60으로 충분합니다. 금액이 클수록 잘못 환불되었을 때 잃는 돈이 커지니 임계값도 더 높아집니다.
코드에 임계값을 그냥 0.7로 적어 두면 $60 환불에도 $15 환불에도 맞지 않습니다. $60 환불이 제조 불량 확률 0.75에 자동으로 나가는데, 이때 기대 손실은 0.25 × $60 = $15입니다. 사람 검토 비용의 두 배가 넘습니다. 반대로 $15 환불은 제조 불량 확률 0.65에서 사람 검토로 가는데, 이때 자동으로 환불했다면 기대 손실은 0.35 × $15 = $5.25입니다. 사람 검토 비용 $6보다 작으니 자동 환불하는 편이 더 싼데도 사람 검토 비용을 쓰는 셈입니다.
임계값은 이렇게 금액과 검토 비용으로 정해지므로, 둘 중 하나가 바뀌면 다시 계산해야 합니다. 검토 비용이 $12까지 오르면 $60 환불의 임계값은 1 - ($12 ÷ $60) = 0.80으로 내려갑니다. Noul 문서도 환불처럼 「예」로 잘못 답했을 때 손해가 크면 임계값을 올리고, 안전 문제처럼 「예」를 놓쳤을 때 손해가 크면 내리라고 권합니다.
이 계산은 0.62가 말 그대로 62%일 때만 맞습니다. 모델이 보정(calibration)됐다는 말은, 0.62를 받은 답을 많이 모으면 그중 62%쯤이 실제로 참이라는 뜻입니다. 상점이라면 0.62를 받은 환불 요청 100건 가운데 62건쯤이 실제 제조 불량이어야 합니다. TypeSafe는 Jev 같은 모델을 보정된 답을 내도록 훈련한다고 밝힙니다. 다만 보정은 여러 건을 묶어 쟀을 때의 이야기이고 답 하나가 맞다는 보장은 아니라고 덧붙입니다. OpenAI 가이드는 probability를 모델의 추정치라고 적을 뿐 보정은 언급하지 않습니다.
OpenAI를 쓰든 TypeSafe를 쓰든, 사람이 판정한 지난 환불 요청 가운데 0.55에서 0.65 사이를 받은 건을 모아 실제로 몇 %가 불량이었는지 세어 보세요. 40%만 불량이었다면 기대 손실 공식에는 0.62 대신 0.40을 넣어야 합니다. 그러면 $15 환불의 기대 손실은 $5.70에서 (1 - 0.40) × $15 = $9까지 올라가서 검토 비용보다 커집니다.
호출마다 숫자가 달라질 수도 있습니다. 얼마나 달라지는지는 TypeSafe가 공개한 실험에서 볼 수 있습니다. TypeSafe는 자동차 보험 청구 한 건을 정해 두고, 「이 손해가 보험의 충돌 보장에 해당하나요?」 같은 예/아니요 질문 14개를 Jev에 한 번에 묻는 호출을 15번 되풀이했습니다(2026-09-11 측정, 응답 모델은 jev-1.13.0). 같은 질문을 LLM 여러 개에도 던져 비교했습니다. 보험 청구와 질문은 15번 모두 같았지만, 15번이 서로 독립된 시도가 되도록 청구 내용과 상관없는 일회용 값 하나를 입력에 넣어 매번 바꿨습니다. 그래서 TypeSafe도 숫자가 달라진 원인이 이 일회용 값인지, 입력이 완전히 같아도 생기는 차이인지는 가를 수 없다고 적었습니다.
질문 14개를 통틀어 보면 숫자 자체는 크게 바뀌지는 않았습니다. 값이 평균에서 얼마나 떨어져 있는지를 나타내는 표준편차를 질문마다 구해 평균을 내면 0.0102인데, 15번의 답이 평균에서 보통 0.01쯤 벗어났다는 뜻입니다. 같은 실험에서 확률을 돌려준 LLM 어느 것보다도 낮은 값입니다. 값이 가장 크게 달라진 두 질문은 이렇습니다. 면책 조항이 적용되는지 묻는 질문의 답은 0.53에서 0.62까지 오갔고, 충돌 보장에 해당하는지 묻는 질문의 답은 0.43에서 0.53까지 오가며 0.5를 넘나들었습니다. 0.5를 임계값으로 쓰는 애플리케이션이라면 같은 보험 청구가 호출에 따라 「예」도 되고 「아니요」도 된다는 뜻입니다.
이 실험의 비교 대상에 gpt-6-luna는 없었고, OpenAI 가이드에는 같은 요청을 다시 보내면 값이 달라지는지 적혀 있지 않습니다. 실험은 보험 청구로 했지만, 환불 요청의 숫자도 값이 가장 크게 달라진 두 질문처럼 0.1쯤 움직인다면 제조 불량 확률이 $15 환불의 임계값 0.60 근처로 나온 요청은 호출할 때마다 결과가 갈릴 수 있습니다.
그래서 상점은 자동 환불과 일반 반품 사이에 사람 검토 구간을 넓게 둡니다. $60 환불이라면 제조 불량 확률이 0.9 이상일 때만 자동으로 환불하고, 0.3 미만일 때만 일반 반품으로 보내고, 그 사이는 모두 사람 검토로 보냅니다. 임계값 근처의 요청은 호출마다 자동 처리와 사람 검토 사이를 오갈 수 있어도, 숫자가 조금 달라졌다고 환불이 반품으로 뒤집히지는 않습니다. 나중에 모델에 다시 물어도 그때와 같은 숫자가 나온다는 보장이 없어서, 결정에 쓴 숫자도 결정과 함께 저장합니다.
임계값을 둘 곳
TypeSafe의 개발 안내는 처리 순서(제어 흐름), 답이 하나로 정해지는 판단(결정론적 규칙), 환불 실행 같은 실제 동작(부수 효과)은 모델에 맡기지 말고 코드에 두라고 권합니다. 임계값 비교도 답이 하나로 정해지는 판단이니, 임계값이 몇 개 안 되고 잘 바뀌지 않으면 코드에 두라는 권고가 맞습니다. 가드레일 쿡북도 임계값을 Python POLICIES 딕셔너리에 둡니다. 다만 코드에 둔 임계값은 바꿀 때마다 배포해야 하고, 새 값이 지난 요청의 결과를 어떻게 바꿨을지는 직접 계산해야 합니다. 임계값이 금액과 이력에 따라 갈리기 시작하면 코드 밖에 두는 쪽도 따져 봐야 합니다.
| 상황 | 임계값을 둘 곳 | 감수할 것 |
|---|---|---|
| 결정 하나에 임계값 하나, 1년에 몇 번만 바꿈 | 코드의 상수 | 바꿀 때마다 배포. 숫자와 모델 이름은 직접 로그로 남김 |
| 임계값 한두 개, 1~2주마다 조정 | 실행 중에 읽는 설정값과 직접 만든 결정 로그 | 배포는 건너뛰지만, 새 값을 지난 요청으로 시험하려면 그 기능을 직접 만들어야 함 |
| 임계값이 여럿이고 금액이나 고객 이력에 따라 달라짐. 여러 팀이 바꾸고, 왜 그렇게 됐는지 묻는 사람이 있음 | 코드 밖의 버전 관리되는 규칙 | 요청이 거치는 서비스가 하나 늘고 비용이 듦 |
상점의 환불 결정은 셋째 줄의 상황입니다. 임계값이 금액마다 다르고, 검토팀은 임계값을 바꾸자고 하고, 고객 지원팀은 $60 블렌더 환불이 왜 사람 검토로 갔는지 알고 싶어 합니다.
LexQ로 옮긴 환불 결정
LexQ는 이런 규칙을 코드 밖에 두고 관리하는 의사결정 운영 플랫폼입니다. 애플리케이션은 refundAmountUsd처럼 이름을 붙인 입력값을 LexQ에 보내는데, 이 값을 fact라고 부릅니다. LexQ는 버전을 매겨 둔 규칙으로 결정을 내리고, 그 결정을 모두 기록한 뒤 돌려줍니다. 환불 요청 한 건은 이렇게 처리됩니다.
- 애플리케이션이 고객의 메시지와 주문 기록을 OpenAI나 TypeSafe에 보내 확률을 받습니다.
- 애플리케이션이 그 확률과 모델 이름, 환불 금액, 최근 90일 환불 횟수를 fact로 LexQ에 보냅니다.
- LexQ가 규칙으로 결정을 내리고, 입력값과 결정을 함께 기록한 뒤 돌려줍니다.
- 애플리케이션이 그 결정대로 환불하거나, 사람 검토로 넘기거나, 일반 반품으로 보냅니다.
모델을 부르는 일은 계속 애플리케이션이 하고, LexQ는 결정을 내리는 동안 바깥으로 호출을 하나도 보내지 않습니다. 그래서 fact와 버전이 같으면 결정도 늘 같습니다. 모델에서 받은 것 가운데 LexQ에 보낼 값은 숫자와 모델 이름뿐이고, 고객의 메시지는 보낼 필요가 없습니다.
상점의 환불 규칙 여섯 개는 애플리케이션이 ID로 호출하는 규칙 묶음인 정책 그룹(Policy Group) 하나에 들어 있습니다. 여섯 개 모두 같은 상호 배타 그룹(Mutex Group)에 속해서, 조건이 맞는 규칙이 여럿이어도 결정은 우선순위가 가장 높은 규칙 하나가 냅니다.
| 우선순위 | 규칙 | 조건 | refundDecision 값 |
|---|---|---|---|
| 1 | 반복 환불 | 최근 90일 환불 3건 이상 | HUMAN_REVIEW |
| 2 | 자동 환불 한도 초과 | $60 초과 환불 | HUMAN_REVIEW |
| 3 | 뚜렷한 불량 | 제조 불량 확률 0.9 이상이고 금액과 환불 횟수 값이 들어 있음 | AUTO_REFUND |
| 4 | 소액이고 불량 가능성이 높음 | $15 이하 환불, 제조 불량 확률 0.6 이상이고 환불 횟수 값이 들어 있음 | AUTO_REFUND |
| 5 | 뚜렷한 마모 | 제조 불량 확률 0.3 미만이고 금액과 환불 횟수 값이 들어 있음 | STANDARD_RETURN |
| 6 | 기본 | 모든 호출 | HUMAN_REVIEW |
refundDecision은 자동 환불(AUTO_REFUND), 사람 검토(HUMAN_REVIEW), 일반 반품(STANDARD_RETURN) 세 값 가운데 하나입니다. 제조 불량인 요청을 STANDARD_RETURN으로 잘못 보내도 고객이 반품 절차를 밟는 수고를 할 뿐 상점이 환불금을 잃지는 않습니다. 그래서 규칙 5는 금액마다 임계값을 따로 두지 않고 Noul 문서가 「아니요」의 시작값으로 든 0.3을 그대로 씁니다.
규칙 1과 2는 확률을 읽지 않습니다. 금액이 한도를 넘는지, 환불이 몇 번째인지는 모델에 물을 일이 아닙니다. TypeSafe도 jev-1.13의 한계를 적은 문서에 「Jev is not a calculator」(Jev는 계산기가 아닙니다)라고 썼습니다. 같은 문서는 모델을 속이려고 일부러 쓴 글이 답을 바꿀 수 있다고도 적었습니다. 메시지를 교묘하게 써서 숫자를 올릴 수는 있어도 금액이나 환불 횟수를 바꾸지는 못합니다. 0.96을 받은 $180 환불도 사람 검토로 가고, 최근 90일에 환불을 4건 받은 고객의 $60 환불은 0.95를 받아도 사람 검토로 갑니다.
규칙 3과 4의 임계값은 공식으로 구한 값으로, $60 환불은 0.9, $15 환불은 0.6입니다. 금액마다 규칙을 따로 두지 않고, 금액 구간에서 가장 큰 금액의 임계값을 씁니다. $15 초과 $60 이하 환불에는 $60의 임계값 0.9를, $15 이하 환불에는 $15의 임계값 0.6을 쓰는 식입니다. 공식대로라면 0.8이면 되는 $30 환불도 0.9 이상이어야 자동으로 나갑니다. 사람 검토로 가는 요청은 그만큼 늘지만, 어떤 금액에서도 공식이 정한 임계값보다 낮은 확률로 자동 환불이 나가지는 않습니다.
애플리케이션은 $60 블렌더 환불을 어떻게 처리할지 LexQ에 아래처럼 묻습니다. {groupId} 자리에는 정책 그룹의 ID가 들어갑니다. defectProbability는 모델이 준 제조 불량 확률, probabilityModel은 그 확률을 낸 모델의 이름, refundAmountUsd는 환불 금액, refundsLast90Days는 최근 90일 환불 횟수입니다.
curl -X POST https://api.lexq.io/api/v1/execution/groups/{groupId} \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"facts": {
"defectProbability": 0.62,
"probabilityModel": "gpt-6-luna",
"refundAmountUsd": 60,
"refundsLast90Days": 0
}
}'
응답에서 입력값과 규칙별 기록은 줄였습니다.
{
"result": "SUCCESS",
"data": {
"traceId": "...",
"inputFacts": { ... },
"mutatedFacts": {},
"generatedVariables": {
"refundDecision": "HUMAN_REVIEW"
},
"executionTraces": [ ... ],
"decisionTraces": [ ... ]
}
}
규칙이 쓴 값은 generatedVariables에 담기고, 애플리케이션은 그 안의 refundDecision만 읽으면 됩니다. 입력값을 고친 규칙이 없어서 mutatedFacts가 비어 있습니다. traceId는 이 호출의 결정 기록을 찾는 ID입니다. 의사결정 트레이스(decisionTraces)를 보면 어느 규칙이 결정을 냈는지 알 수 있고, 실행 트레이스(executionTraces)에는 규칙마다 따진 조건이 남습니다.
이번 호출에서 규칙 1~5는 조건이 맞지 않았습니다. 최근 90일 환불이 없어서 규칙 1이, 금액이 $60 초과가 아니어서 규칙 2가, 확률 0.62가 0.9보다 낮아서 규칙 3이, 금액이 $15보다 커서 규칙 4가, 확률이 0.3보다 높아서 규칙 5가 빠졌습니다. 그래서 모든 호출에 맞는 규칙 6이 결정을 냈습니다.
확률이 같아도 금액이 다르면 결정을 내는 규칙이 달라질 수 있습니다. $15 환불이면 규칙 4가 AUTO_REFUND를 내고, $180 환불이면 확률이 얼마든 규칙 2가 HUMAN_REVIEW를 냅니다.
TypeSafe는 Jev 같은 모델이 자기 판단을 설명하지 않는다고 밝히고, OpenAI의 답에도 설명 필드가 없습니다. 그래서 고객 지원팀은 $60 블렌더 환불이 왜 사람 검토로 갔는지를 LexQ의 결정 기록에서 찾습니다. inputFacts에 남은 0.62와 모델 이름, 결정을 낸 규칙과 그 규칙이 들어 있는 버전, 실행 트레이스에 남은 규칙 3의 조건을 보면 「모델이 제조 불량일 가능성을 0.62로 봤고, $60 환불은 0.9 이상이어야 자동으로 나가므로 사람 검토로 보냈습니다」라고 답할 수 있습니다.
애플리케이션에는 어느 모델의 답이든 defectProbability와 probabilityModel 두 fact로 바꾸는 함수만 있으면 됩니다. OpenAI용 함수와 Jev용 함수를 나란히 적으면 이렇습니다.
const QUESTION =
"Does the message describe a manufacturing defect " +
"rather than normal wear or misuse?";
async function askOpenAI(message: string, order: Order) {
const decision = await openai.decisions.create({
model: "gpt-6-luna",
input: [
{
role: "user",
content: [
{ type: "input_text", text: message },
{ type: "input_json", data: { order } },
],
},
],
questions: [{ type: "predicate", name: "defect", instructions: QUESTION }],
});
const answer = decision.answers[0];
return {
// 거절에는 확률이 없어 null로 바꿉니다.
defectProbability: answer.type === "predicate" ? answer.probability : null,
probabilityModel: decision.model,
};
}
async function askJev(message: string, order: Order) {
const { answers, model } = await typesafe.systemOne({
state: { message, order },
questions: { defect: noul(QUESTION) },
model: "jev-1.14.0",
});
return { defectProbability: answers.defect.noul, probabilityModel: model };
}
두 함수가 돌려주는 값의 모양이 같으므로, 모델을 바꿔도 LexQ를 호출하는 코드는 그대로입니다.
모델이 숫자를 주지 못해도 결정은 나와야 합니다. API 레퍼런스에 따르면 OpenAI는 어느 질문에든 답 대신 거절(refusal)을 돌려줄 수 있습니다. 모델 호출이 실패할 수도 있고, 금액이나 환불 횟수를 읽지 못할 수도 있습니다. 경우마다 애플리케이션이 할 일은 이렇습니다.
| 상황 | 애플리케이션이 할 일 | 결정 |
|---|---|---|
| 모델이 답을 거절하거나 모델 호출이 실패함 | defectProbability에 null을 보냄 | HUMAN_REVIEW |
| 금액이나 환불 횟수를 읽지 못함 | 그 fact에 null을 보냄 | HUMAN_REVIEW |
| LexQ 호출이 실패함 | 결정을 받지 못하니 사람 검토로 보내도록 코드에 적어 둠 | 없음 |
null은 값이 없다는 표시이고, 규칙 조건이 읽는 fact의 값이 null이면 그 조건은 성립하지 않습니다. 규칙 표의 「값이 들어 있음」이 이 점을 이용한 조건입니다. 규칙 3, 4, 5는 금액과 환불 횟수도 >= 0 같은 조건으로 함께 따지기 때문에, 값이 null인 호출은 이 규칙들에 걸리지 않고 HUMAN_REVIEW로 갑니다. 규칙 3에 금액을 따지는 조건이 없다면, 금액이 null인 호출은 규칙 2의 refundAmountUsd > 60에도 걸리지 않으니 확률이 0.96일 때 자동 환불될 수 있습니다. 다만 규칙이 읽는 fact의 키를 아예 빼면 P-015 오류로 호출 전체가 실패하니 키는 남겨 두세요.
모델의 숫자를 fact로 받아 규칙이 결정하는 구조는 에이전트의 도구 호출을 승인할 때도 같습니다. AI 에이전트 액션 승인에서 다뤘습니다.
임계값이나 모델이 바뀔 때
임계값을 바꿀 때
가령 검토팀의 판정을 한 달 뒤에 세어 보니, 0.85에서 0.90 사이를 받은 환불 요청의 92%가 실제 불량이었습니다. 기대 손실 공식에 0.92를 넣으면 $60 환불의 기대 손실은 0.08 × $60 = $4.80입니다. 검토 비용 $6보다 작으므로 이 구간은 자동으로 환불하는 편이 더 효율적입니다. 규칙 3의 임계값을 0.9에서 0.85로 내리는 순서는 이렇습니다.
- 지금 실제 호출을 처리하는 운영 버전 v1을 복제해 초안을 만듭니다. 초안은 트래픽을 받지 않으니 고쳐도 고객이 받는 결정은 달라지지 않습니다.
- 초안에서 규칙 3의 0.9를 0.85로 고칩니다.
- 저장된 호출을 초안으로 다시 돌려 보는 의사결정 재실행(Decision Replay)을 시작합니다. LexQ 콘솔(웹 화면)의 「후보 버전」 칸에서 초안을 고르고 기간을 정하면, 그 기간의 호출 가운데 결정이 「기준 (저장된 결정)」과 달라진 건의 수가 나옵니다.
- 달라진 결정이 예상한 범위 안에 있는지 확인합니다.
- 초안을 발행해 규칙을 더 고칠 수 없게 잠그고, 배포해서 실제 호출을 받게 합니다. 배포한 뒤 문제가 생기면 임계값이 0.9인 v1으로 롤백합니다.
재실행은 호출 때 저장해 둔 확률을 읽을 뿐, OpenAI나 TypeSafe를 한 번도 부르지 않습니다. 값이 흔들릴 일도 없어서, 호출마다 숫자가 달라져 기준과 후보 버전의 결정이 갈리는 일은 없습니다. 저장된 확률이 0.87인 $60 환불 요청이라면 v1에서는 HUMAN_REVIEW이고 초안에서는 AUTO_REFUND입니다.
초안에서 규칙 3만 고쳤다면 달라진 결정은 모두 HUMAN_REVIEW에서 AUTO_REFUND로 바뀐 건이고, 이 호출들은 모두 제조 불량 확률이 0.85 이상 0.90 미만, 금액이 $15 초과 $60 이하, 최근 90일 환불이 3건 미만입니다. 금액이 $15 이하인 호출은 규칙 4가 이 확률에서 이미 자동으로 환불하고 있었고, $60 초과나 환불 3건 이상인 호출은 규칙 2와 규칙 1이 여전히 사람 검토로 보내서 결정이 바뀌지 않습니다. 범위 밖에서 바뀐 결정이 나오면 초안에서 규칙 3 외에 고친 곳이 있는지 보고, 고른 기간에 어떤 호출이 들어 있는지도 보세요. 실패한 LexQ 호출이나 요청 여러 건을 한 번에 보내는 배치 호출, v1 말고 다른 버전이 낸 결정이 섞였을 수 있습니다. 실패한 호출에는 비교할 저장된 결정이 없고, 배치 호출에는 재실행이 읽을 입력값이 저장되지 않습니다.
결과에는 바뀐 호출 가운데 최대 100건이 traceId로 나오고, 확률과 금액은 그 호출의 결정 기록을 열어야 보입니다. 재실행은 기본으로 기간 안의 최근 1,000건만 읽으니, 호출이 더 많으면 최대 레코드 수(maxRecords)를 올리세요. 재실행 결과를 읽는 자세한 방법은 배포 전에 바뀔 결정을 의사결정 재실행으로 세기를 보세요.
모델을 바꿀 때
0.85는 한 모델이 낸 숫자를 받은 요청에서 실제 불량을 세어 정한 임계값이라, 모델이 바뀌면 다시 세야 합니다. TypeSafe는 별칭 jev-latest가 가리키는 모델이 릴리스마다 바뀌므로 코드를 고치지 않아도 답이 달라질 수 있다고 경고합니다. 임계값을 특정 버전에 맞췄다면 그 버전 ID를 쓰라고도 안내합니다. OpenAI의 모델 페이지는 모델의 특정 버전을 가리키는 스냅샷(snapshot)을 쓰면 버전을 고정할 수 있다고 적었지만, GPT-6 Luna의 스냅샷 목록에는 gpt-6-luna 하나만 있습니다.
probabilityModel을 fact로 보내면 모델 응답의 model 값이 결정마다 저장되어, 어떤 숫자를 어느 모델 버전이 냈는지 가릴 수 있습니다. 이름은 그대로인데 모델이 바뀌면 probabilityModel로는 알 수 없으니, 저장된 숫자에서 0.9 이상을 받는 요청의 비율을 따로 지켜보다가 어느 주에 갑자기 달라지면 모델이 바뀌지 않았는지 확인해 보세요.
재실행은 저장된 숫자를 읽을 뿐 모델을 부르지 않으므로, 저장된 호출에 옛 모델의 숫자만 들어 있으면 새 모델을 시험하지 못합니다. 이때는 입력값을 담은 파일을 올려 원하는 버전으로 돌리고 결과를 세는 변경 영향 시뮬레이션(Impact Simulation)을 씁니다.
- 지난달 고객 메시지를 새 모델에 다시 보내 확률을 받습니다.
- 열 이름이 fact 이름과 같은 CSV 파일을 만듭니다. 답이 없는 건은 칸을 비워 두면 LexQ가
null로 읽습니다. - 파일을 올리고, 시뮬레이션을 돌릴 버전을 고르는 대상 버전 칸에 운영 버전을 넣고 돌립니다. 규칙별 통계의 매칭 건수에서 규칙마다 결정을 낸 건수를 볼 수 있습니다.
- 임계값을 바꾼 초안을 대상 버전으로 골라 같은 파일을 한 번 더 돌리고, 두 번의 규칙별 통계를 맞대 봅니다.
파일은 아래처럼 만듭니다. 마지막 줄은 모델이 답하지 않은 건입니다.
defectProbability,probabilityModel,refundAmountUsd,refundsLast90Days
0.91,jev-1.14.0,42.50,0
0.62,jev-1.14.0,60,0
,jev-1.14.0,25,1
LexQ 콘솔은 「규칙별 통계 포함」을 기본으로 켜 두지만, API로 돌릴 때는 includeRuleStats를 true로 줘야 합니다. 파일이 1,000행을 넘으면 최대 레코드 수를 올려야 합니다. 두 기능의 차이는 의사결정 재실행과 변경 영향 시뮬레이션에 정리해 두었습니다.
두 모델을 함께 쓴다면 규칙 조건에 probabilityModel을 넣어, gpt-6-luna의 숫자에는 0.85를, jev-1.14.0의 숫자에는 0.9를 쓰는 식으로 모델마다 임계값을 따로 둘 수 있습니다.
Decisions API와 Jev를 비교할 때
한 모델의 0.62가 다른 모델의 0.62와 같은 뜻이라는 보장은 없습니다. 상점의 환불 요청에 어느 모델이 맞는지 비교하려면 한동안 두 모델에 함께 물어보면 됩니다.
한 달 동안 환불 요청마다 두 모델에 같은 질문을 합니다. 지금 쓰는 모델의 답은 defectProbability로, 시험할 모델의 답은 shadowDefectProbability로 한 호출에 함께 담아, 배치 호출로 묶지 않고 단건 호출로 보냅니다. 운영 규칙은 shadowDefectProbability를 읽지 않으니 고객이 받는 결정은 그대로이고, LexQ는 단건 호출의 fact를 모두 저장하므로 시험할 모델의 숫자가 결정 기록에 쌓입니다.
한 달이 지나면 운영 버전을 복제해 규칙 3, 4, 5가 shadowDefectProbability를 읽게 고치고 그 기간을 재실행합니다. 바뀐 결정을 세면 시험한 모델의 숫자를 썼을 때 몇 건이 달라졌을지 나옵니다.
바뀐 건 가운데 두 모델 중 하나의 숫자가 없는 호출은 빼고, 남은 건을 검토팀의 판정과 맞대 보세요. 숫자가 null이면 모델의 판단과 상관없이 결정이 바뀔 수 있기 때문입니다. 검토팀이 판정한 것은 사람 검토로 간 요청뿐이라, 이 비교로는 전체 환불 요청의 일부만 확인할 수 있습니다.
이렇게 비교하는 한 달 동안은 환불 요청마다 모델을 두 번 부르는 비용이 듭니다. 실제 고객에게 시험하려면 복제해 고친 버전을 발행하고 A/B 테스트를 시작합니다. 요청마다 facts 옆의 context.trafficKey에 고객 ID처럼 바뀌지 않는 값을 넣으세요. LexQ는 이 값으로 트래픽을 나눠서 같은 고객의 요청을 늘 같은 버전으로 보내고, 이 값이 없는 요청은 테스트 버전으로 보내지 않습니다.
LexQ가 필요 없는 경우
사람이 모든 환불을 결정하고 모델의 숫자는 검토 대기열을 정렬하는 데만 쓴다면 관리할 임계값이 없습니다. 숫자로 정렬하면 됩니다.
환불 요청이 한 달에 수십 건뿐이라면 재실행으로 알 수 있는 것이 적습니다. 0.85와 0.90 사이에 든 요청이 몇 건 되지 않아서, 임계값을 바꾼 결과를 세어도 판단 근거가 되지 못합니다. 코드의 상수와 로그로 시작하고, 요청이 늘면 LexQ로 옮기세요.