上下文选择与渐进式披露
问题
每轮都急切加载所有可用上下文的 Agent 运行时会支付复利般的税:token 成本随目录大小增长,启动延迟随信息源数量扩展,并发 Agent 重复相同的昂贵 I/O。模型收到数千个它从不阅读的 token,而它实际需要的数据却迟到或根本不到。如果不进行刻意选择,上下文窗口就变成了垃圾填埋场——技术上是满的,实际上是空的。
这些问题出现在任何维护不断增长的技能目录、从多个持久化存储读取或服务并发请求的 Agent 中。并非特定于任何单一运行时。
黄金法则
懒加载优于急切加载
在上下文变得相关的时刻加载它,而非会话开始的时刻。急切注入所有内容"以防万一"意味着每次 API 调用都为模型可能永远不使用的 token 付费。推迟加载意味着只在模型实际激活能力时才付费。即时获取的一次性延迟几乎总是比膨胀 prompt 的每轮成本更廉价。
三层渐进式披露
将每个上下文来源结构化为三层递增成本:
- 元数据(每项约 100 token):名称、描述和触发提示。始终存在。这是"这是否相关?"的信号。
- 指令(有界,如 < 5000 token):激活能力的完整正文。仅在激活时加载。
- 资源(无界):参考文件、脚本、数据资产。仅在 Agent 明确请求时加载。
发现成本(第一层)随目录大小扩展,但执行成本(第二三层)按每个激活项是恒定的。保持发现廉价使你可以维护一个大目录而无需在每轮为其付费。
备忘化 promise,而非结果
当多个调用者同时请求相同的昂贵上下文时,备忘化进行中的 promise 使所有调用者共享单次 I/O 操作。仅备忘化已解析的值会创建一个窗口,让并发调用者各自看到缓存为空并启动重复工作。promise 本身就是去重的键。
在变更处失效,而非定时器
基于时间或响应式订阅的缓存过期要么提供过时数据,要么重建过于频繁。相反,在每个已知的变更点放置显式的失效调用。这维护起来更费力——每条新的写入路径都必须添加自己的失效——但它保证缓存恰好与最近一次刻意更改一样新鲜。
适用场景
- 技能或能力目录在增长,常驻 prompt 正变得昂贵。
- 启动延迟高,因为上下文在每轮都急切构建。
- 多个并发 Agent 或调用重复相同的昂贵 I/O。
- 你需要支持大目录而不线性增加每轮 token 成本。
- 上下文来源跨越多个可能重叠的目录、存储或服务。
权衡
| 决策 | 收益 | 代价 |
|---|---|---|
| 懒加载 | 低空闲 token 成本 | 首次激活有一次往返延迟 |
| 渐进式披露(三层) | 大目录保持廉价 | 模型无法推理未激活的能力 |
| promise 备忘化 | 并发下无重复 I/O | 缓存逻辑比简单值缓存更微妙 |
| 在变更处手动失效 | 只在必要时重建 | 每条新写入路径都必须包含自己的失效调用 |
| 发现层的 token 预算 | 目录成本有界且可预测 | 每个条目必须适配紧凑的字符上限 |
| 基于路径的条件激活 | 领域特定技能在相关之前保持休眠 | 激活状态必须在进程重启后存活或被重新评估 |
实现模式
- 将每个昂贵的上下文构建器包装在备忘化的异步函数中。存储 promise 本身使并发调用者共享同一进行中的请求,而非各自发起重复 I/O。
- 为每个备忘化构建器暴露一个命名的缓存清除调用。仅从已知的变更处理器调用它——永远不要从通用观察者或轮询循环调用。
- 对于能力目录,仅从元数据字段(名称、描述、触发提示)计算估计的 token 数。完整正文存储在延迟闭包中,仅在激活时获取。
- 将发现层总预算限制在上下文窗口的固定比例(如约 1%),每个条目限制在紧凑的字符上限。
- 使用路径条件元数据门控领域特定能力:能力保持休眠直到会话中触及匹配的文件或目录。
- 通过规范路径去重多来源加载。在插入活跃集之前解析符号链接并规范化——原始路径的字符串比较会产生假阴性,而虚拟或网络文件系统上的 inode 比较不可靠。
- 将共享状态标记为深度不可变。在排除点而非静默地记录从不可变性包装器中排除的字段(如函数类型字段)。
踩坑指南
变更后的过时上下文。 因为缓存从不自动过期,任何外部变更(新文件、切换的标志、更新的存储)在缓存被显式清除之前都不可见。每个变更点都必须调用对应的失效。漏掉一个意味着模型在会话余下时间都在过时数据上操作。
无 promise 备忘化的并发初始化竞态。 如果两个调用同时触发相同的提取且都看到缓存为空,两者都会尝试相同的 I/O。只有一个会干净地成功;另一个可能失败或产生损坏的输出。备忘化 promise 彻底消除竞态。
过早披露膨胀每一轮。 将完整能力正文注入系统 prompt 意味着每次 API 调用都为模型可能永远不使用的 token 付费。有 20 多个能力时,这可能在第一条用户消息到达之前就消耗上下文预算的很大份额。
条件能力必须跟踪激活状态。 被路径条件门控的能力在触及匹配文件之前是休眠的。如果激活状态丢失(如进程重启),能力必须根据当前上下文重新评估——否则它会从会话中静默消失。
去重必须使用规范路径,而非原始路径。 当涉及符号链接、挂载或多个搜索目录时,同一文件可以出现在不同路径下。原始路径的字符串比较会漏掉重复项;比较前解析为规范路径。
Claude Code 实证
Claude Code 的上下文系统是这些原则的生产实现,跨多个来源目录管理不断增长的技能目录:
备忘化加手动失效,而非响应式订阅。 系统上下文构建器和用户上下文构建器都是备忘化的异步函数。缓存在整个进程生命周期内持久化。当某些东西必须改变时——例如系统 prompt 注入被更新——运行时在那个特定变更点显式调用缓存清除。这是一个刻意的选择:上下文从缓存服务廉价,重建昂贵。在每次状态更新时重建的响应式模型将完全违背缓存的目的。
并发初始化的 promise 备忘化。 内置技能提取存储提取 promise 本身而非已解析的值。如果两次技能调用在启动期间竞争,两者等待同一 promise。没有这一设计,两者都会尝试同时写入相同的提取文件,第二个会失败或损坏输出。
token 计数作为渐进式披露的门控指标。 技能加载器仅从名称、描述和触发提示字段计算估计的 token 数——从不从完整 markdown 正文。正文存储在延迟闭包中,仅在调用时获取。这干净地分离了"这是否相关?"的信号(廉价,始终存在)和"这个技能做什么?"的内容(昂贵,延迟加载)。总列表预算限制在上下文窗口的大约百分之一,每个条目限制在约 250 个字符。
跨多来源目录的 realpath 去重。 从管理的、用户、项目和额外目录加载技能时,同一技能文件可能出现在符号链接路径下。加载器在插入已见集之前将每个文件解析为其规范路径。团队选择规范路径比较而非 inode 比较,因为 inode 在虚拟和网络文件系统上不可靠。
深度不可变的共享状态。 主应用状态被包装在深度不可变性类型中,以防止跨并发 Agent 的意外原地修改。任务状态被排除在不可变性包装器之外,因为它包含包装器无法处理的函数类型——但排除在排除点有文档记录,而非静默丢弃。