Index
2026-08-19 — Progress

E6 v3 eval 파이프라인 구현 — 전역 어휘 계약 + 누출 가드

scene_bench | src/sbench_eval, commit 9c9d64c

TL;DR

13
files changed
+773
insertions
-172
deletions
14
new v3 tests
57/4+1/15
obj/room/surf vocab

1 배경 / 목적

260818 260818-e6_eval_design.html에서 사용자가 "물체가 어디 있는지 찾아야 하는데 리스트가 이미 다 알려주고 있다"고 지적했고, 실측 검증(답 835개 전수)으로 정확함이 확인됐다: 에피소드별 후보 리스트(uid + 초기 방 표기)를 읽기만 해도 답의 66.6%(556/835)가 맞았고, find_distractor 질의는 100%(400/400)가 누출이었다(정의상 움직이지 않는 물체라 초기 방=정답). uid 접미사에 방 번호가 그대로 박혀 있어 표기를 지워도 누출이 남는다는 점까지 확인되면서, "uid 선택형 답 계약" 자체를 폐기해야 한다는 결론이 나왔다.

이 카드는 그 설계(v2)가 승인된 문서에 머물지 않고 실제 실행 가능한 코드로 전환됐음을 기록한다 — 설계 문서(plan-e6_v07_eval_design.md §6-c, §7)의 "구현 변경 목록"이 이번 커밋으로 전부 반영됐다.

기존 한계: v2는 설계안일 뿐 코드가 없어 S1 파일럿(3 eps × 5전략 × 2모델, ~$5)을 실행할 수 없는 상태였다.

2 작업 내용

커밋 메시지 그대로 "Implements design v2" — 신규 모듈 7개 + 테스트 재정비로 구성.

신규 파일 (src/sbench_eval/): vocab.py (50줄) — 전역 어휘 로더. vocab_v1.json(92줄, 153 eps에서 1회 생성·고정) 57 objects / 4 rooms(bathroom·bedroom·kitchen·living room) + floor / 15 surfaces. canon_object/canon_location/canon_surface 정규화 함수 prompt.py (75줄) — build_system_v3(): 전역 어휘만 담은 system 프롬프트(에피소드 물체 리스트 없음). build_query_parts(): 질의 턴 = 마지막 프레임 1장 + 질의문만(원래 uid 리스트 삭제). assert_no_episode_leak(): 프롬프트 문자열에 tracked object uid나 uid 해시 접두, "room 0/1" 같은 숫자 방 id가 섞이면 즉시 assert 실패 — 누출 재발 방지용 런타임 가드 answer.py (105줄) — Selection{category, room, surface} + EvalAnswerV3 pydantic 모델. _extract_first_json_object로 코드펜스/잡텍스트 안 JSON도 추출, _canonicalize()가 어휘 밖 값(room/surface/category)을 개별 에러 메시지로 reject → 재질의 유도. 바닥(room=="floor")이면 surface=None 강제(GT 규약과 정합) facts.py (63줄) — GT selections 추출 + tolerant room-name join + 계층화 라벨 생성 grading.py (70줄) — location/place set match, count 정확도, 부분점수 F1 runner_v3.py (35줄) — v3 계약으로 질의 실행하는 러너 run_eval_v3.py (146줄) — 체크포인트 지원 + --dry-run 엔트리포인트 테스트: tests/eval/test_v3_contract.py (+123줄, 신설) — 어휘 크기(57/4/15) 검증, 누출 가드 통과/실패 양쪽 케이스, 파서 정규화, 위치 집합 채점 tests/eval/test_candidates.py (-44줄, 폐기) — 구 v1 후보 리스트 빌더 (계약 자체가 사라짐) tests/eval/test_eval_fixes.py (-44줄, 폐기) — 구 버그 게이트 (생성 측 반영 확인되어 불필요) tests/eval/test_query_runner.py (-67줄, 폐기) — 구 v1 질의 러너 tests/eval/test_memory_strategy.py (31줄 수정) — v3 스키마에 맞게 픽스처 갱신(schema 0.3)

설계 문서(§7 "구현 변경 목록")와 실제 구현을 대조하면 schema.py 항목만 별도 파일 대신 answer.py에 흡수됐고, 나머지(candidates.py 폐기 → vocab.py 신설, query_runner.py → prompt.py/runner_v3.py 분리, scorer.py → grading.py, eval_fixes.py → facts.py 계층화 부분 흡수)는 계획대로 반영됐다.

3 결과 (수치)

항목수치비고
변경 파일13신규 7 + 테스트 정비 4 + 기존 1(test_memory_strategy.py)
총 diff+773 / -172순증가 601줄
v3 계약 테스트14개 (test_v3_contract.py)누출 회귀 포함
폐기 테스트3 파일 (-155줄)v1 계약 전용 — candidates/eval_fixes/query_runner
고정 어휘물체 57 / 방 4+floor / 표면 15vocab_v1.json, 153 eps 전수에서 1회 생성
토큰 절감 추정~30%설계 문서 추정치 — 에피소드 리스트(24줄) 삭제 대신 어휘는 system에 1회만
인프라 상태: v0.7 데이터셋(~/datasets/scene-mem-benchmark)이 현재 41GB 전량 다운로드 완료된 상태를 직접 확인 — 260818 시점 context.md의 "43.7GB 다운로드 중" 블로커가 해소되어 있어, S1 파일럿을 실제로 돌릴 수 있는 상태다(이번 리포팅에서는 실행하지 않음).
미검증: test_v3_contract.pypytestmark = pytest.mark.skipif(not DS.exists(), ...)로 데이터셋 부재 시 스킵되는 구조 — 이번 카드는 커밋 diff와 코드 정독을 근거로 작성했고, 실제 pytest 실행 결과(14개 전부 pass 여부)는 별도 확인이 필요하다.

4 Takeaway

의미

E6는 이제 "설계안"이 아니라 "실행 가능한 파이프라인"이다. 핵심은 assert_no_episode_leak이 단순 코드리뷰가 아니라 매 호출마다 실행되는 런타임 가드라는 점 — 66.6% 누출을 낳았던 실수 패턴(uid/방번호가 프롬프트에 섞임)이 재발하면 조용히 넘어가지 않고 즉시 실패한다. 설계 v2가 "계약"으로만 존재했다면 이 가드가 그 계약을 코드 수준에서 강제한다.

답 계약이 uid 선택 → {category, room, surface}로 바뀌면서 헤드라인 지표도 obj_strict에서 loc_acc/place_strict로 이동한다 — 공식 body-id 채점과는 간접 대응이 되므로(설계 문서에 이미 명시), 다음 단계에서 이 트레이드오프가 실제로 감당 가능한지 S1에서 먼저 확인해야 한다.

5 Next Steps

미해결 한계

① 실제 pytest 실행 결과 미확인(14개 전부 pass인지, 데이터셋 로딩 경로가 맞는지) — 다음 세션에서 먼저 돌려봐야 한다. ② body-id 공식 채점과의 간접 대응 관계가 실제 리더보드에서 얼마나 정보 손실을 만드는지 아직 모름. ③ 방 4종 해상도가 거칠다는 한계는 설계 단계 지적이 그대로 남아있음(twin_nearby 98건 별도 보고 필요).

다음 실험

설계 문서 §6 단계 그대로: S1 검증 (val_101__0 등 3 eps × 5전략 × 2모델, ~$5) — 스키마/파서/픽스처가 신규 계약에서 실라이브로 도는지 확인. 통과하면 S2 층화 파일럿(15 eps, kind×steps×misplaced 층화, ~$50)으로 전략 재필터(5→3?) 및 모델 확정. 데이터셋은 이미 로컬에 있으므로 블로커 없이 바로 착수 가능.