대화 중 시스템 메시지와 도구 변경

대화 중 시스템 메시지와 도구 변경 (Mid-conversation system messages and tool changes)

대화 도중에 시스템 지시나 도구 가용성을 바꿔도 그 앞에 온 캐시된 접두사를 무효화하지 않도록 해 주는 기능이에요. 필요해진 시점에 {"role": "system"} 메시지를 대화에 추가하거나, tool_addition·tool_removal 블록으로 제공되는 도구를 바꾸는 방식으로요. 캐시된 접두사가 그대로 유지돼서 다음 요청도 여전히 캐시에서 읽어요.

출처: 문서

본문

이 기능에 ZDR(제로 데이터 보존)이 어떻게 적용되는지는 [API and data retention](https://platform.claude.com/docs/en/manage-claude/api-and-data-retention)을 보세요.

시스템 지시는 보통 최상위 system 필드에, 대화의 모든 메시지보다 앞에 있어요. 이 위치는 프롬프트 캐싱에 좋아요. 시스템 프롬프트는 안정적인 접두사의 일부라서 이후 턴들이 캐시에 적중하기 때문이에요. 하지만 세션 도중에만 필요하다는 걸 알게 되는 지시에는 좋지 않은 위치예요. 최상위 system 필드를 편집하면 프롬프트의 맨 앞부분이 바뀌어 그 뒤의 모든 것에 대한 캐시를 무효화하기 때문이에요.

대화 중 시스템 메시지가 그 빈틈을 메워줘요. 새 지시가 필요해진 대화 지점에 {"role": "system"} 메시지를 추가하는 거예요. 최상위 system 필드를 편집하는 대신요. 캐시된 접두사는 그대로라 다음 요청이 여전히 캐시에서 읽고, 새 지시는 일반 사용자 텍스트가 아니라 시스템 지시로 적용돼요.

대화 중 시스템 메시지는 Claude API, [Amazon Bedrock의 Claude](https://platform.claude.com/docs/en/build-with-claude/claude-in-amazon-bedrock), [Google Cloud](https://platform.claude.com/docs/en/build-with-claude/claude-on-vertex-ai)에서 사용할 수 있어요.

이 기능은 Claude Fable 5.1, Claude Mythos 5.1, Claude Fable 5, Claude Mythos 5, Claude Opus 5.5, Claude Opus 4.8, Claude Opus 5에서 사용할 수 있어요. 대화 중 시스템 메시지에는 베타 헤더가 필요하지 않아요. 이 기능은 Claude Sonnet 5에서는 사용할 수 없어요. 거기서는 최상위 system 필드를 사용하세요.

대화 중 도구 변경은 베타이며 mid-conversation-tool-changes-2026-07-01 베타 헤더가 필요해요. 같은 모델들에서 Claude API, Amazon Bedrock, Google Cloud에서 사용할 수 있어요. tool_addition 블록 안에서 도구 정의하기는 그 대신 inline-tools-2026-09-15 베타 헤더를 사용하며 Claude API에서 사용할 수 있어요. 그 방식으로 MCP 서버 추가하기mcp-client-2026-09-15 베타 헤더도 필요해요.

턴 범위 시스템 메시지(clear_at)는 베타이며, 대화 중 시스템 메시지와 같은 모델·플랫폼에서 mid-conversation-system-clear-at-2026-08-21 베타 헤더가 필요해요.

대화 중 도구 변경

tools 배열은 최상위 system 필드보다 해시된 요청 접두사에서 훨씬 앞에 있어서, 그것을 편집하면 전체 대화의 프롬프트 캐시를 무효화해요. 대화 중 도구 변경은 대화 중 시스템 메시지의 도구 버전이에요. 대화 수명 동안 도구 목록을 고정하는 대신, 턴 사이에 모델에 제공하는 도구를 바꿔요. 전체 도구 집합을 tools에 미리 선언하고, tool_addition·tool_removal 블록으로 특정 지점부터 모델에 도구를 제공하거나 철회해요. tools 배열 자체는 절대 변하지 않으므로 캐시된 접두사가 온전히 유지돼요.

tool_additiontool_removalrole: "system" 메시지의 content 배열에 있는 콘텐츠 블록이고, 같은 메시지의 text 블록과 섞을 수 있어요. 메시지는 대화 중 시스템 메시지의 배치 규칙을 따르며, 일시 중지된 턴 뒤에는 추가 제한이 하나 있어요(제한 사항 참조). 변경은 그 지점부터 대화에 적용돼요. 각 블록의 tool 필드는 도구를 정의하는 게 아니라 참조해요. {"type": "tool_reference", "name": "..."}는 요청의 tools 배열에 선언된 도구를 지칭하고, MCP 커넥터 도구는 mcp_tool_reference(server_namename)로 개별 참조하거나 mcp_toolset_reference(server_name)로 전체 도구셋을 참조할 수 있어요. tools에 선언되지 않은 이름을 참조하면 400 에러가 반환돼요(Claude API에서는 error.details.error_codetool_reference_unresolved로 설정). inline-tools-2026-09-15 베타 헤더를 쓰면 tool_addition 블록이 도구의 전체 정의를 실을 수도 있어요.

tools에 선언된 모든 도구는 defer_loading: true로 선언되지 않는 한 대화 시작부터 모델에 제공돼요. 그렇게 선언하면 tool_addition 블록이 그것을 표면화할 때까지 보류돼요. tool_addition은 이전 tool_removal이 철회한 도구도 다시 제공해요.

다음 요청은 get_weathertools에 선언하고, 첫 사용자 턴 뒤에 tool_removal 블록으로 철회해요. 대화 중 도구 변경은 베타이므로 요청이 mid-conversation-tool-changes-2026-07-01 베타 헤더를 보내요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mid-conversation-tool-changes-2026-07-01" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather for a location.", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"} }, "required": ["location"] } } ], "messages": [ { "role": "user", "content": "Say OK." }, { "role": "system", "content": [ { "type": "tool_removal", "tool": {"type": "tool_reference", "name": "get_weather"} } ] } ] }' ```
ant beta:messages create --beta mid-conversation-tool-changes-2026-07-01 \
  --transform 'content.#(type=="text").text' --raw-output <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
tools:
  - name: get_weather
    description: Get the current weather for a location.
    input_schema:
      type: object
      properties:
        location:
          type: string
          description: City name
      required:
        - location
messages:
  - role: user
    content: Say OK.
  - role: system
    content:
      - type: tool_removal
        tool:
          type: tool_reference
          name: get_weather
YAML
client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    betas=["mid-conversation-tool-changes-2026-07-01"],
    # The full tool set is declared up front and never changes, so the
    # cached prefix stays intact.
    tools=[
        {
            "name": "get_weather",
            "description": "Get the current weather for a location.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City name"},
                },
                "required": ["location"],
            },
        },
    ],
    messages=[
        {
            "role": "user",
            "content": "Say OK.",
        },
        # Withdraw get_weather from this point onward. The block references
        # the tool by name instead of editing `tools`, so earlier turns stay
        # byte-identical and the cache still hits.
        {
            "role": "system",
            "content": [
                {
                    "type": "tool_removal",
                    "tool": {"type": "tool_reference", "name": "get_weather"},
                },
            ],
        },
    ],
)

for block in response.content:
    if block.type == "text":
        print(block.text)
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  betas: ["mid-conversation-tool-changes-2026-07-01"],
  // The full tool set is declared up front and never changes, so the
  // cached prefix stays intact.
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather for a location.",
      input_schema: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "City name"
          }
        },
        required: ["location"]
      }
    }
  ],
  messages: [
    { role: "user", content: "Say OK." },
    // Withdraw get_weather from this point onward. The block references the
    // tool by name instead of editing `tools`, so earlier turns stay
    // byte-identical and the cache still hits.
    {
      role: "system",
      content: [
        {
          type: "tool_removal",
          tool: { type: "tool_reference", name: "get_weather" }
        }
      ]
    }
  ]
});

for (const block of response.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
using Anthropic.Models.Beta.Messages;
using Messages = Anthropic.Models.Messages;

AnthropicClient client = new();

var response = await client.Beta.Messages.Create(new MessageCreateParams
{
    Model = Messages::Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Betas = ["mid-conversation-tool-changes-2026-07-01"],
    // The full tool set is declared up front and never changes, so the
    // cached prefix stays intact.
    Tools =
    [
        new BetaTool
        {
            Name = "get_weather",
            Description = "Get the current weather for a location.",
            InputSchema = new InputSchema
            {
                Properties = new Dictionary<string, JsonElement>
                {
                    ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }),
                },
                Required = ["location"],
            },
        },
    ],
    Messages =
    [
        new() { Role = Role.User, Content = "Say OK." },
        // Withdraw get_weather from this point onward. The block references
        // the tool by name instead of editing `Tools`, so earlier turns stay
        // byte-identical and the cache still hits.
        new()
        {
            Role = Role.System,
            Content = new(
            [
                new BetaRequestToolRemovalBlock
                {
                    Tool = new BetaToolChangeToolReference { Name = "get_weather" },
                },
            ]),
        },
    ],
});

foreach (var block in response.Content)
{
    if (block.TryPickText(out var text))
    {
        Console.WriteLine(text.Text);
    }
}
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Betas:     []anthropic.AnthropicBeta{"mid-conversation-tool-changes-2026-07-01"},
	// The full tool set is declared up front and never changes, so the
	// cached prefix stays intact.
	Tools: []anthropic.BetaToolUnionParam{
		{OfTool: &anthropic.BetaToolParam{
			Name:        "get_weather",
			Description: anthropic.String("Get the current weather for a location."),
			InputSchema: anthropic.BetaToolInputSchemaParam{
				Properties: map[string]any{
					"location": map[string]any{
						"type":        "string",
						"description": "City name",
					},
				},
				Required: []string{"location"},
			},
		}},
	},
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Say OK.")),
		// Withdraw get_weather from this point onward. The block references
		// the tool by name instead of editing Tools, so earlier turns stay
		// byte-identical and the cache still hits.
		{
			Role: anthropic.BetaMessageParamRoleSystem,
			Content: []anthropic.BetaContentBlockParamUnion{
				anthropic.NewBetaToolRemovalBlock(anthropic.BetaToolChangeToolReferenceParam{
					Name: "get_weather",
				}),
			},
		},
	},
})
if err != nil {
	log.Fatal(err)
}

for _, block := range response.Content {
	if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {
		fmt.Println(textBlock.Text)
	}
}
import com.anthropic.models.beta.messages.BetaContentBlockParam;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaMessageParam;
import com.anthropic.models.beta.messages.BetaRequestToolRemovalBlock;
import com.anthropic.models.beta.messages.BetaTool;
import com.anthropic.models.beta.messages.MessageCreateParams;
// ...
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    // The full tool set is declared up front and never changes, so the
    // cached prefix stays intact.
    BetaTool weatherTool = BetaTool.builder()
        .name("get_weather")
        .description("Get the current weather for a location.")
        .inputSchema(BetaTool.InputSchema.builder()
            .properties(BetaTool.InputSchema.Properties.builder()
                .putAdditionalProperty("location", JsonValue.from(Map.of(
                    "type", "string",
                    "description", "City name")))
                .build())
            .addRequired("location")
            .build())
        .build();

    MessageCreateParams params = MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5_5)
        .maxTokens(1024)
        .addBeta("mid-conversation-tool-changes-2026-07-01")
        .addTool(weatherTool)
        .addUserMessage("Say OK.")
        // Withdraw get_weather from this point onward. The block references
        // the tool by name instead of editing `tools`, so earlier turns stay
        // byte-identical and the cache still hits.
        .addMessage(BetaMessageParam.builder()
            .role(BetaMessageParam.Role.SYSTEM)
            .contentOfBetaContentBlockParams(List.of(
                BetaContentBlockParam.ofToolRemoval(BetaRequestToolRemovalBlock.builder()
                    .referenceTool("get_weather")
                    .build())))
            .build())
        .build();

    BetaMessage response = client.beta().messages().create(params);
    response.content().stream()
        .flatMap(block -> block.text().stream())
        .forEach(textBlock -> IO.println(textBlock.text()));
$client = new Client();

$response = $client->beta->messages->create(
    model: 'claude-opus-5-5',
    maxTokens: 1024,
    betas: ['mid-conversation-tool-changes-2026-07-01'],
    // The full tool set is declared up front and never changes, so the
    // cached prefix stays intact.
    tools: [
        [
            'name' => 'get_weather',
            'description' => 'Get the current weather for a location.',
            'input_schema' => [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'City name',
                    ],
                ],
                'required' => ['location'],
            ],
        ],
    ],
    messages: [
        ['role' => 'user', 'content' => 'Say OK.'],
        // Withdraw get_weather from this point onward. The block references
        // the tool by name instead of editing `tools`, so earlier turns stay
        // byte-identical and the cache still hits.
        [
            'role' => 'system',
            'content' => [
                [
                    'type' => 'tool_removal',
                    'tool' => ['type' => 'tool_reference', 'name' => 'get_weather'],
                ],
            ],
        ],
    ],
);

foreach ($response->content as $block) {
    if ($block->type === 'text') {
        echo $block->text, PHP_EOL;
    }
}
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: "claude-opus-5-5",
  max_tokens: 1024,
  betas: ["mid-conversation-tool-changes-2026-07-01"],
  # The full tool set is declared up front and never changes, so the
  # cached prefix stays intact.
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather for a location.",
      input_schema: {
        type: "object",
        properties: {
          location: { type: "string", description: "City name" }
        },
        required: ["location"]
      }
    }
  ],
  messages: [
    { role: "user", content: "Say OK." },
    # Withdraw get_weather from this point onward. The block references
    # the tool by name instead of editing `tools`, so earlier turns stay
    # byte-identical and the cache still hits.
    {
      role: "system",
      content: [
        {
          type: "tool_removal",
          tool: { type: "tool_reference", name: "get_weather" }
        }
      ]
    }
  ]
)

response.content.each do |block|
  puts block.text if block.type == :text
end

메시지 안에서 도구 정의하기 (beta)

inline-tools-2026-09-15 베타 헤더를 쓰면 tool_addition 블록이 참조로 이름을 대는 대신, 전체 정의를 실어 도구를 값으로 정의할 수 있어요. 이 덕분에 대화 시작 시점에 알 수 없었거나 스키마가 나중에 바뀌는 도구를 role: "system" 메시지를 추가해 도입할 수 있어요. tools 배열과 그 이전의 모든 메시지는 보낸 그대로 유지되므로 프롬프트 캐시가 여전히 적중하고, 추가된 메시지만 새 입력으로 처리돼요. 유일한 예외(비-지연 도구가 없는 tools 배열)는 아래 규칙에 나와 있어요. 헤더는 참조로 도구를 추가·제거하는 것도 포함하므로 mid-conversation-tool-changes-2026-07-01도 함께 보낼 필요가 없어요.

정의를 tool_definition 유형의 tool 객체로 감싸요. definition은 사용자 정의 도구나 Anthropic 정의 클라이언트·서버 도구 같은 tools 항목으로, cache_controldefer_loading을 포함한 평소 설정을 가져요. 베타 동안에는 일부 도구 유형(컴퓨터 사용 도구 포함)이 아직 메시지에서 정의될 수 없고 그렇게 말하는 400 에러를 반환해요. 그런 것들은 tools에 선언하고 참조로 추가하세요. 예를 들어 대화 중에 사용자 정의 도구를 정의하려면:

{
  "role": "system",
  "content": [
    {
      "type": "tool_addition",
      "tool": {
        "type": "tool_definition",
        "definition": {
          "name": "db_query",
          "description": "Run a read-only SQL query against the analytics database.",
          "input_schema": {
            "type": "object",
            "properties": { "sql": { "type": "string" } },
            "required": ["sql"]
          }
        }
      }
    }
  ]
}

그 위치부터 모델은 tools에 선언된 도구를 호출하는 것과 같은 방식으로 그 도구를 호출할 수 있어요. 동일한 정의를 다시 보내는 것은 아무것도 바꾸지 않으므로, 클라이언트가 예를 들어 재시도 시에 안전하게 다시 보낼 수 있어요.

다음 요청은 get_weathertools에 유지하고 첫 사용자 턴 뒤에 db_query를 정의해요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: inline-tools-2026-09-15" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "tools": [ { "name": "get_weather", "description": "Get the current weather for a location.", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"} }, "required": ["location"] } } ], "messages": [ { "role": "user", "content": "How many orders shipped yesterday?" }, { "role": "system", "content": [ { "type": "tool_addition", "tool": { "type": "tool_definition", "definition": { "name": "db_query", "description": "Run a read-only SQL query against the analytics database.", "input_schema": { "type": "object", "properties": {"sql": {"type": "string"}}, "required": ["sql"] } } } } ] } ] }' ```
ant beta:messages create --beta inline-tools-2026-09-15 <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
# Keep at least one non-deferred tool in `tools`, so a tool defined
# later doesn't change the start of the rendered prompt.
tools:
  - name: get_weather
    description: Get the current weather for a location.
    input_schema:
      type: object
      properties:
        location:
          type: string
          description: City name
      required:
        - location
messages:
  - role: user
    content: How many orders shipped yesterday?
  # Define db_query by value from this point onward. `tools` and the
  # earlier messages stay exactly as sent, so the cache still hits.
  - role: system
    content:
      - type: tool_addition
        tool:
          type: tool_definition
          definition:
            name: db_query
            description: Run a read-only SQL query against the analytics database.
            input_schema:
              type: object
              properties:
                sql:
                  type: string
              required:
                - sql
YAML
client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    betas=["inline-tools-2026-09-15"],
    # Keep at least one non-deferred tool in `tools`, so a tool defined
    # later doesn't change the start of the rendered prompt.
    tools=[
        {
            "name": "get_weather",
            "description": "Get the current weather for a location.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City name"},
                },
                "required": ["location"],
            },
        },
    ],
    messages=[
        {"role": "user", "content": "How many orders shipped yesterday?"},
        # Define db_query by value from this point onward. `tools` and the
        # earlier messages stay exactly as sent, so the cache still hits.
        {
            "role": "system",
            "content": [
                {
                    "type": "tool_addition",
                    "tool": {
                        "type": "tool_definition",
                        "definition": {
                            "name": "db_query",
                            "description": "Run a read-only SQL query against the analytics database.",
                            "input_schema": {
                                "type": "object",
                                "properties": {"sql": {"type": "string"}},
                                "required": ["sql"],
                            },
                        },
                    },
                },
            ],
        },
    ],
)

for block in response.content:
    if block.type == "tool_use":
        print(block.name, block.input)
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  betas: ["inline-tools-2026-09-15"],
  // Keep at least one non-deferred tool in `tools`, so a tool defined
  // later doesn't change the start of the rendered prompt.
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather for a location.",
      input_schema: {
        type: "object",
        properties: {
          location: { type: "string", description: "City name" }
        },
        required: ["location"]
      }
    }
  ],
  messages: [
    { role: "user", content: "How many orders shipped yesterday?" },
    // Define db_query by value from this point onward. `tools` and the
    // earlier messages stay exactly as sent, so the cache still hits.
    {
      role: "system",
      content: [
        {
          type: "tool_addition",
          tool: {
            type: "tool_definition",
            definition: {
              name: "db_query",
              description: "Run a read-only SQL query against the analytics database.",
              input_schema: {
                type: "object",
                properties: { sql: { type: "string" } },
                required: ["sql"]
              }
            }
          }
        }
      ]
    }
  ]
});

for (const block of response.content) {
  if (block.type === "tool_use") {
    console.log(block.name, JSON.stringify(block.input));
  }
}
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Messages;
using Messages = Anthropic.Models.Messages;

AnthropicClient client = new();

var response = await client.Beta.Messages.Create(new MessageCreateParams
{
    Model = Messages::Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Betas = [AnthropicBeta.InlineTools2026_09_15],
    // Keep at least one non-deferred tool in `Tools`, so a tool defined
    // later doesn't change the start of the rendered prompt.
    Tools =
    [
        new BetaTool
        {
            Name = "get_weather",
            Description = "Get the current weather for a location.",
            InputSchema = new InputSchema
            {
                Properties = new Dictionary<string, JsonElement>
                {
                    ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }),
                },
                Required = ["location"],
            },
        },
    ],
    Messages =
    [
        new() { Role = Role.User, Content = "How many orders shipped yesterday?" },
        // Define db_query by value from this point onward. `Tools` and the
        // earlier messages stay exactly as sent, so the cache still hits.
        new()
        {
            Role = Role.System,
            Content = new(
            [
                new BetaRequestToolAdditionBlock
                {
                    Tool = new BetaToolChangeToolDefinitionParam
                    {
                        Definition = new BetaTool
                        {
                            Name = "db_query",
                            Description = "Run a read-only SQL query against the analytics database.",
                            InputSchema = new InputSchema
                            {
                                Properties = new Dictionary<string, JsonElement>
                                {
                                    ["sql"] = JsonSerializer.SerializeToElement(new { type = "string" }),
                                },
                                Required = ["sql"],
                            },
                        },
                    },
                },
            ]),
        },
    ],
});

foreach (var block in response.Content)
{
    if (block.TryPickToolUse(out var toolUse))
    {
        Console.WriteLine($"{toolUse.Name} {JsonSerializer.Serialize(toolUse.Input)}");
    }
}
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Betas:     []anthropic.AnthropicBeta{anthropic.AnthropicBetaInlineTools2026_09_15},
	// Keep at least one non-deferred tool in Tools, so a tool defined
	// later doesn't change the start of the rendered prompt.
	Tools: []anthropic.BetaToolUnionParam{
		{OfTool: &anthropic.BetaToolParam{
			Name:        "get_weather",
			Description: anthropic.String("Get the current weather for a location."),
			InputSchema: anthropic.BetaToolInputSchemaParam{
				Properties: map[string]any{
					"location": map[string]any{
						"type":        "string",
						"description": "City name",
					},
				},
				Required: []string{"location"},
			},
		}},
	},
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("How many orders shipped yesterday?")),
		// Define db_query by value from this point onward. Tools and the
		// earlier messages stay exactly as sent, so the cache still hits.
		{
			Role: anthropic.BetaMessageParamRoleSystem,
			Content: []anthropic.BetaContentBlockParamUnion{
				anthropic.NewBetaToolAdditionBlock(anthropic.BetaToolChangeToolDefinitionParam{
					Definition: anthropic.BetaToolUnionParam{OfTool: &anthropic.BetaToolParam{
						Name:        "db_query",
						Description: anthropic.String("Run a read-only SQL query against the analytics database."),
						InputSchema: anthropic.BetaToolInputSchemaParam{
							Properties: map[string]any{
								"sql": map[string]any{"type": "string"},
							},
							Required: []string{"sql"},
						},
					}},
				}),
			},
		},
	},
})
if err != nil {
	log.Fatal(err)
}

for _, block := range response.Content {
	if toolUse, ok := block.AsAny().(anthropic.BetaToolUseBlock); ok {
		fmt.Println(toolUse.Name, toolUse.Input)
	}
}
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaContentBlockParam;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaRequestToolAdditionBlock;
import com.anthropic.models.beta.messages.BetaTool;
import com.anthropic.models.beta.messages.MessageCreateParams;
// ...

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    BetaTool weatherTool = BetaTool.builder()
        .name("get_weather")
        .description("Get the current weather for a location.")
        .inputSchema(BetaTool.InputSchema.builder()
            .properties(BetaTool.InputSchema.Properties.builder()
                .putAdditionalProperty("location", JsonValue.from(Map.of(
                    "type", "string",
                    "description", "City name")))
                .build())
            .addRequired("location")
            .build())
        .build();

    BetaTool dbQueryTool = BetaTool.builder()
        .name("db_query")
        .description("Run a read-only SQL query against the analytics database.")
        .inputSchema(BetaTool.InputSchema.builder()
            .properties(BetaTool.InputSchema.Properties.builder()
                .putAdditionalProperty("sql", JsonValue.from(Map.of("type", "string")))
                .build())
            .addRequired("sql")
            .build())
        .build();

    MessageCreateParams params = MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5_5)
        .maxTokens(1024)
        .addBeta(AnthropicBeta.INLINE_TOOLS_2026_09_15)
        // Keep at least one non-deferred tool in `tools`, so a tool defined
        // later doesn't change the start of the rendered prompt.
        .addTool(weatherTool)
        .addUserMessage("How many orders shipped yesterday?")
        // Define db_query by value from this point onward. `tools` and the
        // earlier messages stay exactly as sent, so the cache still hits.
        .addSystemMessageOfBetaContentBlockParams(List.of(
            BetaContentBlockParam.ofToolAddition(BetaRequestToolAdditionBlock.builder()
                .definitionTool(dbQueryTool)
                .build())))
        .build();

    BetaMessage response = client.beta().messages().create(params);
    response.content().stream()
        .flatMap(block -> block.toolUse().stream())
        .forEach(toolUse -> IO.println(toolUse.name() + " " + toolUse._input()));
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaToolUseBlock;
// ...

$client = new Client();

$response = $client->beta->messages->create(
    model: Model::CLAUDE_OPUS_5_5,
    maxTokens: 1024,
    betas: [AnthropicBeta::INLINE_TOOLS_2026_09_15],
    // Keep at least one non-deferred tool in `tools`, so a tool defined
    // later doesn't change the start of the rendered prompt.
    tools: [
        [
            'name' => 'get_weather',
            'description' => 'Get the current weather for a location.',
            'input_schema' => [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'City name',
                    ],
                ],
                'required' => ['location'],
            ],
        ],
    ],
    messages: [
        ['role' => 'user', 'content' => 'How many orders shipped yesterday?'],
        // Define db_query by value from this point onward. `tools` and the
        // earlier messages stay exactly as sent, so the cache still hits.
        [
            'role' => 'system',
            'content' => [
                [
                    'type' => 'tool_addition',
                    'tool' => [
                        'type' => 'tool_definition',
                        'definition' => [
                            'name' => 'db_query',
                            'description' => 'Run a read-only SQL query against the analytics database.',
                            'input_schema' => [
                                'type' => 'object',
                                'properties' => ['sql' => ['type' => 'string']],
                                'required' => ['sql'],
                            ],
                        ],
                    ],
                ],
            ],
        ],
    ],
);

foreach ($response->content as $block) {
    if ($block instanceof BetaToolUseBlock) {
        echo $block->name, ' ', json_encode($block->input), PHP_EOL;
    }
}
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: Anthropic::Model::CLAUDE_OPUS_5_5,
  max_tokens: 1024,
  betas: [Anthropic::AnthropicBeta::INLINE_TOOLS_2026_09_15],
  # Keep at least one non-deferred tool in `tools`, so a tool defined
  # later doesn't change the start of the rendered prompt.
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather for a location.",
      input_schema: {
        type: "object",
        properties: {
          location: { type: "string", description: "City name" }
        },
        required: ["location"]
      }
    }
  ],
  messages: [
    { role: "user", content: "How many orders shipped yesterday?" },
    # Define db_query by value from this point onward. `tools` and the
    # earlier messages stay exactly as sent, so the cache still hits.
    {
      role: "system",
      content: [
        {
          type: "tool_addition",
          tool: {
            type: "tool_definition",
            definition: {
              name: "db_query",
              description: "Run a read-only SQL query against the analytics database.",
              input_schema: {
                type: "object",
                properties: { sql: { type: "string" } },
                required: ["sql"]
              }
            }
          }
        }
      ]
    }
  ]
)

response.content.each do |block|
  puts "#{block.name} #{block.input}" if block.is_a?(Anthropic::Beta::BetaToolUseBlock)
end

응답의 content에는 새 도구의 tool_use 블록이 포함돼요. 예:

{
  "type": "tool_use",
  "id": "toolu_01A09q90qw90lq917835lq9",
  "name": "db_query",
  "input": {
    "sql": "SELECT COUNT(*) FROM orders WHERE shipped_at::date = CURRENT_DATE - 1"
  }
}

도구의 스키마를 바꾸거나 서버 도구를 최신 버전으로 옮기려면 같은 이름 아래 다른 정의를 보내요. 새 정의는 그 위치부터 이전 정의를 대체해요. 다른 유형의 도구 이름을 재사용하는 정의는 error.details.error_codetool_name_conflict로 설정된 400 에러를 반환해요. 같은 도구의 최신 버전은 다른 유형으로 치지 않아요. tool_removal은 여전히 참조를 받고, 제거된 도구는 나중에 다시 정의하거나 재제공할 수 있어요.

정의가 렌더링되는 위치에서 몇 가지 규칙이 따라와요.

  • 아는 것은 미리 선언하세요. 첫 요청에서 아는 도구는 tools에 속해요. 모델이 아직 못 보게 하려면 defer_loading: true와 나중의 tool_addition 참조를 쓰세요. 첫 요청에서 알 수 없거나 나중에 바뀌는 것만 값으로 정의하세요.
  • tools에 비-지연 도구를 최소 하나 유지하세요. tools 배열에 비-지연 도구가 없는 대화는 허용되지만, 값으로 정의한 첫 도구가 렌더링된 프롬프트의 시작을 바꿔 그 요청에서 한 번 캐시 미스를 냅니다. 도구 검색 도구는 비-지연으로 칩니다.
  • 날짜가 있는 도구 유형은 자체 베타 헤더를 유지해요. 값으로 정의하는 서버 도구에 자체 베타 헤더가 필요하면, 대화의 이후 모든 요청에 그 헤더를 보내세요.
  • cache_control은 블록이나 정의 중 하나에만, 둘 다 아닙니다. 요청의 중단점 한도에 세어요. 지연된 정의는 cache_control을 실을 수 없어요.

다음 제한 중 하나라도 초과하면 요청은 error.details.error_codeavailable_tools_limit_exceeded로 설정된 400 에러를 반환해요.

  • 어떤 메시지 다음에 10,000개 이상의 지연 도구가 사용 가능.
  • 첫 사용자 메시지 뒤에 정의된 도구가 어떤 메시지 다음에 10,000개를 초과.
  • 첫 사용자 메시지 뒤에 보내진 도구 정의 중 어떤 메시지 다음에도 여전히 사용 가능한 것의 총합이 4 MB(4,194,304바이트)를 초과.
  • 렌더링된 도구 텍스트가 4 MB(4,194,304바이트)보다 큼.

대화 중 MCP 서버 추가하기 (beta)

대화 도중에 MCP 커넥터 서버를 추가하려면 inline-tools-2026-09-15와 함께 mcp-client-2026-09-15 베타 헤더를 보내요. 그러면 tool_addition 블록의 definitionmcp_toolset이 될 수 있어서 tools를 편집하지 않고도 서버의 도구를 쓸 수 있어요. 평소처럼 mcp_servers에 서버 연결 정보를 나열한 다음, 서버를 쓸 수 있게 된 위치에 도구셋을 추가해요.

{
  "role": "system",
  "content": [
    {
      "type": "tool_addition",
      "tool": {
        "type": "tool_definition",
        "definition": { "type": "mcp_toolset", "mcp_server_name": "calendar" }
      }
    }
  ]
}

mcp_toolset 객체는 default_configconfigs를 포함해 tools에 넣는 것과 같은 객체예요. tool_addition 블록은 서버 URL이나 토큰을 절대 담지 않아요. 그것들은 mcp_servers에 있어요.

다음 요청은 get_weathertools에 유지하고, calendar 서버를 mcp_servers에 나열하며, 첫 사용자 턴 뒤에 서버의 도구셋을 추가해요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: inline-tools-2026-09-15,mcp-client-2026-09-15" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "mcp_servers": [ { "type": "url", "url": "https://mcp.example.com/calendar", "name": "calendar", "authorization_token": "YOUR_TOKEN" } ], "tools": [ { "name": "get_weather", "description": "Get the current weather for a location.", "input_schema": { "type": "object", "properties": { "location": {"type": "string", "description": "City name"} }, "required": ["location"] } } ], "messages": [ { "role": "user", "content": "What's on my calendar tomorrow?" }, { "role": "system", "content": [ { "type": "tool_addition", "tool": { "type": "tool_definition", "definition": { "type": "mcp_toolset", "mcp_server_name": "calendar" } } } ] } ] }' ```
ant beta:messages create \
  --beta inline-tools-2026-09-15,mcp-client-2026-09-15 <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
mcp_servers:
  - type: url
    url: https://mcp.example.com/calendar
    name: calendar
    authorization_token: YOUR_TOKEN
tools:
  - name: get_weather
    description: Get the current weather for a location.
    input_schema:
      type: object
      properties:
        location:
          type: string
          description: City name
      required:
        - location
messages:
  - role: user
    content: What's on my calendar tomorrow?
  # Make the calendar server's tools available from this point onward.
  # The block names the server; it never holds a URL or token.
  - role: system
    content:
      - type: tool_addition
        tool:
          type: tool_definition
          definition:
            type: mcp_toolset
            mcp_server_name: calendar
YAML
client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    betas=["inline-tools-2026-09-15", "mcp-client-2026-09-15"],
    mcp_servers=[
        {
            "type": "url",
            "url": "https://mcp.example.com/calendar",
            "name": "calendar",
            "authorization_token": "YOUR_TOKEN",
        },
    ],
    tools=[
        {
            "name": "get_weather",
            "description": "Get the current weather for a location.",
            "input_schema": {
                "type": "object",
                "properties": {
                    "location": {"type": "string", "description": "City name"},
                },
                "required": ["location"],
            },
        },
    ],
    messages=[
        {"role": "user", "content": "What's on my calendar tomorrow?"},
        # Make the calendar server's tools available from this point onward.
        # The block names the server; it never holds a URL or token.
        {
            "role": "system",
            "content": [
                {
                    "type": "tool_addition",
                    "tool": {
                        "type": "tool_definition",
                        "definition": {
                            "type": "mcp_toolset",
                            "mcp_server_name": "calendar",
                        },
                    },
                },
            ],
        },
    ],
)

# The response starts with an mcp_tool_listing block for the calendar server,
# so check each block's type instead of reading content[0].
for block in response.content:
    match block.type:
        case "mcp_tool_listing":
            print(block.mcp_server_name, [tool.name for tool in block.tools])
        case "text":
            print(block.text)
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  betas: ["inline-tools-2026-09-15", "mcp-client-2026-09-15"],
  mcp_servers: [
    {
      type: "url",
      url: "https://mcp.example.com/calendar",
      name: "calendar",
      authorization_token: "YOUR_TOKEN"
    }
  ],
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather for a location.",
      input_schema: {
        type: "object",
        properties: {
          location: { type: "string", description: "City name" }
        },
        required: ["location"]
      }
    }
  ],
  messages: [
    { role: "user", content: "What's on my calendar tomorrow?" },
    // Make the calendar server's tools available from this point onward.
    // The block names the server; it never holds a URL or token.
    {
      role: "system",
      content: [
        {
          type: "tool_addition",
          tool: {
            type: "tool_definition",
            definition: { type: "mcp_toolset", mcp_server_name: "calendar" }
          }
        }
      ]
    }
  ]
});

// The response starts with an mcp_tool_listing block for the calendar server,
// so check each block's type instead of reading content[0].
for (const block of response.content) {
  switch (block.type) {
    case "mcp_tool_listing":
      console.log(
        block.mcp_server_name,
        block.tools.map((tool) => tool.name)
      );
      break;
    case "text":
      console.log(block.text);
      break;
  }
}
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Messages;
using Messages = Anthropic.Models.Messages;

AnthropicClient client = new();

var response = await client.Beta.Messages.Create(new MessageCreateParams
{
    Model = Messages::Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Betas = [AnthropicBeta.InlineTools2026_09_15, AnthropicBeta.McpClient2026_09_15],
    McpServers =
    [
        new BetaRequestMcpServerUrlDefinition
        {
            Url = "https://mcp.example.com/calendar",
            Name = "calendar",
            AuthorizationToken = "YOUR_TOKEN",
        },
    ],
    Tools =
    [
        new BetaTool
        {
            Name = "get_weather",
            Description = "Get the current weather for a location.",
            InputSchema = new InputSchema
            {
                Properties = new Dictionary<string, JsonElement>
                {
                    ["location"] = JsonSerializer.SerializeToElement(new { type = "string", description = "City name" }),
                },
                Required = ["location"],
            },
        },
    ],
    Messages =
    [
        new() { Role = Role.User, Content = "What's on my calendar tomorrow?" },
        // Make the calendar server's tools available from this point onward.
        // The block names the server; it never holds a URL or token.
        new()
        {
            Role = Role.System,
            Content = new(
            [
                new BetaRequestToolAdditionBlock
                {
                    Tool = new BetaToolChangeToolDefinitionParam
                    {
                        Definition = new BetaMcpToolset("calendar"),
                    },
                },
            ]),
        },
    ],
});

// The response starts with an mcp_tool_listing block for the calendar server,
// so check each block's type instead of reading Content[0].
foreach (var block in response.Content)
{
    if (block.TryPickMcpToolListing(out var listing))
    {
        Console.WriteLine($"{listing.McpServerName} {JsonSerializer.Serialize(listing.Tools.Select(tool => tool.Name))}");
    }
    else if (block.TryPickText(out var text))
    {
        Console.WriteLine(text.Text);
    }
}
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Betas: []anthropic.AnthropicBeta{
		anthropic.AnthropicBetaInlineTools2026_09_15,
		anthropic.AnthropicBetaMCPClient2026_09_15,
	},
	MCPServers: []anthropic.BetaRequestMCPServerURLDefinitionParam{
		{
			URL:                "https://mcp.example.com/calendar",
			Name:               "calendar",
			AuthorizationToken: anthropic.String("YOUR_TOKEN"),
		},
	},
	Tools: []anthropic.BetaToolUnionParam{
		{OfTool: &anthropic.BetaToolParam{
			Name:        "get_weather",
			Description: anthropic.String("Get the current weather for a location."),
			InputSchema: anthropic.BetaToolInputSchemaParam{
				Properties: map[string]any{
					"location": map[string]any{
						"type":        "string",
						"description": "City name",
					},
				},
				Required: []string{"location"},
			},
		}},
	},
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("What's on my calendar tomorrow?")),
		// Make the calendar server's tools available from this point onward.
		// The block names the server; it never holds a URL or token.
		{
			Role: anthropic.BetaMessageParamRoleSystem,
			Content: []anthropic.BetaContentBlockParamUnion{
				anthropic.NewBetaToolAdditionBlock(anthropic.BetaToolChangeToolDefinitionParam{
					Definition: anthropic.BetaToolUnionParam{OfMCPToolset: &anthropic.BetaMCPToolsetParam{
						MCPServerName: "calendar",
					}},
				}),
			},
		},
	},
})
if err != nil {
	log.Fatal(err)
}

// The response starts with an mcp_tool_listing block for the calendar server,
// so check each block's type instead of reading Content[0].
for _, block := range response.Content {
	switch variant := block.AsAny().(type) {
	case anthropic.BetaMCPToolListingBlock:
		var toolNames []string
		for _, tool := range variant.Tools {
			toolNames = append(toolNames, tool.Name)
		}
		fmt.Println(variant.MCPServerName, toolNames)
	case anthropic.BetaTextBlock:
		fmt.Println(variant.Text)
	}
}
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaContentBlockParam;
import com.anthropic.models.beta.messages.BetaMcpTool;
import com.anthropic.models.beta.messages.BetaMcpToolset;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaRequestMcpServerUrlDefinition;
import com.anthropic.models.beta.messages.BetaRequestToolAdditionBlock;
import com.anthropic.models.beta.messages.BetaTool;
import com.anthropic.models.beta.messages.MessageCreateParams;
// ...

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    BetaTool weatherTool = BetaTool.builder()
        .name("get_weather")
        .description("Get the current weather for a location.")
        .inputSchema(BetaTool.InputSchema.builder()
            .properties(BetaTool.InputSchema.Properties.builder()
                .putAdditionalProperty("location", JsonValue.from(Map.of(
                    "type", "string",
                    "description", "City name")))
                .build())
            .addRequired("location")
            .build())
        .build();

    MessageCreateParams params = MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5_5)
        .maxTokens(1024)
        .addBeta(AnthropicBeta.INLINE_TOOLS_2026_09_15)
        .addBeta(AnthropicBeta.MCP_CLIENT_2026_09_15)
        .addMcpServer(BetaRequestMcpServerUrlDefinition.builder()
            .url("https://mcp.example.com/calendar")
            .name("calendar")
            .authorizationToken("YOUR_TOKEN")
            .build())
        .addTool(weatherTool)
        .addUserMessage("What's on my calendar tomorrow?")
        // Make the calendar server's tools available from this point onward.
        // The block names the server; it never holds a URL or token.
        .addSystemMessageOfBetaContentBlockParams(List.of(
            BetaContentBlockParam.ofToolAddition(BetaRequestToolAdditionBlock.builder()
                .definitionTool(BetaMcpToolset.builder()
                    .mcpServerName("calendar")
                    .build())
                .build())))
        .build();

    BetaMessage response = client.beta().messages().create(params);

    // The response starts with an mcp_tool_listing block for the calendar server,
    // so check each block's type instead of reading the first block.
    for (var block : response.content()) {
        switch (block.type().value()) {
            case MCP_TOOL_LISTING -> {
                var listing = block.asMcpToolListing();
                var toolNames = listing.tools().stream().map(BetaMcpTool::name).toList();
                IO.println(listing.mcpServerName() + " " + toolNames);
            }
            case TEXT -> IO.println(block.asText().text());
        }
    }
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaMCPTool;
use Anthropic\Beta\Messages\BetaMCPToolListingBlock;
use Anthropic\Beta\Messages\BetaTextBlock;
// ...

$client = new Client();

$response = $client->beta->messages->create(
    model: Model::CLAUDE_OPUS_5_5,
    maxTokens: 1024,
    betas: [
        AnthropicBeta::INLINE_TOOLS_2026_09_15,
        AnthropicBeta::MCP_CLIENT_2026_09_15,
    ],
    mcpServers: [
        [
            'type' => 'url',
            'url' => 'https://mcp.example.com/calendar',
            'name' => 'calendar',
            'authorization_token' => 'YOUR_TOKEN',
        ],
    ],
    tools: [
        [
            'name' => 'get_weather',
            'description' => 'Get the current weather for a location.',
            'input_schema' => [
                'type' => 'object',
                'properties' => [
                    'location' => [
                        'type' => 'string',
                        'description' => 'City name',
                    ],
                ],
                'required' => ['location'],
            ],
        ],
    ],
    messages: [
        ['role' => 'user', 'content' => "What's on my calendar tomorrow?"],
        // Make the calendar server's tools available from this point onward.
        // The block names the server; it never holds a URL or token.
        [
            'role' => 'system',
            'content' => [
                [
                    'type' => 'tool_addition',
                    'tool' => [
                        'type' => 'tool_definition',
                        'definition' => [
                            'type' => 'mcp_toolset',
                            'mcp_server_name' => 'calendar',
                        ],
                    ],
                ],
            ],
        ],
    ],
);

// The response starts with an mcp_tool_listing block for the calendar server,
// so check each block's type instead of reading content[0].
foreach ($response->content as $block) {
    switch (true) {
        case $block instanceof BetaMCPToolListingBlock:
            $toolNames = array_map(fn (BetaMCPTool $tool) => $tool->name, $block->tools);
            echo $block->mcpServerName, ' ', json_encode($toolNames), PHP_EOL;
            break;
        case $block instanceof BetaTextBlock:
            echo $block->text, PHP_EOL;
            break;
    }
}
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: Anthropic::Model::CLAUDE_OPUS_5_5,
  max_tokens: 1024,
  betas: [
    Anthropic::AnthropicBeta::INLINE_TOOLS_2026_09_15,
    Anthropic::AnthropicBeta::MCP_CLIENT_2026_09_15
  ],
  mcp_servers: [
    {
      type: "url",
      url: "https://mcp.example.com/calendar",
      name: "calendar",
      authorization_token: "YOUR_TOKEN"
    }
  ],
  tools: [
    {
      name: "get_weather",
      description: "Get the current weather for a location.",
      input_schema: {
        type: "object",
        properties: {
          location: { type: "string", description: "City name" }
        },
        required: ["location"]
      }
    }
  ],
  messages: [
    { role: "user", content: "What's on my calendar tomorrow?" },
    # Make the calendar server's tools available from this point onward.
    # The block names the server; it never holds a URL or token.
    {
      role: "system",
      content: [
        {
          type: "tool_addition",
          tool: {
            type: "tool_definition",
            definition: { type: "mcp_toolset", mcp_server_name: "calendar" }
          }
        }
      ]
    }
  ]
)

# The response starts with an mcp_tool_listing block for the calendar server,
# so check each block's type instead of reading content[0].
response.content.each do |block|
  case block
  when Anthropic::Beta::BetaMCPToolListingBlock
    puts "#{block.mcp_server_name} #{block.tools.map(&:name)}"
  when Anthropic::Beta::BetaTextBlock
    puts block.text
  end
end

mcp-client-2026-09-15로 API가 서버의 도구 목록을 가져온 응답은 서버별로 하나씩 mcp_tool_listing 블록으로 시작해요. 코드가 content[0]을 읽는다면 이 블록들을 건너뛰세요. 도우미 메시지를 이 블록을 포함해 그대로 다시 보내고, 그것을 실은 모든 요청에 mcp-client-2026-09-15를 계속 보내세요. 이후 요청들은 서버에 다시 묻는 대신 기록된 목록을 사용해요. 직접 도구셋을 고정하려면 MCP 서버의 도구 목록 고정에 설명된 대로 그 목록을 mcp_toolsettools 필드에 복사하세요.

mcp-client-2026-09-15mcp-client-2025-11-20이 하는 모든 것을 포함하므로 둘 다 보낼 필요가 없어요. 이 기능들은 Claude API에서 사용할 수 있어요. MCP 커넥터를 사용하는 요청은 그 데이터 보존 조건을 유지해요.

대화 중 시스템 메시지를 언제 쓸까

프롬프트 캐싱은 요청 접두사를 순서대로 해시해요. tools, system, messages 순으로요. 캐시 적중은 접두사가 최근 요청과 캐시 중단점까지 바이트 단위로 정확히 일치해야 해요.

그 순서는 최상위 system 필드가 해시된 접두사의 맨 앞근처에 앉는다는 뜻이에요. 그것을 조금이라도 바꾸면(문장 하나를 추가해도) 다른 해시가 생기고, 요청은 시스템 프롬프트와 그 뒤의 모든 캐시된 메시지에 대해 캐시를 놓쳐요.

대화 중 시스템 메시지는 지시를 메시지 기록의 에 추가하게 해 줘요. 새 지시 앞의 모든 것은 바뀌지 않으므로 기존 캐시 항목이 여전히 일치하고, 새 메시지만 새 입력으로 처리돼요.

중요한 몇 가지 상황:

  • 세션 중 정책이나 페르소나 변경. 긴 에이전틱 세션이 수십 개의 캐시된 턴을 지나 새 제약("이제부터 모든 SQL은 매개변수화된 쿼리로 작성해")을 필요로 해요. 최상위 system 필드에 추가하면 전체 기록을 재처리하게 돼요.
  • 권위가 있어야 하는 턴별 컨텍스트. 신선도 메모, 세션 마감, 도구 가용성 변경을 시스템 수준의 무게로 주입하고 싶은데, 캐시된 접두사에 살기엔 너무 자주 바뀌어요.
  • 쌓이지 말아야 할 턴별 알림. 하네스가 도구 결과 배치마다 모델을 살짝 덧대고 싶어요("독립적인 읽기는 함께 요청해", "사용자가 한동안 소식을 못 들었어"). 모델이 가장 새로운 사본만 보길 원해요. 턴 범위 시스템 메시지는 한 턴만 렌더링하고 그 후엔 비용이 없으며 기록에서 아무것도 지우지 않아요.
  • 애플리케이션이 관찰하는 상태 변경. 애플리케이션이 Claude가 운영자 수준 사실로 다뤄야 할 무언가를 알아차려요. 디스크의 파일 변경, 사용자의 자동 승인 설정 토글, 사용 가능한 도구 변경, 남은 토큰 예산이 임계값 아래로 떨어짐.
  • 에이전틱 루프를 방해해서는 안 되는 사용자 입력. Claude가 이전 요청의 도구를 아직 실행하는 동안 사용자가 후속 입력을 타이핑해요. 다음 도구 결과 뒤에 시스템 메시지로 전달하면, Claude가 새 입력을 전환할 새 요청으로 다루는 대신 이미 하고 있는 작업에 접어 넣을 수 있어요. 도구 결과 뒤 배치를 보세요.
  • 상설 권한을 부여하는 모드 전환. 세션 수준 모드가 대화 중 시스템 메시지로 값비싼 능력(예: 멀티에이전트 워크플로 자동 실행)에 대한 상설 동의를 부여하고, 몇 턴마다 짧은 새로고침과 모드가 꺼질 때의 종료 공지를 함께 쓰면 좋아요. 작업 예시는 조정 모드 구축을 보세요.

이 모든 경우에 일반 user 메시지에 지시를 넣을 수도 있고, Claude는 사용자 턴으로 오는 지시도 따르긴 해요. 차이는 우선순위예요. user 메시지는 최종 사용자에게서 온 것으로, system 메시지는 당신(애플리케이션 운영자)에게서 온 것으로 다뤄져요. 둘이 충돌하면 시스템 지시가 우선하므로, 운영자 수준의 사실과 제약에는 system 역할을 사용하세요. 최종 사용자가 다른 것을 요청해도 유지되어야 하는 것들요. 대화 중 시스템 메시지는 최상위 system 필드 편집의 캐시 미스 비용을 지불하지 않으면서 그 운영자 수준 우선순위를 유지해요.

동작 방식

messages 배열에 "role": "system" 메시지를 추가해요. content에는 일반 문자열이나 콘텐츠 블록을 user·assistant 턴과 같이 사용해요. 지시는 그 지점부터 대화에 적용돼요. 지시가 충돌하면 나중 시스템 메시지가 앞의 것보다 우선하고, 대화 중 시스템 메시지는 뒤따르는 턴들에 대해 최상위 system 필드보다 우선해요.

전체 대화에 적용되어야 할 지시는 여전히 최상위 system 필드를 설정할 수 있어요. 대화 중 시스템 메시지는 나중에 관련이 생기거나, 캐시된 접두사를 무효화하지 않고 추가하고 싶은 지시에 아껴 쓰세요.

role: "system" 메시지는 effort 수준을 다음 user 턴부터 바꾸는 output_config.effort도 실을 수 있어요. 이것은 Claude Fable 5.1, Claude Mythos 5.1, Claude Opus 5.5, Claude Opus 5에서 Claude API와 Google Cloud의 베타이며 mid-conversation-output-config-2026-07-01 베타 헤더가 필요해요. 메시지별 effort를 보세요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "content-type: application/json" \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -d '{ "model": "claude-opus-5-5", "max_tokens": 1024, "cache_control": {"type": "ephemeral"}, "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. For large inputs, consider a generator to avoid materializing the full list." }, { "role": "user", "content": "Now review the calling code that invokes process()." }, { "role": "system", "content": "From now on, every suggestion must include explicit type annotations." } ] }' ```
ant messages create --transform 'content.#(type=="text").text' --raw-output <<'YAML'
model: claude-opus-5-5
max_tokens: 1024
cache_control:
  type: ephemeral
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. For large inputs,
      consider a generator to avoid materializing the full list.
  - role: user
    content: Now review the calling code that invokes process().
  - role: system
    content: From now on, every suggestion must include explicit type annotations.
YAML
client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    # Automatic prompt caching: each request caches the conversation so far,
    # and the next request reads the unchanged prefix from cache.
    cache_control={"type": "ephemeral"},
    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. For large inputs, consider a generator to avoid materializing the full list.",
        },
        {
            "role": "user",
            "content": "Now review the calling code that invokes process().",
        },
        # The reviewer realizes mid-session that all suggestions must
        # also pass the team's strict typing policy. Appending the
        # instruction here keeps earlier turns byte-identical, so the
        # prefix cached by the previous request is still read from cache.
        {
            "role": "system",
            "content": "From now on, every suggestion must include explicit type annotations.",
        },
    ],
)

for block in response.content:
    if block.type == "text":
        print(block.text)
const client = new Anthropic();

const response = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  // Automatic prompt caching: each request caches the conversation so far,
  // and the next request reads the unchanged prefix from cache.
  cache_control: { type: "ephemeral" },
  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. For large inputs, consider a generator to avoid materializing the full list."
    },
    {
      role: "user",
      content: "Now review the calling code that invokes process()."
    },
    // The reviewer realizes mid-session that all suggestions must also pass
    // the team's strict typing policy. Appending the instruction here keeps
    // earlier turns byte-identical, so the prefix cached by the previous
    // request is still read from cache.
    {
      role: "system",
      content: "From now on, every suggestion must include explicit type annotations."
    }
  ]
});

const textBlock = response.content.find(
  (block): block is Anthropic.TextBlock => block.type === "text"
);
console.log(textBlock?.text);
AnthropicClient client = new();

var parameters = new MessageCreateParams
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    // Automatic prompt caching: each request caches the conversation so far,
    // and the next request reads the unchanged prefix from cache.
    CacheControl = new CacheControlEphemeral(),
    System = "You are a code review assistant. Be concise.",
    Messages =
    [
        new()
        {
            Role = Role.User,
            Content = "Review process() in utils.py for performance issues."
        },
        new()
        {
            Role = Role.Assistant,
            Content = "The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list."
        },
        new()
        {
            Role = Role.User,
            Content = "Now review the calling code that invokes process()."
        },
        // The reviewer realizes mid-session that all suggestions must also pass
        // the team's strict typing policy. Appending the instruction here keeps
        // earlier turns byte-identical, so the prefix cached by the previous
        // request is still read from cache.
        new()
        {
            Role = Role.System,
            Content = "From now on, every suggestion must include explicit type annotations."
        }
    ]
};

var response = await client.Messages.Create(parameters);
Console.WriteLine(response);
client := anthropic.NewClient()

response, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	// Automatic prompt caching: each request caches the conversation so far,
	// and the next request reads the unchanged prefix from cache.
	CacheControl: anthropic.NewCacheControlEphemeralParam(),
	System: []anthropic.TextBlockParam{
		{Text: "You are a code review assistant. Be concise."},
	},
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("Review process() in utils.py for performance issues.")),
		anthropic.NewAssistantMessage(anthropic.NewTextBlock("The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.")),
		anthropic.NewUserMessage(anthropic.NewTextBlock("Now review the calling code that invokes process().")),
		// The reviewer realizes mid-session that all suggestions must also
		// pass the team's strict typing policy. Appending the instruction
		// here keeps earlier turns byte-identical, so the prefix cached by
		// the previous request is still read from cache.
		{
			Role: anthropic.MessageParamRoleSystem,
			Content: []anthropic.ContentBlockParamUnion{
				anthropic.NewTextBlock("From now on, every suggestion must include explicit type annotations."),
			},
		},
	},
})
if err != nil {
	log.Fatal(err)
}
for _, block := range response.Content {
	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
		fmt.Println(textBlock.Text)
	}
}
import com.anthropic.models.messages.CacheControlEphemeral;
// ...
import com.anthropic.models.messages.MessageParam;
// ...
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    MessageCreateParams params = MessageCreateParams.builder()
        .model(Model.CLAUDE_OPUS_5_5)
        .maxTokens(1024)
        // Automatic prompt caching: each request caches the conversation so far,
        // and the next request reads the unchanged prefix from cache.
        .cacheControl(CacheControlEphemeral.builder().build())
        .system("You are a code review assistant. Be concise.")
        .addUserMessage("Review process() in utils.py for performance issues.")
        .addAssistantMessage("The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.")
        .addUserMessage("Now review the calling code that invokes process().")
        // The reviewer realizes mid-session that all suggestions must also pass
        // the team's strict typing policy. Appending the instruction here keeps
        // earlier turns byte-identical, so the prefix cached by the previous
        // request is still read from cache.
        .addMessage(MessageParam.builder()
            .role(MessageParam.Role.SYSTEM)
            .content("From now on, every suggestion must include explicit type annotations.")
            .build())
        .build();

    Message response = client.messages().create(params);
    response.content().stream()
        .flatMap(block -> block.text().stream())
        .forEach(textBlock -> IO.println(textBlock.text()));
use Anthropic\Messages\CacheControlEphemeral;
// ...
$client = new Client();

$response = $client->messages->create(
    maxTokens: 1024,
    messages: [
        ['role' => 'user', 'content' => 'Review process() in utils.py for performance issues.'],
        ['role' => 'assistant', 'content' => 'The list comprehension is fine for small inputs. For large inputs, consider a generator to avoid materializing the full list.'],
        ['role' => 'user', 'content' => 'Now review the calling code that invokes process().'],
        // The reviewer realizes mid-session that all suggestions must also pass
        // the team's strict typing policy. Appending the instruction here keeps
        // earlier turns byte-identical, so the prefix cached by the previous
        // request is still read from cache.
        ['role' => 'system', 'content' => 'From now on, every suggestion must include explicit type annotations.']
    ],
    model: 'claude-opus-5-5',
    // Automatic prompt caching: each request caches the conversation so far,
    // and the next request reads the unchanged prefix from cache.
    cacheControl: CacheControlEphemeral::with(),
    system: 'You are a code review assistant. Be concise.',
);

foreach ($response->content as $block) {
    if ($block->type === 'text') {
        echo $block->text, PHP_EOL;
    }
}
client = Anthropic::Client.new

response = client.messages.create(
  model: "claude-opus-5-5",
  max_tokens: 1024,
  # Automatic prompt caching: each request caches the conversation so far,
  # and the next request reads the unchanged prefix from cache.
  cache_control: { type: "ephemeral" },
  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. For large inputs, consider a generator to avoid materializing the full list." },
    { role: "user", content: "Now review the calling code that invokes process()." },
    # The reviewer realizes mid-session that all suggestions must also pass
    # the team's strict typing policy. Appending the instruction here keeps
    # earlier turns byte-identical, so the prefix cached by the previous
    # request is still read from cache.
    { role: "system", content: "From now on, every suggestion must include explicit type annotations." }
  ]
)

response.content.each do |block|
  puts block.text if block.type == :text
end

이 예시는 최상위 cache_control 필드로 자동 캐싱을 활성화해요. 프롬프트 캐싱은 옵트인이에요. 요청에 cache_control 필드(자동 또는 명시적 중단점)가 없으면 아무것도 캐시되지 않고 모든 요청이 전체 대화에 대해 일반 입력 토큰 가격을 지불해요. 캐싱을 켜면 시스템 메시지를 추가해도 이미 캐시된 턴이 바뀌지 않으므로, 새 지시를 실은 요청은 그것들을 다시 처리하는 대신 여전히 캐시에서 읽어요. 캐싱은 또한 대화가 최소 캐시 가능 프롬프트 길이를 충족해야 해요. 이렇게 짧은 예시는 그 아래이므로 대화가 커지기 전까지 cache_creation_input_tokenscache_read_input_tokens가 0으로 유지돼요.

대화 중 시스템 메시지는 user 턴(또는 서버 도구 결과로 끝나는 assistant 턴)을 반드시 바로 뒤따라야 하며, messages의 마지막 항목이거나 바로 다음에 assistant 턴이 와야 해요. tool_result 블록을 실은 user 메시지도 여기에 속해요. 에이전틱 루프에서는 도구 결과 바로 뒤, Claude의 다음 턴 앞에 시스템 메시지를 배치할 수 있어요. 그 외의 위치(assistanttool_use 블록과 그것에 답하는 tool_result 사이 포함)는 400 에러를 반환해요.

도구 결과 뒤 배치

에이전틱 루프에서는 시스템 메시지가 도구 결과를 전달하는 user 메시지 뒤에 와요. 여기가 애플리케이션이 Claude가 작업하는 동안 사용자가 타이핑한 입력을 전달할 수 있는 자리이기도 해요. 턴을 다시 시작하지 않고 새 컨텍스트가 흡수되도록요.

[
  { "role": "user", "content": "Run the test suite and fix any failures." },
  {
    "role": "assistant",
    "content": [{ "type": "tool_use", "id": "toolu_01", "name": "run_tests", "input": {} }]
  },
  {
    "role": "user",
    "content": [
      { "type": "tool_result", "tool_use_id": "toolu_01", "content": "12 passed, 0 failed" }
    ]
  },
  {
    "role": "system",
    "content": "The user sent the following message while you were working: also update the changelog before you finish."
  }
]

시스템 콘텐츠는 사용자를 덮어쓰는 명령이 아니라 컨텍스트로 둘러 말하세요. 사실을 진술하고("사용자에게서 새 입력이 도착했어: X", "남은 토큰 예산은 이제 Y") Claude가 행동하게 두세요. Claude는 사용자에게 반하는 것처럼 보이는 지시에 저항하도록 훈련됐고, 그 보호는 시스템 역할에도 여전히 적용되므로 "사용자가 말한 것은 무시해" 같은 표현은 무엇이 바뀌었는지 말하는 것보다 효과가 떨어져요.

이 패턴은 대화 자체의 최종 사용자 입력을 전달하기 위한 거예요. 도구 출력, 검색된 문서, 기타 제3자 콘텐츠를 전달하는 데 쓰지 마세요. 그런 콘텐츠는 tool_result 블록에 두세요(제한 사항 참조).

턴 범위 시스템 메시지

role: "system" 메시지를 현재 턴으로 한정하려면 clear_at 필드를 설정하세요. 두 값 중 하나를 받아요.

  • "never"(기본값): 메시지는 그것을 포함한 모든 요청에서 그 위치에 렌더링돼요. 필드를 생략하는 것과 동일해요.
  • "next_user_message": 메시지는 턴 범위가 돼요. messages에서 그 뒤에 role: "user" 메시지가 없을 때만 텍스트가 렌더링돼요. tool_result 블록만 실은 user 메시지도 여기서는 user 메시지로 칩니다. 이후 user 메시지가 생기면 메시지는 clear돼요. 배열에는 남지만 아무것도 렌더링하지 않고 입력 토큰을 소모하지 않아요. 그 요청과 이후 모든 요청에서요.

턴 범위 시스템 메시지는 베타예요. 베타 헤더 mid-conversation-system-clear-at-2026-08-21을 포함하세요. 없으면 clear_at이 알 수 없는 필드로 거부돼요.

{
  "role": "system",
  "clear_at": "next_user_message",
  "content": "First privately list what you need next; then request every item that doesn't depend on another's result in this one response."
}

주요 용도는 도구 루프의 턴별 알림이에요. 모델이 보길 원할 때마다 tool_result 메시지 뒤에 알림을 추가하고, 이전 사본들은 그 자리에 남겨두세요. 모델은 마지막 user 메시지 뒤에 오는 사본만 보므로 알림이 쌓이지 않아요. messages의 이전 것들은 변하지 않으므로 프롬프트 캐시가 계속 일치해요. Claude Fable 5.1과 Claude Opus 5.5에서는 이것이 이후 thinking 블록을 유효하게 유지하기도 해요. 이전 알림을 지우면 그 블록들 앞의 대화가 바뀌어 대화 검사를 실패하는데, cleared 메시지는 배열에 남아 그 대화를 바꾸지 않기 때문이에요.

다음 요청은 에이전트 루프의 뒷 단계예요. messages[3]은 배열의 마지막 메시지였던 이전 요청에서 렌더링됐어요. messages[5](이후 user 메시지)가 생기면 messages[3]은 cleared 돼요. cleared 메시지는 배열에 남으므로 messages[4]의 thinking 블록 앞 대화는 바뀌지 않지만, 모델은 더는 그 텍스트를 보지 못해요. messages[6]messages[7]은 둘 다 순서대로 렌더링돼요.

{
  "model": "claude-fable-5-1",
  "max_tokens": 16000,
  "messages": [
    { "role": "user", "content": "Fix the failing test." },
    {
      "role": "assistant",
      "content": [
        { "type": "thinking", "thinking": "", "signature": "..." },
        {
          "type": "tool_use",
          "id": "toolu_01",
          "name": "read_file",
          "input": { "path": "test_auth.py" }
        }
      ]
    },
    {
      "role": "user",
      "content": [{ "type": "tool_result", "tool_use_id": "toolu_01", "content": "..." }]
    },
    {
      "role": "system",
      "clear_at": "next_user_message",
      "content": "Request independent reads in one turn."
    },
    {
      "role": "assistant",
      "content": [
        { "type": "thinking", "thinking": "", "signature": "..." },
        {
          "type": "tool_use",
          "id": "toolu_02",
          "name": "read_file",
          "input": { "path": "auth.py" }
        },
        {
          "type": "tool_use",
          "id": "toolu_03",
          "name": "read_file",
          "input": { "path": "tokens.py" }
        }
      ]
    },
    {
      "role": "user",
      "content": [
        { "type": "tool_result", "tool_use_id": "toolu_02", "content": "..." },
        {
          "type": "tool_result",
          "tool_use_id": "toolu_03",
          "content": "...",
          "cache_control": { "type": "ephemeral" }
        }
      ]
    },
    {
      "role": "system",
      "clear_at": "next_user_message",
      "content": "Request independent reads in one turn."
    },
    {
      "role": "system",
      "clear_at": "next_user_message",
      "content": "The shell exited with status 137."
    }
  ]
}

턴 범위 메시지 규칙:

  • Cleared 메시지를 그대로 다시 보내세요. cleared 메시지는 여전히 대화 기록의 일부예요. 현재 상태(새 토큰 수, 타임스탬프)로 재구성하거나, 중복이라 버리거나, clear_at 값을 바꾸는 것은 이전 메시지를 편집하는 거예요. 그 지점부터 프롬프트 캐시가 빗나가고, Claude Fable 5.1과 Claude Opus 5.5에서는 그 뒤에 생성된 모든 thinking 블록이 대화 검사를 실패해요.
  • 텍스트만. content는 하나 이상의 text 블록(또는 문자열)이에요. tool_additiontool_removal 블록은 턴 범위 메시지에서 400 에러를 반환하고 output_config도 그래요. 그런 것들은 clear_at 없는 별도의 role: "system" 메시지를 사용하세요.
  • 그 블록에 cache_control 없음. cleared 메시지는 캐시 키의 일부가 되지 않으므로 그 위의 중단점은 절대 일치할 수 없어요. 예시처럼 중단점은 앞의 user 턴의 마지막 블록에 두세요. 최상위 자동 캐싱 필드는 중단점을 고를 때 턴 범위 메시지를 건너뛰어요. 메시지를 clear하는 요청에서 재사용 가능한 캐시된 접두사는 그 앞의 user 턴에서 끝나므로, 그 메시지와 새 user 메시지 사이의 assistant 턴 하나만 재처리돼요.
  • 배치 규칙은 계속 적용돼요, cleared든 아니든. 턴 범위 메시지는 user 턴(또는 서버 도구 결과로 끝나는 assistant 턴) 뒤에 와야 하고 assistant 턴 앞이나 배열 끝에 와야 해요. 다른 대화 중 시스템 메시지처럼요. 배열을 끝내는 것은 항상 렌더링돼요. 바로 뒤에 다른 user 메시지가 오는 것은 cleared 메시지가 아니라 400 에러예요. 도구 라운드의 결과를 모두 한 user 메시지에 넣고 알림은 그 뒤에 두세요.
  • Assistant 턴은 clear하지 않아요. 메시지 뒤의 프리필된 또는 일시 중지된 assistant 턴이나 서버 측 도구 루프는 user 메시지를 추가하지 않으므로, 그 연속에서 메시지는 여전히 렌더링돼요. 클라이언트 측 도구 루프를 통해 알림을 보이게 하려면 각 tool_result 메시지 뒤에 다시 추가하세요.
  • 토큰 계산은 렌더링되는 것을 따릅니다. cleared 메시지는 usage.input_tokens토큰 수에 아무것도 추가하지 않아요.
  • 가져온 기록. 한 단계로 구성한 기록(few-shot 예시, 이전 대화)에서, 이미 뒤에 assistant 턴과 user 메시지가 있는 턴 범위 메시지는 첫 요청부터 cleared 되고 절대 렌더링되지 않아요. 이월하는 턴별 알림에는 그 상태가 맞아요. 모델이 모든 요청에서 봐야 하는 메시지에만 clear_at을 설정하지 마세요.

검증 에러는:

messages.3.clear_at: Extra inputs are not permitted
messages.3.clear_at: clear_at is only permitted on role 'system' messages
messages.3.clear_at: Input should be 'next_user_message' or 'never'
messages.3: a turn-scoped system message supports text blocks only (clear_at: 'next_user_message')
messages.3: output_config is not permitted on a turn-scoped system message (clear_at: 'next_user_message')
messages.3.content.0: cache_control is not permitted on a turn-scoped system message (clear_at: 'next_user_message')

첫 번째는 베타 헤더 없이 반환되는 에러예요. Amazon Bedrock과 Google Cloud에서는 베타 헤더에 설명된 대로 베타 값을 전달하세요.

SDK를 통해서는 messagesrole: "system" 항목에 clear_at을 설정하고 베타 헤더를 보내세요. 다음 예시는 user 턴 뒤에 턴 범위 알림을 추가해요. 다음 요청에서 이후 user 메시지가 생기면 알림은 배열에 남지만 더는 렌더링되지 않아요.

```bash cURL curl https://api.anthropic.com/v1/messages \ -H "x-api-key: $ANTHR...KEY" \ -H "anthropic-version: 2023-06-01" \ -H "anthropic-beta: mid-conversation-system-clear-at-2026-08-21" \ -H "content-type: application/json" \ -d '{ "model": "claude-fable-5-1", "max_tokens": 4096, "messages": [ {"role": "user", "content": "Draft a short status update on the database migration for the team channel."}, {"role": "system", "clear_at": "next_user_message", "content": "The reader is on call: keep this reply under 50 words."} ] }' ```
ant beta:messages create --beta mid-conversation-system-clear-at-2026-08-21 \
  --transform 'content.#(type=="text").text' --raw-output <<'YAML'
model: claude-fable-5-1
max_tokens: 4096
messages:
  - role: user
    content: Draft a short status update on the database migration for the team channel.
  # Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
  - role: system
    clear_at: next_user_message
    content: "The reader is on call: keep this reply under 50 words."
YAML
client = anthropic.Anthropic()

response = client.beta.messages.create(
    model="claude-fable-5-1",
    max_tokens=4096,
    messages=[
        {
            "role": "user",
            "content": "Draft a short status update on the database migration for the team channel.",
        },
        # Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
        {
            "role": "system",
            "clear_at": "next_user_message",
            "content": "The reader is on call: keep this reply under 50 words.",
        },
    ],
    betas=["mid-conversation-system-clear-at-2026-08-21"],
)

for block in response.content:
    if block.type == "text":
        print(block.text)
const client = new Anthropic();

const response = await client.beta.messages.create({
  model: "claude-fable-5-1",
  max_tokens: 4096,
  messages: [
    {
      role: "user",
      content: "Draft a short status update on the database migration for the team channel."
    },
    // Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
    {
      role: "system",
      clear_at: "next_user_message",
      content: "The reader is on call: keep this reply under 50 words."
    }
  ],
  betas: ["mid-conversation-system-clear-at-2026-08-21"]
});

for (const block of response.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Messages;

AnthropicClient client = new();

var response = await client.Beta.Messages.Create(new MessageCreateParams
{
    Model = "claude-fable-5-1",
    MaxTokens = 4096,
    Messages =
    [
        new() { Role = Role.User, Content = "Draft a short status update on the database migration for the team channel." },
        // Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
        new()
        {
            Role = Role.System,
            ClearAt = ClearAt.NextUserMessage,
            Content = "The reader is on call: keep this reply under 50 words.",
        },
    ],
    Betas = [AnthropicBeta.MidConversationSystemClearAt2026_08_21],
});

foreach (var block in response.Content)
{
    if (block.TryPickText(out var textBlock))
    {
        Console.WriteLine(textBlock.Text);
    }
}
client := anthropic.NewClient()

response, err := client.Beta.Messages.New(context.Background(), anthropic.BetaMessageNewParams{
	Model:     "claude-fable-5-1",
	MaxTokens: 4096,
	Messages: []anthropic.BetaMessageParam{
		anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Draft a short status update on the database migration for the team channel.")),
		// Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
		{
			Role:    anthropic.BetaMessageParamRoleSystem,
			ClearAt: anthropic.BetaMessageParamClearAtNextUserMessage,
			Content: []anthropic.BetaContentBlockParamUnion{anthropic.NewBetaTextBlock("The reader is on call: keep this reply under 50 words.")},
		},
	},
	Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaMidConversationSystemClearAt2026_08_21},
})
if err != nil {
	log.Fatal(err)
}

for _, block := range response.Content {
	if textBlock, ok := block.AsAny().(anthropic.BetaTextBlock); ok {
		fmt.Println(textBlock.Text)
	}
}
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaMessage;
import com.anthropic.models.beta.messages.BetaMessageParam;
import com.anthropic.models.beta.messages.MessageCreateParams;

void main() {
    AnthropicClient client = AnthropicOkHttpClient.fromEnv();

    MessageCreateParams params = MessageCreateParams.builder()
        .model("claude-fable-5-1")
        .maxTokens(4096L)
        .addBeta(AnthropicBeta.MID_CONVERSATION_SYSTEM_CLEAR_AT_2026_08_21)
        .addUserMessage("Draft a short status update on the database migration for the team channel.")
        // Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
        .addMessage(BetaMessageParam.builder()
            .role(BetaMessageParam.Role.SYSTEM)
            .clearAt(BetaMessageParam.ClearAt.NEXT_USER_MESSAGE)
            .content("The reader is on call: keep this reply under 50 words.")
            .build())
        .build();

    BetaMessage response = client.beta().messages().create(params);
    response.content().stream()
        .flatMap(block -> block.text().stream())
        .forEach(textBlock -> IO.println(textBlock.text()));
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaMessageParam;
use Anthropic\Client;

$client = new Client();

$response = $client->beta->messages->create(
    model: 'claude-fable-5-1',
    maxTokens: 4096,
    messages: [
        BetaMessageParam::with(role: 'user', content: 'Draft a short status update on the database migration for the team channel.'),
        // Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
        BetaMessageParam::with(
            role: 'system',
            clearAt: 'next_user_message',
            content: 'The reader is on call: keep this reply under 50 words.',
        ),
    ],
    betas: [AnthropicBeta::MID_CONVERSATION_SYSTEM_CLEAR_AT_2026_08_21],
);

foreach ($response->content as $block) {
    if ($block->type === 'text') {
        echo $block->text, PHP_EOL;
    }
}
client = Anthropic::Client.new

response = client.beta.messages.create(
  model: "claude-fable-5-1",
  max_tokens: 4096,
  messages: [
    {role: "user", content: "Draft a short status update on the database migration for the team channel."},
    # Turn-scoped reminder: renders for this turn, then clears once a later user message exists.
    {role: "system", clear_at: :next_user_message, content: "The reader is on call: keep this reply under 50 words."}
  ],
  betas: [Anthropic::AnthropicBeta::MID_CONVERSATION_SYSTEM_CLEAR_AT_2026_08_21]
)

response.content.each do |block|
  puts block.text if block.type == :text
end

프롬프트 캐싱과 함께 사용

대화 중 시스템 메시지와 프롬프트 캐싱은 함께 쓰도록 설계됐어요.

  • 캐싱을 명시적으로 켜세요. 캐싱은 요청에 cache_control이 있을 때만 일어나요. 최상위 자동 캐싱 필드나 콘텐츠 블록의 명시적 중단점으로요. 대화 중 시스템 메시지는 자체적으로 캐시 항목을 만들지 않으며, 캐싱을 켜지 않으면 보존할 절감도 없어요.
  • 안정적인 접두사를 평소처럼 캐시하세요. 요청 사이에 일정하게 유지되는 마지막 블록에 cache_control을 두세요. 최상위 system 필드의 끝이든, 도구 정의의 끝이든, 메시지 기록의 안정 지점이든요.
  • 중단점 뒤에 시스템 메시지를 추가하세요. 캐시된 접두사 뒤에 오므로 접두사 해시를 바꾸지 않고 캐시가 여전히 적중해요.
  • 대화 중 시스템 메시지 자체도 캐시 가능해요. 대화에 들어가면 안정적인 기록의 일부가 돼요. 다음 턴에서 캐시 중단점을 그것 너머로 옮기면(자동 캐싱에 맡겨도 되고) 시스템 메시지도 다른 턴처럼 캐시에서 읽혀요.

이미 보낸 대화 중 시스템 메시지를 편집하거나 제거하지 마세요. 다른 이전 메시지 변경처럼 그 지점부터 캐시를 무효화해요. Claude Fable 5.1과 Claude Opus 5.5에서는 이후 모든 assistant 턴의 thinking 블록도 무효화해요. 한 턴에만 적용되는 안내에는 턴 범위 시스템 메시지를 사용하고 그 자리에 남겨두세요. 지시가 진화해야 하면 옛것을 다시 쓰지 말고 새 시스템 메시지를 추가하세요. 연속된 시스템 메시지는 허용되고 단일 시스템 섹션으로 다뤄지며, 전체가 같은 배치 규칙을 따라요.

제한 사항

  • 첫 메시지로는 안 됨. 콘텐츠를 실은 system 메시지는 messages의 첫 항목이 될 수 없어요. 처음부터 적용되는 지시에는 최상위 system 필드를 사용하세요.
  • 배치가 제약됨. 콘텐츠(text, tool_addition, tool_removal 블록)를 실은 system 메시지는 user 턴(tool_result 블록을 실은 user 턴 포함)이나 서버 도구 결과로 끝나는 assistant 턴을 반드시 바로 뒤따라야 하고, assistant 턴 앞이나 배열 끝에 와야 해요. tool_use 블록과 그것에 답하는 tool_result 사이에는 앉을 수 없어요. 다른 곳에 두면 400 에러를 반환해요. 한 가지 예외: tool_additiontool_removal 블록은 일시 중지된 assistant 턴(서버 도구 결과로 끝나는) 바로 뒤에는 허용되지 않지만, text 블록은 허용돼요. 일시 중지된 턴을 먼저 이어가고 다음 system 메시지에서 도구 변경을 보내세요. 빈 contentoutput_config.effort만 설정하는 메시지는 그 위치에 아무것도 렌더링하지 않으며 messages 어디든 허용돼요. 첫 번째나 assistant 턴과 user 턴 사이도요. 연속된 system 메시지는 함께 판단되므로, effort 전용 메시지 옆에 텍스트를 실은 메시지를 추가하면 전체 그룹이 콘텐츠 규칙을 따르게 돼요.
  • 턴 범위 메시지는 텍스트 전용이고 그대로 다시 보내집니다. clear_at: "next_user_message" 메시지는 tool_addition, tool_removal, output_config, cache_control을 실지 않으며, 한번 cleared 되면 이후 요청에서 messages에 바이트 단위로 남아 있어야 해요. 턴 범위 시스템 메시지를 보세요.
  • 신뢰할 수 없는 콘텐츠의 자리가 아님. Claude는 시스템 콘텐츠를 운영자 지시로 여기고 따르고 있어요. 대화 밖의 텍스트(원시 도구 출력, 검색된 문서, 웹 콘텐츠)를 시스템 메시지에 직접 두지 마세요. 그 텍스트에 운영자 수준의 권위를 주는 것이기 때문이에요. 그 데이터는 tool_result 블록에 두고 탈옥과 프롬프트 주입 완화를 계속 따르세요.

더 알아보기 (Learn more)