모델 출력은 확률적이므로 실패를 전제로 시스템을 설계해야 합니다. 이 수업에서는 오류 피드백을 포함한 재시도 루프 설계, 재시도 횟수와 비용의 균형, 재시도마다 무엇을 바꿔야 하는지(무작위성 조절의 역할과 한계), 그리고 반복 실패를 처리하는 폴백(fallback) 경로를 다룹니다.
오류 피드백 재시도의 기본 패턴
검증에 실패한 출력을 버리고 같은 요청을 그대로 다시 보내는 것은 비효율적입니다. 같은 입력과 같은 프롬프트는 같은 실패를 재생산할 확률이 높기 때문입니다. 올바른 패턴은 실패한 출력과 구체적 오류 내용을 대화에 추가해 수정을 요청하는 것입니다. Claude는 자기 출력의 어디가 틀렸는지 알려 주면 높은 확률로 고칩니다.
def call_with_retry(client, messages, validate, max_retries=3):
conversation = list(messages)
for attempt in range(max_retries):
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=2048,
messages=conversation,
)
raw = response.content[0].text
result, errors = validate(raw)
if not errors:
return result
# 실패한 출력과 오류를 대화에 추가해 수정 요청
conversation.append({"role": "assistant", "content": raw})
conversation.append({
"role": "user",
"content": "출력이 검증에 실패했습니다. 오류를 수정해 "
f"전체 JSON을 다시 출력하세요:\n{errors}",
})
raise ExtractionFailed(errors)
핵심 설계 포인트는 세 가지입니다. 첫째, 오류 메시지는 "형식이 잘못됨"이 아니라 "total 필드가 누락됨, date가 YYYY-MM-DD 형식이 아님"처럼 구체적이어야 합니다. 둘째, 수정 요청 시 "전체를 다시 출력하라"고 명시해야 부분 수정문("total을 추가하면 됩니다")이 오는 것을 막습니다. 셋째, 재시도 대화가 길어지면 그 자체가 비용이므로 최대 횟수를 제한합니다.
재시도 횟수, 비용, 그리고 에스컬레이션
재시도는 공짜가 아닙니다. 매 재시도는 지연 시간과 토큰 비용을 추가하며, 재시도 대화에는 이전 실패 출력이 포함되어 입력 토큰이 누적됩니다. 실무 기준은 다음과 같습니다.
최대 2~3회로 제한합니다. 경험적으로 오류 피드백 재시도는 1~2회 안에 성공하거나 계속 실패합니다. 3회 실패한 입력은 프롬프트나 입력 자체에 구조적 문제가 있다는 신호입니다.
실패 유형을 기록합니다. 어떤 입력이, 어떤 오류로, 몇 번째에 성공/실패했는지 로깅하면 프롬프트 개선의 우선순위를 데이터로 정할 수 있습니다. 특정 오류가 반복되면 재시도로 때우지 말고 프롬프트나 스키마를 고쳐야 합니다.
한도 소진 시 에스컬레이션 경로를 정의합니다. 선택지는 세 가지입니다. (1) 사람 검토 큐로 보내기 — 정확성이 중요한 파이프라인의 기본값입니다. (2) 더 강한 모델로 승급 — 일상 처리는 Haiku, 실패 건은 Sonnet으로 보내는 계단식 구성은 비용과 품질의 균형점입니다. (3) 명시적 실패 반환 — 침묵 속에 잘못된 데이터를 흘려보내는 것보다 항상 낫습니다.
def process(document):
try:
return call_with_retry(client, build_messages(document),
validate, max_retries=2)
except ExtractionFailed as e:
log_failure(document.id, e.errors)
return escalate_to_review_queue(document, e.errors)
시험에서는 "재시도 무한 반복"과 "실패를 무시하고 기본값 저장"이 대표적 오답 선택지입니다. 전자는 비용 폭주, 후자는 데이터 오염을 만듭니다.
무작위성과 결정성 전략
모델 출력은 확률 샘플링이므로 같은 입력에도 출력이 달라질 수 있습니다. 이 무작위성을 어떻게 다루는지가 검증·재시도 설계의 나머지 절반입니다.
먼저 현재 API의 사실 하나. temperature·top_p·top_k 같은 샘플링 파라미터는 2026년 기준 최신 모델(Opus 4.7 이후, Opus 5, Sonnet 5 등)에서 제거되어, 기본값이 아닌 값을 보내면 400 오류가 납니다. Opus 4.6·Sonnet 4.6·Haiku 4.5까지만 받습니다. 그래서 "temperature를 낮춘다"는 이제 특정 모델에서만 가능한 보조 수단이고, 결정성은 출력 검증, 툴 호출 강제나 구조화된 출력으로 형식 고정, 그리고 같은 입력의 결과를 재사용하는 결과 캐싱으로 얻습니다. 프롬프트 캐싱은 비용·지연을 줄이는 기능이지 결정성과는 무관합니다.
정답이 있는 작업(추출·분류)에서 무작위성은 순수한 오류 원천입니다. temperature를 받는 모델이라면 0 근처로 두지만, 그것이 완전한 결정성(determinism)을 보장하지는 않는다는 점이 시험 포인트입니다. 같은 입력에도 미세하게 다른 출력이 나올 수 있으므로, 결정성이 필요하면 출력 검증과 결과 캐싱으로 보완해야 합니다.
재시도에서 바꿔야 할 것은 무작위성이 아니라 경로입니다. 같은 실패가 반복된다면 (1) 실패 출력과 구체적 오류를 대화에 붙이고, (2) 예시나 지시를 바꿔 다른 경로를 유도하고, (3) 사고 강도(effort)를 올리거나 상위 모델로 승급합니다. 승급은 원인을 본 뒤의 마지막 수단입니다 — 원인 분석 없이 "더 큰 모델로 교체"하는 선택지는 시험에서 오답으로 처리됩니다. temperature를 받는 모델에서는 재시도에 0.3~0.7로 올려 다른 출력을 유도하는 기법도 있지만, 앞의 셋이 먼저입니다.
생성 작업의 검증은 형식과 내용을 분리합니다. 마케팅 문구처럼 다양성이 필요한 작업은 모델의 기본 무작위성을 그대로 두고, 형식 규칙(길이, 금지어)은 코드 검증으로 잡고 재시도합니다. 다양성과 형식 준수를 모두 얻는 구성입니다.
MODEL = "claude-sonnet-5" # adaptive thinking과 effort를 받는 4.6 이상 모델
def call_adaptive(client, messages, attempt: int):
# 첫 시도는 기본 강도로, 재시도부터 사고 강도를 올린다.
# messages에는 이전 실패 출력과 오류 목록이 이미 추가돼 있어야 한다.
# temperature 상향은 이 값을 아직 받는 모델(Opus 4.6 등)에서만 가능하다.
effort = "medium" if attempt == 0 else "high"
return client.messages.create(
model=MODEL,
max_tokens=2048,
thinking={"type": "adaptive"},
output_config={"effort": effort},
messages=messages,
)
마지막으로, 검증 루프는 4.6에서 다룰 다중 패스 리뷰와 다릅니다. 검증 루프는 기계적 규칙 위반을 코드가 감지해 고치는 것이고, 다중 패스 리뷰는 품질·논리 문제를 모델이 평가하는 것입니다. 두 용어의 구분을 묻는 문제가 출제됩니다.
시험 함정
- '검증 실패 시 같은 요청을 그대로 재전송한다'는 선택지는 오답입니다. 실패 출력과 구체적 오류를 피드백해야 합니다.
- 재시도 횟수를 제한하지 않는 설계는 오답입니다. 2~3회 제한 후 에스컬레이션이 정석입니다.
- 'temperature 0은 완전한 결정성을 보장한다'는 선택지는 함정입니다. 변동성을 줄일 뿐 보장하지 않으며, 최신 모델은 이 값을 받지도 않습니다.
- 반복 실패 입력을 기본값으로 저장하는 선택지는 오답입니다. 침묵 실패는 데이터 오염을 만들며, 사람 검토 큐나 명시적 실패가 정답입니다.
- 추출 작업에 높은 temperature를 권하는 선택지는 오답입니다. 정답이 있는 작업은 무작위성을 최소로 두고 형식은 툴 호출·구조화된 출력으로 고정하는 것이 원칙입니다.
- 코드 기반 검증 루프와 모델 기반 다중 패스 리뷰를 혼동시키는 선택지에 주의하세요.
실습 시나리오
실제 시험과 같은 형식의 시나리오 문제입니다.
일일 1만 건의 영수증을 처리하는 추출 파이프라인에서 약 3%가 스키마 검증에 실패합니다. 현재는 실패 시 빈 값으로 저장합니다. 비용 증가를 최소화하면서 데이터 품질을 높이는 가장 적절한 설계는 무엇입니까?
빌드 연습 · 오류 피드백 재시도 루프와 계단식 에스컬레이션 구현
약 50분1.검증 함수 작성
구문(JSON 파싱)·스키마(필수 필드)·의미(금액 합계) 3계층을 검사하고 구체적 오류 목록을 반환하는 validate 함수를 만듭니다.
기대 결과 · 실패 시 '어느 필드가 왜 틀렸는지' 문자열 목록이 반환됩니다.
2.재시도 루프 구현
실패 출력과 오류 목록을 대화에 추가해 수정을 요청하는 call_with_retry를 구현합니다. 최대 2회로 제한합니다.
기대 결과 · 1회 피드백 후 성공하는 사례가 로그로 확인됩니다.
3.재시도마다 다르게 부르기
첫 시도는 기본 강도, 재시도부터 사고 강도(effort)를 올리고 실패 출력·오류 목록을 붙여 다시 부릅니다. temperature를 받는 모델을 쓴다면 0에서 0.3씩 올리는 변형도 함께 시험합니다.
기대 결과 · 동일 실패 반복 사례에서 재시도 성공률이 변화하는지 관찰됩니다.
4.에스컬레이션 경로
2회 실패 건을 상위 모델로 1회 승급 호출하고, 그래도 실패하면 검토 큐 파일에 기록합니다.
기대 결과 · 실패 건이 침묵 저장되지 않고 모델 승급 → 검토 큐의 경로를 따릅니다.
5.실패 통계 리포트
100건 처리 후 시도 횟수 분포, 오류 유형별 빈도를 집계합니다.
기대 결과 · 가장 잦은 오류 유형이 식별되어 프롬프트 개선 우선순위가 도출됩니다.