AI 코드 리뷰가 환각(Hallucination)을 일으키는 이유와 이를 해결하는 두 가지 게이트
요약
AI 코드 리뷰 시 발생하는 환각(Hallucination) 문제를 해결하기 위한 두 가지 검증 게이트를 제안합니다. 첫 번째는 수정 전 결과의 실제 존재 여부를 검증하는 단계이며, 두 번째는 수정 사항이 원래의 발견 사항과 일치하는지 확인하여 회귀 오류를 방지하는 단계입니다.
핵심 포인트
- AI 리뷰어의 패턴 매칭 오류로 인한 가짜 버그 지적 문제 해결
- 게이트 1: 실제 코드와 대조하여 이슈의 존재 및 영향력을 재검증
- 게이트 2: 수정 사항이 의도 외의 변경을 일으키지 않는지 확인
- 검증기가 반박하는 방향으로 편향되어 불필요한 수정을 방지
AI 코드 리뷰는 신뢰성 문제를 안고 있으며, 이는 단순히 버그를 놓치는 문제가 아닙니다. 바로 버그를 '지어낸다'는 점입니다.
LLM(Large Language Model)을 사용하여 diff(차이점)를 분석해 본 적이 있다면 경험했을 것입니다. 세 줄 위에서 이미 방어 처리가 된 값에 대해 "가능한 null 역참조(null dereference)"라고 지적하거나, ORM(Object-Relational Mapping)이 이미 파라미터화(parameterize)를 수행하고 있는 코드에 대해 "SQL 인젝션(SQL injection)"이라고 말합니다. 발생할 수 없는 "경쟁 상태(race condition)"를 지적하기도 합니다. 그리고 더 심각한 것은, 전혀 문제가 없었던 코드를 "수정"한다며 멀쩡히 작동하는 코드를 자신만만하게 다시 작성한다는 점입니다. 그동안 진짜 버그는 소음 속에 조용히 숨어 있습니다.
문제는 지능이 아닙니다. 대부분의 AI 리뷰어가 첫인상을 곧바로 판결로 보고한다는 점입니다. 모델은 diff를 읽고 "이것은 X처럼 보인다"라고 패턴 매칭을 한 뒤, X가 실제로 이 코드에서 도달 가능한지(reachable) 다시 확인하는 과정 없이 바로 결과(finding)를 내놓습니다. 인간은 두 번째 검토 단계("잠깐, 가격이 상위 단계에서 검증되었나?")를 거칩니다. 하지만 대부분의 AI 리뷰 파이프라인은 이 단계를 건너뜁니다.
여기 이러한 두 번째 검토 단계를 추가하는 두 가지 게이트(gate)와, 이들이 무엇을 잡아내는지 보여주는 스트레스 테스트가 있습니다.
게이트 1: 수정하기 전에 결과 검증하기 (환각 방지)
아이디어는 간단합니다. 별도의 단계에서 실제 코드와 대조하여 재확인하기 전까지는 어떤 결과도 수정 계획(fix plan)에 포함될 수 없도록 하는 것입니다.
감사자(auditors)가 결과를 생성한 후, 검증 단계(verification pass)에서 각 결과에 대해 세 가지 질문을 던집니다:
- 해당 이슈가 인용된 라인에 실제로 존재하는가?
- 그것이 변경된 코드에 있는 것인가, 아니면 diff 외부의 기존 코드에 있는 것인가?
- 명시된 영향이 실제인가, 아니면 이미 다른 곳에서 완화되었는가 — 상위 단계의 방어 코드, 이 시점 이전에 검증된 값, 혹은 다른 모듈에 정의된 설정 등?
핵심 설계 선택: 검증기가 반박하는 방향으로 편향되도록 합니다. 잘못 확인된 결과는 불필요한(때로는 해로운) 수정을 초래하지만, 잘못 폐기된 결과는 복구 비용이 저렴합니다. 따라서 증거가 명확하지 않을 때는 직감에 의존해 수정하지 말고, 해당 결과를 폐기하거나 사람에게 에스컬레이션(escalate)하십시오.
이 한 단계만으로도 환각으로 인한 결과의 대부분을 제거할 수 있습니다. 왜냐하면 환각은 "정확한 라인을 보여주고, 영향이 발생할 수 있음을 증명하라"는 요구 앞에서는 살아남기 어렵기 때문입니다.
Gate 2: 수정 사항이 발견 사항과 일치함을 증명 (anti-regression + provenance)
실제 버그를 잡아내는 것이 업무의 절반이라면, 나머지 절반은 수정 과정에서 새로운 버그를 도입하지 않는 것입니다.
두 가지 저비용 점검을 통해 이를 해결할 수 있습니다:
회귀 차이 (Regression diff): 수정을 적용한 후, 차이 분석 (differential pass)을 통해 수정 사항이 각 발견 사항의 의도 외에 다른 것을 변경하지 않았음을 확인합니다. 즉, 부수적인 부호 반전(sign flip), 기본값 드리프트(default-value drift), 또는 가드(guard)를 조용히 우회하는 새로운 경로가 생성되지 않았음을 보장합니다.
수정→발견 매핑 (Fix→finding mapping): 마지막 게이트에서 테이블을 생성합니다. 확인된 모든 발견 사항은 반드시 수정 사항과 매핑되어야 하며, 모든 변경 사항은 반드시 발견 사항과 매핑되어야 합니다. 수정되지 않은 고립된 발견 사항(orphan finding)이나, 아무런 근거 없이 발생한 유령 변경 사항(phantom change)이 있다면 반드시 수정을 강제해야 합니다.
출처 (Provenance)는 과소평가되어 있습니다. 각 라인이 왜 변경되었는지 지목할 수 없다면, 그 차이(diff)를 신뢰할 수 없습니다.
스트레스 테스트: 하나의 실제 버그, 세 개의 함정
말은 쉽습니다. 그래서 제가 스스로를 정직하게 유지하기 위해 설정한 실행 과정을 소개합니다. 저는 미묘하게 돈을 잃게 만드는 버그 하나와 세 개의 의도적인 오탐(false-positive) 함정을 심어놓은 아주 작은 포지션 사이저(position sizer)를 구축한 뒤, Claude Code를 위한 멀티 에이전트 감사 파이프라인인 CCA-Audit를 실행했습니다.
실제 버그 — 단위 오류:
BPS_PER_UNIT = 10_000 # 1.0 == 10_000 basis points
risk_budget = equity_usd * (risk_limit_bps / 100) # <-- 버그
per_unit_risk = price * (stop_distance_bps / BPS_PER_UNIT) # 정상
risk_limit_bps는 베이시스 포인트 (basis points) 단위입니다. 이를 100으로 나누면 퍼센트 (percent)로 취급하게 됩니다. 결과적으로 100배나 커지게 됩니다. 바로 다음 라인에서 형제 수량(sibling quantity)을 올바르게 변환(/ 10_000)하고 있기 때문에 불일치가 명확히 드러납니다. 하지만 테스트는 단순히 size > 0 인지만 확인했기 때문에 통과(green)되었습니다. 예시 입력값의 경우, 의도했던 $25k가 아닌 $100k 계좌에 $2.5M 명목 가치(notional)의 포지션이 잡히게 됩니다 (25:1 레버리지).
세 개의 함정 — 각각 오탐을 유도하도록 설계되었습니다:
a / b가 ZeroDivisionError처럼 보이는 반환을 하지만 b는 검증기(validator)에 의해 보호됩니다 (price > 0, stop ≥ 1).
동일한 나눗셈이지만, 가드가 다른 오프-diff 파일로 이동된 경우입니다 (리뷰어가 호출 그래프를 추적하는지, 아니면 단순히 플래그만 지정하는지?).
.get()을 사용하여 읽고 기본값이 없는 설정 키 — 이는 None일 수 있어 보이지만, 해당 키는 사전에 존재하는 설정 모듈에 정의되어 있습니다.
발생한 일:
수치 감사자(numeric auditor)가 단위 버그를 포착하고 피해 범위를 정량화했습니다.
버그, 보안 및 성능 감사자들은 각각 0으로 나누기 오류를 직시하며 이를 거부했고, 가드를 추적했습니다: "검증된 모든 요청에 대해 엄격하게 양수임 — 버그가 아님."
설정 트랩의 경우, 검증기가 설정 파일을 읽고 키가 존재함을 확인한 후 통과했으며 — 유령 같은 "누락된 키"는 없었습니다.
이후 검증 게이트(verification gate)는 실제 발견 사항을 확정하고, 네 가지를 하나의 근본 수정으로 중복 제거했으며, 심지어 한 감사자가 실수했던 과장된 영향 수치까지 수정했습니다.
여섯 개의 원시 발견 사항이 단 하나의 한 줄 수정(/ 100 → / BPS_PER_UNIT)으로 통합되었고, 테스트는 녹색 상태를 유지했으며, 최종 아키텍트 게이트가 커밋하기 전에 모든 발견 사항을 수정 사항에 매핑했습니다. 전체 실행 과정에서 환각(hallucination)은 전혀 없었고 — 저는 하나를 강제하기 위해 세 가지 방법을 시도해 보았습니다. 전체 실행 과정 — 모든 발견 사항, 판결, 편집되지 않은 에이전트 기록 — 은 저장소(repo)에 있습니다.
어디든 적용할 수 있는 교훈
특정 도구가 필요하지 않습니다. 어떤 AI 리뷰 환경을 구축했든지 간에:
‘발견(findings)’과 ‘수정(fixes)’ 사이에 검증 단계를 추가하세요. 각 발견 사항을 실제 코드를 기준으로 재추론하고, 이를 반박하는 방향으로 편향시키세요.
수정한 내용(diff)을 확인하여, 그것이 오직 발견된 문제(finding)가 의도한 부분만 변경했는지 확인하세요.
출처(provenance)를 요구하세요. 모든 변경 사항은 이유에 따라 추적 가능해야 합니다.
리뷰어에게 함정(traps)으로 테스트하세요. 확실하게 거짓 양성(false positives)을 몇 개 심고, 그것이 이를 물어뜯는지 보세요. 만약 그렇다면, 당신은 노이즈 문제(noise problem)를 가지고 있으며, ‘47개의 이슈 발견’이라는 말은 아무 가치가 없습니다.
실제 버그를 찾아내고 가짜 버그에 대해서는 침묵하는 AI 리뷰어가 모든 것을 플래그 지정하는 10개보다 훨씬 가치 있습니다. 이 게이트들(gates)이 바로 그곳으로 가는 방법입니다.
스트레스 테스트의 파이프라인은 CCA-Audit — 오픈 소스 (MIT)이며, Claude Code에 하나의 /audit-fix 명령어로 설치됩니다. 위 실행에서 나온 전체 원본 에이전트 기록(agent transcripts)은 제가 작업한 것을 확인하고 싶다면 저장소(repo)에 있습니다. 피드백—특히 환각을 일으킨 실제 사례—은 매우 환영합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기