byteforce

CPN 한국어 자습서 · Claude Certified Developer — Foundations Prep

4-3 · 프로덕션 엔지니어링

테스트와 트레이싱 — 실패가 난 지점 찾기

Testing and tracing

평가(eval)는 결과가 얼마나 좋은지를 숫자 하나로 알려 줍니다. 다만 그 숫자만으로는 실패가 어디에서 났는지까지는 알 수 없고, 통과한 평가가 워크플로(작업 흐름) 어딘가의 고장을 가리고 있는 경우도 있습니다. 이 레슨에서는 실패 유형을 갈라내는 네 단계의 테스트와, 어느 단계가 잘못된 결과를 냈는지 보여 주는 트레이싱을 차례로 살펴봅니다.

이 장에서 배우는 것What you'll learn

약 32분
1

네 단계의 테스트 — 유닛 · 기능 · 통합 · 엔드투엔드가 각자 잡는 실패

2

조용한 실패가 가장 많이 숨는 통합(이음매) 레벨

3

트레이스로 실패가 난 단계를 짚어내는 법

4

질문에 따라 검색 경로를 나누는 라우팅

5

각각은 통과했는데 이음매에서 깨지는 실제 사례

6

트레이스를 읽고 어떤 테스트 레벨의 실패인지 진단하기

먼저 짚고 갈 용어
평가 (eval)
모델이나 워크플로의 출력이 얼마나 좋은지를 정해진 기준으로 채점해 숫자로 나타내는 것.
트레이스 (trace)
한 번의 실행에서 각 단계 — 프롬프트, 도구 호출, 중간 출력, 걸린 시간 — 를 차례로 적어 둔 기록.
유닛 테스트 (unit test)
함수 하나를 따로 떼어, 그것만 제대로 동작하는지 확인하는 테스트.
통합 테스트 (integration test)
두 요소가 맞물려 값을 주고받는 이음매를 함께 돌려 보는 테스트.
이음매 (seam · handoff)
한 요소의 출력이 다음 요소의 입력으로 넘어가는 지점.
라우팅 (routing)
들어온 질문을 값싼 분류 단계로 먼저 살펴, 어떤 처리 경로로 보낼지 정하는 것.

네 단계의 테스트, 각자 다른 실패를 잡는다

Various test levels, each catching a failure the others miss

테스트는 그것이 어떤 실패를 잡아내는지 알 때에만 쓸모가 있습니다. 일을 나눠 맡는 네 단계가 있는데, 조용히 새어 나가는 프로덕션(실서비스) 실패는 그중 한 단계에 몰려 있는 경우가 많습니다.

핵심

조용한 실패가 가장 많이 숨는 자리는 통합 레벨입니다. 두 요소가 각자 테스트는 통과하면서도, 둘 사이에서 값을 주고받는 이음매가 어긋나 있을 수 있기 때문입니다.

트레이싱 — 실패가 난 곳을 찾기

Tracing: finding the source of failure

테스트는 실패가 있다는 것은 알려 주지만, 어느 단계가 그 실패를 냈는지까지는 알려 주지 않습니다. 트레이스가 더해 주는 것이 바로 그 부분입니다.

트레이스는 한 번의 실행에서 각 단계를 차례로 적어 둡니다 — 프롬프트, 도구 호출, 중간 출력, 그리고 걸린 시간까지요. 어떤 경우가 실패하면, 트레이스를 보고 어느 단계가 잘못된 결과를 냈는지 짚을 수 있습니다. 트레이스가 없으면 실패한 평가는 무언가 잘못됐다는 것만 알려 줄 뿐, 어디서 잘못됐는지는 알려 주지 않습니다. 이 차이가 5분이면 끝날 수정과, 워크플로를 손으로 하루 종일 따라가며 찾는 일 사이의 차이를 만듭니다. 트레이스는 실행을 시간 순으로 늘어놓은 것과 비슷해서, 중간 출력까지 보이면 실패한 단계는 대개 눈에 바로 들어옵니다.

TRACE — 한 번의 실행을 시간 순으로 기록

trace · run 8f21c
[trace run_id=8f21c]  case: "Where is my refund?"
  step 1  retrieve(query)        ok      42ms   -> 3 chunks
  step 2  build_prompt(chunks)   ok       1ms   -> prompt 1,240 tok
  step 3  model.call(prompt)     ok     980ms   -> answer "..."
  step 4  parse(answer)          FAIL     2ms   -> KeyError: amount
      final score: 0   (failure localized to step 4, the parser)

트레이스는 "이 경우가 실패했다"를 "4단계: 파서모델이 돌려준 응답을 받아 프로그램이 쓸 형태로 해석하는 다음 단계 코드.가 모델이 돌려주지 않은 필드에서 KeyError를 냈다"로 바꿔 줍니다. 이렇게 되면 변경을 검토하기도 쉬워집니다. 점수가 떨어졌다는 사실만이 아니라, 어느 단계가 움직였는지를 함께 보여 줄 수 있으니까요.

쉽게 말하면

트레이스는 한 번의 실행을 시간 순으로 적어 둔 기록입니다. 어디쯤에서 okFAIL로 바뀌는지만 따라가면, 손댈 단계가 어디인지 금방 보입니다.

질문에 따라 검색 경로를 나누기

Routing so you pay for iteration only when you need it

모든 경우에 같은 검색 전략을 쓸 필요는 없습니다. 값싼 분류 단계 하나를 앞에 두면, 사실 하나만 찾으면 되는 질문은 한 번만 가져오는 경로로, 여러 부분으로 나뉜 질문은 여러 번에 걸쳐 검색하는 경로로 나눠 보낼 수 있습니다. 이렇게 하면 반복 검색이 정말 필요한 질문에만 그 비용을 쓰게 됩니다.

모든 질문을 반복 검색으로 처리하면, 한 번만 찾아도 될 질문에까지 비용과 지연이 붙습니다. 반대로 모든 질문을 미리 만들어 둔 색인 하나로만 처리하면, 여러 번 살펴봐야 답이 나오는 질문에는 얕은 답이 돌아옵니다. 라우터(경로를 고르는 분류 단계)는 질문을 읽고 경로를 고르는 작은 모델 호출 하나입니다.

route() — 질문을 읽고 경로를 고르는 분류 호출

python · route
def route(query):
    kind = classify(query)        # cheap call: "lookup" or "multi_step"
    if kind == "lookup":
        return fetch_once(query)       # static retrieval, one pass
    return agentic_search(query)      # search across rounds

이 분류 호출 한 번은, 한 번의 검색이면 됐을 질문에 반복 검색을 돌리는 것보다 훨씬 쌉니다. 단순 조회도 있고 여러 번 검색이 필요한 질문도 섞여 있을 때 라우터가 제값을 합니다. 반대로 모든 질문이 같은 모양이라면 라우터를 생략하고, 맞는 경로 하나를 바로 쓰면 됩니다.

곁에 두고 보는 참고표

The reference you can keep open while you build

앞에서 본 내용을 한눈에 견주도록 정리한 표입니다. 각 테스트가 무엇을 떼어 확인하는지무엇을 놓치는지를 나란히 두면, 지금 필요한 테스트가 어느 레벨인지 고르기 쉬워집니다.

LEVEL · ISOLATES · CANNOT CATCH — 레벨별 대조표

레벨무엇을 떼어 확인하나놓치는 것
유닛파서나 도구 래퍼 같은 함수 하나를, 따로 떼어 확인한다.요소들이 어떻게 맞물리는지에 관한 것 전부.
기능Claude 호출 하나가 입력에 대해 기대한 형태를 돌려주는지.그 호출 하나를 둘러싼 시스템에서 나는 실패.
통합두 요소가 값을 주고받는 이음매. 예를 들어 검색 결과가 모델로 넘어가는 지점.전체가 함께 돌아갈 때에만 드러나는 흐름 전반의 동작.
엔드투엔드사용자가 실행하는 그대로, 입력부터 출력까지 전체 흐름.정확히 어디서 깨졌는지 — 최종 결과만 보기 때문이다.
검색 선택안정된 자료에서 사실 하나를 찾을 때는, 정해진 묶음을 한 번만 가져온다.여러 단계가 필요한 질문과 계속 바뀌는 자료 — 이런 경우는 여러 번에 걸쳐 검색해야 한다.

STRENGTH · COST · ALTERNATIVE — 한 줄 요약

잘 잡아 주는 것실패를 특정 단계로 좁혀 주고, 각 테스트를 그것이 볼 수 있는 실패에 맞춰 준다.
드는 비용·복잡도트레이싱과 네 단계의 테스트는 직접 만들고 유지해야 하는 구조물이다.
다른 방법이 나을 때안정된 자료에서 사실 하나만 찾는 경우라면, 반복 검색보다 한 번만 가져오는 방식이 낫다.

각각은 통과했는데 이음매에서 깨졌다

Watch Out: The pieces passed and the seam broke

따로 떼어 놓고 보면 멀쩡한 요소들이, 서로 맞물리는 지점에서 조용히 깨지는 경우가 있습니다. 프롬프트와 파서를 각각 따로 테스트해서 둘 다 통과했고, 그래서 전체 흐름도 믿었다고 해 보겠습니다.

평가 실행에서 나온 트레이스를 보면, 파서 유닛 테스트도 모델 호출 기능 테스트도 통과합니다. 둘 다 따로 놓고 보면 기대한 형태를 돌려줍니다. 그런데 엔드투엔드 실행은 실패합니다. 트레이스를 아래로 읽어 내려가 보면, 실패는 검색 결과가 모델 호출로 넘어가는 이음매에서 일어납니다.

EVAL RUN — 유닛·기능은 초록, 엔드투엔드는 이음매에서 빨강

eval run · e2e
PASS  test_parser_unit               parser returns date objects
PASS  test_extract_shape_functional  model call returns {primary_date, issue}
FAIL  test_full_flow_e2e
[trace] step 1 retrieve(q)        ok    -> 3 chunks (list of dicts)
        step 2 build_prompt(ctx)  ok    -> ctx inserted as raw list
        step 3 model.call(prompt) ok    -> answer ignores the context
        step 4 assert answer...   FAIL  -> model answered from memory
cause: retrieve() returns [{"content": ...}], build_prompt() expected
       a plain string, so the model received malformed context.

각 함수는 따로 두면 맞습니다. retrieve()는 청크(검색해서 잘라 온 문서 조각) 딕셔너리 목록을 돌려주고, 프롬프트 빌더는 문자열을 받도록 작성됐습니다. 그래서 컨텍스트(모델에 함께 넣어 주는 참고 자료)가 어긋난 형태로 도착하고, 모델은 검색해 온 정책 대신 자기 기억에서 답을 만듭니다. 두 요소 사이의 이음매는 어떤 테스트도 돌려 본 적이 없습니다. 그 자리를 확인하는 테스트가 없었기 때문입니다. 이것이 통합 레벨이 잡으라고 있는 실패입니다. 유닛 테스트는 유닛 자체가 잘 동작하니 못 잡고, 기능 테스트는 잘 만들어진 입력에서는 호출이 되니 못 잡습니다. 실제 검색 결과로 검색→모델 이음매를 직접 돌려 보는 테스트만이, 사용자보다 먼저 이 어긋남을 드러냅니다.

주의 · WATCH OUT
  • 왜 깨졌나 — 검색 단계와 프롬프트 빌더 사이의 형식 약속이 정해진 적이 없었습니다. 한쪽은 딕셔너리 목록을, 다른 쪽은 문자열을 기대했고, 그 경계를 지켜 줄 장치가 없었습니다.
  • 어떻게 막나 — 실제 검색 결과로 두 요소를 함께 돌려 보는 통합 테스트를 더합니다. 각 요소는 따로 두면 잘 동작하니 유닛 테스트로는 못 잡고, 이음매를 직접 실행하는 테스트만이 사용자보다 먼저 어긋남을 드러냅니다.
기억할 점

스스로 점검 — 어떤 테스트 레벨의 실패인가

Checkpoint · Diagnose which test level a failure belongs to

지금 직접 해 보겠습니다. 아래 트레이스는 유닛 테스트는 모두 통과하는데 엔드투엔드 테스트만 실패하는 경우입니다. 어디서 깨졌는지 짚고, 원인이 무엇인지 이름을 붙인 다음, 겨눈 수정그 실패를 잡았을 테스트 레벨을 세 선택지 중에서 함께 고르세요.

CHECKPOINT TRACE — 유닛은 통과, 엔드투엔드는 실패

trace · checkpoint
PASS  test_retrieve_unit          returns 3 chunks for a known query
PASS  test_model_call_functional  returns a well-formed answer string
FAIL  test_full_flow_e2e
  step 1 retrieve(q)          ok    -> [{"content": ...}, ...]
  step 2 build_prompt(chunks) ok    -> chunks placed without .content
  step 3 model.call(prompt)   ok    -> answer unrelated to the documents
  step 4 assert "30 days"     FAIL  -> phrase not in answer

선택지 A · 파서를 고친다

option a
def parse_date(s): return dateutil.parse(s)   # already passes its unit test

선택지 B · 프롬프트 문구를 고친다

option b
prompt = "Answer carefully and cite the policy."  # rewords, ignores the seam

선택지 C · 이음매를 맞추고 통합 테스트를 더한다

option c
context = "\n".join(c["content"] for c in chunks)  # extract .content
prompt = build_prompt(question, context)
# new test drives retrieve() -> build_prompt() together on real chunks

정답을 먼저 떠올려 본 뒤 골라 보세요. 맞히면 설명이 나옵니다.

Q1위 트레이스가 보여 주는 실패를 고치려면 어느 선택지가 맞을까요?

Q2대부분의 조용한 프로덕션 실패가 숨는 테스트 레벨은 어디일까요?

Q3평가(eval)는 실패가 있다는 것을 알려 줍니다. 여기에 트레이스가 더해 주는 것은 무엇일까요?

MEMBER SESSION REQUIRED · REGISTRATION IS FREE

여기부터는 등록한 분에게 열립니다.

전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 38개 레슨을 끝까지 읽을 수 있습니다.

등록하고 이어서 읽기

이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.