我們如何讓便宜的 LLM 可靠地操作 DynamoDB:工具驗證器 + eval
DynoTable 的 AI 助理是跑在你自己的 憑證上,這表示模型由你挑 — 而許多人挑的是便宜的那個。 所以我們不會拿頂尖模型來調校助理。我們的底線是 Amazon Nova Lite,一個每次查詢成本近乎捨入誤差、 而且會以頂尖模型不會的方式失敗的模型。
這整篇文章的重點就在這裡:在某個 情境上 — 一個需要把複合鍵表格當作基底的 join — Nova Lite 得了 0%。我們沒有換模型、沒有加 few-shot 範例、也沒有微調。 我們只重寫了一則驗證器錯誤訊息,明確告訴它該改做什麼。 它就衝上了 100%,在下一步就把那個 join 翻正了。
模型從來不是瓶頸。訊息才是。
這就是那堂課背後的工程故事:環繞在每次工具呼叫外圍的驗證器、 我們刻意_不_驗證的東西,以及仲裁每一項變更的 eval 測試框架。 如果你正在一個預算型模型上建置 — 不論是操作 DynamoDB 還是別的東西 — 其中大部分都能沿用。
為什麼便宜的模型偏偏在 DynamoDB 上失敗
DynamoDB 對小模型是個充滿敵意的目標,因為它的查詢介面_看起來_像 SQL,卻不是。
PartiQL 接受 SELECT 語法,卻不支援 JOIN、
不支援 GROUP BY、不支援 DISTINCT、不支援 LIKE —
而一個從網路上學會 SQL 的模型,會信心滿滿地把它們全都吐出來。
我們實際觀察到的失敗,每一個都來自一次真實的紀錄執行:
- 掉了
limit參數。 常常在查詢工具上省略那個可選的列數上限。 後端很樂意把所有東西全數回傳,一份 200 KB 的工具結果落進對話, 而下一個模型輪次就在 context 上限上死掉 — 一個迴圈中途的ValidationException,還被歸咎到錯的元件上。 - 不支援的構造。 PartiQL 中的
JOIN、GROUP BY、聚合函式 — DynamoDB 永遠不會執行的語句,之所以被吐出來,是因為它們在 Postgres 上會是對的。 - 識別字調包。 給 Nova Lite
SELECT * FROM customers與 「為 Germany 加上一個 WHERE」的指示,它有時會回傳SELECT * FROM orders WHERE …— 一個使用者從未提到的表格。 該語句完美地通過剖析。沒有任何語法檢查能逮到它。 - 工具導向錯誤。 被問到一個附加檔案時,Nova Lite 呼叫了列出開啟分頁的工具, 想在分頁裡找檔案 — 它幾乎完全不用工具搜尋機制, 於是任何不是直接可見的東西都等於不存在。
這些都不是假設。每一個都催生了下面一道特定的護欄。
在工具邊界驗證,並把錯誤變成一堂課
單一最重要的架構決策:驗證位於工具的 execute 步驟內,而不在 input schema 裡。 一個 schema 拒絕是死路一條 — 迴圈看到一個型別錯誤,卻學不到任何東西。 一個會執行、檢查、然後回傳結構化錯誤的工具, 會給模型一個它能在下一步做出反應的正常工具結果。
而且那則錯誤是寫給模型看的,不是寫給人類的日誌看的。 我們匯出工具的錯誤讀起來就像這樣,逐字:
startExport requires exactly one of {tabId} or {sql}.
FIX: call listOpenTabs and pass {tabId}, or pass {sql} with a single SELECT.startExport {sql} must be a read-only single SELECT.
FIX: remove any INSERT/UPDATE/DELETE/DDL and pass one SELECT statement.查詢驗證失敗會帶著一個 validation: 前綴 —
validation: parse-error: …、validation: partiql-unsupported: JOIN is not supported by DynamoDB PartiQL. — 而 system prompt 把它確立為
_修正語句_而非重試相同輸入的訊號。一則拒絕訊息就是一個教學訊號。
在我們採用 FIX: 慣例之前,同樣的失敗最後都以模型道歉、
並叫使用者自己去點匯出按鈕收場。之後,它會自我修正並完成任務。
那個必然的推論則花了更久才學會:具體範例該擺在哪裡,是一個設計軸線。 如果一個錯誤是被某個驗證器在執行期逮到的,就讓 system prompt 保持通用, 並讓動態的錯誤訊息去攜帶具體的表格與修正方式 — prompt 維持精簡,而那堂課會恰好在需要時抵達。 唯有_任何驗證器都逮不到_的錯誤(例如一開始就把一個聚合問題導向正確的工具), 才值得在 prompt 本身放一個具體範例。
剖析你打算拒絕的東西
用一句通用的 syntax error at offset 27 來拒絕 JOIN,什麼也教不了。
所以我們手寫的 PartiQL 剖析器做了一件稍微有點反常的事:它刻意把 DynamoDB
不支援的構造剖析成一棵有效的語法樹 — 就只為了讓一個 walker 能發出一則具體、
可教的診斷訊息。全都逐字、全都面向模型:
JOIN is not supported by DynamoDB PartiQL.GROUP BY is not supported by DynamoDB PartiQL.TOP N is not supported. Use the API limit parameter.IN list has 120 items. DynamoDB PartiQL caps IN at 50 values (PK column) or 100 values (non-key column).IS NOT NULL is not supported in DynamoDB PartiQL. Use attribute_exists.lower(path) is not a DynamoDB PartiQL function. Use … instead.— 其中的 替代方案會依函式逐一解析出來。
每一則訊息都指名那個構造_以及_那個逃生口。搭配 prompt 規則 「如果 PartiQL 拒絕了一個不支援的構造,就立刻切換到 SQL workbench」, 一個便宜的模型只需一步就能從它的 Postgres 直覺中恢復,而不會原地掙扎。
知道什麼_不_該驗證
我們 SQL 驗證器中理解 schema 的那一層,會拿表格與欄位參照去比對應用程式手上已有的真實表格描述。
早期它也會標記出對 DynamoDB 未宣告之屬性的參照 — 這聽起來對,卻恰恰是錯的。
DynamoDB 的 attributeDefinitions 只列出鍵屬性,
所以那道檢查拒絕了 WHERE <non-key> = … — 存在意義上最常見的正當查詢形態。
那道檢查如今是一個永遠不會擋路的警告。
每個驗證器都是一場賭注,賭被拒絕的形態比較可能是錯的、而非對的。 當資料模型無法支撐那道檢查時,就別下這個賭注。
把結果上限當成行為,而不只是安全措施
針對掉了 limit 而爆掉的那個問題,修法不是「把 limit 加進 prompt」(便宜的模型反正還是會掉它)。
查詢工具的結果被硬性設限 — 10 列或 5 KB,以先到者為準,
而且永遠至少保留第一列 — 而那則截斷通知本身也是規範式的:
Showing 10 of 4,200. To filter: re-run with WHERE.
To view in UI: emit proposeOpenTable.
To aggregate: use runWorkbenchSql GROUP BY.這些上限刻意遠低於 context 能容納的量。目的是行為上的:
_逼模型交給使用者一個真實的表格檢視或一個精確的聚合,
而絕不把列一一列舉進聊天裡。_恢復提示會指名確切的工具,
所以這個上限會在模型正需要時,把導向這件事教給它。同一份封裝在 eval 測試框架中被逐位元組地鏡像複製,
而一個回歸情境針對一個一萬項的表格釘住 truncated: true,讓這個上限永遠不會悄悄消失。

Eval 才是仲裁者
上述沒有一項是靠直覺設計、然後靠信仰保留下來的。每一條 prompt 子句、 驗證器訊息與上限,都受一套 eval 測試套件把關,它會針對一個已植入種子資料的 DynamoDB Local 執行真實的 agent 迴圈 — 真實的工具、真實的查詢執行、 直接匯入而非重新實作的正式版 prompt 建構器與驗證器 — 再由針對工具軌跡、資料庫最終狀態與文字的二元檢查來評分。
eval 逮到、而直覺漏掉的東西:
- 一次「更乾淨」的 prompt 通用化,讓一個情境從 100% 回歸到 0%。 它讀起來對我們比較好。底線模型不同意。只有 eval 注意到了。
- 頂尖模型的 prompt 建議對便宜的模型是不折不扣的錯。 已發表的指引說要軟化 CRITICAL/MUST 這類措辭,因為大模型會對它過度反應。 便宜的模型則是_遵從不足_,需要那個強硬的版本。 我們如今會對任何借來的建議做 eval 把關,而不是靠信仰照單全收。
- 嚴格的工具斷言懲罰了正確的行為。 一個要求模型呼叫
ask工具來釐清的評分器, 在整條模型梯隊上只通過了 19% — 幾乎每一個「失敗」都是一個已經正確地_用文字_請求釐清的模型。 那個評分器如今兩者皆接受。要評分的是面向使用者的意圖,而不是機制。 - 模型梯隊掙來了它的收窄。 我們一開始很廣 — Mistral、AI21、GLM、Qwen、
Nova Pro,還有更多 — 然後把它砍到兩級(Nova Lite、Claude Haiku),
理由只有真實執行才會浮現:某個家族每次呼叫貴上約 10 倍,
因為 Bedrock 沒有為它提供 prompt 快取;Nova Pro 在開放式 join 上
幻覺出像
customers.customerName這種串接起來的屬性名稱。 還有一個家族不得不被直接列入封鎖名單 — Bedrock 統一的 API 會很樂意接受某些模型的工具定義,而這些模型的 adapter 之後卻把函式呼叫吐成_純文字_, 這是 API 中繼資料不會告訴你的事。我們是靠一次付費的 smoke 探測、而非靠規格找到它的。
有兩項紀律讓這套套件保持誠實。範例表格永遠不等於測試表格: prompt 範例使用種子資料中不存在的中性名稱,而 eval 跑在不同的表格上 — 於是一次通過證明的是模型把模式一般化了,而不是背下了我們的範例。 以及$0 的 watch 模式拒絕使用已錄製的 fixture: 重放的模型輸出會綠燈通過那些仰賴真實工具活動的斷言, 所以免費層只驗證接線,而任何宣稱有一個_分數_的東西, 都必須在一個真實模型上花掉真實的 token。
檢查清單,如果你正在打造這類東西
- 先挑好你的底線模型,並始終對著它做 eval;頂尖模型會藏起你的 bug。
- 把驗證放進工具的 execute 步驟;回傳迴圈能對其行動的錯誤,永遠別回傳 schema 死路。
- 把每一則拒絕都寫成
哪裡錯了+FIX: 確切的下一步動作, 並把使用者實際的表格內插進去。 - 剖析你打算拒絕的東西,好讓診斷能夠具體。
- 拿每個驗證器去對照你的資料模型稽核 — 刪掉那些會拒絕正當形態的驗證器。
- 把工具結果設限在容納量以下,並讓截斷通知把模型導向正確的下一個工具。
- 把 eval 當成針對真實(本機)後端的真實迴圈來跑,重用正式版的程式碼路徑; 讓範例資料與測試資料互不相交;讓 eval — 而非品味 — 成為每一次 prompt 與訊息變更的仲裁者。
如果你用的是頂尖模型,這些還重要嗎?
沒那麼常,但確實重要。這些驗證器在每一級都是關鍵 —
一個頂尖模型照樣不能在 DynamoDB 上跑 JOIN,而規範式的錯誤只是被更快地消化掉。
在我們自己的執行中,一個_頂尖等級_模型的晶片發出行為,
曾在一種較小模型能處理的 prompt 措辭下掉到 0% — eval 測試框架也逮到了那個。
可靠性工程不是一種小模型稅;小模型只是讓帳單來得更早。
這些跑在哪裡
這一切都內建在 DynoTable 的助理及其 受控管工具目錄中:跑在你自己 Bedrock 憑證上、 理解 schema 的自然語言查詢、 精確的全表聚合,以及永遠只落在一個可審查暫存區的寫入。 外部 agent 透過 MCP server 取得同一套經過驗證的工具組 — 我們如何安全地打造_那個_,則是它自己的故事,從 OAuth 到憑證隔離。
而當你想要的是決定性、而非生成 — 那個你會提交進程式碼的查詢 — 就完全跳過模型: Expression Builder 會親手組出那個確切的請求。


