시스템이 스스로 테스트를 생성할 수 있을까? 결과로부터 탄생한 판정관
요약
LLM이 실행 결과로부터 스스로 새로운 테스트 속성을 생성하고 검증하는 '발명된 판정관(invented judge)' 메커니즘을 소개합니다. 단순히 테스트 코드를 작성하는 것을 넘어, 올바른 코드에 대한 공격을 통해 유효한 속성만을 Rust 테스트로 변환하는 제약된 프로세스를 제안합니다.
핵심 포인트
- 단순한 단언(assertion) 생성을 넘어 실행 결과 기반의 속성 추출 방식 제안
- 올바른 코드에서 후보 속성을 공격하여 검증하는 반복적 사이클 도입
- LLM의 환각을 방지하기 위해 최종 판결을 cargo test에 위임
- 변이 테스트를 통해 시스템의 테스트 생성 및 검증 능력 입증
이 글은 “LLM의 대안을 구축할 수 있는가?” 시리즈의 네 번째 파트입니다. 첫 번째 파트에서는 정교한 데모와 실제 메모리를 구분하였고, 두 번째 파트에서는 자기 수정 (self-correction) 능력을 테스트했으며, 세 번째 파트에서는 지식이 단순히 저장소에 불과한지 질문했습니다. 코드 및 실험 로그: AuraSDK.
저장소의 특정 함수에 여전히 버그가 살아있음에도 불구하고 cargo test 실행 결과가 초록색(성공)으로 나타날 수 있습니다.
이것은 역설이 아닙니다. 전통적인 테스트는 알려진 몇 가지 합의 사항을 확인합니다. 만약 변이 (mutation)가 그 특정 합의 사항을 위반하지 않는다면, 버그는 살아남습니다. 그렇다면 자율 시스템 (autonomous system)에게 던져지는 질문은 단순히 “테스트를 실행할 수 있는가?”가 아닙니다. 더 어려운 질문은 다음과 같습니다:
코드를 실행한 결과로부터 **새로운 판정 테스트 (new judging test)**를 구축할 수 있는가? 즉, 올바른 구현과 그럴듯하지만 틀린 구현을 구분해낼 수 있는가?
나는 시스템에게 자유 형식의 텍스트로 “유용한 테스트를 발명하라”고 요구하지 않았습니다. 대신 제약된 프로세스를 부여했습니다: 실행을 관찰하고, 단순한 속성 (properties)을 제안하며, 올바른 코드에서 그 속성을 깨뜨리려 시도하고, 살아남은 것들만을 Rust 속성 테스트 (property tests)로 변환하는 방식입니다. 최종 판결은 Python 래퍼 (wrapper)나 LLM에 의해 내려지는 것이 아니라, cargo test에 의해 내려집니다.
단지 단언 (assertion)을 생성하는 것만으로는 부족한 이유
가장 위험한 테스트는 아무것도 찾아내지 못하는 테스트가 아닙니다. 올바른 코드에 대해 확신을 가지고 실패를 선언하는 테스트입니다.
시스템이 여러 번의 실행을 보고 다음과 같이 추론한다고 가정해 봅시다: “결과가 비어 있지 않을 때마다, 그 길이는 항상 최소 3이다.” 이는 시스템이 관찰한 예시들의 우연한 경계값일 뿐일 수 있습니다. 이러한 테스트는 향후의 올바른 구현들을 거부할 수 있습니다.
따라서 후보 속성이 즉시 테스트가 되는 것은 아닙니다. 반드시 다음 사이클을 통과해야 합니다:
실제 코드 실행 (live code executions)
→ 후보 속성 (candidate property)
→ 올바른 코드에서 이를 공격 (attack it on correct code)
...
나는 이것을 _발명된 판정관 (invented judge)_이라고 부릅니다. 이는 “모델이 작성한 테스트”가 아니라, 결과로부터 탄생하여 반박을 견뎌내도록 강제된 제약된 판정관을 의미합니다.
하나의 매력적인 실행이 아닌, 다섯 개의 관문
첫 번째 합성 버전은 이 메커니즘이 작동할 수 있음을 보여주었습니다. 홀드아웃 (held-out) 변이(mutation) 5개 중 5개를 false-fire = 0.0의 최대치로 제거했습니다.
그것이 우리가 Rust 코드를 테스트하는 방법을 학습했다는 의미는 아니었습니다. 따라서 실험은 다섯 개의 관문을 통과했습니다:
- 합성된 Python 세계;
- 실제
cargo test로의 브릿지; - 정적 마이너 (static miner)와 비교된 실제 Aura 함수들의 7개 포트;
- 실제로 Rust로 렌더링되는 속성 판정관 (property judge);
- 실제 Aura 함수 본문에 대한 결합된 코드-세계 (code-world) 관문.
초기 결과 중 두 가지는 정확히 그것이 불편했기 때문에 중요합니다.
첫째, 초기 "실제 Cargo 관문"은 이미 kill = 1.0과 false-fire = 0.0을 달성했음에도 불구하고 솔직하게 NOT_PROVEN으로 끝났습니다. 그 규칙은 사소했습니다. 기존의 취약한 테스트 이상의 새로운 능력을 추가하지 못했기 때문입니다. 이를 해결하기 위해 실험에 abs_ratio 변이를 추가했습니다. 이는 취약한 Cargo 테스트를 통과하고 단순한 경계 판정관 (boundary judge)을 회피했지만, 산술적 속성인 matches_division에서는 실패했습니다.
둘째, 단순히 Python의 결과를 Rust로 가져오는 것은 자기기만이었을 것입니다. 최종 버전에서는 각 후보 속성이 tests/judges.rs 내의 #[test]로 렌더링됩니다. 우리의 방법론과 대조군(control groups) 모두에 동일한 cargo test 결과가 사용됩니다.
최종 관문이 실제로 테스트한 것
최종 스크립트인 scripts/run_invented_judge_cargo_property_gate.py는 6개의 AuraSDK 함수에서 가져온 실제 Rust 본문과 함께 작동합니다:
filter_selected;disjoint;accuracy_per_mille;prune_deaths의 카운트 프로젝션 (count projection);presence_jaccard_rank_key의 비트 버전;has_duplicates.
이 스크립트는 cargo-mutants 스타일로 12개의 변이를 도입했습니다. 4개는 기본 Cargo 테스트에 의해 제거되었는데, 이는 유용한 무결성 검사 (sanity check)였습니다. 8개는 취약한 테스트 세트를 살아남았으며, 이 8개가 새로운 판정관들의 타겟이 되었습니다.
이 관문에는 정답을 조용히 제공해 주는 Python 오라클 (oracle)이 존재하지 않습니다. 대신 Rust 트레이스 하네스 (trace harness), 제한된 속성 프리미티브 (property primitives) 어휘, 그리고 생성된 Rust 테스트가 존재합니다. 이 어휘는 의도적으로 작게 구성되었습니다: >=/<= 경계 (bounds), 스칼라 길이 (scalar length), 부분 집합 (subset), 빈 입력에 대한 빈 출력 (empty output on empty input), 대칭성 (symmetry), 단조성 (monotonicity), 그리고 순열 불변량 (permutation invariants)입니다.
이러한 제한은 중요합니다. 시스템이 임의의 새로운 수학을 발견하는 것이 아니기 때문입니다. 대신 시스템은 이미 알려져 있고 감사가 가능한 속성 언어로부터 판정관을 선택하고 검증합니다.
자신의 경계를 숨기지 않는 결과
안정적인 경계 (stable bounds) 하에서, 미래의 입력들은 이미 "살아있는" 실행들에 의해 표현된 영역 내에 머물렀습니다. 세 개의 독립적인 시드 (seeds) 전체에 걸쳐, 두 접근 방식 모두 동일한 결과를 도출했습니다:
| 영역 (Regime) | 접근 방식 (Approach) | 제거율 (Kill rate) | 오탐 (False-fire) | 순수 결과 (Net) |
|---|---|---|---|---|
| 안정적인 경계 (Stable bounds) | 발명된 판정관 (Invented judge) | 1.00 | 0.00 | +1.00 |
| 안정적인 경계 (Stable bounds) | 정적 마이너 (Static miner) | 1.00 | 0.00 | +1.00 |
여기서 승리를 선언할 이유는 없습니다. 과거의 데이터가 미래를 진정으로 포괄할 때는 정적 일반화 (static generalization) 역시 작동하기 때문입니다.
흥미로운 결과는 취약한 경계 (fragile-bounds) 영역에서 나타났습니다. 여기서는 미래의 정답 사례들이 정적 마이너가 이전 트레이스에서 보았던 우연한 경계를 넘어섭니다.
| 영역 (Regime) | 접근 방식 (Approach) | 제거율 (Kill rate) | 오탐 (False-fire) | 순수 결과 (Net) |
|---|---|---|---|---|
| 취약한 경계 (Fragile bounds) | 발명된 판정관 (Invented judge) | 0.75 | 0.00 | +0.75 |
| 취약한 경계 (Fragile bounds) | 정적 마이너 (Static miner) | 1.00 | 0.50 | −0.50 |
언뜻 보기에 정적 마이너가 더 좋아 보입니다. 모든 변이 (mutation)를 제거했기 때문입니다. 하지만 정적 마이너는 홀드아웃 (held-out) 사례의 절반에서 올바른 코드를 잘못 비난했습니다. 따라서 그 순수 결과는 음수입니다.
우리의 판정관은 하나의 변이를 놓쳤습니다. 이는 그것을 보지 못해서가 아니라, 판정관의 후보 경계가 우연한 것이었기 때문입니다. 프로세스는 해당 일반화에 흉터가 남았음을 인지하고, 잘못된 판정 대신 ABSTAIN (기권)을 반환했습니다. 여기서 핵심적인 이점은 "더 많이 제거하는 것"이 아닙니다. 그것은 일반화를 정당화하기에 결과가 불충분할 때 거짓말을 하지 않는 것입니다.
좁고 테스트 가능한 의미에서의 "테스트 성장 (growing a test)"이란 무엇인가
이 결과가 시스템이 이제 스스로 제품 요구사항을 이해한다는 것을 의미하지는 않습니다. 대신 더 좁은 범위의 주장을 확립합니다:
지정된 속성 언어(language of properties) 내에서, 시스템은 올바른 코드 실행(correct-code execution)과 변이 사각지대(mutation blind spots)를 활용하여, 일부 잘못된 일반화(false generalizations)를 버리고, Rust 속성 테스트(property test)를 생성하며, Cargo를 통해 독립적인 판정(independent verdict)을 얻을 수 있다.
이 공식에는 네 가지 필수 요소가 포함됩니다:
- 정적 코드 읽기(static code reading)뿐만 아니라 **결과(consequences)**를 다룰 것;
- 첫 번째 그럴듯한 가설이 아닌 **반례(counterexamples)**를 찾을 것;
- 속성이 정밀 검토를 견뎌내지 못할 때 **판정을 거부(refusal to judge)**할 것;
- 프로젝트의 일반적인 툴체인(toolchain) 내에서 생성된 테스트를 **독립적으로 실행(independent execution)**할 것.
마지막 요소가 없다면, 테스트의 품질이 아니라 여러분이 사용 중인 평가기(evaluator)의 품질을 실수로 측정하게 되기 쉽습니다.
이것이 아직 증명하지 못한 것
실험의 한계는 각주에 숨겨져서는 안 됩니다:
- 변이(mutations)는
cargo-mutants자체에 의해 생성된 것이 아니라,cargo-mutants스타일로 수동 작성되었습니다; - 탐색 입력 생성기(probe-input generators)는 여전히 수동으로 지정됩니다;
- 규모는 약한 테스트 세트(weak suite)를 통과한 6개의 함수와 8개의 변이에 불과합니다;
- 속성 어휘(property vocabulary)는 미리 정의되어 있으며 제한적입니다;
- 이것은 코드 세계의 관문(code-world gate)일 뿐, 전체 저장소나 임의의 사양(specifications)에 걸친 증거는 아닙니다.
정직한 다음 단계는 이 루프를 실제 개발에 통합하거나, 수동 변이 및 탐색(probes)을 독립적인 도구로 대체하는 것입니다. 자율 테스트(autonomous testing)가 해결되었다고 결론 내리는 것은 잘못된 일일 것입니다.
그럼에도 불구하고, 이것은 어설픈 자동 완성(assertion autocomplete)보다 강력합니다. 시스템은 단순히 테스트 텍스트를 작성하는 것이 아닙니다. 시스템은 그것을 작성할 권리를 획득해야 합니다. 즉, 먼저 올바른 코드에 대한 반박(refutation)에서 살아남음으로써 말입니다.
최종 수치
안정적 영역 (Stable regime): false-fire 0.00에서 1.00 제거.
취약한 경계 (Fragile bounds): 발명된 판정관(invented judge) 순수치 +0.75; 정적 채굴기(static miner) 순수치 −0.50.
중요한 수치는 단순히 킬 레이트 (kill rate)만이 아닙니다. 이는 다른 무언가를 시사합니다. 즉, 미래가 지금까지 경험한 범위를 벗어날 때, 정직한 ABSTAIN (기권)은 확신에 차서 틀린 테스트보다 더 가치 있을 수 있다는 점입니다.
코드, 게이트 시나리오 (gate scenarios), 그리고 로그는 AuraSDK에서 확인할 수 있습니다. 시리즈의 다음 파트에서는 다른 경계 조건을 테스트할 것입니다. 즉, 시스템이 더 이상 미래를 들여다볼 수 없게 되었을 때, 이러한 루프 (loops)가 시간이 지나도 유용한 트레이스 (trace)를 보존할 수 있는지 여부를 다룰 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기