LLM 앱 개발 실전 #12 비용, 평가, 관측
11편까지 기능을 만드는 법을 다뤘습니다. 그런데 만든 앱을 실제로 운영하려면 세 가지가 더 필요합니다. 비용을 가늠하고 줄이는 일, 답의 품질을 측정하는 일, 그리고 무슨 일이 일어나는지 들여다보는 일입니다. 이번 글에서는 이 운영의 토대를 정리합니다.
토큰 비용 가늠하기 #
LLM 비용은 주고받은 토큰 양에 비례합니다. 호출하기 전에 입력이 몇 토큰인지 미리 확인하려면 토큰 계산 기능을 씁니다.
import anthropic
client = anthropic.Anthropic()
result = client.messages.count_tokens(
model="claude-sonnet-4-6",
messages=[{"role": "user", "content": open("long_document.txt").read()}],
)
print(result.input_tokens) # 입력 토큰 수토큰 수는 모델마다 다르게 셀 수 있으므로, 실제로 쓸 모델을 지정해 잽니다. 응답을 받은 뒤에는 response.usage에서 실제 사용량을 확인합니다. input_tokens와 output_tokens로 한 번의 호출이 얼마였는지 추적할 수 있습니다.
프롬프트 캐싱으로 비용 줄이기 #
RAG나 긴 system 프롬프트처럼, 매 호출에서 같은 내용이 앞부분에 반복된다면 캐싱으로 큰 비용을 아낄 수 있습니다. 한 번 캐시된 부분은 다음 호출에서 훨씬 싸게 처리됩니다.
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
system=[
{
"type": "text",
"text": large_shared_context, # 매번 같은 큰 맥락
"cache_control": {"type": "ephemeral"},
}
],
messages=[{"role": "user", "content": "질문"}],
)
print(response.usage.cache_read_input_tokens) # 캐시에서 읽은 토큰캐싱은 앞부분이 글자 단위로 똑같을 때만 적용됩니다. 그래서 변하지 않는 내용(고정된 지침, 공유 문서)을 앞에 두고, 매번 바뀌는 내용(질문, 시각)을 뒤에 둡니다. 제대로 동작하는지는 usage.cache_read_input_tokens로 확인합니다. 이 값이 계속 0이면 앞부분에 매번 바뀌는 무언가가 끼어 캐시가 깨지고 있다는 신호입니다.
모델로 비용 조절하기 #
2편에서 본 세 등급을 비용 관점에서 다시 봅니다. 모든 호출에 가장 강력한 모델을 쓸 필요는 없습니다. 작업 성격에 맞춰 등급을 나누면 비용이 크게 줄어듭니다.
- 단순 분류, 짧은 추출 → 가장 저렴한 Haiku
- 대부분의 실무 작업 → 균형 등급 Sonnet
- 까다로운 추론, 긴 에이전트 작업 → 가장 강력한 Opus
한 앱 안에서도 단계마다 다른 모델을 쓸 수 있습니다. 예를 들어 분류는 Haiku로, 최종 답 생성은 Sonnet으로 나누는 식입니다.
답의 품질 평가하기 #
프롬프트를 바꿨을 때 답이 나아졌는지 어떻게 알까요? 눈으로 몇 개 보는 것만으로는 부족합니다. 평가를 자동화하는 한 방법은, 또 다른 LLM 호출에 채점을 맡기는 것입니다. 이를 LLM-as-judge라고 합니다.
def judge(question: str, answer: str) -> str:
prompt = f"""아래 답변이 질문에 정확하고 도움이 되는지 평가해줘.
'좋음' 또는 '나쁨' 한 단어로만 답해.
질문: {question}
답변: {answer}"""
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=10,
messages=[{"role": "user", "content": prompt}],
)
return next(b.text for b in response.content if b.type == "text")질문과 기대 답의 묶음(평가셋)을 미리 만들어 두고, 프롬프트나 모델을 바꿀 때마다 이 평가를 돌리면, 변경이 품질을 올렸는지 내렸는지를 숫자로 비교할 수 있습니다. 5편에서 본 구조화된 출력으로 채점 결과를 받으면 집계가 더 쉬워집니다.
동작을 들여다보기 #
LLM 앱은 같은 입력에도 답이 달라지므로, 문제가 생겼을 때 무슨 일이 있었는지 보려면 기록이 필요합니다. 최소한 입력 프롬프트, 받은 답, 사용한 토큰을 남깁니다. 문제를 Anthropic에 알릴 때를 대비해 요청 식별자도 함께 기록해 두면 좋습니다.
response = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=messages,
)
log({
"request_id": response._request_id, # 문제 추적용 식별자
"input_tokens": response.usage.input_tokens,
"output_tokens": response.usage.output_tokens,
"stop_reason": response.stop_reason,
})이렇게 쌓은 기록으로 비용 추이를 보고, 답이 자주 잘리는지(stop_reason), 특정 입력에서 문제가 생기는지를 추적합니다.
배치로 더 줄이기 #
급하지 않은 대량 작업이라면 배치 API로 비용을 절반으로 줄일 수 있습니다. 실시간 응답 대신, 많은 요청을 한꺼번에 맡기고 결과를 나중에(보통 한 시간 안에) 받는 방식입니다. 토큰 요금이 50% 할인됩니다.
from anthropic.types.message_create_params import MessageCreateParamsNonStreaming
from anthropic.types.messages.batch_create_params import Request
batch = client.messages.batches.create(
requests=[
Request(
custom_id=f"item-{i}",
params=MessageCreateParamsNonStreaming(
model="claude-sonnet-4-6",
max_tokens=256,
messages=[{"role": "user", "content": text}],
),
)
for i, text in enumerate(texts)
]
)수천 건의 리뷰를 분류하거나 문서를 한꺼번에 요약하는 것처럼, 즉시 답이 필요 없는 작업에 잘 맞습니다. 사용자가 기다리는 대화형 호출에는 맞지 않지만, 백그라운드 일괄 처리에서는 비용을 크게 아낍니다.
흔히 걸려 넘어지는 곳 #
- 캐시가 깨지는 걸 모른다 — system 앞부분에 현재 시각이나 무작위 값이 들어가면 매번 캐시가 깨집니다.
cache_read_input_tokens가 0인지 확인합니다. - 평가 없이 프롬프트를 고친다 — 평가셋 없이 감으로 프롬프트를 바꾸면, 한 경우는 나아지고 다른 경우는 나빠지는 걸 놓칩니다. 작게라도 평가셋을 둡니다.
- 아무것도 기록하지 않는다 — 기록이 없으면 문제가 생겼을 때 재현도 추적도 어렵습니다. 최소한의 사용량과 식별자는 남깁니다.
마무리 #
이번 글에서는 앱을 운영하는 데 필요한 비용, 평가, 관측을 정리했습니다.
- 토큰을 미리 재고(
count_tokens), 반복되는 앞부분은 캐싱으로 줄이며, 작업에 맞는 모델 등급을 고릅니다. - LLM-as-judge와 평가셋으로 변경이 품질에 미친 영향을 숫자로 봅니다.
- 사용량과 요청 식별자를 기록해 비용과 문제를 추적합니다.
이제 기능과 운영의 토대가 모두 모였습니다. 마지막 글인 “LLM 앱 개발 실전 #13 실전 프로젝트"에서는 지금까지의 조각들을 하나로 묶어, 사내 문서에 답하는 Q&A 봇을 처음부터 끝까지 만들어 봅니다.