에이전틱스
도메인 4. 평가와 테스트, 디버깅

4.1디버깅과 오류 처리

예상 학습 시간 25

모델을 쓰는 코드에서 가장 어려운 디버깅은 예외가 나지 않는 실패입니다. 이 수업에서는 오류를 유형별로 가르고, 문제의 출처가 통합 계층인지 모델 출력인지 판별하는 순서를 다룹니다.

오류를 세 갈래로 가르기

대응 방법이 다르므로 먼저 갈라야 합니다.

전송 계층 오류 — 요청이 도달하지 못하거나 거절됩니다. 타임아웃, 요청 한도 초과, 5xx 응답입니다. 대체로 재시도가 답입니다.

요청 오류 — 우리가 잘못 보냈습니다. 형식 오류, 인증 실패, 지원하지 않는 파라미터입니다. 재시도해도 같으므로 고쳐야 합니다.

출력 문제 — 응답은 정상으로 왔는데 내용이 문제입니다. 형식이 어긋나거나, 사실이 틀렸거나, 잘렸습니다. 여기가 가장 다루기 까다롭습니다.

try:
    resp = client.messages.create(...)
except APITimeoutError:
    ...          # 전송 계층 — 재시도
except APIStatusError as e:
    if e.status_code == 429:
        ...      # 전송 계층 — 백오프 후 재시도
    else:
        raise    # 요청 오류 — 고쳐야 한다
# 여기까지 왔어도 끝이 아니다
if resp.stop_reason == "max_tokens":
    ...          # 출력 문제 — 잘렸다

세 번째 갈래를 예외 처리로만 다루려는 것이 흔한 실수입니다. 정상 응답 안의 실패는 예외를 던지지 않으므로 따로 검사해야 합니다.

통합 계층인가 모델 출력인가

"결과가 이상하다"는 신고를 받으면 먼저 이 둘을 갈라야 합니다. 어느 쪽인지에 따라 고칠 곳이 완전히 다릅니다.

가르는 방법은 모델에 실제로 무엇이 들어갔고 무엇이 나왔는지 원문을 보는 것입니다. 요약된 로그가 아니라 실제 요청 본문과 응답 본문이 필요합니다.

  • 들어간 내용이 우리가 의도한 것과 다르면 → 통합 계층 문제입니다. 검색 결과가 비어 있었거나, 템플릿에 값이 안 채워졌거나, 잘못된 문서를 넣었습니다.
  • 들어간 내용은 맞는데 나온 것이 이상하면 → 모델 출력 문제입니다. 프롬프트를 고치거나 예시를 더하거나 모델을 바꿉니다.

실무에서 신고의 상당수가 첫 번째입니다. 프롬프트를 아무리 다듬어도 애초에 잘못된 자료가 들어갔으면 좋은 답이 나올 수 없습니다. 프롬프트부터 고치려 드는 습관이 시간을 가장 많이 낭비합니다.

log.debug("llm_call", extra={
    "request_id": rid,
    "system_hash": sha1(system)[:8],      # 어떤 프롬프트였는지
    "input_preview": user_text[:500],     # 실제로 들어간 것
    "tool_results_len": [len(r) for r in tool_results],   # 비어 있지 않은가
})

형식이 깨질 때

구조화된 출력을 기대했는데 파싱이 실패하는 경우, 원인은 대체로 셋 중 하나입니다.

  1. 잘렸다. stop_reasonmax_tokens이면 JSON이 중간에 끊겨 있습니다. 파서를 고칠 일이 아니라 한도를 늘리거나 출력을 짧게 만들어야 합니다.
  2. 설명이 섞였다. 모델이 JSON 앞뒤에 문장을 붙였습니다. 형식을 강제하는 수단을 쓰거나, 응답 시작을 미리 채워 형태를 고정하면 줄어듭니다.
  3. 스키마를 벗어났다. 열거형에 없는 값이나 예상 밖 필드가 왔습니다.

세 경우에 대응이 다르므로 실패했을 때 원문을 남기는 것이 중요합니다. 파싱 예외만 기록하면 왜 실패했는지 알 수 없습니다.

try:
    data = json.loads(text)
except json.JSONDecodeError:
    log.warning("parse_failed", extra={
        "stop_reason": resp.stop_reason,     # 잘림 여부가 여기서 갈린다
        "raw_head": text[:200],
        "raw_tail": text[-200:],
    })
    raise

재현과 좁히기

고치기 전에 재현이 먼저입니다. 재현되지 않으면 무엇을 고쳤는지 확인할 방법이 없습니다.

순서는 이렇습니다.

  1. 같은 입력으로 다시 돌린다. 같은 결과가 나오면 재현된 것입니다. 나오지 않으면 입력이 실제로 달랐거나 변동 범위 안의 흔들림입니다.
  2. 한 번에 하나씩 뺀다. 도구 결과를 고정값으로 바꿔 보고, 검색을 끄고 넣어 보고, 이력을 지워 봅니다. 어디서 정상으로 돌아오는지가 원인을 가리킵니다.
  3. 최소 사례로 줄인다. 재현되는 가장 작은 입력을 만듭니다.
  4. 고치고 집합으로 확인한다. 그 한 건이 아니라 평가 세트 점수를 봅니다.

변동성 때문에 한 번 돌려서는 판단하기 어려울 때가 있습니다. 같은 입력을 여러 번 돌려 몇 번 실패하는지 세는 편이 낫습니다. 열 번 중 두 번 실패하던 것이 열 번 중 한 번이 되었다면 개선이지만, 한 번 성공했다고 고쳐졌다고 볼 수는 없습니다.

시험 함정

  • 정상 응답이면 성공으로 처리하는 구현 — 잘림과 형식 이탈은 예외를 던지지 않습니다.
  • 모든 실패를 재시도로 대응하는 선택지 — 요청 형식 오류는 재시도해도 같습니다.
  • 결과가 이상할 때 프롬프트부터 고치는 순서 — 무엇이 들어갔는지 먼저 확인해야 합니다.
  • 파싱 실패에서 예외만 기록하는 처리 — stop_reason과 원문이 없으면 원인을 가릴 수 없습니다.
  • JSON이 잘렸을 때 파서를 관대하게 고쳐 해결하려는 접근 — 원인은 출력 한도입니다.
  • 한 번 성공하면 고쳐진 것으로 보는 판정 — 변동성이 있으므로 여러 번 돌려 실패율로 봐야 합니다.

실습 시나리오

실제 시험과 같은 형식의 시나리오 문제입니다.

요약 기능이 가끔 엉뚱한 내용을 반환한다는 신고가 들어왔습니다. 예외는 나지 않고 응답도 정상입니다. 가장 먼저 확인할 것은 무엇입니까?

빌드 연습 · 실패 유형별로 갈라 처리하는 래퍼 만들기

30
  1. 1.유형 분류

    전송 계층·요청 오류·출력 문제를 각각 어떻게 감지할지 정리합니다.

    기대 결과 · 세 갈래와 감지 방법이 표로 남습니다.

  2. 2.정상 응답 검사

    stop_reason과 스키마 검증을 통과하지 못하면 실패로 처리합니다.

    기대 결과 · 예외가 없어도 실패로 잡히는 경로가 생깁니다.

  3. 3.진단 로그

    실패 시 입력 미리보기와 stop_reason, 원문 앞뒤를 남깁니다.

    기대 결과 · 로그만 보고 세 유형 중 무엇인지 판정할 수 있습니다.

  4. 4.출처 가르기

    도구 결과를 고정값으로 바꿔 같은 입력을 다시 돌립니다.

    기대 결과 · 정상으로 돌아오는지로 통합 계층 문제인지 판별됩니다.

  5. 5.실패율 측정

    같은 입력을 10회 돌려 실패 횟수를 기록합니다.

    기대 결과 · 수정 전후의 실패율이 비교 가능한 수치로 남습니다.

출처 및 더 읽기