自动上下文压缩
理解和管理 Claude 的自动上下文压缩机制
🌐 查看英文原文 | 源码 Notebook
自动上下文压缩
长时间运行的代理任务往往超过上下文限制。工具密集型工作流程或长对话会迅速消耗令牌上下文窗口。在有效上下文工程:AI 代理的有效上下文工程中,我们讨论了如何管理上下文可以帮助避免性能下降和上下文退化。
Claude 代理 Python SDK 可以通过在令牌使用超过可配置阈值时自动压缩对话历史来帮助管理此上下文,允许任务在典型的 200k 令牌上下文限制之外继续。
在本食谱中,我们将通过一个 代理客户服务工作流程 来演示上下文压缩。想象你已经构建了一个 AI 客户服务代理,负责处理支持票务队列。对于每张票务,你必须对问题进行分类、搜索知识库、设置优先级、路由到适当的团队、起草回复,并将其标记为完成。随着你处理一张又一张的票务,对话历史中充满了分类、知识库搜索和起草的回复——迅速消耗数千个令牌。
什么是上下文压缩?
当使用工具构建代理工作流程时,随着代理在复杂任务上的迭代,对话可能会变得非常大。compaction_control 参数通过以下方式提供自动上下文管理:
- 监控对话中每轮的令牌使用情况
- 当超过阈值时,注入一个总结提示作为用户轮次
- 让模型生成一个包含
<summary></summary>标签的总结。这些标签不会被解析,但它们在那里以帮助引导模型。 - 清除对话历史并仅使用总结继续
- 使用压缩的上下文继续任务
在本教程结束时,你将能够:
- 理解如何在迭代工作流程中有效管理上下文限制
- 编写利用自动上下文压缩的代理
- 设计能够在多次迭代中保持专注的工作流程
前置条件
在遵循本指南之前,请确保您具备以下条件:
所需知识
- 对代理模式和工具调用的基本理解
所需工具
- Python 3.11 或更高版本
- Anthropic API 密钥
- Anthropic SDK >= 0.74.1
使用 Opus 4.6? 我们建议使用 服务器端压缩,它自动处理上下文管理,无需任何 SDK 级别配置。
本食谱涵盖 基于 SDK 的压缩,如果您使用的是较旧的模型或希望使用不同的(更便宜)模型进行摘要,这很有用。
环境配置
首先,安装所需的依赖项:python # %pip install -qU anthropic python-dotenv注意:确保您的 .env 文件包含:
ANTHROPIC_API_KEY=your_key_here
加载您的环境变量并配置客户端。我们还会加载一个辅助工具来可视化 Claude 消息响应。
def load_environment_variables():
# 加载环境变量
pass
def configure_client():
# 配置客户端
pass
def load_helper_utility():
# 加载辅助工具
pass
``````python
from dotenv import load_dotenv
load_dotenv()
MODEL = "claude-sonnet-4-6"
```## 准备工作
在 [utils/customer_service_tools.py](utils/customer_service_tools.py) 文件中,我们定义了几个用于处理客户服务工单的函数:
- `get_next_ticket()` - 从队列中检索下一个未处理的工单
- `classify_ticket(ticket_id, category)` - 将问题分类为账单、技术、账户、产品或运输
- `search_knowledge_base(query)` - 查找相关的帮助文章和解决方案
- `set_priority(ticket_id, priority)` - 分配优先级(低、中、高、紧急)
- `route_to_team(ticket_id, team)` - 将工单路由到相应的支持团队
- `draft_response(ticket_id, response_text)` - 创建面向客户的响应
- `mark_complete(ticket_id)` - 完成已处理的工单
对于客户服务代理来说,这些工具能够系统地处理工单。每个工单都需要分类、研究、优先级排序、路由和响应草稿。当连续处理20-30个工单时,对话历史会充满来自每个分类、每个知识库搜索和每个草稿响应的工具结果,导致线性token增长。
在工具上使用 `beta_tool` 装饰器,使其对Claude代理可用。装饰器提取函数参数和文档字符串,并将这些信息作为工具元数据提供给Claude。```python
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. 分类问题类别(账单、技术、账户、产品、运输)
3. 在知识库中搜索相关信息
4. 设置适当的优先级(低、中、高、紧急)
5. 路由到正确的团队
6. 起草客户回复
7. 标记工单完成
8. 移动到下一个工单
**挑战**:当队列中有5个工单,每个工单需要7次工具调用时,Claude将进行35次或更多的工具调用。每个步骤的结果,包括分类、知识库搜索和起草的回复,都会累积在会话历史中。在不进行压缩的情况下,所有这些数据都会保留在每个工单的内存中,到第5个工单时,上下文将包括前4个工单的所有详细信息。
让我们首先在不进行压缩的情况下运行这个工作流程,并观察会发生什么:
```html
::: callout-attention
请注意,以下代码示例仅供参考,实际应用时可能需要根据具体情况进行调整。
:::
```python
def process_ticket():
ticket = get_next_ticket()
category = classify_issue(ticket)
info = search_knowledge_base(category)
priority = set_priority(ticket)
team = route_to_team(ticket)
response = draft_customer_response(ticket, info)
mark_ticket_complete(ticket)
move_to_next_ticket()
# 主循环
while True:
process_ticket()
``````python
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。
这导致token消耗量高,并且可能很快就会达到上下文限制。到第27轮时,我们已经有150,000个输入token,只是为了处理5张票据。
让我们回顾一下Claude在处理完所有5张票据且不进行压缩后的最终响应:
```python
def process_tickets(tickets):
# 处理票据的代码
pass
# 示例票据列表
tickets = ["ticket1", "ticket2", "ticket3", "ticket4", "ticket5"]
# 处理票据
process_tickets(tickets)
请注意,上述代码仅为示例,实际处理逻辑可能有所不同。
{
"tickets": ["ticket1", "ticket2", "ticket3", "ticket4", "ticket5"],
"status": "processed"
}SELECT * FROM tickets WHERE status = 'processed';<div id="tickets">
<h2>已处理票据</h2>
<ul>
<li>票据1</li>
<li>票据2</li>
<li>票据3</li>
<li>票据4</li>
<li>票据5</li>
</ul>
</div>
``````python
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后,我们只需要一个 **简要总结**(工单已解决、类别、优先级)——而不是完整的分类结果、知识库搜索和完整的草稿响应。详细的流程应该被丢弃,只保留完成总结。
让我们看看自动上下文压缩是如何解决这个问题。
## 启用自动上下文压缩
让我们运行完全相同的客户服务工作流程,但启用自动上下文压缩。我们只需将 `compaction_control` 参数添加到我们的工具运行器中。
`compaction_control` 参数有一个必填字段和几个可选字段:
- **`enabled`**(必填):布尔值,用于开启/关闭压缩
- **`context_token_threshold`**(可选):触发压缩的token计数(默认:100,000)
- **`model`**(可选):用于摘要的模型(默认为主模型)
- **`summary_prompt`**(可选):用于生成摘要的自定义提示
::: callout-注意
请注意,启用自动上下文压缩可能会影响某些工具的性能,特别是那些需要大量上下文信息的工具。
:::
对于这个客户服务工作流程,我们将使用一个 **5,000个token阈值**。这意味着在处理了几个工单压缩后,将自动触发。这允许Claude执行以下操作:
1. **保留完成摘要**(已解决的工单、类别、结果)
2. **丢弃详细工具结果**(完整的知识库文章、完整的分类、草拟的回复文本)
3. **在处理下一批工单时从头开始**
这模仿了真实支持代理的工作方式:解决工单,简要记录,然后转到下一个案例。
```python
def process_tickets(ticket_list):
# 处理工单列表
pass
{
"tickets": [
// 工单列表
]
}SELECT * FROM tickets WHERE status = 'open';
``````python
# Re-initialize queue and run with compaction
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:
# We can identify compaction when the message count decreases
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)
### 后续步骤
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使用量的减少。
与基线版本相比,我们只使用了79,000个token。我们还打印出了每次压缩事件后生成的摘要信息,展示了Claude如何有效地将之前的工单细节压缩成摘要。
让我们看看启用压缩后处理所有5个工单的最终响应。
```markdown
With automatic context compaction enabled, we can see that our token usage per turn does not grow linearly, but is reduced after each compaction event. There were two compaction events during the processing of tickets, and the follow turn shows a reduction in total token usage.
Compared to the baseline version, we only used 79,000 tokens. We've also printed out the summary messages generated after each compaction event, showing how Claude effectively condensed prior ticket details into summaries.
Let's look at the final response after processing all 5 tickets with compaction enabled.
```python
def process_tickets():
# ... 代码逻辑 ...
pass
process_tickets()
{
"tickets": [
// ... 工单数据 ...
]
}SELECT * FROM tickets WHERE status = 'open';请注意,以下代码块是示例,实际应用中可能需要根据具体情况进行调整。 :::python 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! 🎊
```### 比较结果
启用压缩后,我们可以清楚地看到两次运行在令牌节省方面的差异,同时保留了工作流程和最终摘要的质量。
以下是自动上下文压缩带来的变化:
1. **上下文在处理多个工单后重置** - 当处理5-7个工单生成5k+个工具结果令牌时,SDK会自动:
- 注入一个摘要提示
- 让Claude生成一个包含`<summary></summary>`标签的完成摘要
- 清除对话历史并丢弃详细分类、知识库搜索和响应
- 仅继续使用完成摘要
2. **输入令牌保持界限** - 在处理更多工单时,输入令牌不会累积到100k+,而是在每次压缩后重置。当处理第5号工单时,我们不会携带第1-4号工单的全部工具结果。
3. **任务成功完成** - 工作流程顺利地通过所有工单,没有触及上下文限制
4. **质量得到保留** - 摘要保留了关键信息:
- 处理的工单及其ID
- 分配的类别和优先级
- 路由到的团队
- 总体进度状态
所有工单仍然得到适当的分类、优先级排序、路由和响应。
5. **自然的工作流程** - 这反映了真实支持代理的工作方式:解决一个工单,简要记录在系统中,关闭它,然后转到下一个。您在处理新工单时不会保留每个知识库文章和完整的响应草稿。
让我们可视化令牌节省:```python
# Compare baseline vs compaction
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)
# Calculate savings
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` 检测到令牌使用量超过阈值时,它会自动执行以下操作:
1. **在执行下一个API调用之前暂停工作流程**
2. **注入一个总结请求**,作为用户消息请求Claude总结进度
3. **生成总结** - Claude生成一个包含以下内容的总结,并用 `<summary></summary>` 标签包裹:
- **完成的工单**:已解决的工单的简要记录(ID、类别、优先级、结果)
- **进度状态**:已处理的工单数量,剩余工单数量
- **关键模式**:工单中的任何显著趋势
- **下一步行动**:下一步要做什么(继续处理剩余的工单)
4. **清除历史记录** - 整个对话历史(包括所有工具结果)被仅包含总结的内容所替换
5. **恢复处理** - Claude继续使用压缩后的上下文,处理下一批工单
## 自定义压缩配置
您可以根据具体的使用场景自定义压缩的工作方式。以下是关键配置选项:
### 调整阈值
`context_token_threshold` 决定了何时触发压缩:```python
compaction_control={
"enabled": True,
"context_token_threshold": 5000, # Compact after processing 5-7 tickets
}
```阈值不应设置得太低,否则摘要本身可能会触发压缩。为了演示目的,我们设置了5,000个token的阈值,但在实际操作中,请尝试不同的设置以找到最适合您工作流程的配置。
以下是一些一般性指南:
- **低阈值(5k-20k)**:
- 用于具有明确边界的迭代任务处理
- 更频繁的压缩,最小化上下文积累
- 适用于顺序实体处理
- **中等阈值(50k-100k)**:
- 多阶段工作流程,具有较少的较大自然检查点
- 在上下文保留和管理之间取得平衡
- 适用于具有昂贵工具调用的工作流程
- **高阈值(100k-150k)**:
- 需要大量历史上下文的任务
- 较少压缩可以保留更多原始细节
- 每次调用成本更高,但压缩次数更少
- **默认(100k)**:适用于一般长时间运行任务的良好平衡
**对于工单处理**:5k的阈值效果很好,因为每个工单的工作流程都会生成大量的工具结果,但工单是独立的。在解决工单A之后,处理工单B时不需要其详细的KB搜索。
### 使用不同的模型进行摘要
您还可以使用更快/更便宜的模型来生成摘要:```python
compaction_control={
"enabled": True,
"model": "claude-haiku-4-5", # Use Haiku for cost-effective summaries
}
```### 自定义摘要提示
您可以为生成摘要提供自定义提示,以指导摘要的生成方式。这在需要保留特定类型信息的客户服务流程中特别有用。
例如,我们可以根据我们的需求定义一个自定义提示:
- **工单摘要**:针对所有完成的工单
- **分配的** **类别和优先级**
- **路由到的** **团队**
- **进度状态**(完成的工单,剩余工单)
- **工作流程中的** **下一步**
```html
::: callout-notice
请注意,自定义提示应简洁明了,以便于用户理解。
::: callout-notice
``````python
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. 使用压缩上下文继续对话
让我们看看如何实现这一点:```python
#!/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.
"""
# Configuration
COMPACTION_THRESHOLD = 3000 # Compact when tokens exceed this (low for demo purposes)
# Structured summarization prompt for compaction
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."""
# Message history
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)
# Simulate a conversation for demo purposes
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}")
# Add user message
messages.append({"role": "user", "content": user_input})
# Get Claude's response
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]} ...")
# Check if we should compact
usage = response.usage
# Calculate total tokens (includes cache tokens)
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)
# Get summary using structured prompt
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"
)
# Replace history with summary
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 ```### 理解聊天循环模式
上面的例子演示了在对话环境中的手动压缩。以下是其工作原理:
关键组件:
- Token 跟踪:在每次响应后,计算总 tokens(输入 + 输出 + 缓存 tokens)
- 阈值检查:当总 tokens 超过阈值时,触发压缩
- 摘要请求:向 Claude 发送相同的结构化 SUMMARY_PROMPT
- 历史替换:用摘要替换整个消息历史
- 继续:下一个用户消息基于摘要构建,而不是完整的历史
何时使用此模式:
- 扩展头脑风暴会议:用户与 Claude 进行多轮探索想法
- 学习对话:跨越数十次交换的教程或解释
- 迭代改进:用户对草稿、设计或解决方案提供反馈
- 聊天应用:任何多轮对话界面
与工具运行器的关键区别:
| 方面 | 工具运行器(自动) | 聊天循环(手动) |
|---|---|---|
| 触发 | 达到阈值时自动 | 您实现阈值检查 |
| 摘要 | SDK 处理摘要请求 | 您进行显式的 API 调用 |
| 历史管理 | SDK 替换消息 | 您手动替换列表 |
| 用例 | 带有工具的代理工作流程 | 用户驱动的对话 |
生产注意事项:
- 调整阈值:在实际应用中使用更大的阈值
- 自定义摘要提示:根据您的对话类型(头脑风暴 vs. 技术支持 vs. 辅导)进行定制
- 显示用户指示:显示类似“正在总结对话…”的消息,以便用户理解暂停
- 保留关键上下文:确保摘要提示捕获用户关心的特定领域信息
此模式让您完全控制压缩何时以及如何发生,使其成为 SDK 的自动工具运行器压缩不可用时的对话应用的理想选择。 ## 限制和注意事项
虽然自动上下文压缩功能强大,但了解以下重要限制是必要的:
服务器端采样循环
当前限制:压缩与服务器端采样循环(如服务器端网页搜索工具)配合使用时效果不佳。
原因:缓存标记会在采样循环中累积,这可能导致基于缓存内容而非实际对话历史提前触发压缩。
此功能与以下情况配合使用最佳: - ✅ 客户端工具(如本食谱中的客户服务API) - ✅ 标准化代理工作流程,常规工具使用 - ✅ 文件操作、数据库查询、API调用 - ❌ 服务器端扩展思维 - ❌ 服务器端网页搜索工具
信息丢失
权衡:摘要本质上会丢失一些信息。虽然Claude擅长识别关键点,但一些细节可能会被压缩或省略。
在工单处理中: - ✅ 保留:工单ID、类别、优先级、团队、结果、进度状态 - ❌ 丢失:完整知识库文章文本、完整的草稿回复文本、详细的分类推理
这通常是可接受的,您不需要永久保留每个KB文章和完整的回复文本,只需完成记录即可。
缓解措施: - 使用自定义摘要提示来保留关键信息 - 为需要广泛历史背景的任务设置更高的阈值 - 将任务结构化以模块化(每个阶段基于摘要构建,而不是原始细节)
避免压缩的情况
避免以下情况使用压缩:
- 短任务:如果您的任务在50k-100k标记内完成,压缩会增加不必要的开销
- 需要完整审计跟踪的任务:一些任务需要访问所有之前的详细信息
- 服务器端采样工作流程:如上所述,等待此限制得到解决
- 高度迭代优化:每个步骤都严重依赖于所有之前步骤的精确细节的任务
使用压缩的情况
压缩非常适合: 1. 顺序处理:类似于我们的票务工作流程——一个接一个地处理多个项目 2. 多阶段工作流程:每个阶段都可以在继续之前总结进度 3. 迭代数据处理:分块或逐个实体处理大型数据集 4. 扩展分析会话:跨多个实体分析数据 5. 批量操作:处理数百个独立的项目
票务处理是一个完美的用例,因为: - 每个票务工作流程在很大程度上是独立的 - 您需要完成摘要,而不是完整的工具结果 - 存在自然压缩点(在完成几个票务后) - 工作流程是迭代和顺序的
总结
自动上下文压缩是一个强大的功能,它使长时间运行的代理工作流程能够超过典型的上下文限制。在本指南中,我们通过客户服务票务处理工作流程探讨了压缩。
后续步骤
尝试在自己的工作流程中实现压缩: 1. 确定自然压缩点(在处理每个项目、完成每个阶段等之后) 2. 如果您有清晰的每个项目的边界,可以从激进的阈值(5k-10k)开始 3. 使用自定义摘要提示来保留关键信息 4. 监控压缩触发的时间,并验证质量是否得到保持 5. 根据您的具体需求调整阈值
有关有效上下文管理的更多信息,请参阅有效上下文工程用于AI代理。
中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。