Alternatives for the EDIT tool of LLM agents

Salvatore Sanfilippo

LLM 智能体 EDIT 工具的替代方案

原文由 Salvatore Sanfilippo 发布,订阅该博客

补充:当然,这件事过去早就有人做过了!我本来就没怎么怀疑,推特上也有人向我确认了这一点 :) 不过,还是请继续往下看:文末提到的 CRC32 折中方案是一个很有意思的权衡,而且总体来说这也是一个值得讨论的话题。

我现在正在为我的 DS4 项目开发一个智能体。本地推理的 token 资源非常紧张,每一点优化都至关重要,堪称寸土必争的战场。让我颇感意外的是,现在大家都在用的 EDIT 工具竟然要求 LLM 逐字原样输出旧版文本。这种 CAS(Check and Set)式的运作模式,也就是 EDIT old="foo" new="bar" 这样的形式,之所以必要,是因为经常会出现编辑冲突(比如用户也在同时编辑,或是切换到了不同的分支等等),而且 LLM 本身也可能会幻觉出某一行的内容。

说到底,这意味着光靠行号是非常脆弱的:比如说,用 new="foobar" 去修改第 22 行,这种方式并不可靠。但我也不想让本地 LLM 每次都把 token 浪费在重写旧文本上,尤其是有些旧文本包含大量特殊字符和空格,模型很容易写错;一旦写错,工具就会执行失败,迫使 LLM 不得不重做一次同样的编辑。所以我(重新)设计了一个基于标签的 EDIT 工具,它依然是 CAS 风格,但在 token 效率上更高。

READ 和 SEARCH 工具返回的内容大致如下:

10:Q8fA int count = 10;
11:rA3_ if (count > limit) {
12:Kq9z     count = limit;
13:PX0b }

可以看到,这里既有行号也有标签。标签是 4 个字符,平均约占 2.5 个 LLM token,代表了该行的校验和。现在 LLM 可以这样进行编辑:

{
  "tool": "edit",
  "path": "/tmp/example.c",
  "line": 10,
  "tag": "Q8fA",
  "new": "int count = 11;"
}

或者,像这样进行多行编辑:

{
  "tool": "edit",
  "path": "/tmp/example.c",
  "lines": "11:rA3_\n12:Kq9z\n13:PX0b",
  "new": "if (count > limit)\n    return limit;"
}

这种方式节省的 token 非常可观,尤其是在智能体需要删除大量文本时,即使在一般情况下也能省下不少。不过,由于引入了行号和标签,也会带来一些额外开销。这里面存在一些可能的权衡,比如标签是否应该用 8 个字符、是否应该把行号也纳入哈希计算,还需要具体考察冲突概率和分词情况,才能确定这种方案到底能带来多大的收益,不过我个人很喜欢 line:tag 这种格式,因为 LLM 后续往往能以多种方式利用行号信息,比如在接下来的工具调用中获取某个范围。也许标签还有其他利用方式,比如:这一行是否还是 dj4_?

有意思的是,DeepSeek v4 Flash 能够非常高效地使用这个工具,看来对它而言这种方式是很自然的。虽然我没有精确测量节省了多少,但在实际使用中我能看到,编辑速度快了很多,而且也更可靠了。

另一种替代方案是,每次只返回整个文件的 CRC32(基本上标签就变成了文件标签,即使是部分读取也是如此)。这样我们就只需要处理行号 + CRC,编辑时只需指定 11,12,13,14。当然,这样 token 更少。不过,这也迫使我们每次都要重新计算文件的 CRC32,但对于大小合理的文件来说,这个开销完全可以接受。但这种方法也有局限:如果发生了*不相关*的改动,编辑同样会失败,所以这里存在一个很强的权衡。公平地说,整文件方案的好处在于它可以直接指定 10:23 这样的范围,这是一个巨大的优势。

我感觉,只有通过在多个会话中让 ds4-agent 分别使用这两种方案,才能积累足够的实践证据来判断哪种更好。就目前而言,也许先加一个可以切换编辑模式的命令行选项,才是正确的第一步。

本文章由 muse-spark-1.2-contributor 进行翻译

评论