당신의 LLM이 틀린 이유: 원인은 당신의 코드베이스에 있습니다
요약
LLM 기반 AI 코딩 어시스턴트가 발생하는 오류의 근본 원인이 모델의 결함이 아닌 코드베이스의 '이해 부채(Comprehension debt)'에 있음을 설명합니다. 코드 내에 의도와 제약 조건이 명확히 드러나지 않을 때 AI가 환각을 일으키는 과정을 분석합니다.
핵심 포인트
- 이해 부채는 AI와 개발자 모두에게 코드 파악을 어렵게 만드는 요소임
- AI의 오류는 모델 탓이 아니라 코드의 명명 규칙이나 타입 누락 등에서 기인함
- 함수 이름 생성 오류, 타입 지정 오류, 폐기된 패턴 사용 등 5가지 실패 모드 존재
- AI 어시스턴트의 성능을 높이려면 코드 자체의 명확성과 문서화가 필수적임
그 일은 어느 화요일에 일어났습니다. 저는 AI 코딩 어시스턴트(AI coding assistant)에게 제가 3개월 전에 작성한 함수를 설명해 달라고 요청했습니다. 하지만 모델은 존재하지 않는 함수를 설명했습니다.
완전한 환각 (Hallucination)은 아니었습니다. 그 함수는 존재했습니다. 다만 그 이름이 아니었고, 그 매개변수 (Parameters)를 사용하지 않았으며, 모델이 자신 있게 말한 대로 동작하지도 않았을 뿐입니다. 모델은 모호한 신호들로부터 그럴듯한 이야기를 구성했고, 그 빈틈을 허구로 채워 넣었습니다.
저의 첫 번째 본능은 모델을 탓하는 것이었습니다. 하지만 실제로 도움이 되었던 두 번째 본능은 코드 자체를 살펴보는 것이었습니다.
모델이 고장 난 것이 아니었습니다. 제 코드베이스 (Codebase)가 문제였습니다.
이해 부채 (Comprehension debt)란 무엇인가?
기술 부채 (Technical debt)는 변경하기 어려운 코드입니다. 이해 부채 (Comprehension debt)는 이해하기 어려운 코드입니다. 단순히 미래의 개발자뿐만 아니라, 코드를 처음 접하는 모든 것들, 즉 신입 사원, 러버 덕 (Rubber duck), 그리고 점점 더 늘어나고 있는 AI 어시스턴트에게도 마찬가지입니다.
여러분은 아마도 "다음 유지보수자가 당신의 집 위치를 아는 연쇄 살인마라고 생각하고 코드를 작성하라"는 말을 들어보셨을 것입니다. LLM 버전은 조금 더 관대하지만, 큰 차이는 없습니다.
이해 부채는 코드의 의도가 코드 안에 담겨 있지 않을 때 나타납니다. 로직은 작동합니다. 테스트도 통과합니다. 하지만 소스 코드 어디에도 함수가 왜 그렇게 동작하는지, 제약 조건 (Constraints)은 무엇인지, 혹은 절대 해서는 안 되는 행동은 무엇인지에 대한 정보가 없습니다. 그 지식은 누군가의 머릿속에 있거나, 두 달 전의 Slack 스레드에 있거나, 혹은 아예 존재하지 않습니다.
LLM은 Slack 스레드에 접근할 수 없습니다. 오직 여러분의 소스 코드만을 가질 뿐입니다.
LLM이 보여주는 다섯 가지 신호
AI 어시스턴트가 여러분의 코드베이스를 잘못 파악할 때는 무작위로 일어나지 않습니다. 오류는 특정 실패 모드 (Failure modes)를 중심으로 군집을 이루며, 각 오류는 실제적인 결함을 가리킵니다.
1. 함수 이름을 지어낸다.
모델이 존재하지 않는 함수를 호출하거나, 기존 함수를 잘못된 이름으로 호출합니다. 이는 대개 명명 규칙 (Naming)이 일관되지 않거나, 배럴 수출 (Barrel exports)이 불완전함을 의미합니다. 모델은 서로 일치하지 않는 관습들 사이에서 패턴 매칭 (Pattern matching)을 시도하고 있는 것입니다.
2. 매개변수 타입 (Parameter types)을 잘못 지정합니다.
사용자가 타입이 지정된 열거형 (Enum)을 원하는 곳에 문자열 (String)을 전달하거나, 특정 인터페이스 (Interface)를 정의한 곳에 일반 객체 (Plain object)를 전달합니다. 이는 거의 항상 함수 시그니처 (Function signatures)에서 타입 어노테이션 (Type annotations)이 누락되었거나 암시적 (Implicit)임을 의미합니다. 모델은 추측을 하고 있는 것입니다.
3. 사용하지 않는 패키지를 임포트 (Import)합니다.
이미 해당 패키지들을 래핑 (Wrap)한 유틸리티 래퍼 (Utility wrappers)가 있음에도 불구하고 lodash나 axios를 가져옵니다. 귀하의 실제 내부 추상화 (Internal abstractions)는 모델이 찾을 수 있는 곳 어디에도 문서화되어 있지 않기 때문에 모델이 읽을 수 없습니다. 모델은 학습을 통해 알고 있는 지식으로 회귀합니다.
4. 폐기된 (Deprecated) 패턴을 사용합니다.
8개월 전에 사용을 중단한 이전 버전의 API를 호출합니다. 귀하의 코드베이스에는 여전히 이러한 오래된 패턴이 포함되어 있으며 (하위 호환성을 위해서일 수도 있고, 단순히 정리가 되지 않아서일 수도 있습니다), 모델은 어떤 버전이 최신인지 알지 못합니다. 폐기 (Deprecation) 주석을 작성하는 데는 30초가 걸리지만, 주석이 없으면 어시스턴트와의 상호작용마다 5분의 혼란을 겪게 됩니다.
5. 비즈니스 규칙 (Business rule)을 알지 못합니다.
실제 제약 조건 (Constraint)을 고려한 버전이 아니라, 기술적으로만 유효한 버전의 함수를 제공합니다. "이 사용자 조회는 항상 소프트 삭제 (Soft delete) 플래그를 먼저 확인해야 한다"라는 규칙은 어떤 파일의 주석에도 적혀 있지 않습니다. 그것은 회의 중에 결정되었습니다. 모델은 기록되지 않은 내용은 알 수 없습니다.
이러한 각각의 오류는 무료 감사 (Audit) 항목입니다. 이를 찾기 위해 별도의 도구를 실행할 필요도 없었습니다. 모델이 대신 찾아준 것입니다.
5분 감사 (The five minute audit)
이를 위해 공식적인 프로세스가 필요하지는 않습니다. 그저 LLM의 혼란을 노이즈 (Noise)가 아닌 신호 (Signal)로 취급하기만 하면 됩니다.
모듈을 하나 선택하세요. 만들어진 지 몇 달이 넘은 모듈이라면 무엇이든 좋습니다. 그것을 AI 어시스턴트에게 입력하고 다음 질문들을 던져보세요:
- "이 모듈은 무엇을 하나요? 두 문장으로 설명해 주세요."
- "모든 내보내기(exported) 함수, 매개변수(parameters), 그리고 반환 타입(return types)을 나열해 주세요."
- "만약 제가 이 모듈을 삭제한다면 무엇이 망가지게 될까요?"
- "이 모듈의 메인 함수에 절대 전달해서는 안 되는 것은 무엇인가요?"
- "이 모듈 안에 사용 중단(deprecated)된 것처럼 보이지만 삭제되지 않은 것이 있나요?"
모델이 틀렸을 때 수정해주지 마세요. 무엇을 틀렸는지 기록하세요. 그 목록이 바로 당신의 이해 부채(comprehension debt) 등록부입니다.
건강한 모듈이라면 모델이 이 질문들의 대부분을 맞힐 것입니다. 하지만 이해 부채가 있는 모듈이라면, 이 다섯 가지 신호가 빠르게 나타나는 것을 보게 될 것입니다.
저는 지난 분기에 내부 TypeScript 서비스에 이 테스트를 실행해 보았습니다. 12개의 내보내기(exported) 함수가 있었습니다. 모델은 그중 3개의 이름을 환각(hallucinate)했고, 다른 2개의 반환 타입(return type)을 틀렸으며, 속도 제한(rate limit) 매개변수가 무엇을 위한 것인지 전혀 알지 못했습니다. 제가 잘 관리되고 있다고 생각했던 모듈에서 오답률이 41%에 달했습니다. 잘 관리되고 있었던 것이 아니었습니다. 그저 작동하고 있었을 뿐입니다.
작동하는 것(working)과 읽기 쉬운 것(legible)은 같은 것이 아닙니다.
어떻게 해결해야 하는가
본능적으로 RAG(Retrieval-Augmented Generation, 코드베이스를 청크(chunk)로 나누고, 임베딩(embed)한 뒤, 각 LLM 호출 전에 관련 컨텍스트를 검색하는 방식)를 떠올리게 됩니다. 그것도 도움이 됩니다. 구현 세부 사항이 궁금하시다면 저의 프로덕션 RAG 가이드에서 전체적인 접근 방식을 다루고 있습니다.
하지만 RAG는 당신의 문서를 검색합니다. 만약 당신의 문서가 코드 그 자체이고 그 코드가 불투명하다면, RAG는 모델이 불투명한 코드에 더 잘 접근할 수 있게 해줄 뿐입니다. 근본적인 문제는 변하지 않습니다.
실제 해결책은 생각보다 저렴합니다:
구현(implementation)이 아닌 의도(intent)를 작성하세요. "사용자 객체를 검증하고 정규화합니다. 데이터베이스에 저장하기 전에 항상 이 함수를 호출하십시오. 권한은 확인하지 않습니다."라고 적힌 JSDoc 주석은 모델이 검색할 수 있는 무언가를 제공합니다. 반면 "사용자 검증"이라고 적힌 주석은 그렇지 못합니다.
사용 중단(deprecation) 사항을 인라인(inline)으로 표시하세요. @deprecated 대신 getUserV2를 사용하세요라고 적는 데는 5초밖에 걸리지 않습니다. 이렇게 하면 모델이 이전 API를 자신 있게 추천하는 일을 멈추게 됩니다.
비즈니스 규칙을 이를 강제하는 파일 안에 작성하세요. 티켓(Ticket) 안에 적지 마세요. Confluence에 적지 마세요. 파일 안에 적으세요. 속도 제한 (Rate limit) 파라미터 위에 "이것은 엔터프라이즈 고객과의 과금 계약에 따라 하드코딩된 것이므로, 설정 가능하게 만들지 마십시오"라고 적힌 주석은 실제로 코드와 함께 이동하는 문서가 됩니다.
목표는 인간을 위한 문서를 쓰는 것이 아닙니다. 당신의 LLM 어시스턴트가 올바르게 도움을 줄 수 있도록, 어시스턴트가 파싱 (Parse) 할 수 있는 문서를 쓰는 것입니다. 그 부수적인 효과로 팀의 다음 작업자에게도 도움이 됩니다. 이는 공짜나 다름없습니다.
더 큰 AI 에이전트 (AI agent) 시스템을 구축하는 팀의 경우, 여기서 도움이 되는 메모리 (Memory) 및 컨텍스트 (Context) 패턴은 제가 AI 에이전트 메모리 관리(AI agent memory management)에 관한 포스트에서 설명한 것과 동일합니다. 코드베이스의 이해 부채 (Comprehension debt)와 에이전트의 컨텍스트 공백 (Context gaps)은 동일한 근본 원인, 즉 문서화되지 않은 의도 (Undocumented intent)에서 비롯됩니다.
또한 이 LLM 환각 위험 추정기 (LLM hallucination risk estimator)를 통해 현재의 노출 정도를 빠르게 파악할 수 있습니다. 특정 부채를 진단해주지는 않지만, 어디에 집중해야 할지에 대한 조정된 시작점을 제공합니다.
모델이 곧 테스트입니다
당신의 LLM 어시스턴트는 현재 당신의 코드베이스가 가진 가장 정직한 독자입니다. 어시스턴트는 당신의 머릿속에 있는 컨텍스트를 알지 못합니다. 당신이 2024년에 내린 결정을 기억하지 못합니다. 어시스턴트는 그곳에 있는 것을 읽고 그것을 이해하려고 노력할 뿐입니다.
모델이 무언가 틀렸을 때, 그것은 신호입니다. 모델이 실패하고 있는 것이 아닙니다. 당신의 컨텍스트가 없는 독자가 무엇을 가지고 작업해야 하는지를 정확하게 보여주고 있는 것입니다.
그것은 선물입니다. 대부분의 코드는 다음 엔지니어가 합류하여 똑같이 혼란스러운 질문을 던지기 전까지는 그런 종류의 외부적 읽기 (External read)를 경험하지 못합니다.
이를 활용하세요.
만약 이러한 사고방식을 당신의 실제 코드베이스나 AI 시스템 아키텍처 (AI systems architecture)에 적용하고 싶다면, 바로 제가 수행하는 업무입니다.
실제 운영되는 AI 시스템 (Production AI systems)에 대해 더 자세히 알고 싶다면, mudassirkhan.me에서 다루고 있는 내용을 확인해 보세요.
여러분의 LLM은 여러분의 코드베이스를 얼마나 틀리게 파악하고 있나요? 댓글로 숫자를 남겨주세요. 사람들이 실제 운영 환경 (Production)에서 어느 정도 비율의 오답을 마주하고 있는지 궁금합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기