AI 코드 리뷰 자동화, 판정은 왜 흔들릴까? 원인과 해결법 4가지

같은 코드를 LLM에 두 번 리뷰시켰더니 한 번은 심각도가 critical로, 다른 한 번은 major로 나왔습니다. 코드도, 프롬프트도, 모델도 동일했습니다. 달라진 건 아무것도 없는데 판정이 달라진 겁니다. 제가 인포그랩에서 AI 코드 리뷰 서비스 Faceta를 만들면서 여러 번 겪은 일입니다. LLM에 같은 질문을 반복해 보신 분이라면 형태는 달라도 비슷한 경험을 하셨을 겁니다.
AI 코드 리뷰를 merge gate에 걸거나, LLM 평가 결과를 자동화 파이프라인에 넣거나, LLM 출력을 코드가 파싱해 다음 단계로 넘기면, 이 현상이 그대로 전파됩니다. 게이트는 어제 통과시킨 Merge request(MR)를 오늘은 차단하고, 어제 차단한 MR을 오늘은 통과시키기도 합니다. 개발자는 널뛰기하는 게이트 앞에서 '뭘 수정해야 MR이 통과할 수 있는지' 정확히 알기 어렵습니다. 개발자가 코드를 수정하는 대신 파이프라인을 다시 돌리기 시작하면, 게이트는 넘어야 할 기준이 아니라 운을 시험하는 절차가 됩니다. 반대로 게이트를 한 번 통과했다는 사실도 문제가 없다는 뜻은 되지 못합니다. 같은 코드를 다시 리뷰했다면 차단됐을 수도 있으니까요. 판정이 흔들리는 게이트는 지키는 사람도, 통과하는 사람도 믿지 못하는 게이트가 됩니다.
이렇게 LLM 응답이 흔들리는 가장 눈에 띄는 원인은 모델이 다음 단어를 확률에 따라 뽑는 방식입니다. 같은 입력에 다른 출력이 나오는 이 성질을 비결정성(nondeterminism)이라고 하는데요. 그렇다면 항상 확률이 가장 높은 단어만 뽑도록 무작위성을 조절하는 값(temperature)을 0으로 내리면 이 문제를 해결할 수 있지 않을까란 생각이 들 수도 있죠. 그러나 0으로 내려도 흔들림은 남습니다. 확률적 선택은 흔들림을 만드는 여러 원인 중 하나일 뿐이기 때문입니다.
이 글은 LLM 응답이 왜 비결정적인지 살펴보고, 실무에서 이 문제를 다루는 네 가지 방향을 제시합니다. 그중 두 가지는 직접 검증해 봤습니다. 선택지와 기준표로 출력의 폭을 좁히는 방법, 같은 질문을 여러 번 물어 다수결을 내는 방법입니다. 총 55회 호출로 그 효과를 확인한 결과를 함께 공유하겠습니다.
LLM 판정이 흔들리는 이유: 결정성과 비결정성

LLM 응답이 왜 비결정적인지, 그 원인부터 살펴보겠습니다. 먼저 결정성과 비결정성이라는 두 개념을 짚고 가겠습니다.
결정성은 같은 입력에 항상 같은 출력이 나오는 성질입니다. 우리가 다루는 대부분의 도구가 그렇습니다. 같은 파일의 SHA-256 해시는 언제 어디서 계산해도 같은 값이 나오고, 같은 리스트를 같은 기준으로 정렬하면 늘 같은 순서가 나옵니다. 테스트, 디버깅, 재현이라는 개발 관행 전체가 이 성질 위에 서 있습니다.
LLM은 이 전제를 흔듭니다. 같은 입력을 넣어도 출력이 달라질 수 있는데, 이런 성질을 비결정성이라고 합니다. LLM 응답이 비결정적인 이유는 크게 세 가지입니다.
첫째, 추첨하듯 답을 만듭니다. LLM은 다음에 올 단어(토큰)를 모델이 매긴 확률에 따라 뽑습니다. 무작위성을 조절하는 값(temperature)이 0보다 크면 확률이 조금 낮은 후보 단어도 뽑힐 수 있어 매 자리에서 단어 선택의 폭이 넓어지죠. 앞에서 뽑힌 단어 하나가 달라지면, 그 뒤에 이어지는 문장 전체도 달라집니다.
둘째, 무작위성을 0으로 내려도 비일관성이 완전히 사라지지 않습니다. 모델이 매긴 확률이 가장 높은 단어만 뽑으면 응답이 결정적이어야 할 것 같지만, 실제로는 그렇지 않습니다. 부동소수점 연산은 덧셈 순서에 따라 결과가 아주 미세하게 달라지고((a+b)+c ≠ a+(b+c)), 추론 서버는 여러 사용자의 요청을 묶어 한 번에 처리(배칭)합니다. 내 요청이 몇 개의 다른 요청과 함께 묶이는지는 그 순간의 서버 상황에 달려 있고, 묶음 크기가 달라지면 연산 순서가 달라지고, 후보 단어들의 점수 차이가 근소하면 1위와 2위가 뒤바뀌죠. 이 메커니즘은 AI 연구 기업 Thinking Machines Lab이 2025년에 공개한 분석 Defeating Nondeterminism in LLM Inference가 잘 정리하고 있는데요. 배치 크기에 영향받지 않는 연산 방식을 직접 구현하면 해결할 수 있다는 결론이었습니다. 다만 이건 추론 서버를 직접 운영할 때의 이야기이고, 상용 API를 쓰는 입장에서는 제어하기 어려운 영역입니다.
셋째, 입력에 민감합니다. 프롬프트의 공백 하나, 예시 순서 하나만 바뀌어도 출력이 달라질 수 있습니다. 엄밀히 말하면 이건 '같은 입력'이 아니라 '거의 같은 입력'이 만드는 차이라 앞의 두 원인과는 결이 조금 다르죠. 그러나 실무에서는 프롬프트에 사용자 입력이나 날짜 같은 값이 매번 다르게 채워지기 때문에 입력이 조금씩 달라지는 상황 자체는 일상적입니다. 완전히 똑같은 입력을 두 번 넣는 쪽이 더 드뭅니다.
정리하면, 상용 API로 LLM을 쓰는 우리에게 비결정성은 버그가 아니라 주어진 조건에 가깝습니다. 없앨 수 없다면, 어디까지 허용하고 어디부터 막을지를 설계하는 쪽이 현실적입니다.
채팅에선 괜찮지만, 자동화에선 깨진다
채팅으로 LLM을 사용할 때는 비결정성이 문제되지 않습니다. 매번 다양한 답변은 도움이 될 때가 있죠. 문제는 LLM의 비결정적 출력이 다음 단계의 입력이 되는 순간부터입니다.
- 게이트: '심각도 major 이상이면 merge 차단'과 같은 규칙에 LLM 판정을 연결하면, 같은 코드가 어떤 호출에서는 통과하고 어떤 호출에서는 차단될 수 있습니다.
- 평가: LLM이 매긴 출력 품질 점수를 프롬프트 개선의 잣대로 삼을 때, 점수가 올라도 개선 덕분인지 그날 호출 운인지 가려내기 어렵습니다.
- 형식 처리: 출력 형식이 일관되지 않으면, 그 출력을 JSON으로 파싱하거나 특정 필드만 뽑아 쓰는 뒷단 코드가 깨집니다.
비결정성을 다루는 네 가지 방향
그렇다면 이 비결정성을 실무에서 어떻게 다뤄야 할까요? 저는 Faceta를 만들면서 네 가지 방향을 잡았습니다.

코드 위임
결정적이어야 하는 부분은 코드에 맡깁니다. 가장 근본적인 해법입니다. 코드는 같은 입력에 같은 출력을 내는 결정적 도구라 흔들리면 안 되는 단계에 활용하기 적합합니다. 파이프라인을 설계할 때 "이 단계가 흔들리면 무엇이 무너지나"를 먼저 묻고, 무너지는 게 있다면 그 단계는 LLM이 아니라 코드가 맡는 편이 좋습니다. 예를 들어, 코드베이스의 호출 관계 그래프를 만드는 작업은 정적 분석 도구가 결정적으로, 더 저렴하게 해냅니다. LLM에는 그 위에서 의미를 해석하는 판단만 맡기는 거죠. 실제로 지식그래프 생성을 LLM으로 대체할 수 있는지 실험해 봤는데요. 분류 체계는 깔끔했지만, 빠뜨리지 않고 찾아내는 능력(재현율)이 룰 베이스에 크게 밀렸습니다. 결정적인 작업을 LLM에 맡기면 비용은 더 들면서 결과는 덜 안정적입니다.
폭 좁히기
LLM에 판단을 맡기더라도 출력이 흔들릴 수 있는 공간 자체를 줄입니다. 선택지를 정해두면, 판정이 흔들려도 그 선택지 안에서만 흔들립니다. 출력을 자유 서술로 받으면 표현도, 길이도, 깊이도 제각각이 되는데요. 고를 수 있는 값을 네 개로 제한하면 어긋날 여지도 그 네 개로 한정됩니다. 따라서 자유 서술 대신 정해진 선택지(none/minor/major/critical)를 주고, 점수를 매기게 할 거면 구간별 기준표를 함께 주고, 형식은 프롬프트로 부탁하는 대신 출력 스키마를 강제하는 기능을 사용하는 걸 권장합니다. Faceta도 이 원칙을 따라 리뷰 판정을 자유 서술로 받지 않고 정해진 네 가지 신호(Blocked/Not Ready/Conditional/Ready) 중 하나로만 내리도록 고정했습니다.
다수결
폭을 좁혀도 남는 흔들림은 LLM에 여러 번 물어 다수결로 정합니다. 선택지를 좁혀도 한계는 남습니다. 어떤 코드가 major와 critical 중 어느 쪽인지 애매한 경계에 놓이면, 호출마다 판정이 그 두 값 사이를 오갑니다. 이때는 LLM에 한 번 묻고 믿는 대신, 같은 코드를 같은 프롬프트로 여러 번(예: 다섯 번) 물어 가장 많이 나온 판정을 최종값으로 삼는 걸 추천합니다. 호출마다 무작위로 튀는 판정이 여러 번의 투표로 상쇄되기 때문에 경계선 판정이 안정됩니다. 다만 호출 비용이 투표 수만큼 늘어나기에 게이트처럼 뒤집히면 안 되는 판정에만 골라 쓰는 게 현실적입니다.
하네스
이 장치들을 코드 레벨 하네스로 감쌉니다. 폭 좁히기와 다수결은 프롬프트에 부탁하는 것만으로는 완성되지 않습니다. LLM 호출의 앞뒤를 감싸는 코드 계층, 즉 하네스(harness)가 필요합니다. 하네스는 입력을 일정한 형태로 다듬어 넣고, 출력이 약속된 형식에 맞는지 코드로 검증하고, 어긋나면 자동으로 재시도하고, 다수결 같은 반복 호출도 코드가 지휘하는 구조입니다. 모델이 지시를 지켜주기를 기대하는 대신, 지키지 않았을 때의 처리를 코드가 보장하는 쪽으로 무게를 옮기는 거죠. 하네스의 수준에 따라 결과물의 품질이 어떻게 달라지는지는 저희 팀의 n8n 하네스 글에서 실험으로 다룬 적이 있으니 함께 보셔도 좋겠습니다.
네 방향 가운데 폭 좁히기와 다수결, 이 두 가지는 뒤에서 실험으로 효과를 확인하겠습니다.
한 가지 흔한 오해도 짚고 가면, 프롬프트 캐싱은 결정성과는 관련이 없습니다. 캐시에 저장되는 건 입력을 처리한 중간 결과이지 답변 자체가 아닙니다. 따라서 캐시가 적중해도 답변은 매번 새로 만들어져 동일한 입력에도 출력은 달라질 수 있습니다.
직접 확인해 보기: 실험 설계
위 방향이 실제로 효과가 있을까요? Claude Haiku에 같은 프롬프트를 반복 호출하는 방식으로, 이 중 폭 좁히기와 다수결의 효과를 확인해 봤습니다. 여기서 재는 건 판정의 정확성이 아니라 일관성입니다. 같은 입력에 같은 답이 나오는가를 보는 것이지, critical과 major 중 무엇이 옳은 판정인지를 가리는 실험은 아닙니다.
폭 좁히기는 두 방식으로 확인했습니다. 점수에 구간별 기준표를 주는 방식(A와 B의 비교)과 판정을 정해진 라벨로 제한하는 방식(D)입니다. 다수결은 D의 호출을 묶어(D') 확인했고, 자유 서술(C)은 폭을 좁히지 않았을 때가 어떤지 보는 대조군입니다.
비교한 조건
| 조건 | 과제 | 반복 |
|---|---|---|
| A. 점수, 기준 없음 | 리뷰 코멘트의 심각도를 0~100으로 | 10회 |
| B. 점수, 기준 제공 | 같은 과제 + 구간별 기준표 | 10회 |
| C. 자유 서술 리뷰 | 버그가 심어진 코드를 자유롭게 리뷰 | 10회 |
| D. 라벨 판정 | 같은 코드를 none/minor/major/critical 중 하나로 | 25회 |
| D'. 다수결 | D의 호출을 5회씩 묶어 다수결 | 5표 × 3그룹 |
리뷰 대상 코드는 할인율 계산 함수로, 정수 반올림으로 인한 금액 손실(확실한 버그)과 rate 해석의 모호함(경계선 이슈)을 함께 심어 두었습니다. 경계선 이슈를 일부러 넣은 건 판정이 흔들리는 지점을 관찰하기 위해서입니다. 각 호출은 이전 호출과 독립된 새 세션에서 실행해 누적된 대화 맥락이 없고, temperature 등 생성 파라미터는 기본값 그대로 뒀습니다. 실무에서 CLI나 SDK를 기본값 그대로 쓰는 상황과 같은 셈입니다.
직접 재현해 보실 분들을 위해 실행 환경을 공유합니다.
재현 설정
- 모델: Claude Haiku 4.5 (`claude-haiku-4-5-20251001`)
- 호출 방식: Claude Code CLI 2.1 비대화형 호출(`claude -p --model haiku`), 호출마다 독립된 새 세션
- system prompt: 별도 지정 없음 (CLI 기본값)
- structured output: 미사용. 형식 준수 여부 자체가 관찰 대상이라 형식은 프롬프트 지시로만 요청
- 프롬프트: 같은 조건 안에서는 완전히 동일, 조건(A~D) 사이에는 과제별로 다른 프롬프트
- 고정한 것: 모델, 생성 파라미터(모두 기본값), 조건별 프롬프트 / 바꾼 것: 조건(A~D)과 반복 호출뿐
조건마다 10~25회씩 반복한 소규모 실험이라 통계적으로 엄밀한 벤치마크는 아닙니다. 경향을 확인하는 수준으로 읽어주시면 됩니다.
실험 결과
설계한 네 조건을 차례로 돌린 결과입니다. 폭 좁히기부터 보겠습니다.
점수 평가: 기준표가 흔들림을 절반 이하로 줄임
리뷰 코멘트의 심각도를 0 ~ 100점으로 매기게 하는 과제였습니다. 점수 구간별로 판단 기준을 정해둔 기준표를 줬을 때(B)와 주지 않았을 때(A)를 비교했습니다.
| 기준 없음 (A) | 기준 제공 (B) | |
|---|---|---|
| 분포 | 45~68점 | 58~68점 |
| 범위 | 23점 | 10점 |
| 표준편차 | 8.69 | 3.27 |
| 서로 다른 값 | 7가지 | 4가지 |
| 가장 많이 나온 값 | 50 (3/10) | 65 (6/10) |
기준을 주지 않으면 같은 리뷰 코멘트가 어떤 호출에서는 45점, 어떤 호출에서는 68점으로, 23점이나 벌어졌습니다. 심각도를 판단하는 대상은 같은 코멘트인데 실제 점수는 그날의 호출에 따라 다른 겁니다. 이 점수에 '몇 점 이상이면 자동 알림' 같은 규칙을 걸었다면, 똑같은 코멘트인데도 어떤 호출에서는 알림이 울리고 어떤 호출에서는 울리지 않았을 겁니다. 규칙이 판단하려는 대상은 코드인데 정작 갈리는 건 호출 운이었을 테니까요.
점수 구간별 기준표를 함께 주자 표준편차가 8.69에서 3.27로 절반 아래로 떨어졌고, 10회 중 6회가 같은 값(65점)으로 나왔습니다. 다만 흔들림이 완전히 사라지지는 않았습니다. 기준표는 흔들림을 줄여주지만, 없애주지는 못합니다.
기준표 유무에 따른 심각도 점수 분포 (조건 A·B)
자유 서술 리뷰: '무엇'은 안정적, '어디까지'는 불안정
버그가 심어진 코드를 형식 제한 없이 자유롭게 리뷰하게 한 과제입니다(C). 10회를 돌렸습니다.
핵심 이슈 네 가지(반올림 정밀도, 음수·범위 미검증, 할인율 해석의 모호함, 입력 검증 부재)는 10회 모두 등장했습니다. 생각보다 안정적으로 일관된 답변이 나왔죠. 반면 파이썬 기본 round()가 .5를 짝수 쪽으로 반올림하는 banker's rounding처럼 깊이 들어가야 보이는 지적은 10회 중 2회만 나왔습니다. 답변 길이도 매번 달라서 짧을 때는 240자, 길 때는 460자로 2배 가까이 차이 났습니다. 모델은 '무엇이 문제인지'는 꽤 일관되게 잡아내지만, 어디까지 깊게 파고들지와 어떻게 표현할지는 매번 달라진다는 뜻입니다.
라벨 판정: 단일 호출은 72%, 다수결은 세 그룹 모두 수렴
같은 코드를 네 등급(none/minor/major/critical) 중 하나로 판정하게 한 과제입니다(D). 25회를 돌렸습니다.
결과는 critical 18회, major 7회로 갈렸습니다. critical이 18회로 전체의 72%를 차지했지만 나머지는 major로 넘어간 겁니다. 코드에 심어둔 버그가 '금액은 손실되지만 프로그램이 멈추지는 않는' 경계선에 있다 보니 어떤 호출은 이를 critical로, 어떤 호출은 major로 판정했습니다.
이 25회 가운데 15회를 다섯 개씩 세 그룹으로 나눠 각 그룹의 다수결을 확인하자 세 그룹 모두 critical로 모였습니다. 개별 호출에서는 최다 등급인 critical이 72%에 그쳤지만, 다섯 번을 모아 다수결로 내면 세 그룹 모두 흔들림 없이 같은 판정에 도달한 겁니다. 다만 판정 하나를 얻으려고 호출을 다섯 번 하는 셈이라 비용도 그만큼 늘어납니다.
단일 호출과 5표 다수결의 판정 안정성 비교 (조건 D)
형식 지시조차 매번 지켜지진 않음
판정 내용과 별개로, 형식을 지키는지도 봤습니다. '다른 설명 없이 단어 하나만 출력해'라는 지시를 완전히 지킨 호출은 10회 중 3회뿐이었고, 나머지 7회는 판정 뒤에 이유를 덧붙였습니다. 답변 내용만이 아니라 지시를 지키는지 여부까지 비결정적이라는 뜻입니다. 뒤에서 이 출력을 받아 처리하는 코드가 있다면, 프롬프트로 부탁하는 데서 멈추지 말고 하네스에서 형식을 검증하고 강제해야 하는 이유입니다.
맺음말
"같은 코드인데 판정이 달라진다"는 데서 출발한 글이었는데 실험을 돌려보니 가야 할 방향은 오히려 선명해졌습니다.
비결정성은 없애야 할 결함이 아니라 설계할 때 함께 고려해야 할 조건입니다. 상용 API를 쓰는 이상 완전히 없앨 수는 없으니 파이프라인의 단계마다 이렇게 물어보시면 좋겠습니다.
이 단계의 출력이 흔들리면 무엇이 무너지는가?
- 무너지는 게 구조라면(그래프 생성, 형식 변환, 집계), 코드에 맡기는 편이 좋습니다. LLM에 시키면 비용은 더 들고 결과는 덜 안정적입니다.
- 무너지는 게 판정이라면(심각도, 통과/차단), 선택지와 기준표로 폭을 좁히고, 형식은 하네스에서 강제하고, 그래도 흔들리는 경계선 판정에는 다수결을 붙여 보세요.
- 무너지는 게 없다면(요약, 초안, 아이디어), 그대로 두셔도 됩니다. 거기서는 흔들림은 결함이 아니라 다양성이니까요.
이 장치들은 프롬프트 안의 부탁이 아니라 LLM을 감싸는 코드, 즉 하네스에 담을 때 비로소 보장됩니다. 결국 하고 싶은 말은 하나입니다. 모델이 잘해주길 기대하지 말고, 잘 안 됐을 때를 코드로 받아내자는 겁니다.
이번 실험은 반복 횟수가 적고 과제도 하나이며, 네 방향 중 폭 좁히기와 다수결만 검증했다는 한계가 있습니다. 모델과 과제가 달라지면 수치는 달라질 겁니다. 다만 "기준표는 흔들림을 줄이고, 다수결은 판정을 안정시키고, 형식은 코드로 보장한다"는 방향 자체는 어디서든 유효하리라 봅니다.
이 글의 네 가지 패턴은 저희 인포그랩이 만드는 온프레미스 AI 코드 리뷰어 Faceta를 만들며 얻은 것입니다. 지금의 Faceta에는 그 네 방향이 그대로 담겨 있습니다. 결정적인 영향 범위 분석은 지식그래프가 맡고(코드 위임), 판정은 정해진 신호로만 내리고(폭 좁히기), 여러 관점이 교차로 읽어 판단이 한쪽으로 치우치지 않게 하고(다수결), 이 전체를 하네스가 감싸 형식과 재시도를 코드로 보장합니다(하네스). 이 글이 다룬 다수결이 같은 모델에 여러 번 묻는 방식이라면, Faceta는 그 생각을 여덟 관점이 함께 읽는 교차 검토로 넓혔습니다.
같은 코드에 같은 판정을 주는 AI 코드 리뷰가 필요하시다면, faceta.io에서 이 패턴들이 실제 서비스로 어떻게 동작하는지 확인해 보세요.
흔들리는 AI 코드 리뷰를, 흔들리지 않는 게이트로
같은 코드를 AI로 리뷰할 때마다 판정이 달라지나요? Faceta는 게이트가 운이 아니라 기준이 되게 합니다.
참고 자료
- "Defeating Nondeterminism in LLM Inference", Thinking Machines, 2025-09-10, https://thinkingmachines.ai/blog/defeating-nondeterminism-in-llm-inference/
- Xuezhi Wang, Jason Wei, Dale Schuurmans, Quoc Le, Ed Chi, Sharan Narang, Aakanksha Chowdhery, Denny Zhou, "Self-Consistency Improves Chain of Thought Reasoning in Language Models", arxiv, 2022, https://arxiv.org/abs/2203.11171
- Faceta 공식 홈페이지, https://faceta.io/
- Miles(송민기), "LLM·하네스로 더 좋은 n8n 워크플로 생성하기", 인포그랩, 2026-06-17, https://insight.infograb.net/blog/2026/06/17/n8n-harness/
Owen
AI Platform Engineer
AI Platform Engineer로 LLM을 활용하는 서비스와 에이전트를 제작하고 있습니다. 새로운 기술들을 익히고 적응하는 것을 즐기며 개발, 배포, 운영까지 SDLC 전체를 이해하고 효율적인 시스템을 만드는 것에 관심이 많습니다.
이 저자의 글 모두 보기 →이 글이 도움이 되셨나요?
인포그랩 전문가가 맞춤 상담을 도와드립니다.
