KEEL · 龙骨 · A CURRICULUM FOR THE AI ERA

01. 上下文窗口究竟限制了什么? — keel 龙骨

支付服务的事故诊断已经运行到第二十轮。用户问的是“服务恢复了吗,和上次事故有关吗”,模型却突然回答:“请提供工单号。”日志显示,INC-42 明明在第六轮出现过,程序也保存了全部消息。

支付服务的事故诊断已经运行到第二十轮。用户问的是“服务恢复了吗,和上次事故有关吗”,模型却突然回答:“请提供工单号。”日志显示,INC-42 明明在第六轮出现过,程序也保存了全部消息。

很多团队会把这个现象归因于“模型没有记忆”,然后换一个更大窗口的模型。这个判断太快了。模型可能遇到了两类完全不同的问题:

本章先建立整门课最重要的边界:Context 不是 Agent 的全部记忆,而是某一次模型调用的工作集。

1. 先看一次调用真正经过了什么

在事件诊断 Agent 中,完整状态可能保存在数据库或 checkpoint:

run_id = run-42
原始目标 = 判断支付服务是否恢复,并引用证据
事件历史 = 20 轮消息 + 8 次工具调用 + 2 次 worker 重启
当前事实 = error_rate 0.1%,订单仍失败
历史事故 = INC-42
审批状态 = 只读诊断,无写操作

模型并不会自动读取这份状态。Harness 在每一轮调用前,要从不同来源构造一次请求:

flowchart LR
    A[运行状态 / checkpoint] --> E[Context Builder]
    B[当前用户目标] --> E
    C[工具 schema] --> E
    D[检索与记忆候选] --> E
    E --> F[本轮模型输入]
    F --> G[模型输出]
    G --> A

这一步是“装配”,不是“把历史全部拼上去”。数据库里有某条事实,只表示它可以成为候选;只有通过权限、相关性、依赖和预算检查后,它才进入本轮 context。

2. 四个经常被混用的边界

名称 它回答的问题 典型负责人
Model context window 一次请求最多能容纳多少 token 模型与供应商配置
Request input 本轮系统规则、消息、工具 schema 和附件实际占多少 Context Builder + tokenizer
Output reserve 本轮要为模型生成保留多少空间 Harness 的任务策略
Run state / checkpoint 任务全部事实和可恢复状态保存在哪里 应用数据库、事件存储或工作流引擎

还要再区分长期记忆。Persistent Memory 保存跨运行可能有用的候选;它不是模型当前正在看的内容,也不能绕过租户、权限、新鲜度和预算。

可以把窗口理解为手术台,把 checkpoint 理解为病历系统:病历可以很大,但医生当前只把这次手术需要的材料放上台。把全部病历堆上去既不等于更安全,也不等于更容易找到关键事实。

3. 容量公式必须在调用前成立

本课程使用一个保守、可审计的公式:

input_budget = window_limit
             - output_reserve
             - fixed_reserve
             - safety_margin

dynamic_budget = input_budget
               - system_tokens
               - tool_schema_tokens

不同供应商对输入、输出和 reasoning token 的统计方式可能不同,因此这不是跨模型永远成立的计费公式,而是应用侧的预检契约。上线前必须核对目标模型文档。

4. 运行第一个预算实验

示例把窗口故意缩到 220 个教学 token,让扣减过程一眼可见:

python courses/foundation/context-engineering/course/project/examples/01_measure_and_budget.py

核心调用如下:

plan = BudgetPlan.from_messages(
    window_limit=220,
    output_reserve=50,
    safety_margin=12,
    fixed_messages=[system_message],
    tool_schema_text=tool_schema,
    counter=counter,
)

当前项目的真实输出是:

window: 220
input budget: 158
dynamic budget: 127
system/tool schema: 16 15

从 220 到 158,扣除了 50 个输出预留和 12 个安全余量;再扣除 16 个系统消息 token 与 15 个工具 schema token,历史和证据只剩 127。模型“支持 220”不代表历史可以使用 220。

BudgetPlan 在储备超过窗口时直接抛出错误:

if self.input_budget < 0:
    raise ValueError("reserves exceed the context window")

这是一个重要不变量:无法构造合法请求时,程序必须在调用模型之前失败。

5. 为什么更大的窗口仍然会答错

窗口扩大只能缓解容量,不自动解决信息利用。长上下文还会带来更高输入成本和首 token 延迟、工具 schema 与旧消息之间更强的注意力竞争、陈旧事实冲突、敏感信息暴露面扩大等问题。

Lost in the Middle 的实验表明,相关信息位于长输入中部时,模型表现可能下降。因此 Context Engineering 不只处理“能不能塞下”,还处理“放什么、放在哪、怎样证明模型用到了”。

6. 主动制造两个失败

失败 A:输出没有预留

把 output_reserve 改成 0,输入预算会变大,但如果模型需要生成结构化工具参数,输出可能被截断。错误不是“输入太长”,而是应用没有为下一步动作留空间。

失败 B:完整状态直接进入窗口

把 20 轮原始工具结果全部作为动态材料加入。即使没有超过 220,INC-42 也会与大量重复日志竞争。下一章会先可靠计数,第四章再决定哪些材料应该被选择。

7. 教学实现和生产系统的区别

当前示例使用 CharTokenCounter,通过字符数近似 token,只为了让预算算术稳定可复现。生产系统需要替换为目标模型 tokenizer 或官方计数接口,并在请求返回后用实际 usage 校准误差。

同样,window_limit 不能写成一个脱离模型的全局常量。它至少要绑定模型版本、调用类型、多模态输入和供应商限制;配置变化后,预算策略与回归测试都要重新执行。

8. 本章验收

完成下面的实验:

  1. 把窗口改为 100,保持其他参数不变,记录 dynamic_budget;
  2. 把工具 schema 扩大到原来的三倍,解释它为什么会挤掉历史;
  3. 让储备总量超过窗口,确认程序在模型调用前报错;
  4. 画出你自己的 Agent 中 state、memory、context 和 output reserve 分别由谁负责。

你真正学会的标准不是记住“窗口是容器”,而是能回答:本轮模型实际看到了什么;哪些内容只存在于 checkpoint;输出空间由谁预留;请求成功但模型忽略证据时,为什么不能只扩大窗口。

下一章:Token 怎样被测量?

进入 keel 阅读