지난주에 한 생성(generation) 결과가 200으로 반환되었습니다. 제 파서(parser)는 812번째 문자에서 작동을 멈췄습니다. 그렇다면 이것은 실패였을까요? 아니면 일찍 멈춘 정상적인 답변이었을까요?
저는 클라이언트 코드에서 동일한 버그를 계속 발견합니다. 200이 아닌 모든 응답은 '재시도(retry)'로 간주되고, 200인 모든 응답은 '완료(done)'로 간주됩니다. 이 두 가지 가정 모두 토큰을 소모하고 실제 문제를 숨깁니다.
제가 현재 사용하는 사고 모델은 다음과 같습니다. 모델 호출에는 두 가지 결과가 아니라 세 가지 결과가 있습니다. 실패하거나, 성공하거나, 혹은 중간에 멈추는 것입니다. 대부분의 클라이언트는 앞의 두 경우만 처리할 수 있도록 되어 있습니다.
신화 1: 200 OK는 모델이 답변했다는 것을 의미한다
200은 서버가 요청을 수락했음을 의미하는 것뿐입니다. 그게 전부입니다. 본문(body)은 비어 있을 수도 있고, 200으로 위장된 오류 봉투일 수도 있습니다.
스트리밍(Streaming) 방식은 이를 더 악화시킵니다. 200 상태 코드는 첫 번째 토큰이 존재하기 전에 도착합니다. 상태 라인 자체는 내용에 대해 아무것도 말해주지 않습니다.
그렇다면 상태 라인을 확인한 후 여러분의 코드는 무엇을 검사하나요? 만약 답변이 '없음'이라면, 여러분의 파서는 유효성 검사기(validator) 역할을 하고 있는 것입니다.
신화 2: 스트림이 끝났으니 답변은 완전하다
스트림이 종료되는 것은 연결(connection)이 끊어질 때입니다. 이것은 완료된 생성(finished generation)을 의미하지 않습니다.
원하는 증거는 finish_reason입니다. 이 필드가 누락되었다는 것은 내용이 잘렸다는 것을 의미합니다. length 값이 있다는 것은 토큰 제한(token cap) 때문에 답변이 문장 중간에 끊겼다는 것을 의미합니다.
부분적인 JSON도 신중하게 슬라이싱(slicing)하면 종종 파싱될 수 있습니다. 이것이 문제입니다. 필드가 누락된 유효한 객체를 얻게 되지만, 오류는 전혀 없습니다.
신화 3: 호출을 재시도하는 것은 무료다
재시도는 토큰 비용, 시간(wall time), 그리고 때로는 중복되는 부작용(side effects)이 발생합니다. 만약 호출이 도구(tool)를 실행한다면, 재시도를 할 때 그 도구가 두 번 실행될 수 있습니다.
또한, 재시도 폭풍(retry storms)은 속도 제한(rate limits)을 악화시킵니다. 429 에러에 대한 빡빡한 루프는 스스로 장애를 만드는 것과 같습니다. 대신 실행 자체를 중단시키세요.
신화 4: JSON이 파싱되면 스키마가 완벽하다
파싱(Parsing)은 유효성 검사(Validation)가 아닙니다. 파싱은 '이것이 JSON 문서인가?'라고 묻는 것입니다. 유효성 검사는 '내가 필요로 하는 필드를 가지고 있는가?'를 묻습니다.
의존하는 키들을 확인하세요. 그들의 타입을 확인하세요. 원본 텍스트(raw text)를 첨부하여, 초기에 크고 명확하게 거부해야 합니다.
신화 5: 느린 호출은 고장 난 호출이다
느림(Slow)과 실패(Failed)는 서로 다른 축입니다. 9초가 걸린 성공적인 호출 역시 여전히 성공입니다. 이 둘을 하나의 '비정상(unhealthy)' 지표로 통합하면, 실제로 어떤 문제가 발생했는지 알 수 없습니다.
호출당 경과 시간을 기록하세요. 느낌이 아닌 데이터를 가지고 나중에 결정해도 됩니다.
결과 분류를 위한 작은 탐지기 (A small probe that classifies outcomes)
새로운 엔드포인트를 신뢰하기 전에 제가 실행하는 아티팩트입니다. 하나의 파일, 하나의 로그, 호출당 한 행으로 구성됩니다.
#!/usr/bin/env python3
"""probe.py - classify model-call outcomes instead of guessing.
...
실행한 후, 12개 행을 눈대중으로 요약하지 마세요.
python probe.py --url "$MODEL_URL" --key "$MODEL_KEY" --n 12 --out probe.jsonl
jq -r '.outcome' probe.jsonl | sort | uniq -c | sort -rn
jq -r 'select(.outcome=="TRUNCATED") | [.i,.status,.chars,.finish_reason] | @tsv' probe.jsonl
각 결과를 재시도 규칙으로 전환하기 (Turn each outcome into a retry rule)
하나의 except를 작성하지 마세요. 클래스별로 하나의 규칙을 작성하세요.
| 결과(Outcome) | 재시도 여부(Retry?) | 대기 시간(Wait) | 로깅할 정보(Log) |
|---|
OK | 아니요 (no) | 없음 (none) | 경과 시간, 문자 수 |
| ... | | | |
| 두 가지 규칙이 나머지 모든 규칙보다 더 중요합니다. 본인의 잘못된 요청(bad request)으로 발생한 4xx 오류는 절대 재시도하지 마세요. 스키마 실패(schema failure)는 보통 반복되기 때문에 자동 재시도는 하지 마세요. | | | |
집계가 실제로 알려주는 것 (What the tally actually tells you)
1분 이내에 이루어진 12회의 호출은 스모크 테스트(smoke test)일 뿐입니다. 이는 p50을 측정하는 것이 아니며, 그렇게 보고해서는 안 됩니다.
이것은 더 작은 질문에 답합니다. 현재 제 요청 형태에 대해 이 엔드포인트가 어떤 실패 클래스를 생성하는지? 이것이 바로 재시도 코드가 알아야 할 정확한 내용입니다.
12회의 호출에서 네 가지 클래스가 보인다면, 예전의 except 구문은 그 모든 것을 숨기고 있었던 것입니다. 만약 OK만 보인다면, 당신은 행복한 경로(happy path)만을 배웠을 뿐 아무것도 배우지 못한 것입니다.
탐지기를 실행하는 곳 (Where I run the probe)
제 노트북 뚜껑이 닫힙니다. 그러면 1분짜리 테스트가 중간에 중단됩니다.
이를 위해 저는 작동 시간 동안 깨어있고 자체 네트워크 경로를 가진 작은 박스가 필요합니다. MonkeyCode는 무료 서버 옵션과 모델 측면의 무료 모델 액세스를 제공하므로, 전체 루프를 제 자신의 기기에서 벗어나게 유지할 수 있습니다. 이 두 가지 모두 제가 제시하는 벤치마크가 아니라 제품이 보장하는 가용성 주장입니다.
MonkeyCode의 제품 홍보 활동의 일환으로 작성된 글입니다.
저는 그 상자를 '제 노트북이 아닌 어딘가'로 취급합니다. 스케줄러나 내구성 있는 저장 장치로 취급하지 않습니다. 이 실행은 probe.jsonl을 기록하며, 완료되면 로그를 복사해 나갑니다.
실행 간격과 시간을 짧게 유지하세요. 무료 액세스는 엔드포인트를 남용할 수 있는 면허가 아니며, 저는 그것이 영원히 동일하게 유지된다고 가정하지 않습니다.
이 프로브가 증명하지 못하는 것들
- 벤치마크는 아닙니다. 12회 호출, 1시간, 하나의 네트워크 경로일 뿐입니다.
- 긴 프롬프트(long prompts), 도구 호출(tool calls), 또는 이미지 입력을 테스트하지 않습니다.
- 엔드포인트가 변경되었다고 알려줄 수 없으며, 오늘 발생한 일만 보여줍니다.
- 비용, 할당량(quota), 또는 무료 액세스가 지속되는지에 대해서는 아무것도 말해주지 않습니다.
이것을 건너뛰어야 하는 사람들
SLA(Service Level Agreement) 숫자가 필요하다면, 이 도구는 적합하지 않습니다. 대규모 샘플, 고정된 스케줄, 그리고 적절한 백분위수 보고서가 포함된 실제 부하 테스트를 사용하세요.
대량으로 운영하고 있다면, 대신 프로덕션 환경에 트레이싱(tracing)과 클래스별 메트릭을 추가하세요. 프로브 스크립트는 엔드포인트를 신뢰하기 하루 전의 용도입니다.
중복된 도구 호출이 문제를 일으킬 수 있다면, 먼저 Idempotency(멱등성)를 수정하세요. 재시도 정책은 그 다음 문제입니다.
수정된 사고 모델
응답은 단 하나의 비트가 아닙니다. 그것은 상태(status), 본문(body), 완료 사유(finish reason), 그리고 지속 시간(duration)입니다.
무엇인가를 결정하기 전에 이 네 가지를 모두 읽으세요. 그런 다음 예외 유형별이 아닌 클래스별로 재시도 규칙을 선택하세요.
프로브를 실행한다면, --n 값을 작게 유지하고, 로그를 보관하며, REQUIRED를 자신의 스키마에 맞게 변경하세요.