LLM 코딩 에이전트가 긴 백엔드 작업에서 왜 표류하는가 (그리고 해결 방법)
요약
LLM 코딩 에이전트가 긴 작업 과정에서 초기 제약 조건을 망각하는 '제약 조건 저하(Constraint decay)' 현상을 분석합니다. 이는 컨텍스트 윈도우의 크기 문제가 아니라, 생성된 토큰이 늘어남에 따라 시스템 프롬프트의 어텐션 가중치가 낮아지는 신호 대 잡음비 문제임을 설명합니다.
핵심 포인트
- 제약 조건 저하: 작업이 길어질수록 에이전트가 초기 지침을 준수하지 못하는 현상
- 근본 원인: 컨텍스트 용량 부족이 아닌 어텐션 가중치 분산에 따른 신호 약화
- 해결 전략: 제약 조건을 에이전트의 기억이 아닌 검증 가능한 아티팩트로 외재화
지난달 저는 AI 에이전트가... 대부분은 정확한 코드를 작성했던 Django 서비스의 버그를 잡는 데 사흘을 보냈습니다. 엔드포인트(Endpoints)는 작동했습니다. 테스트도 통과했습니다. 하지만 네 번째 파일쯤에서, 에이전트는 여러 단계의 쓰기 작업에 대한 데이터베이스 트랜잭션 래퍼(database transaction wrapper)를 조용히 누락시켰습니다. 일곱 번째 파일에 이르러서는, 모델 중 하나에 테넌트 스코핑(tenant scoping)이 필요하다는 사실을 잊어버렸습니다.
이것이 바로 제약 조건 저하(Constraint decay)입니다. 그리고 일단 이를 관찰하기 시작하면, 어디에서나 이를 발견하게 됩니다.
제약 조건 저하(Constraint decay)의 실체
LLM 에이전트에게 백엔드 작업을 맡길 때, 당신은 에이전트에게 한 더미의 제약 조건(constraints)을 부여합니다. 어떤 것들은 명시적입니다 (이 ORM을 사용할 것, tenant_id로 스코핑할 것, 쓰기 작업을 트랜잭션으로 감쌀 것). 어떤 것들은 암묵적입니다 (인증 미들웨어(auth middleware)는 모든 경로에 적용됨, 에러는 특정 상태 코드에 매핑됨). 작업 초기에는 이러한 제약 조건들이 컨텍스트(context) 내에 신선하게 유지되어 에이전트가 이를 준수합니다.
작업이 길어짐에 따라 예측 가능한 일이 발생합니다. 에이전트가 더 많은 코드를 생성합니다. 생성된 코드는 원래의 제약 조건들을 어텐션 윈도우(attention window)로부터 더 멀리 밀어냅니다. 여덟 번째 함수를 작성할 때쯤이면, 원래의 지침들은 어텐션 가중치(attention weight)를 얻기 위해 에이전트가 출력한 수천 개의 토큰과 경쟁하게 됩니다. 제약 조건은 흐릿해집니다. 출력은 표류(drift)합니다.
미리 말씀드리자면, 저는 이와 관련된 모든 논문을 상세히 읽지는 않았으며, arXiv의 Constraint Decay 프리프린트(preprint)와 같은 최근 연구들은 여전히 논의 중입니다. 하지만 이 현상 자체는 집에서도 재현 가능합니다. 충분한 제약 조건을 가진 충분히 긴 에이전트 루프(agent loop)를 구축하면, 당신의 컴퓨터에서도 이 현상이 일어나는 것을 목격하게 될 것입니다.
근본 원인: 메모리의 문제가 아니라 신호 대 잡음비(signal-to-noise)의 문제
표류를 목격했을 때 가장 먼저 드는 생각은 "음, 그냥 컨텍스트 윈도우(context window)에 넣으면 되겠네"입니다. 현대의 모델들은 거대한 컨텍스트 윈도우를 가지고 있습니다. 하지만 윈도우 크기가 진짜 문제는 아닙니다.
문제는 어텐션 (Attention)이 전체 컨텍스트에 대한 소프트맥스 (softmax) 연산이라는 점입니다. 시스템 프롬프트 (system prompt)가 200 토큰이고 주변에 생성된 코드가 유사한 형태의 함수 이름, 타입, 패턴을 가진 8,000 토큰이라면, 제약 조건에 부여되는 상대적 가중치는 줄어듭니다. 제약 조건이 존재하지 않는 것은 아닙니다. 단지 두드러지지 않을 뿐입니다.
간단한 실험을 통해 이를 확인할 수 있습니다. 에이전트에게 "모든 데이터베이스 쓰기 작업은 반드시 audit_log()를 거쳐야 한다"와 같은 제약 조건을 부여하십시오. 그리고 다섯 개의 파일을 작성하게 해보세요. 네 번째 파일쯤 되면, 직접적인 쓰기 작업이 슬그머니 포함되는 경우가 많습니다. 원래의 제약 조건만 다시 프롬프트로 입력하면 즉시 준수 상태로 복구됩니다. 제약 조건이 모델을 떠난 것이 아니라, 모델이 그 가중치를 부여하는 것을 멈춘 것뿐입니다.
단계별 해결 방법
올해 약 12개 정도의 에이전트 기반 프로젝트를 진행하며 제가 정착한 패턴입니다. 완벽하지는 않지만, 표류 (drift) 현상을 현저히 줄여줍니다.
1. 제약 조건을 검증 가능한 아티팩트 (artifacts)로 외재화하기
에이전트의 기억력에 의존하지 마십시오. 제약 조건을 기계적으로 검증할 수 있는 형태로 만드세요.
# constraints.py — 횡단 관심사 규칙(cross-cutting rules)을 위한 진실의 원천(source of truth)
INVARIANTS = {
"tenant_scoping": {
...
그다음, 이를 체크하는 작은 AST 워킹 린터 (AST-walking linter)를 작성하십시오. 이제 제약 조건은 성능이 저하되지 않는, '이빨을 가진' 집행자를 갖게 됩니다.
2. 작업을 청크 (chunk) 단위로 나누고, 청크 사이에 새로고침하기
길게 한 번에 생성하는 방식(single-shot generation)에서 성능 저하가 가장 심하게 나타납니다. 작업을 청크 단위로 나누고, 각 청크 사이에서 관련 제약 조건을 다시 재생(replay)하십시오.
def run_agent_task(task, constraints):
chunks = decompose_task(task)
results = []
...
핵심적인 움직임은 이전의 모든 코드를 쏟아붓는 대신 summarize(results)를 사용하는 것입니다. 요약은 수천 개의 코드 토큰으로 제약 조건을 혼잡하게 만들지 않으면서도 아키텍처 결정 사항을 보존합니다.
3. 별도의 제약 조건 체크 패스 (pass) 사용하기
매 청크가 끝날 때마다, 새로운 코드가 제약 조건을 준수하는지 확인하는 것만을 유일한 임무로 하는 별도의 좁은 범위의 LLM 호출을 실행하십시오. 단일 책임(Single responsibility)과 신선한 컨텍스트(fresh context)를 확보하는 것입니다.
def check_chunk(generated_code, constraints):
prompt = (
"Check this code against the constraints below. "
...
이는 메인 에이전트에게 스스로 검토(self-check)하도록 요청하는 것보다 훨씬 더 신뢰할 수 있습니다. 검토자(checker)는 생성(generation)에 따른 인지적 부하(cognitive load)를 짊어지지 않기 때문입니다. 검토자의 컨텍스트(context)는 짧고, 주의력(attention)은 분산되지 않습니다.
4. 위반 사항을 명확하게 실패 처리하기 (Make violations fail loud)
검토자가 위반 사항을 발견했을 때, 문제가 된 파일을 "패치(patch)"하려고 시도하지 마십시오. 해당 청크(chunk)를 롤백(roll back)하고, 위반된 제약 조건(constraint)을 맨 상단에 고정하여—때로는 반복하여—다시 생성하십시오. 반복은 미관상 좋지 않지만 효과적입니다. 모델은 여러 번 나타나는 제약 조건에 더 높은 가중치를 부여합니다.
def regenerate_with_emphasis(chunk, violations):
emphasized = "\n\n".join(
f"CRITICAL CONSTRAINT (do not violate): {v}" for v in violations
...
5. 인간의 검토는 코드가 아닌 제약 조건 영역에 집중하기
이 부분은 사람들이 흔히 건너뛰는 대목입니다. 에이전트가 작성하는 모든 줄을 검토할 필요는 없습니다. 여러분은 _제약 조건 세트(constraint set)_와 _검토자(checker)_를 검토해야 합니다. 이 두 가지가 정확하다면, 표류(drift)는 제한됩니다.
저는 이제 모든 에이전트 프로젝트를 시작할 때 제약 조건 파일(constraints file)을 가장 먼저 작성하는 습관이 생겼습니다. 어떤 코드보다도 먼저 말이죠. 아직 존재하지 않는 코드를 대상으로 규칙을 작성하는 것이라 생소하게 느껴질 수 있지만, 이는 여러분이 명확하게 사고하고 있는 동안 불변량(invariants)을 사전에 명확히 정의하도록 강제합니다.
예방: 제한된 표류를 위한 설계 (design for bounded drift)
애초에 문제를 작게 유지하기 위한 몇 가지 패턴은 다음과 같습니다:
- 좁은 범위의 작업을 선호하세요 (Prefer narrow tasks). "새로운 엔드포인트 추가"는 범위가 제한되어 있습니다. 반면 "관리자 패널 전체 구축"은 그렇지 않습니다. 성능 저하 (Decay)는 작업 길이에 비례하여 커지므로, 짧은 작업일수록 성능 저하가 적습니다.
- 타입 인터페이스 (Typed interfaces)를 적극적으로 사용하세요. 에이전트가 타입 시그니처 (Type signature)를 충족해야 할 때, 해당 타입은 항상 눈에 보이는 국소적인 제약 조건 (Constraint) 역할을 합니다. mypy나 TypeScript와 같은 도구들은 놀라울 정도로 많은 표류 (Drift)를 무료로 잡아냅니다.
- 기존의 스캐폴딩 (Scaffolding)에 의존하세요. 코드베이스에 이미 테넌트 범위 (Tenant scoping)를 강제하는
BaseRepository가 있다면, 에이전트는 상속을 통해 해당 제약 조건을 물려받습니다. 프레임워크는 에이전트가 잊어버리는 것을 기억합니다. - 통과하는 테스트를 신뢰하지 마세요. 코드와 테스트를 모두 작성한 에이전트는 두 가지를 서로 일치시켜 놓은 상태입니다. 실제 앱을 실행하세요.
curl로 엔드포인트를 호출해 보세요. 데이터베이스를 직접 확인하세요.
솔직한 요약은 이렇습니다: LLM 에이전트는 작업의 첫 한 시간 동안은 환상적이지만, 네 번째 시간으로 갈수록 점진적으로 나빠집니다. 만약 당신이 짧은 청크 (Chunks), 외부 제약 조건 (External constraints), 기계적 검증 (Mechanical checking)과 같은 현실을 바탕으로 워크플로우를 설계한다면, 표류 (Drift)는 관리 가능한 수준이 됩니다. 만약 에이전트를 당신이 말한 모든 것을 기억하는 주니어 개발자처럼 취급한다면, 당신은 며칠 동안 소리 없는 제약 조건 위반 (Constraint violations)을 디버깅하게 될 것입니다.
제 경우에는 3일 동안 그랬습니다. 그 이후로 저는 프로젝트 구조를 다르게 설계하고 있습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기