코딩 에이전트나 컴퓨터 사용 에이전트를 돌려 본 사람은 턴이 쌓일수록 응답이 굼떠지는 걸 느낀다. 매 턴마다 지금까지의 대화 전체를 다시 올리고, 서버는 그걸 매번 검증하고 토큰화하기 때문이다. 대화가 수십만 토큰으로 불어나면 첫 글자가 나오기까지의 시간이 눈에 띄게 늘어난다. 최근 Claude API가 이 문제를 겨냥한 기능들을 한 번에 정리해 내놓아서, 실제로 어떻게 붙이는지 정리한다.
결론부터 말하자면, API 버전 2026-10-21과 Anthropic SDK 2.0에서 세 가지를 쓴다. message threads로 턴마다 새 메시지만 보내 첫 토큰 지연을 줄이고, 메시지 배열 끝에 붙이는 중간 system 메시지로 캐시 접두부를 깨지 않으면서 지시를 주입하고, 2026년 2월 4일에 정식(GA) 전환된 structured outputs로 JSON 응답이 스키마를 벗어나지 않게 강제한다.
프롬프트 캐시가 긴 세션에서 깨지는 지점
프롬프트 캐시는 요청 앞부분(system, tools, 그리고 앞선 대화)을 서버가 기억해 두었다가, 다음 요청이 같은 접두부로 시작하면 그 부분을 다시 처리하지 않고 넘기는 기능이다. 긴 세션에서 지연을 잡는 핵심 수단이다.
문제는 이 캐시가 접두부가 바이트 단위로 같아야만 적중한다는 점이다. 세션 중간에 새 지시를 주려고 최상위 system 필드를 고치면, 그 뒤 모든 내용의 캐시가 무효화된다. 그래서 "지금부터 모든 제안에 타입 주석을 붙여라" 같은 중간 지시 하나 때문에 대화 전체가 캐시 미스로 떨어지는 일이 생긴다.
message threads와 중간 system 메시지는 이 문제를 서로 다른 각도에서 푼다. 하나는 올리는 양 자체를 줄이고, 다른 하나는 지시를 접두부가 아니라 끝에 붙여 캐시를 지킨다. 둘은 같이 쓸 수 있다.
message threads로 새 메시지만 보내기
message threads는 대화를 서버에 잠깐 올려 두는 베타 기능이다. 첫 요청에서 전체 대화와 system, tools를 한 번 보내 스레드를 만들고, 이후 턴에서는 새 메시지와 직전 응답의 id만 보낸다. API가 저장된 대화에 새 턴을 이어 붙여 모델을 호출한다.
요청 본문에 thread 필드 하나를 더하고, 모든 요청에 anthropic-beta: message-threads-2026-08-12 헤더를 붙인다.
# 1번 턴: 전체 대화를 보내며 스레드 생성
curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "anthropic-beta: message-threads-2026-08-12" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"cache_control": {"type": "ephemeral"},
"system": "You are a support agent for Acme. Keep replies short.",
"messages": [{"role": "user", "content": "Hi! I have a question about an order."}],
"thread": {"type": "create"}
}'
# 2번 턴: 새 메시지만 보낸다. message_id는 1번 턴 응답의 id.
# system과 tools는 빼고 보낸다. 스레드가 그대로 들고 있다.
curl -sS https://api.anthropic.com/v1/messages \
-H "x-api-key: $ANTHROPIC_API_KEY" \
-H "anthropic-version: 2026-10-21" \
-H "anthropic-beta: message-threads-2026-08-12" \
-H "content-type: application/json" \
-d '{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"cache_control": {"type": "ephemeral"},
"messages": [{"role": "user", "content": "How long does standard shipping usually take?"}],
"thread": {"type": "continue", "previous_message_id": "'"$message_id"'"}
}'
효과는 업로드와 재검증을 줄여 첫 토큰까지의 시간을 낮추는 데 있다. Anthropic은 Claude Code 기준으로 50만에서 100만 토큰짜리 요청에서 첫 토큰까지 시간 중앙값이 약 35% 줄었다고 밝혔다. 체감 폭은 대화 길이와 환경에 따라 다르다.
오해하기 쉬운 지점이 있다. 모델은 여전히 매 턴 전체 대화를 읽는다. 그래서 토큰 요금과 레이트 리밋은 그대로다. 줄어드는 것은 올리고 검증하는 데 드는 지연이지 과금이 아니다.
스레드는 사라질 수 있는 임시 저장이다. 마지막 요청으로부터 5분이 지나면 만료되고, 마지막 사용 후 1시간 안에 삭제된다. 이어 가기(continue) 요청이 더 이상 유효하지 않으면 thread_not_found와 함께 404가 돌아온다. 이때는 내가 들고 있던 전체 대화로 새 create 요청을 보내 다시 시작하면 된다. 그래서 대화의 원본은 항상 내 애플리케이션이 쥐고 있어야 한다. 응답은 그 턴이 저장됐는지 알려 주지 않고, 다음 continue 요청에서야 알게 된다.
캐시는 스레드가 자동으로 해 주지 않는다. 턴마다 캐시를 쓰려면 모든 요청에 최상위 cache_control을 같은 값으로 보낸다. API가 그 요청 messages의 마지막 캐시 가능 블록에 적용해, 앞선 대화는 캐시에서 읽고 새 턴은 캐시에 쓴다. 스레드 요청 하나에 넣을 수 있는 cache_control 중단점은 최대 4개다.
SDK를 쓰면 client.beta.messages.thread(...) 헬퍼가 베타 헤더 부착, create/continue 전환, 404 시 전체 대화 재전송을 알아서 처리한다. 직접 cURL로 붙일 게 아니라면 헬퍼 쪽이 손이 덜 간다.
스레드가 지원하지 않는 것도 알아 두면 좋다. Message Batches, token counting, compaction, 대부분의 context editing과는 함께 못 쓴다. Inference hooks를 켠 조직에서도 쓸 수 없다.
리마인더를 대화 끝에 붙이는 중간 system 메시지
중간 system 메시지는 {"role": "system"} 메시지를 대화 중간, 정확히는 메시지 배열의 끝쪽에 끼워 넣는 기능이다. 지시를 최상위 system 필드(캐시 접두부)가 아니라 대화의 끝에 두기 때문에, 앞선 턴들은 바이트 단위로 그대로 남아 캐시가 계속 적중한다.
세션 중간에 정책이나 페르소나를 바꾸거나, 마감 변경 같은 그 턴에만 유효한 최신 정보를 주거나, 쌓이면 안 되는 턴별 리마인더를 줄 때 쓴다.
{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"system": "You are a code review assistant. Be concise.",
"messages": [
{"role": "user", "content": "Review process() in utils.py for performance issues."},
{"role": "assistant", "content": "The list comprehension is fine for small inputs..."},
{"role": "user", "content": "Now review the calling code."},
{"role": "system", "content": "From now on, every suggestion must include explicit type annotations."}
]
}
배치 규칙이 까다로워서 여기서 자주 막힌다. role: "system" 메시지는 user 턴 뒤에 와야 하고, 메시지 배열의 끝이거나 assistant 턴 바로 앞이어야 한다. 바꿔 말하면 system 메시지 바로 뒤에 user 메시지가 오면 안 된다. 리마인더 system 메시지를 큐에 넣고 그 다음에 user 메시지를 이어 붙이는 구성이 흔한 실수인데, 이러면 규칙에 걸려 400이 난다. 리마인더는 다음 user 턴 앞까지만 존재해야 하므로, user 메시지를 보낼 때 함께 넣는 쪽으로 배치를 잡아야 한다. 또한 tool_use 블록과 그에 대응하는 tool_result 사이에는 system 메시지를 끼울 수 없다.
우선순위는 직관적이다. 뒤쪽 system 메시지가 앞쪽을 이기고, 중간 system 메시지는 그 뒤 턴에 대해 최상위 system 필드보다 우선한다.
턴별 리마인더를 매번 다시 보내다 보면 그게 또 캐시를 깬다. 이걸 막는 베타 옵션이 clear_at: "next_user_message"다. 베타 헤더 anthropic-beta: mid-conversation-system-clear-at-2026-08-21을 붙이면, 이 메시지는 뒤에 user 메시지가 생기기 전까지만 렌더되고 그 뒤로는 사라진다. 지워진 다음에는 입력 토큰을 먹지 않고, 리마인더를 똑같이 다시 보내도 캐시 미스가 나지 않는다.
{
"role": "system",
"clear_at": "next_user_message",
"content": "Request independent reads in one turn."
}
한 가지 제약은 모델이다. 중간 system 메시지는 Claude Opus 5.5, Sonnet 5.5, Haiku 5.5, Opus 5, Fable 5, Mythos 5와 그 이후 모델에서 쓸 수 있다. Claude Sonnet 5에서는 지원하지 않으므로 그 경우에는 최상위 system 필드를 쓴다. API 버전은 2026-10-21 이상이어야 한다.
structured outputs로 JSON이 스키마를 벗어나지 않게
에이전트가 JSON을 뱉게 해 놓고 파싱하다 보면, 필드가 빠지거나 타입이 어긋나거나 설명 문장이 앞에 붙어 깨지는 경우를 겪는다. structured outputs는 응답이 지정한 JSON 스키마를 반드시 따르도록 강제해 이 문제를 없앤다. Anthropic은 이 기능을 2026년 2월 4일에 Claude Developer Platform에서 정식(GA)으로 전환했다. 별도 베타 헤더 없이 anthropic-version: 2026-10-21로 쓴다.
두 가지 적용점이 있다. 응답 본문을 JSON 스키마로 강제하려면 output_config.format에 json_schema를 준다.
{
"model": "claude-opus-5-5",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Extract the order fields."}],
"output_config": {
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"order_id": {"type": "string"},
"status": {"type": "string", "enum": ["pending", "shipped", "delivered"]}
},
"required": ["order_id", "status"],
"additionalProperties": false
}
}
}
}
도구 호출의 입력을 스키마에 맞추려면 해당 tool에 strict: true를 준다.
{
"tools": [
{
"name": "search_flights",
"strict": true,
"input_schema": {
"type": "object",
"properties": {
"from": {"type": "string"},
"to": {"type": "string"}
},
"required": ["from", "to"],
"additionalProperties": false
}
}
]
}
스키마에 제약이 있다. 재귀 스키마, minimum/maximum/minLength 같은 수치 제약, false가 아닌 additionalProperties, 외부 $ref URL은 지원하지 않는다. 첫 요청에서는 스키마를 문법으로 컴파일하느라 약간의 추가 지연이 붙지만, 컴파일된 문법은 24시간 캐시되어 이후 요청은 빨라진다.
2026-10-21로 올리기 전에 걸리는 것
위 기능들은 전부 API 버전 2026-10-21을 요구한다. 그런데 이 버전에는 코드를 바로 깨뜨리는 변경이 하나 있다. temperature, top_p, top_k 요청 파라미터가 제거됐다. 기본값이라도 이 중 하나를 보내면 모든 모델에서 400이 돌아온다. 샘플링 파라미터를 습관적으로 넣어 둔 코드가 있다면 먼저 걷어내야 한다.
SDK도 새 메이저로 올라갔다. Python SDK는 2.0.0, TypeScript SDK는 1.0.0이고, 라이선스가 MIT에서 Apache 2.0으로 바뀌었다. Python에서는 MessageParam이 유니온이 되어 생성자로 호출하지 말고 딕셔너리로 넘겨야 한다. SDK 자체가 위 파라미터들을 제거했기 때문에, 잘못된 인자는 요청을 보내기 전에 SDK 단에서 먼저 걸러진다.
순서를 잡자면, 먼저 샘플링 파라미터를 제거하고 SDK를 올려 기존 호출이 통과하는지 확인한 다음, structured outputs부터 붙이는 게 손이 덜 간다. 이건 GA라 헤더만 맞으면 된다. message threads와 중간 system 메시지는 베타 헤더가 따로 필요하고 재전송 같은 예외 처리를 설계해야 하므로, 긴 세션에서 지연이 실제로 문제가 될 때 SDK 헬퍼를 끼워 넣는 쪽이 현실적이다.