LLM 잘 쓰는 법

"완료했습니다"를 믿지 마라 — LLM을 잘 쓰는 법 (3)

RoutineCode Team·2026-10-04·9분

에이전트가 자신 있게 보고한다. "다 고쳤습니다. 테스트도 통과했습니다." 그런데 막상 돌려보면 빌드가 깨져 있거나, 배포 후 프로덕션에서 터진다. "분명 됐다고 했는데 왜 안 되지?" — 가장 많이 겪으면서도 가장 조용히 새는 실패다.

여기서 중요한 건, 이게 에이전트가 거짓말을 하는 게 아니라는 점이다. 모델은 당신을 속일 의도가 없다. "완료했습니다"라는 보고는 LLM의 작동 방식에서 구조적으로 나오는 출력이다. 그 구조를 이해하면, 왜 믿으면 안 되는지와 어떻게 해야 하는지가 같이 따라 나온다.

무슨 일이 벌어지나

에이전트에게 수정을 맡기면 거의 항상 확신에 찬 마무리가 돌아온다 — "수정 완료", "엣지 케이스까지 처리했습니다", "모든 테스트 통과". 문제는 이 문장들이 실제 결과와 무관하게 나온다는 것이다. 실행해 보면 다르고, 때로는 에이전트가 돌리지도 않은 테스트 결과를 "통과했다"고 보고하기도 한다.

왜 그런가 — LLM의 작동 원리

네 가지가 겹친다.

첫째, 생성은 확률적 다음-토큰 샘플링이다. LLM은 다음 토큰을 "이 맥락에서 가장 그럴듯한 것"의 확률분포에서 뽑는다. 코드 수정 블록 뒤에 "완료했습니다 / 테스트 통과" 같은 유창한 마무리는, 실제 성공 여부와 무관하게 그 자리에서 확률이 높다. 모델은 "성공 보고가 어떻게 생겼는지"를 예측하는 것이지, 성공을 확인하는 게 아니다.

둘째, 기본적으로 grounding이 없다. 텍스트 생성은 코드 실행이 아니다. 실제 관측 — 빌드 종료코드, 테스트 결과, 런타임 에러 — 이 컨텍스트에 들어와 있지 않으면, "됐다"는 주장은 현실이 아니라 사전확률(prior) 에 근거한다. 모델은 "보통 이런 수정은 통과한다"는 분포를 따라 말할 뿐이다.

셋째, 증거마저 환각할 수 있다. 모델은 그럴듯한 텍스트를 만드는 데 능하다. 그래서 실행하지 않은 테스트의 로그를 지어내 "7 passed, 0 failed"처럼 보고하는 일도 생긴다. 없는 관측을 생성하는 것이다 — 악의가 아니라, 그게 통계적으로 그럴듯한 출력이기 때문이다.

넷째, 자기검증 편향이 있다. 같은 세션에서 "정말 됐어?"라고 물으면, 모델은 자기 직전 출력에 조건화된 상태다. 방금 "완료"라고 쓴 토큰이 컨텍스트에 남아 다음 판단을 끌어당긴다 — 그래서 "네, 확인했습니다"로 자기 확증하기 쉽다.

현장에서 클립보드로 항목을 점검하는 사람
"완료" 도장은 쉽다. 실제 검증은 현실과 대조하는 별도의 행위다.

여기에 에이전트 루프의 구조가 더해진다. Anthropic은 이렇게 표현한다 — "Claude는 작업이 done처럼 보이면 멈춘다. 체크가 없으면 '그럴듯함'이 유일한 신호이고, 네가 검증 루프가 된다." 외부의 합격/불합격 신호가 없으면, 에이전트는 현실이 아니라 그럴듯함에서 멈춘다.

그래서 어떻게 해야 하나 — 주장을 증거에 조건화시켜라

핵심 원리는 하나다: 모델의 사전확률이 아니라 실제 도구 관측에 주장을 묶는 것(grounding). 약한 것부터 강한 것 순으로.

  • ① 확언 대신 증거를 요구한다. "됐어?"는 자기검증 편향만 부른다. 대신 "빌드 출력을 그대로 붙여줘", "테스트 실행 결과를 보여줘". 모델이 실제 명령을 돌리고 그 출력에 조건화되게 한다.
  • ② 루프 안에 기계 판독 합격/불합격을 넣는다. 테스트 스위트, 타입 체크, 린터, 종료코드로 끝나는 스크립트. 대화 안에 착지하는 pass/fail이 있으면 에이전트가 사람 대신 그 신호로 스스로 교정한다. 이게 "그럴듯함에서 멈추는" 문제를 직접 끊는다.
  • ③ 작성자와 검증자를 분리한다. 방금 코드를 쓴 세션은 그 출력에 편향돼 있다. 새 컨텍스트(또는 다른 에이전트)가 diff만 보고 요구사항 대비 누락·버그를 찾게 하라 — "스타일 말고 정확성/요구사항 gap만".
  • ④ 게이트를 결정적으로 만든다(hook/CI). CLAUDE.md의 "항상 테스트 돌려라"는 확률적 지침이라 무시될 수 있다. hook이나 CI는 코드라 반드시 실행된다. 합격하지 못하면 "완료"로 넘어가지 못하게 코드로 막아라.
  • ⑤ 증거 없는 완료는 미완료로 취급한다. 이건 습관이다. 보고를 받으면 반사적으로 근거를 되묻는다. 되묻는 비용은 거의 0이고, 막는 건 배포 사고다.

RoutineCode도 같은 원리 위에 있다

이 다섯 번째 습관을 사람이 매번 기억하지 않아도 되게 만든 것이 RoutineCode의 차별점이다.

순정 Claude Code에서 "완료"는 기본적으로 자기보고다. 증거를 강제하는 장치가 없으니, 결국 사람이 매번 "진짜 됐나" 확인하는 검증 루프 역할을 떠안는다. RoutineCode는 그 위에 한 겹을 더한다 — 구현 후 빌드·테스트를 실제로 돌려 그 증거를 남기고, 검증을 통과하지 못하면 작업을 완료로 인정하지 않는다. 증거를 내지 못하면 "완료했다"고 보고하지 않는 것이다. 여기에 더해, 같은 수정이 여러 번 실패하면 거짓 자신감으로 같은 루프를 도는 대신 접근 자체를 전환하도록 강제한다. 당신이 증거를 되묻는 습관과 RoutineCode의 증거 기반 완료는 같은 목적 — 주장을 그럴듯함이 아니라 현실에 묶는 것 — 을 위한 두 방향의 노력이다.

기대효과와 결과

  • "겉만 완료"를 배포 전에 걸러낸다. 그럴듯한 보고가 아니라 실제 pass/fail로 판정되니, 안 된 걸 된 줄 알고 넘어가는 일이 준다.
  • 사람이 수동 QA에서 벗어난다. 검증 루프를 도구가 맡으면, 당신은 결과를 승인하는 역할로 올라간다.
  • "됐다더니 프로덕션에서 터짐" 사고가 준다. 가장 비싼 실패가 가장 싼 단계에서 걸린다.

에이전트가 틀리는 걸 막을 수는 없다. 하지만 틀린 걸 "됐다"고 말하고 넘어가는 것은 막을 수 있다 — 주장을 증거에 묶기만 하면 된다.


이 글은 "LLM을 잘 쓰는 법" 시리즈의 3편이다. 다음 편에서는 같은 오류를 세 번 치고 있을 때 — 밀어붙이지 말고 리셋해야 하는 이유(실패한 접근이 다음 시도를 오염시키는 앵커링)를 다룬다.

참고 자료

#LLM#검증#AI에이전트#테스트자동화#ClaudeCode

Want posts like this weekly?

Subscribe for AI dev-workflow insights.