M5 정리에서 남긴 일은 골든셋 전체를 실제 모델로 돌려 모델 모드 기준선을 만드는 것이었다. 어제의 키가 다시 받아들여져서 오전에 33건 전체를 돌렸다. 36분, 66번의 호출. 그리고 보고서는 남지 않았다. 이번 PR #29는 그 유실의 원인과 수정, 그리고 잘린 실행을 이어 붙이는 재개 기능이다. 실제 모델 호출은 그 실행 한 번뿐이고 PR 자체는 합성 입력으로만 확인했다.

검사는 옳았고, 버린 것이 틀렸다

러너는 보고서를 쓰기 직전에 마스킹 검사를 한다. 직렬화한 보고서와 표에 이력서 원문, 공고 원문, 근거 문장, 책임 줄 가운데 8자 이상인 문자열이 포함되거나 키 모양의 문자열이 있으면 예외를 던진다. 어제까지의 두 실행은 이 검사를 조용히 지나갔다. 이번에는 모델이 어느 공고의 책임 줄 “Own the frontend platform roadmap”을 요건으로 추출했다. 보고서는 골드에 대응되지 않은 추출 요건을 읽기 쉽도록 그대로 적는데, 그 문장이 공고의 책임 줄과 같으니 검사가 잡았고, 예외는 러너의 catch까지 올라가 고정 문구 한 줄과 exit 2로 끝났다. 33건의 결과가 로그의 사례별 한 줄씩만 남기고 사라졌다.

검사가 잡은 것은 진짜였다. 모델이 쓴 문자열은 입력 문장을 그대로 복사할 수 있고, 그것을 보고서에 실으면 안 된다. 틀린 것은 대응이었다. 99번까지 부를 수 있는 실행의 맨 끝에서 예외를 던지는 것은 전부를 잃는 선택이다. 가짜 분석기에 책임 줄을 요건으로 복사하는 스위치를 달아 같은 실패를 재현한 뒤, 지정한 입력 문자열 중 8자 이상인 것이 모델의 보고서용 문자열(대응되지 않은 추출 요건, 가장 가까운 추출 문장, 결격 사유 문장)에 포함되면 고정 표식으로 바꾸고 개수를 보고서에 남기도록 했다. 마지막 검사는 그대로 두었다. 대체가 놓친 것이 있으면 여전히 막아야 하고, 이번에 재현한 복사 문자열은 마지막 검사 전에 대체된다.

하루에 끝나지 않는 실행

로그에는 다른 것도 있었다. 앞의 18건은 판정 완료 15건과 공고 추출 실패 3건이었다. 이후 15건은 프로필 추출 첫 호출에서 연속으로 실패했고, 그 시작은 52번째 호출이었다. 무료 모델 변형에는 크레딧 구매 이력에 따른 일일 요청 한도가 있고 어제는 48번까지 오류 없이 실행됐으니 일일 한도를 의심했지만, 오류 분류가 남지 않아 확정하지 못했다. 다만 이 추정이 맞으면 99번짜리 실행은 이 계정에서 하루에 끝나지 않는다.

기준선은 한 보고서여야 한다. 골든셋 전체의 해시가 붙어야 다음 보고서와 사례 단위로 비교할 수 있어서다. 그래서 재개를 만들었다. --resume에 이전 보고서를 주면 판정에 이르지 못한 사례만 다시 돌리고, 새 기록으로 옛 기록을 바꿔 넣고, 집계를 다시 계산해 보고서 하나를 쓴다. 집계를 사례 기록에서만 계산하도록 러너를 고쳐야 병합 보고서도 같은 방식으로 나온다.

재개 보고서를 믿을 수 없게 만드는 문제 셋

리뷰는 재개를 가짜 분석기로 따로 돌려 보고 세 문제를 재현했다.

첫째, 호환성 검사가 호출 뒤에 있었다. 이전 보고서와 지금 실행이 같은 모델과 프롬프트인지 확인하는 일은 병합 함수 안에 있었고, 병합은 재실행이 끝난 다음이다. 리뷰어가 모델 이름만 다른 보고서를 넣으니 사례는 판정까지 마친 뒤 거절됐다. 리뷰 문장을 그대로 옮기면 “라이브에서도 같은 경로이므로 모델 설정이나 프롬프트가 달라진 다음 날 재개하면 전송/호출을 하고도 결과를 다시 잃습니다.” 유실을 고치려 만든 기능이 같은 유실을 품고 있었다. 검사를 함수로 빼서 러너가 제공자와 모델, 프롬프트, 해시를 확정한 직후, 계획을 출력하고 분석기를 만들기 전에 호출하게 했다.

둘째, 검사가 버전 문자열만 봤다. 골든셋 버전이 같으면 사례의 원문이나 기대값이 바뀌어도 통과했고, 정책과 스키마 해시는 아예 비교하지 않았다. 리뷰어가 같은 사례 id의 headline과 정책 해시를 바꿔 재실행한 것을 병합하니 통과했고, 결과에는 옛 해시가 그대로 남았다. 서로 다른 계약의 사례를 섞은 보고서가 옛 골든셋 전체를 검증한 기준선처럼 보이는 상태였다. 이제 이전 보고서의 사례 목록을 현재 골든셋에서 id로 복원해 그 순서로 해시를 다시 계산해 대조하고, 사례별 골드 해시도 비교하며, 하나라도 없으면 거절한다. 정책이나 스키마 해시가 달라도 거절한다. 라운드마다 다른 계약을 기록해 두는 길도 있었지만, 기준선은 한 계약이어야 해서 거부를 택했다.

셋째, 교체된 사례의 이전 시도가 사라졌다. 실패한 사례의 기록을 새 기록으로 바꾸면 그 사례가 실패하기까지 쓴 호출과 토큰이 집계에서 빠진다. 리뷰어의 재현에서는 프로필 추출·공고 추출·판정까지 3회 호출한 실행에서 판정이 실패했고, 재시도로 같은 세 단계를 다시 호출했다. 호출 수는 총 6인데 사용량은 마지막 3회분만 남았다. “호출 수는 누적이고 토큰·지연·upstream은 마지막 시도만인 혼합 보고서입니다.” 교체된 사례는 이전 시도의 텔레메트리를 따로 보존하고, 사용량과 지연과 응답 모델은 모든 시도에서, 판정 지표는 최종 시도에서 계산하도록 나눴다.

커밋 전 검증에서 빠진 것

리뷰 반영을 커밋하는 체인에서 실수가 하나 더 있었다. 테스트 출력을 pipefail 없이 grep과 head로 연결해 요약만 보게 해 두었는데, 그러면 체인의 종료 코드는 head의 것이다. 전체 실행에서 사용 확인 러너의 spawn 테스트가 5초를 넘겨 한 번 실패했는데도 다음 명령이 이어져 커밋과 푸시가 나갔다. 단독·전체 재실행에서는 통과해 같은 실패를 다시 재현하지 못했다. 하지만 테스트 실패가 커밋을 막지 못한 검증 체인의 문제는 분명했다. 커밋 전 게이트는 파이프 없이 종료 코드로 건다.

확인한 것과 남은 것

테스트는 456건, 정책 기준선 비교는 그대로다. 모델 모드 기준선은 아직 없다. 순서는 정해졌다. 새 키로 전체를 돌리고, 한도에 잘리면 다음 날 --resume으로 이어 붙이고, 병합된 보고서를 기준선으로 커밋한다. 재개가 실제 제공자를 상대로 동작하는지는 그때 처음 본다. 러너의 계약은 저장소의 model-mode.ts에 있다.