본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 26. 10:37

평가(Eval)를 통과하는 것이 과제를 해결하는 것은 아니다: 3가지 누출(Leaks), 60줄의 코드

요약

에이전트 평가(Eval) 과정에서 발생하는 데이터 누출(Leak) 문제를 다룹니다. 에이전트가 채점기가 읽는 경로에 직접 접근하거나 정답 데이터에 노출될 경우, 실제 성능과 무관하게 높은 점수가 나올 수 있음을 경고합니다.

핵심 포인트

  • 평가 통과가 실제 과제 해결을 보장하지 않음
  • 에이전트의 쓰기 경로와 채점기의 읽기 경로 분리 필수
  • 정답(Reference answer) 데이터에 대한 에이전트 접근 차단 필요
  • 데이터 누출(Leak)을 방지하기 위한 하네스 설계의 중요성

평가(Eval)를 통과하는 것이 과제를 해결하는 것은 아닙니다. 에이전트가 채점기(Grader)가 읽는 파일을 작성할 수 있거나 정답(Reference answer)에 도달할 수 있다면, 에이전트 평가 결과가 녹색(Pass)으로 나온다고 해서 아무것도 증명되지 않습니다. 이 60줄짜리 정적 프로브(Static probe)는 하나의 하네스(Harness) 사양을 읽어 3개의 오염(Contamination) 지점(쓰기-읽기 2곳, 정답 누출 1곳)을 찾아냈으며, 에이전트를 실행하지 않고도 1곳을 종료(Exit)시켰습니다.

저는 에이전트 평가(Agent-eval) 스레드에서 똑같은 스크린샷을 계속 보게 됩니다. 녹색 체크 표시가 가득하고, "통과율 98%"라고 적혀 있으며, 바로 출시(Ship it)하라는 내용 말이죠. 그러고 나서 똑같은 에이전트가 실제 운영 환경(Production)에서 통과했던 평가 사례와 완전히 동일해 보이는 과제를 수행하다가 처참하게 실패합니다.

일반적인 설명은 "평가 세트(Eval set)가 너무 쉽다"거나 "분포 변화(Distribution shift)"입니다. 때로는 맞을 수도 있습니다. 하지만 아무도 스크린샷을 찍지 않는, 더 조용한 실패가 있습니다. 왜냐하면 그것은 결코 빨간색(Fail)으로 나타나지 않기 때문입니다. 바로 하네스가 에이전트 스스로가 작성한 숫자를 채점한 경우입니다.

실행 가능한 도구와 함께 제가 옹호할 주장은 다음과 같습니다: 평가를 통과하는 것이 과제를 해결하는 것과 같지는 않다는 것입니다. 녹색 실행 결과는 다음 두 가지 조건이 충족될 때만 에이전트를 인증합니다. 첫째, 에이전트가 쓸 수 있는 채널이 채점기가 읽는 채널과 분리(Disjoint)되어 있어야 합니다. 둘째, 정답(Reference answer)이 채점 전 에이전트가 열어볼 수 있는 어딘가에 놓여 있지 않아야 합니다. 이 중 하나라도 어긋나면 당신의 "98%"는 구조적으로 판정 불가능(Undecidable)한 수치가 됩니다. 에이전트가 과제를 완벽히 수행했을 수도 있지만, 채점기가 신뢰하는 파일에 {"passed": true}라고 작성했을 수도 있기 때문입니다. 점수만으로는 어느 쪽인지 알 수 없습니다.

이를 잡아내기 위해 평가를 다시 실행할 필요는 없습니다. 배선(Wiring)을 읽으면 됩니다.

평가 하네스(Eval harness)에서 오염이 발생하는 모습

잠시 모델은 잊어버리세요. 에이전트 평가 하네스는 대부분 파일 배관(File plumbing) 작업입니다. 에이전트가 작성할 수 있는 경로 세트(워크스페이스, 출력 디렉토리, 공유 스크래치 DB 등)가 있습니다. 채점기가 통과 또는 실패를 결정하기 위해 읽는 경로 세트(결과 파일, 종료 코드 덤프, 오라클 점수 등)가 있습니다. 그리고 채점기가 비교 대상으로 삼는 황금 정답(Gold solution)인 정답(Reference answer)이 있습니다.

이 세 가지 세트는 신중하게 분리되어야 합니다. 실제 테스트 환경(harness)에서는 아주 어처구니없는 이유로 서로 누출(leak)되곤 합니다. 누군가 "결과물(artifact)을 재사용"하려고 채점기(grader)가 run/results.json을 가리키도록 설정했는데, 마침 run/ 디렉토리가 에이전트(agent)가 쓰기 가능한 상태였던 경우입니다. 혹은 편의를 위해 예상 정답을 task_config.json에 넣어두었는데, 에이전트가 작업을 이해하기 위해 읽는 설정 파일이 바로 그 파일인 경우도 있습니다.

제가 목격한 대부분의 사례는 다음 두 가지 형태의 누출을 포함합니다:

  • C1, 쓰기-읽기 중첩 (write-read overlap). 에이전트가 쓸 수 있는 경로와 채점기가 읽는 경로가 교차합니다. 에이전트는 채점기가 검사하는 바로 그 결과물(artifact)을 직접 조작(fabricate)할 수 있습니다. 이는 상태 오염(state pollution)입니다. 채점기는 실제 정답(ground truth)을 읽고 있다고 생각하지만, 실제로는 에이전트가 작성하고 에이전트가 채점한 숙제를 읽고 있는 것입니다.
  • C2, 참조 누출 (reference leak). 참조(reference) 또는 황금 정답(gold answer)이 에이전트의 읽기 세트(read-set) 또는 쓰기 세트(write-set) 내에서 접근 가능한 상태입니다. 에이전트는 정답을 도출하는 대신 정답을 그대로 복사할 수 있습니다. 이는 사람들이 한동안 지적해 온 WebArena 스타일의 패턴입니다. 즉, 에이전트에게 전달되는 작업 설정(task config) 안에 예상 정답이 들어있는 경우입니다. (여기서는 벤치마크의 통과율을 인용하는 것이 아니라 그 '형태'를 설명하고 있습니다. 이 포스트 뒷부분에 나오는 수치들은 공개된 테스트 환경(harness)의 것이 아니라 저의 자체 조사(probe) 결과입니다.)

두 가지 형태 모두 평가(eval) 자체의 점수에는 나타나지 않습니다. 어떤 경우든 평가는 '통과(green)'로 나옵니다. 이것이 바로 문제입니다. 오염된 테스트 환경(harness)은 스스로의 오염을 보고할 수 없습니다. 왜냐하면 오염이 보고되는 수치보다 상류(upstream)에서 발생하기 때문입니다.

조사(probe): 선언된 경로를 교차시키기, 아무것도 실행하지 않기

그래서 저는 영리한 방법 대신 지루한 방법을 택했습니다. 샌드박스(sandbox)도, 계측(instrumentation)도, 루프 내의 모델(model in the loop)도 사용하지 않았습니다. 그저 테스트 환경(harness)이 선언한 '누가 무엇을 만질 수 있는지'에 대한 정보를 가져와서 그 집합들을 교차(intersect)시켰을 뿐입니다.

입력값은 네 가지 경로 세트를 설명하는 하나의 JSON 파일입니다:

{
  "name": "contaminated-webarena-task-042",
  "agent_write_set":  ["run/results.json", "run/output/*", "shared/state.db"],
...

프로브(probe)는 세 가지 규칙으로 경로를 매칭합니다. 실제 경로 선언은 단순한 문자열 일치(string equality)가 아니기 때문입니다. 정확한 일치(Exact match), 양방향 글로브 매칭(Glob match, 따라서 out/*out/score.json을 포착함), 그리고 디렉토리 포함 관계(directory containment, 따라서 logs/라는 쓰기 범위는 logs/run.txt에 대한 채점자(grader)의 읽기를 포함함)가 그것입니다. 판단이 포함된 유일한 부분인 매처(matcher)는 다음과 같습니다.

def overlap(a, b):
    """두 개의 선언된 경로 패턴이 동일한 위치를 참조할 수 있으면 True를 반환합니다."""
    a, b = norm(a), norm(b)
...

분석은 두 개의 루프로 이루어집니다. C1을 위해 에이전트 쓰기(agent-write)와 채점자 읽기(grader-read)를 교차 검증합니다. C2를 위해 참조 경로(reference paths)를 두 에이전트 세트 모두와 교차 검증합니다.

def analyze(spec):
    aw  = [norm(p) for p in spec.get("agent_write_set", [])]
    ar  = [norm(p) for p in spec.get("agent_read_set", [])]
...

이것이 전부입니다. CLI와 세 가지 종료 코드(exit codes)를 포함한 도구 전체의 로직은 약 60줄에 불과합니다. sys, json, fnmatch만을 임포트합니다. 그 외에는 아무것도 없습니다. 네트워크, subprocess, eval, 모델 호출도 없습니다. 요청하더라도 여러분의 에이전트나 채점자를 실행할 수 없는데, 이것이 핵심입니다. 즉, 완전히 신뢰할 수 없는 저장소(repo)에서 머지(merge) 전이나 게시(publish) 전의 게이트(gate)로서 CI에 안전하게 투입할 수 있다는 뜻입니다.

종료 코드가 게이트 역할을 합니다:

  • 0: 깨끗한 연결(clean wiring), 채널들이 서로 분리되어 있음.
  • 1: 오염(contamination) 발견, 최소 하나 이상의 중첩(overlap)이 있음. 통과된 평가(green eval)를 미결정 상태로 취급하고 머지 또는 결과를 차단함.
  • 2: 잘못된 입력, 스펙(spec)이 누락되었거나, 유효한 JSON이 아니거나, JSON 객체가 아님.

실행 결과: 0점은 깨끗함, 3점은 오염됨

세 가지 피스처(fixtures)에 대해 프로브를 실행했습니다. 다음은 터미널에서 직접 복사한 터미널 기록(transcript)이며, 다시 타이핑한 것이 아닙니다. $ 명령줄과 마지막 exit= 줄은 셸(shell)이며 프로그램이 아닙니다. 그 사이의 모든 내용은 프로브의 표준 출력(stdout)입니다.

깨끗한 하네스(harness): 에이전트는 자신의 워크스페이스에만 쓰고, 채점자는 에이전트가 볼 수 없는 격리된 오라클(oracle)을 읽으며, 참조(reference)는 손이 닿지 않는 곳에 유지됩니다.

$ python3 eval_contamination_probe.py fixtures/fixture_clean.json
eval: clean-swe-task-001
agent_write_set=2 grader_read_set=2 reference_paths=1
...

0점. 밀도(Density) 0. Exit 0. 이것은 필요조건일 뿐, 충분조건은 아닙니다. exit 0은 단지 선언된 경로 집합(path sets)이 이 매처(matcher) 하에서 교차하지 않는다는 것만을 의미합니다. 런타임 경로(runtime paths), 심볼릭 링크(symlinks), .. 별칭(aliases), 또는 선언하지 않은 채널(channels)은 확인할 수 없습니다. 따라서 깨끗한 실행 결과는 "평가가 증명 가능하게 유효하다"가 아니라, "내가 선언한 배선(wiring)이 서로 분리되어 있다"로 읽어야 합니다.

이제 위 JSON에 나타난 오염된 하네스(contaminated harness)를 보겠습니다:

$ python3 eval_contamination_probe.py fixtures/fixture_contaminated.json
eval: contaminated-webarena-task-042
agent_write_set=3 grader_read_set=3 reference_paths=1
...

세 군데의 오염 지점(contamination points)이 발견되었습니다. 두 곳은 C1입니다: 채점자(grader)가 run/results.jsonshared/state.db를 읽는데, 이 두 파일 모두 에이전트가 쓸 수 있는 파일입니다. 따라서 에이전트는 채점자에게 자신이 통과했다고 적힌 결과 파일을 건네줄 수 있습니다. 세 번째는 C2입니다: 참조 정답(reference answer)이 에이전트의 읽기 집합(read-set)에 포함된 config/task_config.json에 들어 있습니다. 에이전트가 정답을 읽을 수 있는 것입니다.

밀도(Density)는 오염 지점의 수를 채점자가 읽는 소스(grader read-sources)의 개수로 나눈 뒤 100을 곱한 값입니다. 여기서는 3 / 3 = 100.0입니다. 이 숫자를 해석할 때 주의하십시오. 이것은 심각도 비율(severity ratio)이지, 제한된 범위 내의 백분율(bounded percentage)이 아닙니다. 여기서는 오염 지점이 3개이고 채점자 읽기 소스가 3개라서 우연히 100이 된 것이지만, 분자와 분모는 서로 같은 것이 아닙니다. 분자는 C2 참조 누출(reference leak)을 포함하여 모든 중첩을 계산하는데, C2는 채점자 읽기 소스조차 아닙니다. 따라서 밀도는 100을 넘어 올라갈 수 있습니다 (세 개의 에이전트 경로가 하나의 채점자 글로브(grader glob)를 모두 타격하면 density=300.0이 됩니다). 이를 "어느 정도의 비율인가"가 아니라 "얼마나 경고할 만한가"로 취급하십시오. 여기서 이 수치가 말해주는 바는 매우 직설적입니다: 이 채점자가 읽는 세 가지 중 두 가지는 에이전트가 쓸 수 있는 것이며, 정답(gold answer)은 에이전트의 읽기 집합에 놓여 있습니다. 이 하네스는 에이전트의 능력에 대해 거의 아무것도 측정하지 못합니다. 대부분의 경우, 에이전트가 파일을 쓸 수 있는지 여부만을 측정하는데, 이는 모든 에이전트가 할 수 있는 일입니다.

그리고 잘못된 입력(bad input)을 처리하여, CI가 고장 난 명세(spec)를 조용히 통과하는 대신 명확하게 실패하도록 합니다:

$ python3 eval_contamination_probe.py fixtures/fixture_badinput.json
error: fixtures/fixture_badinput.json is not valid JSON: Expecting value: line 1 column 42 (char 41)
exit=2

출력은 결정론적(deterministic)입니다. 깨끗한 실행의 표준 출력(stdout, 셸의 $exit= 줄을 제외한 프로그램의 출력)을 shasum -a 256에 두 번 파이프(pipe)로 연결했을 때, 두 번 모두 동일한 다이제스트(digest)를 얻었습니다: c6accb251262e8911422145c5d462318e2b29cb548ded150dc7f15398484c448. 오염된 실행은 e281f8fc31e9ada72177186f2d7d6c567636c237b4f3cb1e8f0e8493bc9858ac로 해싱되었으며, 두 번의 실행 모두 동일했습니다. 게이트(gate) 역할을 하는 시스템에서는 결정론적 특성이 중요합니다. 불안정한(flaky) 게이트는 일주일 안에 비활성화되며, 그러면 당신은 다시 초록색 체크마크를 신뢰하는 상태로 돌아가게 됩니다.

이것이 "내 테스트가 무언가를 테스트하고 있는가"와 다른 점

만약 제가 이전에 작성한 당신의 테스트가 실제로 무언가를 테스트하고 있는지에 대한 포스트를 읽어보셨다면, 이 내용이 유사해 보일 수 있습니다. 하지만 목표가 다릅니다. 그 도구는 당신의 애플리케이션(application) 단위 테스트가 실제로 코드를 실행하는지, 아니면 단순히 코드를 그대로 반영하고 있는지를 묻습니다. 거기서의 대상은 테스트 본문(test body)입니다.

이 프로브(probe)는 애플리케이션이나 그 테스트를 전혀 보지 않습니다. 대신 에이전트를 채점하는 장치인 평가 하네스(eval harness) 자체를 살펴봅니다. 대상은 배선(wiring), 즉 어떤 경로의 설정(set)이 어떤 설정에 영향을 미치는지입니다. 당신은 완벽한 애플리케이션 테스트를 가지고 있으면서도, 그 위에 완전히 오염된 평가 하네스를 올려둘 수 있습니다. 이들은 서로 다른 계층(layer)이며, 잘못된 계층에 표시된 초록색 체크마크가 바로 함정입니다.

또한 이것은 선언된 의존성(declared dependencies)과 임포트(imports)를 교차 검증하는 의존성 격차 감사기(dependency-gap auditor)도 아닙니다. 같은 장르이긴 하지만, 종료 코드(exit code)를 가진 정적 읽기 전용 프리 머지 게이트(pre-merge gate)라는 점은 같으나 교차 검증 대상이 다릅니다. 즉, 임포트 그래프(import graphs)가 아니라 평가 사양(eval spec)에 선언된 경로(paths)를 대상으로 합니다. 그리고 이것은 에이전트가 단일 호출에 대해 거짓말을 하면서도 확신에 찬 200 응답을 반환하는 것과는 다른 문제입니다. 후자가 단일 응답에 관한 것이라면, 이것은 단일 응답이 아닌 전체 채점 프로세스(grading process)의 무결성(integrity)에 관한 것입니다. 또한 이것은 불안정한 LLM 판사(LLM judge)를 대체하는 결정론적 프리 게이트(deterministic pre-gate)도 아닙니다. 후자는 채점의 무결성이 아닌 채점 비용에 관한 것입니다.

이 모든 것의 밑바탕에 깔린 프랜차이즈는 제가 계속해서 옹호하는 동일한 의견입니다: 신뢰하기 전에 게이트를 두라(gate before you trust). 평가가 성공적으로 수행되었다고 로그를 남기는 것은 통제(control)가 아닙니다. 평가가 실패할 수도 있었음을 증명하는 것이 통제입니다.

이것이 아닌 것

저는 여러분이 잘못된 이유로 이 도구를 채택하기보다는, 올바른 이유로 이 도구를 거부하기를 바랍니다. 따라서 한계를 명확히 밝힙니다.

이것은 정적(static)이며, 동적(dynamic)이지 않습니다. 프로브(probe)는 '누가 무엇을 건드릴 수 있는지'에 대한 선언을 읽습니다. 실제 실행을 관찰하지는 않습니다. 만약 여러분의 하네스(harness)가 자체 경로 집합에 대해 거짓을 말하거나, 사양(spec)에 없는 경로를 런타임(runtime)에 계산한다면, 프로브는 이를 볼 수 없습니다. 프로브는 여러분이 선언하여 존재하게 만든 오염을 잡아내며, 제 경험상 대부분의 오염은 이렇지만 전부는 아닙니다. 런타임 트레이서(runtime tracer)는 더 많은 것을 잡아내겠지만 비용도 더 많이 들 것입니다.

플래그(flag)는 위험 신호이지, 에이전트에 대한 판결이 아닙니다. 중첩(overlap)이 발생했다는 것은 배선(wiring)이 오염을 허용할 수 있음을 의미합니다. 그것이 에이전트가 오염을 악용했다는 것을 증명하지는 않습니다. 여러분의 에이전트는 매우 정직한 내용으로 run/results.json을 작성하고 있을 수도 있습니다. 핵심은 평가가 더 이상 정직한 것과 조작된 것을 구분할 수 없게 되어, 초록색 결과(green result)가 결정되지 않은 상태가 된다는 점입니다. 이는 의미 있고 실행 가능한 정보입니다. 이것은 비난이 아닙니다.

글로브 매칭 (glob matching)은 근사치입니다. 저는 fnmatch와 디렉토리 포함 여부를 결합하여 매칭합니다. 이는 의도적으로 약간 성급하게(eager) 설계되었으며, out/*out/anything과 겹치는 것으로 취급합니다. 언급할 만한 주의 사항(gotcha)이 하나 있습니다. Python의 fnmatch*/를 가로지르는 것을 허용하므로, out/*는 단일 세그먼트뿐만 아니라 out/sub/deep.json과도 매칭됩니다. 이로 인해 제가 별도로 만든 "글로브-디렉토리 vs 파일" 규칙은 대부분 불필요해지며, 매처(matcher)는 셸 글로브(shell glob)보다 더 넓은 범위를 가집니다. 또한 리터럴 경로 이름 내부에서도 *, ?, [...]를 와일드카드로 읽기 때문에, 해당 문자가 포함된 경로는 실수로 매칭될 수 있습니다. 글로브가 실제로 그곳에 위치한 파일들보다 더 넓은 범위를 가질 때 발생하는 거짓 양성(false positives)을 예상해야 하며, 정준 경로(canonical-path) 해결, 심볼릭 링크(symlink) 또는 대소문자 처리는 기대하지 마십시오. 저는 실제 누출(leak)을 놓치기보다는 게이트(gate)가 과하게 플래그를 지정하는 쪽을 택하겠지만, 만약 제 규칙이 모델링하지 못하는 경로 체계를 사용 중이라면 노이즈가 발생할 것입니다. 매처는 10줄에 불과하므로, 귀하의 리포지토리(repo)에 맞게 조정하십시오.

경로 매니페스트(path manifest)가 없으면, 의견도 없습니다. 만약 귀하의 하네스(harness)가 경로 집합을 선언하지 않는다면, 프로브(probe)는 교차할 대상이 없으므로 침묵을 유지합니다. 프로브는 볼 수 없는 오염을 스스로 만들어내지 않습니다. 그 침묵은 정직한 것이지, 통과(pass)를 의미하는 것이 아닙니다. 게이트의 성능은 귀하가 입력하는 명세(spec)만큼만 유효하며, 이는 그 자체로 하네스가 채널을 명시적으로 선언하도록 유도하는 장치가 됩니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0