CPN 한국어 자습서 · Claude Certified Developer — Foundations Prep
4-3 · 프로덕션 엔지니어링
Testing and tracing
평가(eval)는 결과가 얼마나 좋은지를 숫자 하나로 알려 줍니다. 다만 그 숫자만으로는 실패가 어디에서 났는지까지는 알 수 없고, 통과한 평가가 워크플로(작업 흐름) 어딘가의 고장을 가리고 있는 경우도 있습니다. 이 레슨에서는 실패 유형을 갈라내는 네 단계의 테스트와, 어느 단계가 잘못된 결과를 냈는지 보여 주는 트레이싱을 차례로 살펴봅니다.
이 장에서 배우는 것What you'll learn
약 32분네 단계의 테스트 — 유닛 · 기능 · 통합 · 엔드투엔드가 각자 잡는 실패
조용한 실패가 가장 많이 숨는 통합(이음매) 레벨
트레이스로 실패가 난 단계를 짚어내는 법
질문에 따라 검색 경로를 나누는 라우팅
각각은 통과했는데 이음매에서 깨지는 실제 사례
트레이스를 읽고 어떤 테스트 레벨의 실패인지 진단하기
테스트는 그것이 어떤 실패를 잡아내는지 알 때에만 쓸모가 있습니다. 일을 나눠 맡는 네 단계가 있는데, 조용히 새어 나가는 프로덕션(실서비스) 실패는 그중 한 단계에 몰려 있는 경우가 많습니다.
조용한 실패가 가장 많이 숨는 자리는 통합 레벨입니다. 두 요소가 각자 테스트는 통과하면서도, 둘 사이에서 값을 주고받는 이음매가 어긋나 있을 수 있기 때문입니다.
테스트는 실패가 있다는 것은 알려 주지만, 어느 단계가 그 실패를 냈는지까지는 알려 주지 않습니다. 트레이스가 더해 주는 것이 바로 그 부분입니다.
트레이스는 한 번의 실행에서 각 단계를 차례로 적어 둡니다 — 프롬프트, 도구 호출, 중간 출력, 그리고 걸린 시간까지요. 어떤 경우가 실패하면, 트레이스를 보고 어느 단계가 잘못된 결과를 냈는지 짚을 수 있습니다. 트레이스가 없으면 실패한 평가는 무언가 잘못됐다는 것만 알려 줄 뿐, 어디서 잘못됐는지는 알려 주지 않습니다. 이 차이가 5분이면 끝날 수정과, 워크플로를 손으로 하루 종일 따라가며 찾는 일 사이의 차이를 만듭니다. 트레이스는 실행을 시간 순으로 늘어놓은 것과 비슷해서, 중간 출력까지 보이면 실패한 단계는 대개 눈에 바로 들어옵니다.
TRACE — 한 번의 실행을 시간 순으로 기록
[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를 냈다"로 바꿔 줍니다. 이렇게 되면 변경을 검토하기도 쉬워집니다. 점수가 떨어졌다는 사실만이 아니라, 어느 단계가 움직였는지를 함께 보여 줄 수 있으니까요.
트레이스는 한 번의 실행을 시간 순으로 적어 둔 기록입니다. 어디쯤에서 ok가 FAIL로 바뀌는지만 따라가면, 손댈 단계가 어디인지 금방 보입니다.
모든 경우에 같은 검색 전략을 쓸 필요는 없습니다. 값싼 분류 단계 하나를 앞에 두면, 사실 하나만 찾으면 되는 질문은 한 번만 가져오는 경로로, 여러 부분으로 나뉜 질문은 여러 번에 걸쳐 검색하는 경로로 나눠 보낼 수 있습니다. 이렇게 하면 반복 검색이 정말 필요한 질문에만 그 비용을 쓰게 됩니다.
모든 질문을 반복 검색으로 처리하면, 한 번만 찾아도 될 질문에까지 비용과 지연이 붙습니다. 반대로 모든 질문을 미리 만들어 둔 색인 하나로만 처리하면, 여러 번 살펴봐야 답이 나오는 질문에는 얕은 답이 돌아옵니다. 라우터(경로를 고르는 분류 단계)는 질문을 읽고 경로를 고르는 작은 모델 호출 하나입니다.
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
이 분류 호출 한 번은, 한 번의 검색이면 됐을 질문에 반복 검색을 돌리는 것보다 훨씬 쌉니다. 단순 조회도 있고 여러 번 검색이 필요한 질문도 섞여 있을 때 라우터가 제값을 합니다. 반대로 모든 질문이 같은 모양이라면 라우터를 생략하고, 맞는 경로 하나를 바로 쓰면 됩니다.
앞에서 본 내용을 한눈에 견주도록 정리한 표입니다. 각 테스트가 무엇을 떼어 확인하는지와 무엇을 놓치는지를 나란히 두면, 지금 필요한 테스트가 어느 레벨인지 고르기 쉬워집니다.
LEVEL · ISOLATES · CANNOT CATCH — 레벨별 대조표
| 레벨 | 무엇을 떼어 확인하나 | 놓치는 것 |
|---|---|---|
| 유닛 | 파서나 도구 래퍼 같은 함수 하나를, 따로 떼어 확인한다. | 요소들이 어떻게 맞물리는지에 관한 것 전부. |
| 기능 | Claude 호출 하나가 입력에 대해 기대한 형태를 돌려주는지. | 그 호출 하나를 둘러싼 시스템에서 나는 실패. |
| 통합 | 두 요소가 값을 주고받는 이음매. 예를 들어 검색 결과가 모델로 넘어가는 지점. | 전체가 함께 돌아갈 때에만 드러나는 흐름 전반의 동작. |
| 엔드투엔드 | 사용자가 실행하는 그대로, 입력부터 출력까지 전체 흐름. | 정확히 어디서 깨졌는지 — 최종 결과만 보기 때문이다. |
| 검색 선택 | 안정된 자료에서 사실 하나를 찾을 때는, 정해진 묶음을 한 번만 가져온다. | 여러 단계가 필요한 질문과 계속 바뀌는 자료 — 이런 경우는 여러 번에 걸쳐 검색해야 한다. |
STRENGTH · COST · ALTERNATIVE — 한 줄 요약
| 잘 잡아 주는 것 | 실패를 특정 단계로 좁혀 주고, 각 테스트를 그것이 볼 수 있는 실패에 맞춰 준다. |
|---|---|
| 드는 비용·복잡도 | 트레이싱과 네 단계의 테스트는 직접 만들고 유지해야 하는 구조물이다. |
| 다른 방법이 나을 때 | 안정된 자료에서 사실 하나만 찾는 경우라면, 반복 검색보다 한 번만 가져오는 방식이 낫다. |
따로 떼어 놓고 보면 멀쩡한 요소들이, 서로 맞물리는 지점에서 조용히 깨지는 경우가 있습니다. 프롬프트와 파서를 각각 따로 테스트해서 둘 다 통과했고, 그래서 전체 흐름도 믿었다고 해 보겠습니다.
평가 실행에서 나온 트레이스를 보면, 파서 유닛 테스트도 모델 호출 기능 테스트도 통과합니다. 둘 다 따로 놓고 보면 기대한 형태를 돌려줍니다. 그런데 엔드투엔드 실행은 실패합니다. 트레이스를 아래로 읽어 내려가 보면, 실패는 검색 결과가 모델 호출로 넘어가는 이음매에서 일어납니다.
EVAL RUN — 유닛·기능은 초록, 엔드투엔드는 이음매에서 빨강
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()는 청크(검색해서 잘라 온 문서 조각) 딕셔너리 목록을 돌려주고, 프롬프트 빌더는 문자열을 받도록 작성됐습니다. 그래서 컨텍스트(모델에 함께 넣어 주는 참고 자료)가 어긋난 형태로 도착하고, 모델은 검색해 온 정책 대신 자기 기억에서 답을 만듭니다. 두 요소 사이의 이음매는 어떤 테스트도 돌려 본 적이 없습니다. 그 자리를 확인하는 테스트가 없었기 때문입니다. 이것이 통합 레벨이 잡으라고 있는 실패입니다. 유닛 테스트는 유닛 자체가 잘 동작하니 못 잡고, 기능 테스트는 잘 만들어진 입력에서는 호출이 되니 못 잡습니다. 실제 검색 결과로 검색→모델 이음매를 직접 돌려 보는 테스트만이, 사용자보다 먼저 이 어긋남을 드러냅니다.
지금 직접 해 보겠습니다. 아래 트레이스는 유닛 테스트는 모두 통과하는데 엔드투엔드 테스트만 실패하는 경우입니다. 어디서 깨졌는지 짚고, 원인이 무엇인지 이름을 붙인 다음, 겨눈 수정과 그 실패를 잡았을 테스트 레벨을 세 선택지 중에서 함께 고르세요.
CHECKPOINT TRACE — 유닛은 통과, 엔드투엔드는 실패
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 · 파서를 고친다
def parse_date(s): return dateutil.parse(s) # already passes its unit test
선택지 B · 프롬프트 문구를 고친다
prompt = "Answer carefully and cite the policy." # rewords, ignores the seam
선택지 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위 트레이스가 보여 주는 실패를 고치려면 어느 선택지가 맞을까요?
정답은 C, 통합 레벨입니다. 트레이스에서 두 요소는 따로 두면 통과하므로, 실패는 둘 사이의 이음매에만 있을 수 있습니다. retrieve()는 청크 딕셔너리를 돌려주는데 build_prompt()가 content 필드를 읽지 않아서 모델이 어긋난 컨텍스트를 받았습니다. C는 content를 꺼내 붙이고, 바로 그 이음매를 겨눈 테스트를 더합니다. A는 이미 통과하는 파서를 건드릴 뿐이고, B는 이음매를 그대로 둔 채 문구만 바꿉니다.
Q2대부분의 조용한 프로덕션 실패가 숨는 테스트 레벨은 어디일까요?
통합 레벨입니다. 두 요소가 값을 주고받는 이음매에 조용한 실패가 가장 많이 숨습니다. 양쪽이 각자 테스트는 통과하면서도 그 사이의 전달이 깨져 있을 수 있기 때문입니다.
Q3평가(eval)는 실패가 있다는 것을 알려 줍니다. 여기에 트레이스가 더해 주는 것은 무엇일까요?
트레이스는 각 단계 — 프롬프트·도구 호출·중간 출력·걸린 시간 — 를 차례로 기록하기 때문에, 어느 단계가 잘못된 결과를 냈는지 짚을 수 있습니다. 실패 여부만 아는 것과 실패 지점을 아는 것의 차이가, 하루짜리 수작업과 5분짜리 수정의 차이를 만듭니다.
모듈 4의 다음 레슨으로 이어집니다. 한국어판이 준비되는 대로 이 자리에서 이어집니다. → 다음 레슨
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 38개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.