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
window_limit是当前模型和调用方式允许的总边界;output_reserve为本轮回答、结构化输出或工具请求留空间;fixed_reserve覆盖图片、供应商包装或暂时无法精确计数的协议开销;safety_margin吸收 tokenizer 估算误差和版本差异;dynamic_budget才能分给历史、工具结果、检索材料和记忆候选。
不同供应商对输入、输出和 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. 本章验收
完成下面的实验:
- 把窗口改为 100,保持其他参数不变,记录
dynamic_budget; - 把工具 schema 扩大到原来的三倍,解释它为什么会挤掉历史;
- 让储备总量超过窗口,确认程序在模型调用前报错;
- 画出你自己的 Agent 中 state、memory、context 和 output reserve 分别由谁负责。
你真正学会的标准不是记住“窗口是容器”,而是能回答:本轮模型实际看到了什么;哪些内容只存在于 checkpoint;输出空间由谁预留;请求成功但模型忽略证据时,为什么不能只扩大窗口。