본문 바로가기
클로드 아카데미클로드 아카데미
이 글은 Anthropic의 공개 자료를 한국어로 옮긴 비공식 커뮤니티 번역입니다. 원문: Extended thinking

확장 사고(Extended thinking)

복잡한 작업에서 Claude가 더 깊이 추론하도록 하고, 사고 내용이 어떻게 반환되는지를 제어합니다.


이 기능은 Zero Data Retention(ZDR) 대상입니다. 조직에 ZDR 계약이 적용되어 있는 경우, 이 기능을 통해 전송된 데이터는 API 응답이 반환된 이후 저장되지 않습니다.

확장 사고를 쓰면 Claude가 복잡한 작업에서 더 깊이 추론하며, 최종 답변을 내놓기 전에 거치는 단계별 사고 과정을 투명성 수준을 달리해 들여다볼 수 있습니다.

지원 모델

확장 사고는 현재 제공되는 모든 Claude 모델에서 사용할 수 있습니다. 다만 활성화 방식은 모델에 따라 다릅니다.

모델 수동 확장 사고(budget_tokens) 권장 방식
Claude Fable 5
Claude Mythos 5 미지원(400 오류) 적응형 사고, 항상 켜짐. 깊이는 effort로 조절
Claude Mythos Preview 지원 적응형 사고, 기본값으로 켜짐
Claude Opus 4.8 미지원(400 오류) effort를 사용한 적응형 사고
Claude Opus 4.7 미지원(400 오류) effort를 사용한 적응형 사고
Claude Opus 4.6 지원 중단(deprecated) effort를 사용한 적응형 사고
Claude Sonnet 4.6 지원 중단(deprecated) effort를 사용한 적응형 사고
Claude Opus 4.5 지원 해당 없음
Claude Haiku 4.5 지원 해당 없음
이전 Claude 4 모델 지원 해당 없음

적응형 사고에서는 매 요청마다 모델이 언제, 얼마나 사고할지를 스스로 결정합니다. Claude Mythos Preview, Claude Fable 5, Claude Mythos 5에서는 thinking: {type: "disabled"}가 지원되지 않습니다. 모델별 동작 차이(사고 출력, 인터리브드 사고, 블록 보존)에 대해서는 모델 버전별 사고 차이 문서를 참고하세요.

확장 사고의 동작 방식

확장 사고가 켜지면 Claude는 내부 추론을 출력하는 thinking 콘텐츠 블록을 만듭니다. Claude는 이 추론에서 얻은 통찰을 반영한 뒤 최종 응답을 작성합니다.

API 응답에는 thinking 콘텐츠 블록이 먼저 포함되고, 그 뒤에 text 콘텐츠 블록이 이어집니다.

다음은 기본 응답 형식의 예시입니다.

{
  "content": [
    {
      "type": "thinking",
      "thinking": "Let me analyze this step by step...",
      "signature": "WaUjzkypQ2mUEVM36O2TxuC06KN8xyfbJwyem2dw3URve/op91XWHOEBLLqIOMfFG/UvLEczmEsUjavL...."
    },
    {
      "type": "text",
      "text": "Based on my analysis..."
    }
  ]
}

확장 사고의 응답 형식에 대한 자세한 내용은 Messages API 레퍼런스를 참고하세요.

확장 사고 사용 방법

다음은 Messages API에서 확장 사고를 사용하는 예시입니다.

curl https://api.anthropic.com/v1/messages \
     --header "x-api-key: $ANTHROPIC_API_KEY" \
     --header "anthropic-version: 2023-06-01" \
     --header "content-type: application/json" \
     --data \
'{
    "model": "claude-sonnet-4-6",
    "max_tokens": 16000,
    "thinking": {
        "type": "enabled",
        "budget_tokens": 10000
    },
    "messages": [
        {
            "role": "user",
            "content": "Are there an infinite number of prime numbers such that n mod 4 == 3?"
        }
    ]
}'
import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={"type": "enabled", "budget_tokens": 10000},
    messages=[
        {
            "role": "user",
            "content": "Are there an infinite number of prime numbers such that n mod 4 == 3?",
        }
    ],
)

# 응답에는 요약된 사고 블록과 텍스트 블록이 포함됩니다
for block in response.content:
    if block.type == "thinking":
        print(f"\nThinking summary: {block.thinking}")
    elif block.type == "text":
        print(f"\nResponse: {block.text}")
import Anthropic from "@anthropic-ai/sdk";

const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-sonnet-4-6",
  max_tokens: 16000,
  thinking: {
    type: "enabled",
    budget_tokens: 10000
  },
  messages: [
    {
      role: "user",
      content: "Are there an infinite number of prime numbers such that n mod 4 == 3?"
    }
  ]
});

// 응답에는 요약된 사고 블록과 텍스트 블록이 포함됩니다
for (const block of response.content) {
  if (block.type === "thinking") {
    console.log(`\nThinking summary: ${block.thinking}`);
  } else if (block.type === "text") {
    console.log(`\nResponse: ${block.text}`);
  }
}

확장 사고를 켜려면 thinking 객체를 추가하고 typeenabled로 설정한 뒤 budget_tokens 값을 지정합니다. 수동 확장 사고가 지원 중단되었거나 지원되지 않는 모델(지원 모델 표 참고)에서는, 적응형 사고에서 설명하는 것처럼 type: "adaptive"를 대신 사용하세요.

budget_tokens 파라미터는 Claude가 내부 추론 과정에 사용할 수 있는 최대 토큰 수를 설정합니다. 이 한도는 요약된 출력이 아니라 전체 사고 토큰에 적용됩니다. 예산을 늘리면 복잡한 문제를 더 철저히 분석할 수 있어 응답 품질이 높아질 수 있습니다. 다만 Claude가 할당된 예산을 모두 쓰지 않을 수도 있으며, 특히 32k를 넘는 범위에서 그런 경향이 있습니다.

budget_tokens는 Claude Opus 4.6 및 Claude Sonnet 4.6에서 지원 중단되었으며 향후 모델 릴리스에서 제거될 예정입니다. 사고 깊이를 제어하려면 effort 파라미터를 함께 사용하는 적응형 사고를 이용하세요.

Claude Mythos Preview, Claude Opus 4.8, Claude Opus 4.7, Claude Opus 4.6, Claude Sonnet 4.6은 최대 128k 출력 토큰을 지원합니다. Claude Haiku 4.5는 64k까지 지원합니다. 레거시 모델의 한도는 모델 개요 문서를 참고하세요. Message Batches API에서는 output-300k-2026-03-24 베타 헤더를 사용하면 Claude Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6의 출력 한도가 300k로 상향됩니다.

budget_tokens는 반드시 max_tokens보다 작은 값으로 설정해야 합니다. 다만 도구와 함께 인터리브드 사고를 사용할 때는 토큰 한도가 컨텍스트 윈도 전체로 확장되므로 이 한도를 초과할 수 있습니다. budget_tokensmax_tokens보다 작아야 하기 때문에, 확장 사고는 max_tokens: 0(캐시 예열, cache pre-warming)과 함께 사용할 수 없습니다.

사고 표시 제어

사고 설정의 display 필드는 사고 내용이 API 응답에서 어떻게 반환되는지를 제어합니다. 두 가지 값을 받습니다.

  • "summarized": 사고 블록에 요약된 사고 텍스트가 담깁니다. 자세한 내용은 요약된 사고 절을 참고하세요. Claude Opus 4.6, Claude Sonnet 4.6 및 이전 Claude 4 모델에서의 기본값입니다.
  • "omitted": 사고 블록이 빈 thinking 필드와 함께 반환됩니다. signature 필드에는 다중 턴 연속성을 위해 암호화된 전체 사고가 그대로 담깁니다(사고 암호화 참고). Claude Fable 5, Claude Mythos 5, Claude Opus 4.8, Claude Opus 4.7, Claude Mythos Preview에서의 기본값입니다.

display: "omitted" 설정은 애플리케이션이 사고 내용을 사용자에게 노출하지 않을 때 유용합니다. 주요 이점은 스트리밍 시 첫 텍스트 토큰까지 걸리는 시간이 단축된다는 점입니다. 서버가 사고 토큰 스트리밍을 아예 건너뛰고 signature만 전달하므로, 최종 텍스트 응답이 더 빨리 스트리밍되기 시작합니다.

생략된 사고와 관련해 유의할 중요한 사항은 다음과 같습니다.

  • 전체 사고 토큰에 대한 요금은 여전히 부과됩니다. 생략은 비용이 아니라 지연 시간을 줄여줍니다.
  • 다중 턴 대화에서 사고 블록을 다시 전달할 때는 변경하지 말고 그대로 전달하세요. 서버가 signature를 복호화해 프롬프트 구성을 위한 원래 사고를 재구성합니다(사고 블록 보존 참고). 왕복 전달하는 생략된 블록의 thinking 필드에 텍스트를 넣더라도 무시됩니다.
  • displaythinking.type: "disabled"와 함께 사용할 수 없습니다(표시할 내용이 없기 때문입니다).
  • thinking.type: "adaptive"를 사용할 때 모델이 단순한 요청에 대해 사고를 건너뛰면, display 값과 무관하게 사고 블록이 생성되지 않습니다.

signature 필드는 display"summarized"이든 "omitted"이든 동일합니다. 대화 도중 턴 간에 display 값을 전환하는 것도 지원됩니다.

Claude Mythos Preview에서는 display의 기본값이 "omitted"입니다. 이 절의 예시들은 모든 모델에 적용되도록 display를 명시적으로 전달하지만, Mythos Preview에서는 설정하지 않아도 동일하게 동작합니다. Mythos Preview에서 요약된 사고를 받으려면 display: "summarized"를 명시적으로 설정하세요.

사고 내용을 최종 사용자에게 전혀 노출하지 않는 자동화 파이프라인이라면, 사고 토큰을 네트워크로 전송받는 부담을 건너뛸 수 있습니다. 지연에 민감한 애플리케이션은 사고 텍스트가 모두 스트리밍될 때까지 기다리지 않고도 동일한 추론 품질을 유지하면서 최종 응답을 더 빨리 시작할 수 있습니다.

curl https://api.anthropic.com/v1/messages \
     --header "x-api-key: $ANTHROPIC_API_KEY" \
     --header "anthropic-version: 2023-06-01" \
     --header "content-type: application/json" \
     --data \
'{
    "model": "claude-sonnet-4-6",
    "max_tokens": 16000,
    "thinking": {
        "type": "enabled",
        "budget_tokens": 10000,
        "display": "omitted"
    },
    "messages": [
        {
            "role": "user",
            "content": "What is 27 * 453?"
        }
    ]
}'
import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={
        "type": "enabled",
        "budget_tokens": 10000,
        "display": "omitted",
    },
    messages=[
        {"role": "user", "content": "What is 27 * 453?"},
    ],
)

for block in response.content:
    if block.type == "thinking":
        if block.thinking:
            print(f"Thinking: {block.thinking}")
        else:
            print("Thinking: [omitted]")
    elif block.type == "text":
        print(f"Response: {block.text}")

display: "omitted"가 설정되면, 응답에는 thinking 필드가 빈 사고 블록이 담깁니다.

{
  "content": [
    {
      "type": "thinking",
      "thinking": "",
      "signature": "EosnCkYICxIMMb3LzNrMu..."
    },
    {
      "type": "text",
      "text": "The answer is 12,231."
    }
  ]
}

display: "omitted"로 스트리밍할 때는 thinking_delta 이벤트가 발생하지 않습니다. 이벤트 순서는 사고 스트리밍 절을 참고하세요.

요약된 사고

확장 사고가 켜진 상태에서, Claude 4 모델의 Messages API는 Claude의 전체 사고 과정을 요약해 반환합니다. 요약된 사고는 확장 사고의 지능적 이점을 온전히 제공하면서도 오용을 방지합니다. 이는 사고 설정의 display 필드가 지정되지 않았거나 "summarized"로 설정된 경우 Claude 4 모델의 기본 동작입니다. Claude Fable 5, Claude Mythos 5, Claude Opus 4.8, Claude Opus 4.7, Claude Mythos Preview에서는 display의 기본값이 "omitted"이므로, 요약된 사고를 받으려면 display: "summarized"를 명시적으로 설정해야 합니다.

요약된 사고와 관련해 유의할 중요한 사항은 다음과 같습니다.

  • 요금은 요약본의 토큰이 아니라 원래 요청에서 생성된 전체 사고 토큰을 기준으로 부과됩니다.
  • 청구되는 출력 토큰 수는 응답에서 보이는 토큰 수와 일치하지 않습니다.
  • Claude 4 모델에서는 사고 출력의 처음 몇 줄이 더 상세합니다. 이 상세한 추론은 특히 프롬프트 엔지니어링 목적에 유용합니다. Claude Mythos Preview는 첫 토큰부터 요약하므로, 사고 블록에 이러한 상세 도입부가 나타나지 않습니다.
  • Anthropic이 확장 사고 기능을 개선해 나감에 따라 요약 동작은 변경될 수 있습니다.
  • 요약은 추가 지연을 최소화하면서 Claude 사고 과정의 핵심 아이디어를 보존해, 매끄럽게 스트리밍되는 사용자 경험을 제공합니다.
  • 요약은 요청 시 지정한 모델과는 다른 별도의 모델이 처리합니다. 사고를 수행하는 모델은 요약된 출력을 보지 않습니다.

Claude 4 모델에서 전체 사고 출력에 접근해야 하는 드문 경우에는 Anthropic 영업팀에 문의하세요.

사고 스트리밍

서버 전송 이벤트(SSE)를 사용해 확장 사고 응답을 스트리밍할 수 있습니다.

확장 사고에서 스트리밍을 켜면, 사고 내용을 thinking_delta 이벤트로 받게 됩니다.

display: "omitted"가 설정된 경우에는 thinking_delta 이벤트가 발생하지 않습니다. 사고 표시 제어 절을 참고하세요.

Messages API를 통한 스트리밍에 대한 더 자세한 문서는 메시지 스트리밍 문서를 참고하세요.

사고와 함께 스트리밍을 처리하는 방법은 다음과 같습니다.

import anthropic

client = anthropic.Anthropic()

with client.messages.stream(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={"type": "enabled", "budget_tokens": 10000},
    messages=[
        {
            "role": "user",
            "content": "What is the greatest common divisor of 1071 and 462?",
        }
    ],
) as stream:
    thinking_started = False
    response_started = False

    for event in stream:
        if event.type == "content_block_start":
            print(f"\nStarting {event.content_block.type} block...")
            # 새 블록마다 플래그를 초기화합니다
            thinking_started = False
            response_started = False
        elif event.type == "content_block_delta":
            if event.delta.type == "thinking_delta":
                if not thinking_started:
                    print("Thinking: ", end="", flush=True)
                    thinking_started = True
                print(event.delta.thinking, end="", flush=True)
            elif event.delta.type == "text_delta":
                if not response_started:
                    print("Response: ", end="", flush=True)
                    response_started = True
                print(event.delta.text, end="", flush=True)
        elif event.type == "content_block_stop":
            print("\nBlock complete.")

스트리밍 출력 예시는 다음과 같습니다.

event: message_start
data: {"type": "message_start", "message": {"id": "msg_01...", "type": "message", "role": "assistant", "content": [], "model": "claude-sonnet-4-6", "stop_reason": null, "stop_sequence": null}}

event: content_block_start
data: {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": "", "signature": ""}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "I need to find the GCD of 1071 and 462 using the Euclidean algorithm.\n\n1071 = 2 × 462 + 147"}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": "\n462 = 3 × 147 + 21\n147 = 7 × 21 + 0\n\nSo GCD(1071, 462) = 21"}}

// 추가 사고 델타...

event: content_block_delta
data: {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "EqQBCgIYAhIM1gbcDa9GJwZA2b3hGgxBdjrkzLoky3dl1pkiMOYds..."}}

event: content_block_stop
data: {"type": "content_block_stop", "index": 0}

event: content_block_start
data: {"type": "content_block_start", "index": 1, "content_block": {"type": "text", "text": ""}}

event: content_block_delta
data: {"type": "content_block_delta", "index": 1, "delta": {"type": "text_delta", "text": "The greatest common divisor of 1071 and 462 is **21**."}}

// 추가 텍스트 델타...

event: content_block_stop
data: {"type": "content_block_stop", "index": 1}

event: message_delta
data: {"type": "message_delta", "delta": {"stop_reason": "end_turn", "stop_sequence": null}}

event: message_stop
data: {"type": "message_stop"}

display: "omitted"가 설정되면, 사고 블록이 열리고 signature_delta가 한 번 도착한 뒤 어떤 thinking_delta 이벤트도 없이 블록이 닫힙니다. 그 직후 곧바로 텍스트 스트리밍이 시작됩니다.

event: content_block_start
data: {"type":"content_block_start","index":0,"content_block":{"type":"thinking","thinking":"","signature":""}}

event: content_block_delta
data: {"type":"content_block_delta","index":0,"delta":{"type":"signature_delta","signature":"EosnCkYICxIMMb3LzNrMu..."}}

event: content_block_stop
data: {"type":"content_block_stop","index":0}

event: content_block_start
data: {"type":"content_block_start","index":1,"content_block":{"type":"text","text":""}}

사고를 켠 상태로 스트리밍을 사용하면, 텍스트가 때로는 큰 덩어리로, 때로는 토큰 단위로 번갈아 도착하는 것을 볼 수 있습니다. 이는 특히 사고 콘텐츠에서 정상적인 동작입니다.

스트리밍 시스템은 최적의 성능을 위해 콘텐츠를 배치로 처리해야 하며, 그 결과 이렇게 "덩어리진" 전달 패턴이 나타나고 스트리밍 이벤트 사이에 지연이 발생할 수 있습니다.

도구 사용과 함께하는 확장 사고

확장 사고는 도구 사용과 함께 사용할 수 있어, Claude가 도구 선택과 결과 처리를 추론으로 거치도록 할 수 있습니다.

확장 사고를 도구 사용과 함께 쓸 때는 다음 제약 사항에 유의하세요.

  1. 도구 선택 제약: 사고와 함께하는 도구 사용은 tool_choice: {"type": "auto"}(기본값) 또는 tool_choice: {"type": "none"}만 지원합니다. tool_choice: {"type": "any"}tool_choice: {"type": "tool", "name": "..."}를 사용하면 오류가 발생합니다. 이 옵션들은 도구 사용을 강제하는데, 이는 확장 사고와 호환되지 않기 때문입니다.
  2. 사고 블록 보존: 도구 사용 중에는 마지막 assistant 메시지의 thinking 블록을 API로 다시 전달해야 합니다. 추론 연속성을 유지하려면 해당 블록 전체를 수정하지 않은 채 그대로 API에 다시 포함하세요.

대화 중 사고 모드 전환

assistant 턴의 중간에는, 도구 사용 루프 도중을 포함해 사고를 전환할 수 없습니다. 하나의 assistant 턴 전체는 단일 사고 모드로 동작해야 합니다.

  • 사고가 켜져 있다면, 마지막 assistant 턴은 사고 블록으로 시작해야 합니다.
  • 사고가 꺼져 있다면, 마지막 assistant 턴에는 어떤 사고 블록도 포함되어서는 안 됩니다.

모델의 관점에서 도구 사용 루프는 assistant 턴의 일부입니다. assistant 턴은 Claude가 전체 응답을 마칠 때까지 완료되지 않으며, 그 응답에는 여러 번의 도구 호출과 결과가 포함될 수 있습니다.

예를 들어, 다음 시퀀스 전체가 하나의 assistant 턴입니다.

User: "What's the weather in Paris?"
Assistant: [thinking] + [tool_use: get_weather]
User: [tool_result: "20°C, sunny"]
Assistant: [text: "The weather in Paris is 20°C and sunny"]

여러 개의 API 메시지가 있지만, 도구 사용 루프는 개념적으로 하나의 연속된 assistant 응답의 일부입니다.

사고의 우아한 저하(graceful degradation)

턴 중간에 사고 충돌이 발생하면(예: 도구 사용 루프 도중 사고를 켜거나 끄는 경우), API는 해당 요청에 대해 사고를 자동으로 비활성화합니다. 모델 품질을 유지하고 분포를 벗어나지 않도록(on-distribution), API는 다음과 같이 동작할 수 있습니다.

  • 사고 블록이 유효하지 않은 턴 구조를 만들게 되는 경우, 대화에서 해당 사고 블록을 제거합니다.
  • 대화 기록이 사고 활성화와 호환되지 않는 경우, 현재 요청에 대해 사고를 비활성화합니다.

즉, 턴 중간에 사고를 전환하려 해도 오류는 발생하지 않지만, 해당 요청에 대해서는 사고가 조용히 비활성화됩니다. 사고가 실제로 활성화되었는지 확인하려면 응답에 thinking 블록이 있는지 확인하세요.

실용적 지침

베스트 프랙티스: 턴 중간에 전환하려 하지 말고, 각 턴의 시작 시점에 사고 전략을 계획하세요.

예시: 턴 완료 후 사고 전환하기

User: "What's the weather?"
Assistant: [tool_use] (사고 비활성화)
User: [tool_result]
Assistant: [text: "It's sunny"]
User: "What about tomorrow?"
Assistant: [thinking] + [text: "..."] (사고 활성화 - 새 턴)

사고를 전환하기 전에 assistant 턴을 완료함으로써, 새 요청에서 사고가 실제로 활성화되도록 보장할 수 있습니다.

또한 사고 모드를 전환하면 메시지 기록에 대한 프롬프트 캐싱이 무효화됩니다. 자세한 내용은 프롬프트 캐싱과 함께하는 확장 사고 절을 참고하세요.

사고 블록을 보존하면서 도구 결과를 제공하는 실용적인 예시는 다음과 같습니다.

import anthropic

client = anthropic.Anthropic()

weather_tool = {
    "name": "get_weather",
    "description": "Get current weather for a location",
    "input_schema": {
        "type": "object",
        "properties": {"location": {"type": "string", "description": "City name"}},
        "required": ["location"],
    },
}

# 첫 번째 요청 - Claude가 사고와 도구 요청으로 응답합니다
response = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=16000,
    thinking={"type": "enabled", "budget_tokens": 10000},
    tools=[weather_tool],
    messages=[{"role": "user", "content": "What's the weather in Paris?"}],
)

프롬프트 캐싱과 함께하는 확장 사고

사고와 함께 프롬프트 캐싱을 사용할 때는 몇 가지 중요한 고려 사항이 있습니다.

확장 사고 작업은 완료까지 5분을 넘기는 경우가 많습니다. 더 긴 사고 세션과 다단계 워크플로 전반에서 캐시 적중을 유지하려면 1시간 캐시 지속 시간을 사용하는 것을 고려하세요.

사고 블록의 컨텍스트 제거

  • 이전 Opus/Sonnet 모델과 모든 Haiku 모델에서는 이전 턴의 사고 블록이 컨텍스트에서 제거되며, 그 결과 캐시 분기점(cache breakpoint)이 영향을 받을 수 있습니다. Opus 4.5+ 및 Sonnet 4.6+에서는 기본적으로 유지됩니다.
  • 도구 사용이 포함된 대화를 이어갈 때, 사고 블록은 캐시되며 캐시에서 읽을 때 입력 토큰으로 집계됩니다.
  • 이로 인해 트레이드오프가 생깁니다. 사고 블록은 컨텍스트 윈도 공간을 시각적으로는 차지하지 않지만, 캐시될 때 여전히 입력 토큰 사용량에 포함됩니다.
  • 사고가 비활성화되었는데 현재 도구 사용 턴에서 사고 콘텐츠를 전달하면, 그 사고 콘텐츠는 제거되고 해당 요청에서 사고는 계속 비활성화 상태로 남습니다.

캐시 무효화 패턴

  • 사고 파라미터의 변경(켜기/끄기 또는 예산 할당)은 메시지 캐시 분기점을 무효화합니다.
  • 인터리브드 사고는 사고 블록이 여러 도구 호출 사이에 발생할 수 있어 캐시 무효화를 증폭시킵니다.
  • 시스템 프롬프트와 도구는 사고 파라미터 변경이나 블록 제거와 무관하게 캐시된 상태로 유지됩니다.

이전 Opus/Sonnet 모델과 모든 Haiku 모델에서는 캐싱 및 컨텍스트 계산을 위해 사고 블록이 제거되며, Opus 4.5+ 및 Sonnet 4.6+에서는 기본적으로 유지됩니다. 어느 경우든, 도구 사용이 포함된 대화를 이어갈 때, 특히 인터리브드 사고를 사용할 때는 사고 블록을 반드시 보존해야 합니다.

사고 블록의 캐싱 동작 이해

확장 사고를 도구 사용과 함께 쓸 때, 사고 블록은 토큰 집계에 영향을 주는 특정한 캐싱 동작을 보입니다.

동작 방식:

  1. 캐싱은 도구 결과를 포함한 후속 요청을 보낼 때만 발생합니다.
  2. 후속 요청이 보내지면, 이전 대화 기록(사고 블록 포함)이 캐시될 수 있습니다.
  3. 이렇게 캐시된 사고 블록은 캐시에서 읽힐 때 사용량 지표에서 입력 토큰으로 집계됩니다.
  4. tool_result가 아닌 user 블록이 포함된 경우: Opus 4.5+ 및 Sonnet 4.6+에서는 이전 사고 블록이 유지되고, 이전 Opus/Sonnet 모델과 모든 Haiku 모델에서는 이전 사고 블록이 모두 무시되어 컨텍스트에서 제거됩니다.

상세 예시 흐름:

요청 1:

User: "What's the weather in Paris?"

응답 1:

[thinking_block_1] + [tool_use block 1]

요청 2:

User: ["What's the weather in Paris?"],
Assistant: [thinking_block_1] + [tool_use block 1],
User: [tool_result_1, cache=True]

응답 2:

[thinking_block_2] + [text block 2]

요청 2는 응답이 아니라 요청 콘텐츠의 캐시를 기록합니다. 캐시에는 원래의 user 메시지, 첫 번째 사고 블록, tool_use 블록, 그리고 tool_result가 포함됩니다.

요청 3:

User: ["What's the weather in Paris?"],
Assistant: [thinking_block_1] + [tool_use block 1],
User: [tool_result_1, cache=True],
Assistant: [thinking_block_2] + [text block 2],
User: [Text response, cache=True]

Opus 4.5+ 및 Sonnet 4.6+에서는 이전 사고 블록이 모두 기본적으로 유지됩니다. 이전 Opus/Sonnet 모델과 모든 Haiku 모델에서는 tool_result가 아닌 user 블록이 포함되었기 때문에, 이전 사고 블록이 모두 무시되어 컨텍스트에서 제거됩니다. 이 요청은 다음과 동일하게 처리됩니다.

User: ["What's the weather in Paris?"],
Assistant: [tool_use block 1],
User: [tool_result_1, cache=True],
Assistant: [text block 2],
User: [Text response, cache=True]

핵심 포인트:

  • 이 캐싱 동작은 명시적인 cache_control 마커가 없어도 자동으로 발생합니다.
  • 이 동작은 일반 사고를 쓰든 인터리브드 사고를 쓰든 일관되게 적용됩니다.
from anthropic import Anthropic
import requests
from bs4 import BeautifulSoup

client = Anthropic()

def fetch_article_content(url):
    response = requests.get(url)
    soup = BeautifulSoup(response.content, "html.parser")

    # script와 style 요소를 제거합니다
    for script in soup(["script", "style"]):
        script.decompose()

    # 텍스트를 가져옵니다
    text = soup.get_text()

    # 줄 단위로 나누고 각 줄의 앞뒤 공백을 제거합니다
    lines = (line.strip() for line in text.splitlines())
    # 여러 줄로 된 헤드라인을 한 줄씩 나눕니다
    chunks = (phrase.strip() for line in lines for phrase in line.split("  "))
    # 빈 줄을 제거합니다
    text = "\n".join(chunk for chunk in chunks if chunk)

    return text

# 글의 콘텐츠를 가져옵니다
book_url = "https://www.gutenberg.org/cache/epub/1342/pg1342.txt"
book_content = fetch_article_content(book_url)
# 캐싱에 충분한 만큼의 텍스트만 사용합니다(앞쪽 몇 개 장)
LARGE_TEXT = book_content[:10000]

SYSTEM_PROMPT = [
    {
        "type": "text",
        "text": "You are an AI assistant that is tasked with literary analysis. Analyze the following text carefully.",
    },
    {"type": "text", "text": LARGE_TEXT, "cache_control": {"type": "ephemeral"}},
]

MESSAGES = [{"role": "user", "content": "Analyze the tone of this passage."}]

# 첫 번째 요청 - 캐시를 설정합니다
print("First request - establishing cache")
response1 = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=20000,
    thinking={"type": "enabled", "budget_tokens": 4000},
    system=SYSTEM_PROMPT,
    messages=MESSAGES,
)

print(f"First response usage: {response1.usage}")

MESSAGES.append({"role": "assistant", "content": response1.content})
MESSAGES.append({"role": "user", "content": "Analyze the characters in this passage."})
# 두 번째 요청 - 동일한 사고 파라미터(캐시 적중 예상)
print("\nSecond request - same thinking parameters (cache hit expected)")
response2 = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=20000,
    thinking={"type": "enabled", "budget_tokens": 4000},
    system=SYSTEM_PROMPT,
    messages=MESSAGES,
)

print(f"Second response usage: {response2.usage}")

# 세 번째 요청 - 다른 사고 파라미터(메시지에 대해 캐시 미스)
print("\nThird request - different thinking parameters (cache miss for messages)")
response3 = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=20000,
    thinking={
        "type": "enabled",
        "budget_tokens": 8000,  # 사고 예산 변경
    },
    system=SYSTEM_PROMPT,  # 시스템 프롬프트는 계속 캐시됨
    messages=MESSAGES,  # 메시지 캐시는 무효화됨
)

print(f"Third response usage: {response3.usage}")