이 시험에서 비중이 가장 큰 스킬입니다. 메시지 구조와 종료 사유, 스트리밍과 배치, 캐싱까지 API가 실제로 어떻게 동작하는지를 코드 수준에서 다룹니다.
메시지와 종료 사유
요청의 뼈대는 역할이 번갈아 나오는 메시지 배열입니다. system은 메시지 배열이 아니라 별도 파라미터로 넣습니다.
resp = client.messages.create(
model=MODEL,
max_tokens=1024,
system="당신은 사내 문서 검색을 돕는 도우미입니다.", # 배열이 아니라 파라미터
messages=[
{"role": "user", "content": "지난 분기 매출 보고서를 찾아줘"},
],
)
응답에서 가장 먼저 볼 것은 stop_reason입니다. 무엇 때문에 생성이 멈췄는지 알려 줍니다.
end_turn— 모델이 할 말을 마쳤습니다. 정상 종료입니다.tool_use— 도구를 호출하려 합니다. 실행하고 결과를 돌려줘야 이어집니다.max_tokens— 출력 한도에 걸려 잘렸습니다. 내용이 완결되지 않았을 수 있습니다.stop_sequence— 지정한 중단 문자열을 만났습니다.
max_tokens로 끝난 응답을 완성본으로 취급하는 것이 흔한 버그입니다. JSON을 요청했는데 중간에 잘리면 파싱이 실패하고, 그때 원인을 파서에서 찾느라 시간을 씁니다. 응답을 쓰기 전에 stop_reason을 확인하는 습관이 필요합니다.
도구 호출의 왕복
도구를 쓰면 한 번의 요청이 여러 번의 왕복이 됩니다. 순서가 정해져 있습니다.
- 도구 목록과 함께 요청을 보냅니다.
- 응답의
stop_reason이tool_use이고tool_use블록에 이름과 인자가 들어옵니다. - 모델 응답 전체를 assistant 메시지로 그대로 붙입니다.
- 도구를 실행하고 결과를 user 메시지의
tool_result블록으로 붙입니다.tool_use_id로 어느 호출의 답인지 표시합니다. - 다시 요청을 보냅니다.
messages.append({"role": "assistant", "content": resp.content})
results = []
for block in resp.content:
if block.type == "tool_use":
try:
output = execute(block.name, block.input)
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": output})
except ToolError as e:
results.append({"type": "tool_result", "tool_use_id": block.id,
"content": str(e), "is_error": True})
messages.append({"role": "user", "content": results})
한 응답에 도구 호출이 여럿 들어올 수 있습니다. 그때는 모두 실행해 결과를 한 번에 돌려줍니다. 하나만 처리하고 나머지를 버리면 짝이 맞지 않아 다음 요청이 거부됩니다. 실패한 실행도 is_error를 붙여 결과로 돌려줘야 모델이 다음 수를 고를 수 있습니다.
스트리밍
스트리밍은 완성된 응답을 기다리지 않고 조각으로 받습니다. 총 소요 시간이 줄지는 않지만 첫 글자까지의 시간이 크게 줄어 체감이 달라집니다.
with client.messages.stream(model=MODEL, max_tokens=1024, messages=messages) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
final = stream.get_final_message() # 완성본과 usage는 여기서
구현에서 자주 걸리는 지점이 있습니다. 도구 호출의 인자도 조각으로 옵니다. 부분 JSON이 순차로 도착하므로, 도착하는 대로 파싱하려 하면 깨집니다. 인자는 해당 블록이 끝난 뒤에 완성된 것을 써야 합니다.
또 usage 정보와 stop_reason은 스트림이 끝나야 확정됩니다. 토큰을 세거나 잘림을 판정하려면 최종 메시지를 받아야 합니다.
스트리밍이 맞지 않는 경우도 있습니다. 출력을 그대로 프로그램이 소비하고 사람이 보지 않는다면, 조각을 모으는 복잡도만 늘어납니다. 사람이 읽는 화면에 쓰는 것이 본래 용도입니다.
배치와 캐싱
배치는 급하지 않은 대량 처리를 위한 방식입니다. 요청을 모아 제출하고 나중에 결과를 받습니다. 실시간 호출보다 크게 저렴한 대신 즉시 응답을 기대할 수 없습니다.
앞서 본 것처럼 custom_id로 결과와 입력을 짝지어야 합니다. 순서를 신뢰하면 안 됩니다.
프롬프트 캐싱은 반복되는 앞부분을 재사용해 비용과 지연을 줄입니다. 핵심 성질은 접두어 일치입니다. 요청의 앞에서부터 같은 부분만 캐시가 됩니다.
system=[
{"type": "text", "text": LONG_POLICY_DOC,
"cache_control": {"type": "ephemeral"}}, # 잘 안 바뀌는 것을 앞에
]
messages=[{"role": "user", "content": user_question}] # 매번 바뀌는 것은 뒤에
여기서 가장 흔한 실수는 앞부분에 매번 바뀌는 값을 넣는 것입니다. 현재 시각이나 요청 ID를 시스템 프롬프트 맨 앞에 넣으면 접두어가 매번 달라져 캐시가 한 번도 맞지 않습니다. 도구 정의를 바꿔도 마찬가지로 그 뒤가 전부 무효가 됩니다.
캐시가 실제로 맞았는지는 응답의 사용량 정보에서 캐시 읽기 토큰으로 확인합니다. 설정만 하고 확인하지 않으면 안 맞는 채로 오래 갑니다.
시험 함정
- system을 messages 배열 안에 넣는 선택지 — 별도 파라미터로 전달합니다.
- stop_reason이 max_tokens인 응답을 완결된 결과로 다루는 처리 — 잘린 것이라 파싱이 실패할 수 있습니다.
- 한 응답에 담긴 여러 도구 호출 중 하나만 처리하는 선택지 — 모두 실행해 결과를 함께 돌려줘야 짝이 맞습니다.
- 스트리밍이 총 처리 시간을 줄인다는 서술 — 줄어드는 것은 첫 글자까지의 시간입니다.
- 스트리밍 중 도착하는 부분 JSON을 즉시 파싱하는 구현 — 블록이 끝난 뒤 완성된 인자를 써야 합니다.
- 캐싱 대상 앞에 시각이나 요청 ID를 넣는 배치 — 접두어가 매번 달라져 캐시가 맞지 않습니다.
실습 시나리오
실제 시험과 같은 형식의 시나리오 문제입니다.
긴 정책 문서를 시스템 프롬프트에 넣고 프롬프트 캐싱을 켰는데 비용이 줄지 않습니다. 확인해 보니 시스템 프롬프트 맨 앞에 요청 시각이 들어가 있습니다. 원인으로 가장 적절한 것은 무엇입니까?
빌드 연습 · 도구 왕복과 캐싱을 직접 계측하기
약 45분1.종료 사유 관찰
max_tokens를 아주 작게 주고 응답의 stop_reason을 확인합니다.
기대 결과 · max_tokens 값이 관찰되고 내용이 중간에 끊긴 것이 보입니다.
2.도구 왕복 구현
도구 하나를 붙여 assistant·tool_result 순서를 지켜 한 바퀴 돌립니다.
기대 결과 · tool_use_id가 짝을 이루고 두 번째 요청이 정상 처리됩니다.
3.다중 호출 처리
한 응답에 도구 두 개가 호출되게 만들고 모두 실행해 함께 반환합니다.
기대 결과 · 결과 두 개가 한 user 메시지에 담깁니다.
4.캐싱 적용
긴 문서를 앞에 두고 cache_control을 붙여 두 번 호출합니다.
기대 결과 · 두 번째 호출에서 캐시 읽기 토큰이 관찰됩니다.
5.캐시 깨뜨리기
앞부분에 현재 시각을 넣어 다시 두 번 호출합니다.
기대 결과 · 캐시 읽기 토큰이 사라지는 것이 확인됩니다.