CPN 한국어 자습서 · Claude Certified Developer — Foundations Prep
2-9 · 누적 디버그 과제
Cumulative debug task — identify each bug, then write the corrected version
모듈 2에서 다룬 스키마 설계 · 스트리밍 · 컨텍스트 구성 · 메모리, 네 가지를 한 번에 점검하는 누적 디버그 과제입니다. 아래 에이전트 구현에는 층마다 하나씩, 모두 네 개의 버그가 심어져 있습니다. 먼저 어느 층에서 무엇이 어긋나는지 짚어 보고, 그다음 고친 버전을 직접 써 봅니다.
이 장에서 배우는 것What you'll learn
약 18분한 구현에 심어진 네 층의 버그 — 스키마 · 스트리밍 · 컨텍스트 · 메모리
실패 증상을 보고 어느 층의 문제인지 짚어 내는 법
도구 설명이 모호할 때 도구 선택이 어긋나는 이유
스트리밍 커밋을 stop_seen으로 잠그고 사고 블록을 지키는 이유
여러 tool_result를 하나의 사용자 메시지로 모으는 구조
지난 세션을 통째로 이어 붙일 때 컨텍스트 창이 넘치는 문제와 해결
end_turn이 나올 때까지 이어진다.message_stop 이벤트가 온다.tool_use, 그 도구의 실행 결과를 다시 모델에 돌려주는 블록이 tool_result. 둘은 짝을 이뤄야 한다.model_context_window_exceeded 오류가 난다.end_turn이 온다.이 과제는 새로운 내용을 배우는 자리가 아니라, 모듈 2에서 익힌 것들이 실제 코드에서 어떻게 어긋나는지 확인하는 자리입니다. 버그는 모두 네 개이고, 서로 다른 네 층에 하나씩 들어 있습니다.
과제는 두 단계로 나뉩니다. 1단계에서는 네 개의 버그가 각각 어느 층에 있는지 짚고, 런타임에 무엇을 일으키는지 한 문장으로 적어 봅니다. 2단계에서는 고친 코드를 직접 써 보고, 그 수정이 무엇을 바꾸는지 이름을 붙여 봅니다.
에이전트 하나에 성격이 다른 실수 네 개를 일부러 심어 두고, 그것을 찾아 고치는 연습입니다. 실무에서 층마다 자주 만나는 실수를 한 번에 복습하는 셈입니다.
아래는 도구를 호출하는 에이전트 한 개의 전체 구현입니다. 겉보기에는 정상적으로 돌아가는 코드처럼 보이지만, 네 층에 각각 하나씩 결함이 들어 있습니다. 먼저 코드를 천천히 읽어 두세요.
BUGGY IMPLEMENTATION — 네 개의 층이 한 파일에
# --- TOOL DEFINITIONS --- tools = [ { "name": "get_customer_data", "description": "Gets data.", "input_schema": {"type":"object","properties":{"id":{"type":"string"}},"required":["id"]} } ] # --- AGENT LOOP --- def run_agent(user_request, session_history): messages = session_history + [{"role":"user","content":user_request}] while True: blocks = {} stop_seen = False with client.messages.stream( model=model, max_tokens=4096, tools=tools, messages=messages, thinking={"type": "adaptive"} ) as stream: for event in stream: if event.type == "content_block_start": blocks[event.index] = init_block(event) elif event.type == "content_block_delta": apply_delta(blocks[event.index], event.delta) elif event.type == "message_stop": stop_seen = True assistant_content = [b for b in assemble(blocks) if b["type"] != "thinking"] messages.append({"role":"assistant","content": assistant_content}) response = finalize(blocks) if response.stop_reason == "end_turn": return response for block in response.content: if block.type == "tool_use": result = execute_tool(block.name, block.input) messages.append({"role":"user","content":[{"type":"tool_result", "tool_use_id":block.id,"content":result}]}) # --- MEMORY --- def build_session_history(prior_sessions): # Concatenating all prior session transcripts in-context full_history = [] for session in prior_sessions: full_history.extend(session["messages"]) return full_history
버그를 찾을 때는 층마다 코드의 어느 부분을 봐야 하는지 먼저 정리해 두면 도움이 됩니다. 아래 표는 각 층이 코드에서 어디에 해당하는지 짚어 줍니다. 무엇이 잘못됐는지는 아직 적혀 있지 않으니, 표를 길잡이 삼아 직접 진단해 보세요.
FOUR LAYERS → CODE — 층과 코드 위치
| 스키마 층 | tools 정의의 description — 도구가 무엇을 하는지 적는 자리입니다. |
|---|---|
| 스트리밍 층 | 스트림 이벤트를 조립한 뒤 assistant_content를 만들어 messages에 커밋하는 부분입니다. |
| 컨텍스트 층 | for block in response.content 루프에서 tool_result를 사용자 메시지로 만들어 붙이는 부분입니다. |
| 메모리 층 | build_session_history — 지난 세션 기록을 다음 요청 앞에 쌓는 부분입니다. |
이제 직접 진단할 차례입니다. 네 개의 버그마다 어느 층에 속하는지 이름을 붙이고, 그 버그가 런타임에 무엇을 일으키는지 한 문장으로 적어 보세요. 아래 칸에 먼저 써 본 뒤, 모범 답안을 열어 맞춰 보면 됩니다.
MODEL ANSWER · STAGE 1 — 층별 진단
| 층 | 심어진 결함 | 런타임에 무엇이 어긋나나 |
|---|---|---|
| 스키마 | 도구 설명이 "Gets data."로 너무 모호하다. | 모델이 이 도구를 언제 · 왜 불러야 할지 구분하지 못합니다. 다른 조회 도구와 헷갈려 의도가 아니라 표면적인 단어 일치로 도구를 고르거나, 아예 건너뜁니다. |
| 스트리밍 | 커밋을 stop_seen으로 잠그지 않고, 조립할 때 사고 블록을 걸러 낸다. | 스트림이 중간에 끊기면 절반만 만들어진 턴이 기록에 들어가 다음 요청을 오염시킵니다. 또한 사고 블록을 지우면 서명이 맞지 않아, 되돌리기 규칙이 깨지고 API가 다음 요청을 거절합니다. |
| 컨텍스트 | tool_result를 루프 안에서 각각 별도의 사용자 메시지로 추가한다. | 한 턴에서 나온 여러 도구 결과가 여러 사용자 메시지로 쪼개집니다. 턴 구조가 깨지면서 API 검증 오류가 납니다. |
| 메모리 | 지난 세션 기록을 다듬지 않고 전부 그대로 이어 붙인다. | 세션이 쌓일수록 컨텍스트 창이 끝없이 커집니다. 네다섯 번째 세션쯤이면 현재 요청을 처리하기도 전에 창이 가득 차 model_context_window_exceeded로 실패합니다. |
네 결함은 성격이 서로 다릅니다. 스키마는 모델의 판단을 흐리고, 스트리밍과 컨텍스트는 대화 기록의 구조를 깨뜨리며, 메모리는 컨텍스트 창의 크기를 통제하지 못합니다. 그래서 고치는 방법도 층마다 다릅니다.
이번에는 앞에서 짚은 네 개의 버그를 고친 코드로 써 볼 차례입니다. 결함마다 고친 부분을 적고, 그 수정이 무엇을 바꾸는지 함께 적어 보세요. 다 쓴 뒤 모범 답안을 열어 맞춰 봅니다.
MODEL ANSWER · STAGE 2 — 층별 수정
모호한 "Gets data."를, 도구를 언제 써야 하는지 의도가 드러나는 설명으로 바꿉니다. 여기에 "이럴 때는 쓰지 말라"는 제외 조건까지 덧붙이면 도구 선택이 더 또렷해집니다.
# 의도와 제외 조건을 함께 밝힌 설명으로 교체
"description": "Use this to retrieve full account and contact
details for a customer by customer ID. Do not use this for
order history or transaction records."바뀌는 것 — 언제 · 왜 이 도구를 골라야 하는지 모델이 판단할 근거가 생깁니다. 표면적인 단어가 아니라 의도를 보고 도구를 고르게 됩니다.
사고 블록을 포함해 모든 블록을 그대로 조립하고, 커밋을 stop_seen으로 잠급니다. 스트림이 끊긴 경우에는 예외를 던져 부분 턴을 버리고 다시 시도하게 합니다.
assistant_content = assemble(blocks) # 사고 블록까지 모두 유지
if stop_seen:
messages.append({"role": "assistant", "content": assistant_content})
else:
raise StreamInterruptedError(
"Discarding partial turn; retry from last complete turn."
)바뀌는 것 — 온전히 다 받은 응답만 기록에 들어갑니다. 끊긴 스트림은 컨텍스트를 오염시키는 대신 예외로 걸러져 재시도됩니다. 사고 블록을 지우지 않으므로 서명도 그대로 유지됩니다.
도구 결과를 루프 안에서 하나씩 붙이지 않고, 먼저 한 리스트에 모은 뒤 루프가 끝나고 나서 사용자 메시지 한 개로 붙입니다.
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "user", "content": tool_results})바뀌는 것 — 한 턴에서 나온 도구 결과가 모두 하나의 사용자 메시지에 담깁니다. 턴 구조가 유지되어 API 검증 오류가 사라집니다.
지난 세션을 전부 이어 붙이는 대신 요약하거나 다듬어 넣습니다. 오래된 세션을 요약해 담는 방법이 하나이고, 외부 저장소에 기록을 두고 세션을 시작할 때 요약만 불러오는 방법이 또 하나입니다.
# 방법 A — 오래된 세션을 요약해 담는다 def build_session_history(prior_sessions): full_history = [] for session in prior_sessions: full_history.append({ "role": "user", "content": summarize(session["messages"]) }) return full_history # 방법 B — 외부 저장소에 두고 요약만 불러온다 def build_session_history(prior_sessions): if not prior_sessions: return [] summary = load_session_summary(prior_sessions[-1]["id"]) return [{"role": "user", "content": f"Session context: {summary}"}]
바뀌는 것 — 기록의 길이가 요약으로 묶여, 컨텍스트 창이 세션마다 무한정 커지지 않습니다. 세션이 쌓여도 model_context_window_exceeded로 멈추지 않게 됩니다.
stop_seen으로 잠그지 않은 것, 그리고 사고 블록을 걸러 낸 것입니다. 앞엣것만 고치고 뒤엣것을 지나치면, 사고 블록이 빠진 응답의 서명이 맞지 않아 다음 요청에서 다시 거절당하는 경우가 많습니다.tool_use 블록까지 온전히 담아 기록하고, tool_result를 그 뒤에 하나의 사용자 메시지로 붙이면, 두 층이 함께 요구하는 tool_use–tool_result 짝 맞춤 규칙이 지켜집니다.정답을 먼저 떠올려 본 뒤 골라 보세요. 맞히면 설명이 나옵니다. 네 문항은 각각 한 층씩을 다룹니다.
Q1도구 설명이 "Gets data."인 것은 어느 층의 버그이고, 런타임에 무엇을 일으킬까요?
스키마 층입니다. "Gets data."는 언제 · 왜 이 도구를 써야 하는지 알려 주지 않아서, 모델이 여러 조회 도구 가운데 무엇을 고를지 판단할 근거가 없습니다. 고치는 방법은 의도가 드러나는 구체적인 설명으로 바꾸는 것입니다.
Q2스트리밍 층에서 assistant_content를 기록에 붙이기 전에 stop_seen을 확인하지 않으면 무슨 일이 생길까요?
stop_seen은 message_stop 이벤트에서 켜지지만, 커밋하기 전에 확인되지 않습니다. 스트림이 끊긴 채로 부분 턴이 기록에 들어가면 다음 요청의 컨텍스트가 오염됩니다. 커밋을 stop_seen으로 잠그고, 끊긴 경우에는 예외를 던져 다시 시도하도록 고칩니다.
Q3여러 tool_result를 for 루프 안에서 각각 별도의 사용자 메시지로 붙이면 컨텍스트 층에서 무엇이 문제가 될까요?
한 어시스턴트 턴에서 나온 tool_result 블록들은 하나의 사용자 메시지에 모아 담아야 합니다. 루프 안에서 하나씩 별도 메시지로 붙이면 턴 구조가 어긋나 API가 요청을 거절합니다. 결과를 리스트에 모아 두었다가 루프가 끝난 뒤 한 번만 붙이도록 고칩니다.
Q4메모리 층에서 build_session_history가 지난 세션 기록을 전부 이어 붙이면 세션이 쌓일수록 무엇이 일어날까요?
다듬거나 요약하지 않고 모든 지난 기록을 그대로 이어 붙이면 컨텍스트 창이 세션마다 커집니다. 결국 현재 요청을 처리하기도 전에 창이 가득 차 model_context_window_exceeded로 실패합니다. 오래된 세션을 요약하거나, 외부 저장소에 두고 요약만 불러오도록 고칩니다.
모듈 2의 다음 레슨은 이미지 · PDF 같은 여러 형식의 입력과 대량 처리(Multimodal and Batch Ingestion)를 다룹니다. → 이미지·PDF 입력과 대량 처리
전 코스는 계속 무료입니다. 등록하면 이 코스의 남은 38개 레슨을 끝까지 읽을 수 있습니다.
이미 등록하셨다면 그때 쓰신 이메일을 넣어 주세요.