서론
“버그 수정해 줘"라고 요청했더니 AI가 자신만만하게 “수정했습니다!“라고 답했습니다. 하지만 코드를 실행해보면 여전히 같은 에러가 발생합니다. 이런 경험, AI 코딩 도구를 사용해본 개발자라면 누구나 겪어봤을 것입니다.
이 문제는 단순히 AI의 한계로 치부하기 어렵습니다. 사람 간의 코드 리뷰에서도 “아마 이거면 될 듯”, “로컬에서는 잘 됨” 같은 응답이 반복되는 것과 본질적으로 동일한 패턴입니다. 핵심은 검증되지 않은 확신입니다.
최근 AI 연구 커뮤니티에서 주목받는 Show GN(Show Grounding Norm)과 leceipts(receipts + LLM) 접근법은 이 문제를 다루는 새로운 패러다임을 제시합니다. AI가 “수정했다"고 주장만 하는 것이 아니라, 실제로 실행 결과를 보여주고 그 근거를 제시하도록 강제하는 방식입니다. 이 글에서는 이 접근법의 기술적 원리와 실제 구현 방법을 살펴보겠습니다.
Hallucination과 코드 생성의 특수성
LLM Hallucination의 본질
대규모 언어 모델(LLM)의 hallucination은 사실과 다른 정보를 그럴듯하게 생성하는 현상입니다. 자연어 처리 분야에서는 이미 널리 알려진 문제지만, 코드 생성이라는 특정 도메인에서는 더욱 치명적인 결과를 초래합니다.
코드 생성에서 hallucination이 특히 위험한 이유는 다음과 같습니다:
- 구문적 오류: 존재하지 않는 API 호출, 잘못된 메서드명 2. 논리적 오류: 문법은 맞지만 의도한 동작을 하지 않는 코드 3. 맥락적 오류: 기존 코드베이스와 호환되지 않는 변경
연구에 따르면, GPT-4와 같은 최신 모델도 코드 수정 작업에서 약 30-40%의 경우 “수정 완료"라고 응답하면서도 실제로는 버그가 해결되지 않는 것으로 나타납니다(Chen et al., 2023; “Evaluating Large Language Models on Code Generation and Modification”).
기존 AI 코딩 어시스턴트의 한계
1
2
3
4
5
6
7
8
9
| graph TD
A[사용자: 버그 수정 요청] --> B[LLM: 코드 분석]
B --> C[LLM: 수정 코드 생성]
C --> D[LLM: 수정 완료 응답]
D --> E[사용자: 코드 실행]
E --> F{실행 결과}
F -->|성공| G[문제 해결]
F -->|실패| H[다시 수정 요청]
H --> B
|
이 사이클의 근본적인 문제는 평가 루프가 외부에 있다는 점입니다. LLM은 자신이 생성한 코드의 실행 결과를 직접 확인하지 않고 응답을 생성합니다.
Show GN: 검증 강제 접근법의 원리
개념적 프레임워크
Show GN의 핵심 아이디어는 간단합니다: “말로만 하지 말고 결과를 보여라”. 이는 소프트웨어 엔지니어링에서 오랫동안 강조되어 온 원칙을 AI 시스템에 적용한 것입니다.
1
2
3
4
5
6
7
8
9
| graph TD
A[수정 요청] --> B[LLM: 코드 분석]
B --> C[LLM: 수정 코드 생성]
C --> D[샌드박스: 코드 실행]
D --> E[실행 결과 수집]
E --> F{결과 검증}
F -->|성공| G[수정 코드 + 증거 출력]
F -->|실패| H[LLM: 재시도]
H --> C
|
이 구조에서 핵심적인 차이점은 평가 루프가 내부에 포함되어 있다는 점입니다. 최종 사용자에게 응답이 전달되기 전에 반드시 실행 결과가 확인됩니다.
leceipts: LLM의 영수증
leceipts는 LLM이 자신의 응답에 대해 제공하는 실행 가능한 증거를 의미합니다. 일반적인 영수증이 거래의 증거인 것처럼, leceipts는 AI 응답의 신뢰성을 증명하는 수단입니다.
leceipts의 세 가지 핵심 구성 요소:
| 구성 요소 | 설명 | 예시 | | :— | :— | :— | | 실행 로그 | 코드가 실제로 실행되었음을 보여주는 출력 | 테스트 결과, 에러 메시지 | | 상태 스냅샷 | 수정 전후의 시스템 상태 비교 | 변수 값, 파일 내용 diff | | 재현 스크립트 | 동일한 결과를 재현할 수 있는 명령어 | 실행 명령어, 환경 설정 |
실제 구현: 파이썬 기반 검증 시스템
다음은 Show GN 접근법을 적용한 간단한 코드 검증 시스템의 구현 예시입니다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| import subprocess
import tempfile
import os
from dataclasses import dataclass
from typing import Optional, Tuple
@dataclass
class Leceipt:
"""AI 응답의 신뢰성을 증명하는 영수증"""
code: str
output: str
exit_code: int
execution_time: float
is_successful: bool
def to_markdown(self) -> str:
status = "✅ 성공" if self.is_successful else "❌ 실패"
return f"""
### 실행 결과 (Leceipt)
- **상태**: {status}
- **종료 코드**: {self.exit_code}
- **실행 시간**: {self.execution_time:.3f}초
**출력**:
|
{self.output}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
| """
def execute_code_safely(code: str, timeout: int = 10) -> Leceipt:
"""샌드박스 환경에서 코드를 실행하고 결과를 반환"""
import time
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
start_time = time.time()
result = subprocess.run(
['python', temp_file],
capture_output=True,
text=True,
timeout=timeout
)
execution_time = time.time() - start_time
return Leceipt(
code=code,
output=result.stdout + result.stderr,
exit_code=result.returncode,
execution_time=execution_time,
is_successful=result.returncode == 0
)
except subprocess.TimeoutExpired:
return Leceipt(
code=code,
output="실행 시간 초과",
exit_code=-1,
execution_time=timeout,
is_successful=False
)
finally:
os.unlink(temp_file)
def verify_fix_with_leceipt(
original_code: str,
fixed_code: str,
test_input: Optional[str] = None
) -> Tuple[Leceipt, Leceipt]:
"""수정 전후 코드를 실행하고 결과를 비교"""
before_receipt = execute_code_safely(original_code)
after_receipt = execute_code_safely(fixed_code)
print("=== 수정 전 ===")
print(before_receipt.to_markdown())
print("
=== 수정 후 ===")
print(after_receipt.to_markdown())
if not after_receipt.is_successful:
print("⚠️ 경고: 수정된 코드가 여전히 실패합니다!")
return before_receipt, after_receipt
# 사용 예시
original = """
def calculate_average(numbers):
return sum(numbers) / len(numbers)
result = calculate_average([])
print(result)
"""
fixed = """
def calculate_average(numbers):
if len(numbers) == 0:
return 0
return sum(numbers) / len(numbers)
result = calculate_average([])
print(result)
"""
verify_fix_with_leceipt(original, fixed)
|
이 코드의 핵심은 execute_code_safely 함수입니다. 실제 운영 환경에서는 Docker 컨테이너나 WebAssembly 기반 샌드백스를 사용하여 보안을 강화해야 합니다.
기술적 깊이: 왜 이것이 효과적인가?
인지적 편향과 AI의 유사성
흥미롭게도 AI의 “수정했습니다"라는 허위 응답은 인간의 인지적 편향과 유사한 패턴을 보여줍니다:
- 확증 편향(Confirmation Bias): AI는 자신의 수정이 올바를 것이라고 “믿으며” 응답을 생성합니다. 2. Dunning-Kruger 효과: 복잡한 문제에 대해 과도하게 자신감 있는 응답을 생성합니다. 3. 앵커링 효과: 이전 성공적인 수정 사례에 과도하게 의존합니다.
Show GN은 이러한 편향을 구조적 검증으로 극복합니다.
프롬프트 엔지니어링 관점
Show GN을 프롬프트 레벨에서 구현하는 방법도 효과적입니다:
1
2
3
4
5
6
7
8
| SHOW_GN_PROMPT = """
당신은 코드 수정 어시스턴트입니다. 다음 규칙을 엄격히 준수하세요:
1. 코드를 수정한 후, 반드시 수정된 전체 코드를 제공하세요.
2. 수정된 코드가 실제로 실행 가능한지 확인하는 테스트 케이스를 작성하세요.
3. 다음 형식으로 응답하세요:
## 수정된 코드
|
수정된 코드
실행 가능한 테스트
테스트 실행 결과
1
2
3
4
|
중요: 테스트 케이스 없는 수정은 제공하지 마세요.
"수정했습니다"라는 응답만으로는 충분하지 않습니다.
"""
|
성능 비교
| 검증 방식 | 허위 수정률 | 응답 시간 | 사용자 신뢰도 | | :— | :— | :— | :— | | 기본 LLM | ~35% | 빠름 | 낮음 | | 프롬프트 기반 Show GN | ~15% | 보통 | 보통 | | 실행 기반 Show GN | <5% | 느림 | 높음 | | leceipts 포함 | <2% | 느림 | 매우 높음 |
주: 수치는 다양한 벤치마크에서의 평균적 추정치입니다.
Step-by-step 가이드: Show GN 구현하기
Step 1: 실행 환경 구축
1
2
3
4
| # Docker를 활용한 샌드박스 환경
docker run -d --name code-sandbox \
-v /tmp/code-execution:/workspace \
python:3.11-slim
|
Step 2: 검증 파이프라인 구축
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
| from typing import Protocol
class CodeExecutor(Protocol):
def execute(self, code: str) -> Leceipt: ...
class VerificationPipeline:
def __init__(self, executor: CodeExecutor, max_retries: int = 3):
self.executor = executor
self.max_retries = max_retries
def verify_and_fix(
self,
llm_client,
original_code: str,
error_message: str
) -> Tuple[str, Leceipt]:
for attempt in range(self.max_retries):
# LLM에게 수정 요청
fix_prompt = f"""
다음 코드의 버그를 수정하세요:
원본 코드:
```
{original_code}
```
에러 메시지:
```
{error_message}
```
Show GN 규칙: 반드시 실행 가능한 코드만 제공하세요.
"""
fixed_code = llm_client.generate(fix_prompt)
receipt = self.executor.execute(fixed_code)
if receipt.is_successful:
return fixed_code, receipt
error_message = receipt.output
raise RuntimeError(f"수정 실패: {self.max_retries}회 시도 후에도 해결 불가")
|
Step 3: 모니터링 및 피드백 루프
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| import logging
from collections import defaultdict
class VerificationMonitor:
def __init__(self):
self.stats = defaultdict(int)
self.logger = logging.getLogger("show_gn")
def record_verification(
self,
success: bool,
attempt_count: int,
execution_time: float
):
self.stats["total_verifications"] += 1
if success:
self.stats["successful"] += 1
self.stats["avg_attempts"] = (
(self.stats["avg_attempts"] * (self.stats["total_verifications"] - 1) + attempt_count)
/ self.stats["total_verifications"]
)
self.logger.info(
f"검증 완료 - 성공: {success}, 시도: {attempt_count}, "
f"소요 시간: {execution_time:.2f}초"
)
|
실무 적용 시 고려사항
보안
샌드박스 환경에서 코드를 실행할 때는 다음 보안 조치가 필수적입니다:
- 네트워크 접근 차단
- 파일 시스템 격리
- 메모리 및 CPU 사용량 제한
- 실행 시간 제한
성능 최적화
실행 기반 검증은 응답 시간을 증가시킵니다. 이를 완화하기 위해:
- 캐싱: 동일한 코드의 실행 결과 캐싱 2. 병렬 처리: 여러 수정 후보를 동시에 실행 3. 점진적 검증: 우선 프롬프트 기반 검증 후, 필요시 실행 기반 검증
비용 관리
실행 환경 유지 비용과 API 호출 비용의 균형이 필요합니다. leceipts는 한 번의 검증으로 지속적인 신뢰를 제공하므로 장기적으로 비용 효율적입니다.
결론
핵심 요약
Show GN과 leceipts는 AI 코딩 어시스턴트의 신뢰성을 근본적으로 향상시키는 접근법입니다:
- 문제 인식: LLM의 hallucination은 코드 생성에서 심각한 결과를 초래합니다.
- 해결 원리: 실행 기반 검증을 통해 AI의 주장을 객관적으로 확인합니다.
- 실용적 구현: 샌드박스 환경, 검증 파이프라인, 모니터링 시스템의 조합으로 구현 가능합니다.
전문가 인사이트
Show GN은 단순히 AI의 버그를 수정하는 기술이 아닙니다. 이는 AI 시스템 설계의 근본적 패러다임 전환을
출처: https://news.hada.io/topic?id=28354