AI 코딩 도구의 활용과 검증
먼저 읽을 장 4.1 Git의 모델과 기본 작업
AI 코딩 도구는 연구 코드를 쓰는 속도를 크게 바꾸었다. 처음 보는 라이브러리의 사용법을 묻고, 데이터 변환 스크립트의 초안을 받고, 오류 메시지의 원인을 찾는 일이 몇 분 안에 끝난다. 그런데 연구 코드에는 일반 소프트웨어와 다른 위험이 있다. 웹 서비스의 버그는 대개 오류나 이상한 화면으로 드러나지만, 연구 코드의 버그는 그럴듯한 숫자로 드러난다. 데이터 분할이 조금 새거나 metric 정의가 조금 다르면 코드는 잘 돌고, 결과는 조금 좋아 보이고, 논문의 결론이 틀린다.
이 장은 AI 코딩 도구를 쓰는 법보다 검증하는 법에 무게를 둔다. 어떤 형태의 도구가 있고 각각 무엇을 맡기기 좋은지 정리한 뒤, AI가 만든 코드와 수식에서 무엇을 어떻게 확인해야 하는지를 계산으로 보이고, 검증 비용이 감당할 수 있는 크기가 되도록 작업을 나누는 법을 다룬다. 특정 제품의 기능·가격·모델 이름은 빠르게 바뀌므로 다루지 않는다. 여기서 다루는 검증 원칙은 도구가 바뀌어도 그대로다.
학습 목표
- 대화형·IDE형·에이전트형 도구의 차이를 안다
- AI가 만든 코드와 수식을 검증한다
- 생산성과 검증 비용의 균형을 잡는다
도구의 형태
AI 코딩 도구는 대부분 같은 종류의 언어 모델 위에 만들어지지만, 모델이 무엇을 볼 수 있고 무엇을 할 수 있는가에 따라 쓰임과 위험이 달라진다.
| 형태 | 모델이 보는 것 | 모델이 하는 것 | 결과를 적용하는 주체 |
|---|---|---|---|
| 대화형 assistant | 사용자가 붙여 넣은 코드·질문 | 텍스트(설명, 코드 조각) 생성 | 사람이 복사해 적용 |
| IDE 자동완성·편집 | 열린 파일과 주변 코드 | 다음 몇 줄 제안, 선택 영역 수정 | 사람이 수락 |
| 코딩 에이전트(터미널·클라우드) | 저장소 전체, 명령 실행 결과 | 파일 수정, 명령 실행, 결과를 보고 다시 시도 | 에이전트가 직접 |
대화형 assistant는 맥락이 사용자가 준 것으로 한정된다. 저장소의 다른 파일, 설치된 라이브러리 버전, 실제 데이터의 모양을 모르고 답한다. 그래서 그럴듯하지만 그 프로젝트에는 맞지 않는 코드가 나오기 쉽다. 대신 결과가 텍스트이므로 사람이 읽고 나서 적용한다.
IDE 자동완성은 열린 파일의 맥락을 보고 짧은 단위로 제안한다. 제안 하나는 짧아 검증하기 쉽지만, 수락 버튼을 누르는 속도가 읽는 속도보다 빨라지면 검증이 사라진다. 축 순서(axis=0과 axis=1), 부호, 단위가 자동완성에서 흔히 틀리는 곳이다.
코딩 에이전트는 저장소를 탐색하고, 파일을 고치고, 테스트를 실행하고, 실패하면 다시 고치는 반복을 스스로 한다. 큰 작업을 맡길 수 있다는 장점이 있지만, 이제 검증 대상이 "제안된 코드"에서 "실행된 행동과 바뀐 파일 전체"로 넓어진다. 에이전트가 테스트를 통과시키려고 테스트 자체를 고쳤는지, 설정 파일의 seed를 바꿨는지, 실패하는 데이터를 조용히 건너뛰게 했는지 확인해야 한다. 그리고 어떤 행동(파일 수정, 명령 실행, 네트워크 접근, push)을 허용할지 권한을 정해야 한다. 에이전트에게 주는 지침은 4.4장, 도구 연결과 권한은 4.5장, 에이전트를 쓰는 연구 작업 흐름은 4.6장에서 다룬다.
세 형태 모두 같은 원칙이 적용된다. 도구가 만든 결과의 책임은 그것을 commit한 사람에게 있다. 논문의 그림을 만든 코드에 버그가 있었을 때 "AI가 짠 코드였다"는 설명은 아무것도 해결하지 않는다.
잘 맡길 수 있는 일
AI 도구를 잘 쓰는 기준은 단순하다. 결과를 검증하는 비용이 직접 하는 비용보다 충분히 작은 일을 맡긴다. 이 기준으로 보면 연구에서 잘 맡길 수 있는 일이 분명해진다.
| 일 | 왜 잘 맞는가 | 검증 방법 |
|---|---|---|
| 코드 탐색·설명 | 처음 보는 저장소에서 "어디서 무엇을 하는가"를 빨리 찾는다 | 지목한 파일·함수를 직접 열어 확인 |
| 초안 작성 | 빈 파일에서 시작하는 비용을 줄인다 | 읽고 고친다. 초안은 초안이다 |
| 리팩터링 | 동작은 그대로, 구조만 바꾼다 | 기존 테스트 통과, 출력이 이전과 같음 |
| 테스트 작성 | 경계 조건·예외 사례를 빠짐없이 나열한다 | 테스트가 버그를 일부러 넣었을 때 실패하는지 |
| 문서·docstring | 코드에서 기계적으로 읽히는 부분 | 코드와 대조 |
| CLI·설정 파싱·파일 변환 | 정해진 형식을 다른 형식으로 옮긴다 | 입출력 예시로 확인 |
| 오류 분석 | 오류 메시지와 스택을 해석해 원인 후보를 낸다 | 후보를 실제로 재현해 확인 |
공통점은 정답을 확인할 독립적인 수단이 있다는 것이다. 리팩터링은 기존 테스트와 출력 비교로, 파일 변환은 입출력 예시로, 오류 분석은 재현으로 확인한다. 반대로 맡기기 어려운 일은 정답을 확인할 수단이 없거나, 확인하려면 처음부터 다시 해야 하는 일이다. 새 손실 함수의 유도, 실험 설계, 결과 해석이 그렇다. 이런 일에서 AI는 아이디어를 내는 상대로는 쓸 수 있지만 결과를 그대로 받아들일 대상은 아니다.
검증해야 하는 것
AI 도구가 내는 오류는 크게 두 종류다. 하나는 드러나는 오류다. 존재하지 않는 함수를 호출하거나, 없는 옵션을 쓰거나, 설치되지 않은 패키지를 import한다. 이런 오류는 실행하면 바로 실패하므로 비용이 작다. 다른 하나는 조용한 오류다. 코드는 잘 돌고 숫자도 그럴듯한데 틀렸다. 연구에서 위험한 것은 뒤쪽이다.
존재하지 않는 API. 모델은 그럴듯한 이름의 함수나 인자를 만들어 낼 수 있다. 실행하면 오류가 나므로 대부분 바로 잡히지만, 더 위험한 경우가 있다. 이름은 존재하는데 의미가 다른 경우다. 예를 들어 이름은 같지만 라이브러리 버전에 따라 기본값이 바뀐 인자, 비슷한 이름의 두 함수(분산의 자유도 보정 여부, 정규화 축) 같은 것이다. 쓰는 버전의 공식 문서에서 인자의 의미와 기본값을 확인한다.
잘못된 수식과 가짜 인용. AI가 유도한 수식은 부호, 계수, 전치가 틀리기 쉽고, 틀린 수식도 매끄러운 문장으로 설명된다. AI가 제시한 인용은 존재하지 않는 논문이거나, 실존 저자에 엉뚱한 제목을 붙였거나, 실제 논문이지만 그런 주장을 하지 않은 경우가 있다. 인용은 데이터베이스와 원문으로, 수식은 손으로 다시 유도하거나 작은 수치 예로 확인한다. 이 책의 저장소 지침(AGENTS.md)도 "존재를 직접 확인하지 않은 논문·API·수치를 쓰지 않는다. 인용은 원문을 확인한 것만"이라는 규칙을 에이전트에게 준다.
조용히 바뀐 실험 조건. 코드를 고쳐 달라고 했는데 seed, 데이터 경로, epoch 수, 전처리 옵션, 평가에 쓰는 split이 함께 바뀌는 경우가 있다. 요청한 변경과 무관해 보이는 줄이 diff에 있으면 반드시 이유를 확인한다. 4.1장의 git diff가 여기서 검증 도구가 된다.
이제 조용한 오류를 계산으로 보인다. 연구 코드에서 특히 확인할 곳은 수식↔코드 대응, shape, gradient, 데이터 분할, metric이다.
shape. NumPy와 PyTorch의 broadcasting은 shape이 맞지 않는 배열끼리의 연산을 오류 없이 수행한다. 모델 출력이 (N, 1)이고 정답이 (N,)이면, 그 차이는 (N, N) 행렬이 된다.
import numpy as np
rng = np.random.default_rng(0)
y = rng.normal(size=4) # (N,) 정답
pred = y[:, None] + 0.1 # (N, 1) 모델 출력이 열벡터로 나왔다
mse_wrong = ((pred - y) ** 2).mean() # (N, 1) - (N,) → (N, N)
mse_right = ((pred[:, 0] - y) ** 2).mean()
print("pred - y 의 shape:", (pred - y).shape)
print(f"틀린 MSE = {mse_wrong:.4f}, 맞는 MSE = {mse_right:.4f}")pred - y 의 shape: (4, 4) 틀린 MSE = 0.1689, 맞는 MSE = 0.0100
모든 예측이 정답보다 정확히 0.1 크므로 맞는 MSE는 이다. 틀린 코드는 모든 예측을 모든 정답과 비교한 평균을 내므로 17배 가까이 크다. 학습은 이 틀린 손실로도 진행되고 손실도 줄어든다. 손실이 줄어든다는 사실은 아무것도 보증하지 않는다. 방어는 간단하다. 손실 함수 첫 줄에 assert pred.shape == y.shape을 두고, 텐서에는 shape 주석을 단다.
gradient. AI가 손실 함수의 gradient를 손으로 유도해 구현했다면, 정의에서 바로 나오는 독립적인 계산인 유한차분과 비교한다. 파라미터 의 번째 성분에 대한 중앙 차분은
이다. 여기서 는 번째 성분만 1인 단위벡터, 는 작은 수다. 오차는 이다. 아래는 라벨 인 logistic 손실 의 gradient를 AI가 구현한 코드와, 손으로 다시 유도해 고친 코드를 비교한 것이다. 정답은 이고, 다.
def loss(w, X, s): # s: ±1 라벨 (N,), X: (N, D)
return np.mean(np.logaddexp(0.0, -s * (X @ w)))
def grad_ai(w, X, s): # AI가 쓴 gradient (검증 대상)
m = s * (X @ w) # (N,)
return -(X * (s / (1 + np.exp(-m)))[:, None]).mean(axis=0) # (D,)
def grad_num(w, X, s, h=1e-6): # 중앙 차분
E = np.eye(w.size) * h # (D, D)
return np.array([(loss(w + e, X, s) - loss(w - e, X, s)) / (2 * h) for e in E])
def grad_fixed(w, X, s): # 손으로 유도해 고친 gradient
m = s * (X @ w) # (N,)
return -(X * (s / (1 + np.exp(m)))[:, None]).mean(axis=0) # (D,)
X = rng.normal(size=(50, 3)) # (N, D)
s = np.sign(rng.normal(size=50)) # (N,)
w = rng.normal(size=3) # (D,)
g_num = grad_num(w, X, s)
rel = lambda g: np.linalg.norm(g - g_num) / np.linalg.norm(g_num)
print(f"AI gradient 상대 오차 {rel(grad_ai(w, X, s)):.1e}")
print(f"고친 gradient 상대 오차 {rel(grad_fixed(w, X, s)):.1e}")AI gradient 상대 오차 1.1e+00 고친 gradient 상대 오차 2.3e-10
AI의 코드는 자리에 을 썼다. 지수의 부호 하나 차이다. 상대 오차가 1을 넘으므로 방향부터 틀렸다. 고친 코드는 수준으로 유한차분과 일치한다. PyTorch처럼 자동미분을 쓰면 gradient를 손으로 쓸 일이 줄지만, 사용자 정의 backward, 수치적으로 안정화한 손실, 물리 모델의 adjoint처럼 손으로 쓰는 경우는 여전히 있다. 그때마다 이 검사를 테스트로 남긴다(자동미분과 gradient check의 정밀도 문제는 8.4장에서 자세히 다룬다).
데이터 분할. 연구 결론을 가장 크게 오염시키는 조용한 오류는 데이터 누수(leakage)다. test 데이터의 정보가 학습 과정 어딘가로 새어 들어가는 것이다. AI가 짠 파이프라인에서 흔한 형태는 전처리나 특징 선택을 분할 전에 전체 데이터로 하는 것이다. 극단적인 예로 확인한다. 입력은 순수한 잡음이고 라벨은 입력과 무관하게 무작위로 만들었으므로, 어떤 방법도 처음 보는 데이터에서 정확도 0.5를 의미 있게 넘을 수 없다.
from sklearn.feature_selection import SelectKBest, f_classif
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_val_score
from sklearn.pipeline import make_pipeline
X = rng.normal(size=(100, 5000)) # (N, D) 순수한 잡음
y = rng.integers(0, 2, size=100) # (N,) 입력과 무관한 라벨
X_sel = SelectKBest(f_classif, k=20).fit_transform(X, y) # 전체 데이터로 고른다
leaky = cross_val_score(LogisticRegression(), X_sel, y, cv=5).mean()
pipe = make_pipeline(SelectKBest(f_classif, k=20), LogisticRegression())
honest = cross_val_score(pipe, X, y, cv=5).mean() # fold 안에서 고른다
print(f"누수 있는 CV 정확도 {leaky:.2f}, 올바른 CV 정확도 {honest:.2f}")누수 있는 CV 정확도 0.81, 올바른 CV 정확도 0.52
신호가 전혀 없는 데이터에서 0.81이 나왔다. 5000개 특징 중 이 100개 표본에서 우연히 라벨과 상관된 20개를 전체 데이터로 골랐기 때문에, 교차검증의 각 test fold도 이미 선택 과정에 쓰인 셈이다. 특징 선택을 파이프라인에 넣어 fold 안에서만 하면 0.5 근처로 돌아온다. 같은 문제가 표준화 통계, 결측값 대체, 하이퍼파라미터 선택에도 있다. 또 행 단위로 무작위 분할했는데 같은 환자·같은 시료·같은 날의 측정이 train과 test에 섞여 있으면, 모델은 일반화가 아니라 개체를 외운다. 데이터 분할과 누수는 15.4장에서 체계적으로 다룬다.
metric. 함수 이름이 rmse라도 실제로 무엇을 계산하는지 읽는다. 평균 절대 오차를 RMSE라 부르거나, 마스크해야 할 격자점을 포함하거나, 클래스별 평균(macro)과 전체 평균(micro)을 섞거나, 정규화된 단위에서 계산한 값을 원래 단위로 보고하는 오류가 흔하다. 확인 방법은 손으로 답을 아는 작은 입력을 넣는 것이다. 잔차가 [0, 0, 0, 4]이면 MAE는 1, RMSE는 2다.
수식↔코드 대응. 위의 모든 검사를 묶는 원칙이다. 논문이나 노트의 수식 한 줄과 코드 한 줄이 대응하도록 쓰고, 대응을 주석으로 남긴다. 기호의 첨자가 코드의 어느 축인지(합이 batch 축에 대한 것인지 feature 축에 대한 것인지), 상수 계수(, )가 어디 있는지 대조한다. 이 책이 코드에 shape 주석과 단위 접미사를 요구하는 이유도 이 대조를 쉽게 하기 위해서다(editorial/style-guide.md §5).
작업 단위와 프롬프트
검증 비용은 요청의 크기에 따라 비선형으로 늘어난다. 50줄짜리 diff는 꼼꼼히 읽을 수 있지만, 2000줄짜리 diff는 읽는 척만 하게 된다. 그래서 요청을 설계하는 것이 곧 검증을 설계하는 것이다.
작고 검증 가능한 단위로 요청한다. 좋은 단위는 다음 조건을 만족한다.
- 목적이 하나다. 리팩터링과 기능 변경, 기능 변경과 실험 조건 변경을 한 요청에 섞지 않는다. 4.2장의 PR 단위와 같은 원칙이다.
- 완료 조건이 검증 가능하다. "더 좋게"가 아니라 "기존 테스트 통과, 출력 배열이 이전 구현과 같음, 새 테스트 3개 추가".
- diff를 한 번에 읽을 수 있다. 대략 한 화면에서 몇 화면 사이.
맥락을 준다. 도구가 모르는 것을 알려 준다. 어느 파일을 보면 되는지, 쓰는 라이브러리와 버전, 데이터의 shape과 단위, 유지해야 할 불변 조건(예: "test split은 건드리지 않는다"), 프로젝트의 규칙(코딩 스타일, 테스트 명령). 반복해서 주는 맥락은 매번 쓰지 말고 프로젝트 지침 파일에 한 번 적는다. 그것이 4.4장의 주제다.
완료 조건을 명시한다. 요청의 형식을 예로 들면 다음과 같다.
목표: data/loader.py의 load_field()가 HDF5 파일 전체를 메모리에 올리지 않고 chunk 단위로 읽게 한다.
맥락: 파일 하나는 약 40 GB, 배열 shape은 (T, H, W) = (8760, 721, 1440), float32.
반환 형식과 함수 시그니처는 바꾸지 않는다. tests/는 수정하지 않는다.
완료 조건:
1. pytest tests/test_loader.py 통과
2. tests/fixtures/small.h5에 대해 새 구현과 기존 구현의 출력이 np.array_equal로 같음
3. 바뀐 파일 목록과 각 변경의 이유를 요약
마지막 항목처럼 무엇을 바꿨는지 보고하게 하면 diff를 읽을 때 기준이 생긴다. 보고에 없는 변경이 diff에 있으면 그것부터 확인한다.
생산성과 검증 비용의 균형. AI 도구를 써서 빨라진 시간의 일부는 검증에 다시 써야 한다. 이 비용을 줄이는 방법은 검증을 생략하는 것이 아니라 검증을 자동화하는 것이다. 테스트, 유한차분 gradient check, shape assert, 작은 기준 데이터에 대한 출력 비교를 한 번 만들어 두면, 이후의 모든 AI 변경을 같은 기준으로 싸게 확인할 수 있다. 테스트가 없는 코드베이스에 AI 도구를 쓰기 시작한다면, 가장 먼저 맡길 일은 기존 동작을 고정하는 테스트를 쓰는 것이다(그 테스트도 사람이 검토한다). 테스트와 디버깅은 6.6장에서 다룬다.
실습
AI 코딩 도구가 작성했다고 가정한 학습 코드를 리뷰한다. 코드는 labs/git-and-agents/ai-coding-tools/review_target.py다. 요청한 과제는 "배터리 셀 8개의 충방전 기록으로 방전 용량(Ah)을 예측하는 선형 모델을 학습하고, 처음 보는 셀에 대한 RMSE를 보고하라. L2 정규화 세기는 검증으로 골라라"였다. 스크립트는 오류 없이 실행되고 다음을 출력한다(집필 시점 실행 결과).
lam=0 train loss=0.0064 test RMSE=0.0043 Ah lam=0.0001 train loss=0.0069 test RMSE=0.0043 Ah lam=0.001 train loss=0.0117 test RMSE=0.0057 Ah lam=0.01 train loss=0.0807 test RMSE=0.0554 Ah 최종: lam=0, 처음 보는 셀에 대한 RMSE = 0.0043 Ah
이 숫자는 요청한 질문에 대한 답이 아니다. 다음 순서로 리뷰한다.
- 실행하기 전에 읽는다. 요청과 코드를 대조해, 요청의 각 문장("처음 보는 셀", "RMSE", "검증으로 고른다")이 코드의 어디에 구현되었는지 표시한다.
- 네 관점으로 검사한다. shape(배열마다 shape을 주석으로 적고 연산마다 맞는지 확인), gradient(유한차분과 비교), 데이터 분할(train과 test의 셀 집합 비교), metric(손으로 답을 아는 입력으로 확인). 각 검사를 짧은 코드로 실제로 실행한다.
- 결함을 기록한다. 결함마다 위치, 관찰(재현 코드와 그 출력), 결론에 영향을 주는지, 수정 방법을 쓴다. 결론에 영향을 주는 결함과 그렇지 않은 결함을 구분한다.
- 고친 평가로 다시 잰다. 처음 보는 셀에 대한 오차를 올바른 절차로 계산하고, 보고된 0.0043 Ah와 비교한다.
리뷰를 마친 뒤 같은 폴더의 review_key.md(해설)와 test_review.py(각 결함이 실제로 결과를 바꾸는지 확인하는 테스트)를 열어 자신의 기록과 비교한다. 해설에 없는 결함을 찾았다면 그것도 기록한다. 마지막으로, 자신이 쓰는 AI 코딩 도구에 같은 과제를 요청하고 그 결과를 같은 네 관점으로 리뷰한다.
실패 사례
- 돌아가는 코드를 맞는 코드로 착각하는 것. 이 장의 세 예(broadcasting된 손실, 부호가 틀린 gradient, 누수된 특징 선택)는 모두 오류 없이 실행되었고, 손실은 줄었고, 숫자는 그럴듯했다. "실행된다"는 검증의 출발점이지 끝이 아니다. 특히 결과가 기대보다 좋을 때 더 의심한다. 누수와 평가 오류는 거의 항상 결과를 좋게 만드는 방향으로 작용한다.
- AI가 제안한 인용을 확인 없이 사용하는 것. 존재하지 않는 논문, 주장을 하지 않은 논문을 인용한 원고는 심사에서 신뢰를 잃고, 출판 뒤에 발견되면 정정 대상이 된다. 원문을 찾아 해당 부분을 읽은 인용만 쓴다.
- 에이전트가 테스트를 통과시키는 방법을 확인하지 않는 것. "테스트를 통과시켜라"는 요청에 에이전트가 테스트의 기대값을 바꾸거나, 실패하는 테스트를 건너뛰게 표시하거나, 예외를 삼키는 코드를 넣을 수 있다. 테스트 파일이 diff에 있으면 그 변경이 정당한지 따로 검토한다. 테스트 파일을 고치지 말라는 조건을 요청에 넣는 것도 방법이다.
- 검증을 다른 AI의 답으로 대신하는 것. 두 도구가 같은 답을 내도 둘 다 틀릴 수 있다. 검증은 독립적인 수단(테스트, 유한차분, 원문, 손 계산)으로 한다.
통과 기준
review_target.py를 리뷰해 shape·gradient·데이터 분할·metric 관점에서 결함을 기록하고, 각 결함을 재현하는 코드와 출력을 함께 남긴다. 그중 연구 결론(보고된 오차)에 영향을 주는 결함을 구분해 표시한다.- 고친 절차로 처음 보는 셀에 대한 RMSE를 다시 계산하고, 보고된 값과의 차이를 설명한다.
- 자신이 쓰는 AI 코딩 도구에 범위·맥락·완료 조건을 명시한 요청을 하나 작성해 결과를 받고, 완료 조건을 실제로 확인한 기록을 남긴다.