에이전틱 검증

TMT

https://addyo.substack.com/p/agentic-verification

AI 에이전트에게 자기 작업을 독립적으로 검증할 수단을 주면 결과물의 품질이 크게 좋아질 수 있습니다. 에이전트는 “대규모로 보면 평균적으로 훨씬 나은” 코드를 작성합니다. 여기서 “대규모로”라는 부분은 검증에서 나옵니다. 에이전트가 있으면 검사 비용이 싸집니다.

“좋은 코드”를 비싸게 만들던 것들(테스트, 벤치마크, 명세, 실제 E2E 실행)이 이제는 에이전트에게 부탁하기만 하면 되는 값싼 일이 되었습니다. 검사 자체를 더 낫게 만들 수도 있습니다.

그래서 우리의 일은 “모든 줄을 리뷰하기”보다 “에이전트가 스스로 돌릴 수 있는 루프 만들기”에 가까워졌습니다. 에이전트에게 작업을 검증할 수단을 주세요. 품질은 바로 그 루프에서 나옵니다.

그리고 안목이 복리로 쌓이게 하세요. 에이전트가 무언가를 잘못하면 그 내용을 기록해 두세요(공유 설정, 훅, 테스트 등). 그러면 같은 실수가 두 번 일어나지 않습니다. 무엇을 내보낼지는 여전히 사람이 책임지지만, 이제 사람은 diff만 보는 것이 아니라 증거를 검토합니다.

에이전트가 고품질의 결과물을 내게 하려면, 자기 작업을 검사할 수단을 쥐여 주세요. 테스트는 그 수단의 일부입니다.

제가 투자할 가치가 있다고 생각하는 것들은 다음과 같습니다.

- 실제 사용자 흐름을 시뮬레이션하고 정답 기준(ground truth) 역할을 하는 엔드투엔드 테스트를 작성하세요
- 속성 기반 테스트로 절대 일어나면 안 되는 일을 명시하고, 그 제약을 깨뜨리려는 테스트 케이스를 수천 개 생성하세요
- 시스템을 교체한다면 무작위로 고른 입력으로 기존 버전과 새 버전을 비교하세요
- 테스트가 빠르고 결정적으로 실행되도록 하세요. 루프가 느리거나 불안정하면 에이전트는 재시도하는 쪽으로 길들여집니다(문제를 고치는 대신 재시도하는 법을 배울 수 있습니다)

예시 테스트는 무엇이 일어나야 하는지를 말하고, 속성은 무엇이 절대 일어나면 안 되는지를 말합니다.

Image

병목은 검증입니다

오늘날의 모델은 충분히 뛰어나서, 이제 가장 큰 병목은 코드를 작성하는 일이 아니라 모델이 작성한 코드가 올바른지 검증하는 능력입니다.

에이전트가 자기 결과물을 검증하지 못한다면 사실상 짐작으로 일하는 셈이고, 잘못 짐작한 결과는 결국 여러분의 테스트 스위트에서 검증받게 됩니다. 다시 말해, 여러분이 diff를 읽고, 테스트를 돌리고, 앱을 직접 확인하고, 버그를 찾아 다시 알려 주기를 되풀이하는 피드백 루프가 생깁니다. 이것은 가장 느리고 비용이 많이 드는 피드백 루프이며, 사람이 한 명만 넘어가도 확장되지 않습니다.

하지만 에이전트가 자기 결과물을 테스트할 수 있다면, 변경을 작성하고, 검사를 실행하고, 출력을 읽고, 검사를 통과할 때까지 몇 번이고 다시 시도할 것입니다. 이 모든 일이 사람이 무언가를 보기도 전에 일어납니다. 스스로 검증하는 에이전트가 사람이 주도하는 피드백 루프보다 품질을 얼마나 끌어올리는지는 아무리 강조해도 지나치지 않습니다. 저라면 언제나 약간 더 나은 모델보다 훨씬 나은 피드백 루프를 택하겠습니다.

Image

테스트의 의미가 새로 정의되는 셈입니다. 수년 동안 테스트는 사람이 버그를 찾고 안심하고 리팩터링하는 데 쓰는 주된 도구였습니다. 이것은 지금도 테스트를 작성할 좋은 이유이지만, 이제 테스트는 에이전트가 세상을 감지하고 언제 일이 끝났는지 아는 수단이기도 합니다.

이 목적에서는 모든 테스트가 똑같이 가치 있지는 않습니다.

테스트에도 여러 종류가 있습니다

E2E 테스트

테스트를 딱 한 종류만 고르라면 E2E 테스트입니다. E2E 테스트는 실제 사용자가 하는 일을 기술합니다.

E2E 테스트는 사람이 할 법한 일을 그대로 합니다. 가입하고, 장바구니에 상품을 담고, 결제하고, 확인 화면을 봅니다. 코드의 내부 구조는 무시하고 사용자가 볼 수 있는 것만 봅니다.

에이전트에게 필요한 것이 바로 이런 오라클(정답을 판정하는 기준)입니다. 기계는 검사는 통과하지만 큰 그림을 놓치는 코드를 자주 작성합니다. 목(mock) 기반 단위 테스트는 통과하는데도 기능은 망가져 있을 수 있습니다. 에이전트와 테스트가 같은 오해를 공유하기 때문입니다. 반면 E2E 테스트는 속이기가 매우 어렵습니다. 전체가 동작하거나, 동작하지 않거나 둘 중 하나입니다.

Image

CI/CD 파이프라인에서 쓰이는 것 말고도, E2E 테스트가 에이전트에게 특히 가치 있는 이유는 무엇일까요?

- 가능한 모든 경로가 아니라 중요하고 의미 있는 사용자 흐름만 다룹니다. - 무엇이 잘못됐는지 알려 줍니다. 스크린샷, 트레이스, 어느 단계에서 실패했는지에 대한 설명이 있으면, 에이전트는 어둠 속에서 허우적대지 않고 무엇이 잘못됐는지 알 수 있습니다. 반면 “30초 후 타임아웃”이라는 메시지로는 아무것도 알 수 없습니다. - 에이전트의 로컬 구성에서도, 배포 환경에서도 동작합니다. - “완료”를 정의합니다. “이 기능을 구현해”는 너무 막연합니다. “이 E2E 테스트를 통과시켜”는 에이전트가 스스로 쫓을 수 있는 목표입니다.

E2E 테스트에 분명한 트레이드오프가 있다는 점도 짚고 넘어가야 합니다. 전통적인 단위 테스트보다 느리고 더 불안정합니다. 그렇다고 E2E 테스트를 없애자는 것이 아니라, 가치가 높은 소수의 테스트를 우선하자는 것입니다.

참고: E2E 테스트는 훌륭하지만, Lauren(@poteto)이 강조하듯 실제로 돌아가는 시스템을 만지고 볼 수 있는 손과 눈을 에이전트에게 주는 것도 그만한 가치가 있습니다. 앱을 실행하고, 조작하고, 관찰하는 방법(CDP, 시뮬레이터, API 하네스 등)을 아는 스킬이나 CLI를 꾸준히 관리하고, 여기에 기능 지도(feature map)를 더하는 것입니다. 기능 지도는 사용자 관점에서 정리한 목록으로, 어떤 기능이 있는지, 실제 사용자가 그 기능에 어떻게 도달하는지, 키보드 단축키와 셀렉터, 기대 결과와 주의할 점을 담습니다.

속성 테스트: 절대 깨지면 안 되는 제약

많은 종류의 테스트는 사람이 보기에 잘못될 수 있는 부분을 다룹니다. 속성 테스트는 사람이 미처 생각하지 못한 부분, 즉 에이전트가 실수하는 경우를 다룹니다.

사람이 테스트에 쓰는 내용은 대부분 예시입니다. “입력이 이렇다면 출력은 이래야 한다”는 식입니다. 이런 테스트는 무언가가 동작해야 함을 보장하는 데는 좋지만, 무언가가 절대 실패하지 않아야 함을 보장하는 데는 약합니다. 사람은 실패를 일으킬 수 있는 입력을 전부 떠올릴 수 없기 때문입니다.

Image

속성 기반 테스트는 전제를 뒤집습니다. 항상 성립해야 하는 규칙을 하나 만들면, 도구가 그 규칙을 깨뜨리려고 수백, 수천 개의 입력을 생성합니다. JS/TS의 fast-check, Python의 Hypothesis, Rust의 proptest 같은 도구가 이 일을 잘합니다. 실패를 찾으면 도구는 그 입력을 테스트를 깨뜨리는 최소 사례로 줄여 줍니다. 그래서 알아보기 힘든 무작위 데이터 덩어리를 던져 놓고 끝나지 않습니다.

거의 모든 소프트웨어 프로젝트에서 흔히 쓰이는 속성은 다음과 같습니다.

  • 왕복 인코딩: 값을 인코딩했다가 디코딩하면 처음 값과 같습니다. 값을 파싱하고 직렬화하면 처음 값이 나옵니다.
  • 불변 조건: 정렬해도 컬렉션의 원소 개수나 내용은 바뀌지 않습니다. 잔액은 절대 음수가 되지 않습니다. 들어온 돈과 나간 돈은 맞아떨어집니다.
  • 멱등성: 같은 대상을 두 번 마이그레이션하거나, 정규화하거나, 재시도해도 한 번 했을 때와 결과가 같습니다.
  • 동등성: 최적화한 함수는 단순하고 올바른 함수와 같은 결과를 냅니다.

에이전트와 함께 일할 때는 이 점이 매우 중요합니다. 에이전트는 자기 내부 논리에 따라 통과할 만한 테스트 예시를 스스로 지어내기 때문입니다. 속성을 쓰면 에이전트는 자기가 고른 입력뿐 아니라 생성된 모든 입력을 감당해야 합니다.

참고: 속성 테스트와 차분 검사로 많은 부분을 다룰 수 있지만, Claude Code의 Boris Cherny는 가장 까다로운 상태 기계, 동시성, 데이터 흐름 부분에 형식 모델(Lean, TLA+)을 활용해 왔습니다. 짧은 프롬프트만으로도 모델을 만들고, 반례를 찾고, 재현하고, 고치는 과정을 거쳐 경쟁 조건과 미묘한 버그를 드러낼 수 있습니다.

속성은 요구 사항을 명세하는 훌륭한 방법입니다. 장바구니 합계는 항상 개별 품목 금액의 합과 같아야 한다고 정하면, 그것만으로 훌륭한 명세가 됩니다. 에이전트는 그 명세를 바탕으로 코드를 짤 수 있고, 생성기는 어긋나는 부분을 찾아낼 수 있습니다.

한꺼번에 다 하려 들지 않고 시작하는 방법:

  • 파서, 포매터, 가격 계산처럼 작고 사실상 순수한 함수부터 시작하세요.
  • 코드로 구현하기 전에 속성을 평이한 말로 먼저 써 보세요.
  • 테스트 라이브러리가 문제를 찾으면, 최소화된 테스트 사례를 저장해 회귀 테스트에 쓰세요.
  • 새 코드를 만들 때는 에이전트에게 테스트 예시 대신 속성을 요청하세요. 그렇게 하라고 말해 주기만 하면 에이전트는 이 일을 아주 잘합니다.
  • 기존 시스템을 새 시스템으로 교체할 때는 두 시스템의 차이(diff)를 뽑아 보세요.
  • 재작성이나 마이그레이션을 할 때는 기존 시스템을 오라클로 삼으세요.

재작성이 흔히 실패하는 전형적인 방식 하나는, 새 테스트 스위트는 통과하지만 문서화되지 않은 작은 부분 여러 곳에서 기존 시스템과 달라지는 것입니다. 공식 명세와 실제 동작 사이에 차이가 생기는 것입니다. 사용자는 명세상 동작해야 하는 방식이 아니라 실제로 동작하는 방식에 기대게 됩니다.

차분 테스트: 기존 시스템이 오라클입니다

차분 테스트(differential test)는 그 간극을 메웁니다. 새 테스트 스위트에 기대는 대신, 같은 입력으로 기존 시스템과 새 시스템을 나란히 실행하세요. 출력이 어긋난다면 새 시스템에 버그를 넣었거나, 의도적으로 바꾼 것입니다. 의도한 변경이라면 호환성을 깨는 변경(breaking change)으로 문서화해야 합니다.

Image

여기서는 입력을 어떻게 고르느냐가 중요합니다. 직접 손으로 고른 테스트 사례만 쓰면 드문 엣지 케이스를 놓칩니다. 입력을 무작위로 생성하거나, 실제 트래픽 일부를 재생하거나, 둘 다 하세요. 이는 새 시스템의 출력이 기존 시스템의 출력과 같다는 속성을 둔 속성 기반 테스트와 같습니다.

차분 테스트는 에이전트를 활용한 대규모 마이그레이션에 아주 잘 맞습니다. 새 시스템을 조정하고, 차분 테스트를 실행하고, 불일치를 검토하고, 고친 다음 다시 루프를 시작하는 과정을 반복하면 됩니다. 기존 시스템이 오라클이므로 에이전트에게 엣지 케이스를 일일이 설명할 필요가 없습니다. 두 버전을 모두 만들고 diff를 돌려 본 팀들은, 하나를 고치면 다른 데가 망가지던 상황에서 비교 결과를 차근차근 체계적으로 해결해 나가는 상황으로 바뀌었다고 말합니다.

실무 참고 사항 몇 가지:

  • 중요하지 않은 차이와 의미가 없는 순서는 모두 정규화하세요. 비교하기 전에 이런 차이를 걷어 내야 거짓 diff가 끝없이 쏟아지는 일을 막을 수 있습니다.
  • 의도한 변경은 승인된 변경 목록으로 관리하세요. 바꾸려는 모든 변경을 이유와 함께 기록하는 것입니다. 그래야 “다르다”는 상태가 그냥 용인되는 일을 막을 수 있습니다.
  • 확신을 얻는 비결은 마지막 한 조각까지 모두 옮기는 것입니다. diff 하네스가 있으면 기존 시스템을 걷어 낼 확신이 생깁니다. 두 시스템을 계속 함께 운영하면 어느 한쪽을 이해하는 데 드는 일이 두 배가 되고, 마이그레이션이 깨질 수 있는 경로도 두 갈래로 늘어납니다.

루프를 빠르고 결정적으로 만드세요

테스트는 빠르고 결정적이어야 합니다. 그렇지 않으면 에이전트는 엉뚱한 교훈을 얻습니다.

이것은 단순히 시간을 잡아먹는 문제가 아니라 일종의 훈련입니다. 테스트가 느리거나 불안정하면, 에이전트는 문제를 고치려 애쓰기보다 그냥 테스트를 다시 돌리는 편이 나을 때가 있다고 배웁니다.

사람은 나쁜 테스트 인프라에 더 너그럽습니다. 우리는 어떤 테스트가 플래키(flaky)한지 알고, 실패하면 CI를 다시 돌려야 한다는 것도 알며, 불안정할지도 모르는 빌드를 20분씩 기다릴 의향도 있습니다. 하지만 에이전트는 루프가 주는 인센티브에서 배웁니다. 실패하는 테스트를 재시도해서 가끔이라도 통과할 수 있다면, 에이전트는 재시도를 가장 선호하는 전략으로 택합니다. 테스트 실행에 15분이 걸리면 테스트를 덜 자주 돌리고, 실행 사이에 변경을 더 많이 몰아넣게 되어, 어느 변경이 실패를 일으켰는지 정확히 짚어 내지 못하게 됩니다.

두 가지 실패 방식 모두 에이전트를 더 나쁘게 만들지만 눈에 띄는 실패 신호는 전혀 없습니다. 그래서 결국 사람이 리뷰할 결과물의 품질만 떨어집니다.

좋은 상태는 이렇습니다

제가 생각하는 “좋은” 모습을 몇 가지 적어 봅니다. 에이전트는 루프가 주는 인센티브에서 배워야 합니다.

Image
  • 테스트 스위트는 사이클마다 돌릴 수 있을 만큼 빠릅니다. 핵심 루프라면 1초 미만이 이상적입니다. 에이전트가 쉽게 찾아 실행할 수 있는 명령을 만들고, 해당되는 quiet 플래그도 붙여 두세요.
  • 테스트 스위트는 매번 같은 결과를 냅니다. 시드를 고정한 난수, 고정된 시계, 통제된 네트워크, 격리된 상태를 사용하세요. 테스트가 실패한다면 시스템이 망가진 것입니다.
  • 플래키 테스트는 메인 스위트에서 격리하고 즉시 제거합니다. 메인 스위트에 있는 플래키 테스트는 테스트가 아예 없는 것보다 나쁩니다. 다른 모든 결과에 대한 신뢰를 무너뜨리기 때문입니다.
  • 출력은 짧고 요점만 담습니다. 통과한 항목에 대한 방대한 로그보다 무엇이 실패했는지 알려 주는 구체적인 메시지가 낫습니다.
  • 불필요한 출력은 매 턴마다 에이전트의 주의력과 사고 여력을 잡아먹습니다.
  • 에이전트의 첫 루프는 빠르게 돌리고, 무거운 E2E 테스트와 diff 테스트는 변경을 통합하기 전에 돌립니다.
  • 테스트 인프라를 에이전트가 의존하는 구성 요소 가운데 하나로 다룹니다. 테스트 인프라를 개선하면 모든 작업에 좋은 효과가 복리처럼 쌓입니다.

참고: 실용적인 습관을 몇 가지 더 들자면 이렇습니다. 코딩 전에 목표를 명시적으로 다시 말하게 하기(“성공 기준이 무엇이라고 생각하는지 다시 말해 봐”), 검증에는 새 컨텍스트나 별도의 리뷰어 에이전트 쓰기, 생성된 테스트 자체도 따져 봐야 할 산출물로 다루기(이 테스트가 올바른 구현과, 그럴듯하지만 틀린 구현을 실제로 구분해 내는가?)입니다.

코드보다 검증을 먼저 작성하세요

에이전트에게 무언가를 만들라고 하기 전에, 그것이 무엇에 성공해야 하는지부터 알아야 합니다. 엔드투엔드 워크플로일 수도 있고, 특정 속성일 수도 있고, 이전 시스템과의 비교일 수도 있습니다. 무엇이든 빠르고 결정적으로 만들어 두면, 에이전트에게 해법을 반복해서 다듬도록 맡길 수 있습니다.

여러분이 출력을 한 줄 한 줄 읽을 필요는 없지만, 성공이 어떤 모습인지는 정의해야 합니다. 그렇게 하면 여러분의 판단력에서 더 큰 가치를 얻을 수 있고, 직접 diff를 읽는 방식보다 훨씬 더 잘 확장됩니다.

에이전트의 작업을 테스트할 때는 무엇이 일어나야 하는지 보여 주는 예시와, 무엇이 일어나면 안 되는지 정한 속성이 모두 있어야 합니다. 이기려면 에이전트의 테스트 과정이 빠르고 정확해야 합니다.

Edit this page

Search the archive

Find a page by title, or search the text inside one.