왜 한 번의 생성으로는 부족한가
LLM은 그럴듯한 코드를 잘 만들지만, 컴파일 오류·런타임 예외·경계 조건 실패를 스스로 알 수 없다. 프롬프트만으로 정확도를 끌어올리려는 시도는 금방 한계에 부딪힌다. 실무에서 신뢰할 수 있는 결과를 얻으려면 생성 → 격리 실행 → 테스트 결과 회수 → 재생성으로 이어지는 폐루프가 필요하다. 핵심은 모델에게 자연어 지시가 아니라 실제 실행 신호를 피드백으로 주는 것이다.
루프의 기본 구조
루프는 네 단계로 나뉜다. (1) 모델이 코드와 테스트를 생성한다. (2) 샌드박스에서 실행한다. (3) stdout/stderr, 종료 코드, 실패한 테스트 이름을 구조화해 수집한다. (4) 이 신호를 다음 프롬프트에 주입해 수정본을 요청한다. 종료 조건은 전체 테스트 통과 또는 최대 반복 횟수 도달이다. 무한 루프와 비용 폭주를 막으려면 반복 상한과 토큰 예산을 반드시 둔다.
def repair_loop(task, gen, run_tests, max_iter=4):
code = gen(task, feedback=None)
for i in range(max_iter):
result = run_tests(code) # 샌드박스 실행
if result.passed:
return code, i
feedback = format_feedback(result) # 실패만 압축해 전달
code = gen(task, feedback=feedback)
return code, max_iter # 실패 반환, 사람이 검토
피드백을 어떻게 압축할 것인가
실행 로그 전체를 그대로 넣으면 컨텍스트를 낭비하고 모델이 핵심을 놓친다. 실패한 테스트의 이름, assertion 메시지, 예외 트레이스백의 마지막 프레임만 추린다. 스택 전체보다 파일:라인 + 예외 타입 + 기대값/실제값이 훨씬 효과적이다. 성공한 테스트는 언급하지 않는다. 토큰당 정보 밀도를 높이는 것이 반복 수를 줄이는 가장 확실한 방법이다.
격리 실행 환경
생성된 코드는 신뢰할 수 없다. 파일 삭제, 네트워크 호출, 무한 루프가 언제든 나올 수 있으므로 반드시 격리한다. 컨테이너에 CPU·메모리·시간 제한을 걸고 네트워크를 차단한다.
docker run --rm \
--network none \
--memory 512m --cpus 1 \
--pids-limit 128 \
--read-only --tmpfs /tmp:size=64m \
-v "$PWD/work:/work:ro" \
python:3.12-slim \
timeout 30 python -m pytest /work -q
전략 비교
| 방식 | 정확도 | 비용 | 적합한 상황 |
|---|---|---|---|
| 단일 생성 | 낮음 | 최저 | 초안·프로토타입 |
| 실행 피드백 루프 | 높음 | 중간 | 테스트가 있는 작업 |
| 다중 후보 + 테스트 선택 | 가장 높음 | 높음 | 정확도가 최우선 |
테스트 자체를 믿을 수 있는가
모델이 코드와 테스트를 함께 생성하면 테스트를 통과하도록 코드를 왜곡하거나, 반대로 약한 테스트를 만들어 거짓 통과를 유도할 수 있다. 대응책은 두 가지다. 첫째, 명세에서 유도한 사람 작성 골든 테스트를 최소 한 벌 고정해 두고 모델이 수정할 수 없게 한다. 둘째, 모델이 만든 테스트는 참고용으로만 쓰고 최종 판정은 고정 테스트로 한다.
운영 시 주의점
반복 상한과 토큰 예산을 코드로 강제한다. 같은 오류가 두 번 반복되면 프롬프트 전략을 바꾸거나 조기 종료해 사람에게 넘긴다 — 동일 피드백은 대개 동일 실패를 낳는다. 실행 결과는 캐시해 중복 실행을 줄이고, 각 반복의 코드·피드백·판정을 로그로 남겨 회귀 분석과 비용 추적에 쓴다. 마지막으로 루프가 통과시킨 코드라도 병합 전 사람 리뷰를 거치는 것을 기본값으로 둔다. 자동 루프는 검증을 자동화할 뿐, 책임까지 넘기지는 못한다.