Coding Agents — 「对世界采取动作」分支里最成熟的一支
这条分支在货架里的位置: 总纲把这 189 个库的共性讲成一句话——「把一个只会吐文本的 LLM,包成能在循环里感知-动作、能被放心运营的程序」。围绕这条共性分出 6 条分支,本章是其中**分支 B「对世界采取动作」**里被拆得最透的一支:当那个「动作」是改一个真实代码库时,LLM 文本怎么可靠地变成对磁盘文件的精确编辑。 别的分支只是把「动作」换成查文档 / 控浏览器 / 搭流程——内核同源,但编码 agent 把「意图→精确落盘」这一环逼到了极致,所以最值得先读。
30 秒导读: 这一档库都在干同一件事——让 LLM 可靠地改一个真实的代码库。难点从来不是「调用模型」,而是四个工程问题:① 该改哪(怎么给模型看代码上下文)、② 把模型说的改动精确落到磁盘文件、③ 改错了怎么恢复、④ 一个大任务跑很久怎么管。本章把 19 个库横过来切,告诉你这四个问题各有哪几种流派、各自的代表作、以及整个领域在往哪走。读完你应该能自己画出这张地图。
怎么读本章: 正文是给人读的白话综述,不出现路径行号 。每个论断后面挂一个脚注
[^x],精确的仓库/路径:行+ 符号名都在脚注里;对比矩阵则单开一列「代码锚点」放引用。人读正文,agent 读脚注 / 锚点列。
1. 这条分支要解决什么(第一性原理)
承上:整个货架的共性是「给只会说话的 LLM 装手脚,让它能对世界采取动作」。本分支把「动作」锁死成**「改代码」**——这恰好是最难、也最能看清工程取舍的一种动作,因为「改对一个文件」比「点一个按钮」要求高得多:差一个缩进就落盘失败。
假设你在一个几百个文件的项目里,想让 AI 帮你加个功能。把整个项目贴进聊天窗口不现实(太大),AI 给你的代码你还得手动对齐、复制粘贴到正确位置。一个「编码 agent」就是替你把这两件苦工自动化的程序。
把它拆开,所有编码 agent 都在解四个子问题:
| 子问题 | 白话 | 难在哪 |
|---|---|---|
| 代码上下文 | 该让模型看哪些代码? | 仓库装不进上下文窗口 |
| 编辑落盘 | 「把这段换成那段」怎么打到文件上? | 模型给的旧代码几乎从不和文件一字不差 |
| 错误恢复 | 对不上 / 跑测试报错怎么办? | 不能静默吞,也不能死循环 |
| 长任务管理 | 改十个文件、跑半小时怎么不跑飞? | 上下文会爆、会偏题、会忘 |
一句话直觉: 模型负责「理解 需求、想出改动」,确定性代码负责「把改动精确落盘、挑相关上下文、出错重试」。一个编码 agent 的工程价值,几乎全在后半句。本章 §2–§3 主要围绕编辑落盘和代码上下文这两条主轴展开,因为它们的流派分化最清楚、最能体现各库的取舍。
形态上,这 19 个库落在三种壳子里:CLI / 终端(aider、codex、crush、gemini-cli、qwen-code、plandex、gptme、pi、oh-my-pi、ra-aid、kilocode、openwork)、IDE 扩展(cline、continue、tabby)、Neovim 插件(avante-nvim、codecompanion-nvim)。还有两个特例:tabby 其实是代码补全服务器(不做 agentic 编辑),auto-code-rover 是面向 SWE-bench 的研究型程序修复 agent。
2. 流派一:编辑怎么落盘(本分支的灵魂)
这是分化最剧烈的一条主轴。「模型描述一处改动,程序把它打到文件上」——光这一步就长出了五个流派。
怎么读这张图: 从左到右是「模型负责精确」的程度递增。最左边模型只管出意图、程序兜底容错;越往右越依赖模型自己给出能精确定位的编辑。最下面单列的⑤换了一套思路——不匹配文本,靠结构 / 锚点定位。
模型只出意图 ────────────────────────────────► 模型自己给精确定位
程序兜底容错 运行时只做执行
┌──────────┐ ┌──────────┐ ┌──────────────┐ ┌──────────────┐
│ ① 整文件 │──►│ ② 文本 │──►│ ③ 结构化工具 │──►│ ④ 第二个模型 │
│ 重写 │ │ diff 块 │ │ 调用 │ │ 来 apply │
│whole file│ │SEARCH/ │ │str_replace / │ │ apply model │
│ │ │REPLACE │ │ apply_patch │ │ fast-apply │
└──────────┘ └──────────┘ └──────────────┘ └──────────────┘
┌────────────────────────────────────────────────────────────────┐
│ ⑤ 结构 / 锚点定位(不匹配文本):tree-sitter 引用 / hash anchor │
└────────────────────────────────────────────────────────────────┘
下面逐个流派接地。关键洞察:无论哪个流派,只要模型给的是「一段要被找到的旧文本」,就绕不开模糊匹配——这是②③⑤共同的暗线(见 §2.6)。
2.1 整文件重写(whole file)
最朴素:让 LLM 把整个文件重写一遍,程序直接覆盖写盘。简单、不会「对不上」,但浪费 token、还容易误删没动的代码。
- aider 把整文件重写做成一个独立 coder,并且它是模型设置里的默认格式——弱模型用它最稳。1 详见子库 doc aider/04-loop-and-git.md §4.6。
- plandex 在需要时走「重建整文件」路径,有专门的整文件提示模板。2
- cline 的整文件写工具(
write_to_file)同样直接覆盖,创建新文件或大段重写时用它。3
2.2 文本 diff 块:SEARCH / REPLACE
更省的做法:让模型只说「把这段旧代码换成这段新代码」,用一对围栏标记包起来。这是 aider 的默认,也是被抄得最多的格式。
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
- aider:解析靠一个状态机逐行扫围栏(
find_original_update_blocks)——这是该流派的教科书实现。4 详见子库 doc aider/01-edit-formats.md。 - cline(VS Code 主程序):
replace_in_file工具用同一套SEARCH … ======= … REPLACE围栏,失败恢复提示还会教模型「一次只改 50–100 行」。5 - gptme:
patch_many工具用ORIGINAL / DIVIDER / UPDATED标记,且能一次原子改多文件。6 - avante-nvim:非 fast-apply 模式下把 LLM 的 diff 转成 search/replace 再落盘。7
2.3 结构化工具调用:str_replace / apply_patch
新一代趋势:不再让模型在自由文本里写围栏,而是给它一个带 schema 的工具,参数是 old_string / new_string(或一段 patch),由工具运行时保证精确。这把「格式正确性」从「模型自觉」变成了「schema 强约束」。两个子流派:
(a) str_replace 风格(old_string 必须在文件里唯一出现):
- crush:
edit工具,出现 0 次或多次直接报错,要求模型加上下文或显式replace_all。8 - pi:
edit工具,要求旧文本「在原文件唯一」,支持一次传一批编辑批量改。9 - cline(SDK):
edit_file工具,严格要求「恰好命中一次」,0 次或多次都报错。10 - gemini-cli / qwen-code:都是
replace工具,默认要求唯一命中、要改多处得显式开门控;但门控参数已分化——gemini-cli 用allow_multiple,qwen-code 在 fork 后改用布尔replace_all。11
(b) apply_patch 语法(一段带 *** Update File / @@ 上下文的 patch):
- codex:有专门的 apply-patch crate,工具名
apply_patch,patch 语法用*** Begin Patch/*** Update File/@@ 上下文行。12 - cline(SDK):除了
edit_file,还提供apply_patch工具,直接声明用「canonical apply_patch grammar」——同一个 agent 同时给两种工具,是这一代的典型做法。13
2.4 第二个模型来 apply(apply model / fast-apply)
一个更激进的思路:让「主模型」只输出带省略号的新代码(// ... existing code ...),再交给一个专门的、便宜的 apply 模型把它和原文件合并。主模型不必精确,合并交给专家。
- continue:先判断能不能「确定性瞬时应用」(用 AST 把省略块直接塞回去,判定函数
isLazyText),不行再调 apply 模型流式合并(streamLazyApply)。14 - avante-nvim:agentic 模式下若开 fast-apply,
edit_file工具走 morph 这个 fast-apply 提供商做合并,失败再退回 str_replace。15
2.5 结构 / 锚点定位(tree-sitter / hash anchor)
最后一类不靠「匹配文本」,而靠结构或锚点定位要改的位置:
- plandex:用 tree-sitter 把改动表示成对原文件的引用和删除区间,再按锚点把「提议文件」重建出来,另有独立的「唯一替换」校验。16
- oh-my-pi:独有 hashline 模式——给每行一个哈希锚点,模型用锚点而非行号定位,patcher 验证锚点再落盘;锚点对不上时会精确提示「re-read the file」而不是让模型瞎猜。17
2.6 暗线:几乎人人都要做「多级降级模糊匹配」
只要模型给的是「一段要在文件里找到的旧文本」(②③⑤都是),它就几乎从不和文件一字不差——缩进会差、会用 ... 省略、空行会多。于是各库不约而同地实现了一套从严到松、命中即停的降级匹配。这是本领域最值得带走的工程智慧:
| 库 | 降级阶梯(从严到松,命中即停) | 代码锚点 |
|---|---|---|
| aider | 精确 → 容忍缩进 → 丢开头空行 → ... 省略 | 18 |
| opencode(kilocode / openwork 行为) | 九级级联:Simple → LineTrimmed → BlockAnchor → WhitespaceNormalized → IndentationFlexible → EscapeNormalized → TrimmedBoundary → ContextAware → MultiOccurrence | 19 |
| codex | 精确 → 忽略行尾空白 → 忽略首尾空白(注释明说 "decreasing strictness") | 20 |
| gemini-cli | 精确 → 逐行 trim 的 flexible 匹配并按真实缩进重排 → 再不行调 LLM 去修 | 21 |
| codecompanion | exact_match → trimmed_lines …,多个候选相似度差 < 0.15 就换下一策略 | 22 |
领域智慧: opencode 的九级级联和 aider 的四级,本质是同一个发现——「宁可多写几级确定性的容错,也别让模型去精确数空格」。gemini-cli 更进一步,把 LLM 自己当作最后一级模糊匹配器(对不上就让模型修),这是 §6 趋势的伏笔。
3. 流派二:代码上下文怎么给
第二条主轴:仓库装不进上下文窗口,该让模型看哪些代码?三个流派。
怎么读这张图: 从左到右是「谁来挑上下文」——左边程序预先压缩,右边模型自己边查边读。
程序预先压缩 ──────────────────────────────► 模型自己边查边读
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ ① 静态 │──►│ ② 结构感知 │──►│ ③ agentic 探索 │
│ repo-map │ │ 检索 │ │ 模型自己 │
│ tree-sitter │ │ AST 符号 API │ │ grep / read │
│ + PageRank │ │ │ │ + LSP │
└──────────────┘ └──────────────┘ └──────────────────┘
aider auto-code-rover crush / codex / cline …
3.1 静态 repo-map(程序预先压缩)
把整个仓库压成一张「目录 + 关键符号」的地图,预先塞进提示。代表作只有一个,但极有分量:
- aider:用 tree-sitter 抽符号,把「谁引用谁」建成图跑 PageRank 排重要性,再二分搜索贴近 token 预算挑选。23 这是该流派的标志性实现,详见子库 doc aider/03-repo-map.md。
3.2 结构感知检索(AST 符号 API)
不给全图,而给模型一组按结构查代码的工具——「查这个类/方法/代码片段定义在哪」。
- auto-code-rover:核心就是一组 AST 检索 API(
search_class/search_method/search_method_in_class/search_code),模型迭代调用它们「按符号定位」上下文。24 这是它在 SWE-bench 上的方法论核心。
3.3 agentic 探索(模型自己 grep / read)
最主流的当代做法:不预先压缩,给模型 grep / glob / read / ls 等工具,让它自己边查边读,按需检索。
- crush:完整的探索工具集
glob/grep/rg/ls/view/sourcegraph,外加 LSP 取诊断与符号。25 - codex:没有 repo-map,靠 shell 里的 grep/glob + 环境上下文做 agentic 探索。26
- cline / gemini-cli / qwen-code / pi / oh-my-pi / gptme / kilocode / openwork:都属此类——给模型搜索与读文件工具,让它自主探索。
对照: aider 偏预先压缩(一张静态地图,便宜但有损),agentic 流派偏按需检索(更准但更费 token、更慢)。auto-code-rover 介于两者之间——结构化但仍由模型驱动检索。
4. 对比矩阵(逐格接地)
维度:编辑落盘方式(§2)、上下文策略(§3)、错误恢复、模型绑定、形态。描述列写白话,精确
path:line+符号 全在最右「代码锚点」列。—表示该维度对此库不适用。
| 库 (id) | 编辑落盘 | 上下文策略 | 错误恢复 | 形态 | 代码锚点 |
|---|---|---|---|---|---|
| aider | ② SEARCH/REPLACE(默认),含 whole/udiff 变体 | ① 静态 repo-map + PageRank | 多级模糊匹配 → 对不上报错反喂 | CLI | 01-edit-formats;03-repo-map;editblock_coder.py:439/:157,repomap.py:365 |
| cline | ③ edit_file(唯一)+ apply_patch;VS Code 程序另有 SEARCH/REPLACE | ③ agentic | 严格唯一匹配,失败即报错引导 | IDE 扩展 + CLI + SDK | sdk/.../executors/editor.ts:133,schemas.ts:204;apps/vscode/.../shared/tools.ts:12 |
| continue | ④ apply model:lazy 块 → 确定性 AST 或 apply 模型合并 | ③ agentic + 自家检索 | apply 不支持模型则报错 | IDE 扩展(VS Code/JetBrains) | core/edit/lazy/streamLazyApply.ts:14,deterministic.ts:19 |
| codex | ③ apply_patch 语法,专属 crate | ③ agentic(shell grep/glob,无 repo-map) | seek_sequence 三级降级匹配 | CLI(Rust) | codex-rs/core/src/tools/handlers/apply_patch.rs;apply-patch/src/seek_sequence.rs:1 |
| crush | ③ edit(old/new + replace_all)+ multiedit | ③ agentic:glob/grep/rg/view + LSP | 0/多次匹配报错要求加上下文 | CLI(Go) | internal/agent/tools/edit.go:26/:47;internal/lsp/manager.go |
| qwen-code | ③ replace(默认唯一,replace_all 多匹配);fork 自 gemini-cli 后已分化 | ③ agentic(承袭 gemini-cli 工具) | 同 gemini-cli flexible 匹配 | CLI(TS) | packages/core/src/tools/edit.ts:104/:148/:305(replace_all);README.md:157(fork 自述) |
| gemini-cli | ③ replace(默认唯一,allow_multiple 多匹配) | ③ agentic | 精确 → flexible(trim+重排)→ LLM 修 | CLI(TS) | packages/core/src/tools/edit.ts:368(allow_multiple)/:299/:547;utils/llm-edit-fixer.ts:142 |
| kilocode | ②/③ 打包 opencode 的九级替换器 edit 工具 | ③ agentic(opencode 工具集) | 九级替换器级联 | CLI(OpenCode fork)+ VS Code 扩展 | packages/opencode/src/tool/edit.ts:241/:710 |
| plandex | ⑤ tree-sitter 结构/引用定位 + ① whole-file 回退 | ③ agentic(面向大项目/长任务) | 唯一替换校验 | CLI(Go) | app/server/syntax/structured_edits_apply.go:91;unique_replacement.go;prompts/build_whole_file.go |
| gpt-pilot | ② diff 块(unified hunk) | 文件描述 + relevant_files 过滤 | 逐 hunk review:apply/rework/ignore + 人在环上 | CLI(自治开发者,step-by-step) | core/agents/code_monkey.py:26/:37-49/:68 |
| tabby | — (代码补全,不做 agentic 编辑) | FIM:prefix/suffix segments | — | 补全服务器 + IDE 客户端 | crates/tabby/src/services/completion.rs:135 Segments |
| pi | ③ edit(唯一 + edits[] 批量) | ③ agentic(Read/Write/Edit/Bash 极简集) | 唯一匹配要求 | CLI(TS,极简自扩展) | packages/coding-agent/src/core/tools/edit.ts:35 |
| oh-my-pi | ⑤ hashline 哈希锚点 + 兼容 pi 的 str_replace | ③ agentic + LSP + subagents | 锚点不符精确提示 re-read + noop 循环守卫 | CLI(Pi 衍生) | packages/coding-agent/src/edit/hashline/execute.ts:21/:73;noop-loop-guard.ts |
| avante-nvim | ④ fast-apply(morph)→ ② diff2search_replace 回退 | ③ agentic(llm_tools/) | fast-apply 失败退 str_replace | Neovim 插件 | lua/avante/llm_tools/edit_file.lua:62/:218;utils/diff2search_replace.lua |
| codecompanion-nvim | ② insert_edit_into_file 多策略匹配 | ③ agentic + 编辑器上下文 | 多策略 + 相似度阈值 0.15 换策略 | Neovim 插件 | .../insert_edit_into_file/strategies.lua:8/:122/:177 |
| gptme | ② patch_many,ORIGINAL/UPDATED 标记,多文件原子 | ③ agentic(read/shell/python/browser 工具) | patch 对不上报错重试 | CLI | gptme/tools/patch_many.py:18/:30 |
| ra-aid | 委派给 aider 落盘 | ③ agentic(ripgrep + emit_related_files) | 由 aider 兜底 | CLI(研究→规划→实现三阶段) | ra_aid/tools/programmer.py:31/:58 |
| auto-code-rover | ② SEARCH/REPLACE 风格补丁(SWE-bench 程序修复) | ② 结构感知:search_class/method/code | 检索→定位→打补丁迭代,失败回检索 | 研究型 agent(SWE-bench) | app/search/search_backend.py:276/:451/:481 |
| openwork | 行为继承自 opencode 的 edit(同 kilocode 九级替换器);本仓不 vendor opencode 源码,在 PATH 上跑外部 opencode | ③ agentic(opencode 工具集) | 同 opencode 九级级联(由外部 opencode 提供) | 桌面 app(OpenCode 驱动) | openwork README.md:10「powered by OpenCode」;九级实现见 kilocode/packages/opencode/src/tool/edit.ts:710 |
| kun | ③ edit(exact str_replace:oldText/newText,支持 edits[] 批量 disjoint,要求唯一命中) | ③ agentic:ripgrep/fd backed ls/grep/read + lsp 工具(无 repo-map) | 无模糊匹配:0 次/多次命中即报错要求加上下文或拆分;ToolStormBreaker 拦重复调用 + 截断参数提示重试 | 桌面 app(Electron GUI + 本地 kun serve 运行时,PolyForm 非商业许可) | kun/src/adapters/tool/builtin-file-tools.ts:87(edit);builtin-tool-utils.ts:718 findOccurrences/:735-740 唯一命中报错;builtin-search-tools.ts:31/:183(ls/grep)、builtin-lsp-tool.ts:66(lsp);loop/tool-storm-breaker.ts;loop/agent-loop.ts:762 AgentLoop |
| whale | ③ edit(search/replace/all,str_replace 风格)+ multi_edit;提交前用字节快照校验文件未变,否则回 write_conflict | ③ agentic(grep/view/ls/search_files;无 repo-map、无 LSP) | 多级模糊匹配(exact → whitespace-relaxed → JSON-escape unwrap)+ 行级 divergence 诊断;搜不到报 search_not_found 引导 re-read | CLI/TUI(Go,仅 DeepSeek) | internal/tools/edit.go:45/:147(resolveEditSearch)/:271(editSearchDivergence);file_mutation.go:111(verifyFilePlanCurrent, bytes.Equal);catalog_files.go:74(edit)/:108(multi_edit) |
矩阵里反复出现的两个「家族」:opencode 家族(kilocode 直接 vendor 了 opencode 的
edit工具源码,openwork 则在 PATH 上跑外部 opencode、行为同源)和 pi 家族(pi → oh-my-pi)。长尾库不必逐一拆解——它们在编辑/上下文上往往落进上面某个已接地的格子。