Python으로 신뢰할 수 있는 LLM 애플리케이션 구축하기
요약
Anthropic SDK와 Python을 사용하여 프로덕션 수준의 신뢰할 수 있는 LLM 애플리케이션을 구축하는 엔지니어링 가이드를 제공합니다. 모델 선택, 구조화된 출력 활용, RAG를 통한 환각 방지 및 예외 처리 전략을 다룹니다.
핵심 포인트
- 모델 출력을 사실이 아닌 검증해야 할 가설로 취급할 것
- Pydantic을 활용한 구조화된 출력(Structured Output) 사용 권장
- RAG와 명시적 지침을 통해 LLM의 환각 현상 최소화
- 지수 백오프 등 SDK의 재시도 메커니즘을 활용한 예외 처리
서론
LLM API를 호출하는 것은 쉽습니다. 하지만 그 위에 신뢰할 수 있는(reliable로) 애플리케이션을 구축하는 것 — 즉, 예측 가능한 방식으로 실패하고, 잘못된 답변을 내놓는 환각 (hallucination) 현상이 없으며, 예상치 못한 비용 청구로 당신을 놀라게 하지 않는 애플리케이션을 만드는 것 — 은 진정한 엔지니어링 분야입니다.
핵심적인 사고방식의 전환: 모델의 출력을 신뢰해야 할 사실이 아니라, 검증해야 할 가설로 취급하십시오. 이 포스트에서는 Anthropic의 Claude와 공식 anthropic SDK를 사용하여 Python LLM 애플리케이션을 프로덕션 수준(production-grade)으로 만드는 관행들을 다룹니다.
작업에 적합한 모델 선택하기
모델 선택은 기본 설정이 아니라 하나의 결정입니다. 작업의 난이도에 맞춰 모델의 등급을 맞추십시오:
import anthropic
client = anthropic.Anthropic() # 환경 변수에서 ANTHROPIC_API_KEY를 읽어옵니다
...
저렴한 모델로 충분한 곳에서 비싼 모델을 절대 사용하지 마시고, 품질이 중요한 곳에서 자원을 과소 할당하지 마십시오. 비용과 지연 시간 (latency)은 기능(feature)입니다 — 이를 추적하십시오.
구조화된 출력(Structured Output) 얻기 — 산문(Prose)을 파싱하지 마세요
LLM 앱에서 취약성이 발생하는 가장 큰 원인은 자유 형식의 텍스트에서 구조화된 데이터를 스크래핑하는 것입니다. 스키마(schema)에 따라 검증된 타입 지정 출력(typed outputs)을 선호하십시오. Anthropic SDK를 사용하면 messages.parse()가 Pydantic 모델을 기준으로 응답을 대신 검증해 줍니다:
from pydantic import BaseModel
class Invoice(BaseModel):
...
검증은 모델이 제약을 받는 스키마를 대상으로 이루어지므로, 파싱되기를 기도해야 하는 문자열 대신 타입이 지정된 객체를 얻을 수 있습니다. 구조화된 출력은 "모델이 보통 JSON을 반환한다"를 "모델이 _이러한 형태_를 반환한다"로 바꿔줍니다.
모델에 근거 제공하기 — 환각(Hallucinate)하게 두지 마세요
LLM은 자신 있게 사실을 지어낼 것입니다. 반드시 정확해야 하는 모든 것에 대해, 모델에 소스 자료를 제공하고 오직 그 자료만을 사용하여 답변하도록 지시하십시오. 이것이 가장 단순한 형태의 검색 증강 생성 (RAG, retrieval-augmented generation)입니다:
prompt = f"""아래의 문맥(context)만을 사용하여 질문에 답하세요.
만약 답변이 문맥에 없다면, "모르겠습니다"라고 말하세요.
...
이 두 가지 요소가 신뢰성을 보장합니다. 바로 명시적인 "문맥에서만(only from context)" 지침과, 모델이 답변을 꾸며내야 한다는 압박을 느끼지 않도록 하는 명시적인 탈출구("모르겠습니다라고 말하세요")입니다. 그다음에는 인용(cite) 하세요. 모델이 어떤 구절을 사용했는지 가리키게 하여, 사람이 이를 검증할 수 있도록 해야 합니다.
예외적인 경로(Unhappy Path) 처리하기
네트워크는 실패할 수 있고 속도 제한(rate limits)도 발생합니다. Anthropic SDK는 이미 지수 백오프(exponential backoff)를 사용하여 일시적인 오류(429, 5xx, 연결 오류)를 재시도합니다. 이를 새로 구현하기보다는 기존 설정을 구성하여 사용하세요:
client = anthropic.Anthropic(max_retries=4, timeout=30.0)
특정(specific) 예외를 포착하고, 재시도 가능한 오류와 종료해야 하는 오류를 구분하여 분기 처리하세요:
try:
response = client.messages.create(...)
except anthropic.RateLimitError as exc:
...
부수 효과(side effects)가 있는 모든 작업(카드 결제, 모델의 결정에 따른 이메일 발송 등)에 대해서는 **멱등성(idempotent)**을 확보하세요. 모델이나 재시도로 인해 동일한 작업이 두 번 실행될 수 있습니다.
제어 흐름은 코드에, 판단은 모델에
모델은 판단을 위해 사용하고, 장부 기록(bookkeeping)은 코드를 사용하세요. 루프(loops), 분기(branching), 팬아웃(fan-out)은 모델에게 "완료될 때까지 계속하세요"라고 요청하는 프롬프트가 아니라, 결정론적인(deterministic) Python 코드에 속해야 합니다. 도구를 사용하는 에이전트적(agentic) 작업의 경우, 각 도구 호출을 가로채고, 검증하고, 로그를 남길 수 있도록 루프를 직접 제어하세요:
messages = [{"role": "user", "content": user_input}]
while True:
response = client.messages.create(
...
모델은 무엇을 할지 결정하고, 여러분의 코드는 그것이 _허용되는지_를 결정하며 발생한 일을 기록합니다.
다른 신뢰할 수 없는 입력과 마찬가지로 출력을 평가하기
테스트 없이 함수를 배포하지 않듯이, 평가(evals) 없이 프롬프트를 배포하지 마세요. 검증된 정답(known-good outputs)이 포함된 대표적인 입력값들로 구성된 작은 데이터셋을 구축하고, 프롬프트나 모델을 변경할 때마다 이를 기준으로 모델을 점수화하세요:
def evaluate(cases: list[dict]) -> float:
passed = 0
for case in cases:
...
평가(Evals)는 프롬프트 수정이 한 가지 사례에는 도움이 되었지만 다른 열 가지 사례를 조용히 망가뜨리는 회귀(regression) 현상을 잡아냅니다. 이는 실패하는 단위 테스트(unit test)와 LLM에서의 동일한 개념입니다.
비용과 지연 시간(Latency)을 줄이기 위한 반복되는 컨텍스트 캐싱 (Cache Repeated Context)
만약 많은 요청이 시스템 프롬프트(system prompt), 방대한 문서, 퓨샷 예시(few-shot examples)와 같이 크고 고정된 접두사(prefix)를 공유한다면, 프롬프트 캐싱(prompt caching)을 통해 해당 접두사를 훨씬 저렴한 비용과 낮은 지연 시간으로 제공할 수 있습니다:
response = client.messages.create(
model="claude-opus-4-8",
max_tokens=1024,
...
캐싱은 접두사 일치(prefix match) 방식입니다. 안정적인 콘텐츠를 앞에 두고, 요청마다 변하는 내용(타임스탬프, 사용자의 질문 등)은 그
뒤에 배치하십시오. 반복되는 호출 동안 cache_read_input_tokens가 계속 0으로 유지된다면, 무언가 변동성이 있는 요소가 접두사를 무효화하고 있는 것입니다.
실무 체크리스트 (Practical Checklist)
| 관행 (Practice) | 중요한 이유 (Why it matters) |
|---|---|
| 작업 난이도에 맞는 모델 티어(model tier) 선택 | 과도한 비용 지불이나 자원 부족 방지 |
| ... |
마치며 (Final Thoughts)
신뢰할 수 있는 LLM 애플리케이션은 완벽한 프롬프트를 찾는 것만으로 구축되지 않습니다. 이는 다른 모든 시스템과 마찬가지로 동일한 엔지니어링 규율(engineering discipline)을 통해 구축됩니다. 즉, 경계에서의 강력한 타이핑(strong typing), 신뢰할 수 없는 출력에 대한 검증(verification), 결정론적 제어 흐름(deterministic control flow), 우아한 실패 처리(graceful failure handling), 그리고 측정 가능한 평가(measurable evaluation)가 필요합니다.
모델은 판단력을 제공합니다. 그 주변의 모든 것 — 구조, 검증, 가드레일(guardrails) — 은 여러분의 몫입니다. 이 부분들을 제대로 구축한다면, LLM은 예상치 못한 변수의 근원이 아닌 신뢰할 수 있는 컴포넌트가 될 것입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기