요청 시 컴팩션
요청 시 컴팩션 (Compaction on demand)
요청 시 컴팩션(on-demand compaction)을 쓰면 애플리케이션이 직접 대화 요약 시점을 정할 수 있어요. compaction 매개변수를 담은 요청을 한 번 보내면 Claude가 답변 대신 요약을 반환해요. 요약은 읽을 수 있는 텍스트와 서명(signature)을 담은 단일 compaction 블록으로 주어져요.
출처: 문서
본문
요청 시 컴팩션으로는 애플리케이션이 대화를 언제 요약할지 정해요. compaction 매개변수를 담은 요청을 보내면 Claude가 답변 대신 요약을 반환해요.
요청 시 컴팩션 동작 방식
컴팩션 요청은 대화 턴과는 별개예요. 현재 상태의 대화를 compaction 매개변수와 함께 보내면 응답에 단일 compaction 블록이 담겨요. 블록은 읽을 수 있는 텍스트로 된 요약과 서명을 담아요. 이후 요청에는 받은 그대로 보내세요.
그때부터 블록은 그것이 요약한 메시지들의 자리를 차지해요. messages에서 맨 앞으로 가고, 요약된 메시지들은 제거되며, 다음 턴이 뒤따라요. Claude는 요약된 메시지들이 있던 자리에 요약을 보게 돼요.
요약 요청하기
요약을 요청하는 요청과 서명된 블록을 실은 이후의 모든 요청에 compact-2026-09-04 베타 헤더를 보내요. 모델이 요청 시 컴팩션을 지원하는지 확인하려면 베타 헤더와 함께 Models API를 호출하고 각 모델의 capabilities.compaction을 읽어보세요. compaction은 한 요청에서 context_management와 함께 쓸 수 없어요.
현재 상태의 대화를 "compaction": {"type": "summarize"}와 함께 보내요. API는 요청 안의 모든 메시지를 한 번 요약하고, 이후 답변을 생성하지 않으며, stop_reason "compaction"과 함께 블록만 반환해요. 대화의 나머지에서 쓰는 것과 같은 system 프롬프트와 tools를 보내세요. 요약기는 그것들을 읽고, 보존 thinking 모델에서 블록 뒤에 턴을 유지한다면 그 턴의 thinking은 system과 tools가 일치할 때만 유효하게 남아요. 이 예시 대화는 system 프롬프트나 도구가 없으므로 요청도 그것들을 보내지 않아요.
ant beta:messages create --beta compact-2026-09-04 <<'YAML'
model: claude-opus-5-5
# max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
max_tokens: 4096
messages:
- role: user
content: I am building a recipe app. Help me name the main entities in the data model.
- role: assistant
content: Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe.
- role: user
content: Good. Now suggest field names for Recipe.
compaction:
type: summarize
YAML
from anthropic.types.beta import BetaMessageParam
client = anthropic.Anthropic()
history: list[BetaMessageParam] = [
{
"role": "user",
"content": "I am building a recipe app. Help me name the main entities in the data model.",
},
{
"role": "assistant",
"content": "Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe.",
},
{"role": "user", "content": "Good. Now suggest field names for Recipe."},
]
response = client.beta.messages.create(
model="claude-opus-5-5",
# max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
max_tokens=4096,
betas=["compact-2026-09-04"],
messages=history,
compaction={"type": "summarize"},
)
print(f"Stop reason: {response.stop_reason}")
const client = new Anthropic();
const history: Anthropic.Beta.Messages.BetaMessageParam[] = [
{
role: "user",
content: "I am building a recipe app. Help me name the main entities in the data model."
},
{
role: "assistant",
content:
"Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe."
},
{ role: "user", content: "Good. Now suggest field names for Recipe." }
];
const response = await client.beta.messages.create({
model: "claude-opus-5-5",
// max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
max_tokens: 4096,
betas: ["compact-2026-09-04"],
messages: history,
compaction: { type: "summarize" }
});
console.log(`Stop reason: ${response.stop_reason}`);
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Messages;
using Model = Anthropic.Models.Messages.Model;
AnthropicClient client = new();
List<BetaMessageParam> history =
[
new()
{
Role = Role.User,
Content = "I am building a recipe app. Help me name the main entities in the data model.",
},
new()
{
Role = Role.Assistant,
Content = "Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe.",
},
new() { Role = Role.User, Content = "Good. Now suggest field names for Recipe." },
];
var response = await client.Beta.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
// max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
MaxTokens = 4096,
Betas = [AnthropicBeta.Compact2026_09_04],
Messages = history,
Compaction = new BetaCompactionConfig(), // type defaults to "summarize"
});
Console.WriteLine($"Stop reason: {response.StopReason?.Raw()}");
client := anthropic.NewClient()
history := []anthropic.BetaMessageParam{
anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("I am building a recipe app. Help me name the main entities in the data model.")),
{
Role: anthropic.BetaMessageParamRoleAssistant,
Content: []anthropic.BetaContentBlockParamUnion{anthropic.NewBetaTextBlock("Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe.")},
},
anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock("Good. Now suggest field names for Recipe.")),
}
response, err := client.Beta.Messages.New(context.TODO(), anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
// max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
MaxTokens: 4096,
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCompact2026_09_04},
Messages: history,
Compaction: anthropic.BetaCompactionConfigUnionParam{
OfSummarize: &anthropic.BetaSummarizeCompactionParam{},
},
})
if err != nil {
log.Fatal(err)
}
fmt.Println("Stop reason:", response.StopReason)
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaCompactionConfig;
import com.anthropic.models.beta.messages.MessageCreateParams;
void main() {
var client = AnthropicOkHttpClient.fromEnv();
var params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
// max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
.maxTokens(4096)
.addBeta(AnthropicBeta.COMPACT_2026_09_04)
.addUserMessage("I am building a recipe app. Help me name the main entities in the data model.")
.addAssistantMessage("Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe.")
.addUserMessage("Good. Now suggest field names for Recipe.")
.compaction(BetaCompactionConfig.builder().build()) // type defaults to "summarize"
.build();
var response = client.beta().messages().create(params);
response.stopReason().ifPresent(reason -> IO.println("Stop reason: " + reason));
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaCompactionConfig;
use Anthropic\Beta\Messages\BetaMessageParam;
use Anthropic\Beta\Messages\BetaMessageParam\Role;
$client = new Client();
$history = [
BetaMessageParam::with(
role: Role::USER,
content: 'I am building a recipe app. Help me name the main entities in the data model.',
),
BetaMessageParam::with(
role: Role::ASSISTANT,
content: 'Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe.',
),
BetaMessageParam::with(role: Role::USER, content: 'Good. Now suggest field names for Recipe.'),
];
$response = $client->beta->messages->create(
model: Model::CLAUDE_OPUS_5_5,
// max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
maxTokens: 4096,
betas: [AnthropicBeta::COMPACT_2026_09_04],
messages: $history,
compaction: BetaCompactionConfig::with(), // type defaults to 'summarize'
);
echo "Stop reason: {$response->stopReason}", PHP_EOL;
client = Anthropic::Client.new
history = [
{
role: "user",
content: "I am building a recipe app. Help me name the main entities in the data model."
},
{
role: "assistant",
content: "Start with Recipe, Ingredient, and Step. Add a RecipeIngredient entry that holds the quantity and unit for each ingredient in a recipe."
},
{ role: "user", content: "Good. Now suggest field names for Recipe." }
]
response = client.beta.messages.create(
model: Anthropic::Model::CLAUDE_OPUS_5_5,
# max_tokens caps the whole call, including any thinking, so allow several thousand tokens.
max_tokens: 4096,
betas: [Anthropic::AnthropicBeta::COMPACT_2026_09_04],
messages: history,
compaction: { type: "summarize" }
)
puts "Stop reason: #{response.stop_reason}"
{
"id": "msg_013Zva2CMHLNnXjNJJKqJ2EF",
"type": "message",
"role": "assistant",
"model": "claude-opus-5-5",
"content": [
{
"type": "compaction",
"content": "Summary of the conversation: the user is designing the data model for a recipe app. The entities agreed so far are Recipe, Ingredient, Step, and RecipeIngredient, which holds the quantity and unit. The user then asked for field names for Recipe.",
"signature": "EuYBCkQY..."
}
],
"stop_reason": "compaction",
"usage": {
"input_tokens": 0,
"output_tokens": 0,
"iterations": [{ "type": "compaction", "input_tokens": 144, "output_tokens": 276 }]
}
}
요약화 호출은 요청의 모델, system, tools, thinking 설정, max_tokens를 사용해요. 요약기는 도구 정의를 읽지만 도구를 실행하진 않으며, 응답에는 thinking이 담기지 않아요. max_tokens는 모델이 요약을 쓰기 전에 하는 thinking을 포함한 전체 호출을 제한하므로 수천 토큰을 허용하세요. 이 호출이 어떻게 청구되는지는 Count compaction usage를 보세요.
마지막 assistant 턴이 아직 결과가 없는 도구 호출로 끝나면 API가 요청을 거부해요. 그 턴의 도구 결과를 먼저 보내세요. 또한 stop_sequences, 구조화 출력 output_config.format, 그리고 any 또는 tool 유형의 tool_choice도 빼세요. 그것들은 요약화 호출에서 아무 역할도 하지 않으며 API가 거부해요. 대화는 여전히 모델의 컨텍스트 창에 맞아야 하므로, 초과하기 전에 컴팩션하세요. 초과한 뒤에는 안 돼요.
응답을 스트리밍하면 블록이 통째로 도착해요. 완전한 블록을 담은 content_block_start 이벤트 하나와 content_block_stop이 오고, content_block_delta 이벤트는 없어요. ping 이벤트는 그 앞이나 사이에 도착할 수 있어요.
요약에서 이어가기
기록에서 보낸 메시지를 반환된 도우미 메시지로 바꾸세요. compaction 블록은 signature를 포함해 API가 반환한 그대로 유지하세요. 컴팩션 요청을 보낸 뒤에 이뤄진 턴들은 블록 뒤에 그대로 이어져요. 이것이 Compaction in the background가 기반으로 하는 방식이에요. 이후의 모든 요청에 베타 헤더와 함께 블록을 맨 앞에 보내세요.
{
"model": "claude-opus-5-5",
"max_tokens": 2048,
"messages": [
{
"role": "assistant",
"content": [
{
"type": "compaction",
"content": "Summary of the conversation: the user is designing the data model for a recipe app. The entities agreed so far are Recipe, Ingredient, Step, and RecipeIngredient, which holds the quantity and unit. The user then asked for field names for Recipe.",
"signature": "EuYBCkQY..."
}
]
},
{
"role": "assistant",
"content": "For Recipe, use title, description, servings, prep_minutes, and cook_minutes. Add created_at and updated_at timestamps."
},
{ "role": "user", "content": "Now do the same for Ingredient." }
]
}
이 예시는 user 턴으로 끝난 요청 샘플을 이어가요. 다이어그램은 요약이 쓰이는 동안 턴이 없었던 더 단순한 경우를 보여줘요. 여기서 두 번째 assistant 메시지는 요약된 마지막 user 턴에 대한 답변이에요. 요약이 쓰이는 동안 도착했으므로 요약된 메시지들에 포함되지 않았어요. 블록이 여전히 맨 앞에 오므로 여기서 assistant 메시지가 연달아 오는 건 괜찮아요.
API는 블록이 서 있는 자리에 요약을 두고 이후의 모든 메시지를 Claude에 그대로 전달해요. 다음 규칙을 따르세요.
- 블록을
messages맨 앞에 두세요. 그것 자체가assistant메시지이거나, 첫 메시지(user든assistant든)의 첫 콘텐츠 블록으로요. - 요약된 메시지를 제거하세요. 블록 앞에 남아 있으면 요청이 400 에러(
compaction_block_misplaced)를 반환해요. - 이후의 모든 요청에서 정확히 하나의
compaction블록을 보내세요.
임계값 컴팩션은 반대로 동작해요. 블록은 요약하는 메시지들을 뒤따르고, API가 그것들을 대신 제거해줘요. Passing compaction blocks back를 보세요.
Python에서는 이 페이지의 샘플처럼 client.beta.messages를 사용하세요. client.messages를 호출하고 블록을 직접 직렬화한다면 to_dict()나 model_dump(exclude_none=True)를 쓰세요. 단순 model_dump()는 블록에 citations: null과 text: null을 추가해서 API가 거부해요.
블록 뒤에 턴을 유지하고 그 thinking 블록을 다시 보낸다면, 그 thinking을 유효하게 유지하는 조건은 Compaction and preserved thinking에 있어요.
다시 컴팩션하기
이미 블록으로 시작하는 대화를 컴팩션하려면 compaction을 다시 보내요. 새 블록은 이전 요약과 그 뒤의 모든 것을 요약해요. 이후부터는 가장 새로운 블록만 보내면 돼요.
루프 안에서 컴팩션하기
각 턴 후에 루프는 마지막 응답의 입력·출력 토큰을 더해요. 다음 요청도 그 답변을 보내기 때문이에요. 그 합계가 한도를 넘고 아직 올 턴이 있다면 같은 모델과 system 프롬프트로 컴팩션 요청을 보내고, stop_reason을 확인하며, 기록을 반환된 메시지로 바꾸고, 이전에 컴팩션한 턴을 출력해요. 샘플의 2,500 토큰 한도는 의도적으로 낮아서 짧은 대화도 컴팩션되게 해요. 실제 입력 예산 근처로 설정하세요.
client = anthropic.Anthropic()
Set this near your real input budget. It is low here so a short conversation compacts.
COMPACT_AT_TOKENS = 2500 SYSTEM = "You help design a recipe app's data model. Keep answers short."
QUESTIONS = [ "What are the main entities in the data model?", "Which fields should Recipe have?", "Which fields should Ingredient have?", "Which fields should RecipeIngredient have?", "Which fields should Step have?", "Which indexes should these tables have?", "Which fields should be required?", "Which fields should have default values?", ]
history: list[BetaMessageParam] = [] for turn, question in enumerate(QUESTIONS, start=1): history.append({"role": "user", "content": question}) response = client.beta.messages.create( model="claude-opus-5-5", max_tokens=8192, system=SYSTEM, betas=["compact-2026-09-04"], messages=history, ) history.append({"role": "assistant", "content": response.content})
# The next request sends this reply too, so count it.
conversation_tokens = response.usage.input_tokens + response.usage.output_tokens
if conversation_tokens > COMPACT_AT_TOKENS and turn < len(QUESTIONS):
summary = client.beta.messages.create(
model="claude-opus-5-5",
max_tokens=4096,
system=SYSTEM,
betas=["compact-2026-09-04"],
messages=history,
compaction={"type": "summarize"},
)
if summary.stop_reason == "compaction":
history = [{"role": "assistant", "content": summary.content}]
print(f"Compacted before turn {turn + 1}")
```typescript TypeScript
const client = new Anthropic();
// Set this near your real input budget. It is low here so a short conversation compacts.
const compactAtTokens = 2500;
const systemPrompt = "You help design a recipe app's data model. Keep answers short.";
const questions = [
"What are the main entities in the data model?",
"Which fields should Recipe have?",
"Which fields should Ingredient have?",
"Which fields should RecipeIngredient have?",
"Which fields should Step have?",
"Which indexes should these tables have?",
"Which fields should be required?",
"Which fields should have default values?"
];
let history: Anthropic.Beta.Messages.BetaMessageParam[] = [];
for (const [index, question] of questions.entries()) {
const turn = index + 1;
history.push({ role: "user", content: question });
const response = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 8192,
system: systemPrompt,
betas: ["compact-2026-09-04"],
messages: history
});
history.push({ role: "assistant", content: response.content });
// The next request sends this reply too, so count it.
const conversationTokens = response.usage.input_tokens + response.usage.output_tokens;
if (conversationTokens > compactAtTokens && turn < questions.length) {
const summary = await client.beta.messages.create({
model: "claude-opus-5-5",
max_tokens: 4096,
system: systemPrompt,
betas: ["compact-2026-09-04"],
messages: history,
compaction: { type: "summarize" }
});
if (summary.stop_reason === "compaction") {
history = [{ role: "assistant", content: summary.content }];
console.log(`Compacted before turn ${turn + 1}`);
}
}
}
using Anthropic.Models.Beta;
using Anthropic.Models.Beta.Messages;
using Model = Anthropic.Models.Messages.Model;
AnthropicClient client = new();
// Set this near your real input budget. It is low here so a short conversation compacts.
const int CompactAtTokens = 2500;
const string SystemPrompt = "You help design a recipe app's data model. Keep answers short.";
string[] questions =
[
"What are the main entities in the data model?",
"Which fields should Recipe have?",
"Which fields should Ingredient have?",
"Which fields should RecipeIngredient have?",
"Which fields should Step have?",
"Which indexes should these tables have?",
"Which fields should be required?",
"Which fields should have default values?",
];
List<BetaMessageParam> history = [];
foreach (var (index, question) in questions.Index())
{
var turn = index + 1;
history.Add(new() { Role = Role.User, Content = question });
var response = await client.Beta.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 8192,
System = SystemPrompt,
Betas = [AnthropicBeta.Compact2026_09_04],
Messages = history,
});
history.Add(new()
{
Role = Role.Assistant,
Content = response.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
});
// The next request sends this reply too, so count it.
var conversationTokens = response.Usage.InputTokens + response.Usage.OutputTokens;
if (conversationTokens > CompactAtTokens && turn < questions.Length)
{
var summary = await client.Beta.Messages.Create(new MessageCreateParams
{
Model = Model.ClaudeOpus5_5,
MaxTokens = 4096,
System = SystemPrompt,
Betas = [AnthropicBeta.Compact2026_09_04],
Messages = history,
Compaction = new BetaCompactionConfig(), // type defaults to "summarize"
});
if (summary.StopReason == BetaStopReason.Compaction)
{
history =
[
new()
{
Role = Role.Assistant,
Content = summary.Content.Select(block => new BetaContentBlockParam(block.Json)).ToList(),
},
];
Console.WriteLine($"Compacted before turn {turn + 1}");
}
}
}
ctx := context.Background()
client := anthropic.NewClient()
// Set this near your real input budget. It is low here so a short conversation compacts.
const compactAtTokens = 2500
system := []anthropic.BetaTextBlockParam{{Text: "You help design a recipe app's data model. Keep answers short."}}
questions := []string{
"What are the main entities in the data model?",
"Which fields should Recipe have?",
"Which fields should Ingredient have?",
"Which fields should RecipeIngredient have?",
"Which fields should Step have?",
"Which indexes should these tables have?",
"Which fields should be required?",
"Which fields should have default values?",
}
var history []anthropic.BetaMessageParam
for i, question := range questions {
turn := i + 1
history = append(history, anthropic.NewBetaUserMessage(anthropic.NewBetaTextBlock(question)))
response, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 8192,
System: system,
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCompact2026_09_04},
Messages: history,
})
if err != nil {
log.Fatal(err)
}
history = append(history, response.ToParam())
// The next request sends this reply too, so count it.
conversationTokens := response.Usage.InputTokens + response.Usage.OutputTokens
if conversationTokens > compactAtTokens && turn < len(questions) {
summary, err := client.Beta.Messages.New(ctx, anthropic.BetaMessageNewParams{
Model: anthropic.ModelClaudeOpus5_5,
MaxTokens: 4096,
System: system,
Betas: []anthropic.AnthropicBeta{anthropic.AnthropicBetaCompact2026_09_04},
Messages: history,
Compaction: anthropic.BetaCompactionConfigUnionParam{
OfSummarize: &anthropic.BetaSummarizeCompactionParam{},
},
})
if err != nil {
log.Fatal(err)
}
if summary.StopReason == anthropic.BetaStopReasonCompaction {
history = []anthropic.BetaMessageParam{summary.ToParam()}
fmt.Printf("Compacted before turn %d\n", turn+1)
}
}
}
import com.anthropic.models.beta.AnthropicBeta;
import com.anthropic.models.beta.messages.BetaCompactionConfig;
import com.anthropic.models.beta.messages.BetaMessageParam;
import com.anthropic.models.beta.messages.BetaStopReason;
import com.anthropic.models.beta.messages.MessageCreateParams;
// Set this near your real input budget. It is low here so a short conversation compacts.
static final long COMPACT_AT_TOKENS = 2500;
static final String SYSTEM = "You help design a recipe app's data model. Keep answers short.";
void main() {
var client = AnthropicOkHttpClient.fromEnv();
var questions = List.of(
"What are the main entities in the data model?",
"Which fields should Recipe have?",
"Which fields should Ingredient have?",
"Which fields should RecipeIngredient have?",
"Which fields should Step have?",
"Which indexes should these tables have?",
"Which fields should be required?",
"Which fields should have default values?"
);
var history = new ArrayList<BetaMessageParam>();
for (int turn = 1; turn <= questions.size(); turn++) {
history.add(BetaMessageParam.builder()
.role(BetaMessageParam.Role.USER)
.content(questions.get(turn - 1))
.build());
var params = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(8192)
.system(SYSTEM)
.addBeta(AnthropicBeta.COMPACT_2026_09_04)
.messages(history)
.build();
var response = client.beta().messages().create(params);
history.add(response.toParam());
// The next request sends this reply too, so count it.
long conversationTokens = response.usage().inputTokens() + response.usage().outputTokens();
if (conversationTokens > COMPACT_AT_TOKENS && turn < questions.size()) {
var summaryParams = MessageCreateParams.builder()
.model(Model.CLAUDE_OPUS_5_5)
.maxTokens(4096)
.system(SYSTEM)
.addBeta(AnthropicBeta.COMPACT_2026_09_04)
.messages(history)
.compaction(BetaCompactionConfig.builder().build()) // type defaults to "summarize"
.build();
var summary = client.beta().messages().create(summaryParams);
if (summary.stopReason().map(BetaStopReason.COMPACTION::equals).orElse(false)) {
history.clear();
history.add(summary.toParam());
IO.println("Compacted before turn " + (turn + 1));
}
}
}
}
use Anthropic\Beta\AnthropicBeta;
use Anthropic\Beta\Messages\BetaCompactionConfig;
use Anthropic\Beta\Messages\BetaMessageParam;
use Anthropic\Beta\Messages\BetaMessageParam\Role;
use Anthropic\Beta\Messages\BetaStopReason;
$client = new Client();
// Set this near your real input budget. It is low here so a short conversation compacts.
const COMPACT_AT_TOKENS = 2500;
const SYSTEM = "You help design a recipe app's data model. Keep answers short.";
$questions = [
'What are the main entities in the data model?',
'Which fields should Recipe have?',
'Which fields should Ingredient have?',
'Which fields should RecipeIngredient have?',
'Which fields should Step have?',
'Which indexes should these tables have?',
'Which fields should be required?',
'Which fields should have default values?',
];
$history = [];
foreach ($questions as $index => $question) {
$turn = $index + 1;
$history[] = BetaMessageParam::with(role: Role::USER, content: $question);
$response = $client->beta->messages->create(
model: Model::CLAUDE_OPUS_5_5,
maxTokens: 8192,
system: SYSTEM,
betas: [AnthropicBeta::COMPACT_2026_09_04],
messages: $history,
);
$history[] = BetaMessageParam::with(role: Role::ASSISTANT, content: $response->content);
// The next request sends this reply too, so count it.
$conversationTokens = $response->usage->inputTokens + $response->usage->outputTokens;
if ($conversationTokens > COMPACT_AT_TOKENS && $turn < count($questions)) {
$summary = $client->beta->messages->create(
model: Model::CLAUDE_OPUS_5_5,
maxTokens: 4096,
system: SYSTEM,
betas: [AnthropicBeta::COMPACT_2026_09_04],
messages: $history,
compaction: new BetaCompactionConfig(), // type defaults to 'summarize'
);
if ($summary->stopReason === BetaStopReason::COMPACTION->value) {
$history = [BetaMessageParam::with(role: Role::ASSISTANT, content: $summary->content)];
printf("Compacted before turn %d\n", $turn + 1);
}
}
}
client = Anthropic::Client.new
# Set this near your real input budget. It is low here so a short conversation compacts.
COMPACT_AT_TOKENS = 2500
SYSTEM = "You help design a recipe app's data model. Keep answers short."
questions = [
"What are the main entities in the data model?",
"Which fields should Recipe have?",
"Which fields should Ingredient have?",
"Which fields should RecipeIngredient have?",
"Which fields should Step have?",
"Which indexes should these tables have?",
"Which fields should be required?",
"Which fields should have default values?"
]
history = []
questions.each.with_index(1) do |question, turn|
history << { role: "user", content: question }
response = client.beta.messages.create(
model: Anthropic::Model::CLAUDE_OPUS_5_5,
max_tokens: 8192,
system_: SYSTEM,
betas: [Anthropic::AnthropicBeta::COMPACT_2026_09_04],
messages: history
)
history << { role: "assistant", content: response.content }
# The next request sends this reply too, so count it.
conversation_tokens = response.usage.input_tokens + response.usage.output_tokens
if conversation_tokens > COMPACT_AT_TOKENS && turn < questions.length
summary = client.beta.messages.create(
model: Anthropic::Model::CLAUDE_OPUS_5_5,
max_tokens: 4096,
system_: SYSTEM,
betas: [Anthropic::AnthropicBeta::COMPACT_2026_09_04],
messages: history,
compaction: { type: "summarize" }
)
if summary.stop_reason == :compaction
history = [{ role: "assistant", content: summary.content }]
puts "Compacted before turn #{turn + 1}"
end
end
end
stop_reason 확인은 코드가 블록을 찾기 전에 와요. Handle a missing summary or an error가 그 이유를 설명해요. 기록은 추가되는 게 아니라 교체돼요. 반환된 메시지가 요청이 실은 모든 메시지를 Continue from the summary의 규칙에 따라 교체해요. 요약이 안 오면 루프는 기록을 유지하고 다음 턴 후에 다시 요청해요.
Python, TypeScript, C#, Go, Java, PHP, Ruby의 SDK 도구 러너가 컴팩션 요청을 대신 보내줄 수 있어요. 컴팩션을 결정하면 러너에서 compact_before_next_turn()(TypeScript, Java, PHP는 compactBeforeNextTurn(), C#, Go는 CompactBeforeNextTurn())을 호출하세요. 현재 턴과 그 도구 호출이 끝나면 러너가 컴팩션 요청을 보내고 기록을 반환된 메시지로 바꿔요. 러너는 스스로 추가하지 않으므로 compact-2026-09-04 베타로 러너를 만드세요.
러너는 자신의 매개변수로 컴팩션 요청을 구성하고 context_management는 빼요. 또한 stop_sequences, any 또는 tool 유형의 tool_choice, 구조화 출력 output_config.format도 빼요. 컴팩션 요청에서 API가 거부하기 때문이에요. Request a summary가 그 이유를 설명해요. 러너는 이후 요청에서 그것들을 다시 보내요. Python 1.8.0, TypeScript 0.128.0, C# 12.50.0, Go 1.75.0, Java 2.65.0 이전 SDK 버전은 컴팩션 요청에도 그것들을 보내요. 그 버전들에서는 이 매개변수 중 하나라도 설정한 러너가 400 에러를 받아요. 러너는 task budget을 그대로 보내요. output_config.task_budget이 remaining을 설정하면 컴팩션 요청이 400 에러를 반환하므로, Limits and interactions with other features에서 말한 대로 remaining을 설정하지 마세요. 러너는 context_management에 컴팩션 편집이 있는 동안에는 컴팩션을 거부하므로, 러너에서는 컴팩션 한 종류만 쓰세요.
언제 컴팩션할까
완료된 턴이 지나면 언제든 컴팩션 요청을 보낼 수 있으므로 코드가 시점을 정해요.
다음 요청이 얼마나 커질지 추정하려면 루프처럼 마지막 응답의 usage에서 input_tokens와 output_tokens를 더하세요. 프롬프트 캐싱에서 input_tokens는 마지막 캐시 중단점 이후의 토큰만 세므로 cache_read_input_tokens와 cache_creation_input_tokens도 더하세요. 같은 메시지를 토큰 계산 엔드포인트에 보낼 수도 있어요.
그 숫자를 모델의 컨텍스트 창 아래에서 고른 한도와 비교하세요.
나만의 요약화 프롬프트 작성
instructions가 없으면 API는 자체 요약화 프롬프트를 사용해요. 비어 있지 않은 instructions 문자열(최대 16,384자)은 그 프롬프트를 완전히 대체해요. 예:
{
"compaction": {
"type": "summarize",
"instructions": "Summarize this recipe app design conversation. Preserve every entity and field name agreed so far, and the user's latest open request. Do not call tools; respond with the summary text only."
}
}
요약기는 instructions가 있든 없든 전체 대화(이전 thinking 포함)를 읽어요. instructions에는 요약이 보존해야 할 것과 모델에게 도구를 호출하지 말라고 알려주세요. 요약화 호출은 다른 요청과 같은 안전장치 아래에서 실행돼요.
누락된 요약이나 에러 처리하기
요약은 요약화 호출이 텍스트와 함께, 도구 호출 없이 정상적으로 끝날 때만 생성돼요. 그렇지 않으면 응답은 여전히 빈 content의 200이므로, 블록을 찾기 전에 stop_reason을 확인하세요. 호출은 여전히 청구되고 usage.iterations에 보고되며, 호출을 만들 수 없으면 usage가 0이에요. stop_reason은 요약화 호출이 끝난 값이에요. 어떤 경우든 요약 없이 계속하고 나중에 컴팩션할 수 있어요.
stop_reason |
원인 | 할 일 |
|---|---|---|
"max_tokens" |
요약이 잘렸어요. | 더 큰 max_tokens로 다시 보내세요. |
"model_context_window_exceeded" |
요약화 프롬프트 자리가 없었어요. | 더 짧은 instructions나 더 적은 메시지로 다시 보내세요. |
"tool_use" |
모델이 요약 대신 도구를 호출했어요. | 도구를 호출하지 말라고 하는 instructions로 다시 보내세요. |
"refusal" |
요청이 거절됐어요. | 요약 없이 계속하세요. |
"end_turn" |
호출이 텍스트를 반환하지 않았어요. | 요약 없이 계속하세요. |
요약화 호출은 다른 요청과 같은 안전장치를 받아요. "refusal" 후에는 stop_details가 그 뒤의 정책 범주를 식별해줘요.
에러
컴팩션 요청이나 블록을 실은 요청은 완전히 실패할 수도 있어요. 대부분의 400 에러는 무엇을 제거하거나 다시 보낼지 알려주는 메시지를 담아요. 일부는 compaction_으로 시작하는 error.details.error_code도 실어요. compaction과 결합할 수 없는 필드 같은 매개변수 에러는 메시지만 담아요.
| 에러 | 원인 | 할 일 |
|---|---|---|
529 overloaded_error, error.details.error_code compaction_unavailable |
블록을 만드는 동안이나 받은 블록을 읽는 동안 일시적인 서버 문제. | 요청을 재시도하세요. |
400 compaction_block_misplaced |
요약된 메시지가 블록 앞에 남아 있어요. | 블록이 messages 맨 앞에 오도록 그것들을 제거하세요. |
400 compaction_signature_invalid 또는 compaction_content_mismatch |
블록의 signature나 content가 API가 반환한 뒤 변경됐어요. |
signature를 포함해 반환된 그대로 보내세요. |
| 400 | 요청이 하나보다 많은 compaction 블록을 실어요. |
가장 새로운 것 딱 하나를 보내세요. |
| 400 | 마지막 assistant 턴이 아직 결과가 없는 도구 호출로 끝나요. |
그 턴의 도구 결과를 보내고 컴팩션하세요. |
400 compaction_nothing_to_summarize |
messages에 user나 assistant 콘텐츠가 없어요(예: 빈 목록). |
user나 assistant 메시지를 하나 이상 보내세요. |
컴팩션 요청의 400, compaction 매개변수가 requires anthropic-beta: compact-2026-09-04라고 하는 메시지 |
컴팩션 요청이 베타 헤더를 빼먹었어요. | 베타 헤더를 추가하세요. Request a summary. |
블록을 실은 이후 요청의 400: compaction이 예상 콘텐츠 블록 유형 중 하나가 아니라는 검증 에러. 메시지는 헤더를 언급하지 않아요. |
그 요청이 베타 헤더를 빼먹었어요. | 블록을 실은 모든 요청에 베타 헤더를 추가하세요. Request a summary. |
400 검증 에러, 예: messages.0.content.0.compaction.citations: Extra inputs are not permitted |
API가 반환하지 않은 필드(예: citations: null)가 있는 블록을 다시 보냈어요. |
반환된 그대로 보내세요. Continue from the summary. |
컴팩션 usage 세기
요약화 호출은 다른 요청처럼 청구·레이트제한되고, usage.iterations가 compaction 항목으로 보고해요. 답변이 생성되지 않았으므로 최상위 input_tokens와 output_tokens는 0이에요. 대화가 소비한 것을 세려면 최상위 필드가 아니라 usage.iterations 전체를 더하세요. 이후 요청에서 블록을 다시 보내는 것은 컴팩션 비용을 추가하지 않아요.
대화를 컴팩션하고 누락된 요약을 처리하는 동작하는 루프가 생겼어요. 실행 방식을 바꾸는 두 페이지가 있고 둘을 결합할 수 있어요. Compaction that keeps recent turns은 마지막 턴들을 그대로 유지하고, Compaction in the background는 요약이 쓰이는 동안 대화가 계속되게 해요. Compaction and preserved thinking은 thinking 블록을 다시 보내고 둘 중 하나를 지정할 때 적용돼요.
제한과 다른 기능과의 상호작용
- 임계값 컴팩션과 컨텍스트 편집. 같은 요청에
compaction과context_management를 보낼 수 없어요. 임계값 컴팩션(compact_20260112)은 서명된 블록을 실은 요청에서 실행할 수 없어요. - 프롬프트 캐싱. 블록의
cache_control은 요약 뒤에 중단점을 둬요. - 대화 중 시스템 메시지와 도구 변경. 요약된 범위 안의
role: "system"메시지도 요약되므로, 블록이 자리를 차지하면 그 텍스트 지시는 적용을 멈춰요. 지시가 여전히 중요하다면role: "system"메시지로 다시 명시하세요. 그 메시지를 다음 새user턴 바로 뒤에 보내고 이후로 기록에 남겨두세요. 도구 변경과 블록 뒤에 턴을 유지할 때 그 메시지가 어디로 가는지는 Change the system prompt or tools를 보세요. - Task budgets. task budget의
remaining값(output_config.task_budget.remaining)을compaction과 함께 또는 블록을 실은 요청에 보내지 마세요. 그러면 400 에러가 반환돼요. - 토큰 계산. 토큰 계산 엔드포인트는
compaction매개변수를 무시해요. - 요약이 담을 수 없는 콘텐츠. 요약된 메시지 안의 이미지, 문서,
container_upload블록, 가져온 URL은 블록이 자리를 차지하면 사라져요. 이후 턴이 여전히 필요로 하는 것은 다시 명시하거나 다시 업로드하세요.