최근 턴을 유지하는 컴팩션

최근 턴을 유지하는 컴팩션 (Compaction that keeps recent turns)

keep-tail 컴팩션은 대화의 마지막 몇 턴을 요약 뒤에 그대로 유지해요. 컴팩션 루프에서 두 가지를 바꿔요: 어떤 메시지가 컴팩션 요청에 들어가는지, 그리고 블록 뒤에 무엇을 보내는지요. 요약에서 이어가기의 모든 것이 그대로 적용돼요.

출처: 문서

본문

keep-tail 컴팩션은 대화의 마지막 몇 턴을 요약 뒤에 그대로 유지해요. 컴팩션 루프에서 두 가지를 바꿔요: 어떤 메시지가 컴팩션 요청에 들어가는지, 그리고 블록 뒤에 무엇을 보내는지요. 요약에서 이어가기의 모든 것이 그대로 적용돼요.

유지할 턴 선택하기

어떤 턴이 유지되는지 설정하는 매개변수는 없어요. 당신이 히스토리에서 컷 지점을 고르면 돼요: 그 앞의 메시지는 컴팩션 요청에 들어가고, 그 지점부터의 메시지는 유지돼요.

유지된 턴은 전체 길이로 Claude에게 돌아가므로, 더 많이 유지할수록 컴팩션이 확보하는 공간은 줄어요.

열린 도구 호출이 없는 곳에 컷을 두고, 각 도구 호출과 그 결과가 같은 쪽에 있게 하세요. 보내는 메시지가 아직 결과가 없는 도구 호출을 가진 assistant 턴으로 끝나면 API는 컴팩션 요청을 거부해요.

오래된 턴을 컴팩션하고 나머지를 블록 뒤에 보내기

최근 턴들을 그대로 유지하려면, 그 턴들을 컴팩션 요청에서 빼세요. API는 보내는 모든 메시지를 요약하므로, 오래된 턴만 보내고 나서 블록을 유지한 턴들 앞에 두세요.

유지된 턴을 히스토리에 있는 그대로, thinking 블록을 포함해서 보내세요. 두 요청 모두 요약 요청하기에서처럼 베타 헤더를 지녀요.

다음 예시에서 히스토리는 두 턴을 보유하고, 컷이 두 번째를 유지해요. 컴팩션 요청은 첫 번째 턴을 전달해요:

{
  "model": "claude-opus-5-5",
  "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."
    }
  ],
  "compaction": { "type": "summarize" }
}

다음 요청은 반환된 블록을 먼저 보내고, 유지된 턴을 정확히 그대로, 그 다음 새 user 메시지를 보내요. 요약에서 이어가기가 블록으로 시작하는 요청을 보여줘요.

다음 프로그램은 루프에서 컴팩션하기의 루프를 마지막 두 턴을 유지하도록 바꾼 버전이에요. 강조된 줄은 루프와 다른 점을 보여줘요.

```python Python from anthropic.types.beta import BetaMessageParam

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." KEEP_TURNS = 2

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 KEEP_TURNS < turn < len(QUESTIONS):
      # A turn is one user message and one assistant reply,
      # so the kept turns start with a user message.
      split = -2 * KEEP_TURNS
      older, recent = history[:split], history[split:]
      summary = client.beta.messages.create(
          model="claude-opus-5-5",
          max_tokens=4096,
          system=SYSTEM,
          betas=["compact-2026-09-04"],
          messages=older,
          compaction={"type": "summarize"},
      )
      if summary.stop_reason == "compaction":
          history = [{"role": "assistant", "content": summary.content}, *recent]
          print(f"Kept {len(recent) // 2} turns after the block")

```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 keepTurns = 2;

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 > keepTurns && turn < questions.length) {
    // A turn is one user message and one assistant reply, so the kept turns start with a user message.
    const older = history.slice(0, -2 * keepTurns);
    const recent = history.slice(-2 * keepTurns);
    const summary = await client.beta.messages.create({
      model: "claude-opus-5-5",
      max_tokens: 4096,
      system: systemPrompt,
      betas: ["compact-2026-09-04"],
      messages: older,
      compaction: { type: "summarize" }
    });
    if (summary.stop_reason === "compaction") {
      history = [{ role: "assistant", content: summary.content }, ...recent];
      console.log(`Kept ${recent.length / 2} turns after the block`);
    }
  }
}
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.";
const int KeepTurns = 2;

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 > KeepTurns && turn < questions.Length)
    {
        // A turn is one user message and one assistant reply, so the kept turns start with a user message.
        var older = history[..^(2 * KeepTurns)];
        var recent = history[^(2 * KeepTurns)..];
        var summary = await client.Beta.Messages.Create(new MessageCreateParams
        {
            Model = Model.ClaudeOpus5_5,
            MaxTokens = 4096,
            System = SystemPrompt,
            Betas = [AnthropicBeta.Compact2026_09_04],
            Messages = older,
            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(),
                },
                .. recent,
            ];
            Console.WriteLine($"Kept {recent.Count / 2} turns after the block");
        }
    }
}
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."}}
const keepTurns = 2

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 > keepTurns && turn < len(questions) {
		// A turn is one user message and one assistant reply, so the kept turns start with a user message.
		split := len(history) - 2*keepTurns
		older, recent := history[:split], history[split:]
		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:  older,
			Compaction: anthropic.BetaCompactionConfigUnionParam{
				OfSummarize: &anthropic.BetaSummarizeCompactionParam{},
			},
		})
		if err != nil {
			log.Fatal(err)
		}
		if summary.StopReason == anthropic.BetaStopReasonCompaction {
			history = slices.Replace(history, 0, split, summary.ToParam())
			fmt.Printf("Kept %d turns after the block\n", len(recent)/2)
		}
	}
}
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.";
static final int KEEP_TURNS = 2;

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 > KEEP_TURNS && turn < questions.size()) {
            // A turn is one user message and one assistant reply, so the kept turns start with a user message.
            var older = history.subList(0, history.size() - 2 * KEEP_TURNS);
            var summaryParams = MessageCreateParams.builder()
                .model(Model.CLAUDE_OPUS_5_5)
                .maxTokens(4096)
                .system(SYSTEM)
                .addBeta(AnthropicBeta.COMPACT_2026_09_04)
                .messages(older)
                .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)) {
                older.clear();
                history.addFirst(summary.toParam());
                IO.println("Kept " + (history.size() - 1) / 2 + " turns after the block");
            }
        }
    }
}
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.";
const KEEP_TURNS = 2;

$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 > KEEP_TURNS && $turn < count($questions)) {
        // A turn is one user message and one assistant reply, so the kept turns start with a user message.
        $older = array_slice($history, 0, -2 * KEEP_TURNS);
        $recent = array_slice($history, -2 * KEEP_TURNS);
        $summary = $client->beta->messages->create(
            model: Model::CLAUDE_OPUS_5_5,
            maxTokens: 4096,
            system: SYSTEM,
            betas: [AnthropicBeta::COMPACT_2026_09_04],
            messages: $older,
            compaction: new BetaCompactionConfig(), // type defaults to 'summarize'
        );
        if ($summary->stopReason === BetaStopReason::COMPACTION->value) {
            $history = [BetaMessageParam::with(role: Role::ASSISTANT, content: $summary->content), ...$recent];
            printf("Kept %d turns after the block\n", intdiv(count($recent), 2));
        }
    }
}
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."
KEEP_TURNS = 2

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 > KEEP_TURNS && turn < questions.length
    # A turn is one user message and one assistant reply, so the kept turns start with a user message.
    older, recent = history[...-2 * KEEP_TURNS], history.last(2 * KEEP_TURNS)
    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: older,
      compaction: { type: "summarize" }
    )
    if summary.stop_reason == :compaction
      history = [{ role: "assistant", content: summary.content }, *recent]
      puts "Kept #{recent.length / 2} turns after the block"
    end
  end
end
  • 컷 정하기: 프로그램은 마지막 두 턴을 유지해요. 여기서 턴은 하나의 user 메시지와 그에 대한 응답이에요. 끝에서 네 메시지 뒤로 히스토리를 나누어, 유지된 턴이 user 메시지로 시작하게 해요.
  • 언제 컴팩션할지 결정하기: 크기 검사는 대화가 프로그램이 유지하는 턴 수보다 더 많은 턴을 가질 때도 요구해요. 그래서 오래된 부분이 결코 비지 않아요.
  • 컴팩션 요청: 루프가 전체 히스토리를 보내는 곳에서, 이 버전은 오래된 메시지만 보내요.
  • 교체: 루프가 전체 히스토리를 반환된 메시지로 교체하는 곳에서, 이 버전의 새 히스토리는 반환된 메시지 다음에 유지된 턴들이 와요.

stop_reason 검사와 교체 이후의 모든 요청은 루프와 동일해요.

유지된 턴에서 thinking을 유효하게 유지하기

보존 thinking이 있는 모델에서 thinking 블록을 다시 보내면, 유지된 턴의 thinking은 유지된 thinking이 유효하게 유지되는 조건이 성립하는 동안에만 유효하고, 그 중 하나가 컷이 놓일 수 있는 위치를 제한해요.

프로그램의 컷(응답과 다음 user 메시지 사이)은 그 조건을 충족해요. 이미 한 요청의 끝에서 자르는 것도 그래요: 정확히 그 요청의 messages를 컴팩션하고, 이후 히스토리가 얻은 모든 것을 유지하세요.

더 알아보기 (Learn more)