本文整理自 CNB 上的开源课程 《Vibe Agent:从零搭建 Coding Agent》。课程讲义、示例代码与练习仓库均在 CNB 上公开,文中示例出自该仓库对应章节。

把一个编程任务交给 Agent,看到的是连续的工具调用:列目录、读文件、改几行、跑一次命令。这一串动作不是随机的,它对应人写代码时的固定路径。人在 IDE 里改一个函数,流程也差不多:展开目录看结构,用 Cmd+P 或全文搜索定位文件,打开文件读上下文,改那几行,跑测试确认。Coding Agent 的工作模式就是这五步,只是每一步换成了工具调用。

五个阶段与对应的工具

阶段 人做什么 工具
浏览 展开目录树,了解项目结构 list_dir
定位 按文件名找文件,按内容搜文本 glob、grep
理解 打开关键文件读代码 read
修改 新建文件,改某一段代码 write、edit
验证 跑测试,执行脚本 bash

这五个阶段会反复循环。测试失败,回到修改;改完再跑,直到通过。Cursor、Claude Code、Devin 的核心循环都落在这五个阶段上,差别在实现精度。

课程列出的必做工具是五个:list_dir、read、write、edit、bash。glob 与 grep 属于定位阶段的补强,不是闭环的必需品,靠 list_dir 一层层看下去也能找到文件,只是慢。五个工具覆盖了最低限度的能力。

按阶段分类,不按文件操作分类

按文件操作的动词来切工具,是很容易踩的坑:给模型一组 open、close、read、write,看起来齐全,实际多出几个用不上的选项。

open 与 close 就没有必要。模型不需要持有文件句柄,read 一次把内容读回来就够,读过之后的”打开状态”没有任何用处。

delete 也没有放进最小工具集。删文件是高风险操作,误删之后很难恢复,它也不属于五个阶段里的任何一个。模型真要删,可以用 bash 执行 rm,但那就进入了另一个受约束的通道。

move 同理,bash 里一条 mv 就能完成,不值得单独开一个工具。

工具集的边界由工作流决定,不由文件系统的接口决定。每多一个工具,模型的选择空间就多一份误判的可能;参数越相似,选错的代价越高。工具越多,每轮请求携带的工具描述也越长,这部分 token 每轮都要付一次。

工具的输出是给模型看的界面

五个工具在数据流上是一条链:list_dir 的输出决定下一步 read 哪个文件,read 的输出决定 edit 改哪一段,bash 的输出去决定要不要再改一轮。这条链能跑通的前提是上一步的输出格式刚好能喂给下一步。

工具 输出设计 服务对象
read 每行带行号 让模型能引用具体位置,让报错定位更准
list_dir 区分文件与目录 让模型知道下一步是 read 还是继续往下列
bash 附带退出码 让模型判断命令是成功还是失败

read 返回的格式是这样:

1
2
3
4
1:def greet() -> None:
2: print('hello')
3:
4:greet()

行号在这里的作用是让模型能说”第 4 行调用有问题”,而不是”文件末尾附近有问题”。read 还接受 offset 与 limit,大文件可以只读一段,避免一次把上下文塞满。list_dir 用 [D] 与 [F] 前缀区分目录和文件,模型据此决定是继续浏览还是开始阅读。bash 在命令返回非零时把退出码放在输出开头,模型据此判断测试是否通过;没有退出码,它只能从文本里猜,猜错的概率不低。

设计输出格式时,要问的是下一个工具能从这段输出里拿走什么,打印出来好不好看无关紧要。

修改用 edit,新建用 write

一个常见的用法是让模型每次都用 write 重写整个文件,哪怕只改一个变量名。每改一行都要重写全篇,成本随文件大小线性上涨,而模型按 token 计费。长文件写到后面容易漏段,输出长度有上限,少写的函数要等运行报错才会暴露。整篇重写之后也看不出这一次到底改了什么,等价于用 git commit -a 却不写提交信息。

区分标准只有一条:文件已存在,用 edit 改目标段;文件不存在,才用 write 从头建。

1
2
3
4
5
edit(
"main.py",
old_content="def greet():\n print('hello')",
new_content="def greet():\n print('=== 开场 ===')\n print('hello')",
)

old_content 必须与文件里的内容逐字符一致,包括缩进和换行。这个参数怎么定位、为什么要强制唯一,下一节展开。

走一遍完整流程

任务可以很小:给 main.py 里的 greet 函数加一句开场白,再运行确认。

1
2
3
4
list_dir(".")            看到 main.py 与 utils.py
read("main.py") 拿到带行号的源码
edit("main.py", ...) 替换目标段
bash("python main.py") 运行并拿到输出

执行到 bash,看到输出,任务结束。输出不符合预期就退回 edit,重读、重改、重跑。这四步构成的循环,就是 Agent 写代码时反复做的事。

循环不能无限转。模型可能一直改不到点上,也可能反复跑同一条命令,所以 Agent 的 run 方法带最大轮次参数,课程示例的默认值是 12 轮,到顶就停下来交还结果。

把这一串工具调用交给模型,配上 tool_calls 协议,循环由模型自己驱动,第 5 章的 Coding Agent 就是按这个结构组装的。

工具集变大之后,模型在错误的阶段绕圈的机会也会变多。工具按阶段切分、输出按下一步的需要设计,循环才会收敛。

这对搭 Agent 意味着什么:写代码这件事可以拆成有明确输入输出的阶段,把工具看作这条数据流上的节点,比把它当成一份能力清单更有用。先想清楚阶段,再决定每个工具收什么参数、返回什么格式,比先堆功能再补流程省事得多。

工具的参数和返回值还有一层设计,专门用来兜住模型常犯的错误。下一节拆开其中四个关键选择。