主题
Context Budget
本课交付结果
你将交付 packContext(snippets, maxChars):按分数稳定排序,在字符预算内选择完整片段,返回已用字符数和是否截断,不修改调用方输入。
Week 2 到此形成“发现—读取—筛选”链路。Agent 不再把搜索命中的全部文件无界塞进模型请求。
岗位问题
上下文窗口很大也不是无限资源。未经筛选的搜索结果会挤掉任务约束、关键文件和工具观察;简单从尾部截字符串还可能切断代码语法,让模型看到不可解释的半段证据。
岗位实现需要明确预算单位、相关性顺序、同分稳定性与装箱策略,并把“哪些证据被舍弃”暴露为可观测状态。
前置检查
前置知识快照
先验证安全读取与结构化搜索:
bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 08
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 09本课用字符数建立可重复教学模型;生产系统应根据目标模型 tokenizer 计算 token,并为系统提示、历史、工具结果和输出分别预留预算。
先区分模型窗口与可用上下文预算。窗口是输入和输出的总容量,系统提示、对话历史、工具目录、当前证据和预期输出共同竞争空间。不能把模型宣称的窗口全部交给代码片段;如果没有给回复和工具循环预留余量,请求即使勉强成功,也可能让模型无法输出完整补丁。
上一课搜索返回的是候选,不是都应进入提示的事实。候选通常有相关性分数、来源与文本,预算器只负责确定性选择。检索负责“可能有用”,装箱负责“本轮带什么”,模型负责“如何推理”。把三者分开,才能分别测召回、选择和答案质量。
原理拆解
装箱算法按以下约束工作:
maxChars必须是非负整数。- 片段按 score 从高到低排序,同分保持输入顺序。
- 只放入完整片段,不在任意字符处截断代码。
- 当前大片段放不下时,可继续尝试后续较小片段,提高预算利用率。
- 返回
usedChars与truncated,供 Trace 和策略层判断证据损失。
这是一个可解释的贪心基线,不宣称求得全局最优。未来可以加入文件多样性、依赖关系与 query-aware reranking。
mermaid
flowchart LR
A[搜索与读取候选] --> B[校验预算]
B --> C[按分数稳定排序]
C --> D{当前完整片段装得下?}
D -- 是 --> E[复制并加入]
D -- 否 --> F[记录省略并继续]
E --> G[累计 usedChars]
F --> H{还有候选?}
G --> H
H --> I[selected + truncated]稳定性是可评测系统的基础。相同候选和预算应生成相同上下文,否则模型结果变化时无法区分是模型随机性还是装箱抖动。Lab 同分时保留原始输入顺序,所以排序前携带索引;总项目则按稳定 id 破同分,适合来自多个异步检索器、输入顺序本就不稳定的生产候选。两者服务于不同输入契约。
“完整片段”不是完整文件,而是检索器已经切出的语义单元,例如函数、测试用例或有限行窗口。预算器不在任意字符处切割,因为半个函数缺少声明、闭括号或断言,会让模型得到误导证据。若单个片段总是太大,应回到切片阶段按语法或行边界缩小,而不是在装箱层悄悄砍断。
遇到高分大片段放不下时为什么继续?假设预算还剩两百字符,第一候选三百字符,第二候选一百字符。break 会浪费全部余量,continue 能纳入第二条证据。这个贪心策略不保证组合总价值最高,但行为直观、线性扫描、容易解释,适合建立可回归的基线。
truncated 表示至少一个候选因预算未被选择,不表示某段文本被截断。命名虽然相同,语义必须在合同中写清。上层看到它可以决定缩小任务、增加预算、分轮读取或在答案中声明证据不完整。没有这个信号,模型与用户都会误以为候选已经全部覆盖。
不可变性保护跨层复用。搜索结果可能同时进入 Trace、评测和另一个策略;若预算器直接对输入 sort(),其他消费者看到的顺序随调用发生变化。返回时复制选中对象,进一步避免上层为渲染添加字段时污染原始候选。长运行 Agent 中,共享可变数据是最难复现的问题之一。
代码实验
失败实现
最常见错误是原地排序,遇到第一个过大候选就停止:
ts
function unsafePack(snippets: Snippet[], maxChars: number) {
snippets.sort((a, b) => b.score - a.score);
const selected = [];
let used = 0;
for (const snippet of snippets) {
if (used + snippet.content.length > maxChars) break;
selected.push(snippet);
used += snippet.content.length;
}
return selected;
}正确基线复制、稳定排序并跳过装不下的候选:
ts
export function packContext(snippets: readonly Snippet[], maxChars: number): PackResult {
if (!Number.isInteger(maxChars) || maxChars < 0) throw new Error("maxChars 必须是非负整数");
const ranked = snippets.map((snippet, index) => ({ snippet, index }))
.sort((a, b) => b.snippet.score - a.snippet.score || a.index - b.index);
const selected: Snippet[] = [];
let usedChars = 0;
for (const { snippet } of ranked) {
if (usedChars + snippet.content.length > maxChars) continue;
selected.push(structuredClone(snippet));
usedChars += snippet.content.length;
}
return { selected, usedChars, truncated: selected.length !== snippets.length };
}注意 Lab 字段名是 content,总项目使用 text;Lab 同分按原始索引,总项目同分按 id。不要在文章示例与生产路径间偷换语义,迁移时应通过适配器显式转换。
bash
pnpm --dir bootcamps/coding-agent/labs/10-context-budget/starter test
pnpm --dir bootcamps/coding-agent/labs/10-context-budget/solution testStarter 会以 EXERCISE_NOT_IMPLEMENTED:实现上下文排序与装箱 失败。Solution 应通过 5 项测试,覆盖稳定排序、跳过过大片段、零预算、非法预算和输入不可变。
下面测试同时证明“跳过而非终止”和完整装箱:
ts
it("高分大片段放不下时继续尝试小片段", () => {
const snippets = [
{ id: "large", content: "过大的完整片段", score: 10 },
{ id: "small", content: "可用", score: 5 },
] as const;
const result = packContext(snippets, 2);
expect(result.selected.map((item) => item.id)).toEqual(["small"]);
expect(result).toMatchObject({ usedChars: 2, truncated: true });
});关键实现讲解
先携带原始索引排序,避免依赖运行时排序实现细节:
ts
const ranked = snippets
.map((snippet, index) => ({ snippet, index }))
.sort((a, b) => b.snippet.score - a.snippet.score || a.index - b.index);循环中只有 usedChars + text.length <= maxChars 才加入完整片段。返回新对象和新数组,不要对输入 sort(),否则同一批候选在重试、Trace 和 Eval 中会出现难以解释的顺序变化。
预算校验必须在任何排序与复制前完成。负数、小数、无穷和非数字值都不具备清晰语义,应立即失败;零是合法预算,返回空选择,并在有候选时标记截断。把零当默认值或异常,会让调用方无法显式要求“本轮不带仓库证据”。
字符计数使用 JavaScript 字符串长度只是教学近似,某些 Unicode 字符占两个 UTF-16 code unit,模型 token 又是另一种切分。重要的是计数器可注入、选择算法不依赖具体单位。生产实现先给每段缓存 token 成本,再用同一装箱逻辑处理,避免在循环中重复调用昂贵 tokenizer。
运行与验证
真实运行输出
bash
pnpm --filter @learn-traeai/coding-agent-bootcamp verify:lesson -- 10
pnpm --filter @coding-agent/repo-context test单课门禁应报告 observedTests: 5。构造一个高分超大片段和一个低分小片段,确认算法跳过前者后仍装入后者;再冻结输入数组,证明实现没有原地排序。
实际执行 Starter 五项失败,Solution 五项通过:
text
starter: 5 failed, 0 passed
solution: 5 passed, 0 failed
覆盖: 稳定排序 / 跳过过大候选 / 零预算 / 非法预算 / 输入不可变验收时冻结数组与嵌套对象,然后连续调用两次并深比较结果。再调换同分候选,确认 Lab 按新的输入顺序稳定变化。最后用预算恰好等于文本长度的样本证明边界使用小于等于,而不是错误地浪费最后一个字符。
常见失败与排查
故障案例 1
装箱后原始搜索结果顺序改变。
症状:Trace 中候选顺序与检索阶段不同,第二次策略调用得到另一套结果。
根因:直接对调用方数组执行原地 sort(),共享数据被预算器改写。
定位:调用前冻结数组或保存深拷贝,调用后比较;若抛出只读错误或顺序变化,就是可变性泄漏。
修复:先映射到带索引的新数组,返回选中对象副本;函数签名接收 readonly 强化意图。
故障案例 2
高分巨型文件吞掉全部机会。
症状:预算仍有空间,结果却为空,后续多个小测试片段都未被选择。
根因:第一个候选放不下时使用 break,把“当前不合适”误当成“后面都不合适”。
定位:构造一大一小两个候选,大的高分且超限,小的可容纳;若小的未出现,循环控制错误。
修复:过大时记录省略原因并 continue;片段级截断交给上游语义切片器。
故障案例 3
评测结果在相同输入上抖动。
症状:候选分数相同且预算只能装部分时,多次运行选择不同文件。
根因:没有明确同分规则,或输入来自异步集合而顺序本身不稳定。
定位:固定候选内容与分数重复运行并比较选中 id;再检查排序比较器是否在同分返回确定次序。
修复:Lab 携带原始索引,生产候选使用稳定 id;把排序规则与序列化结果纳入快照测试。
课后作业
把字符预算替换为可注入的 token 计数器,并为每个文件增加最多两个片段的多样性约束。写测试证明预留输出 token 后不会超总窗口,同时 Trace 能记录被丢弃片段的 id 与原因。
进阶要求是设计两阶段预算:先为任务约束、错误日志、实现和测试保留各自最低配额,再在剩余空间按分数竞争。用固定夹具比较纯贪心与多样性策略,记录最终补丁通过率,而不是只比较预算利用率。
验收 Rubric
| 维度 | 通过标准 | 常见扣分 |
|---|---|---|
| 排序 | 高分优先且同分稳定 | 相同输入产生不同顺序 |
| 装箱 | 只加入完整且可容纳片段 | 任意截断或过大即停止 |
| 不可变性 | 不修改输入数组和对象 | 原地排序污染调用方状态 |
| 可观测性 | 报告已用预算与截断状态 | 无法解释证据为何缺失 |
总项目增量
总项目包:@coding-agent/repo-context
总项目路径:packages/repo-context/src/budget.ts
总项目验证命令:pnpm --filter @coding-agent/repo-context test
Week 2 检查点至此完成:工具有合同和注册表,文件访问有真实路径边界,搜索无 shell 且结构化,上下文有稳定预算。Week 3 将进入补丁生成、校验和应用。
延伸阅读
方案对比与工程取舍
字符预算实现简单、可重复、无需知道模型,适合教学和快速保护;token 预算更贴近真实计费与窗口,却绑定 tokenizer 版本并增加计算;直接依赖 Provider 的预估最省本地代码,但难以离线测试。生产系统通常注入模型特定计数器,并保留字符或字节硬上限作为故障保护。
纯分数贪心可解释且快速,却可能连续选择同一文件的重复片段。零一背包能在离散成本与价值下求更优组合,但相关性分数不一定可加,计算也随候选和预算增长。MMR 一类方法会惩罚相似度,提升多样性,却引入额外参数。正确选择应通过任务成功率评测,而不是只看装满比例。
“装得最满”不等于“最有用”。一堆短小低价值片段可能占满预算,却挤掉稍大但决定性的接口定义。分数需要反映任务相关性、符号完整匹配、文件角色和新颖性;预算器只执行给定价值。如果评分失真,应修检索与重排,而不是让装箱器猜业务。
上下文还存在位置效应。重要约束放在开头,关键证据放在显著位置,可能比简单分数降序更好;模型对中间内容的利用常弱于两端。可以把任务与验收固定在首尾,代码证据按组排列,并在每段前标明来源。布局属于提示组装层,不应改变预算选择的事实记录。
预算必须分区。总窗口先减去系统提示、工具定义、对话历史和预期输出,再把剩余给仓库上下文;工具调用轮次还要预留结果增长。若历史无限累积,再聪明的代码装箱也会被挤到零。会话层需要摘要、淘汰和检查点,与本课片段预算共同工作。
输出预留尤其关键。修复任务可能需要生成长补丁与解释,如果输入占满窗口,模型会截断输出或无法完成。可以按任务类型设最低输出额度,并在实际响应接近上限时让 Agent 分步修改。预算是一份资源计划,不只是输入剪刀。
片段去重应在装箱前完成。搜索同一符号可能返回定义、导出和重复生成文件,重叠行窗口也会重复文本。按文件与行范围合并重叠片段,或按规范化内容哈希去重,能把空间留给新证据。去重过程同样要稳定,并记录哪些候选合并到了哪个片段。
文件多样性可以设软约束:每个文件先选最高分片段,再进行第二轮;也可以对同文件后续候选衰减分数。硬上限容易错过同一核心文件中相距很远的实现与测试,软衰减更灵活但难解释。课程作业用最多两个片段建立直观基线,再让评测决定是否调整。
相关依赖有时必须成组出现。例如接口与实现、函数与测试、错误堆栈与对应代码,单独一段价值很低。可在上游生成带 group id 的复合候选,装箱按整组成本决定;若组太大,回到切片器缩小。不要让预算器随机选到一半关系后假装证据完整。
可观测结果建议包含每个候选的 id、原始分数、成本、最终排名、选择状态和省略原因。长期 Trace 不必保存全文,可以保存哈希、相对路径与行范围。线上任务失败时,团队能判断是没召回、评分太低、预算不足还是位置布局问题,而不是笼统归因于模型。
缓存能降低重复 token 计数与提示成本。片段内容哈希对应 token 成本,模型与 tokenizer 版本加入缓存键;候选集合与预算产生确定选择哈希。内容或模型变化时自然失效。不要只按文件路径缓存,文件更新后会使用过期成本和证据。
处理动态工具结果时,应给单次观察设置独立上限。错误日志、测试输出和搜索命中可能在循环中增长;先由工具做结构化截断,再由上下文层竞争预算。若把原始无限输出交给预算器,它在计算成本之前就可能耗尽内存。
零预算是很有用的边界测试,也代表合法策略:只依据用户提供内容回答,或在审批前不发送仓库代码。返回空数组、零使用量,并在存在候选时标记截断,语义完整。非法预算则应同步失败,避免默默修正掩盖上层计算错误。
验收不应停在单元测试。选取若干真实修复任务,记录候选全集、预算选择、模型答案和测试结果;逐步改变预算,观察成功率、成本和延迟。若预算翻倍却成功率不升,瓶颈可能是召回或噪声;若小幅增加就显著改善,应调整默认分区。
用一个数字例子检查算法。预算为十个字符,候选甲成本十二、分数九,候选乙成本六、分数八,候选丙成本四、分数七。排序后甲首先被跳过,乙与丙依次装入,使用量恰好为十,截断为真。若错误地遇甲就停止,使用量为零;若任意截断甲,模型只得到不完整高分证据;若按最短优先,虽然也装满,却忽略相关性顺序。这个例子把循环控制、完整性和价值优先同时固定下来。
再看同分稳定性。甲乙分数都为八,Lab 接收顺序是甲后乙,预算只能放一个,就选择甲;调用方反转输入后应选择乙,因为原始顺序就是 Lab 的次级规则。总项目候选可能来自并发搜索,输入到达顺序不可靠,所以改用 id。稳定不代表永远按一种字段,而是对输入契约给出明确且可重复的破局规则。
评分值本身要校验。非数字、无穷或缺失分数会让比较器返回异常结果,不同运行时可能排序不同。候选生成阶段应将分数归一到有限范围,预算器可拒绝非法候选或采用明确最低分。静默把 NaN 当零会掩盖重排器故障,导致关键证据无故落后。
文本成本也不只是正文长度。真正发送时通常包含路径标题、行号、代码围栏与分隔符;若预算器只计算正文,组装后仍可能超限。生产候选应预先计算完整渲染成本,或让渲染器与计数器使用同一模板。预算计算和实际序列化必须来自同一事实来源。
片段一旦选择,顺序可以保持排名,也可以按文件与行号重组。排名顺序突出相关性,文件顺序更容易理解代码上下文。无论怎么布局,都不要改变“选择了谁”的记录,且重新组装后再次核对总成本。选择与呈现是两个阶段,混在一个循环里会让测试难以解释。
被省略原因至少分为超预算、重复、文件配额、非法内容和策略拒绝。单一 truncated 供快速控制流,详细原因供 Trace 与评测。若大量高分候选因单段过大被拒绝,应改进切片;若因重复被拒绝说明去重有效;不同原因对应完全不同的优化动作。
摘要是另一种压缩手段,但不能和原文等价。模型生成摘要可能遗漏精确类型、错误消息和边界条件,适合对话历史或背景说明,不适合替代即将修改的关键代码。可以把低优先级文件先摘要,把高风险实现与测试保留原文,并在证据元数据中标注来源是原文还是派生摘要。
隐私也能通过预算层加强。候选进入模型前检查敏感路径、密钥模式和项目策略;被拒绝片段不参与装箱。不要为了提高预算利用率把被安全策略排除的内容重新补回。相关性、资源和权限是三条独立约束,只有同时满足的候选才有资格竞争。
多模型系统要按实际目标模型计数。规划模型、代码模型和总结模型的 tokenizer 与窗口可能不同,同一选择不能盲目复用。候选与分数可以共享,成本和最终装箱应按目标重新计算;缓存键包含模型标识。这样路由变化不会造成偶发超窗。
当窗口不足时,系统应优先降级而不是随机删内容:先减少低分候选,再压缩历史,再缩短工具目录,始终保留用户任务、安全约束和当前失败输出。优先级是一项产品策略,应写成可测试配置。把所有消息统一从尾部截断,最可能删掉恰好解释失败的部分。
Week 2 检查点可以做端到端演练:注册搜索和读取工具,在临时仓库查找符号,安全读取命中片段,给每段打分,使用小预算装箱,断言最终上下文没有越界路径、没有超过预算、顺序确定且明确报告省略。单课测试保护局部,演练证明组合后的能力仍符合合同。
下一课衔接
Week 2 已把模型意图接到受控工具:合同描述能力,Registry 唯一分发,文件工具限制真实路径,搜索提供有界候选,预算器选择完整证据。Week 3 将使用这些证据生成统一 diff,先解析和校验补丁,再安全应用与回滚。上下文是否准确,将直接决定补丁质量。
- 0/1 Knapsack 与贪心基线的取舍。
- Retrieval reranking、MMR 与上下文多样性。
- Token budget、prompt caching 与 lost-in-the-middle 现象。
预算算法只能优化已检索到的候选;如果搜索召回错误,再精细的装箱也无法恢复缺失证据。