我心目中最佳的 Coding Harness

背景:AI 擅长生成,却不擅长修改

《Vibe Coding 实践记录》里,我记录过一个观察,现在看来越发清晰:

AI 非常适合于代码的生成,却不一定适合于代码的修改,尤其是大范围的代码修改。

大范围重构时,AI 的表现会急转直下:大量代码错误、大量单元测试失效,它会陷入「改一处、坏十处、再改、再坏」的循环,token 烧得飞快,目标却越来越远。那时我的结论是「 与其让 AI 修复这些代码,不如大刀阔斧地重写」。

但为什么?AI 明明能「写」出几百行新代码,却「改」不动一个跨文件的符号重命名?这两件事的难度差,本质不在 AI 的能力,而在它手里的工具。

为什么:因为 Agent 在操作 Notepad,而不是 IDE

拆开现在大多数 coding agent 的工具箱,底层就三样:read(读文件)、search(正则搜索)、edit(文本替换)。配上 bash ,就是一台能敲键盘的记事本。

Notepad 模式的本质,是让 LLM 操作字节、行号、正则匹配——而不是操作符号、类型、调用关系。

这带来两个直接缺陷:

  1. 太耗时、太耗 token。 改一个 2000 行的文件,AI 要先把整个文件读进上下文才能定位要改的那几行;查一个符号被谁引用,它要 search 命中、再逐个 read 上下文、再人工判断哪些是「同名不同符号」的误报。每多一层间接,就多一轮 tool call、多一块 token。

  2. 重构的稳定性差。 文本替换是「点对点」的:改一个函数名,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 / 影响面
运行时变量值、栈帧、goroutineDAP

真正的分水岭不在「有没有 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-grepAST 节点结构精确,语义仍可能漂移
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,语义层普遍处于「起步、缺失、或靠第三方补丁」的状态。

agentNotepad 兜底语法(AST)符号(LSP)关系(call graph)运行时(DAP)
Claude Code⚠️ 起步
Codex CLI⚠️ 需求
Gemini CLI⚠️ 靠补丁
Aider⚠️ tree-sitter
Cursor⚠️⚠️
omp✅ ast-grep✅ 14 op⚠️✅ 28 op
dsh⚠️ 只读

图例:✅ 原生支持;⚠️ 部分 / 起步 / 靠补丁;❌ 无;— 不适用。下表基于公开信息(issue、文档、社区工具),具体能力随版本快速变化,标注的是方向而非精确边界。

几个值得单独说的点:

  1. Claude Code 的 LSP 是「刚起步且不完整」的。 它现在有 built-in LSP,但覆盖的是基础导航(findReferences 一类);rename、call hierarchy、code actions 这些真正能「改符号」和「看影响面」的操作,至今仍是社区里的高票 feature request。换句话说,它读得到符号,改不动符号。

  2. Codex CLI 的 LSP 集成还停在 feature request 阶段(社区有 auto-detect + auto-install 的明确诉求),原生语义导航尚未成为底座。

  3. Aider 的 repo map 是「符号级,不是语义级」。 tree-sitter 提取出函数和类的名字给模型,这省了 token,但既不解析引用关系、也不支持 rename—— 它是语法层,不是符号层。

  4. 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 是静态近似——这恰好印证了前面那句话,关系级是最容易被做坏的一层,一张没有边类型和置信度的图,对模型的价值是打折的。

  5. 极少数把语义层真正做进去的,是 omp。 14 个 LSP 操作 + rename 联动 willRenameFiles + ast-grep 结构化改写,再叠加 28 个 DAP 操作的真调试器——它把「符号级 + 运行时级」兑现了;「关系级」仍要靠 references 手动推 caller,还是半成品。

  6. dsh 是「有看没有改」的反例。 它有 LSP 能力缝,但只有 4 个只读操作,且显式拒绝 workspace/applyEdit——模型能查影响面,改符号本身还得退回文本替换。

把这些摊开看,分水岭不是「有没有 LSP」,而是两点:语义「写」能不能落地(rename 而非 references),以及运行时「验」能不能落地(DAP 而非 print)。前者决定重构稳不稳,后者决定修 bug 快不快——而这两点,恰好是大多数主流 agent 还没补齐的两块。

理想的 Harness 需要哪些 IDE 能力

IDE 的核心资产不是 UI,是三样东西:语义索引(读)、原子重构(写)、验证反馈(验)。harness 要搬的是这三样,不是高亮和 minimap。下面结合 JetBrains 和 VS Code 的能力,整理出清单。

读:代码理解

IDE 能力JetBrainsVS Code对 LLM 的价值
Go to Definition / Type / Implementation精确定位,替代 grep 猜
Find All References影响面评估的底子
Call Hierarchy(caller/callee)✅ 强项⚠️ 弱重构影响范围的核心,文本做不到
Type Hierarchy(继承树)✅ 强项⚠️ 弱多态 / 接口影响面
Hover / 推断类型结构化「这段是什么类型」
结构大纲省 token 的声明摘要
依赖分析(DSM)✅ 强项⚠️模块级影响面

写:代码修改

IDE 能力JetBrainsVS Code对 LLM 的价值
Rename(语义重命名)跨文件联动,不落半截
Extract Method / Variable / Field结构性重构一步到位
Inline(内联)反向提取
Change Signature✅ 强项⚠️改签名联动所有调用方
Move(移动 + 联动 import)移文件自动改引用
Safe Delete✅ 强项确认无引用才删,避免删出坑
自动导入 / organize imports补 import、清理 import
结构性搜索替换(AST 而非文本)✅ 强项精确批量改写
生成代码(ctor / getter / override)样板代码

验:代码验证

IDE 能力JetBrainsVS Code对 LLM 的价值
实时诊断(编译 / lint 错误)改完立刻知道对不对
Quick Fix(诊断 → 修复一步)「报错 → 修」闭环
调试器(断点 / 单步 / 变量 / 栈帧 / evaluate)修 bug 从「猜」到「读」
运行测试 + 失败定位回归验证
性能剖析(profiler)⚠️性能 bug 定位
覆盖率补测试的靶向

关系:全局影响面

IDE 能力JetBrainsVS 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 客户端」,还要自己补两块:

  1. LSP 没标准化的写操作:Change Signature、Safe Delete、结构搜索替换——靠 ast-grep + 引用分析 + 守卫自己实现;
  2. 关系级的预计算:Call Graph / 影响面摘要——靠 language server 的 callHierarchy 只够一半,另一半要 harness 自己建图、缓存、排序,再喂给模型。

这正是前面说的「认知下沉」:把机械的留给机器,把判断的留给 LLM——而这份清单,就是机器该替 LLM 做的全部机械工作。

结论

「AI 擅长生成、不擅长修改」不是 AI 的固有能力边界,而是当前工具形态的边界。当 agent 手里只有 Notepad,它自然只能在「重写」和「硬改」之间二选一;当 agent 手里有了 IDE 的语义索引和原子改写能力,「修改」和「生成」才会回到同一条难度曲线上。

我心目中最佳的 coding harness,就是把这个隐喻做对: 让 LLM 以 Notepad 为底、以 IDE 为增强、以运行时为验证 。三层齐全不稀奇,稀奇的是分对工、维护好层间一致性、并坚持「认知下沉」——把机械的留给机器,把判断的留给 LLM。

这才是「让 LLM 操作 IDE」的真正含义:不是给它 IDE 的按钮,而是给它 IDE 的 语义