· 閱讀時間 9 分鐘

你的智慧體不需要把每一個工具都放進上下文裡

DynoTable 的 AI 智慧體能夠觸及 38 個工具。它很少一次看到全部。我們在「模型立刻拿到什麼」和「它得自己去找什麼」之間劃下的那條線,最終成了整個工具集裡最有分量的一個決定 —— 而這條線的落點,和我們最初畫的地方相去甚遠。

我們構建了一套發現機制,好讓模型從一個小集合出發,再去搜尋其餘的部分。然後我們看著廉價模型使用它,於是把 38 個工具中的 27 個搬回了始終可見的核心。機制留了下來。我們關於誰需要它的理論沒有。

以下就是我們為那些不能指望它們主動去找的模型構建工具介面時學到的東西。

一個模型從不呼叫的工具,照樣讓你付出代價

你暴露的每一個工具,都是它的名字、它的描述和它完整的輸入 schema,在使用者敲下任何字之前就已經序列化進了請求。38 個這樣的東西並不免費。

token 只是賬單裡較小的那一半。真正的代價是選擇準確率:一個模型掃過的近乎雷同的選項越多,它挑錯的次數就越多。而我們的目錄裡滿是近乎雷同的選項 —— 這是故意的。我們有五個工具存在兩份 —— openTableproposeOpenTableopenWorkbenchproposeOpenWorkbench,等等。每一對做的是同一件事;一個立刻就做,另一個則先發出一個 chip,由使用者點選。這個區分對安全性是承重的,而在一份扁平的名字清單裡幾乎看不見。

自己的用戶端指南開篇講的就是這一點:預先載入每一個工具定義會浪費 token、增加延遲,並讓模型的表現變差。同意這一點很容易。決定哪些工具失去座位,才是有意思的地方。

我們造了一個搜尋工具。下限模型不肯呼叫它。

這套機制分兩層。一組工具從第一步起就是活躍的。其餘的在模型呼叫 searchTools(query) 之前都是不可見的 —— 該呼叫會按名字、描述和關鍵詞給目錄打分,返回匹配項,並把它們加入模型在後續步驟中被允許呼叫的工具集合。

工具目录智能体循环模型工具目录智能体循环模型第 1 步 —— 活跃集合 = 内联核心第 2 步 —— 活跃集合已扩大searchTools("export csv")按名字 + 关键词打分startExport, getExportStatus,listActiveExports匹配项(这些名字现在可调用)startExport({tabId})

然後我們拿它去跑我們的下限模型。我們不會對著一個前沿模型去調這個智慧體 —— 它跑在你自己的 Bedrock 憑證上,所以人們會挑廉價模型,而我們為最便宜的那一個做最佳化。被問到一個附件檔案時,那個模型跑去翻找開啟的標籤頁清單了。它幾乎根本不呼叫那個搜尋工具。任何不直接可見的東西,對它來說就不存在。

這個結果宣判了那個顯而易見的設計。如果發現是通往某個工具的唯一路徑,那麼每一個需要該工具的請求,都取決於模型是否選擇去找 —— 而最可能需要幫助的模型,恰恰最不可能開口去問。

於是這條分割線不再是「小核心、大長尾」,而變成了一個關於請求、而非關於工具的問題:使用者的措辭會點出這個工具嗎? 我們保留為可發現的那 11 個工具,就是答案為「是」的那些。「把這個匯出成 CSV」會讓模型去搜尋 export。「給我看上個月的訂單」不會讓它去搜尋一個設定過濾條件的工具,所以那個工具留在內聯裡。索引統計、已儲存的規格、關係自省,以及暫存改動的那些介面,全都是使用者想要時會指名索取、而絕不會隱式觸發的東西。

27 個內聯不是我們事先會去辯護的數字。它是在與我們真正交付時所對標的那個模型交手之後,活下來的那個數字。

那個本會讓發現機制悄悄失效的競態

發現機制有一個時序約束,很容易搞錯,又很難被注意到。

當搜尋工具返回匹配項時,那些名字必須_在_模型的下一步被準備好_之前_加入允許集合。最顯而易見的落點,是那個「一步完成時觸發」的鉤子。而文件寫明,在某些 SDK 版本里,這個鉤子下一步的準備工作之後才觸發 —— 也就是說這次變更晚了整整一步。

失敗模式很難纏。模型搜尋。它拿到一個正確的結果,裡面點名了它需要的工具。它在緊接著的下一步呼叫那個工具,卻被告知這個工具不存在。它時有時無,取決於你解析到的是哪個 SDK 版本,而且它讀起來像是模型太蠢,而不是承載它的那層框架壞了。

修復辦法是在搜尋工具自己的執行過程內部去改動這個允許集合 —— 這一步保證會在迴圈推進之前完成。這只是一行語句放在哪裡的差別,而它就是「一套能用的發現機制」和「一套有一定機率失敗、且沒人會把原因歸對的機制」之間的差別。

搜尋三次,然後停下

搜尋被限制在每個智慧體回合 3 次呼叫。第四次不會執行,而是返回這個:

{"error": "search-budget-exhausted", "budgetCap": 3}

這個上限的存在,是因為一個具體的迴圈:模型搜尋,沒找到它想象中的東西,換個同義詞再搜,還是沒找到,然後在一次都沒碰過資料庫的情況下,把整個步驟預算燒在了搜尋工具裡。設上限會逼出一個決定 —— 要麼就用已經找到的某個工具,要麼去問使用者 —— 而這個決定恰好落在繼續搜尋已經不再划算的那一刻。

當模型呼叫一個它尚未發現的工具時,錯誤訊息遵循的,是我們對智慧體裡每一個校驗器都採用的同一條原則:

Tool 'startExport' not in active set. Call searchTools(query='startExport')
to discover it, or use one of: <inline tool names>

一條點出了恢復動作的拒絕,代價是多走一步。一條只會說「不行」的拒絕,代價是整個回合。

每個工具一行,其餘一切都由此派生

每個工具只宣告一次,在一份扁平的清單裡,而這一行承載著該工具的完整身份:它的名字和描述、搜尋所匹配的關鍵詞、它是內聯起步還是可發現、它跑在哪一層,以及它如何透過 MCP 對外暴露。

這些層級和可見性的劃分同樣重要。21 個工具是靜默的 —— 不打擾任何人就跑完的讀操作。16 個是受門控的,擋在授權階梯後面。恰好有一個兩邊都不屬於,因為搜尋工具並不是智慧體作用在你資料上的一項能力;它是迴圈本身的一部分。MCP 暴露是同一行上的第三個維度:唯讀、暫存、完全,或者乾脆排除在外 —— 有三個工具就屬於這一類。

讓這套東西保持誠實的規則是:系統裡其他每一份清單都從這些行派生而來 —— 靜默層集合、MCP 作用域層級、寫入作用域集合 —— 而它們沒有一份是手工維護的。一份手工維護的靜默清單,旁邊再放一份手工維護的 MCP 清單,正是一個工具在聊天裡被正確門控、卻對外部用戶端悄悄放行的經典成因。

我們沒料到的那個約束是:這份宣告清單必須包含零個執行時 import。它由桌面端 UI 和後端共用,而只要有一個 import,就會經由某個工具的實現,傳遞性地牽扯到一個只能在 Node 上跑的加密依賴。把它拽進瀏覽器打包產物,應用就會在模組載入時崩掉。型別檢查器和單元測試都抓不到它 —— 兩者都能愉快地解析這個 import。能抓到它的,是一個把該檔案當作文字讀取、只要出現任何 import 語句就失敗的測試;這做法看著很糙,直到它第一次救了你。

如果你也在做一個,哪些經驗可以遷移

  • 在為你的架構辯護之前,先數一數你的工具。正確的劃分是一次測量,而不是一條原則。
  • 拿你最弱的模型去測發現機制。前沿模型該搜的時候就會搜;這關於你的使用者實際會挑的那個模型,什麼也說明不了。
  • 用「使用者自己的措辭會不會點出這個工具」來決定可見性。被隱式呼叫的工具屬於內聯;人們會指名索取的工具可以留給發現。
  • 在把任何對順序敏感的東西放進框架的步驟鉤子之前,先查清它們究竟在什麼時候觸發。
  • 給元工具設上限。任何能被反覆呼叫而不觸及真實狀態的東西,就一定會被反覆呼叫,而花在搜尋上的步驟預算,等於浪費掉一個回合。
  • 讓「工具未被發現」的錯誤點出恢復呼叫,和其他任何校驗器錯誤一樣。
  • 每個工具只宣告一次,其餘每一份清單都由它派生。兩份手工維護的同一批工具清單終將出現分歧,而分歧會在一條安全邊界上現形。
  • 如果一個模組承載著編譯器無法表達的承重約束,那就寫下那個把它當作文字來強制執行的糙測試。

這些跑在哪裡

這一切都交付在 DynoTable工具目錄之中 —— 在你自己的 憑證上進行具備 schema 感知的查詢,而寫入永遠只會落入一個可審查的暫存區。同一批宣告也驅動著外部智慧體所連線的 MCP 伺服器,在那裡,每一行上的暴露層級就變成了外部用戶端被授予的作用域;我們如何讓那件事變得安全 —— OAuth、使用者同意、憑證隔離 —— 則是另一個故事了。

而支撐這一切的底層,也就是那些讓這裡每一個工具都能被一個廉價模型扛下來的校驗器,自成一篇

不必透過主控台就能操作 DynamoDB

一款快速的 DynamoDB 桌面用戶端,可執行 DynamoDB 無法執行的真正 SQL — JOINs、GROUP BY、聚合 — 並支援視覺化編輯與使用你自己的 Bedrock 金鑰的 AI 代理。

30 天免費試用,無需信用卡 — 之後為無時間限制的免費方案。