
用 GROUP BY ROLLUP 與純量子查詢消除 text-to-SQL 智能體的文字計算 — 禁止規則擋不住的加總與扇出誤差
text-to-SQL 智能體違反了「不可手動加總」的規則。GROUP BY ROLLUP 與純量子查詢不是修規則,而是直接消除了這個步驟。
本頁目錄
引言
我們的 text-to-SQL 智能體回報的總計約為 71.2 萬,而正確數字約為 61.2 萬。它執行的 SQL 是正確的,回答的格式也沒問題,也沒有出現任何錯誤。它取得了十列資料,在回覆的文字中自行相加,結果在十萬位上錯了一位數。
這件事之所以值得深究,是因為系統提示早就禁止了這種行為。規則就寫在一個叫做「用 SQL 做計算」的段落裡,附有一個錯誤示範和一個正確示範。一條附有範例的規則,還是被打破了,而通常會有的反應,也就是把規則寫得更嚴,當初寫下這段規則的人早就試過了。
真正的修正並不是更好的禁止規則,而是改變了智能體所寫查詢的兩種形狀,讓錯誤從此無處發生:合計改用 GROUP BY ROLLUP,JOIN 則改用純量子查詢取代。本文涵蓋這兩個失敗、兩個修正,以及另外兩個被實測否決的變更。
禁止規則早就存在
提示中相關段落的內容大意是:絕不在回答文字裡進行計算。比率、百分比、單位換算,以及小計,全都應該在 SELECT 子句中完成,寫下的數字必須就是查詢回傳的數字。
裡面附了兩個範例。一個示範了錯誤的做法:從兩個分別取得的數字,在文字中計算出單位平均值。另一個示範了另一種錯誤:把明細用手動方式相加,1,951 + 278 = 2,229。
而第二個範例,幾乎就是後來實際發生的那個失敗。
這排除了所有討喜的解釋:規則不是不存在,不是含糊不清,也不是埋在一大段沒有範例的文字裡。模型明明被告知了規則,也看過錯誤版本和正確版本,卻還是做出了錯誤的版本。
失敗一:在文字中把三列相加
問題要求的是全國總計。提示中另一處的慣例規定,這種總計必須分兩層回報:先是合併子公司的合計,接著是納入權益法適用公司的數字。
智能體執行了一支查詢:
SELECT company_group, consolidation, stock_countFROM stockWHERE fiscal_year = 2026結果返回了十列。它在回覆中把這些數字相加,結果錯了一位數。
智能體同時做到了兩件事:完全符合兩層報告的規定,也完全違反了計算禁止規則。原因是它寫的查詢無法同時滿足這兩者。
回傳明細的查詢,回傳的是部分,不是整體。 要回報第二層數字,智能體需要一個沒有任何儲存格裝著的數值,它的選項只有兩個:再執行一支查詢,或是自己把列相加。它選了自己相加。
把這件事讀成「不服從」,會指向錯誤的修法。規則不是被忽略,而是在那個位置上根本無法同時滿足,不管把規則重寫幾次,那個位置本身都不會改變。
GROUP BY ROLLUP:總計以一個儲存格的形式返回
GROUP BY ROLLUP 是智能體提出的,我採用了這個做法。Presto 與 Trino 都支援它,所以在 Athena 上不用修改就能執行:
SELECT COALESCE(consolidation, 'total') AS tier, SUM(stock_count) AS stock_countFROM stockWHERE fiscal_year = 2026GROUP BY ROLLUP(consolidation)ORDER BY 1tier stock_countconsolidated 524,000equity_method 88,000total 612,000兩層回答所需的兩個數字,現在都成了現成的儲存格。相加不是被勸阻,而是這個步驟本身已經不存在了。 智能體只要引用這兩個數值就能完成回答。
提示端的變更,是在描述兩層回答的段落裡加入一個附範例的樣板,再加上一句話把限制收窄:你可以用兩支個別的查詢分別取得小計與總計,唯一被禁止的是在回覆文字中把明細的列自行相加。與其強制規定一支「唯一正確」的查詢,不如只點名一個禁止的動作。這樣既留給智能體空間,也堵住了這個失敗。
變更之後,測試套件中需要兩層回答的四個問題各執行了三次。十二次全部通過,原本失敗的那一題,三次都採用了 ROLLUP 樣板,每次只需一次工具呼叫,比原本的四次少了許多。
失敗二:一列 JOIN 到三列,營收變成三倍
第二個失敗來自一個人均比率的問題:某個事業部門一年的營收,除以某個時間點該集團的業務人力。營收存放在按月更新的 financials 資料表,人力則存放在按月更新的 headcount 快照中。
智能體正確地把營收彙總成一列,接著把它 JOIN 到人力資料表:
SELECT SUM(r.revenue) / NULLIF(MAX(p.sales_staff), 0)FROM ( SELECT company_group, SUM(revenue) AS revenue FROM financials WHERE fiscal_year = 2025 AND segment = 'construction' GROUP BY 1) rJOIN headcount p ON r.company_group = p.company_groupWHERE p.fiscal_year = 2025 AND p.month = 3這個集團底下有三家公司,所以 headcount 回傳了三列。原本只有一列的營收,對應到這三列時各自被複製了一份,SUM 因此把它算了三次:原本約 21,000 的數字,變成了約 63,000,比率也隨之被放大成約三倍。
有兩個細節,讓這個失敗比第一個更棘手。人力數字是對的,因為三家公司裡有兩家的業務人力是零,所以 SUM 和 MAX 剛好一致;半對半錯的答案,比起徹底錯誤的答案更不容易引起懷疑。而且它是間歇性的:針對這一題觀測 14 次,失敗只出現過一次,所以單次通過根本無法證明任何事。
提示中原本就有一段講扇出的內容,也早就規定了標準的處理方式:在 JOIN 之前先用子查詢把兩邊都彙總好。就像計算禁止規則一樣,指引明明存在,失敗還是照樣發生了。
純量子查詢:單一實體時完全不要 JOIN
解決這個問題的關鍵發現是:這支查詢本來就不需要 JOIN。 問題只針對一個集團。分子是一個數字,分母也是一個數字,而 JOIN 是用來比對集合的機制:
SELECT ROUND( (SELECT SUM(revenue) FROM financials WHERE company_group = 'north' AND fiscal_year = 2025 AND segment = 'construction') / NULLIF((SELECT SUM(sales_staff) FROM headcount WHERE company_group = 'north' AND fiscal_year = 2025 AND month = 3), 0), 2) AS revenue_per_head每一支子查詢都精確地只回傳一個值。 既然沒有列可以被複製,扇出在這裡不是被避免,而是根本無法表達出來。 這與子查詢先 JOIN 的做法所提供的保證不同:後者雖然正確,但仍然仰賴智能體每次都確實把兩邊彙總好。
改寫後的段落,把這種寫法列在單一實體比率的最前面,並把 JOIN 這套模式降格為它真正該用的場合:比較或排名多個群組。這種情況下才真的需要 JOIN,而 GROUP BY 自然會把智能體導向把兩側都彙總好。
變更之後,測試套件中十一個比率相關的問題全數通過。在八個單一實體的問題裡,有五個完全拿掉了 JOIN。 剩下三個保留 JOIN 的問題,是針對年度快照用 MAX 來 JOIN,而 MAX 不受重複列影響,因為重複的列不會改變最大值。危險的組合,也就是跨多列 JOIN 的 SUM,不再出現在產生的 SQL 裡。
只會拉高基準分數的修正
在這兩個提示變更之前,智能體其實已經實作了另一個做法:在 SQL 工具背後的 Lambda 裡計算欄位總計。任何多列的結果,都會自動附上可加總數值欄位的加總值,這樣一來,總計就永遠是一個可以直接引用的值,而不是需要計算的加總。這個做法還附上了八個單元測試,涵蓋要排除的欄位型別、單列結果,以及非數值儲存格。
但它在部署前就被撤回了。查證助理平台實際上如何執行 SQL 之後發現,查詢工具其實是一個平台內建的功能,靠一個旗標和一個資料庫名稱來設定,直接執行 Athena。那個剛改良完的 Lambda,其實只有我們的基準測試環境才能碰到。
如果照樣出貨,結果會是正式環境完全沒有改變,基準分數卻上升了。因為測量工具本身改善而變好的測量結果,比完全不測量還糟,因為它回報了根本沒有發生的進步。發現這件事之後,我裁定變更範圍只限於主提示與審查器,上面提到的兩種寫法就是在這個限制下做出來的。
讓情況變得更糟的變更
另一個被否決的變更同樣值得記錄,因為它顯示這套方法不只能確認假設,也能推翻假設。
有幾個失敗都跟把日曆月份換算成會計年度有關,於是智能體把計算規則換成一張查表,明確列出範圍內每一年的對應關係。移除計算這個做法才剛在兩個地方成功,把這個想法延伸出去,看起來顯然是對的。
這個變更針對 13 個與日期相關的問題,測了四輪。原本一直穩定通過的三個問題開始失敗,而且都錯在同一個年份上,其中一個問題甚至在 SQL 註解裡寫著正確的年份,卻在 WHERE 子句裡放了另一個年份。可能的原因是互相干擾:提示現在用兩張重疊的表格描述一到三月的對應關係,模型要比對的不再是一條該套用的規則,而是兩個互相競爭的樣式。
還原之後,之前的行為就恢復了,並在受影響的三個問題上連續驗證了 18 次通過。產生兩個好變更的同一種推論,也產生了一個壞變更,能分辨兩者的只有實測。
總結
套用這些變更之後的那一輪執行,測試套件在 57 題中全數通過,計算與扇出相關的失敗都消失了。總共嘗試了四個變更,最後保留了兩個:
| 變更 | 結果 |
|---|---|
兩層總計改用 GROUP BY ROLLUP |
保留。三輪共 12/12,每次都採用了樣板 |
| 單一實體比率改用純量子查詢 | 保留。11/11,危險的 JOIN 形狀不再產生 |
| 在 SQL 工具裡計算欄位總計 | 撤回。正式環境不會經過那個工具 |
| 日期換算改用查表 | 還原。破壞了三個原本通過的問題 |
可以推廣的部分,在於這兩個保留下來的變更,與它們所取代的規則之間的差異。禁止規則要求的是每一次未來執行都要遵守;而一種無法表達出錯誤的查詢形狀,則什麼都不要求。 當一條附有範例的規則還是被打破時,再加第三個範例通常是最弱的做法。更有效的問法是:模型該改寫成什麼樣子,以及能不能讓這個失敗在那個位置上根本無法表達出來。
還有兩件比較小的事值得留下來。第一,在假定一條規則被忽略之前,先確認它在被打破的那個位置上是不是可以同時滿足的。這裡的第一個失敗,其實是兩條正確規則之間真正的衝突。第二,不要只信任套件的總分,要針對每個問題重複執行來測量:一個大約 14 次才出現一次的間歇性失敗,在單次執行中是看不見的,而正是重複測量,才同時抓出了那個毫無幫助的變更,以及那個造成傷害的變更。
由於這是人機協作完成的工作,這裡記錄一下判斷落在誰身上:診斷規則衝突、提出兩種機制、寫出提示變更、並發現 Lambda 不在正式路徑上的,都是智能體。在那個發現之後設定範圍、決定不要求證明效果就先出貨扇出的修正、並拍板最後保留哪些變更的,是我。智能體提出的方案本身很好,但它對修正該放在哪裡的第一直覺是錯的。 這大概就是該預期的分配方式。



