AI 노트 02 / 06
환각과 조용한 실패를 코드로 막는다
형식만 맞는 빈 응답, 기동마다 흔들리는 프롬프트 지문, 문장으로는 맞아 보이던 지표 정의 — 스키마나 합의가 아니라 코드 검증과 데이터 검산으로 잡은 사례들입니다.
- LLM
- 결정론
- 검산
LLM 출력의 문제는 틀리는 것보다 '틀렸는데 아무것도 깨지지 않는 것'입니다. 아래 네 사례는 전부 화면이 정상으로 보이는 상태에서 발견됐고, 그래서 규칙이 하나씩 남았습니다.
사례
02-1
형식은 맞는데 내용이 빈 응답이 '성공'으로 판정되던 결함
- 문제
- AI 진단 배지가 'AI 분석'으로 바뀌었는데 화면은 빈 껍데기였습니다. 받은 응답은 headline이 placeholder, metric과 value가 빈 문자열인 형태였습니다.
- 설계
- JSON 스키마의 required는 키의 존재만 보장하고 빈 문자열·빈 배열도 통과시킵니다. 스키마 위에 세 겹을 더했습니다 — 재료 통제(프롬프트에 들어갈 숫자는 코드가 계산한다) → 코드 게이트(표본 5건 미만이면 호출하지 않는다) → 응답 검증(파싱 단계에서 빈 값을 거부한다). LLM은 계산 결과를 문장으로 옮기는 역할만 맡습니다.
- 결과
- 빈 응답은 실패로 분류돼 규칙 기반 폴백이 표시됩니다. 부가 원인으로 모델 effort를 낮춰 사고 토큰이 0이 된 것이 빈 응답을 유발했음을 확인했습니다.
02-2
같은 입력인데 프롬프트 지문이 흔들린다
- 문제
- 저장된 진단을 재사용해야 하는데 매번 폴백이 표시됐습니다. 폴백이 정상 동작해서 아무것도 깨지지 않았고, 그래서 한참 눈치채지 못했습니다.
- 설계
- 재사용 판정용 SHA-256 지문이 JVM 기동마다 달라졌습니다. Map.copyOf가 돌려주는 불변 맵의 순회 순서가 기동 시 nanoTime으로 뽑는 SALT에 좌우되기 때문입니다. '실제로 얼마나 흔들리는가'를 JVM 30회 기동으로 실측한 뒤 enum 선언 순서로 순회를 고정하고 회귀 테스트를 붙였습니다.
- 결과
- 항목 4개 컬렉션에서 7가지 순서 관측. 같은 원인이 2회, 정렬 tiebreaker 누락으로 같은 규칙의 다른 표면이 1회 더 발견돼 '프롬프트에 들어가는 컬렉션은 순회 순서를 코드로 고정한다'를 규칙으로 승격했습니다. 저장분을 버릴 때 지문 불일치를 로그에 남기도록 바꿨습니다.
02-3
합의는 증거가 아니다 — 지표 정의를 데이터로 검산
- 문제
- 밀집 구간 정의 문답에서 '간격 4일 이하 경기가 2회 이상 연속'이 추천되고 수용 직전이었습니다. 문장으로 읽으면 자연스러웠고 반론이 없었습니다.
- 설계
- 실제 한 시즌 52경기로 추천안과 대안(임의의 14일 창 안 5경기 이상, 슬라이딩 윈도우)을 각각 계산했습니다. 지표의 목적이 '전 대회 일정이 리그 단독보다 얼마나 빡빡한가'이므로, 리그 단독에서도 밀집을 검출하는 정의는 변별력이 없습니다.
- 결과
- 추천안 전 대회 8구간·리그만 3구간, 대안 3구간·0구간. 대안을 채택해 응답 계약에 승격(14일·5경기는 파라미터)하고 인수 테스트에 수치를 고정했습니다. '숫자를 만드는 규칙은 문장이 아니라 결과표로 승인한다'가 규칙으로 남았습니다.
02-4
회사 코드베이스 — 정답을 참조 구현의 파일:라인으로 고정
- 문제
- 'AI야 이 페이지 테스트 짜줘'는 AI가 현재 코드를 정답으로 삼게 만듭니다. 현재 코드가 틀렸으면 틀린 테스트가 통과합니다. 세션이 끝나면 알아낸 함정도 사라져 다음 세션이 같은 함정을 다시 밟았습니다.
- 설계
- 테스트를 짜기 전에 정답의 출처를 참조 구현(모바일 앱)의 파일:라인으로 문서에 먼저 고정하고, AI에게 인용을 강제하고 추측을 금지했습니다. 세션을 넘어 유지할 지식은 프로젝트 메모리(CLAUDE.md)에 Truth Source·Invariants·Known Trap·Fix 형식으로 적었습니다.
- 결과
- 77개 페이지 시나리오 약 700건이 참조 구현 인용을 달고 작성됐습니다. 두 레이어의 결제 금액이 달랐던 사례는 저장소 4개를 교차 검증해 원인 5종(페이로드 필드 누락·서버 반올림·설정 미적용·메모이제이션 의존성 누락·참조 구현과의 패턴 불일치)을 동시에 정합했습니다.
남은 규칙
- 01스키마는 형식만 본다. 내용의 비어 있음은 코드가 거부한다.
- 02숫자는 코드가 만들고, LLM은 문장으로 옮기기만 한다.
- 03프롬프트에 들어가는 컬렉션은 순회 순서를 코드로 고정한다.
- 04폴백이 있는 실패는 반드시 로그를 남긴다 — 잘 동작하는 폴백이 결함을 숨긴다.
- 05숫자를 만드는 규칙은 문장 합의가 아니라 실제 데이터 결과표로 승인한다.
- 06테스트의 정답은 현재 코드가 아니라 참조 구현의 파일:라인이다.