VS Code Copilot xtab 提示工程中最近查看文件的两类剪枝策略:AroundEditRange 与 Proportional 深度解析
VS Code Copilot 扩展的 NES/xtab(Next Edit Suggestion)提示工程需要在有限 token 预算下,从"最近查看过的文件"中挑选并截断内容注入模型提示词。本文基于仓库中的规格文档 extensions/copilot/src/extension/xtab/common/recentFilesForPrompt.md,结合其实现文件 extensions/copilot/src/extension/xtab/common/recentFilesForPrompt.ts 与配套测试 extensions/copilot/src/extension/xtab/test/common/recentFilesForPrompt.spec.ts,完整剖析 AroundEditRange 与 Proportional 两种剪枝(clipping)策略的输入约定、算法流程、预算保证与边界行为,帮助读者理解模型上下文预算是如何被精确分配和强制执行的。
一、共享概念:输入、分页与输出顺序
两类策略共用一组输入约定与底层机制。理解这些共享概念是读懂两种策略算法差异的前提。
输入
| 输入 | 说明 |
|---|---|
recentlyViewedCodeSnippets |
按最近优先(most-recent-first)排序的文件列表。每个条目携带文件内容、可选的焦点范围(focal ranges,即编辑位置对应的字符偏移区间)以及 editEntryCount 权重 |
totalBudget |
整个"最近查看文件"区块的 token 上限(即 opts.recentlyViewedDocuments.maxTokens) |
pageSize |
分页剪枝时每页的行数 |
computeTokens |
token 计数函数 |
这些输入在源码中的对应关系清晰可查:buildCodeSnippetsUsingPagedClipping 从 opts.pagedClipping.pageSize 读取页大小(未定义时直接抛出 illegalArgument),从 opts.recentlyViewedDocuments.maxTokens 读取总预算,见 recentFilesForPrompt.ts。
分页剪枝(Paged Clipping)
所有策略都采用页(page)为单位的剪枝:文件内容被切分为每页 pageSize 行的"页",token 成本按页计算;页是整体纳入或整体剔除的最小单位,绝不允许出现半页。实现上,clipFullDocument 通过 batchArrayElements(lines, pageSize) 把行数组分批后逐页扣减预算,预算不够装下一页时立即 break。
输出顺序
所有策略在返回前都会对收集到的片段数组执行 reverse()。也就是说:内部处理顺序是最近优先,但最终提示词中的呈现顺序是最旧在最上、最近在最下(least-recent-first)。从源码结构看,这一反转同时发生在贪心路径(buildCodeSnippetsGreedy 末尾的 result.snippets.reverse())和比例路径(buildCodeSnippetsWithProportionalBudget 末尾)。
clipAroundFocalRanges 中的预算强制
AroundEditRange 与 Proportional 策略对带焦点范围的文件共享 clipAroundFocalRanges 函数。该函数执行严格预算:如果焦点页(focal pages)本身的 token 成本就超过了剩余预算,该文件会被整体跳过(返回 undefined)。这是防止任何单个文件"击穿"预算的关键机制——宁可整个文件不出现,也不允许超发。
源码中对应的逻辑是:expandRangeToPageRange 返回的 budgetLeft 为负数(表示焦点页放不下)时,clipAroundFocalRanges 返回 undefined 而非负数预算:
// If the focal pages alone exceed the budget (negative budgetLeft from
// expandRangeToPageRange), skip this file rather than silently overshooting.
if (budgetLeft < 0) {
return undefined;
}
调用方对 undefined 的解读因策略而异:贪心策略视其为"装不下"并直接 break 终止整个循环;比例策略则把该文件的分配额原封不动地结转(carry-forward)给下一个文件。
焦点范围跨度上限(Focal Range Span Capping)
当围绕焦点范围剪枝时,selectFocalRangesWithinSpanCap 会把焦点范围的行跨度限制在 pageSize × 3 行以内。焦点范围按最近优先排序,函数贪心地逐个纳入范围,直到"再加入下一个会超出行跨度上限"为止。其目的是防止宽散点编辑(例如第 10 行和第 90 行各有一处编辑)导致初始焦点跨度覆盖整个文档、从而瞬间耗尽预算。
export function selectFocalRangesWithinSpanCap(
focalRanges: readonly OffsetRange[],
getLineNumber: (offset: number) => number,
maxSpanLines: number,
): readonly OffsetRange[] {
if (focalRanges.length <= 1) {
return focalRanges;
}
const selected: OffsetRange[] = [focalRanges[0]];
let startLine = getLineNumber(focalRanges[0].start);
let endLine = getLineNumber(Math.max(focalRanges[0].start, focalRanges[0].endExclusive - 1));
for (let i = 1; i < focalRanges.length; i++) {
// 候选跨度 = min(所有起点行) 到 max(所有终点行)
// 超出 maxSpanLines 即停止纳入
}
return selected;
}
测试用例 RC3: focal range span capping 验证了这一行为:一个 100 行文件在第 5 行(最近)和第 95 行(较早)各有编辑、pageSize = 5 时,剪枝结果只包含 line_005 附近内容而不包含 line_095;反之,两个相距 2 行的编辑都会被纳入。
二、配置参数与默认值
上述输入参数均来自提示词配置 PromptOptions,定义在 xtabPromptOptions.ts。与本文主题直接相关的 RecentlyViewedDocumentsOptions 字段及仓库默认值如下:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
nDocuments |
number | 5 |
最多收集多少个去重后的最近文档 |
maxTokens |
number | 2000 |
最近查看文件区块的 token 总预算 |
includeViewedFiles |
boolean | false |
是否把"仅查看未编辑"(visibleRanges)的文件也纳入 |
includeLineNumbers |
enum | None |
片段行号格式:withSpaceAfter / withoutSpaceAfter / none |
clippingStrategy |
enum | AroundEditRange |
本文主角:选择贪心或比例策略 |
useLeftoverBudgetFromAbove |
boolean | false |
焦点范围上方页未用完的预算是否捐给下方页 |
同时 DEFAULT_OPTIONS.pagedClipping.pageSize 默认为 10(即每页 10 行)。策略枚举本身定义如下:
export enum RecentFileClippingStrategy {
/** Center clipping around the edit location in each file (greedy budget). */
AroundEditRange = 'aroundEditRange',
/** Proportionally allocate budget across files, centered on edit locations. */
Proportional = 'proportional',
}
入口函数 getRecentCodeSnippets 根据 clippingStrategy 分叉:Proportional 走"分组收集 → historyEntriesToCodeSnippet 合并"路径,否则走"单条收集 → historyEntryToCodeSnippet"路径,随后统一交给 buildCodeSnippetsUsingPagedClipping 完成剪枝。
三、策略一:AroundEditRange(贪心,围绕编辑位置)
历史收集
该策略使用 collectRecentDocuments:从 xtab 历史尾部向头部扫描,收集最近 nDocuments 个去重后的文档,每个文档只保留最新的一条历史条目,并排除当前活动文档。若 includeViewedFiles 为 false,则跳过 visibleRanges 类型的条目(默认行为只保留编辑条目)。
焦点范围的来源:
- 编辑条目:取自
edit.getNewRanges(),即替换文本在编辑后文档中的字符偏移区间; - 查看范围条目:直接使用该条目的
visibleRanges。
算法:最近优先的贪心分配
- 按最近优先顺序处理文件。
- 对有焦点范围的文件调用
clipAroundFocalRanges:- 焦点范围先被跨度上限收敛到
pageSize × 3行; - 焦点范围映射到页索引区间,这些页("焦点页")必然被包含;
- 若焦点页超出剩余预算,跳过该文件;
- 否则,剩余预算被均分给焦点页的上方扩展和下方扩展,两个方向各自逐页向外扩展,直到各自的半额预算耗尽(
useLeftoverBudgetFromAbove为true时,上方未用完的余量会追加到下方)。
- 焦点范围先被跨度上限收敛到
- 对没有焦点范围的文件回退到
clipFullDocument(从文件顶部逐页向下截断)。 - 当预算归零、或某文件装不下时,停止处理后续文件。
这一"半额预算双向扩展"的具体实现在 promptCrafting.ts 的 expandRangeToPageRange 中:先用 Math.floor(availableTokenBudget / 2) 确定上半区预算并向上逐页扩展 firstPageIdx,再(可选地叠加上方余量)确定下半区预算并向下扩展 lastPageIdxIncl。
行为特征
- 剪枝中心是编辑/查看位置而非文件顶部——测试用例
centers snippet on focal range instead of top of file验证了:对 100 行文件、编辑位于第 80 行、pageSize = 5时,结果包含line_080附近内容而不包含line_000; - 预算被贪心消耗——最近的文件获得最多的上下文;
- 每个文档只取一条历史条目,因此一个文件即使被编辑过多次,也只有最近一次编辑位置决定剪枝中心。
预算耗尽的边界行为
测试套件 RC1: budget enforcement in clipAroundFocalRanges 覆盖了两类边界:
- 预算为 0 时不输出任何内容;
- 第一个文件的焦点页超出预算时,不会把负预算传染给后续文件——贪心循环直接
break,第二个小文件也随之被排除(does not cascade negative budget to subsequent files)。
四、策略二:Proportional(两趟比例分配)
历史收集:按文档分组
该策略改用 collectRecentDocumentsGrouped:同样收集最近 nDocuments 个去重文档,但保留每个文档的全部历史条目(而非仅最新一条),并按"最近活跃的文档"排序输出分组。这让 historyEntriesToCodeSnippet 可以合并同一文件内多次编辑的焦点范围,使模型能看到每个文档中所有最近的编辑位置。
historyEntriesToCodeSnippet 的核心机制:
- 以最新条目的内容作为基准内容;
- 仅从编辑条目收集焦点范围(
visibleRanges条目被跳过,因为其字符偏移对应的是旧版本文档快照,无法可靠地变换到当前内容的坐标空间); - 较早编辑条目的焦点范围会沿着后续编辑链向前变换(
applyToOffsetRange),使其在最新内容中依然有效。源码中的链式不变量是:editEntries[j].postEdit ≈ editEntries[j-1].base,因此对下标 j-1 到 0 的每层中间编辑依次施加坐标变换,即可把旧条目的区间投影到最新内容上; editEntryCount取Math.max(editEntries.length, 1),作为后续比例分配的权重。
测试用例完整验证了该变换链:例如 AABB 上先做旧编辑(替换 [0,2) 为 XXX)再做新编辑(在偏移 0 插入 YY),合并后焦点范围应包含新编辑自身的 [0,2) 与被前移变换后的 [2,5);三编辑链式用例(abcdef → 删除 → 插入 XY → 插入 Z)也逐位核对了每个变换后的区间。
算法:两趟处理
第一趟——计算最小焦点成本并筛选文件
- 对每个文件用
computeFocalPageCost计算焦点页成本:即包含该文件焦点范围(经跨度上限收敛后)的那些页的 token 总成本。无焦点范围的文件焦点成本记为 0。 - 求和所有焦点成本。若总和超过
totalBudget,则从列表尾部(最旧的)逐个丢弃文件,直到总和满足预算:
let includedCount = recentlyViewedCodeSnippets.length;
let sumFocalCosts = focalCosts.reduce((a, b) => a + b, 0);
while (includedCount > 0 && sumFocalCosts > totalBudget) {
includedCount--;
sumFocalCosts -= focalCosts[includedCount];
}
- 若没有任何文件能纳入,直接返回空结果。
第二趟——分配扩展预算并剪枝
- 计算
expansionBudget = totalBudget − sumFocalCosts。 - 每个文件的权重取自
editEntryCount(缺省为 1)。 - 每个被纳入文件的扩展份额为:
floor(expansionBudget × (weight / totalWeight))。 - 按最近优先顺序处理。每个文件的有效预算为:
focalCost + expansionShare + unspentBudget(上一文件结转的未花完预算)。 - 有焦点范围的文件调用
clipAroundFocalRanges并传入有效预算(焦点页能装下是由第一趟构造性保证的);无焦点范围的文件调用clipFullDocument。 - 未花完的预算结转到下一个文件;最终
tokensConsumed = totalBudget − 最终未花完预算。
不变量(Invariants)
规格文档明确列出五条不变量,均有对应测试背书:
- 预算保证:所有纳入文件的原始代码 token 之和从不超过
totalBudget。格式化开销(标签、文件路径头)不计入预算,这一点与所有策略一致。测试proportional strategy total tokens never exceed budget用 3 个文件、maxTokens = 300断言总 token 不超过上限; - 焦点页保证:每个被纳入文件的焦点页都必然出现在输出中;一个文件只有在扣除其他所有纳入文件焦点成本后的剩余预算内装得下其焦点成本时才会被纳入;
- 近期优先:需要丢弃文件时,最旧的(输入序列末尾的)先被丢弃。测试
proportional strategy drops oldest files first when over budget用computeFocalPageCost精确算出"恰好装下 2 个文件焦点成本"的预算,断言第 3 个(最旧)文件被丢弃; - 比例公平:焦点页之外的扩展预算按编辑条目数成正比分配,编辑越多的文件获得越多的周围上下文。测试
gives more budget to files with more edit locations验证editEntryCount: 3的文件的片段长于editEntryCount: 1的文件; - 结转:某文件实际用量少于其分配额(例如文件本身很小)时,未花完的 token 流向下一个文件。
五、两种策略对比
| 属性 | AroundEditRange | Proportional |
|---|---|---|
| 预算分配 | 贪心,最近优先 | 两趟比例分配 |
| 剪枝中心 | 编辑/查看范围 | 编辑范围 |
| 文件丢弃 | 隐式(焦点成本超出剩余预算) | 显式(最旧优先) |
| 每文件多次编辑 | 每文件仅单条历史条目 | 全部条目合并 |
| 预算超发风险 | 无 | 无 |
| 历史收集 | collectRecentDocuments |
collectRecentDocumentsGrouped |
两者都把"预算超发"归零:贪心策略靠 clipAroundFocalRanges 的 undefined 信号提前终止,比例策略靠第一趟的焦点成本总和对账。区别在于资源倾向——贪心策略让最近的编辑位置拿走尽可能多的上下文,比例策略则以编辑次数为权重做全局公平分配,保证较旧的文件也有机会进入提示词。
六、焦点页成本的计算细节
computeFocalPageCost 是比例策略第一趟的原子操作,其步骤为:
- 先用
selectFocalRangesWithinSpanCap把焦点范围跨度收敛到pageSize × 3行(优先保留最近的焦点范围); - 用内容变换器(
content.getTransformer())把字符偏移区间换算为行号:startLine取所有选中范围最小起点的行号,endLine取最大终点(endExclusive - 1)的行号; - 映射到页索引:
firstPageIdx = floor((startLine − 1) / pageSize),lastPageIdxIncl = floor((endLine − 1) / pageSize); - 对
firstPageIdx到lastPageIdxIncl(含)的每一页,取content.getLines()中对应行段并用countTokensForLines求 token 成本,累加返回。
值得注意的是该函数在没有任何可用焦点范围时返回 undefined,而比例策略的第一趟会将其 ?? 0 归零——与规格文档"无焦点范围文件的焦点成本为 0"的约定严格一致。
七、端到端串联与验证
完整链路由 getRecentCodeSnippets 串联:根据 clippingStrategy 选择收集路径 → buildCodeSnippetsUsingPagedClipping 按策略分派到 buildCodeSnippetsGreedy 或 buildCodeSnippetsWithProportionalBudget → 输出以 \n\n 拼接的片段字符串、去重集合 docsInPrompt 与分区块渲染结果。每个片段由 formatCodeSnippet 包装为:
<|recently_viewed_code_snippet|>
code_snippet_file_path: /src/first.txt (truncated)
10| content_line_10
11| content_line_11
...
<|/recently_viewed_code_snippet|>
当片段被截断时文件路径行会附加 (truncated) 标记;includeLineNumbers 配置决定行号前缀格式,且焦点剪枝后的行号从首个保留页的行号起算(例如第 10 行开始的页显示 10| ... 而非 0| ...,测试用例 includes line numbers with offset when visible range is in middle of file 专门验证了这一点)。测试套件 recentFilesForPrompt.spec.ts 以 Math.ceil(s.length / 4) 作为确定性 token 计数器,用内联快照精确锁定了分页、行号偏移、贪心/比例分配、预算强制与跨度上限等全部关键行为。
小结
这篇规格文档及其实现给出了一个可参照的工程范本:在 LLM 提示词的 token 预算约束下,如何围绕"用户最近编辑的位置"组织上下文,并用分页为单位的原子剪枝、焦点页硬保证、跨度上限和显式/隐式文件淘汰四道防线,把"预算超发"从可能性变为不可能。AroundEditRange 适合强调最近编辑位置的高保真场景,Proportional 则通过两趟计算换取多文件间的公平覆盖;二者的选择通过 clippingStrategy 配置项切换,默认值为 AroundEditRange,页大小默认 10 行、总预算默认 2000 token、最多 5 个文档——这些默认值均可在 xtabPromptOptions.ts 的 DEFAULT_OPTIONS 中查证。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00