모든 원본 벤더 응답을 커밋하기: API 키 없이 누구나 재점수화할 수 있는 벤치마크 만들기
요약
문서 추출 API 벤치마크의 신뢰성을 높이기 위해 원본 벤더 응답을 모두 커밋하여 누구나 재점수화할 수 있는 설계 방식을 제안합니다. API 키 없이도 결과 재현이 가능하며, 타임아웃 설정을 통해 실행 중단 문제를 해결하는 과정을 다룹니다.
핵심 포인트
- 원본 응답을 JSON으로 저장하여 API 키 없이도 결과 재현 가능
- 실행(Runner)과 점수 매기기(Scoring) 단계를 분리하여 비용 절감
- 메트릭 수정 시 유료 API 재호출 없이 로컬에서 즉시 반영 가능
- 소켓 정체 현상 해결을 위해 명시적인 타임아웃(Deadline) 설정 필수
저는 제가 참여한 업체가 포함된 문서 추출 API (document-extraction APIs) 벤치마크를 발표했습니다. 이는 글로써 해결할 수 없는 신뢰성의 문제입니다. 유일한 해결책은 제 제품이 패배한 실행 건을 포함하여, 제가 말하는 그 어떤 것도 믿지 않는 낯선 이가 수치를 재계산할 수 있도록 만드는 것입니다.
이를 가능하게 하는 설계 결정은 작습니다: 실행기(runner)는 방금 가져온 데이터에 대해 어떠한 점수도 매기지 않습니다.
원본 응답 단계에서 실행을 절반으로 나누기
모든 벤더 호출은 하나의 있는 그대로의 JSON 파일을 작성하고 거기서 멈춥니다:
// results/raw/<vendor>/<dataset>/<docId>.json
const rec = { vendor, dataset, docId, at: new Date().toISOString(),
latencyMs: r.latencyMs, raw: r.raw };
...
점수 매기기(Scoring)는 해당 파일들을 읽기만 하며 네트워크에는 전혀 손을 대지 않는 별도의 진입점(--replay)입니다. 1,210개의 모든 원본 응답이 커밋됩니다. 누구든지 저장소(repo)를 클론하고, pnpm replay && pnpm report를 실행하여, 자격 증명(credentials)이나 비용 지출 없이 README 테이블을 다시 생성할 수 있습니다.
이러한 분리에서 세 가지 결과가 도출되었으며, 그중 하나만이 목표였습니다.
재현에는 키(key)가 필요 없습니다. 그것이 핵심이었습니다.
점수 매기기 버그를 수정하는 비용이 들지 않습니다. 메트릭(Metrics)은 벤치마크가 틀리기 가장 쉬운 부분이며, 정규화 도구(normalizer)를 수정하기 위해 500번의 유료 API 호출을 다시 실행하는 것은 버그 수정을 조용히 저해하는 종류의 비용입니다. 여기에서 메트릭 수정은 단순한 재현(replay)일 뿐입니다.
중단된 실행은 무료로 재개됩니다. 그 existsSync 필터는 멱등성(idempotency)을 위해 작성되었으나, 다음에 일어난 일 때문에 실행을 구해낸 요소가 되었습니다.
정체 현상은 제가 예상치 못한 곳에서 발생했습니다
전체 실행이 꽉 막혔습니다. 에러도 없고, 종료되지도 않았으며, 단지 프로세스가 하나의 문서에 영원히 머물러 있는 상태였습니다. 저의 첫 번째 직감은 속도 제한(rate limiting)이었고, 재시도 로직(retry logic)을 찾아보았습니다.
재시도 로직(retry logic)은 문제가 없었습니다. 단지 실행되지 않았을 뿐입니다. 저의 재시도 래퍼(retry wrapper)는 백오프(backoff)를 포함하여 세 번의 시도를 수행하며, AWS SDK 클라이언트에는 그 외에도 maxAttempts: 3 설정이 되어 있었습니다. 하지만 재시도는 무언가가 '실패(fail)'할 때만 발생하며, 멈춰버린 소켓(hung socket)은 실패하지 않습니다. 그저 가만히 있을 뿐입니다. 제가 구축한 모든 회복 탄력성(resilience) 계층은 결코 발생하지 않을 에러의 하류(downstream)에 위치해 있었습니다.
따라서 해결책은 재시도가 아닌 데드라인(deadlines)이었습니다. 모든 어댑터의 모든 fetch 호출에 signal: AbortSignal.timeout(120_000)을 적용했으며, 여기에는 자체적으로 더 짧은 타임아웃이 설정된 3초 단위의 작업 폴링(job-poll) 루프도 포함되었습니다. AWS 클라이언트에는 명시적인 NodeHttpHandler({ connectionTimeout: 10_000, requestTimeout: 120_000, socketTimeout: 120_000 })를 설정했습니다. fetch 호출을 위한 커밋 하나와 SDK 클라이언트를 위한 커밋 하나, 이렇게 두 번의 커밋이 이루어졌습니다. SDK는 동일한 메커니즘을 따르지 않기 때문이며, 30분 후 두 번째 정체 현상을 발견하고 나서야 이 사실을 알게 되었습니다.
이 사례에서 한 가지만 기억하신다면: 멈춰버린 요청(hung request)은 느린 요청(slow request)이 아니며, 아무리 많은 재시도 설정을 해도 하나를 다른 하나로 바꿀 수는 없다는 점입니다. 타임아웃이 있어야만 maxAttempts가 시도할 대상이 생깁니다.
첫 호출 전 서브셋(subset) 사전 등록
선택(Selection)은 대개 의도치 않더라도 속임수를 쓰기 가장 쉬운 부분입니다. 그래서 서브셋 ID 목록은 어댑터가 비용을 청구하기 전에 커밋되었으며, 매니페스트(manifest)에 포함된 시드(20260710)를 사용한 시드 기반 셔플(seeded shuffle)로 생성되었습니다. 이 방법론은 저에게 점수를 깎아먹는 한 가지 규칙을 고정합니다: 결과를 확인한 후에는 어떤 문서도 제외하지 않는다. 벤더의 실패는 누락된 문서가 아니라 실패한 문서로 간주됩니다.
주요 지표(Headline metrics)는 타입 인지 정규화(type-aware normalization)를 거친 후 정확히 일치(exact match)하는 값을 기준으로 합니다. 금액은 0.005 이내, 날짜는 ISO 형식으로, 문자열은 문장 부호를 제거하고 NFKC 소문자화(NFKC-lowercased)를 적용합니다. ANLS(퍼지 레벤슈타인 유사도, fuzzy Levenshtein similarity)는 계산되어 보고되지만, 주요 지표 수치에는 절대 포함되지 않습니다. 유사도 임계값(similarity threshold)은 조절 가능한 다이얼과 같아서, 제가 직접 그 다이얼을 돌리는 상황을 만들고 싶지 않았기 때문입니다.
그 정직함이 실제로 치른 대가
송장 (invoices) 데이터에서 제 API는 필드 정확도 (field accuracy) 99.4%를 기록하며, 비교 대상인 93.1% 및 92.2%를 앞섰습니다. 문서당 중앙값 (median) 처리 시간은 12.8초로, Textract의 1.8초보다는 느립니다. CORD-v2 품목 (line items) 데이터에서는 Textract의 77.1%에 비해 41.9%의 F1 점수를 기록했습니다. FinTabNet 테이블 (tables) 데이터에서는 0.791의 TEDS 점수를 기록했으나, 100개의 문서 중 14개는 model returned unparseable output (모델이 파싱할 수 없는 출력을 반환함) 오류로 인해 완전히 실패했습니다.
이 마지막 두 수치는 README 파일 내에, 승리 기록과 동일한 테이블 스타일로 굵게 표시되어 있습니다. 이 수치들을 공개하는 것이야말로 첫 번째 수치(정확도)가 의미를 갖게 만드는 유일한 이유입니다.
여기서 벤치마크한 추출 API (extraction API)는 Kynth Core이며, 저희가 이를 구축한 방법은 다음과 같습니다: https://kynth.studio
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기