文章建议将一次性脚本统一为开发者最熟悉的语言,以便真正审查 Agent 生成的代码,而不是盲目接受结果。作者还尝试量化 Ruby 相比 Python 的 token 节省效果。
Lucian Ghinda 发表了一篇文章,主张你应该让 coding agent 使用 Ruby 编写那些用完即弃的脚本。下面是他建议你完整粘贴到 Agent 指令文件里的内容:
## Scripts
Write throwaway and utility scripts (data munging, one-off migrations,
file renames, glue code) in Ruby, even in projects written in another
language. If it needs a pipe, a loop, a conditional, or more than one
line, it is a script: write it in Ruby, not Python, Node, or bash.
Single self-contained commands (`grep`, `git status`) are fine as-is.
Use only the Ruby standard library. If a gem would clearly save
significant effort, stop and ask before using it.
Put temporary scripts in a scratch or temp directory, not the repo
root, and delete them when done unless asked to keep them.
当天晚上,我就把它粘贴进了自己的全局 CLAUDE.md。他的论点围绕代码审查展开:他每天都在读 Ruby,所以当 Agent 编写 Ruby 时,他依然能扮演审查者,而不是看着 diff 频频点头。他给出了三个理由,却没有一个与成本有关。于是,我开始寻找他遗漏的那个数字。
“这条规则能否节省 token”,只有与 Agent 在没有它时会写出的代码相比才有意义。所以,我要做的第一件事,就是从全局配置中移除这段规则。一个已经携带这条规则的 Agent,无法告诉你没有它时自己会怎么做。
下面的每一组实验都通过 Claude Opus 5 的 claude --safe-mode 运行。在这种模式下,它不会加载任何 CLAUDE.md、skills、plugins 或 hooks。其中有两组实验故意没有使用这个参数,我会在它们出现时明确说明:这两组用来衡量我自己的配置会怎样影响结果。值得一提的是,即使移除了这条规则,我的全局配置中仍然有一行要求 Agent 编写能够完成任务的最少代码,还有一行要求优先使用 bun 而不是 node。
四项任务,分别对应规则中提到的四类脚本:整理日志、在一棵配置文件目录树中重命名某个 key、重新编号一堆截图,以及把一个 CSV 转换成另一个 CSV。我直接从他的列表中选择任务,而不是自己发明,这样我就无法悄悄挑选更适合 Ruby 的场景。
在看到任何数字之前,有三点实验设置值得说明。这三点都是我做出的选择,也都存在讨论空间:
没有任何一组实验直接粘贴规则原文。每组实验都直接指定一种语言和“只能使用标准库”的约束,因为这才是规则产生的实际效果,而不是规则本身的文字。
bash 组不能使用 jq,因为规则要求只使用标准库,而 jq 需要单独安装。
只有 Ruby 组被明确告知,可以使用它的一种简洁惯用写法。这确实是在给 Ruby 加码,不过在 Ruby 总计 1660 字节的代码中,它大约只影响了 20 字节。
在证明实现等价之前,我不会统计任何内容。每项任务的每个实现都会与一个参考实现进行对比,必须在 stdout 上输出完全相同的字节,并且对其留下的每一个文件产生完全相同的摘要。比较功能不同的程序长度毫无意义,因此必须先通过下面这道检查:
ruby src/run.rb text
t1_logs count requests and errors per path in an access log 53 impls identical
t2_config rename one key across a tree of .env files, decoy key left alone 53 impls identical
t3_rename renumber screenshot-N.png to zero-padded shot-NNNN.png 53 impls identical
t4_csv filter a CSV to rows with stock, compute totals, write a CSV 53 impls identical
EQUIVALENT: every implementation matches one reference (stdout + output tree)
十次运行,使用相同的四份任务说明,且任何地方都没有指定语言。其中五次在无配置干扰的环境中运行,另五次则在移除规则后、加载我的真实配置运行。
这份 prompt 并不是盲测,我应该说清楚。它要求 Agent 用一句话解释自己为何选择这门语言,这等于告诉 Agent:有人正在观察它的选择。下一步,我会再做一个盲测版本。
十次运行全部选择了 Python。
没有一次选择 Ruby、bash 或 JavaScript。Agent 给出的理由都与标准库有关:Python 几乎随处都预装了,而且无须依赖就能处理行终止符保留和定点小数格式化。因此,这条规则并不是让 Agent 在 Ruby 与某个抽象的候选集合之间做选择。它每次取代的都是 Python。
其中五次运行加载了我的配置,却依然选择了 Python。只有在能够证明配置确实加载的情况下,这件事才值得报告。事实也确实如此:直接询问处于这种状态的 session,它会向我复述 CLAUDE.md 中的内容;而且如下所示,同一套配置会让代码字节数最多变化 59%。配置已经被读取了。只不过在移除那条规则之后,它对于语言选择没有任何剩余意见。
于是,成本问题变得具体,而答案有着压倒性的差距:
Ruby 是四种语言中代码最短的;在无配置干扰的环境里,为实现相同且经过验证的行为,Agent 生成的 Python 源码长度超过 Ruby 的两倍。
这个数字取决于实验条件,而我自己的环境给出的结果就没这么漂亮了。加载我的真实配置重新运行后,Python 相对 Ruby 的长度优势从多出 120% 降到了多出 18%。这条规则能为你带来多少收益,取决于还有哪些因素正在塑造 Agent 编写代码的方式。
两行数据都是五次运行对一次运行,并使用相同方法测量。无配置干扰的那一行隔离出了规则本身的影响。如果你的 Agent 配置过任何内容,那么底部那一行才更接近你的实际处境,也是我在制定计划时会参考的数字。
计数器会忽略注释和空行,但保留 import,因为必须导入模块确实是这门语言的真实成本。它也保留 Python docstring;这一点对 Python 不利,因为如果 Agent 使用的是 # 注释,就不会被计入。这些 docstring 占无配置干扰环境下 Python 总量的 8%;移除它们之后,Python 不再比 Ruby 大 120%,而是大 101%。
而且,这个差距并不全是 Ruby 带来的。120% 中的大部分并非来自语言本身,而是 Agent 包裹在代码周围的东西:docstring、main()、if __name__ guard,以及行终止符处理。阻止这些习惯之后,Python 相对 Ruby 的差距会降到 18%,接近我手写两种版本时得到的 36%。
以其中一项任务——配置迁移——为例,下面是中性 prompt 生成的结果。先看 Ruby:
root = ARGV[0]
Dir.glob(File.join(root, "conf", "*.env")).sort.each do |path|
content = File.read(path)
renamed = 0
rewritten = content.lines.map do |line|
if line.start_with?("CACHE_TTL=")
renamed += 1
"TTL_SECONDS=" + line[("CACHE_TTL=".length)..]
else
line
end
end.join
File.write(path, rewritten) if renamed > 0
puts [File.basename(path), renamed].join("\t")
end
这就是完整程序。读一遍,你就知道它是正确的。
Python 对同一项任务的实现有 38 行。下面是它的中间部分,也就是实际执行工作的循环:
renamed = 0
lines = text.splitlines(keepends=True)
for i, line in enumerate(lines):
body = line.rstrip("\r\n")
ending = line[len(body):]
key, sep, value = body.partition("=")
if sep and key == OLD_KEY:
lines[i] = NEW_KEY + "=" + value + ending
renamed += 1
text 是文件内容;OLD_KEY 和 NEW_KEY 是两个 key 的名称,定义在文件顶部。这段循环与 Ruby 版本完成的是同一件事,只是额外加入了手动处理行终止符的逻辑,外面还包着一个带 docstring 的 main()、四个 import 和一个 if __name__ guard。这些都不是糟糕的 Python。只是代码更多,而统计结果也证明了这一点。
Ruby 赢下了两项与阅读成本相关的指标,但在最长行指标上输给了 bash。我是在运行实验之前就选定这四项指标的,并且无论结果如何都会全部报告,因为没有公认的方法可以衡量可读性,而事后才选择指标,衡量的其实是作者自己。
在没有特别要求的情况下,Agent 会编写谨慎的 bash:使用 shopt -s nullglob、显式排序、为每个输入创建临时文件,还会检查原文件是否以换行符结尾,以便在输出时恢复。这些代码总计 2333 字节,比 Ruby 长 41%。
于是,我第二次运行了中性 prompt,只改变了一件事:关闭 --safe-mode,让系统加载我的真实配置,而不是使用无配置干扰环境。prompt 相同、任务说明相同、fixture 也相同。
每种语言的代码都缩短了,胜者也发生了变化。在无配置干扰的环境中,Ruby 最短;加载我的配置后,bash 比 Ruby 短 31%,得出了那个整洁的“直接用 bash”结论——而这个结果完全是由我自己的环境产生的。
我无法把这种变化归因于某一句配置,也不应该尝试这么做。关闭 --safe-mode 会同时恢复我的 CLAUDE.md、skills、hooks 和 plugins,其中一个 plugin 注入了一种围绕“编写能工作的最短代码”构建的 persona。那一行数据最诚实的标签应该是“我平时运行的所有东西”。
两个数字都是真实的。它们回答的是不同的问题,而其中只有一个是我最初提出的问题。
如果一行配置就能让数字发生如此大的变化,那么能够有意促成这种变化的最便宜指令是什么?我做了一组阶梯实验:相同的任务说明、相同的无配置干扰环境,每个阶梯上的每种语言分别运行一个 Agent,唯一变化的是指令。
两个单词就能拿到总计 69 个百分点中的 64 个。之后增加的 34 个单词只换来了剩余 5 个百分点,而在每个单元格只运行一次的情况下,这完全落在噪声范围内:相邻阶梯中,每种语言的结果波动最高可达 11%,而两个单词的阶梯已经胜过了三个单词的阶梯。
单词本身远比单词数量重要。"Be terse." 和 "Golf it." 都只有两个单词,但 "Golf it." 生成的代码整体少了 39%,并且在四种语言中都更短。"Golf" 是一个专业术语,它会带入 "code golf" 这个词组蕴含的一切。"Terse" 只是在礼貌地提出要求。
如果你想让 Agent 编写紧凑的代码,整个 prompt 只需要一句 "Golf it."。
下面再次展示同一个配置迁移任务,这次是阶梯最底端的版本:
Dir[ARGV[0]+"/conf/*.env"].sort.each{n=0;File.write(it,File.read(it).gsub(/^CACHE_TTL=/){n+=1;"TTL_SECONDS="});puts [File.basename(it),n]*"\t"}
一行,144 字节,而之前是 409 字节;它也通过了同样的逐字节检查。
其中有三处需要解释:
it 是 Ruby 的隐式 block 参数,因此每个 it 都代表当前文件名。它需要 Ruby 3.4 或更高版本。在任何更早的版本中,这一行都会直接报错,而不只是难以阅读。
gsub block 会在统计匹配次数的同时返回替换内容,因此 n 会在构建新文本的过程中作为副作用递增。
[File.basename(it),n]*"\t" 表示使用制表符连接数组,因为对数组使用 * 就是 join。
即便这是我自己的语言,我也不得不看上两遍。
代码行数急剧减少,但每一行本身却变得更糟。Ruby 的最长行从 80 个字符增加到 151 个,JavaScript 从 73 个增加到 199 个;标点符号在非空白字符中的占比也从 24% 上升到 37%。你买到的并不是紧凑的代码,而是高密度代码;你付出的代价,恰恰是当初采用这条规则所看重的东西。
接下来,我让全部 152 个实现处理一个目录名中包含空格的路径,其中包括六个阶梯,以及作为对照组的手写实现。有一个阶梯失败了,而且正是最昂贵的那个:在 36 个单词的指令下编写的四个 bash 脚本全部失败,而更短阶梯中的所有 bash 脚本都正常运行。
awk '{e[$7]+=$9>=400;t[$7]++}END{for(p in t)print p"\t"e[p]"\t"t[p]}' $1/logs/access.log|LC_ALL=C sort -t$'\t' -k2,2nr -k1,1
这里的 $1 没有加引号。其中三个脚本仍然以状态码 0 退出,因为失败的 awk 通过管道传给 sort 后,最终报告的是 sort 的退出状态。这三个脚本中,有两个完全不输出内容,第三个输出 -1。错误确实会到达 stderr,因此盯着终端的人能够看到它。任何只检查退出状态的东西都看不到。
所以,指令中最后增加的 34 个单词换来了 5 个百分点的长度优势,却破坏了它们接触到的每一个 bash 脚本。"Golf it." 本身则没有造成这种问题。
这里的长度单位是字节。我没有对这些代码集运行 tokenizer,因此本文中的每个百分比都是字节百分比;它与 token 账单的接近程度,只是一个尚未经过验证的假设。
每个阶梯上的每种语言都只运行了一个 Agent。四种语言、六个阶梯呈现出的方向是一致的,但具体百分比并不可靠。
四项任务取自原文列出的四种类别,因此我无法挑选有利于自己的场景,但终究也只有四项。
十次运行足以说明这里的默认选择是 Python,却不足以量化它究竟有多大概率选择 Python。
无配置干扰环境移除的是配置,而不是模型本身的先验倾向。"No config" 是我能达到的最诚实下限,却不是一个中立宇宙;换一个模型,完全可能默认选择别的语言。
包含空格的路径故障,只是在一台机器上的一个棘手路径。它展示了这种故障模式,却不能给出可迁移到其他场景的故障率。
保留这条规则。仅从成本角度看,它也能胜出,尽管这并不是原作者提出它的理由:相较于你什么都不说时 Agent 会选择的 Python,在有配置的环境中,它能让源代码减少约三分之一;在裸环境中,则能减少一半。
不要改用“写得更简短”之类的指令。它们确实比这条规则减少了更多代码,但它们买到的是密度,而不是简洁:在一门并非由你选择的语言中,得到更少、却无人能读懂的代码。沿着这条阶梯推得足够远之后,四个 bash 脚本都因为路径中存在空格而失败了,尽管 "Golf it." 本身并没有导致失败。
有三种情况应该覆盖这条规则。第一,所需的库只存在于其他语言中——这也是 Ghinda 自己提出的例外。第二,脚本必须在没有安装 Ruby 的环境中运行——基线实验中的 Agent 之所以选择 Python,正是因为它几乎随处都预装了。第三,任务确实只需要一条诚实的 shell 命令,而规则本身已经对此做出了豁免。
Ghinda 自己给出的理由依然更好,而且本文中的任何表格都无法对此做出定论。可审查性取决于阅读代码的人。对于一个完全看不懂 Ruby 的人来说,上述数字依然成立,但对他而言,上面的每一行数据都失去了意义。
还有一点不只适用于 Ruby:仅仅取决于是否加载了我自己的配置,本文中的每个数字都会发生变化,其中两个结果甚至出现了逆转。如果你要对 Agent 进行 benchmark,请先移除自己的配置,否则你测量的其实是配置。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。