LLM 代理的 EDIT 工具替代方案
補充:當然,這早就有人做過了!我本來就沒什麼懷疑,只是在 Twitter 上有人幫我確認了這一點 :) 不過,請繼續往下看:文末的 CRC32 折衷方案是個有趣的取捨,整體而言,這也是個值得討論的好議題。
目前我正在為我的 DS4 專案打造一個代理。本地推論的 token 非常吃緊,這是一個每個最佳化都至關重要的戰場。讓我相當驚訝的是,現在大家都在用的 EDIT 工具竟然會強迫 LLM 一字不漏地逐字輸出舊版文字。這種 CAS(檢查並設定) 運作模式,也就是以 EDIT old="foo" new="bar" 的方式進行,之所以必要,是因為經常會發生編輯衝突(例如使用者同時也在編輯,或是切換到不同的分支等等),而且也因為 LLM 可能會憑空幻覺出某一行的內容。
基本上,這意味著光靠行號是非常脆弱的:舉例來說,指定將第 22 行改成 new="foobar" 並不是個好做法。然而,我也不想讓本地的 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 個字元並把行號也納入 hash(雜湊) 的計算,還需要精確檢視碰撞可能性與 tokenization(斷詞) 的情況,才能看出究竟能省多少,但我喜歡 line:tag 這種格式,因為 LLM 之後往往能以多種方式利用行號資訊,例如在接續的工具呼叫中取得範圍。也許還有其他利用標籤的方式,例如:這一行是否還是 dj4_?
有趣的是,DeepSeek v4 Flash 能夠非常有效地使用這個工具,所以看起來對它而言這種方式相當自然。而且,雖然我沒有精確測量節省的幅度,但在實際使用中我發現編輯速度快得多,甚至也更可靠。
另一個替代方案是每次只回傳整個檔案的 CRC32(基本上標籤就變成了檔案標籤,即使是部分讀取也一樣)。這樣我們就只需要用行號加上 CRC 來操作,編輯時只需指定 11,12,13,14。當然,這會用到更少的 token。然而,這會迫使我們每次都重新計算檔案的 CRC32,不過對於大小合理的檔案來說,這個成本還算便宜。但這種做法也有其限制:即使發生的是*不相關*的變更,我們也會讓編輯失敗,因此這裡存在著強烈的取捨。平心而論,整個檔案的方法有個好處是,它允許指定像 10:23 這樣的範圍,這是一個巨大的優勢。
我覺得只有透過在多個工作階段中使用 ds4-agent 實際測試這兩種系統、累積足夠的實務經驗後,才能判斷哪一種更好。目前來說,也許先提供一個可用來切換編輯模式的命令列選項,會是正確的第一步。
隨機一篇部落格