我們曾尋找 Claude Code 中能設定工具呼叫最小間隔的選項,但並未找到。因此,在我們的一個腳本導致帳戶被停權後,我們利用 PreToolUse/PostToolUse 鉤子實作了這個間隔機制。現在,代理程式對任何網站進行的瀏覽器工具呼叫,都必須等待該網站前一次請求完成後至少 10 秒才能執行,無論是哪個會話或腳本發出的請求。
在手動測試中,這個間隔精準地控制在 10,000 毫秒。我們很想知道大家是如何控制代理程式的工具呼叫速率的。
我們的 Claude Code 代理程式曾因對第三方網站發出過多請求而導致帳戶被停權。為了解決此問題,我們開發了一組 PreToolUse/PostToolUse 鉤子,強制代理程式在每次瀏覽器呼叫同一網站之間等待至少 10 秒。這項機制確保了即使有多個代理同時運作,也能有效管理請求速率,避免觸發網站的速率限制。

我們曾尋找 Claude Code 中能設定工具呼叫最小間隔的選項,但並未找到。因此,在我們的一個腳本導致帳戶被停權後,我們利用 PreToolUse/PostToolUse 鉤子實作了這個間隔機制。現在,代理程式對任何網站進行的瀏覽器工具呼叫,都必須等待該網站前一次請求完成後至少 10 秒才能執行,無論是哪個會話或腳本發出的請求。
在手動測試中,這個間隔精準地控制在 10,000 毫秒。我們很想知道大家是如何控制代理程式的工具呼叫速率的。
我們的錯誤
我們的 Claude Code 代理程式每天透過 API 和實際的 Chrome 視窗,在幾個外部網站上執行任務。在 2026 年 10 月 6 日 17:02 UTC,它執行了一個在第三方社群網站上追蹤帳戶的腳本。該腳本在 67 秒內發送了 103 個追蹤請求。
在最後一個請求發出後的六分鐘內,該網站就停權了我們的帳戶。我們無法得知該網站的系統判斷標準,但我們知道問題出在我們這邊:該腳本的每日追蹤上限是 150 個,但請求之間完全沒有任何間隔。每日上限並非速率限制,而我們卻將其視為速率限制,這是我們的錯誤,而非網站的問題。
在帳戶停權後的一小時內,代理程式就為該網站的追蹤和按讚操作增加了間隔。大約八小時後,負責人將這項規則擴展到代理程式接觸的所有網站:現在,對同一網站的每個請求(包括讀取操作)都必須在前一個請求完成後至少 10 秒才能開始。
兩種呼叫者
我們的腳本是比較容易處理的部分。每個網站的 API 用戶端都透過一個封裝過的 fetch 函數進行呼叫,如果原始碼檔案直接呼叫 fetch(),測試就會失敗,除非該檔案在一個我們確認不會與這些網站通訊的白名單上。
瀏覽器部分則比較困難。當代理程式透過 claude-in-chrome MCP 伺服器驅動 Chrome 時,每個導航、點擊和腳本都是 Claude Code 為模型執行的工具呼叫,而請求是來自 Chrome 本身。封裝過的 fetch 函數無法偵測到這些請求。
此外,單一程序內的休眠機制也無法滿足需求。在繁忙的日子裡,同一台機器上可能會開啟多個 Claude Code 會話,其中一些會啟動子代理,並且一個腳本可能同時呼叫同一個網站的 API。每個會話都會認為自己是唯一的呼叫者。
因此,狀態被儲存在磁碟上。在 ~/.cache 目錄下,每個網站都有一個鎖定目錄和一個小型 JSON 檔案,該 JSON 檔案記錄了對該網站的最後一次請求完成時間。機器上任何想要與網站通訊的程序,都會先取得該網站的鎖定,等待自上次請求完成時間起 10 秒過去後,再發出請求,寫入新的完成時間,然後釋放鎖定。
等待時間由一個小型函數計算:如果 lastFinishedAtMs 為空,則返回 0;否則,它會計算 lastFinishedAtMs 加上 minIntervalMs 再減去 nowMs 的最大值(與 0 比較),以確保等待時間不為負數。
鎖定機制是透過建立目錄來實現的,因為 mkdir 要麼成功建立目錄,要麼因 EEXIST 錯誤而失敗,確保只有一個程序能成功取得鎖定。這個 acquireLock 函數會不斷嘗試建立鎖定目錄,如果失敗且錯誤碼不是 EEXIST,則拋出錯誤;如果是 EEXIST,則嘗試移除過期的鎖定,然後等待一段時間後重試。
如果鎖定目錄是由一個已崩潰的程序遺留下來的,那麼一旦該程序消失,或者鎖定時間超過 15 分鐘,它就會被移除。
鉤子配對
以下是 .claude/settings.json 中用於控制速率的鉤子設定,已精簡至只顯示該鉤子。同一個 PreToolUse 群組也執行了第二個不相關的鉤子。這個設定定義了 PreToolUse 和 PostToolUse 兩個階段,它們都使用 mcp__claude-in-chrome__.* 作為匹配器,並執行一個 Bash 命令腳本來處理瀏覽器通道操作,逾時時間設定為 300 秒。
其中 .* 的使用至關重要。Claude Code 將 MCP 工具命名為 mcp__<server>__<tool> 的格式,而如果匹配器只由字母、數字、底線和連字號組成,它會被視為精確字串比對,因此單獨的 mcp__claude-in-chrome 將無法匹配任何工具。
這個 shell 腳本只會切換到專案目錄,並將鉤子的 JSON 透過標準輸入傳遞給一個 TypeScript 命令。在 PreToolUse 階段,該命令會將每個呼叫分為三組。
第一組是無需計數即可放行的呼叫。例如,截圖、讀取頁面文字、尋找元素和列出分頁等操作,都只是讀取瀏覽器已有的內容,因此鉤子不會對它們進行計數。這個列表是我們判斷哪些工具不會向網站發出請求的依據。
第二組是被拒絕的呼叫。browser_batch 在一個工具呼叫中執行多個動作,因此無法在它們之間強制執行間隔。而一個 javascript_tool 呼叫,如果其程式碼包含兩個或更多 fetch() 呼叫,則會連續發出兩個請求。
對於這兩種情況,鉤子會將原因寫入標準錯誤輸出並以結束碼 2 退出。在 PreToolUse 階段,結束碼 2 會阻擋工具呼叫,Claude 會將標準錯誤輸出的文字視為原因。我們的要求是每次呼叫只執行一個動作。
fetch 呼叫的計數是透過對程式碼執行正規表達式來完成的,它會計算程式碼中 fetch( 出現的次數。
其餘的呼叫則會依序等待。鉤子會判斷該呼叫涉及哪個網站:如果是導航操作,則從 URL 判斷;否則從分頁判斷。claude-in-chrome 會在其工具結果的末尾列出所有開啟的分頁及其 URL,而 PostToolUse 階段會將此列表儲存為從分頁 ID 到網站的映射。如果鉤子無法識別分頁,它會將該呼叫計入所有網站,因為等待時間過長總比遺漏一個網站的限制要好。
接著,它會取得該呼叫所涉及的每個網站的鎖定,然後等待,並將該網站標記為忙碌狀態(我們的理由字串是日文,因此使用 reasonJa)。我們設定了一個 BROWSER_OPERATION_IN_FLIGHT_HOLD_MS 常數為 120,000 毫秒(即 120 秒)。
handlePreToolUse 函數會根據工具名稱、輸入和分頁通道來決定瀏覽器工具的速率控制策略。如果決定是「阻擋」,它會將日文理由寫入標準錯誤輸出並返回 2;如果決定是「跳過」,則返回 0。否則,它會為每個相關通道啟動外部操作,並將其標記為忙碌狀態,同時儲存這些標記資訊供 PostToolUse 階段使用,最後返回 0。
「忙碌」狀態意味著儲存的完成時間被設定為呼叫開始後 120 秒。當工具返回時,PostToolUse 鉤子會用實際的完成時間覆寫它,下一個呼叫的 10 秒間隔將從那裡開始計算。這樣一來,間隔是從一個呼叫的結束到下一個呼叫的開始,緩慢的頁面載入不會縮短等待時間。
這 120 秒是為失敗的呼叫而設計的。PostToolUse 在工具成功完成後運行;失敗的呼叫會觸發 PostToolUseFailure,而我們沒有為這個事件設定鉤子。如果沒有這個保持時間,下一個呼叫將只會從失敗呼叫的開始時間等待 10 秒,即使瀏覽器仍然忙碌。
有了這個保持時間,該網站將從失敗呼叫的開始時間起關閉 130 秒。如果鉤子命令本身崩潰,它也會以結束碼 2 退出。在 PreToolUse 階段,這會阻擋呼叫。我們選擇了「故障關閉」策略:一個損壞的速率控制器會停止代理程式,而不是讓呼叫在沒有控制的情況下通過。
我們測量到的結果
我們透過手動將鉤子事件透過標準輸入傳遞給腳本來測試這個鉤子,每個事件都由一個新的程序處理,就像 Claude Code 餵送命令鉤子一樣:一個網站導航的 PreToolUse 和 PostToolUse 配對,然後是該分頁上的左鍵點擊,接著是截圖,最後是 browser_batch。一個小型腳本將閘道的日誌轉換為間隔時間:
導航在 01:18:18.058 UTC 結束,點擊在 01:18:28.058 開始,間隔為 10,000 毫秒。截圖操作無需等待即可通過,而 browser_batch 則因其原因以結束碼 2 退出。
幾分鐘前的第一次嘗試讓我們學到了更多。我們手寫的導航 PostToolUse 事件在 JSON 字串中包含了一個原始換行符,導致鉤子無法解析並以結束碼 2 退出。導航的結束時間從未被記錄,鉤子也從未得知該分頁屬於哪個網站。隨後的點擊等待了 129 秒,並被計入閘道所知道的所有四個網站。
這 129 秒是 120 秒的保持時間加上 10 秒,從導航開始時測量。這正是我們在呼叫失敗後所期望的行為。這也顯示了損壞的 PostToolUse 步驟會是什麼樣子,這很有用:損壞的步驟在日誌中顯示為長時間等待,而不是呼叫未受控制地通過。
我們的腳本也透過相同的鎖定目錄運行,因此閘道的日誌也顯示了實際的流量。在 2026 年 10 月 7 日,一個程序對某網站的最後一次請求在 01:50:32.823 UTC 完成,而來自不同程序的對該網站的下一個請求在 01:50:42.824 開始,間隔為 10,001 毫秒。另一個在不同網站上兩個程序之間的交接,間隔為 10,135 毫秒。
這項機制無法做到的事
它僅限於單一機器。鎖定機制是基於本機磁碟上的目錄。CI 執行器上的排程任務會與同一任務中的其他程序共享鎖定,但不會與在筆記型電腦上同時呼叫同一網站的會話共享。
它控制的是我們的動作,而非頁面本身的流量。一次點擊可能會導致頁面發出許多自己的請求。fetch 計數器將程式碼視為文字。迴圈內的一個 fetch() 呼叫只會被計數一次,window.fetch() 因為前面的點號而未被計數,並且沒有考慮 XMLHttpRequest。它只能捕捉到粗心大意的案例,而非有意的規避。
它可能會逾時而開放。如果 PreToolUse 命令鉤子運行超過其逾時時間,Claude Code 會取消它,並且工具呼叫會繼續。我們最長的預計等待時間是 130 秒的保持,遠低於鉤子 300 秒的逾時時間,但理論上,如果一個網站有足夠長的會話隊列等待,仍可能超過這個限制。
10 秒是我們設定的數字。這是負責人在帳戶停權後選擇的。這個數字並非來自任何網站的文件,我們也不知道它是否比實際所需更慢,或者對某些網站來說仍然太快。
我們想知道什麼
你們是否會控制代理程式的工具呼叫速率,還是依賴網站本身的速率限制,並在收到 429 錯誤時才退避?如果你們有控制速率,間隔時間是多少?是針對每個網站、每個工具,還是採用一個全域間隔?是從結束到開始(finish-to-start)還是從開始到開始(start-to-start)?我們選擇了從結束到開始,這樣緩慢的呼叫就不會縮短間隔,但代價是犧牲了吞吐量。
你們的速率控制機制位於何處:是鉤子、MCP 伺服器的包裝器、瀏覽器前的代理,還是提示詞中的指令?你們的代理程式是否曾因速度過快而被標記帳戶,之後又做了哪些改變?我們很想在下面的評論中讀到你們如何控制代理程式的工具呼叫,或者為什麼你們認為不需要控制。我們會在評論中一一回覆。
覺得值得一看?分享給朋友。
以上為 AI 自動翻譯導讀。原文版權歸 DEV Community (Claude Code) 所有, 建議透過「閱讀原文」前往原始網站,以取得最完整資訊並支持原作者。
43 個來源、每 2 小時更新,太多看不完。電子報每週挑最值得看的幾則,附上 Max 的看法。