大型语言模型 (LLM) 正在改变软件开发。从自动补全整个函数到回答复杂的代码库级别问题,这些人工智能助手正迅速变得不可或缺。
但它们有一个致命弱点:** 上下文长度**。
当你让 LLM 处理一个大型项目时,你通常需要向它输入数万行代码。这种长上下文会引发一系列问题:
- 中间信息丢失: 当重要 token 被淹没在海量文本之中时,模型可能难以捕捉到相关片段。
- 生成缓慢: 注意力机制的计算复杂度随输入长度呈二次方增长,因此长输入会导致延迟飙升。
- 成本高昂: 商业 API 按 token 数量计费,长上下文会迅速推高账单。
对于源代码来说,这个问题尤为严重。与普通文本不同,代码存在复杂的相互依赖性。一个文件中的函数可能对分散在其他几十个文件中的逻辑至关重要。为了节省成本而随机裁剪代码,可能会破坏可编译的结构并丢失关键约束。
现有的解决方案,如*检索增强生成 *(Retrieval-Augmented Generation, RAG) ,尝试只获取相关的片段。但 RAG 通常依赖于基于嵌入的文本相似度,这在查找名称相似的函数时很有效,却无法捕捉那些细微而隐蔽的依赖关系。
那么,我们是否能智能地压缩代码——精确保留关键部分,舍弃其余内容?
最近的一篇研究论文——《LongCodeZip: 为代码语言模型压缩长上下文》——提出了这样一个框架。它无需训练、与具体模型无关,且专为代码设计。利用源代码的结构和语义,它可实现高达 5.6× 压缩,而不降低 (甚至有时提升) 性能。
让我们看看它是如何工作的。
相似度的困境: 为什么 RAG 处理代码时常常失效
检索增强生成是处理长上下文的常见方法。它通过模型嵌入和比较片段,然后检索那些与目标查询或待补全代码“最近”的片段。
但在代码中,“相似度”有多种形式:
- 词法层面: 共享变量名、关键字。
- 结构层面: 匹配的函数签名。
- 语义 / 依赖层面: 只有理解程序流程后才能发现的关联。
RAG 在捕捉词法和浅层结构匹配方面表现良好,但经常错过更深层的、基于依赖的联系——尤其是当这些联系是隐式时。
图 1: RAG 能找到词法上相似的代码 (左) ,但未能识别关键且不显眼的依赖关系 (右) ,比如设置优化器所需的配置类。
例如,如果要补全 get_email_by_id
,RAG 会很容易地找到 get_account_by_id
——一个几乎完美的词法匹配。这就是相似度相关性。
但在另一任务中——补全 train_model
——必需的 Config
类位于别处,并且没有任何相同的标识符。如果不了解依赖关系,RAG 会错过它,并将无关代码排在更高优先级。结果就是补全不完整或不正确。
我们需要一种度量方式能够理解功能和依赖关系,而不仅仅是表面相似度。
LongCodeZip 简介: 一个从粗到细的压缩框架
LongCodeZip 通过衡量信息增益而非简单相似度来解决问题。它问的是: 哪些代码片段能最大程度降低模型的不确定性?
困惑度作为相关性信号
困惑度 (Perplexity) 衡量模型对一个序列的“惊讶”程度。如果添加一个上下文片段让模型对指令的困惑度降低——即更不感到“惊讶”——那么这个片段很可能很重要。
作者定义了近似互信息 (AMI) :
\[ AMI(c, q) = PPL(q) - PPL(q \mid c) \]其中:
- \(PPL(q)\): 指令本身的困惑度。
- \(PPL(q|c)\): 给定上下文 \(c\) 时的困惑度。
更高的 AMI 意味着上下文 \(c\) 让模型更有信心——既能捕捉相似度相关性,也能捕捉依赖相关性。
阶段一: 粗粒度压缩 (选择相关函数)
第一阶段会过滤掉不相关的整个函数或类。
函数级分块:
在函数/类的边界处拆分代码,保持语法有效性,同时保留自包含的逻辑块。指令感知排序:
针对任务指令,使用 AMI 对每个代码块打分。预算约束选择:
贪婪地选择排名最高的代码块,直到填满粗略预算 (\(B_{\mathrm{coarse}}\)) 。未选的块用占位符替代,以保留整体文件结构。
阶段二: 细粒度压缩 (函数内部剪枝)
在选出最相关的函数后,LongCodeZip 会进一步裁剪它们:
图 2: 第一阶段选择相关函数;第二阶段在函数内部进行代码块剪枝。
基于困惑度的代码块切分:
将函数划分为语义块。在一个连贯块中,困惑度保持稳定;困惑度突增通常意味着新的逻辑单元。边界设在困惑度相对相邻区域急剧上升处。自适应预算分配:
并非所有函数重要性相同。- 极短函数 (<5 行) 完整保留。
- 较长函数按基线保留率,结合归一化 AMI 和重要度参数 \(\beta\) 进行调整:
\[ R_{\mathrm{biased}, i} = R_{\mathrm{base}} \cdot \left(1 + \beta \cdot (2 \times AMI_{\text{norm}, i} - 1) \right) \] - 对比率重新缩放,使总 token 数匹配最终预算。
动态代码块选择 (背包优化) :
将每个代码块作为一个物品:- 价值 = AMI 分数
- 重量 = token 长度
在预算内选择,以最大化总价值,保留最重要的代码。
实验评估
研究团队在以下任务上测试了 LongCodeZip:
表 1: 基准任务的平均上下文长度约为 1 万个 token。
任务:
- 长代码补全
- 长模块摘要
- RepoQA (代码库级问答)
基准方法: RAG (Sliding/Function) 、文本压缩器 (LLMLingua、LongLLMLingua) 、代码压缩器 (DietCode、SlimCode) 以及上/下界基准 (不压缩、无上下文) 。
RQ1: 压缩与性能
表 2: LongCodeZip 一贯取得最高 ES/EM 分数,有时甚至超过“不压缩”基准——使用的 token 远少于其。
亮点:
- 在长代码补全任务中,它有时超越不压缩基准。
例如: Seed-Coder-8B 的 ES 仅下降 0.93 点,但只用了约 18% 的 token (压缩 5.6×) 。 - 在 RepoQA 上,移除无关上下文简化了检索,在 GPT-4o 和 Claude-3.7-Sonnet 上的表现有时甚至超越不压缩基准。
表 3: 在顶级闭源模型上同样表现优异。
RQ2: 消融实验——关键因素
表 4: 粗粒度困惑度排序是最核心的组件。
将 AMI 排序替换为相似度排序会导致 ES 下降约 8 点。移除细粒度优化 (自适应预算、背包算法) 则进一步损害性能,证明每个模块都不可或缺。
RQ3 & RQ4: 泛化与效率
- 跨模型泛化:
一个小型 0.5B 模型可以负责压缩,再将优化后的上下文传给更大的生成模型,效果几乎一致。 - 效率:
压缩仅有约 2.6 秒的开销,但带来巨大收益:- 生成时间由 15.7 秒 → 6.6 秒
- 输入 token 减少 77% ≈ 成本降低约 77%
图 3: 即便在极端压缩率下也能保持高 ES 分数。
案例研究: 补全 execute_blind
图 4: 困惑度峰值界定语义块;背包算法保留最关键的块。
evaluate_blind
函数被识别为相关函数。困惑度分析将其切分为多个块;AMI 排序优先保留初始化 action
与 call_name
的块。保留这些部分确保模型正确补全 execute_blind
。
结论: 更智能的代码长上下文处理
LongCodeZip 在用 LLM 处理大型代码库方面实现了重大突破。主要贡献包括:
- 分层压缩: 函数过滤加代码块剪枝,在压缩率与语义完整性间取得平衡。
- 困惑度驱动相关性: AMI 捕捉到基于相似度方法遗漏的深层依赖。
- 实用且即插即用: 无需训练,模型无关,易于集成。
在不牺牲性能的前提下缩减上下文、减少生成时间、降低 API 成本,LongCodeZip 使 LLM 在真实工程中的应用更可行——尤其是在“整个代码库”无法放入上下文时。
有时候,少即是多。