17장. Project 02. LLM 추론 API 서빙
평가표와 발표
가게를 열기 전에는 리허설을 합니다. 맛을 보고, 주문 받는 말을 연습하고, 점심 손님이 몰리는 상황을 흉내 내고, 안내문을 붙입니다. 추론 API 도 약속, 품질, 속도, 쓰는 법을 함께 평가합니다.
네 카드가 이 장의 평가 항목입니다. 약속 시험은 정상 요청과 422, 503, 504 가 약속대로 나오는지, 품질은 요약의 핵심 사실과 분류의 정확도, 속도는 p50·p95 와 초당 처리 수, 쓰는 법은 /docs 화면과 다른 팀이 따라 쓸 부르는 예입니다. 여기서 리허설이 부하 시험과 약속 시험입니다.
평가 항목
| 항목 | 재는 법 | 프로젝트 |
|---|---|---|
| 약속 | 정상, 짧은 글(422), 붐빔(503), 시간 초과(504)를 일부러 만들어 상태 코드를 본다 | 1 |
| 요약 품질 | 문서마다 핵심 사실 셋 가운데 요약에 든 수, 문장 수를 지킨 수 | 1 |
| 분류 품질 | 손으로 쓴 시험 문의 40개의 정확도, LLM 에 넘긴 수 | 2 |
| 속도 | 동시 요청 수를 바꿔 가며 초당 처리 수, p50, p95 | 3 |
| 쓰는 법 | /docs 가 열리고, 503 을 받으면 기다렸다 다시 보내는 부르는 예가 있다 | 3 |
부르는 쪽의 예
다른 팀에 API 를 건넬 때는 503 을 받으면 어떻게 하는지까지 예로 줍니다.
import time
answers = iter([(503, {"Retry-After": "1"}), (503, {"Retry-After": "1"}), (200, {})])
def send():
"""실제로는 HTTP 요청을 보낸다. 여기서는 503 두 번 뒤에 200 을 돌려주는 흉내"""
return next(answers)
def call_with_retry(send, tries=4):
"""503 이면 Retry-After 만큼 기다렸다 다시 보낸다. 다른 코드는 바로 돌려준다"""
for i in range(tries):
status, headers = send()
if status != 503:
return status, i + 1
time.sleep(float(headers.get("Retry-After", 1)) * 0.1) # 예시라 기다리는 시간을 10분의 1로 줄였다
return status, tries
print(call_with_retry(send))(200, 3) 이 나옵니다. 두 번 붐빔을 받고 세 번째에 답을 받았다는 뜻입니다. 422 처럼 다시 보내도 같은 결과가 나올 오류는 다시 보내지 않습니다.
코드에 나오는 함수
| 코드 | 쉬운 뜻 |
|---|---|
iter([...]) | iterator(반복자)를 만든다. 목록의 값을 하나씩 꺼낼 수 있다 |
next(answers) | 다음 값을 하나 꺼낸다. 여기서는 서버의 답을 흉내 낸다 |
call_with_retry(send, tries=4) | retry(다시 시도)를 붙인 부르기 함수다. 최대 네 번까지 보낸다 |
headers.get("Retry-After", 1) | 머리글에 Retry-After 가 없으면 1초로 둔다 |
float(...) | 머리글의 값은 글자라 초를 소수로 바꾼다 |
time.sleep(... * 0.1) | 예시라서 기다릴 시간을 10분의 1로 줄였다 |
return status, i + 1 | 마지막 상태 코드와 몇 번째에 받았는지를 함께 돌려준다 |
이 코드가 뜻하는 것
인기 식당에 전화했더니 "1분 뒤에 다시 걸어 주세요"라는 안내가 두 번 나오고, 세 번째에 주문을 받았습니다. 안내대로 기다렸다 거는 손님이 식당도 덜 붐비게 합니다. 여기서 안내 멘트가 503 과 Retry-After, 전화 거는 손님이 call_with_retry 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
answers = iter([...]) | 503, 503, 200 을 차례로 돌려줄 흉내 서버 | 두 번은 안내 멘트, 세 번째에 연결 |
status, headers = send() | 요청을 보내고 답을 받는다 | 전화를 건다 |
if status != 503: | 503 이 아니면 바로 돌려준다 | 연결되면(또는 다른 이유면) 끊고 결과를 쓴다 |
time.sleep(...) | Retry-After 만큼 기다린다 | 안내대로 1분 기다린다 |
print(...) | (200, 3) | 세 번째 전화에서 주문했다 |
결과 (200, 3) 은 성공 코드와 시도 횟수입니다. 422 같은 다른 오류는 다시 걸어도 같으므로 첫 시도에서 바로 돌려줍니다.
작게 줄여서 눈으로 보기
iter 와 next, 머리글의 기본값을 따로 떼어 봅니다. 값은 설명용입니다.
answers = iter(["503", "200"]) # 차례로 꺼낼 답(설명용)
print(next(answers))
print(next(answers))
try:
next(answers)
except StopIteration:
print("더 꺼낼 것이 없다")
headers = {"Retry-After": "2"}
print(float(headers.get("Retry-After", 1)), float({}.get("Retry-After", 1)))503
200
더 꺼낼 것이 없다
2.0 1.0| 결과 | 읽는 법 |
|---|---|
503, 200 | next 를 부를 때마다 목록의 다음 값이 하나씩 나온다 |
더 꺼낼 것이 없다 | 값이 다 떨어지면 StopIteration 이 난다. 위 예시가 tries=4 로 횟수를 묶은 것도 끝없이 다시 보내지 않으려는 것이다 |
2.0 1.0 | 머리글에 값이 있으면 그 값(2초), 없으면 기본값 1초를 소수로 바꿨다 |
주의할 점
- 부하 시험은 실제로 받을 법한 글로 합니다. 같은 글만 되풀이하면 캐시가 모두 받아 내어 실제보다 빠르게 나옵니다.
- 발표에는 잘 되는 요청만이 아니라 503, 504 가 나오는 장면도 보여 줍니다. 붐빌 때 어떻게 되는지 아는 서비스가 믿을 만한 서비스입니다.