粗暴全选的时代已经结束
面对一个包含 500 个文件的微服务模块,简单地把所有代码拼接起来会直接突破大模型的上下文上限。
智能剪枝三阶段流水线
- 符号定义剥离(Outline Skeleton):非核心依赖文件只保留函数签名与接口定义,将具体实现函数体折叠为
// ... implementation hidden, 节省 70% 空间; - 调用图深度裁剪(Depth-limited BFS):以当前修改点为圆心,沿 AST 调用链仅向下展开 2 层间接调用方,超出范围一律剪掉;
- 测试文件与 Mock 数据隔离:除非当前任务明确要求编写单元测试,否则自动将
__tests__/目录从初筛列表中剔除。
先算预算,再决定裁多少
裁剪的目标不是「越少越好」,而是在固定预算下让最相关的代码留在窗口里。一个可复用的预算分配大致如下(比例为经验值,按模型上下文长度等比缩放):
| 内容 | 建议占比 | 说明 |
|---|---|---|
| 任务描述与报错原文 | 约 10% | 用户的原话与堆栈不要改写 |
| 直接修改点文件全文 | 约 30% | 唯一允许完整保留实现的部分 |
| 一跳依赖(调用方/被调用方) | 约 25% | 签名 + 关键分支,函数体可折叠 |
| 类型与接口定义 | 约 15% | 字段名与枚举值必须原样保留 |
| 配置与构建相关文件 | 约 10% | 只取与本次改动相关的片段 |
| 余量 | 约 10% | 给模型输出和多轮追加留空间 |
实现顺序:索引、评分、拼装
第一步是离线索引。用现成的解析器产出符号表与调用边(语言侧一般都有成熟的 AST/符号工具),加上全文或语义检索作为召回兜底。这一步的结果可以缓存,只有增量文件需要重算。
第二步是给候选文件打分,把多路信号合成一个排序。常见信号与权重大致是:
| 信号 | 相对权重 | 取值理由 |
|---|---|---|
| 报错栈或用户点名提到的文件 | 最高 | 命中即进入必选集合 |
| 与修改点直接相连的调用边 | 高 | 一跳内的双向依赖 |
| 导出的公共接口(被外部引用) | 中高 | 改动会影响契约 |
| 关键词或语义检索命中 | 中 | 兜住静态分析看不到的隐式引用 |
| 近期变更频率 | 低 | 仅作同分时 tie-break |
第三步是拼装与降级。按分数从高到低装填,装到预算耗尽即停;被裁掉的目录要在开头显式列出(例如「未纳入:src/generated/**、**/__tests__/**」),让模型知道还有东西可查,而不是以为仓库就这么大。
几条不能折叠的硬规则
正在调试的函数体不能只留签名,堆栈指向的那几行必须完整。类型定义的字段名与枚举值不能省略,模型一旦猜错字段名,产出就是不可编译的代码。配置文件按行截取,通常只需要数据源、开关与路径相关的片段。代码块前后要带路径与行号范围,否则模型无法定位应该改哪里,只会重新输出一遍整文件。
反过来,能安全折叠的东西也很明确:内部工具函数实现、自动生成代码、快照与测试夹具、示例数据。
验证方法
用消融对比代替主观感觉:同一批任务分别用「全量拼接」和「裁剪后上下文」各跑一遍,比较三件事——模型是否引用了正确文件与正确符号、编辑是否落在预期位置、产出代码是否可编译。同时记录一个反例集合:那些被裁剪后反而做对的任务,通常是无关文件里的相似命名把模型带偏了,这说明召回策略需要收紧而不是放宽。跑批时固定温度与模型版本,否则两次结果差异分不清来自裁剪还是采样。
常见故障速查
| 现象 | 原因 | 处理 |
|---|---|---|
| 改了 A 却需要 B 一起改 | 调用图只做了单向 | 双向各展开一跳 |
| 模型凭空发明字段名 | 类型定义被折叠 | 类型与枚举原样保留 |
| 输出重复整份文件 | 缺少路径与行号锚点 | 每段代码显式标注来源 |
| 上下文仍然超限 | 预算未约束检索条数 | 按占比硬性截断并显式声明裁剪范围 |
| 改动越跑越保守 | 相似命名文件干扰 | 收紧关键词召回,提高 AST 权重 |
小结
裁剪算法是否合格,看两个可测的数字:模型在正确文件上发起编辑的比例,以及产出代码一次通过编译或类型检查的比例。两项相比全量拼接没有下降,而 token 消耗明显更低,这个策略就成立。若某类任务反复失败且失败原因都是「没看到某个文件」,先检查那条依赖是否属于生成代码或动态引用这类静态分析盲区,再决定是否给它开一个必选白名单。