在「8 小时级连续编程会话」里,上下文会同时面临两类膨胀:
- 单次工具结果过大:一次
ReadFile可能返回数万字符的日志或源码。 - 历史累积过长:上百轮 Thought/Action/Observation 累积,最终超出窗口。
如果用「单一机制」处理:只做历史摘要会放过超大单条结果;只做落盘又会丢失历史脉络。KKCode 因此把压缩拆成两层,各司其职。
核心实现见 kkcode/context/manager.py。
persist_tool_result():将超大工具结果写入sessions/tool-results/,上下文里仅保留约 2KB 预览 + 引用指针。apply_tool_result_budget():以 200KB 为上限,超出即触发落盘替换。- 幂等:同一结果可重复写入而不产生副作用,保证恢复时能稳定重建。
Layer 1 解决「单条太大」,不关心历史多长。
当历史总量逼近窗口时触发 auto_compact(),采用双阈值策略:
| 阈值 | 取值 | 含义 |
|---|---|---|
| 软边距 | 13K token | 超过即开始准备压缩 |
| 硬边距 | 3K token | 必须完成压缩,否则拒绝继续 |
摘要使用结构化的 SUMMARY_PROMPT,要求模型产出 <analysis> + <summary> 两段,便于后续检索与恢复。
为避免摘要「丢失最近的关键上下文」,压缩时强制保留:
KEEP_RECENT_TOKENS = 10K:最近 10K token 原文不摘要;MIN_KEEP_MESSAGES = 5:至少保留最近 5 条消息。
长会话中压缩本身也可能失败(例如摘要请求再次超窗),因此配套三道保险:
class CompactCircuitBreaker:
def __init__(self, max_failures: int = 3): ...连续失败 3 次 后暂停自动压缩,避免「压缩请求本身反复消耗 token」的雪崩。该熔断器已在 kkcode/agent.py 的多处接入(压缩前/后及主循环),而非仅在 manager 内孤立存在。
_group_messages_by_turn() 将消息按轮次分组;摘要请求失败时按 max_retries = 3 进行 PTL(Partial/Turn-Level)重试,只重发相关轮次而非整段历史。
class RecoveryState: ...
def build_recovery_attachment(state: RecoveryState | None, tool_schemas): ...压缩中断或崩溃时,RecoveryState + build_recovery_attachment() 重建一份「恢复段」附加到新上下文,确保会话可从断点无损续接。
工具结果 ──Layer1──> 落盘 + 2KB 预览
│
历史累积 ──Layer2──> 双阈值触发 auto_compact()
│
┌─────────────┼───────────────────────┐
│ │ │
SUMMARY_PROMPT 保尾窗口(10K/5) CompactCircuitBreaker
│ │ │
<analysis>+ 保留近期原文 失败3次暂停
<summary> │
│ │
PTL 重试(max=3) ◄───────────────────────┘
│
RecoveryState 续接
| 文件 | 关键符号 |
|---|---|
kkcode/context/manager.py |
persist_tool_result, apply_tool_result_budget, auto_compact, SUMMARY_PROMPT, CompactCircuitBreaker, RecoveryState, build_recovery_attachment, KEEP_RECENT_TOKENS, MIN_KEEP_MESSAGES |
kkcode/agent.py |
熔断器三处接入点(压缩前/后/主循环) |
返回:核心机制详解