에이전트가 "완료"라고 했지만 기능이 작동하지 않는다: AI 에이전트 작업을 검증하는 방법
에이전트가 "완료"라고 말하는 이유는 스스로 작업을 평가하고 자신이 검증한 것만 보기 때문입니다. 보통 unit 테스트뿐이고, 이는 모듈 사이의 연결, migration, 설정, 엔드투엔드 시나리오를 보지 못합니다. 해결책은 프롬프트가 아니라 구조입니다: AGENTS.md의 Definition of Done, 필수 E2E 검사, 통과 표시만 찍을 수 있는 검증 스크립트, 그리고 깨끗한 context를 가진 독립 평가자.
왜 이런 일이 생기는가
AI 에이전트와 일해 본 사람이라면 누구나 아는 상황입니다: 에이전트가 "완료"라고 했지만 기능은 작동하지 않습니다. 에이전트가 일부러 거짓말하는 것은 아닙니다. 자신이 검증한 것을 보고하고, 그것으로 충분하다고 진심으로 믿습니다. 보고와 현실 사이의 간극은 특정 모델의 결함이 아니라, "실행자가 자신의 작업을 스스로 평가한다"는 구조 자체의 속성입니다.
세 가지 이유가 있습니다. 첫째, 모델은 과신하는 경향이 있습니다. 응답의 확신이 정확도보다 큽니다. 둘째, 에이전트는 자신이 작업한 context 안에서 그 작업을 평가합니다. 해결책이 맞다고 이미 스스로를 설득했고, "더 객관적으로 봐"라고 요청해도 context는 바뀌지 않습니다. 셋째, 가장 실용적인 이유: 에이전트가 쓸 수 있는 검증은 부분적입니다. unit 테스트는 빨리 초록색이 되지만, 엔드투엔드 시나리오는 아무도 실행하지 않았습니다.
잘못된 "완료"의 이야기
에이전트에게 비밀번호 재설정 작업이 주어졌습니다. 데이터베이스 스키마를 바꾸고, 엔드포인트를 추가하고, 이메일 템플릿을 연결하고, unit 테스트를 실행했습니다. 모두 초록색입니다. 채팅창에는 자신 있게 "완료"라고 적혔습니다.
실제로는 다음과 같았습니다:
- migration이 중간에 실패해서 스키마가 일관성 없는 상태로 남았습니다;
- 이메일 템플릿은 있지만, 메일 서버(SMTP)가 환경에 설정되지 않아 메일이 나가지 않습니다;
- "사용자가 링크를 누른다 → 새 비밀번호를 입력한다 → 로그인한다" 시나리오는 한 번도 실행되지 않았습니다.
이 문제들 중 어느 것도 unit 테스트가 잡을 수 없었습니다: 데이터베이스는 mock이었고, 메일 발송도 mock이었으며, 엔드투엔드 실행은 아예 없었습니다.
초록색 테스트가 기능이 작동한다는 증거가 아닌 이유
Unit 테스트는 모듈을 격리해서 검사합니다. 의존성은 mock으로 대체되고 외부 세계는 꺼집니다. 이는 속도와 정확성을 주지만, 체계적인 맹점도 만듭니다. 이 맹점은 테스트의 결함이 아니라 설계의 결과입니다. 초록색 unit 테스트는 각 부품이 따로 작동한다는 것만 증명하고, 부품들이 함께 작동한다는 것은 증명하지 않습니다.
unit 테스트가 결코 보지 못하는 네 가지 종류의 결함이 있습니다:
| 결함 종류 | 예시 | unit 테스트가 못 잡는 이유 |
|---|---|---|
| 인터페이스 불일치 | 한 컴포넌트는 상대 경로를 주고, 다른 컴포넌트는 절대 경로를 기대함 | 각각 자신의 mock으로 검사되고, mock은 어떤 형식이든 받아들임 |
| 상태 전파 | 스키마는 바뀌었지만 ORM 캐시는 예전 상태임 | unit 테스트에는 실제 데이터베이스도 캐시도 없음 |
| 자원의 생명주기 | 파일 디스크립터와 연결이 닫히지 않고, 큰 파일에서 메모리가 샘 | 테스트는 작은 데이터로 돌고 자원이 바닥나기 전에 끝남 |
| 환경과 설정 | 환경 변수가 없고, SMTP가 설정되지 않았고, migration이 적용되지 않음 | 테스트는 실제 설정으로 애플리케이션을 실행하지 않음 |
검사 단계는 서로 포개져 있습니다: unit 테스트가 잡는 것은 integration 테스트도 잡고, integration 테스트가 잡는 것은 엔드투엔드(E2E) 실행도 잡습니다. 각 상위 단계는 하위 단계보다 엄격하게 더 많이 봅니다. 그래서 "완료"라고 말할 권한은 가장 상위 단계에만 있어야 합니다.
verification gap 측정하기
고치기 전에 세어 보세요. 에이전트가 마감한 최근 10개 작업을 고르세요. 각각에 대해 두 가지 질문에 답하세요: 에이전트가 "완료"라고 했는가, 그리고 독립된 검사가 이를 확인했는가(여러분이 시나리오를 직접 실행했는가, E2E 테스트가 초록색인가)? 이 둘이 어긋나는 비율이 여러분의 verification gap입니다.
이 숫자는 두 가지 이유로 유용합니다. "네, 잘 되는 것 같아요"라는 논쟁 없이 문제의 규모를 보여 줍니다. 그리고 기준선이 됩니다: 모든 변경 후 비슷한 작업들로 다시 계산해서 효과가 있었는지 확인합니다.
실제로 검증하는 방법: 세 계층 검사
유용한 비유: 자동차는 정비소의 세 개 작업대를 차례로 지나가며, 어느 것도 건너뛸 수 없습니다.
- 진단 (초 단위): 문법, 타입, linter. 오타와 거친 실수를 잡습니다.
- 테스트 벤치 (분 단위): unit과 integration 테스트, 애플리케이션이 기동함. 엔진은 돌아가지만 차는 아직 제자리에 있습니다.
- 시운전: 사용자가 실행하는 그대로의 엔드투엔드 시나리오. API, 브라우저, CLI를 통해, 실제 데이터베이스와 설정으로.
진행 규칙: 진단에 오류가 남아 있으면 시운전에 나가지 않고, 시운전을 통과하지 않으면 차를 인도하지 않습니다. 초록색 unit 테스트는 두 번째 작업대를 통과한 것이지, 세 번째를 통과한 것이 아닙니다.
모든 것을 하나의 진입점에 모으는 것이 편리합니다:
# Makefile
check:
ruff check .
mypy app
pytest tests/unit tests/integration
e2e:
docker compose up -d db mailhog
alembic upgrade head
pytest tests/e2e
verify: check e2e
여기서 E2E는 실제 데이터베이스와 테스트 메일 서버를 띄우고, migration을 적용하고, 시나리오를 처음부터 끝까지 실행합니다. 깨진 migration이나 설정되지 않은 메일은 고객이 아니라 바로 여기서 드러납니다.
AGENTS.md의 Definition of Done
에이전트가 "완료"가 무엇을 뜻하는지 스스로 정하게 해서는 안 됩니다. 기준은 지침 파일에 적고, 검증 가능한 형태로 표현해야 합니다:
## Definition of Done
기능은 아래 항목을 모두 충족할 때만 완료입니다:
1. make check가 코드 0으로 끝남
2. make e2e가 코드 0으로 끝남, 기능의 시나리오가 tests/e2e에 있음
3. Migration이 빈 데이터베이스에 적용되고, 되돌릴 수도 있음
4. 새 환경 변수가 .env.example에 기술됨
5. PROGRESS.md에 무엇을 했고 어떻게 검증했는지 기록됨
완료의 증거로 인정하지 않는 것:
- "코드를 작성했고 맞아 보인다"
- "unit 테스트가 통과한다"
- "내 로컬에서는 된다"
무엇이 완료가 아닌지를 명시적으로 나열하는 것은 기대보다 효과적입니다: 에이전트가 첫 번째 초록색 실행에서 멈추지 않게 됩니다.
누가 passing 표시를 찍는가
다음 단계는 완료 표시를 에이전트의 손에서 완전히 빼앗는 것입니다. 기능은 목록에 보관합니다(예: feature_list.json). 필드는 "동작", "검증 명령", "상태"입니다. 기능을 passing으로 바꿀 수 있는 것은 스크립트뿐입니다: 공통 make check를 실행하고, 그다음 해당 기능의 검증 명령을 실행해서, 성공하면 상태를 커밋 해시와 함께 증거로 기록합니다. 통과하지 못하면 기능은 활성 상태로 남습니다. "거의 됐다"는 없습니다.
{
"id": "F04",
"behavior": "사용자가 링크가 담긴 이메일을 받고 비밀번호를 변경한다",
"verification": "pytest tests/e2e/test_password_reset.py",
"state": "active"
}
여기에 함정이 있습니다: 스크립트는 verification 필드에 적힌 것만 검사합니다. 아키텍처 검사가 Definition of Done에는 있지만 기능의 명령에는 없다면, 그 구멍은 보이지 않은 채 남습니다. 그래서 공통 make check에는 테스트뿐 아니라 모든 필수 검사가 포함되어야 합니다.
독립 평가자
실행자의 자체 보고는 결코 독립적인 진실의 출처가 아닙니다. 해결책은 역할을 나누는 것입니다:
- 계획자는 요구사항을 인수 기준이 있는 기능으로 나누고 코드는 쓰지 않습니다;
- 생성자는 기능 하나를 구현하고 테스트를 실행하지만, 완료 여부를 결정하지 않습니다;
- 평가자는 체크리스트의 기준에 따라 판정을 내립니다.
평가자는 생성자의 추론을 보면 안 됩니다. 그러면 생성자의 확신을 물려받게 됩니다. 평가자는 요구사항, diff, 실행 결과만 받아야 합니다. Claude Code에서는 자체 깨끗한 context를 가진 subagent로 쉽게 구현할 수 있습니다. 더 좋은 방법은 평가자가 다른 모델인 것입니다. 맹점이 다르기 때문입니다. "괜찮아 보인다"식 평가는 금지입니다: 모든 결론은 파일, 줄, 로그를 근거로 해야 합니다.
에이전트가 스스로 고칠 수 있는 오류 메시지
검사는 출력이 수정으로 이어질 때만 유용합니다. "Test failed"나 맨 AssertionError는 에이전트에게 아무것도 알려 주지 않습니다. 좋은 메시지는 무엇이 깨졌는지, 규칙이 왜 존재하는지, 어떻게 고칠지를 담습니다:
ERROR arch-check: app/services/refund.py가 app/db/session.py를 import함
WHY: 서비스는 데이터베이스와 직접 통신하지 않고 repository를 통해서만 함
FIX: app/repositories/refund.py의 RefundRepository를 사용하세요
이런 형식은 검사를 금지에서 제안으로 바꿉니다: 에이전트가 여러분의 도움 없이 위반을 고칩니다.
사람의 검사가 여전히 필요한 곳
자동화가 사람을 없애지는 않고, 역할을 좁힐 뿐입니다. 기준을 형식화하기 어려운 곳에서는 사람의 검사가 필요합니다: 인터페이스의 외관, 문구, 시나리오의 사용 편의성. "대충 클릭해 보기"가 아니라 인수 기준으로 이루어진 체크리스트로 하세요. 같은 의견을 에이전트에게 두 번째로 쓰고 있다면, 그것을 자동 검사로 바꿀 신호입니다: 테스트, linter 규칙, 또는 make check 안의 grep-guard.
E2E는 실행 전에 에이전트의 행동을 바꾼다
엔드투엔드 테스트의 가치는 잡아낸 버그에만 있지 않습니다. 에이전트가 자신의 작업이 E2E 시나리오로 검사된다는 것을 알면, 다르게 코딩합니다: 자기 함수만이 아니라 이웃 컴포넌트와의 계약을 생각하고, 오류 경로를 처리하고, 아키텍처 경계를 어기지 않습니다. 위반이 실행에서 드러날 것이기 때문입니다. 검사는 출력에서의 필터일 뿐 아니라 입력에서의 경계로 작동합니다.
조건이 하나 있습니다: E2E는 시스템의 경계가 명확할 때만 의미가 있습니다. 스파게티 아키텍처에서 엔드투엔드 테스트는 스파게티가 돌아간다는 것만 증명하고, 설계가 어디서 어긋났는지는 보여 주지 않습니다. 그러니 먼저 계층과 의존성 규칙을 고정하고, 그다음에 엔드투엔드 시나리오를 만드세요. 에이전트는 저장소의 기존 패턴을 좋은 것이든 나쁜 것이든 쉽게 복사하므로, 경계는 첫날부터 두세요.
흔한 실수
- 에이전트에게 "더 주의 깊게 검사해"라고 요청하기. context는 같고, 평가는 여전히 부풀려집니다.
- 100% 커버리지를 보장으로 여기기. Unit 커버리지는 연결부를 덮지 않습니다.
- 기능이 확인되기 전에 리팩터링하기. 순서는 엄격합니다: 정확성 → 성능 → 스타일.
- 가장 자주 깨지는 것을 E2E에서 mock하기. 데이터베이스, migration, 설정은 엔드투엔드 테스트에서 실제로 돌아야 합니다. 유료 외부 API는 테스트 서버로 대체해도 됩니다.
마무리
에이전트가 "완료"라고 했는데 기능이 작동하지 않을 때, 문제는 거의 항상 모델이 아니라 검증 과정에 있습니다. verification gap을 측정하고, AGENTS.md에 Definition of Done을 적고, E2E를 필수로 만들고, 완료 표시는 스크립트에 맡기고, 독립 평가자를 추가하세요. 이 방식은 실제 프로젝트를 중심으로 한 실습 과정에서 자세히 다룹니다.
자주 묻는 질문
에이전트가 E2E 테스트를 직접 실행하게 만들 수 있나요?
네, 명령이 AGENTS.md에 Definition of Done의 필수 부분으로 적혀 있고 필요한 도구에 접근할 수 있다면 가능합니다. 에이전트의 기억에 의존하지 않도록 hook이나 CI로도 실행하는 것이 더 안전합니다.
E2E 테스트는 느린데, 변경할 때마다 돌려야 하나요?
아닙니다. Lint, 타입, unit 테스트는 항상 돌리고, E2E는 여러 계층에 영향을 주는 변경이 있을 때와 기능을 완료로 선언하기 전에 돌리세요.
에이전트가 만든 프론트엔드는 어떻게 검사하나요?
실제 화면을 클릭하는 브라우저 기반 엔드투엔드 테스트(예: Playwright)로, 외관은 체크리스트에 따른 수동 검사나 평가자가 보는 스크린샷으로 검사합니다.
평가자 대신 코드 리뷰만으로 충분하지 않나요?
사람의 리뷰는 유용하지만 비싸고, 몇 시간씩 이어지는 자율 작업으로는 확장되지 않습니다. 루브릭을 갖춘 평가자가 명백한 문제를 사람에게 닿기 전에 걸러 내고, 쟁점이 되는 문제는 사람이 결정합니다.
이 주제는 “Harness Engineering: инженерия надёжности AI-агентов” 강의에서 실습과 함께 자세히 다룹니다.
- Агент написал «готово», а фича не работает: verification gap и Definition of Done
- Все тесты зелёные, а фича сломана: E2E-тесты и архитектурные правила для AI-агента