LLM 智能体 EDIT 工具的替代方案
EDIT:当然,这件事过去早就有人做过了!我本来就没怎么怀疑,Twitter 上的人也向我确认了这一点 :) 不过,请继续往下看:文末提到的 CRC32 折衷方案是一个有趣的权衡,这个讨论本身也很有价值。
眼下我正在为自己的 DS4 项目开发一个智能体。本地推理的 token 非常紧缺,这是一场分毫必争的战场。让我颇感意外的是,现在大家都在用的 EDIT 工具会强制 LLM 逐字原样输出旧版文本。这种 CAS(检查并设置)模式的操作方式,也就是我说 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;"
}这种方式节省的开销非常可观,尤其是在智能体需要删除大量文本时,在一般情况下也是如此。然而,由于我们引入了行号和标签,也会带来一些额外开销。这里存在潜在的权衡,也许标签应该是 8 个字符,并且把行号也包含到哈希中,需要仔细检查碰撞可能性和分词情况,才能看清到底能带来多少收益,但我喜欢 line:tag 这种格式,因为 LLM 之后往往能在很多场景下利用行号信息,比如在后续的工具调用中获取范围。也许还有其他利用标签的方式,比如:这一行还是 dj4_ 吗?
有意思的是,DeepSeek v4 Flash 能够非常高效地使用这个工具,看起来对它来说很自然。虽然我没有精确测量节省了多少,但在实际使用中我看到编辑速度快了很多,甚至也更可靠了。
另一种替代方案是每次只返回整个文件的 CRC32(基本上标签就变成了文件标签,即使是部分读取也是如此)。这样我们就只需要处理行号 + CRC,编辑时只需指定 11,12,13,14。当然,这样 token 更少。然而,这会迫使每次都要重新计算文件的 CRC32,但对于大小合理的文件来说,这个开销足够小。不过这种方法也有局限:即使发生了*无关的*改动,编辑也会失败,因此这里存在很强的权衡。公平地说,整文件方法也有其优点,它允许像 10:23 这样指定范围,这是一个巨大的优势。
我感觉只有通过在多个会话中使用 ds4-agent 对比这两套系统,积累足够的实践证据,才能判断哪一种更好。就目前而言,也许先提供一个用于切换编辑模式的命令行选项是正确的第一步。
随机一篇博客