AI 운영 자동화에서 모델보다 검증기를 먼저 만들어야 하는 이유
목차
대사에서 안 맞는 건이 잡히면 상담원이 고객에게 안내 문구를 써야 합니다(대사 = 두 기록을 하루치씩 대조해 안 맞는 건을 찾는 작업). 그 초안 쓰기를 모델에게 맡기되, 모델이 지어낸 숫자가 섞이면 초안을 버리게 만들었습니다.
그래서 검증기를 먼저 만들었습니다. 그런데 실제 결제 데이터를 넣자 가장 확신이 높은 건의 초안이 검증에서 탈락했습니다. 270원은 모델이 지어낸 숫자가 아니라 제가 만든 분류기가 계산한 값이었습니다. 문제는 모델이 아니라 검증기의 계약이었습니다.
이 글은 그 계약을 고치고, 실제 모델을 붙여 결함 셋을 더 찾은 기록입니다. 검증 통과율은 7/12(58%)에서 12/12(100%)가 됐고, 대가로 생성 지연이 5,233ms → 8,825ms로 늘었습니다.
결론부터
- 모델보다 먼저 숫자와 날짜의 출처를 검증하는 계약을 만들었습니다.
- 검증기가 계산기의 값을 반려한 것은 모델 문제가 아니라, 사실 묶음에 허용된 출처를 빠뜨린 설계 문제였습니다.
- 검증 통과율은 7/12에서 12/12로 올라갔지만 생성 지연은 5,233ms에서 8,825ms로 늘었습니다. 검증은 공짜가 아니었습니다.
검증기가 보장하는 범위도 제한했습니다. 숫자가 출처에 존재하는지는 확인하지만, 그 숫자가 문장 안에서 올바른 주장에 사용됐는지까지 보장하지는 않습니다.
모델을 고르기 전에 무엇을 실패로 볼지 먼저 정했다
이 프로젝트가 금융 규제를 적용받는 건 아닙니다. 다만 금융사 내부 업무망 배치를 가정하면 결제 데이터를 외부 생성형 AI API로 못 보내서, 그 제약을 스스로 걸었습니다. 그래서 모델 대신 포트와 숫자 검증기를 먼저 만들었습니다. 포트(DraftPort)는 초안 자리만 인터페이스로 뚫어 구현을 갈아끼우게 한 것이고, 검증기(NumericProvenanceGuard)는 초안의 숫자가 코드가 낸 값인지 대조합니다. 코드가 낸 숫자만 쓰고 모델이 지어낸 숫자는 버린다가 계약입니다.
OrderTimeline + CauseSuggestion ← 8편에서 만든 것 (결정적 조립) ↓ FactPack ← 코드가 만든 사실 문장 + 허용 금액·날짜 집합 ↓ DraftPort ← 템플릿이든 모델이든 여기서 갈립니다 ↓ NumericProvenanceGuard ← 초안의 금액·날짜를 FactPack과 대조 ↓ 상담원이 검토·수정 후 발송기본 구현은 외부 호출 없는 템플릿이고, 임시물로 두지 않았습니다. 모델이 붙었을 때의 기준선이자 검증에 걸렸을 때의 폴백입니다. 기준선 없이는 좋아 보이는 것과 좋아진 것을 구분할 수 없습니다.
순서를 모델보다 잣대로 잡은 건 되돌린 전례가 있어서입니다. Klarna는 AI 상담으로 해결시간을 11분에서 2분으로 줄였다가 되돌렸고, CEO가 인정한 원인은 기술이 아니었습니다.
비용이 지나치게 지배적인 평가 요소였고, 그래서 품질이 낮아졌다
모델을 고른 뒤에 잣대를 만들면 잣대가 그 모델에 맞춰 휩니다.
검증기가 보장하는 건 출처뿐입니다. 초안의 금액·날짜가 코드가 낸 값인가는 보되, 맞는 주장에 쓰였는지는 못 봅니다. 외부 기록에 100,000원이 있으면 “청구 금액은 그대로 유지됩니다”도 통과합니다. 그래서 이름도 NumberGuard에서 NumericProvenanceGuard로 바꿨습니다. “숫자를 지킨다”로 읽히면 보장보다 넓어집니다.
차액 3,000원이 확인되었습니다.
차액 3,200원이 확인되었습니다.
초안의 금액·날짜를 전부 뽑아 FactPack과 대조해 하나라도 없으면 초안을 버립니다. 실패한 초안은 “참고용”으로도 안 보여줍니다. 사람은 눈앞의 문장에 끌려갑니다. 그 위층(“그 숫자가 맞는 주장에 쓰였는가”)은 채점 기준이 맡는데 12편에서 다룹니다.
검사 범위를 어디까지 잡을지가 갈렸습니다. 맨 숫자까지 넓히면 “항목 2개”, “3일 이내” 같은 정상 초안이 대량 반려되고, 금액·날짜만 보면 원 없는 지어낸 수치가 샐 수 있습니다.
| 허용 목록 | 넓히면 | 좁히면 |
|---|---|---|
| 맨 숫자까지 | 정상 초안이 대량 반려 | — |
| 금액·날짜만 (택함) | — | 원 없는 지어낸 수치가 샐 수 있음 |
초안이 실제로 주장하는 사실이 금액과 날짜라서 이 경계로 잡았고, 한계는 모델이 붙은 뒤 실측해 조정할 값으로 남겼습니다.
잣대가 잣대인지도 확인했습니다. 통과가 당연하면 하네스가 도는지 모르니, 값을 지어내는 가짜 구현을 같이 돌렸습니다.
[잣대 검증] template 초안 3/4 (75%) 사실정합 3/3 (100%) fixture:invents-amount 초안 3/4 (75%) 사실정합 0/3 (0%) fixture:invents-date 초안 3/4 (75%) 사실정합 0/3 (0%)100% 대 0%로 갈립니다. 전부 통과시키는 하네스는 잣대가 아닙니다. 이 상태는 AI를 안 붙인 게 아니라, 붙일 자리를 만들어놓고 모델 하나를 비워둔 것입니다.
실제 데이터를 넣자 검증기가 진짜 270원을 거짓말이라고 했다
이 글의 수치는 통계적 성능 평가가 아닙니다. 표본은 대사 예외 4건이고, 그 4건으로 프롬프트·루브릭·가점을 고쳤습니다. 이미 개발 세트라 홀드아웃이 아닙니다. 아래 숫자는 전부 그 4건에서 돌린 탐색 실행의 결과로 읽어야 합니다.
잣대만 만들고 재지 않으면 알 수 없어서 앱을 띄워 실제 API로 대사 불일치를 만들었습니다(주문 생성 → 결제 승인 → 부분취소 → PG 정산 업로드 → 대사 실행). 네 건 중 2건이 반려됐고, 하필 최악의 두 건이었습니다.
id=1 FEE_CALCULATION_DIFF (DECISIVE) verified=false rejected=['출처에 없는 금액: 270원']id=3 SUSPECTED_TAMPERING (WEAK) verified=false rejected=['출처에 없는 금액: 8,888원', '출처에 없는 금액: 270원']FEE_CALCULATION_DIFF: 확신이 가장 높은 규칙. 차액이 수수료율과 원 단위까지 맞는 건SUSPECTED_TAMPERING: 초안이 가장 필요한 건. 근거가 “설명되지 않는다”뿐이라 사람이 처음부터 조사해야 합니다
반려된 8,888과 270은 타임라인 금액 필드가 아니라 분류기가 계산한 근거였습니다.
차액 8,888원이 수수료(270원)로도 취소로도 설명되지 않는다
허용 목록을 타임라인 금액으로만 만들어 그 근거를 반려하고 있었습니다. 계약을 “타임라인에 있는 숫자만”에서 “코드가 낸 숫자만 쓰고, 모델이 지어낸 숫자는 쓰지 마라” 로 고쳤습니다. 분류기도 코드라 그 근거의 숫자는 통과해야 맞습니다.
손으로 만든 입력으로 돌린 단위 테스트 7개는 전부 통과하고 있었습니다. FactPack에 내가 넣은 숫자만 있어 분류기가 계산한 값이 섞이지 않았습니다. 고친 뒤 4/4를 확인하고, “분류기 근거 금액은 통과한다”와 “근거에 없는 숫자는 여전히 걸린다” 두 회귀 테스트로 고정했습니다.
그다음 실제 모델을 붙였습니다. 붙이자마자 둘이 걸렸습니다. 추론 토큰이 켜져 한 건에 19초 걸리던 것이 5초대가 됐고, [쓸 수 있는 금액] 레이블을 모델이 잔액으로 읽어서 [인용해도 되는 금액]으로 바꿨습니다.
사용 가능한 금액은 10,000원과 270원입니다.
4건을 3번씩 12회 돌렸습니다. 12회 중 7회 통과(58%). 반려는 전부 2026-09-06으로, 에스크로 사실 문장의 코드가 만든 진짜 날짜인데 허용 날짜를 entry.at()(사건이 일어난 시각)으로만 모으고 문장이 말하는 다른 날짜를 빠뜨린 것이었습니다. 고친 뒤 12회 전부 통과했습니다.
결함이 어느 층에서 보이는지가 갈렸습니다.
| # | 결함 | 발견 조건 |
|---|---|---|
| 1 | 분류기 근거 금액 반려 | 실데이터 + 템플릿 |
| 2 | 사실 문장 속 금액 반려 | 실제 모델만 |
| 3 | 사실 문장 속 날짜 반려 | 실제 모델 + 12회 반복만 |
셋의 뿌리는 구조화 필드만 모으고 문장 속 값을 빠뜨린 것입니다. 2번은 템플릿으로 절대 못 찾고, 3번은 모델이 매번 다르게 쓰니 12회를 돌려야 보였습니다.
테스트를 많이 돌린 게 아니라 입력의 현실성을 한 단계씩 높일 때마다 다른 결함이 드러났습니다.
통과율은 100%가 됐지만 읽어보니 쓸 만하지 않았습니다. 내부 용어가 샜고, 하필 어려운 건에서 약했습니다.
대사 AMOUNT_MISMATCH로 인해 … 주문 상태 PAID 확인 중입니다
고객이 물어본 “금액이 왜 다른가”를 한 마디도 말하지 않았습니다. 처방은 둘로 갈렸습니다. 내부 용어는 글로서리를 실어 출하 전 검수로, 어려운 건은 좋은 예와 나쁜 예를 나란히 주는 few-shot으로. 나쁜 예는 나쁘다고 이름 붙여 같이 줬습니다.
나쁜 초안 (이렇게 쓰지 마십시오): 2026-08-30 결제 승인 완료 및 주문 상태 PAID 확인 중입니다.나쁜 이유: (1) 고객이 물어본 <금액이 왜 다른가>를 한 마디도 말하지 않았습니다. (2) PAID 같은 내부 코드를 그대로 썼습니다.지어낸 숫자는 버렸지만, 내부 용어는 버리지 않았습니다. 지어낸 숫자는 사실이 틀려 상담원이 고칠 수 없으니 버리고, 내부 용어 누출은 고칠 수 있으니 표시하고 셉니다. 같은 잣대로 버리면 쓸 만한 초안까지 사라집니다.
| 지표 | 전 | 후 | 무엇을 바꾼 뒤 |
|---|---|---|---|
| 검증 통과율 | 7/12 = 58% | 12/12 = 100% | 날짜 추출 수정 |
| 용어 누출 | 발생 | 0 / 12 = 0% | 글로서리를 프롬프트에 싣고 출하 전 용어 검수 |
| 지연 중앙값 | 5,233ms | 8,825ms (+68%) | 글로서리 22줄 + few-shot 예시 3개 |
지연이 68% 늘었습니다. 사전 22줄과 예시 3개가 붙은 대가입니다. 섀도 모드는 비동기라 문제없지만 사람이 기다리는 경로면 8.8초는 길어서 문서에 그대로 적어뒀습니다. 어려운 건의 초안은 이렇게 바뀌었습니다.
2026-08-30 결제 승인 10,000원이 정상 확인됩니다.
결제사 정산 내역과의 금액 차이 8,888원 원인을 확인 중입니다.
확인이 완료되면 안내드리겠습니다.
few-shot 출력이 예시를 닮는 게 당연해서 예시에 없는 케이스 두 개로 확인했고, 둘 다 용어 누출 0이었습니다. 외부에만 있는 건의 어조는 검증기도 사전도 못 잡아 상담원 판단 영역으로 그대로 뒀습니다.
남은 구멍
- 만들고 나서 다시 보니 카드사 웹훅에서 온 분쟁 사유가 사실 문장 → 프롬프트까지 그대로 들어갔습니다. 인젝션은 예방이 아니라 봉쇄로 봤습니다. 위험한 문장을 판정하면 그 판정이 또 틀리니 내용은 검열하지 않고 구조만 지키고(개행·제어문자 제거, 200자 상한), 진짜 방어선은 권한(읽기 전용)입니다. 실제 인젝션으로 뚫리는지 재현은 안 했습니다. 이 편에서 유일하게 실측이 아니라 설계인 항목입니다.
- “상담 시간 N% 단축” 같은 숫자는 안 적었습니다. 자동화된 건 초안 생성·검증까지이고, 쓸 만한지는 사람이 판단해야 하는데 그 사람이 없습니다.
- 이 검증기를 두 번째 기능에 그대로 재사용한 기록은 받는 사람이 가드를 정하게 한 편에, 심판 모델로 품질을 재려다 실패한 기록은 심판 편향 편에 있습니다.
참고
- 규제: 전자금융감독규정시행세칙 개정(2026-04-20 시행) 보도 — 내부 업무망에서 별도 혁신금융서비스 심사 없이 SaaS를 쓸 수 있게 됐지만, 생성형 AI에 대해서는 이렇게 적혀 있었습니다.
SaaS에 이어 향후 생성형 AI 서비스 도입 등과 관련해서도 금융권과 적극적으로 소통하며 신속히 망분리 규제 예외가 적용될 수 있도록 추진할 계획
- Klarna: 초기 발표, CEO 품질 저하 인정 — Bloomberg, TechCrunch
- Uber Genie(도움률 48.9%, 13,000 엔지니어링 시간 절감): Uber 엔지니어링
- Meta 인시던트 대응(상위 5개 안에 42%): engineering.fb.com
- Zalando 사후분석(표면적 귀인오류 ~10%): Zalando 엔지니어링
- 프롬프트 인젝션은 봉쇄: OWASP 2026 LLM Top 10 (Help Net Security), Cybersecurity News
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.