第 7 章:上下文窗口管理

上下文窗口是 Agent 的"工作台":太小放不下材料,太大成本爆炸。管理好它,Agent 才能在复杂任务中保持清醒。 但这不是魔法——压缩会丢信息,预算会限制能力。没有免费的午餐。


你让 Agent 帮你调试一个生产问题。

对话进行了 50 轮,它终于定位到是数据库连接池配置不对。

然后你问:"刚才你说的连接池配置是什么来着?"

它回答:"抱歉,我不记得了。"

你愣住了。

50 轮对话,烧了几万 token,结果它把关键信息忘了?

这不是 Agent 的问题,是上下文管理没做好


7.1 问题在哪?

LLM 有一个叫"上下文窗口"的东西——你可以把它理解成 Agent 的"工作台"。

工作台上能放多少东西,取决于窗口大小。

⚠️ 时效性提示 (2026-01): 模型上下文窗口和定价频繁变化,以下为示意。请查阅官方文档获取最新信息。

模型上下文窗口换算成字数(粗估)
GPT-4o128K tokens~50万字
Claude 3.5 Sonnet200K tokens~80万字
Gemini 1.5 Pro2M tokens~800万字
常见开源模型4K - 32K~1.6-13万字

看起来挺大?800 万字,一本书都放得下。

但你想想实际场景:

  • 50 轮对话,每轮平均 500 tokens
  • 系统提示词(System Prompt)占 2000 tokens
  • 工具定义(每个工具约 200 tokens,10 个工具就是 2000)
  • 历史记录还在持续增长...

很快就撑满了。

更要命的是,每个 token 都是钱:

每轮对话 500 tokens × 50  = 25,000 tokens(输入)
GPT-4o 输入价格 $2.5/百万 tokens
50 轮对话的输入成本  $0.0625

听起来不多?但这只是一个会话。
如果每天 10 万个会话,每个 50 ...
每天成本 = $6,250 = 每月 $187,500

而且这还没算输出 token(更贵)。

上下文管理要解决四个核心问题:

问题后果解决方向
超限请求直接失败,用户体验崩溃压缩、裁剪
成本历史越长,每次请求越贵预算控制
信息丢失关键上下文被截掉,Agent 犯傻智能保留
噪音干扰太多无关信息,降低回答质量相关性过滤

这四个问题互相矛盾:压缩了会丢信息,不压缩会超限和超预算。

没有完美的方案,只有取舍。


7.2 Token:Agent 的计量单位

在讲解决方案之前,先搞清楚什么是 token。

什么是 Token?

⚠️ 时效性提示 (2026-01): Token 计数依赖具体 tokenizer(GPT-4 用 cl100k_base,Claude 用自有分词器)。以下换算为约数,实际使用请调用 tiktoken 或对应 SDK 的 token 计数 API。

Token 不是字符,也不是单词。它是 LLM 用来"切分"文本的最小单位。

英文: "Hello world"  ["Hello", " world"]  2 tokens
中文: "你好世界"  ["", "", "", ""]  4 tokens(大概)
代码: "function foo() {}"  ["function", " foo", "()", " {}"]  4 tokens

不同语言的 token 效率不同:

语言平均 token/字符说明
英文~4 字符/token按词根切分
中文~1.5 字符/token每个汉字约 1-2 token
代码~3 字符/token符号多,token 密度高

这意味着:同样的语义内容,中文可能比英文消耗更多 token。

为什么不用精确计数?

精确计算 token 需要调用 tokenizer,那样太慢了(每次都要调 API 或加载模型)。

Shannon 用了一个实测足够准的估算方法:

// 简化版 token 估算
func EstimateTokens(messages []Message) int {
    total := 0
    for _, msg := range messages {
        // 每 4 个字符约 1 个 token
        total += len([]rune(msg.Content)) / 4
        // 每条消息有格式开销(role, content 结构)
        total += 5
    }
    // 加 10% 安全边际
    return int(float64(total) * 1.1)
}
组成部分估算方式说明
普通文本字符数 / 4标准 GPT 估算
消息格式每条 +5role/content 结构开销
代码字符数 / 3代码 token 密度更高
安全边际+10%防止估算偏小

我测试过,这个估算误差在 10-15% 以内,对于预算控制来说够用了。


7.3 滑动窗口压缩

这是 Shannon 的核心策略:保留重要的,压缩中间的

核心思路

原始对话历史 (500条消息,约 100K tokens)
                
    [前3条] + [中间压缩成摘要] + [后20条]
                
压缩后 (23条消息 + 摘要,约 15K tokens)

为什么这样设计?

  • 前 3 条(Primers):保留上下文建立。用户最开始说了什么需求,系统做了什么设定。
  • 中间部分(Summary):压缩成语义摘要。关键决策、发现、待解决问题。
  • 后 20 条(Recents):保留最近对话。保持连贯性,用户说的"刚才那个"能找到。

什么时候触发压缩?

不是每次都压缩——那样太浪费计算资源。Shannon 的策略是:

预算使用率 >= 75%    触发压缩
压缩目标           预算的 37.5%

比如预算是 50K tokens,用到 37.5K 就开始压缩,压到 18.75K 左右。

为什么是 75% 和 37.5%?

  • 75%:留 25% 余量给当前轮的输入输出
  • 37.5%:压到一半以下,留更多空间给后续对话

这些数字不是死的,可以根据场景调。调试场景可以把触发阈值调高(比如 85%),让 Agent 看到更多历史。

压缩的代价

压缩会丢信息。这是必须承认的。

一段 10000 字的对话,压缩成 500 字的摘要,不可能保留所有细节。

摘要能保留的是:

  • 关键决策点
  • 重要发现
  • 待解决问题
  • 实体和关系

摘要会丢失的是:

  • 具体的代码细节
  • 中间的试错过程
  • 用户的情绪表达
  • 上下文暗示

所以压缩不是万能药。有些场景(比如代码调试),你需要更大的上下文窗口,而不是更激进的压缩。


7.4 三段式保留策略

三段式保留策略

Primers:为什么保留开头?

对话的"上下文"往往在开头建立。

用户第一句话可能是:"我在调试一个 Kubernetes 集群的网络问题,Pod 之间无法互相访问。"

这个背景信息对整个对话都很重要。如果丢了,Agent 后续可能给出完全不相关的建议。

默认保留开头 3 条:

primers_count: 3  # 可配置

什么场景需要调整?

  • 多轮确认场景:用户可能前 5 轮都在确认需求,可以调到 5
  • 简单问答场景:第一句话就是完整问题,可以调到 1

Summary:中间的压缩

中间那些对话,用 LLM 压缩成一段摘要。

Shannon 调用 llm-service 的 /context/compress 端点:

# llm-service 侧的压缩实现(概念示例)
async def compress_context(messages: list, target_tokens: int = 400):
    prompt = f"""Compress this conversation into a factual summary.

Focus on:
- Key decisions made
- Important discoveries
- Unresolved questions
- Named entities and their relationships

Keep the summary under {target_tokens} tokens.
Use the SAME LANGUAGE as the conversation.

Conversation:
{format_messages(messages)}
"""

    result = await providers.generate_completion(
        messages=[{"role": "user", "content": prompt}],
        tier=ModelTier.SMALL,  # 用小模型,省钱
        max_tokens=target_tokens,
        temperature=0.2,  # 低温度,保证准确
    )
    return result["output_text"]

摘要长这样:

Previous context summary:
用户正在调试一个 Kubernetes 网络问题。关键发现:
- Pod 无法访问外部服务
- CoreDNS 配置正常
- NetworkPolicy 存在限制
待解决:确认 NetworkPolicy 规则的具体配置

Recents:保持对话连贯

保留最近的 20 条消息。

这是"热"上下文——用户最近说了什么、Agent 最近回答了什么,都在这里。

用户说"刚才那个方案",Agent 能在 Recents 里找到。

recents_count: 20  # 可配置

调整建议:

  • 调试场景:调到 30-50,保留更多细节
  • 简单问答:调到 10,省 token

7.5 Token 预算管理

光有压缩还不够。还需要预算控制——设定硬上限,超了就停。

多层预算架构

Shannon 实现了三层预算控制:

多层预算架构

  • Session 预算:整个会话的总预算
  • Task 预算:单次任务的预算
  • Agent 预算:单个 Agent 的预算(多 Agent 场景)

为什么要分层?

一个复杂任务可能调用多个 Agent(研究、分析、写作)。如果只有一个总预算,第一个 Agent 可能把预算烧光,后面的 Agent 没得用。

分层预算让每个 Agent 都有自己的额度,不会相互挤占。

预算检查逻辑

每次请求前检查预算:

func (bm *BudgetManager) CheckBudget(sessionID string, estimatedTokens int) *BudgetCheckResult {
    budget := bm.sessionBudgets[sessionID]
    result := &BudgetCheckResult{CanProceed: true}

    // 检查是否超限
    if budget.TaskTokensUsed + estimatedTokens > budget.TaskBudget {
        if budget.HardLimit {
            result.CanProceed = false
            result.Reason = "Task budget exceeded"
        } else {
            result.RequireApproval = budget.RequireApproval
            result.Warnings = append(result.Warnings, "Will exceed budget")
        }
    }

    // 检查警告阈值(比如 80% 时发警告)
    usagePercent := float64(budget.TaskTokensUsed) / float64(budget.TaskBudget)
    if usagePercent > budget.WarningThreshold {
        bm.emitWarningEvent(sessionID, usagePercent)
    }

    return result
}

硬限制 vs 软限制 vs 审批模式

模式行为适用场景
硬限制超预算直接拒绝成本敏感、API 服务
软限制超预算发警告,继续执行任务优先、内部工具
审批模式超预算暂停,等人工确认需要人工把关的场景

选哪个?看你的场景:

  • 对外 API:硬限制,防止单个用户烧光资源
  • 内部调试:软限制,任务能完成更重要
  • 关键任务:审批模式,超预算了让人确认是否继续

7.6 背压机制

当预算压力增大时,不是突然停止,而是渐进式限流。

func calculateBackpressureDelay(usagePercent float64) time.Duration {
    switch {
    case usagePercent >= 0.95:
        return 1500 * time.Millisecond  // 重度限流
    case usagePercent >= 0.9:
        return 750 * time.Millisecond
    case usagePercent >= 0.85:
        return 300 * time.Millisecond
    case usagePercent >= 0.8:
        return 50 * time.Millisecond    // 轻微限流
    default:
        return 0                         // 正常执行
    }
}

这样做的好处:

  1. 用户感知:响应变慢了,知道该省着点用
  2. 平滑降级:不是突然断掉,有缓冲时间
  3. 自动恢复:用量下来后自动恢复正常

背压不是万能的。如果用户不理会变慢的信号继续猛烧,最终还是会触发硬限制。


7.7 配置最佳实践

场景化配置

不同场景需要不同的配置:

普通问答场景

session:
  context_window_default: 30
  token_budget_per_agent: 25000
  primers_count: 2
  recents_count: 10

特点:预算小,窗口小,省钱。适合简单问答、客服场景。

调试/开发场景

session:
  context_window_debugging: 100
  token_budget_per_agent: 75000
  primers_count: 5
  recents_count: 30

特点:预算大,窗口大,保留更多上下文。适合代码调试、复杂分析。

长时任务场景

session:
  context_window_default: 75
  token_budget_per_task: 300000
  max_history: 1000

特点:任务预算大,历史条数多。适合研究任务、文档生成。

请求级覆盖

有时候需要在请求级别覆盖配置:

{
  "query": "Fix the race condition in worker pool",
  "context": {
    "use_case_preset": "debugging",
    "history_window_size": 100,
    "token_budget_per_agent": 60000
  }
}

优先级顺序:

  1. 请求级覆盖(最高)
  2. 用例预设(use_case_preset)
  3. 环境变量
  4. 配置文件默认值

7.8 压缩效果

实测数据:

场景原始 Token压缩后压缩率说明
50 条消息~10k无压缩0%未触发阈值
100 条消息~25k~12k52%轻度压缩
500 条消息~125k~15k88%重度压缩
1000 条消息~250k~15k94%极限压缩

压缩开销:

操作耗时说明
Token 估算<5ms本地计算
历史整形<1ms切片操作
摘要生成(LLM)200-500ms主要开销
总压缩开销~500ms只在触发时执行

摘要生成是最慢的,但只在触发压缩时才跑,不是每次请求都跑。


7.9 常见的坑

坑 1:压缩阈值设太高

# 太晚触发,没留余量给当前轮
COMPRESSION_TRIGGER_RATIO=0.95

# 留够余量(推荐)
COMPRESSION_TRIGGER_RATIO=0.75

后果:触发压缩时已经接近限额,压缩完还是不够用,请求失败。

坑 2:Primers/Recents 配置不当

# Primers 太少,丢失初始上下文
primers_count: 1
recents_count: 50

# 平衡配置(推荐)
primers_count: 3
recents_count: 20

后果:Primers 太少,Agent 忘记最初的需求;Recents 太多,摘要太短,中间信息全丢。

坑 3:硬限制 + 小预算

# 预算太小,硬限制导致任务经常失败
token_budget_per_agent: 10000
hard_limit: true

# 合理预算 + 软限制(推荐)
token_budget_per_agent: 50000
hard_limit: false
require_approval: true

后果:稍微复杂点的任务就超预算,用户体验极差。

坑 4:会话 ID 不一致

# 每次请求用新会话,历史不连续
SESSION_ID="session-$(date +%s)"

# 相关工作用同一个会话 ID(推荐)
SESSION_ID="feature-xyz-debug"

后果:每次请求都是新会话,上下文窗口管理形同虚设,历史完全不连续。

坑 5:忽略压缩质量

压缩摘要的质量取决于:

  • 摘要模型的能力
  • 压缩 prompt 的设计
  • 目标 token 数的合理性

后果:摘要太短,关键信息丢失;摘要太笼统,没有可操作的细节。

建议:定期抽检压缩摘要的质量,确保关键信息被保留。


Shannon Lab(10 分钟上手)

本节帮你在 10 分钟内把本章概念对应到 Shannon 源码。

必读(1 个文件)

  • docs/context-window-management.md:重点看 "Sliding Window Compression"、"Token Budget Management"、"Configuration" 部分,理解压缩触发条件和多层预算

选读深挖(2 个,按兴趣挑)


练习

练习 1:配置优化

你有一个代码调试场景,用户经常抱怨"Agent 忘记了之前说的东西"。

写出你的配置调整方案:

  • 要调整哪些参数?
  • 为什么这样调整?
  • 有什么代价?

练习 2:源码阅读

context_compress.go,回答:

  1. redactPII 函数处理了哪些类型的敏感信息?
  2. 为什么用 []rune 而不是直接用 string 截断?
  3. 如果 embedding 服务不可用,会发生什么?

练习 3(进阶):设计新功能

设计一个"重要消息标记"功能:用户可以标记某些消息为"重要",压缩时这些消息不能被删除。

思考:

  • 数据结构怎么设计?
  • 压缩逻辑怎么修改?
  • 如果标记的消息太多,超过预算怎么办?

划重点

核心就一句话:上下文窗口是 Agent 的工作台,管不好会遗忘、会超限、会烧钱

要点:

  1. Token 估算:约 4 字符/token + 每消息 5 token 格式开销
  2. 滑动窗口压缩:Primers(前3) + Summary(中间压缩) + Recents(后20)
  3. 多层预算:Session - Task - Agent 三级控制
  4. 背压机制:随使用率渐进式增加延迟
  5. 压缩是有损的:关键信息可能丢失,不是万能药

上下文窗口管理解决了"短期记忆"问题——单次对话内的上下文。但 Agent 如何在跨会话、跨任务之间保持"长期记忆"?

下一章我们来聊记忆架构——如何通过向量数据库实现语义检索和知识积累。

准备好了?往下走。


延伸阅读

引用本文 / Cite
Zhang, W. (2026). 第 7 章:上下文窗口管理. In AI Agent 架构:从单体到企业级多智能体. https://waylandz.com/ai-agent-book/第07章-上下文窗口管理
@incollection{zhang2026aiagent_第07章_上下文窗口管理,
  author = {Zhang, Wayland},
  title = {第 7 章:上下文窗口管理},
  booktitle = {AI Agent 架构:从单体到企业级多智能体},
  year = {2026},
  url = {https://waylandz.com/ai-agent-book/第07章-上下文窗口管理}
}