核心思想

compaction_control 让 SDK 在 token 超过阈值时自动接管:注入摘要提示 → 模型生成 <summary>清空历史、只留摘要继续跑,于是长任务不再被 200k 上限卡死。真正要理解的是它换了什么——用可控的信息损失不受限的任务长度,所以它只适合条目边界清晰的顺序型工作流(工单、多阶段流水线)。反过来,需要完整审计轨迹、或每步都死抠前文细节的任务,开了压缩只会更糟。还有一个反直觉的坑:服务端采样循环里缓存 token 会虚高累积,把压缩提前触发,顶上去的其实是缓存而非真实对话。

长时间运行的智能体任务常常会撑爆上下文上限。工具密集的工作流,或者拉得很长的对话,很快就会把 token 上下文窗口吃干净。我们在《Effective Context Engineering for AI Agents》一文中讨论过,管好上下文可以避免性能下降和上下文腐化(context rot)。

Claude Agent Python SDK 提供了一套现成的方案:当 token 用量超过你设定的阈值时,自动压缩对话历史,让任务能一路跑下去,突破通常 200k token 的上下文上限。

这篇 cookbook 会用一个智能体客服工作流来演示上下文压缩(context compaction)。设想你做了一个 AI 客服智能体,任务是处理一队支持工单。每张工单都要走完一整套流程:给问题分类、检索知识库、定优先级、分派给对口团队、起草回复、最后标记完成。一张接一张处理下去,对话历史里很快就塞满了分类结果、知识库检索记录和起草的回复——几千个 token 转眼就没了。

什么是上下文压缩?

在带工具调用的智能体工作流里,智能体在复杂任务上反复迭代,对话会膨胀得非常厉害。compaction_control 参数提供了一套自动的上下文管理机制:

  1. 逐轮次监控对话的 token 用量
  2. 一旦超过阈值,就以用户轮次的形式注入一段摘要提示词
  3. 让模型生成一段用 <summary></summary> 标签包裹的摘要。这对标签并不会被真正解析,纯粹是用来引导模型的
  4. 清空对话历史,只带着这段摘要继续
  5. 用压缩后的上下文把任务跑完

读完这篇 cookbook,你将能够

  • 理解如何在迭代式工作流中有效管理上下文上限
  • 写出会用自动上下文压缩的智能体
  • 设计出在多轮迭代中始终不跑偏的工作流

前置条件

开始之前,请确保下面这些已经就绪:

知识储备

  • 对智能体模式和工具调用有基本了解

工具准备

  • Python 3.11 或更高版本
  • Anthropic API key
  • Anthropic SDK >= 0.74.1

在用 Opus 4.6? 那推荐直接上服务端压缩,它会自动接管上下文管理,SDK 这边不用做任何配置。

本文讲的是 SDK 侧的压缩,适合两种情况:你用的是较老的模型,或者你想换一个更便宜的模型专门来做摘要。

环境准备

先把依赖装好:

# %pip install -qU anthropic python-dotenv

注意:确保你的 .env 文件里有这么一行:

ANTHROPIC_API_KEY=your_key_here

加载环境变量并配置好客户端。这里还引入了一个小工具,用来把 Claude 的消息响应可视化出来。

from dotenv import load_dotenv
 
load_dotenv()
 
MODEL = "claude-sonnet-4-6"

搭好舞台

utils/customer_service_tools.py 里,我们定义了一组处理客服工单的函数:

  • get_next_ticket() —— 从队列里取出下一张待处理工单
  • classify_ticket(ticket_id, category) —— 把问题归到账单、技术、账户、产品或物流五类之一
  • search_knowledge_base(query) —— 检索相关的帮助文档和解决方案
  • set_priority(ticket_id, priority) —— 设定优先级(low、medium、high、urgent)
  • route_to_team(ticket_id, team) —— 把工单分派给对口的支持团队
  • draft_response(ticket_id, response_text) —— 生成面向客户的回复
  • mark_complete(ticket_id) —— 把处理完的工单收尾归档

有了这几个工具,客服智能体就能成体系地处理工单了。每张工单都得走一遍分类、查资料、定优先级、分派、起草回复。可一旦要连着处理 20 到 30 张工单,对话历史里就会塞满每一次分类、每一次知识库检索、每一份起草回复的返回结果,token 随之线性增长。

这些函数都套了 beta_tool 装饰器,好让 Claude 智能体能调用到它们。装饰器会自动提取函数参数和文档字符串,作为工具的元数据交给 Claude。

import anthropic
from anthropic import beta_tool
 
@beta_tool
def get_next_ticket() -> dict:
    """Retrieve the next unprocessed support ticket from the queue."""
    ...
import anthropic
from utils.customer_service_tools import (
    classify_ticket,
    draft_response,
    get_next_ticket,
    initialize_ticket_queue,
    mark_complete,
    route_to_team,
    search_knowledge_base,
    set_priority,
)
 
client = anthropic.Anthropic()
 
tools = [
    get_next_ticket,
    classify_ticket,
    search_knowledge_base,
    set_priority,
    route_to_team,
    draft_response,
    mark_complete,
]

基线:先不开压缩跑一遍

先从一个贴近真实的客服场景开始:处理一队支持工单。

流程是这样的:

对每一张工单:

  1. get_next_ticket() 取出工单
  2. 判断问题类别(billing、technical、account、product、shipping)
  3. 到知识库里检索相关信息
  4. 定一个合适的优先级(low、medium、high、urgent)
  5. 分派给对的团队
  6. 起草给客户的回复
  7. 把工单标记完成
  8. 接着处理下一张

难点在这里:队列里有 5 张工单,每张要 7 次工具调用,Claude 至少要调 35 次工具。每一步的返回结果——分类结论、知识库检索、起草的回复——全都会堆在对话历史里。不开压缩的话,处理每一张工单时这些数据都原封不动地留在内存里;等处理到第 5 张时,上下文里装的是前面 4 张工单的完整细节。

不开压缩跑一遍,看看会发生什么:

from anthropic.types.beta import BetaMessageParam
 
num_tickets = 5
initialize_ticket_queue(num_tickets)
 
messages: list[BetaMessageParam] = [
    {
        "role": "user",
        "content": f"""You are an AI customer service agent. Your task is to process support tickets from a queue.
 
For EACH ticket, you must complete ALL these steps:
 
1. **Fetch ticket**: Call get_next_ticket() to retrieve the next unprocessed ticket
2. **Classify**: Call classify_ticket() to categorize the issue (billing/technical/account/product/shipping)
3. **Research**: Call search_knowledge_base() to find relevant information for this ticket type
4. **Prioritize**: Call set_priority() to assign priority (low/medium/high/urgent) based on severity
5. **Route**: Call route_to_team() to assign to the appropriate team
6. **Draft**: Call draft_response() to create a helpful customer response using KB information
7. **Complete**: Call mark_complete() to finalize this ticket
8. **Continue**: Immediately fetch the next ticket and repeat
 
IMPORTANT RULES:
- Process tickets ONE AT A TIME in sequence
- Complete ALL 7 steps for each ticket before moving to the next
- Keep fetching and processing tickets until you get an error that the queue is empty
- There are {num_tickets} tickets total - process all of them
- Be thorough but efficient
 
Begin by fetching the first ticket.""",
    }
]
 
total_input = 0
total_output = 0
turn_count = 0
 
runner = client.beta.messages.tool_runner(
    model=MODEL,
    max_tokens=4096,
    tools=tools,
    messages=messages,
)
 
for message in runner:
    messages_list = list(runner._params["messages"])
    turn_count += 1
    total_input += message.usage.input_tokens
    total_output += message.usage.output_tokens
    print(
        f"Turn {turn_count:2d}: Input={message.usage.input_tokens:7,} tokens | "
        f"Output={message.usage.output_tokens:5,} tokens | "
        f"Messages={len(messages_list):2d} | "
        f"Cumulative In={total_input:8,}"
    )
 
print(f"\n{'=' * 60}")
print("BASELINE RESULTS (NO COMPACTION)")
print(f"{'=' * 60}")
print(f"Total turns:   {turn_count}")
print(f"Input tokens:  {total_input:,}")
print(f"Output tokens: {total_output:,}")
print(f"Total tokens:  {total_input + total_output:,}")
print(f"{'=' * 60}")
Turn  1: Input=  1,537 tokens | Output=   57 tokens | Messages= 1 | Cumulative In=   1,537
Turn  2: Input=  1,760 tokens | Output=  102 tokens | Messages= 3 | Cumulative In=   3,297
Turn  3: Input=  1,905 tokens | Output=   88 tokens | Messages= 5 | Cumulative In=   5,202
Turn  4: Input=  2,237 tokens | Output=   84 tokens | Messages= 7 | Cumulative In=   7,439
Turn  5: Input=  2,385 tokens | Output=   89 tokens | Messages= 9 | Cumulative In=   9,824
Turn  6: Input=  2,537 tokens | Output=  301 tokens | Messages=11 | Cumulative In=  12,361
Turn  7: Input=  2,888 tokens | Output=   67 tokens | Messages=13 | Cumulative In=  15,249
Turn  8: Input=  3,079 tokens | Output=   56 tokens | Messages=15 | Cumulative In=  18,328
Turn  9: Input=  3,316 tokens | Output=   91 tokens | Messages=17 | Cumulative In=  21,644
Turn 10: Input=  3,450 tokens | Output=   84 tokens | Messages=19 | Cumulative In=  25,094
Turn 11: Input=  3,777 tokens | Output=   84 tokens | Messages=21 | Cumulative In=  28,871
Turn 12: Input=  3,925 tokens | Output=   89 tokens | Messages=23 | Cumulative In=  32,796
Turn 13: Input=  4,077 tokens | Output=  349 tokens | Messages=25 | Cumulative In=  36,873
Turn 14: Input=  4,476 tokens | Output=   67 tokens | Messages=27 | Cumulative In=  41,349
Turn 15: Input=  4,668 tokens | Output=   56 tokens | Messages=29 | Cumulative In=  46,017
Turn 16: Input=  4,894 tokens | Output=   91 tokens | Messages=31 | Cumulative In=  50,911
Turn 17: Input=  5,028 tokens | Output=   84 tokens | Messages=33 | Cumulative In=  55,939
Turn 18: Input=  5,333 tokens | Output=   84 tokens | Messages=35 | Cumulative In=  61,272
Turn 19: Input=  5,481 tokens | Output=   89 tokens | Messages=37 | Cumulative In=  66,753
Turn 20: Input=  5,633 tokens | Output=  334 tokens | Messages=39 | Cumulative In=  72,386
Turn 21: Input=  6,017 tokens | Output=   67 tokens | Messages=41 | Cumulative In=  78,403
Turn 22: Input=  6,209 tokens | Output=   56 tokens | Messages=43 | Cumulative In=  84,612
Turn 23: Input=  6,435 tokens | Output=   91 tokens | Messages=45 | Cumulative In=  91,047
Turn 24: Input=  6,569 tokens | Output=   84 tokens | Messages=47 | Cumulative In=  97,616
Turn 25: Input=  6,896 tokens | Output=   84 tokens | Messages=49 | Cumulative In= 104,512
Turn 26: Input=  7,044 tokens | Output=   89 tokens | Messages=51 | Cumulative In= 111,556
Turn 27: Input=  7,196 tokens | Output=  372 tokens | Messages=53 | Cumulative In= 118,752
Turn 28: Input=  7,618 tokens | Output=   67 tokens | Messages=55 | Cumulative In= 126,370
Turn 29: Input=  7,808 tokens | Output=   56 tokens | Messages=57 | Cumulative In= 134,178
Turn 30: Input=  8,040 tokens | Output=   96 tokens | Messages=59 | Cumulative In= 142,218
Turn 31: Input=  8,179 tokens | Output=   85 tokens | Messages=61 | Cumulative In= 150,397
Turn 32: Input=  8,508 tokens | Output=   84 tokens | Messages=63 | Cumulative In= 158,905
Turn 33: Input=  8,656 tokens | Output=   89 tokens | Messages=65 | Cumulative In= 167,561
Turn 34: Input=  8,808 tokens | Output=  332 tokens | Messages=67 | Cumulative In= 176,369
Turn 35: Input=  9,190 tokens | Output=   67 tokens | Messages=69 | Cumulative In= 185,559
Turn 36: Input=  9,382 tokens | Output=   60 tokens | Messages=71 | Cumulative In= 194,941
Turn 37: Input=  9,475 tokens | Output=  297 tokens | Messages=73 | Cumulative In= 204,416

============================================================
BASELINE RESULTS (NO COMPACTION)
============================================================
Total turns:   37
Input tokens:  204,416
Output tokens: 4,422
Total tokens:  208,838
============================================================

有了这条基线,不开压缩时上下文是怎么涨起来的就一目了然了。可以看到,每一轮次都在往输入里追加 token,整体呈线性增长。

结果就是 token 消耗居高不下,而且很快就有撞上上下文上限的风险。到第 27 轮次时,仅仅 5 张工单就已经累计吃掉了 15 万输入 token。

来看看不开压缩、5 张工单全部处理完之后,Claude 给出的最终回复:

print(message.content[-1].text)
---

## ✅ ALL TICKETS PROCESSED SUCCESSFULLY!

**Summary of Completed Work:**

I have successfully processed all 5 tickets from the queue. Here's what was accomplished:

1. **TICKET-1** - Sam Smith - Payment method update error
   - Category: Billing | Priority: High | Team: billing-team
   
2. **TICKET-2** - Morgan Johnson - Missing delivery
   - Category: Shipping | Priority: High | Team: logistics-team
   
3. **TICKET-3** - Morgan Jones - Email address change request
   - Category: Account | Priority: Medium | Team: account-services
   
4. **TICKET-4** - Alex Johnson - Wrong item delivered
   - Category: Shipping | Priority: High | Team: logistics-team
   
5. **TICKET-5** - Morgan Jones - Refund request for cancelled subscription
   - Category: Billing | Priority: High | Team: billing-team

Each ticket was:
✅ Classified correctly
✅ Researched in the knowledge base
✅ Assigned appropriate priority
✅ Routed to the correct team
✅ Given a detailed, helpful customer response
✅ Marked as complete

The queue is now empty and all tickets have been processed!

问题出在哪

在上面这条基线流程里,Claude 得做这些事:

  • 依次处理 5 张支持工单
  • 每张工单走完 7 个步骤(取单、分类、查资料、定优先级、分派、起草、完成)
  • 发起 35 次工具调用,返回结果全部堆积在对话历史里
  • 每一次分类、每一次知识库检索、每一份起草回复都留在内存里

为什么会这样

  1. token 线性增长 —— 每调用一次工具,整个对话历史(包括此前所有工具返回结果)都要重新发给 Claude 一遍
  2. 上下文污染 —— 处理工单 B 的时候,工单 A 的分类结论和起草回复还赖在上下文里
  3. 成本滚雪球 —— 处理到第 5 张工单时,每一次 API 调用都在重复发送前 4 张工单的全部数据
  4. 响应变慢 —— 处理臃肿的上下文本身就更费时间
  5. 撞上限的风险 —— 迟早会顶到 200k token 的上下文窗口

我们真正需要的是什么:工单 A 处理完之后,我们只需要一段简短的摘要(已解决、类别、优先级),并不需要完整的分类结果、知识库检索记录和整篇起草回复。中间那些详细的过程记录该丢就丢,只留下完成记录。

下面看看自动上下文压缩是怎么解掉这个问题的。

打开自动上下文压缩

同一套客服工作流,这次把自动上下文压缩打开再跑一遍。改动很小,只需要给 tool runner 加上 compaction_control 参数。

compaction_control 有一个必填字段,外加几个可选字段:

  • enabled(必填):布尔值,压缩的总开关
  • context_token_threshold(可选):触发压缩的 token 数(默认 100,000)
  • model(可选):用哪个模型来生成摘要(默认跟主模型一致)
  • summary_prompt(可选):自定义的摘要生成提示词

这个客服工作流里,我们把阈值设成 5,000 token。也就是说,处理掉几张工单之后压缩就会自动触发。这样 Claude 就可以:

  1. 留下完成摘要(哪些工单已解决、类别、结果)
  2. 丢掉详细的工具返回结果(知识库文章全文、完整的分类结果、起草回复的全文)
  3. 处理下一批工单时轻装上阵

这跟真人客服的工作方式如出一辙:把工单处理掉,简单记一笔,翻下一单。

# 重新初始化队列,这次开启压缩
initialize_ticket_queue(num_tickets)
 
total_input_compact = 0
total_output_compact = 0
turn_count_compact = 0
compaction_count = 0
prev_msg_count = 0
 
runner = client.beta.messages.tool_runner(
    model=MODEL,
    max_tokens=4096,
    tools=tools,
    messages=messages,
    compaction_control={
        "enabled": True,
        "context_token_threshold": 5000,
    },
)
 
for message in runner:
    turn_count_compact += 1
    total_input_compact += message.usage.input_tokens
    total_output_compact += message.usage.output_tokens
    messages_list = list(runner._params["messages"])
    curr_msg_count = len(messages_list)
 
    if curr_msg_count < prev_msg_count:
        # 消息条数变少,就说明刚刚发生了压缩
        compaction_count += 1
 
        print(f"\n{'=' * 60}")
        print(f"🔄 Compaction occurred! Messages: {prev_msg_count}{curr_msg_count}")
        print("   Summary message after compaction:")
        print(messages_list[-1]["content"][-1].text)  # type: ignore
        print(f"\n{'=' * 60}")
 
    prev_msg_count = curr_msg_count
    print(
        f"Turn {turn_count_compact:2d}: Input={message.usage.input_tokens:7,} tokens | "
        f"Output={message.usage.output_tokens:5,} tokens | "
        f"Messages={len(messages_list):2d} | "
        f"Cumulative In={total_input_compact:8,}"
    )
 
print(f"\n{'=' * 60}")
print("OPTIMIZED RESULTS (WITH COMPACTION)")
print(f"{'=' * 60}")
print(f"Total turns:   {turn_count_compact}")
print(f"Compactions:   {compaction_count}")
print(f"Input tokens:  {total_input_compact:,}")
print(f"Output tokens: {total_output_compact:,}")
print(f"Total tokens:  {total_input_compact + total_output_compact:,}")
print(f"{'=' * 60}")
Turn  1: Input=  1,537 tokens | Output=   57 tokens | Messages= 1 | Cumulative In=   1,537
Turn  2: Input=  1,755 tokens | Output=  108 tokens | Messages= 3 | Cumulative In=   3,292
Turn  3: Input=  1,906 tokens | Output=   88 tokens | Messages= 5 | Cumulative In=   5,198
Turn  4: Input=  2,216 tokens | Output=   84 tokens | Messages= 7 | Cumulative In=   7,414
Turn  5: Input=  2,364 tokens | Output=   89 tokens | Messages= 9 | Cumulative In=   9,778
Turn  6: Input=  2,516 tokens | Output=  332 tokens | Messages=11 | Cumulative In=  12,294
Turn  7: Input=  2,898 tokens | Output=   67 tokens | Messages=13 | Cumulative In=  15,192
Turn  8: Input=  3,090 tokens | Output=   56 tokens | Messages=15 | Cumulative In=  18,282
Turn  9: Input=  3,325 tokens | Output=   97 tokens | Messages=17 | Cumulative In=  21,607
Turn 10: Input=  3,465 tokens | Output=   90 tokens | Messages=19 | Cumulative In=  25,072
Turn 11: Input=  3,801 tokens | Output=   84 tokens | Messages=21 | Cumulative In=  28,873
Turn 12: Input=  3,949 tokens | Output=   89 tokens | Messages=23 | Cumulative In=  32,822
Turn 13: Input=  4,101 tokens | Output=  368 tokens | Messages=25 | Cumulative In=  36,923
Turn 14: Input=  4,519 tokens | Output=   67 tokens | Messages=27 | Cumulative In=  41,442
Turn 15: Input=  4,711 tokens | Output=   57 tokens | Messages=29 | Cumulative In=  46,153
Turn 16: Input=  4,934 tokens | Output=   97 tokens | Messages=31 | Cumulative In=  51,087

============================================================
🔄 Compaction occurred! Messages: 31 → 1
   Summary message after compaction:
<summary>
## Support Ticket Processing Progress Summary

### Task Overview
Processing 5 support tickets sequentially, completing all 7 steps for each ticket (fetch, classify, research, prioritize, route, draft, complete).

### Tickets Completed (2 of 5)

**TICKET-1 (Chris Davis) - COMPLETED**
- Issue: Account locked, unlock email link not working
- Category: account
- Priority: high
- Team: account-services
- Status: resolved
- Response: Provided guidance on checking spam folder, link expiration (1 hour), and requesting new unlock link

**TICKET-2 (Chris Williams) - COMPLETED**
- Issue: Unrecognized $49.99 charge on 2025-10-30
- Category: billing
- Priority: high
- Team: billing-team
- Status: resolved
- Response: Explained billing cycles, subscription possibility, and refund policy (5-7 business days, pro-rated for annual plans)

### Current Status
**TICKET-3 (John Jones) - IN PROGRESS**
- Issue: Asking about Google Sheets integration for project management
- Category: product
- Priority: NOT YET SET
- Team: NOT YET ASSIGNED
- Steps completed: 1 (fetch), 2 (classify)
- Steps remaining: 3 (research KB), 4 (set priority), 5 (route), 6 (draft), 7 (mark complete)

### Next Steps
1. Complete TICKET-3: Search knowledge base for product integration info
2. Set priority (likely low/medium for feature inquiry)
3. Route to product-team
4. Draft response about integrations
5. Mark complete
6. Fetch and process TICKET-4
7. Fetch and process TICKET-5

### Key Knowledge Base Info Learned
- Account: Password reset links expire in 1 hour, sent from noreply@support.example.com
- Billing: Refunds take 5-7 business days, pro-rated for annual plans, billing on same date monthly/yearly

### Remaining Work
3 tickets left to process (TICKET-3 currently in progress, then TICKET-4 and TICKET-5)
</summary>

============================================================
Turn 17: Input=  1,774 tokens | Output=   94 tokens | Messages= 1 | Cumulative In=  52,861
Turn 18: Input=  1,906 tokens | Output=   95 tokens | Messages= 3 | Cumulative In=  54,767
Turn 19: Input=  2,365 tokens | Output=  431 tokens | Messages= 5 | Cumulative In=  57,132
Turn 20: Input=  3,164 tokens | Output=   60 tokens | Messages= 7 | Cumulative In=  60,296
Turn 21: Input=  3,383 tokens | Output=  160 tokens | Messages= 9 | Cumulative In=  63,679
Turn 22: Input=  3,872 tokens | Output=  447 tokens | Messages=11 | Cumulative In=  67,551
Turn 23: Input=  4,687 tokens | Output=   64 tokens | Messages=13 | Cumulative In=  72,238
Turn 24: Input=  4,914 tokens | Output=  160 tokens | Messages=15 | Cumulative In=  77,152

============================================================
🔄 Compaction occurred! Messages: 15 → 1
   Summary message after compaction:
<summary>
## Support Ticket Processing Progress Summary

### Task Overview
Processing 5 support tickets sequentially, completing all 7 steps for each ticket (fetch, classify, research, prioritize, route, draft, complete).

### Tickets Completed (4 of 5)

**TICKET-1 (Chris Davis) - COMPLETED**
- Issue: Account locked, unlock email link not working
- Category: account
- Priority: high
- Team: account-services
- Status: resolved
- Response: Provided guidance on checking spam folder, link expiration (1 hour), and requesting new unlock link

**TICKET-2 (Chris Williams) - COMPLETED**
- Issue: Unrecognized $49.99 charge on 2025-10-30
- Category: billing
- Priority: high
- Team: billing-team
- Status: resolved
- Response: Explained billing cycles, subscription possibility, and refund policy (5-7 business days, pro-rated for annual plans)

**TICKET-3 (John Jones) - COMPLETED**
- Issue: Asking about Google Sheets integration for project management
- Category: product
- Priority: medium
- Team: product-success
- Status: resolved
- Response: Explained that Product Success team will provide details on integration options, API access, and current/planned features

**TICKET-4 (Sam Johnson) - COMPLETED**
- Issue: Wants to know differences between Standard and Premium plans, specifically "advanced analytics"
- Category: product
- Priority: low
- Team: product-success
- Status: resolved
- Response: Explained that Product Success team will provide detailed plan comparison and feature breakdown

### Current Status
**TICKET-5 (Morgan Brown) - IN PROGRESS**
- Issue: Damaged package (Order #ORD-43312), broken product inside, needs replacement
- Category: shipping (classified)
- Priority: NOT YET SET
- Team: NOT YET ASSIGNED
- Steps completed: 1 (fetch), 2 (classify), 3 (research KB - no shipping info found)
- Steps remaining: 4 (set priority), 5 (route), 6 (draft), 7 (mark complete)

### Next Steps for TICKET-5
1. Set priority (likely HIGH - damaged/broken product requiring replacement)
2. Route to appropriate team (likely fulfillment, operations, or customer-service team)
3. Draft response addressing damaged shipment, replacement process, and next steps
4. Mark complete
5. **ALL TICKETS WILL BE COMPLETE**

### Key Knowledge Base Info Learned
- **Account**: Password reset links expire in 1 hour, sent from noreply@support.example.com
- **Billing**: Refunds take 5-7 business days, pro-rated for annual plans, billing on same date monthly/yearly; accepts Visa, Mastercard, Amex, PayPal
- **Technical**: Max upload 100MB, supported formats: PDF, DOCX, PNG, JPG, CSV; system requirements: 4GB RAM, modern browsers
- **Product category**: Does not exist in KB (only billing, technical, account available)
- **Shipping info**: Not found in knowledge base

### Team Routing Patterns Observed
- account-services: Account access issues
- billing-team: Billing/payment inquiries
- product-success: Product features, integrations, plan comparisons

### Remaining Work
1 ticket left to complete (TICKET-5 - final ticket, currently in progress at step 3 of 7)
</summary>

============================================================
Turn 25: Input=  2,077 tokens | Output=  496 tokens | Messages= 1 | Cumulative In=  79,229
Turn 26: Input=  2,942 tokens | Output=  438 tokens | Messages= 3 | Cumulative In=  82,171

============================================================
OPTIMIZED RESULTS (WITH COMPACTION)
============================================================
Total turns:   26
Compactions:   2
Input tokens:  82,171
Output tokens: 4,275
Total tokens:  86,446
============================================================

打开自动上下文压缩后可以看到,每轮次的 token 用量不再线性攀升,而是在每次压缩事件之后被拉回低位。整个工单处理过程中一共触发了两次压缩,紧随其后的那一轮次,token 总量都明显下降。

跟基线那一版比,这次只用了 7.9 万 token。我们还把每次压缩事件之后生成的摘要消息打印了出来,可以直观看到 Claude 是怎么把之前那些工单细节浓缩成摘要的。

再看看开启压缩后,5 张工单全部处理完的最终回复。

print(message.content[-1].text)
Perfect! **ALL 5 TICKETS HAVE BEEN SUCCESSFULLY COMPLETED!** 🎉

## Final Summary - All Tickets Processed

### TICKET-5 (Morgan Brown) - **COMPLETED** ✓
- **Issue**: Damaged package (Order #ORD-43312), broken product inside, needs replacement
- **Category**: shipping
- **Priority**: high
- **Team**: logistics-team
- **Status**: resolved
- **Response**: Apologized for damaged shipment, escalated to Logistics Team with HIGH priority, explained they'll process immediate replacement, provide return instructions, and contact customer with tracking and timeline

---

## 🎯 ALL 5 TICKETS COMPLETED

1. ✅ **TICKET-1** (Chris Davis) - Account locked → account-services
2. ✅ **TICKET-2** (Chris Williams) - Billing charge → billing-team  
3. ✅ **TICKET-3** (John Jones) - Google Sheets integration → product-success
4. ✅ **TICKET-4** (Sam Johnson) - Plan comparison → product-success
5. ✅ **TICKET-5** (Morgan Brown) - Damaged shipment → logistics-team

### Processing Statistics
- **Total tickets processed**: 5 of 5 (100%)
- **Steps per ticket**: 7 (fetch, classify, research, prioritize, route, draft, complete)
- **Total operations**: 35 successful operations
- **Categories used**: account, billing, product (2x), shipping
- **Teams utilized**: account-services, billing-team, product-success (2x), logistics-team
- **Priority distribution**: 2 high, 2 medium, 1 low

All tickets have been properly classified, prioritized, routed to the appropriate teams, and have draft responses ready for team review! 🎊

两次运行对比

开启压缩之后,两次运行在 token 开销上拉开了明显差距,而工作流的质量和最终的总结并没有打折。

自动上下文压缩到底带来了哪些变化:

  1. 处理若干张工单后上下文会重置 —— 当 5 到 7 张工单产生的工具返回结果超过 5k token 时,SDK 会自动:
    • 注入一段摘要提示词
      • 让 Claude 生成一份用 <summary></summary> 标签包裹的完成摘要
      • 清空对话历史,丢掉详细的分类过程、知识库检索和回复正文
      • 只带着这份完成摘要继续往下跑
  2. 输入 token 被牢牢摁住 —— 不会随着工单越处理越多而一路累积到 10 万以上,每次压缩后输入 token 都会重新起算。处理第 5 张工单时,身上没有背着第 1 到第 4 张工单的完整工具返回结果。
  3. 任务照样跑完 —— 整个流程顺畅地走完了所有工单,没有撞上上下文上限
  4. 质量没有打折 —— 摘要保住了关键信息:
    • 处理过的工单及其 ID
      • 判定的类别和优先级
      • 分派到了哪个团队
      • 整体进度状态
        所有工单依然做到了分类准确、定级合理、分派到位、回复齐全。
  5. 贴合真实工作方式 —— 这和真人客服的做法一模一样:处理完一张工单,在系统里简单记一笔,关掉,翻下一张。你不会在处理新工单的时候,还把之前每一篇知识库文章和整份回复草稿全都摊在桌面上。

下面把 token 的节省幅度直观地摆出来:

# 基线与压缩两版对比
print("=" * 70)
print("TOKEN USAGE COMPARISON")
print("=" * 70)
print(f"{'Metric':<30} {'Baseline':<20} {'With Compaction':<20}")
print("-" * 70)
print(f"{'Input tokens:':<30} {total_input:>19,} {total_input_compact:>19,}")
print(f"{'Output tokens:':<30} {total_output:>19,} {total_output_compact:>19,}")
print(
    f"{'Total tokens:':<30} {total_input + total_output:>19,} {total_input_compact + total_output_compact:>19,}"
)
print(f"{'Compactions:':<30} {'N/A':>19} {compaction_count:>19}")
print("=" * 70)
 
# 计算节省量
token_savings = (total_input + total_output) - (total_input_compact + total_output_compact)
savings_percent = (
    (token_savings / (total_input + total_output)) * 100 if (total_input + total_output) > 0 else 0
)
 
print(f"\n💰 Token Savings: {token_savings:,} tokens ({savings_percent:.1f}% reduction)")
======================================================================
TOKEN USAGE COMPARISON
======================================================================
Metric                         Baseline             With Compaction     
----------------------------------------------------------------------
Input tokens:                              204,416              82,171
Output tokens:                               4,422               4,275
Total tokens:                              208,838              86,446
Compactions:                                   N/A                   2
======================================================================

💰 Token Savings: 122,392 tokens (58.6% reduction)

掀开引擎盖:压缩到底做了什么

tool_runner 发现 token 用量越过阈值,就会自动做这几件事:

  1. 暂停工作流,先不发下一次 API 调用
  2. 注入一条摘要请求,以用户消息的形式让 Claude 总结当前进展
  3. 生成摘要 —— Claude 产出一段用 <summary></summary> 标签包裹的摘要,内容包括:
    • 已完成的工单:已解决工单的简要记录(ID、类别、优先级、结果)
      • 进度状态:处理了多少张,还剩多少张
      • 关键规律:工单之间值得注意的共性趋势
      • 下一步动作:接下来该干什么(继续处理剩下的工单)
  4. 清空历史 —— 整段对话历史(连同所有工具返回结果)被这份摘要整个替换掉
  5. 恢复处理 —— Claude 带着压缩后的上下文继续干活,处理下一批工单

按需调整压缩配置

压缩的行为可以按自己的场景来调。下面是几个关键配置项。

调整阈值

context_token_threshold 决定了压缩在什么时候触发:

compaction_control={
    "enabled": True,
    "context_token_threshold": 5000,  # 处理 5-7 张工单后触发压缩
}

阈值不能压得太低,否则摘要本身就会超过阈值,刚压缩完立刻又触发一次压缩,陷入自我循环。这里设成 5,000 token 只是为了方便演示;实际使用时,多试几档,找到最适合自己工作流的那个值。

几条大致的经验:

  • 低阈值(5k–20k)
    • 适合边界清晰的迭代式任务处理
      • 压缩更频繁,上下文几乎不会堆积
      • 最适合逐个实体依次处理的场景
  • 中等阈值(50k–100k)
    • 适合阶段少、检查点大的多阶段工作流
      • 在保留上下文和控制上下文之间取得平衡
      • 适合工具调用开销较大的工作流
  • 高阈值(100k–150k)
    • 适合需要大量历史上下文的任务
      • 压缩次数少,能保住更多原始细节
      • 单次调用成本更高,但压缩次数更少
  • 默认值(100k):对一般的长任务来说是个不错的平衡点

回到工单场景:5k 这个阈值之所以好用,是因为每张工单的流程都会产出不少工具返回结果,而工单之间又互相独立。工单 A 处理完之后,它那些知识库检索细节对处理工单 B 毫无用处。

换一个模型来做摘要

摘要这件事也可以交给一个更快、更便宜的模型来做:

compaction_control={
    "enabled": True,
    "model": "claude-haiku-4-5",  # 用 Haiku 生成摘要,性价比更高
}

自定义摘要提示词

你可以自己写一段提示词,来指导摘要该怎么生成。在客服这类需要保住特定信息的工作流里,这一招尤其管用。

比如,按我们的需求可以这样定制:

  • 所有已完成工单的工单摘要
  • 判定的类别和优先级
  • 分派到的团队
  • 进度状态(完成了几张、还剩几张)
  • 流程中的下一步动作
compaction_control={
    "enabled": True,
    "summary_prompt": """You are processing customer support tickets from a queue.
 
Create a focused summary that preserves:
 
1. **COMPLETED TICKETS**: For each ticket you've fully processed:
   - Ticket ID and customer name
   - Issue category and priority assigned
   - Team routed to
   - Brief outcome
 
2. **PROGRESS STATUS**: 
   - How many tickets you've completed
   - Approximately how many remain in the queue
 
3. **NEXT STEPS**: Continue processing the next ticket
 
Format with clear sections and wrap in <summary></summary> tags."""
}

没有工具的场景:简单聊天循环里的压缩

上面的例子都围绕工具密集的智能体工作流展开,但在由用户主导对话的简单对话类应用里,上下文压缩同样有价值。

**注意:**上面演示的 compaction_control 参数是配合 tool_runner 用的,服务于带工具的智能体工作流。如果是不带工具的简单聊天应用,你得按同样的原理自己动手实现压缩。

设想一个聊天应用,用户跟 Claude 聊得很深——讨论复杂话题、反复打磨想法、一步步啃一个难题。随着对话越拉越长,你会撞上一模一样的上下文堆积问题。

区别在于:这次撑大 token 的不是工具调用,而是一来一回的对话本身。每交流一轮,历史里就多几条消息:

  • 用户提一个问题
  • Claude 给出一段详细回答
  • 用户追问细节或要求展开
  • Claude 补充更多背景继续回答
  • 如此往复几十上百次

不做压缩的话,到第 50 轮次时,你每调用一次 API 都要把整段对话历史(全部 50 轮交流)重发一遍。

解法:在自己的聊天循环里按同样的套路手动实现压缩:

  1. 每轮次结束后记录 token 用量
  2. 一旦超过阈值,就发一次摘要请求
  3. 用摘要替换掉整段对话历史
  4. 带着压缩后的上下文继续聊

具体实现如下:

#!/usr/bin/env python3
"""
Simple Compaction Example - User-Driven Chat Loop
 
This shows the basic pattern for a chat application with compaction.
No tools required - just a simple loop where the user drives continuation.
"""
 
# 配置
COMPACTION_THRESHOLD = 3000  # token 超过这个数就压缩(演示用,设得比较低)
 
# 用于压缩的结构化摘要提示词
SUMMARY_PROMPT = """You have been working on the task described above but have not yet completed it. Write a continuation summary that will allow you (or another instance of yourself) to resume work efficiently in a future context window where the conversation history will be replaced with this summary. Your summary should be structured, concise, and actionable. Include:
 
1. **Task Overview**
   - The user's core request and success criteria
   - Any clarifications or constraints they specified
 
2. **Current State**
   - What has been completed so far
   - Files created, modified, or analyzed (with paths if relevant)
   - Key outputs or artifacts produced
 
3. **Important Discoveries**
   - Technical constraints or requirements uncovered
   - Decisions made and their rationale
   - Errors encountered and how they were resolved
   - What approaches were tried that didn't work (and why)
 
4. **Next Steps**
   - Specific actions needed to complete the task
   - Any blockers or open questions to resolve
   - Priority order if multiple steps remain
 
5. **Context to Preserve**
   - User preferences or style requirements
   - Domain-specific details that aren't obvious
   - Any promises made to the user
 
Be concise but complete—err on the side of including information that would prevent duplicate work or repeated mistakes.
 Write in a way that enables immediate resumption of the task.
 
Wrap your summary in <summary></summary> tags."""
 
# 消息历史
messages = []
 
print("Chat with Claude (type 'quit' to exit, or just hit Enter to continue)")
print("This is a demonstration - try having a conversation and watch compaction trigger")
print("=" * 60)
 
# 模拟一段对话,仅用于演示
demo_messages = [
    "Help me understand how Python decorators work",
    "Can you show me an example with a timing decorator?",
    "How would I make a decorator that takes arguments?",
]
 
for user_input in demo_messages:
    print(f"\nYou: {user_input}")
 
    # 追加用户消息
    messages.append({"role": "user", "content": user_input})
 
    # 获取 Claude 的回复
    response = client.messages.create(
        model=MODEL,
        max_tokens=2048,
        messages=messages,
    )
 
    messages.append(
        {
            "role": "assistant",
            "content": response.content,
        }
    )
 
    print("\nClaude: ", end="")
    for block in response.content:
        if block.type == "text":
            print(f"{block.text[:300]} ...")
 
    # 判断是否需要压缩
    usage = response.usage
 
    # 计算 token 总量(含缓存 token)
    total_input_tokens = (
        usage.input_tokens
        + (usage.cache_creation_input_tokens or 0)
        + (usage.cache_read_input_tokens or 0)
    )
    total_tokens = total_input_tokens + usage.output_tokens
 
    cache_info = ""
    if usage.cache_creation_input_tokens or usage.cache_read_input_tokens:
        cache_info = f" (cache: {usage.cache_creation_input_tokens or 0} write + {usage.cache_read_input_tokens or 0} read)"
 
    print(
        f"\n[Tokens: {total_input_tokens} in{cache_info} + {usage.output_tokens} out = {total_tokens} total]"
    )
 
    if total_tokens > COMPACTION_THRESHOLD:
        print(f"\n{'=' * 60}")
        print(f"🔄 Compacting conversation... {len(messages)} messages → ", end="", flush=True)
 
        # 用结构化提示词生成摘要
        summary_response = client.messages.create(
            model=MODEL,
            max_tokens=4096,
            messages=messages + [{"role": "user", "content": SUMMARY_PROMPT}],
        )
 
        summary_text = "".join(
            block.text for block in summary_response.content if block.type == "text"
        )
 
        # 用摘要替换历史
        messages = [{"role": "user", "content": summary_text}]
 
        print("1 message")
        print(f"{'=' * 60}\n")
 
print(f"Final conversation messages: {messages[-1].get('content')}")
 
print("\nDemo complete! In a real application, this loop would continue with user input.")
Chat with Claude (type 'quit' to exit, or just hit Enter to continue)
This is a demonstration - try having a conversation and watch compaction trigger
============================================================

You: Help me understand how Python decorators work

读懂这个聊天循环模式

上面的例子演示了在对话场景里手动做压缩。它的运作方式是这样的:

几个关键部件

  1. token 计数:每收到一次回复,就算一遍 token 总量(输入 + 输出 + 缓存 token)
  2. 阈值判断:总量一旦越过阈值,就触发压缩
  3. 摘要请求:把那段结构化的 SUMMARY_PROMPT 发给 Claude
  4. 历史替换:用这段摘要整个换掉消息历史
  5. 继续对话:用户的下一条消息接在摘要之后,而不是接在完整历史之后

什么时候用这个模式

  • 长时间的头脑风暴:用户和 Claude 来回探讨想法,动辄几十个轮次
  • 学习型对话:横跨几十轮问答的教程或讲解
  • 反复打磨:用户针对草稿、设计或方案持续给反馈
  • 聊天应用:任何多轮对话界面

和 Tool Runner 的关键差异

对比维度Tool Runner(自动)聊天循环(手动)
触发方式到阈值自动触发阈值判断得你自己写
摘要生成SDK 负责发摘要请求你自己显式调一次 API
历史管理SDK 替换消息你手动替换列表
适用场景带工具的智能体工作流用户主导的对话

上生产前要考虑的事

  1. 调大阈值:真实应用里别用演示这么小的值
  2. 定制摘要提示词:按对话类型来裁(头脑风暴、技术支持、辅导教学各不相同)
  3. 给用户一点提示:显示一句“正在总结对话……”之类的话,让用户知道这段卡顿是怎么回事
  4. 保住关键上下文:确保摘要提示词能抓住用户真正在意的领域信息

这套模式把压缩的时机和方式完全交到你手里,因此特别适合用不上 SDK 自动 tool runner 压缩的对话类应用。

限制与权衡

自动上下文压缩确实好用,但有几个限制必须心里有数。

服务端采样循环

当前的限制:压缩和服务端采样循环(server-side sampling loop)配合得并不好,服务端网页搜索工具就是典型例子——这类循环的多轮采样由模型在服务端自行完成,客户端看不到中间过程。

原因:缓存 token 会在一轮轮采样循环中不断累积,把总量撑得虚高,于是压缩会被提前触发——真正把它顶上去的是缓存内容,而不是实际的对话历史

这个特性在下面这些场景里表现最好:

  • ✅ 客户端工具(比如本文这套客服 API)
  • ✅ 常规工具调用的标准智能体工作流
  • ✅ 文件操作、数据库查询、API 调用
  • ❌ 服务端扩展思考
  • ❌ 服务端网页搜索工具

信息损失

权衡取舍:摘要天然就会丢掉一部分信息。Claude 抓重点的能力不错,但总有些细节会被压掉或略去。

放到工单场景里看

  • 保住了:工单 ID、类别、优先级、团队、处理结果、进度状态
  • 丢掉了:知识库文章全文、起草回复的完整文本、分类时的详细推理过程

这通常是可以接受的——你并不需要把每篇知识库文章和每份回复全文一直留着,留下完成记录就够了。

怎么缓解

  • 用自定义摘要提示词把关键信息钉住
  • 对需要大量历史上下文的任务,把阈值调高
  • 把任务拆成模块化的结构(每个阶段接的是摘要,而不是原始细节)

什么时候不该用压缩

这几种情况就别开压缩了:

  1. 短任务:任务在 50k–100k token 内就能收尾的话,压缩纯属徒增开销
  2. 需要完整审计轨迹的任务:有些任务必须能回溯到此前的每一处细节
  3. 走服务端采样的工作流:如前所述,等官方把这个限制解决掉再说
  4. 高度依赖迭代打磨的任务:每一步都死死依赖前面所有步骤精确细节的那种

什么时候该用压缩

压缩在这些场景里最能发挥价值:

  1. 顺序处理:就像本文的工单流程,一件接一件地处理
  2. 多阶段工作流:每个阶段结束前都能先把进展总结一遍再往下走
  3. 迭代式数据处理:把大数据集分块处理,或者一次处理一个实体
  4. 长时间的分析任务:横跨大量实体做数据分析
  5. 批量操作:处理成百上千个彼此独立的条目

工单处理是压缩的绝佳用例,因为:

  • 每张工单的流程基本互不相干
  • 你要的是完成摘要,不是完整的工具返回结果
  • 天然存在压缩节点(处理完几张工单之后)
  • 整个流程本身就是迭代式、顺序式的

小结

自动上下文压缩是个很有分量的能力,它让长时间运行的智能体工作流得以突破通常的上下文上限。这篇 cookbook 借着一个客服工单处理流程,把压缩完整地走了一遍。

不妨在自己的工作流里试着接入压缩:

  1. 找出天然的压缩节点(处理完一个条目、走完一个阶段等等)
  2. 如果每个条目的边界很清晰,一开始就大胆用激进的阈值(5k–10k)
  3. 用自定义摘要提示词把关键信息保下来
  4. 盯住压缩的触发时机,确认质量没有滑坡
  5. 再根据自己的实际需要微调阈值

相关笔记