Trawling

LLM 4,768회 실행에서 단 한 번의 스윕도 잃지 않기 — 타임아웃·멈춤·비용을 견디는 필드 테스트 러너 만들기

CauterRule v0.3.0은 40개 코퍼스와 2개 모델을 대상으로 4,768개 trajectory-run을 완료한 필드 테스트 보고서입니다. per-trajectory timeout, 비재시도 타임아웃, 토큰 상한, 격리, 종료 시 취소라는 다섯 가지 방어 장치로 멈춘 요청 하나가 전체 평가를 막지 않게 했으며, 호출별 토큰 사용량도 비용으로 환산했습니다.

본문

CauterRule v0.3.0은 반복되는 agent 실패에서 지속적으로 적용할 규칙을 추출하고, replay test를 거쳐 승격하는 open-source sidecar입니다. 이번 릴리스의 핵심은 새로운 prompt나 matcher가 아니라, 대규모 field test를 끝까지 실행할 수 있도록 runner를 hardening한 데 있습니다. 보고서는 40개 corpus와 2개 cloud model을 대상으로 총 4,768개 trajectory-run을 수행했으며, 이 과정에서 외부 model provider의 무응답, 무한 생성, 재시도에 따른 비용 증가가 benchmark 자체를 무너뜨릴 수 있다는 문제를 다룹니다.

소규모 실행에서 숨겨졌던 세 가지 가정

기존 runner는 약 50회 실행에서는 문제가 드러나지 않던 세 가지 가정에 기대고 있었습니다. 첫째, provider는 항상 응답한다는 가정입니다. 실제로는 예외나 오류도 내보내지 않은 채 요청이 영원히 반환되지 않을 수 있으며, 이 경우 worker가 차단되고 queue의 처리량이 0이 됩니다. 둘째, timeout은 일시적인 오류이므로 retry해야 한다는 가정입니다. 이미 종료됐거나 응답하지 않는 요청을 다시 시도하면 같은 요청에 대한 비용과 지연만 커질 수 있습니다. 셋째, 실패한 trajectory는 성공할 때까지 retry해야 한다는 가정입니다. 대규모 실행에서는 하나의 잘못된 입력이 무제한 retry loop를 만들 수 있습니다. 글은 이 세 문제가 model 자체의 문제가 아니라 harness의 문제이며, 실행 규모가 커질수록 서로 누적된다고 설명합니다.

이번 field test는 40개 corpus × 2개 model 규모로 진행됐고, model별로 약 2,384개 trajectory가 실행됐으며 각 trajectory에는 여러 LLM call이 포함됐습니다. 이 규모에서 runner가 견뎌야 할 최소한의 안전장치로 다섯 가지 guard를 적용했습니다.

전체 sweep을 살린 다섯 가지 guard

첫 번째는 per-trajectory timeout입니다. 각 future의 결과를 future.result(timeout=120)으로 감시하고, client 생성 시에도 timeout=30을 설정해 하나의 멈춘 call이 worker를 영원히 붙잡지 않게 했습니다. timeout이 발생한 trajectory는 실패로 기록한 뒤 다음 작업을 계속 처리합니다.

두 번째는 timeout을 non-retryable terminal failure로 분류하는 것입니다. timeout을 일시적인 오류로 간주해 retry하면 이미 죽은 요청이 비용을 다시 발생시키므로, 해당 trajectory를 종료 상태로 기록하고 재시도하지 않습니다. 글이 강조하는 원칙은 “나쁜 요청 하나의 비용은 trajectory 하나에 그쳐야 하며, 전체 sweep의 비용이 되어서는 안 된다”는 것입니다.

세 번째는 max_tokens=4096이라는 token cap입니다. runaway generation이 계속되면 latency와 API spend가 함께 증가할 수 있으므로 출력 길이에 상한을 둡니다. 네 번째는 CAUTERULE_QUARANTINE_IDS를 이용한 quarantine입니다. 이미 문제가 확인된 ID는 retry하지 않고 건너뛰어, 알려진 실패 입력 하나가 반복 실행을 방해하지 않도록 합니다.

다섯 번째는 shutdown 시 작업 취소입니다. executor.shutdown(wait=False, cancel_futures=True)를 사용해 종료 과정에서 대기 중인 future를 취소하고, 멈춘 thread를 끝없이 기다리지 않도록 했습니다. 글은 특히 with ThreadPoolExecutor context manager에만 의존하면 안 된다고 설명합니다. context manager의 __exit__는 shutdown(wait=True)를 호출하므로 단 하나의 stuck thread가 전체 sweep의 종료를 막을 수 있기 때문입니다. runner는 future를 제출하고 결과를 수집하면서 timeout을 기록한 다음, 종료 시에는 stuck thread를 기다리지 않는 방식으로 바뀌었습니다.

4,768개 실행을 완료한 결과

hardening 이후 field test는 4,768개 trajectory-run을 끝까지 완료했습니다. 사용한 model은 gpt-4o-mini와 llama-3.1-8b 두 개이며, corpus는 40개입니다. safety gate는 model별로 580개 trajectory를 제외했고, 그 결과 model별로 1,160개의 LLM call을 피했습니다. safety silence 결과는 60/60 · 60/60으로 기록됐고, adversarial promotion은 0건이었습니다.

글은 이 safety 수치가 모든 corpus가 완료됐기 때문에 의미를 가진다고 설명합니다. 쉬운 corpus만 처리한 뒤 어려운 corpus에서 benchmark가 중단됐다면, 결과는 실제 안전성이나 일반화 성능이 아니라 runner가 처리할 수 있었던 입력만 측정했을 수 있습니다. timeout과 quarantine으로 문제가 있는 run을 fail-soft 방식으로 제외하면서도, 전체 결과를 비교 가능한 상태로 유지했다는 점이 이번 실행의 중요한 결과입니다.

토큰 사용량을 실제 비용으로 바꾸기

runner가 안정적으로 끝나자 실행 비용도 측정할 수 있게 됐습니다. v0.3.0은 모든 response에서 token usage를 수집하도록 LLMResponse dataclass에 prompt_tokens와 completion_tokens 필드를 추가했습니다. SDK는 원래 각 response에 usage 정보를 반환하고 있었지만, 기존 runner가 이를 버리고 있었습니다. 호출 지점에서 두 필드를 보존하고 provider의 실제 단가를 적용하면 “trajectory 1,000개를 실행하는 데 얼마가 드는가”를 추정이 아니라 측정 문제로 바꿀 수 있습니다.

보고서에 제시된 단가는 gpt-4o-mini가 input 1M token당 0.15달러, output 1M token당 0.60달러이며, llama-3.1-8b는 input과 output 모두 1M token당 0.06달러입니다. 다만 corpus별 실제 비용, 즉 model별 총액이 아니라 corpus 단위의 $ / 1k-trajectory 수치는 아직 공개하지 않았다고 밝힙니다.

필드 테스트가 드러낸 한계와 남은 질문

이번 hardening은 local model 환경의 한계도 분명하게 드러냈습니다. 같은 runner 개선을 적용한 뒤에도 local OMLX setup은 규모가 커지자 지나치게 느렸고 raw/ci에서 멈췄습니다. 따라서 이번 field test는 cloud-only로 진행됐으며, v0.1.0 보고서에서 제시했던 “local model로 ship” 경로는 40개 corpus 규모에서 유지되지 않았습니다.

또한 cross-session repeat-failure reduction과 human-vs-replay agreement를 측정할 수 있는 cross_session.py, human_agreement.py 및 runner flag를 만들어 두었지만, 실제 protocol은 실행하지 않았습니다. 측정 도구를 배포한 것과 측정을 수행한 것은 다르다는 점을 보고서가 명시합니다. timeout이 어느 시점에 얼마나 발생했는지에 대한 분포도 기록하지 않았습니다. 따라서 120초 timeout이 정상적으로 느린 multi-step extraction을 잘라내는지, timeout이 raw/ci에 집중되는지, quarantine이 provider regression을 숨길 가능성이 있는지 역시 확인되지 않았습니다.

이 글의 결론은 대규모 evaluation에서 throughput과 종료 가능성이 품질의 전제라는 것입니다. flaky external API를 상대로 실행하는 benchmark에서는 infrastructure가 과학적 측정과 분리된 부수 작업이 아닙니다. timeout을 명시적으로 terminal 또는 transient로 분류하고, retry가 비용 버그가 되지 않도록 해야 합니다. 또한 client를 직접 소유한 호출 지점에서 token usage를 즉시 보존해야 나중에 실행 비용과 채택 조건을 측정할 수 있습니다. CauterRule v0.3.0의 가장 중요한 결과는 4,768개 요청을 완료하게 만든 다섯 가지 guard와, 그 실행을 가격으로 환산하게 만든 두 개의 dataclass 필드입니다. CauterRule은 GitHub와 PyPI에 공개됐으며 pip install cauterule로 CLI, framework adapter, rule lifecycle, rule pack을 설치할 수 있습니다.

원문: dev.to / 번역·요약: Trawling

참고 자료

오류 제보