背景:AI 擅长生成,却不擅长修改
在《Vibe Coding 实践记录》里,我记录过一个观察,现在看来越发清晰:
AI 非常适合于代码的生成,却不一定适合于代码的修改,尤其是大范围的代码修改。
大范围重构时,AI 的表现会急转直下:大量代码错误、大量单元测试失效,它会陷入「改一处、坏十处、再改、再坏」的循环,token 烧得飞快,目标却越来越远。那时我的结论是「 与其让 AI 修复这些代码,不如大刀阔斧地重写」。
但为什么?AI 明明能「写」出几百行新代码,却「改」不动一个跨文件的符号重命名?这两件事的难度差,本质不在 AI 的能力,而在它手里的工具。
为什么:因为 Agent 在操作 Notepad,而不是 IDE
拆开现在大多数 coding agent 的工具箱,底层就三样:read(读文件)、search(正则搜索)、edit(文本替换)。配上 bash
,就是一台能敲键盘的记事本。
Notepad 模式的本质,是让 LLM 操作字节、行号、正则匹配——而不是操作符号、类型、调用关系。
这带来两个直接缺陷:
-
太耗时、太耗 token。 改一个 2000 行的文件,AI 要先把整个文件读进上下文才能定位要改的那几行;查一个符号被谁引用,它要
search命中、再逐个read上下文、再人工判断哪些是「同名不同符号」的误报。每多一层间接,就多一轮 tool call、多一块 token。 -
重构的稳定性差。 文本替换是「点对点」的:改一个函数名,
search只能给出文本匹配,漏掉间接引用、误伤同名符号、改不完 re-export 和别名 import。跨文件重构在文本层是「猜」,在语义层才是「算」。
而人类程序员从来不用记事本改大型代码——我们用的是 IDE。IDE 的核心不是「好看」,是 语义索引:你改一个符号,IDE 靠 language server 精确找出所有引用、联动改完 re-export 和 barrel 文件、顺带告诉你哪些调用方会受影响。
所以问题的答案很朴素: 最有效的 coding harness,应该让 LLM 操作 IDE,而不是操作 Notepad。
核心隐喻:LLM + IDE + Notepad
但「用 IDE 替代 Notepad」是错的。正确的架构不是替代,是叠加——三层,各司其职:
| 层 | 角色 | 成本 | 精度 |
|---|---|---|---|
| LLM | 决策:理解意图、规划、判断 | 最贵、最慢、最易错 | 语义理解 |
| IDE | 增强:符号解析、原子改写、运行时真相 | 便宜、快 | 机械精确 |
| Notepad | 兜底:文本读写,永不失效 | 最便宜 | 字节级 |
三层的分工有一条总原则,一句话:
能下沉到便宜层的认知工作,绝不让 LLM 做。
LLM 只做「语义层也给不了的判断」;harness 负责读、写、验里所有机械的部分。这条原则贯穿了下面每一个设计决策。
LLM:决策层
LLM 的本职是理解「用户要什么」、规划「怎么做」、判断「做到什么程度算对」。它是三层里唯一有「语义理解」的一层,也是唯一该为「想错了」负责的一层。
它不该被浪费在:读文件全文来定位、正则搜引用、逐行数 UTF-16 偏移、单步调试。这些是机械工作,机械工作该由机器做。
IDE:语义增强层
IDE 层的能力边际很广——基础重构、代码调试、语义分析——但它不是一块铁板,而是一条 语义深度的光谱:
| 深度 | 模型操作什么 | 代表能力 |
|---|---|---|
| token/字节 | 行号、文本 | str_replace / grep |
| 语法 | AST 节点 | tree-sitter / ast-grep |
| 符号 | 引用、定义 | LSP references / rename |
| 关系 | caller/callee、类型、数据流 | call graph / 影响面 |
| 运行时 | 变量值、栈帧、goroutine | DAP |
真正的分水岭不在「有没有 LSP」,而在「IDE 层是让 LLM 操作结构化事实,还是让 LLM 拿着结构化工具继续爬」。
多数号称支持 LSP 的 harness 做的是后者:给 LLM 一个 references 工具,让它自己一步步递归查询。这不是语义增强,这是 给
Notepad 加了个放大镜。前者是让 harness 预计算影响面、把「这个改动波及 N 个调用方」直接喂进上下文——LLM 查询语义索引应该是 O
(1) 的读,而不是 O (N) 的爬。
Notepad:兜底层
永远有「没有 language server / 未索引 / 未保存」的文件,语义层失效时不能整体崩。所以兜底层不是被废弃,而是要 极速化:
- 文本搜索、glob、find 进程内执行,零 fork-exec;
- 文件缓存按 mtime 键控,改了才失效;
- 结构化摘要——read 一个文件,返回的是声明骨架而非全文。
Notepad 层的目标不是「够用」,是「快得让兜底不再疼痛」。
五个设计原则
三层堆在一起不会自动变最佳。真正决定成败的是下面五个决策,每一个都对应一个常见的「假语义增强」陷阱。
1. 认知下沉
能由便宜层做的,绝不让 LLM 做。这是总原则,其余四条是它的推论。
影响范围评估是「读」,读该由 harness 用 CPU 预计算,而不是让 LLM 用 turn 和 token 爬 call graph。CPU 便宜、快、可缓存;LLM 的 turn 贵、慢、易断。
2. 语义「写」:从「字符串替换」到「符号改写」
改源代码最忌讳 sed 式的字符串替换——它做的是「点对点」的模式匹配,不感知语义:改一个函数名,同名注释、字符串字面量、无关变量全被误伤;正则的边界 case 一旦漏掉,就是一次误伤;跨文件更无从谈起,sed 根本没有「哪些是同一个符号」的概念。
所以写操作本身也有一条从「易错到精确」的谱系:
| 方式 | 匹配什么 | 风险 |
|---|---|---|
| sed / str_replace | 字符串模式 | 误伤同名、漏掉间接引用 |
| ast-grep | AST 节点 | 结构精确,语义仍可能漂移 |
| symbol rename | 符号 + 引用关系 | 语义精确,需 willRenameFiles 联动 |
| graph 搜索 + 点位修改 | 影响面 + 精确位置 | 先定位后修改,最精确 |
越往右,一次操作动的点越多,出错要回滚的也越多——所以原子性不是可选项,是前提:
- symbol rename 走
willRenameFiles联动 re-export/barrel/别名; - ast 改写 staged 预览(propose → resolve 才落盘);
- 影响面评估先基于 call graph 搜索出精确点位,再逐点修改。
一次操作、全局一致、不落半截。
3. 运行时「验」不是「爬」
DAP 如果被设计成「让 LLM 逐步单步调试」,那还是让 LLM 爬。正确形态是:LLM 提假设,harness 用 DAP 验证,返回结构化事实。
agent 猜: 空指针
→ harness attach 读那一帧
→ 返回: x = null,来源第 N 行
这是「下沉」原则在调试场景的落地:单步是机械的,该由 harness 做;判断「为什么 null」才是 LLM 该做的。
4. 兜底极速化,不是废弃
没有 language server 的语言、项目、文件类型永远存在。语义层失效时,兜底层就是全部。把 grep/glob/find 做到进程内、把 read 做成摘要、把编辑锚点做成内容哈希—— 兜底层的质量决定语义层失效时的下限。
5. 层间一致性:三层共享一个「事实来源」
这是最容易被忽略、也最致命的一点:Notepad 改文件、IDE 改符号、LLM 看上下文,三者必须指向 同一个文件状态。
否则出现「文本层以为改了、语义层索引还是旧的」的不一致——LLM 基于旧索引做 rename,落盘后才发现文件已经被别的操作改过。维护层间一致性靠两样东西:
- mtime 键控缓存:语义索引跟随文件 mtime 失效;
- 内容哈希锚点:文本层检测「编辑后文件状态漂移」。
没有这两样,三层之间的不一致会吃掉语义层带来的全部收益。
主流 Coding Agent 做到哪一层了
把光谱当标尺,横向看目前主流的 coding agent,结论是:底座几乎清一色还是 Notepad,语义层普遍处于「起步、缺失、或靠第三方补丁」的状态。
| agent | Notepad 兜底 | 语法(AST) | 符号(LSP) | 关系(call graph) | 运行时(DAP) |
|---|---|---|---|---|---|
| Claude Code | ✅ | — | ⚠️ 起步 | ❌ | ❌ |
| Codex CLI | ✅ | — | ⚠️ 需求 | ❌ | ❌ |
| Gemini CLI | ✅ | — | ⚠️ 靠补丁 | ❌ | ❌ |
| Aider | ✅ | ⚠️ tree-sitter | ❌ | ❌ | ❌ |
| Cursor | ✅ | ✅ | ✅ | ⚠️ | ⚠️ |
| omp | ✅ | ✅ ast-grep | ✅ 14 op | ⚠️ | ✅ 28 op |
| dsh | ✅ | ❌ | ⚠️ 只读 | ❌ | ❌ |
图例:✅ 原生支持;⚠️ 部分 / 起步 / 靠补丁;❌ 无;— 不适用。下表基于公开信息(issue、文档、社区工具),具体能力随版本快速变化,标注的是方向而非精确边界。
几个值得单独说的点:
-
Claude Code 的 LSP 是「刚起步且不完整」的。 它现在有 built-in LSP,但覆盖的是基础导航(
findReferences一类);rename、call hierarchy、code actions 这些真正能「改符号」和「看影响面」的操作,至今仍是社区里的高票 feature request。换句话说,它读得到符号,改不动符号。 -
Codex CLI 的 LSP 集成还停在 feature request 阶段(社区有 auto-detect + auto-install 的明确诉求),原生语义导航尚未成为底座。
-
Aider 的 repo map 是「符号级,不是语义级」。 tree-sitter 提取出函数和类的名字给模型,这省了 token,但既不解析引用关系、也不支持 rename—— 它是语法层,不是符号层。
-
Serena 和 Graphify 的存在,是最有力的论据。 这两个专门给 LLM 补语义层的第三方工具,恰好对应光谱上两级不同的空白:
- Serena 补「符号级」:spin up 真实的 language server(Pyright、rust-analyzer 等),通过 MCP 把 symbol-level 的 references / rename 暴露给 Claude Code、Codex、Gemini CLI、Cursor 等客户端;
- Graphify 补「关系级」:用 tree-sitter + NetworkX 把整个项目(代码、文档、论文、图)建成可查询的知识图谱,支持 caller/callee 和 PR impact analysis,让模型用更少的 context 理解代码库结构。
一个社区要专门造两个「补丁」去给主流 agent 补 LSP 和 call graph——这件事本身就说明:「缺语义层」是行业共识,且主流还没做透。
也值得点破 Graphify 的精度边界:它建图靠 tree-sitter(语法级),不是 compiler 级语义,所以它的 caller/callee 是静态近似——这恰好印证了前面那句话,关系级是最容易被做坏的一层,一张没有边类型和置信度的图,对模型的价值是打折的。
-
极少数把语义层真正做进去的,是 omp。 14 个 LSP 操作 + rename 联动
willRenameFiles+ ast-grep 结构化改写,再叠加 28 个 DAP 操作的真调试器——它把「符号级 + 运行时级」兑现了;「关系级」仍要靠 references 手动推 caller,还是半成品。 -
dsh 是「有看没有改」的反例。 它有 LSP 能力缝,但只有 4 个只读操作,且显式拒绝
workspace/applyEdit——模型能查影响面,改符号本身还得退回文本替换。
把这些摊开看,分水岭不是「有没有 LSP」,而是两点:语义「写」能不能落地(rename 而非 references),以及运行时「验」能不能落地(DAP 而非 print)。前者决定重构稳不稳,后者决定修 bug 快不快——而这两点,恰好是大多数主流 agent 还没补齐的两块。
理想的 Harness 需要哪些 IDE 能力
IDE 的核心资产不是 UI,是三样东西:语义索引(读)、原子重构(写)、验证反馈(验)。harness 要搬的是这三样,不是高亮和 minimap。下面结合 JetBrains 和 VS Code 的能力,整理出清单。
读:代码理解
| IDE 能力 | JetBrains | VS Code | 对 LLM 的价值 |
|---|---|---|---|
| Go to Definition / Type / Implementation | ✅ | ✅ | 精确定位,替代 grep 猜 |
| Find All References | ✅ | ✅ | 影响面评估的底子 |
| Call Hierarchy(caller/callee) | ✅ 强项 | ⚠️ 弱 | 重构影响范围的核心,文本做不到 |
| Type Hierarchy(继承树) | ✅ 强项 | ⚠️ 弱 | 多态 / 接口影响面 |
| Hover / 推断类型 | ✅ | ✅ | 结构化「这段是什么类型」 |
| 结构大纲 | ✅ | ✅ | 省 token 的声明摘要 |
| 依赖分析(DSM) | ✅ 强项 | ⚠️ | 模块级影响面 |
写:代码修改
| IDE 能力 | JetBrains | VS Code | 对 LLM 的价值 |
|---|---|---|---|
| Rename(语义重命名) | ✅ | ✅ | 跨文件联动,不落半截 |
| Extract Method / Variable / Field | ✅ | ✅ | 结构性重构一步到位 |
| Inline(内联) | ✅ | ✅ | 反向提取 |
| Change Signature | ✅ 强项 | ⚠️ | 改签名联动所有调用方 |
| Move(移动 + 联动 import) | ✅ | ✅ | 移文件自动改引用 |
| Safe Delete | ✅ 强项 | ❌ | 确认无引用才删,避免删出坑 |
| 自动导入 / organize imports | ✅ | ✅ | 补 import、清理 import |
| 结构性搜索替换(AST 而非文本) | ✅ 强项 | ❌ | 精确批量改写 |
| 生成代码(ctor / getter / override) | ✅ | ✅ | 样板代码 |
验:代码验证
| IDE 能力 | JetBrains | VS Code | 对 LLM 的价值 |
|---|---|---|---|
| 实时诊断(编译 / lint 错误) | ✅ | ✅ | 改完立刻知道对不对 |
| Quick Fix(诊断 → 修复一步) | ✅ | ✅ | 「报错 → 修」闭环 |
| 调试器(断点 / 单步 / 变量 / 栈帧 / evaluate) | ✅ | ✅ | 修 bug 从「猜」到「读」 |
| 运行测试 + 失败定位 | ✅ | ✅ | 回归验证 |
| 性能剖析(profiler) | ✅ | ⚠️ | 性能 bug 定位 |
| 覆盖率 | ✅ | ✅ | 补测试的靶向 |
关系:全局影响面
| IDE 能力 | JetBrains | VS Code | 对 LLM 的价值 |
|---|---|---|---|
| Call Graph / Data Flow | ✅ 强项 | ⚠️ | 影响范围评估,动手前知道波及谁 |
| 依赖矩阵(DSM) | ✅ 强项 | ❌ | 模块级耦合 |
| Inspections(成百上千条规则) | ✅ 强项 | ⚠️ | 代码质量审查 |
| 代码索引 | ✅ | ✅ | 一切语义能力的底座 |
图例:✅ 原生具备;⚠️ 弱 / 部分;❌ 缺失;「强项」表示该 IDE 的招牌能力。
把四张表摊开,一个关键结论浮现:VS Code 定义的 LSP + DAP 两个协议,覆盖了「读」和「验」的大部分,却覆盖不了「写」的高杠杆项,也覆盖不了「关系」的全部。
具体说,Change Signature、Safe Delete、结构性搜索替换这三样「大范围重构」的利器,LSP 至今没有标准化;Call Graph / 影响面的预计算,LSP 的
callHierarchy 也只够一半。
所以理想的 harness 不能只做一个「LSP 客户端 + DAP 客户端」,还要自己补两块:
- LSP 没标准化的写操作:Change Signature、Safe Delete、结构搜索替换——靠 ast-grep + 引用分析 + 守卫自己实现;
- 关系级的预计算:Call Graph / 影响面摘要——靠 language server 的
callHierarchy只够一半,另一半要 harness 自己建图、缓存、排序,再喂给模型。
这正是前面说的「认知下沉」:把机械的留给机器,把判断的留给 LLM——而这份清单,就是机器该替 LLM 做的全部机械工作。
结论
「AI 擅长生成、不擅长修改」不是 AI 的固有能力边界,而是当前工具形态的边界。当 agent 手里只有 Notepad,它自然只能在「重写」和「硬改」之间二选一;当 agent 手里有了 IDE 的语义索引和原子改写能力,「修改」和「生成」才会回到同一条难度曲线上。
我心目中最佳的 coding harness,就是把这个隐喻做对: 让 LLM 以 Notepad 为底、以 IDE 为增强、以运行时为验证 。三层齐全不稀奇,稀奇的是分对工、维护好层间一致性、并坚持「认知下沉」——把机械的留给机器,把判断的留给 LLM。
这才是「让 LLM 操作 IDE」的真正含义:不是给它 IDE 的按钮,而是给它 IDE 的 语义。