跳到內容

工具情境與提示快取

內建執行環境所發出的每一個請求都有兩件事成立:工具定義是其中最大的一筆固定成本,而它們幾乎都不會被用到。本頁說明 Control Center 如何處理這件事,以及為何答案不是單純「少送一些工具」。

Control Center 註冊了大約 126 個工具。若完整送出,這些定義每次請求大約要花費 23,000 個 token — 這是任何壓縮都無法觸及的一筆成本,因為工具定義與對話位於請求中彼此獨立的部分。

token 帳單是最顯而易見的抱怨,而有了提示快取之後,它也是較小的一項。真正的成本在於選擇準確度。已發表的工具呼叫評測一致顯示,當情境中的工具數量約在 30–50 個以內時,模型能可靠地選對工具,超過這個範圍之後便會逐步變差。公認的原因並非 token 數量,而是語意重疊:一旦工具目錄中存在多個描述讀起來雷同的工具,模型就是在相似者之間做選擇,而不是在回想某項能力。

在 126 個工具的情況下,代理在每一個回合都遠遠超出這個區間 — 包括許多它只需要讀取一個檔案的回合。

因此,一次執行會看到兩類內容:

  • 一個常駐集合 — 大約二十來個工具,每次請求都連同完整結構描述一併送出;
  • 其餘所有工具的一份名稱索引,按「Tickets and projects」或「Memory and knowledge」之類的標題分組,整個目錄只花費數百個 token。

以目前的目錄實測,這讓工具區塊從 ~23.1k token 降到 ~5.0k,縮小約 78%,並讓永遠可見的集合落在選擇可靠的區間之內。

常駐集合是一次執行在大多數情況下都會用到的工具:檔案與 shell 詞彙(read、write、edit、search、bash…)、探索工具、任務清單、記憶搜尋,以及目前模式為交出結果而必須呼叫的動詞。模式自身的輸出動詞一律常駐 — 一次執行絕不應該為了找出那個產生其交付成果的呼叫而四處尋找。

這正是讓這個設計安全、而不只是便宜的部分。**隨需載入的工具從第一回合起就可以呼叫。**被扣住的只是它的結構描述。

代理會在提示中看到這個名稱。如果它直接呼叫該名稱,執行環境會在同一步內載入結構描述並執行該工具 — 不會出錯,也不需要往返一趟。模型無需知道這個工具曾經被「卸載」過。

當代理知道任務卻不知道名稱時,search_tools 會接受一段自然語言描述(「assign a ticket to someone」),對本次執行的整個工具介面排序,並回傳相符的項目,且已載入它們的結構描述,這樣下一個回合就能直接呼叫其中一個。如果搜尋一無所獲,它會如實說明,並建議換個說法或擴大搜尋範圍 — 這是刻意為之,因為把空搜尋結果理解成「這項能力不存在」的代理,會自己編造一個替代品,而不是再找一次。

search_tools 只存在於內建執行環境中。外部 MCP 用戶端改用 search_tool_bm25,它對同一個索引排序,但只描述相符的項目 — 這些用戶端已經從 tools/list 收到所有結構描述,因此沒有東西需要載入。

隨需載入是在模式的工具篩選之後套用的,絕不會取代它。它會隱藏結構描述,卻無法顯露模式已經拒絕的工具。被啟用的工具仍會像常駐工具一樣,通過核准關卡與動作護欄,而且工具無法載入一個打從一開始就不在本次執行工具介面上的名稱。

外部用戶端看到的 MCP 工具介面不受影響。tools/list 仍會回傳完整目錄。早先一次試圖裁剪它的嘗試直接弄壞了代理:MCP 用戶端會拿工具名稱與自己快取的清單核對,對未列出的名稱直接拒絕,從不詢問伺服器,所以「隱藏但可呼叫」的工具根本無法觸達。隨需載入存在於執行環境之中,因為在執行途中載入工具,是執行環境確實做得到的事。

--tool-deferral=off(或 CC_SERVER_TOOL_DEFERRAL=off)會讓每個被准入的工具重新常駐,產生與隨需載入出現之前伺服器所發出的完全相同的請求。參閱 cc_server CLI 參考。

代理的工作負載是偏斜的:它們一個回合接一個回合地送出龐大的提示、收到很小的回答,而且每次前綴都相同。供應商會快取這個前綴,而一次快取讀取的代價大約是新算一次的十分之一。因此,快取命中率是執行環境的首要特性,而不是一項最佳化 — 在 Anthropic 上它還能換來吞吐量,因為已快取的 token 不計入每分鐘的輸入上限。

快取的依據是精確的前綴比對,由此得出一條支配性規則。

任何有變動的位元組,都會使快取從該處起失效。因此執行環境只會追加:新訊息加在末端,而新啟用的工具結構描述是追加在已快取的工具區塊之後,而不是合併進去。正是這個順序讓啟用變得便宜 — 已快取的前綴保持完全一致,這次執行只需為剛剛載入的結構描述付費。

這個迴圈也會裁剪較早回合中過期的工具輸出,以回收情境。它會等到可回收的量足夠多、值得重寫時才動手,因此裁剪是一次罕見的深度重寫,而不是持續不斷的快取未命中。

供應商允許的快取標記數量有限,Control Center 會有意識地把它們花在刀口上:

標記 涵蓋範圍 存活期
最後一個常駐工具 工具區塊 1 小時
最後一個系統區塊 工具 + 系統提示 1 小時
上一回合的尾端 截至上一次請求的歷史紀錄 5 分鐘
目前回合的尾端 完整歷史紀錄 5 分鐘

最後兩個是一組滾動配對,而它們成對出現的理由很細微:快取查詢會往回搜尋有限數量的區塊,尋找較早某個請求所存入的內容。一個並行執行許多工具的單一回合,可能產生比這個窗口更多的區塊,此時其中一個尾端標記就會無聲地未命中,請求便得付全額費用,而回應中沒有任何資訊說明這件事。標記出上一次請求停止的確切位置,能讓命中成為結構性的,而不是一場競速。

工具加系統的那段前綴採用較長的存活期,因為它在整次執行中都是恆定的、會被每一次形態相同的執行與子代理共用,也因為計時是從請求開始的那一刻起算 — 一個持續串流數分鐘的回合,在下一次請求形成之前就已經耗掉了短窗口的大部分時間。

同一類型的子代理會產生逐位元組完全相同的前綴,因此它們應該讀取同一個快取項目,而不是各自寫入自己的。但一個項目要等到寫入它的那個請求開始回應之後才可讀取,所以一次過全部啟動的扇出,會讓每個子代理都為它們共同分享的前綴付上額外代價。

執行環境會讓某個形態的第一個子代理先啟動,然後在它開始串流的那一刻放行它的同伴。這個等待是有界的:第一個子代理慢,代價只是一次快取命中,絕不會賠上整個扇出。基於同樣理由,每個子代理無論其設定檔為何,都會拿到同一組常駐工具 — 按設定檔分別調整只會無謂地切碎共用的前綴,因為設定檔早已決定了子代理可以呼叫什麼。

這兩半都是可觀察的,而不是靠假設:

  • 對話上的情境瀏覽器會報告常駐工具、隨需載入索引,以及每個被扣住的結構描述原本會花費多少;這些都由真實執行所用的同一條程式碼路徑產生。
  • 每次執行都會記錄它載入了哪些隨需載入工具,以及是什麼觸發了載入,因此一次一無所獲的搜尋會以檢索未命中的形式顯現,而不會消失在一個悄悄去做了鄰近事情的代理身上。
  • 可觀測性會把快取率報告為快取讀取量對所處理全部提示 token 的比例,其中包含快取寫入 — 一個不斷重建自身前綴的執行會得到較低的分數,而不會像從不需要快取那樣得分。