LLM이 숫자를 지어내지 못하게 만드는 법 — 계산을 모델 밖으로 내보냈다
클라우드 청구서를 받아 낭비 항목과 절감액이 담긴 진단 리포트를 만드는 시스템을 짰다. 처음엔 단순하게 접근했다. CSV를 그대로 LLM에 넣고 “분석해서 리포트로 써줘”라고 했다.
결과물은 멀쩡해 보였다. 문장이 매끄럽고 구조가 깔끔했고 숫자가 구체적이었다.
그리고 그 숫자 중 일부는 청구서에 없는 숫자였다.
왜 이게 다른 종류의 버그인가
코드 버그는 터진다. 예외가 나거나 테스트가 깨지거나 최소한 로그에 남는다.
숫자 환각은 안 터진다. 그럴싸한 숫자로 조용히 나온다.
“유휴 리소스 정리로 월 480만 원 절감 가능”이라고 적힌 리포트를 보면, 그게 청구서에서 계산된 480인지 문장의 리듬에 맞게 생성된 480인지 읽는 쪽에서 구별할 방법이 없다. 쓴 쪽에서도 없다. 그래서 이건 품질 문제가 아니라 확인할 방법이 없다는 문제다.
여기서 흔한 처방이 프롬프트 강화다. “추측하지 마라”, “원본에 없는 값을 만들지 마라”, “근거를 반드시 인용하라”. 나도 했다. 효과는 있었다. 빈도가 줄었다.
빈도가 줄었다는 게 문제다. 지켰는지 확인할 방법이 없는 제약은 제약이 아니다. 프롬프트는 계약이 아니라 부탁이다. 부탁으로 돌아가는 파이프라인은 언젠가 틀린 숫자를 고객에게 보낸다. 그 리포트가 상품이면 그 한 번으로 끝난다.
그래서 방향을 바꿨다. LLM이 숫자를 안 지어내게 설득하는 대신, 숫자가 지나가는 경로에서 LLM을 내렸다.
3단 분리
원칙은 한 줄로 줄었다.
숫자는 스크립트가, 판정은 룰셋이, 서술만 LLM이.
비용 CSV
│
▼
[1단 파서] ─── 집계만. 판정하지 않는다
│ 출력: aggregates.json
▼
[2단 룰셋] ─── 31개 룰 + 고정 상수표. 판정과 등급
│
▼
[3단 LLM] ─── 서술만. 없는 숫자는 쓸 재료가 없다
│
▼
[4단 게이트] ─── 산술 대조. 안 맞으면 발행 중단
1단 — 파서는 집계만 하고 아무 판단도 하지 않는다
580줄쯤 되는 파이썬 스크립트 하나다. 표준 라이브러리만 쓴다.
AWS CUR, Cost Explorer 요약, Azure Cost Export, FOCUS 네 포맷을 헤더로 감지하고
.csv / .csv.gz / .zip 안의 csv를 스트리밍으로 읽는다.
출력은 집계 JSON 하나뿐이다.
{
"meta": { "formats": [...], "providers": [...], "rows": 24,
"period": {...}, "matrix_only": false },
"totals": { "gross_cost": 5136.1, "monthly_avg": 1672.03,
"credits": -50.0, "tax": 120.0 },
"services": { "AmazonEC2": 2241.1, "AmazonS3": 975.0, ... },
"usage_types_top": { "APN2-EBS:VolumeUsage.gp2": 400.0, ... }
}
이 단계에서 지킨 규칙이 둘이다.
판정을 하지 않는다. 파서는 “gp2 볼륨에 400달러가 찍혔다”까지만 안다. 그게 낭비인지, 얼마를 아낄 수 있는지는 모른다. 알려고 하면 판정 로직이 파서로 새어 들어오고, 그러면 어디서 무슨 판단이 일어났는지 추적이 끊긴다.
원본에 없는 값은 만들지 않고 null로 남긴다.
이게 생각보다 중요했다. 계산이 안 되는 필드에 0을 넣으면 편하다. 하류에서 분기가 줄고 템플릿이 안 깨진다. 대신 0은 사실로 전파된다. “미사용 EIP 비용 0원”은 “미사용 EIP가 없다”로 읽히지만 실제로는 “그 정보가 이 파일에 없다”였다.
null은 “모른다”를 하류까지 그대로 들고 간다. 리포트는 그 항목을 “판단 불가”로
표시하거나 아예 빼고, 절감액 합계에서 제외한다. 모른다는 정보가 사라지지 않는 게
파이프라인 전체의 신뢰를 지탱한다.
2단 — 판정은 룰셋과 고정 상수가 한다
31개 룰이 마크다운 문서 하나에 있다. 유휴 / 과다 프로비저닝 / 스토리지·네트워크 / 약정·거버넌스 네 영역이다. 룰마다 세 가지가 붙는다.
신뢰도 3단계가 리포트의 척추다.
| 단계 | 뜻 | 절감액 처리 |
|---|---|---|
| 확정 | 청구 데이터만으로 사실이 확정된다 | 단일 수치로 표기 |
| 추정 | 청구 데이터 기반 보수 추정 | 보수 계수 적용 + 각주 필수 |
| 정밀진단 | 사용량 메트릭·구성 조회가 필요 | 절감액 합계에 넣지 않는다 |
미사용 공인 IP는 확정이다. 붙어 있지 않다는 게 청구 라인에 그대로 있다. CPU 사용률 3% 미만인 유휴 인스턴스는 정밀진단이다. 청구서에 CPU 사용률이 없다. 이 둘을 같은 합계에 넣는 순간 그 합계는 못 쓰는 숫자가 된다.
보수 계수는 고정 상수표다.
| 코드 | 의미 | 계수 |
|---|---|---|
K_SNAP |
고아 스냅샷 정리 가능 비율 | 40% |
K_VOL |
미연결 볼륨 의심 비율 | 15% |
K_GEN |
세대 교체 평균 절감 | 20% |
K_GP3 |
gp2→gp3 단가 차 | 20% |
K_SCHED |
비프로덕션 스케줄링 절감 (168h→주 60h의 보수치) | 50% |
K_NAT |
NAT 트래픽 중 엔드포인트 우회 가능분 | 60% |
K_SP |
약정 할인율 (1yr NoUpfront Compute SP 보수치) | 27% |
계수를 표에 못 박고 “임의 변경 금지”를 붙인 이유가 있다.
계수를 고를 자유가 있으면, 결론에 맞는 계수가 선택된다. 사람도 그렇고 LLM도 그렇다. 절감액이 초라해 보이는 리포트를 앞에 두고 “이 케이스는 스케줄링 절감이 70%쯤 되겠지”라고 판단할 여지를 주면, 그 판단은 매번 위쪽으로 움직인다. 악의가 아니라 구조가 그렇다. 그래서 판정의 자유도를 0으로 만들었다.
등급 산정식도 고정이다.
월 낭비 추정액 = Σ(확정) + Σ(추정) # 정밀진단은 제외
낭비율 = 월 낭비 추정액 / totals.monthly_avg
등급 = <10% A · <18% B · <27% C · <36% D · ≥36% F
리포트의 결론(등급)이 산술로 결정된다. 서술이 등급을 바꿀 수 없다.
3단 — LLM은 서술만 한다
여기까지 오면 LLM에게 남은 일은 문장이다. 집계 JSON과 판정 결과를 받아 왜 이 항목이 1순위인지, 어떤 순서로 손대야 하는지, 무엇이 위험한지를 쓴다.
LLM을 못 믿어서 이렇게 한 게 아니다. 우선순위를 설명하고 맥락을 붙이고 읽는 사람에게 맞는 톤으로 쓰는 일은 LLM이 스크립트보다 훨씬 잘한다. 그래서 그 일만 줬다.
그리고 이 구조에서 LLM은 없는 숫자를 쓸 재료가 없다. 집계에 없는 값은 근거 키를 댈 수 없다. 근거 키가 없는 항목은 다음 단계에서 걸린다.
4단 — 발행 게이트: 출력을 산술로 잠근다
마지막이 실질적으로 가장 큰 몫을 한다. 리포트를 내보내기 전에 대조식을 통과해야 한다.
☐ Top 10 절감액의 합 == 헤드라인 낭비액 (정밀진단 제외 기준)
☐ 모든 확정·추정 항목에 evidence(집계 키·값)가 있다
☐ 정밀진단 항목이 절감액 합산에 섞이지 않았다
☐ 등급과 낭비율이 산정식과 일치한다
☐ 산정 기준 각주 3종(보수 추정·약정 전제·데이터 파기)이 있다
첫 줄이 핵심이다. 헤드라인 숫자와 항목별 숫자의 합이 일치하는지 더해본다. LLM이 헤드라인을 조금 크게 쓰거나 항목 하나를 빠뜨리면 여기서 안 맞는다. 프롬프트로 “정확히 써라”라고 부탁하는 것과, 안 맞으면 발행이 멈추는 것은 다른 종류의 보장이다.
부수 효과로 얻은 것: 회귀 테스트가 가능해졌다
의도하지 않았는데 이게 제일 컸다.
파서 출력이 결정적이므로 골든 파일과 그냥 diff로 비교된다.
python3 scripts/parse_bill.py tests/aws_cur.csv -o /tmp/o.json
diff /tmp/o.json tests/out_aws.json
포맷별로 골든 출력을 두고 파서를 고칠 때마다 돌린다. LLM이 끼는 파이프라인에서 회귀 테스트가 돌아간다는 건 큰 이득이다. 숫자 경로가 결정적이면 그 경로는 평범한 소프트웨어처럼 테스트된다. LLM 단계는 평가가 어렵지만, 애초에 그 단계엔 틀려도 조용한 숫자가 없다.
대가
공짜가 아니다.
룰셋 유지비가 생긴다. 새 서비스, 새 요금제, 새 인스턴스 패밀리가 나오면 사람이 룰을 추가해야 한다. 여기서 “룰도 LLM이 만들게 하면?” 하는 유혹이 오는데, 그러면 고정 상수의 의미가 사라지고 원점이다. 룰 추가는 사람이 하고, 추가된 룰은 골든 테스트로 고정한다.
적용 범위가 좁다. 이 구조는 답이 숫자이고 원본에 근거가 있는 도메인에만 맞다. 청구·재무 분석, 용량 산정, 성능 리포트, 정산. 반대로 열린 질문에 답하거나 아이디어를 내는 작업에 이 구조를 씌우면 LLM을 쓰는 이유가 없어진다.
다른 데 옮길 때 쓸 체크리스트
숫자가 결과물에 들어가는 LLM 파이프라인을 만든다면 이 네 개만 봐도 대부분 막힌다.
- 숫자가 지나가는 경로에서 LLM을 내려라. 집계는 코드가 한다.
- 모르는 값은 0이 아니라
null. 0은 사실로 전파되고null은 모른다를 전파한다. - 판정 상수를 코드에 못 박아라. 프롬프트에 둔 계수는 결론에 맞춰 움직인다.
- 출력에 검산식을 붙이고 게이트로 만들어라. 통과 못 하면 발행이 멈춰야 한다. 프롬프트의 부탁은 보장이 아니다.
환각은 “하지 말라”고 말해서 막히지 않았다. 환각할 수 없는 자리로 옮겨야 막혔다.
이 구조로 돌아가는 판정 룰 31개 전문은 따로 공개해뒀다. 판정 신호와 보수 계수, 등급 산정식까지 그대로 적었다.
산출물 실물이 궁금하다면 샘플 성적표를 올려뒀다 (가상 회사 데이터로 만든 실제 산출물이다).
비용 CSV 한 장으로 성적표를 만들어 드리는 실험도 하고 있습니다 — 룰셋 검증용 실데이터가 필요해서입니다. 계정 권한 불필요, 파일은 30일 후 파기: 무료 진단 신청