首页
/ VS Code Copilot xtab 提示工程中最近查看文件的两类剪枝策略:AroundEditRange 与 Proportional 深度解析

VS Code Copilot xtab 提示工程中最近查看文件的两类剪枝策略:AroundEditRange 与 Proportional 深度解析

2026-09-04 14:07:25作者:袁立春Spencer

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,完整剖析 AroundEditRangeProportional 两种剪枝(clipping)策略的输入约定、算法流程、预算保证与边界行为,帮助读者理解模型上下文预算是如何被精确分配和强制执行的。

一、共享概念:输入、分页与输出顺序

两类策略共用一组输入约定与底层机制。理解这些共享概念是读懂两种策略算法差异的前提。

输入

输入 说明
recentlyViewedCodeSnippets 最近优先(most-recent-first)排序的文件列表。每个条目携带文件内容、可选的焦点范围(focal ranges,即编辑位置对应的字符偏移区间)以及 editEntryCount 权重
totalBudget 整个"最近查看文件"区块的 token 上限(即 opts.recentlyViewedDocuments.maxTokens
pageSize 分页剪枝时每页的行数
computeTokens token 计数函数

这些输入在源码中的对应关系清晰可查:buildCodeSnippetsUsingPagedClippingopts.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 中的预算强制

AroundEditRangeProportional 策略对带焦点范围的文件共享 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 个去重后的文档,每个文档只保留最新的一条历史条目,并排除当前活动文档。若 includeViewedFilesfalse,则跳过 visibleRanges 类型的条目(默认行为只保留编辑条目)。

焦点范围的来源:

  • 编辑条目:取自 edit.getNewRanges(),即替换文本在编辑后文档中的字符偏移区间;
  • 查看范围条目:直接使用该条目的 visibleRanges

算法:最近优先的贪心分配

  1. 按最近优先顺序处理文件。
  2. 有焦点范围的文件调用 clipAroundFocalRanges
    • 焦点范围先被跨度上限收敛到 pageSize × 3 行;
    • 焦点范围映射到页索引区间,这些页("焦点页")必然被包含;
    • 若焦点页超出剩余预算,跳过该文件
    • 否则,剩余预算被均分给焦点页的上方扩展和下方扩展,两个方向各自逐页向外扩展,直到各自的半额预算耗尽(useLeftoverBudgetFromAbovetrue 时,上方未用完的余量会追加到下方)。
  3. 没有焦点范围的文件回退到 clipFullDocument(从文件顶部逐页向下截断)。
  4. 当预算归零、或某文件装不下时,停止处理后续文件。

这一"半额预算双向扩展"的具体实现在 promptCrafting.tsexpandRangeToPageRange 中:先用 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 的每层中间编辑依次施加坐标变换,即可把旧条目的区间投影到最新内容上;
  • editEntryCountMath.max(editEntries.length, 1),作为后续比例分配的权重。

测试用例完整验证了该变换链:例如 AABB 上先做旧编辑(替换 [0,2)XXX)再做新编辑(在偏移 0 插入 YY),合并后焦点范围应包含新编辑自身的 [0,2) 与被前移变换后的 [2,5);三编辑链式用例(abcdef → 删除 → 插入 XY → 插入 Z)也逐位核对了每个变换后的区间。

算法:两趟处理

第一趟——计算最小焦点成本并筛选文件

  1. 对每个文件用 computeFocalPageCost 计算焦点页成本:即包含该文件焦点范围(经跨度上限收敛后)的那些页的 token 总成本。无焦点范围的文件焦点成本记为 0。
  2. 求和所有焦点成本。若总和超过 totalBudget,则从列表尾部(最旧的)逐个丢弃文件,直到总和满足预算:
let includedCount = recentlyViewedCodeSnippets.length;
let sumFocalCosts = focalCosts.reduce((a, b) => a + b, 0);
while (includedCount > 0 && sumFocalCosts > totalBudget) {
    includedCount--;
    sumFocalCosts -= focalCosts[includedCount];
}
  1. 若没有任何文件能纳入,直接返回空结果。

第二趟——分配扩展预算并剪枝

  1. 计算 expansionBudget = totalBudget − sumFocalCosts
  2. 每个文件的权重取自 editEntryCount(缺省为 1)。
  3. 每个被纳入文件的扩展份额为:floor(expansionBudget × (weight / totalWeight))
  4. 按最近优先顺序处理。每个文件的有效预算为:focalCost + expansionShare + unspentBudget(上一文件结转的未花完预算)。
  5. 有焦点范围的文件调用 clipAroundFocalRanges 并传入有效预算(焦点页能装下是由第一趟构造性保证的);无焦点范围的文件调用 clipFullDocument
  6. 未花完的预算结转到下一个文件;最终 tokensConsumed = totalBudget − 最终未花完预算

不变量(Invariants)

规格文档明确列出五条不变量,均有对应测试背书:

  • 预算保证:所有纳入文件的原始代码 token 之和从不超过 totalBudget。格式化开销(标签、文件路径头)不计入预算,这一点与所有策略一致。测试 proportional strategy total tokens never exceed budget 用 3 个文件、maxTokens = 300 断言总 token 不超过上限;
  • 焦点页保证:每个被纳入文件的焦点页都必然出现在输出中;一个文件只有在扣除其他所有纳入文件焦点成本后的剩余预算内装得下其焦点成本时才会被纳入;
  • 近期优先:需要丢弃文件时,最旧的(输入序列末尾的)先被丢弃。测试 proportional strategy drops oldest files first when over budgetcomputeFocalPageCost 精确算出"恰好装下 2 个文件焦点成本"的预算,断言第 3 个(最旧)文件被丢弃;
  • 比例公平:焦点页之外的扩展预算按编辑条目数成正比分配,编辑越多的文件获得越多的周围上下文。测试 gives more budget to files with more edit locations 验证 editEntryCount: 3 的文件的片段长于 editEntryCount: 1 的文件;
  • 结转:某文件实际用量少于其分配额(例如文件本身很小)时,未花完的 token 流向下一个文件。

五、两种策略对比

属性 AroundEditRange Proportional
预算分配 贪心,最近优先 两趟比例分配
剪枝中心 编辑/查看范围 编辑范围
文件丢弃 隐式(焦点成本超出剩余预算) 显式(最旧优先)
每文件多次编辑 每文件仅单条历史条目 全部条目合并
预算超发风险
历史收集 collectRecentDocuments collectRecentDocumentsGrouped

两者都把"预算超发"归零:贪心策略靠 clipAroundFocalRangesundefined 信号提前终止,比例策略靠第一趟的焦点成本总和对账。区别在于资源倾向——贪心策略让最近的编辑位置拿走尽可能多的上下文,比例策略则以编辑次数为权重做全局公平分配,保证较旧的文件也有机会进入提示词。

六、焦点页成本的计算细节

computeFocalPageCost 是比例策略第一趟的原子操作,其步骤为:

  1. 先用 selectFocalRangesWithinSpanCap 把焦点范围跨度收敛到 pageSize × 3 行(优先保留最近的焦点范围);
  2. 用内容变换器(content.getTransformer())把字符偏移区间换算为行号:startLine 取所有选中范围最小起点的行号,endLine 取最大终点(endExclusive - 1)的行号;
  3. 映射到页索引:firstPageIdx = floor((startLine − 1) / pageSize)lastPageIdxIncl = floor((endLine − 1) / pageSize)
  4. firstPageIdxlastPageIdxIncl(含)的每一页,取 content.getLines() 中对应行段并用 countTokensForLines 求 token 成本,累加返回。

值得注意的是该函数在没有任何可用焦点范围时返回 undefined,而比例策略的第一趟会将其 ?? 0 归零——与规格文档"无焦点范围文件的焦点成本为 0"的约定严格一致。

七、端到端串联与验证

完整链路由 getRecentCodeSnippets 串联:根据 clippingStrategy 选择收集路径 → buildCodeSnippetsUsingPagedClipping 按策略分派到 buildCodeSnippetsGreedybuildCodeSnippetsWithProportionalBudget → 输出以 \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.tsMath.ceil(s.length / 4) 作为确定性 token 计数器,用内联快照精确锁定了分页、行号偏移、贪心/比例分配、预算强制与跨度上限等全部关键行为。

小结

这篇规格文档及其实现给出了一个可参照的工程范本:在 LLM 提示词的 token 预算约束下,如何围绕"用户最近编辑的位置"组织上下文,并用分页为单位的原子剪枝焦点页硬保证跨度上限显式/隐式文件淘汰四道防线,把"预算超发"从可能性变为不可能。AroundEditRange 适合强调最近编辑位置的高保真场景,Proportional 则通过两趟计算换取多文件间的公平覆盖;二者的选择通过 clippingStrategy 配置项切换,默认值为 AroundEditRange,页大小默认 10 行、总预算默认 2000 token、最多 5 个文档——这些默认值均可在 xtabPromptOptions.tsDEFAULT_OPTIONS 中查证。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384