多文件重构管线:从 LSP 分析到安全 Diff 应用
这一篇专门讲“多文件改写/重构”这一条重型管线:从一个起点符号出发,如何借助 LSP 和索引找出影响面,再让模型生成跨文件 patch,并在 IDE 里安全地落地。
场景定义:什么算“多文件重构”?
在 IDE 里,典型的多文件重构需求包括:
- 把一个接口改成 async 风格,并更新所有调用点;
- 给某个 DTO 增加字段,串起所有读写逻辑;
- 把一层 API 的错误处理统一改成某种模式;
- 重命名一个公共方法/类,并保持调用方一致。
这类需求的几个共性:
- 起点通常是某个“符号”(函数、方法、类、接口、类型别名等);
- 影响范围跨越多个文件/模块,靠肉眼全局搜索既费力又容易漏;
- 修改本身是成批的,必须以“可 review 的 patch 集合”的形式存在。
因此,多文件重构需要一条和 Inline Chat 不同的管线:
先分析影响面,再生成 patch,最后在 IDE 里做分级 review 和应用。
第一步:用 LSP 和索引找出“影响面”
多文件重构的第一步,是从一个起点符号出发,找出所有受影响的地方。
这里 LSP 是天然的“前锋”:
LSP 能直接回答的:
- 符号定义位置(go to definition);
- 所有引用位置(find references);
- 实现/重写关系(implementations / type hierarchy);
- 局部/全局 rename 的影响范围。
索引服务可以补充的:
- 更复杂的模式匹配(例如特定调用链的组合);
- “看起来像是同一模式”的代码片段(语义相似的调用/实现)。
一个比较稳妥的做法是:
- 先用 LSP 找出“一定相关”的点:定义、引用、实现、重写;
- 再用索引做“可能相关”的补充召回:
- 按相同接口名/类型名搜索;
- 按 embedding 搜索结构相似的调用代码。
最终得到一个候选列表,大致长这样:
1[
2 { "file": "src/api/user.ts", "kind": "definition", "range": ... },
3 { "file": "src/api/user.ts", "kind": "call", "range": ... },
4 { "file": "src/services/a.ts", "kind": "call", "range": ... },
5 { "file": "src/services/b.ts", "kind": "call", "range": ... },
6 { "file": "src/tests/user.test.ts","kind": "test", "range": ... }
7]
这就是“影响面”的起点集合。
第二步:构造多文件重构任务的上下文
在确定了影响面之后,需要给模型构造一个能理解“这是一组联动改动”的上下文。
一个常见策略是按“起点 + 调用点/实现集”的方式组织:
- 起点定义(接口/函数/类)完整代码;
- 每一类调用点/实现的代表性片段(不要全塞,按 token 预算采样);
- 涉及到的 DTO/类型定义;
- 用户的重构指令(自然语言)。
例如“把这个接口改成 async 风格”的 prompt 结构可能是:
1系统提示:
2你是一个 IDE 助手,负责在多个文件中进行一致性的 API 重构。
3请基于给出的定义和调用点,生成一组补丁,使得:
4- API 改为 async/await 风格
5- 所有调用点都保持语义一致
6- 尽量保留原有错误处理和日志逻辑
7
8接口定义:
9// File: src/api/user.ts
10<完整定义>
11
12调用点示例:
13// File: src/services/a.ts
14<调用代码片段>
15
16// File: src/services/b.ts
17<调用代码片段>
18
19测试代码示例:
20// File: src/tests/user.test.ts
21<相关测试片段>
22
23用户指令:
24把上面的接口和所有调用点改成 async/await 风格,保证测试可以通过。
25
26输出要求:
27请按下面格式输出多文件补丁:
28@@ file: <path> @@
29@@ range: <startLine>,<endLine> @@
30- 原始行...
31+ 新的行...
关键点在于:
- 让模型看到“代表性”的调用点和测试,而不是所有调用点;
- 用统一的 patch 格式描述输出,方便后续解析;
- 用系统提示限制模型不要“自己发明新 API”。
第三步:生成并解析跨文件 patch
模型返回后,AI 编排层需要把结果解析成结构化的 patch 列表,例如:
1[
2 {
3 "file": "src/api/user.ts",
4 "hunks": [ { "startLine": 10, "endLine": 25, "oldText": "...", "newText": "..." } ]
5 },
6 {
7 "file": "src/services/a.ts",
8 "hunks": [ ... ]
9 },
10 {
11 "file": "src/services/b.ts",
12 "hunks": [ ... ]
13 },
14 {
15 "file": "src/tests/user.test.ts",
16 "hunks": [ ... ]
17 }
18]
解析时需要做几层校验:
- 文件路径是否在允许修改的范围内(例如只允许
src/和tests/); - hunk 的
oldText是否和当前文件内容一致(防止基于旧版本生成的 patch 误改); - 每个文件的改动行数是否在可接受范围内(防止一次性大爆改)。
这些检查通过之后,才会把 patch 传给 IDE 侧进行可视化展示。
第四步:IDE 中的多文件 Diff 体验
在 IDE 里,多文件 patch 的展示通常要兼顾几件事:
分级 review:
- 顶层:受影响文件列表(按模块/重要性分组);
- 中层:每个文件的 diff 视图;
- 底层:hunk 级别的启用/禁用。
粒度控制:
- 允许“全部应用”作为快捷操作;
- 也支持只选某几个文件、某几个 hunk 应用。
后置检查:
- patch 应用后自动触发一次 LSP 诊断/编译;
- 把新增的错误明显标注出来。
从用户视角看,这更像是一种“AI 辅助的批量重构工具”:
模型负责提出一套候选改动,人来拍板,LSP 来兜底。
第五步:安全策略与回滚
多文件重构的风险明显高于局部重写,需要额外的安全网:
路径白名单/黑名单:
- 禁止改动某些关键目录(例如安全/权限/支付核心模块),只允许给出建议;
- 禁止跨仓库/工作区之外的路径。
行数与文件数限制:
- 单次重构最多改动多少个文件、多少行代码;
- 超过阈值时要求拆批执行。
回滚机制:
- 把整个 AI 重构操作当成一次“原子操作”,支持一键撤销;
- 在提交前,鼓励用户使用 git diff 再看一眼。
这些策略其实是在回答两个问题:
- “2️⃣ 如何支持多文件重构?” ——通过 LSP+索引分析影响面 + 多文件 patch pipeline 实现;
- “6️⃣ 如何保证 Diff 安全应用?” ——通过路径/行数限制、AST/语法校验、强制 review 和回滚能力来兜底。
和 Inline Chat 的关系:两条互补的管线
最后再强调一下边界:
Inline Chat:
- 以“选区”为中心,负责局部、小范围改写;
- 改动范围通常在一个函数/类内部。
多文件重构管线:
- 以“符号”为中心,负责跨文件、一致性改写;
- 改动范围跨多个文件/模块。
从 IDE 工程角度看,把这两条管线拆开设计,会比“一个 Chat 什么都干”更可控:
既方便在局部场景快速迭代体验,又能在重构场景上引入更强的分析和安全约束。