用「自動產生的思維鏈」做 Text-to-SQL 的脈絡學習 —— 不需人工標註、一次 API 呼叫即可生成 SQL,在 Spider dev set 上取得脈絡學習方法的 SOTA。
Text-to-SQL 的目標是:在給定資料庫 schema 的前提下,把自然語言問句翻譯成對應的 SQL 查詢,讓一般使用者能以更直覺的方式存取關聯式資料庫。
過去研究主軸放在跨領域(cross-domain)的語意剖析器:在 Spider、SParC、CoSQL 這類資料集中,train / dev / test 所使用的資料庫互不重疊。傳統做法是訓練專用的 text-to-SQL 模型(如 RAT-SQL、LGESQL、PICARD),雖然成績亮眼,但有三個結構性痛點:
需要大量高品質的人工標註訓練樣本,建置成本高。
微調(finetuning)消耗大量運算資源與時間。
模型結構複雜(如 AST-based),上線維運不易。
LLM 的出現帶來脈絡學習(in-context learning)這條新路:免微調、靠 zero-shot / few-shot prompt 即可運作。但作者指出當時的 LLM 方法仍有明顯缺口:
① 簡單 prompt 成績差:如 Rajkumar et al. (2022) 的 prompt 設計過於樸素,落後微調模型。
② 複雜流程又慢又貴:如 DIN-SQL 用多步驟工作流,生成一條 SQL 需呼叫 LLM API 多次(約 4 次)。
③ 缺乏自動 CoT:CoT 雖能提升推理,但須人工挑選範例與標註,text-to-SQL 領域沒有自動產生 CoT 的方法。
④ 未延伸到多輪:脈絡學習方法尚未擴展到 SParC、CoSQL 等多輪對話資料集。
系統性研究 DB schema 呈現格式、few-shot 範例選擇策略對 LLM 表現的影響。
自動生成思維鏈,每條 SQL 只需一次 API 呼叫,省時省錢,並在 Spider dev set 取得脈絡學習 SOTA。
在 SParC、CoSQL 上達到與微調模型相當的成績。
作者把 SQL 生成過程形式化為一次 LLM 呼叫,表現好壞取決於三個變數:資料庫呈現格式、範例選擇策略,以及範例 prompt 的設計。
在 zero-shot 下,schema 的呈現格式幾乎主宰 LLM 表現。作者比較五種格式:
| 格式 | 說明 |
|---|---|
Table(Column) | 每行列出一個 table 及其欄位,遵循 OpenAI 官方文件範例。 |
Table(Column)(PF) | 在上者末端加上主鍵(PK)與外鍵(FK)資訊。 |
Create(NoPF) | 用 SQL 的 create table 語法描述,含欄位型別,但無 PK/FK。 |
Create(EoC) | 在 Create 基礎上,把 PK/FK 加在「對應欄位」末端(End of Column)。 |
Create(EoT) | 在 Create 基礎上,把 PK/FK 加在「整張表」末端(End of Table)。 |
此外還會在每張表後附上 c 筆範例資料列(DB contents),幫助模型判斷實際儲存值(例如分辨 "France" vs "French")。
create table singer (
Singer_ID number,
Name text,
Country text,
...
primary key (Singer_ID)
)
/* 3 example rows from table singer:
Singer_ID Name Country ...
1 Joe Sharp Netherlands ...
2 Timbaland United States ...
3 Justin Brown France ... */
作者採用靜態 + 動態的混合策略,每個測試案例共有 ns + nd 個範例:
從訓練集隨機抽 ns 個,固定用於每一個測試案例的脈絡中。
針對當前測試問句,用預訓練模型計算問句相似度,挑出 train set 中最相關的 top-nd 個。
核心直覺:問句越相近的動態範例,越能提供有效資訊給 LLM。
作者把 CoT prompt 設計成「類似 schema linking」的形式 —— 但關鍵在於它能自動生成,完全不需人工標註。
傳統 schema linking 會把問句 token 與 schema 項目(table / column)做字串對應並連結。ACT-SQL 沿用此精神:對訓練樣本(schema + 問句 + 正解 SQL),自動推導出「哪段問句切片對應到哪個欄位/表」的推理鏈。
給定問句 q = (q₁,…,q|q|) 與 SQL s,定義切片 qᵢ,ⱼ = (qᵢ,…,qⱼ)。流程分三步:
對每個 [tab].[col],用預訓練模型計算它與所有問句切片的相似度,取最相關切片連結。忽略 GROUP BY 子句中的欄位(通常不會在問句中直接提到)。
排除已在步驟①出現過的表,剩下只出現在 FROM 的表,同樣找最相關問句切片連結。
把 SQL 中出現的數值(values)加入 CoT,最後把完整 SQL 接在 CoT 末端。
問句:「找出有高畫質電視的 TV channel 的方案選擇與系列名稱」。Few-shot 版本誤把 Hight_definition_TV 多放進 SELECT;套用 ACT-SQL 後,模型先完整做 schema linking,最終寫出無冗餘欄位的正確 SQL:
Let's think step by step.
According to "TV channel that has high definition TV",
columns [TV_Channel.Hight_definition_TV] may be used.
According to "package choice and series name",
columns [TV_Channel.Package_Option] and
[TV_Channel.series_name] may be used.
So the final answer is:
SELECT package_option , series_name
FROM TV_Channel WHERE Hight_definition_TV = 'yes'
關鍵優勢:整段思維鏈是 LLM 在「同一次」輸出裡自己生成的 —— ACT-SQL 不需要額外的 LLM API 呼叫去產生 CoT,因此比 DIN-SQL 等多步驟方法更快、更便宜。
多輪問句彼此有上下文依賴,單輪 prompt 與 auto-CoT 無法直接套用(schema linking 資訊可能散落在多個句子中)。
作者採兩階段法:先用 LLM 把多輪問句改寫(rewrite)成去除上下文依賴的獨立問句,將多輪資料集轉成單輪資料集,再套用前述脈絡學習方法。每個多輪資料集會人工標註 10 個改寫範例以固定格式、提升品質。
已知弱點:改寫品質直接影響成績。若改寫遺漏關鍵資訊(如第一句的 "airline" 在後續句子消失),錯誤會沿著 schema linking 傳播 —— 這也是 ACT-SQL 在多輪資料集表現不如單輪的主因。
| 資料集 | 規模 / 特性 | 用途 |
|---|---|---|
| Spider | 200 個資料庫、138 領域;train 8,659 筆、dev 1,034 筆(test 未公開)。附評測腳本,把 SQL 分為 easy / medium / hard / extra 四級難度。 | 主要評測基準 |
| Spider-Syn | 把問句中 schema 相關詞換成同義詞,破壞單純字串比對。 | 同義詞穩健性 |
| Spider-DK | 定義五類領域知識,加入反映真實改寫的範例。 | 領域知識泛化 |
| Spider-Realistic | 移除問句中對欄位名的明確提及,測 text-table 對齊能力。 | 文字-表格對齊 |
4,298 條連貫問句序列,含 12k+ 個別問句與對應 SQL。
10k+ 標註 SQL;每段對話模擬「使用者探索資料庫、專家以 SQL 取數」的真實情境。
Exact Match:預測 SQL 每個元件須與正解等價(不比對數值)。
Execution Accuracy:執行結果須正確,通常比 EM 更精準。
Test-Suite:在同 schema 的多個資料庫實例下執行結果都須正確。
多輪另用 QM(問句層級)與 IM(整段互動全對才得分)。
主力模型為低成本的 GPT-3.5-turbo,並用 GPT-4 在 Spider 上驗證 auto-CoT;相似度模型用 text2vec-base-chinese;temperature = 0(greedy)。
固定 Table(Column) 格式、只調整附帶的範例資料列數。不附內容時最差;設為 3 列時最佳;再多也不會更好。後續實驗一律用 3 列。
| DB Style | EM | EX | TS |
|---|---|---|---|
| Table(Column) | 45.3 | 78.3 | 69.4 |
| Table(Column)(PF) | 45.4 | 79.0 | 69.1 |
| Create(NoPF) | 45.3 | 77.0 | 66.1 |
| Create(EoC) | 44.8 | 79.2 | 67.7 |
| Create(EoT) | 44.8 | 78.3 | 67.9 |
zero-shot 下 Table(Column) 系列較佳(貼近 OpenAI 官方文件、近似預訓練資料);含 PK/FK 的格式在 EX/TS 上有幫助,顯示鍵資訊對模型有效。
| LLM | 方法 | API/SQL | EM | EX | TS |
|---|---|---|---|---|---|
| Codex | Rajkumar (2022) | 1 | – | 67.0 | 55.1 |
| Codex | Chang & Fosler (2023) | 1 | – | 76.8 | – |
| Codex | DIN-SQL | 4 | 57.2 | – | 69.9 |
| GPT-4 | DIN-SQL | 4 | 60.1 | 82.8 | 74.2 |
| GPT-3.5-turbo | ACT-SQL (本文) | 1 | 62.7 | 80.4 | 71.4 |
| GPT-4 | ACT-SQL (本文) | 1 | 61.7 | 82.9 | 74.5 |
ACT-SQL 只用 1 次 API 呼叫,就在 EM / EX / TS 上取得既有脈絡學習方法中的最佳成績 —— GPT-4 版本 EX 82.9、TS 74.5 均超越需 4 次呼叫的 DIN-SQL;GPT-3.5 版本 EM 更達 62.7。
| 類型 | 方法 | Dev | Test | 趨勢 |
|---|---|---|---|---|
| 微調 | Graphix-3B+PICARD | 81.0 | 77.6 | ↓ |
| 微調 | RESDSQL-3B+NatSQL | 84.1 | 79.9 | ↓ |
| 脈絡學習 | C3 | 81.8 | 82.3 | ↑ |
| 脈絡學習 | DIN-SQL | 82.8 | 85.3 | ↑ |
微調模型因依 dev 表現挑選,易過擬合,test 反而下滑;脈絡學習方法 dev/test 對模型而言「等價」,故無此落差 —— 意味 ACT-SQL 的 dev 成績更具代表性。
59.3
ACT-SQL TS;few-shot 為 54.5。CoT 帶來明顯提升。
62.4
ACT-SQL TS;EX 達 68.2,超越微調模型(得益於 LLM 內含廣域領域知識)。
61.2
ACT-SQL TS,與微調模型相當。
| 資料集 | 方法 | QM (TS) | IM (TS) |
|---|---|---|---|
| SParC | GAZP+BERT (微調) | — | — |
| Few-shot (本文) | 55.8 | 31.5 | |
| ACT-SQL (本文) | 56.9 | 29.6 | |
| CoSQL | Few-shot (本文) | 55.5 | 22.9 |
| ACT-SQL (本文) | 55.2 | 21.5 |
多輪情境下 ACT-SQL 相對 few-shot 沒有明顯優勢(IM 甚至略降),整體與微調的 GAZP+BERT 相當但仍有改進空間 —— 作者歸因於兩階段法中的「問句改寫」品質不穩。
下面是從 X-LANCE/text2sql-GPT 直接擷取的關鍵程式碼,呈現 ACT-SQL 實際上是怎麼組裝 prompt 的。論文附錄 C 的範例都源自這些函式。
不論 zero-shot 或 few-shot,給 Chat 模型的 system message 永遠是這一行(取自 util/prompt.py get_prompt()):
# util/prompt.py · line 66
prompt = [{'role': 'system',
'content': 'Given the database schema, you need to '
'translate the question into the SQL query.'}]
注意 沒有複雜的角色扮演、沒有規則列表、沒有 few-shot 引導語。整個系統提示只有 14 個字,所有「教學」都靠後續的 user/assistant 範例對話來傳遞。
get_prompt()這是論文方法的核心入口:把 system message、若干 (user 問句 + assistant SQL 或 CoT) 範例、最後再加上實際測試案例的 user 問句,串成一個多輪對話。
# util/prompt.py · get_prompt() · 精簡版
def get_prompt(self, args, db_id=None, question=None, shots=[], c_num=-1):
if c_num < 0:
c_num = args.content # DB 範例資料列數(論文設 3)
if args.gpt in GPT_CHAT_MODELS: # gpt-3.5-turbo / gpt-4
prompt = [{'role': 'system',
'content': 'Given the database schema, you need to '
'translate the question into the SQL query.'}]
# ===== 逐個塞入 ns + nd 個範例(靜態 + 動態) =====
for shot in shots:
prompt.append({'role': 'user',
'content': f"Database schema:\n{self.db_prompts[shot['db_id']][c_num]}\n"
f"Question: {shot['question']}"})
if args.cot:
prompt.append({'role': 'assistant', 'content': shot['cot']})
else:
prompt.append({'role': 'assistant', 'content': shot['query']})
# ===== 最後附上實際要回答的測試案例 =====
if db_id and question:
prompt.append({'role': 'user',
'content': f'Database schema:\n{self.db_prompts[db_id][c_num]}\n'
f'Question: {question}'})
elif args.gpt in GPT_COMPLETION_MODELS: # code-davinci-002, text-davinci-003
prompt = ''
for shot in shots:
prompt += 'Given the database schema:\n'
prompt += self.db_prompts[shot['db_id']][c_num] + '\n'
prompt += 'Translate the natural utterance into the SQL query: ' \
+ shot['question'] + '\n'
prompt += (shot['cot'] if args.cot else shot['query']) + '\n'
if db_id and question:
prompt += 'Given the database schema:\n'
prompt += self.db_prompts[db_id][c_num] + '\n'
prompt += 'Translate the natural utterance into the SQL query: ' + question + '\n'
# 觸發 CoT 推理
prompt += "Let's think step by step." if args.cot else 'SELECT'
return prompt
實作層級可觀察的細節:
① Chat 模型用多輪 user/assistant 訊息傳遞 few-shot;Completion 模型則接成單一字串。
② CoT 模式下,把 shot['cot'](在 cot.py 預先生成的整段思維鏈)直接當作 assistant 回應。
③ Completion 模型在末端會強制塞入 "Let's think step by step."(CoT)或 "SELECT"(觸發 SQL 直接續寫);Chat 模型不需這招因為直接靠 role=assistant 接續。
所有格式都由 PromptMaker.__init__ 在啟動時預先 build 好快取在 self.db_prompts[db_id][c_num]。關鍵分支:
# util/prompt.py · __init__ 精簡版
for i in range(len(tabs)):
if args.api_doc:
# ===== Table(Column) 格式:OpenAI 官方文件風 =====
self.db_prompts[db_id][c_num] += f"# {tabs[i]}({', '.join([col[1] for col in cols if col[0] == i])})\n"
else:
# ===== Create(...) 系列:SQL DDL 風 =====
self.db_prompts[db_id][c_num] += f'create table {tabs[i]} (\n'
for j in range(len(cols)):
if cols[j][0] == i:
self.db_prompts[db_id][c_num] += f" {cols[j][1]} {db['column_types'][j]}"
if args.pf == 'eoc': # End of Column:PK/FK 放欄位末
if j in db['primary_keys']:
self.db_prompts[db_id][c_num] += ' primary key'
for fk in db['foreign_keys']:
if fk[0] == j:
self.db_prompts[db_id][c_num] += f' references {tabs[cols[fk[1]][0]]}({cols[fk[1]][1]})'
self.db_prompts[db_id][c_num] += ',\n'
if args.pf == 'eot': # End of Table:PK/FK 放整張表末
pks = [cols[pk][1] for pk in db['primary_keys'] if cols[pk][0] == i]
if len(pks) > 0:
self.db_prompts[db_id][c_num] += f" primary key ({', '.join(pks)}),\n"
for fk in db['foreign_keys']:
if cols[fk[0]][0] == i:
self.db_prompts[db_id][c_num] += f' foreign key ({cols[fk[0]][1]}) references ...,\n'
self.db_prompts[db_id][c_num] = self.db_prompts[db_id][c_num][:-2] + '\n)\n'
# ===== 附加 DB 範例資料列(c_num 列,論文設 3) =====
if c_num > 0 and os.path.exists(db_path):
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
db_contents = cursor.execute(f'SELECT * FROM {tabs[i]} LIMIT {c_num}').fetchall()
self.db_prompts[db_id][c_num] += '/*\n'
self.db_prompts[db_id][c_num] += f"{len(db_contents)} example rows from table {tabs[i]}:\n"
self.db_prompts[db_id][c_num] += '\t'.join([col[1] for col in cols if col[0] == i]) + '\n'
for record in db_contents:
self.db_prompts[db_id][c_num] += '\t'.join([str(record[col[1]]) for col in cols if col[0] == i]) + '\n'
self.db_prompts[db_id][c_num] += '*/\n'
對應的 CLI flag:
| 論文格式 | --api_doc | --pf |
|---|---|---|
| Table(Column) | True | no |
| Table(Column)(PF) | True | eoc / eot |
| Create(NoPF) | False | no |
| Create(EoC) | False | eoc |
| Create(EoT) ★ 最佳 | False | eot |
cot.py 最核心邏輯這段把論文「自動生成思維鏈」的演算法落地:拿訓練樣本(schema + 問句 + 正解 SQL),對 SQL 中每個表/欄/值找出最相關的問句切片並拼接成 CoT。
# cot.py · 核心迴圈(已精簡註解)
for i, example in enumerate(dataset):
if 'cot' in example: continue
# ① 切出問句所有 (i,j) 連續切片並編碼
words = word_tokenize(example['question'])
phrases = [' '.join(words[a:b]) for a,b in combinations(range(len(words)+1), 2)]
phrase_encodings = sentence_encoder.encode(phrases, normalize_embeddings=True, ...)
# ② 從正解 SQL 抓出涉及的表、欄、值
tables = get_tables_in_sql (example['sql'], dbs[example['db_id']])
columns = get_columns_in_sql(example['sql'], dbs[example['db_id']])
values = get_values_in_sql (example['sql'])
# ③ 已被欄位涵蓋的表不重複列出(論文有提到)
schema_items = []
for table in tables:
for column in columns:
if table in column: break
else:
schema_items.append((table, 'table'))
for column in columns:
schema_items.append((column, 'column'))
# ④ 對每個 schema 項目,找相似度最高的問句切片
schema_linkings = {}
for schema_item in schema_items:
encoding = sentence_encoder.encode(schema_item[0], ...)
scores = util.cos_sim(encoding, phrase_encodings).squeeze(0).tolist()
phrase = phrases[max(enumerate(scores), key=lambda x: x[1])[0]]
if phrase not in schema_linkings:
schema_linkings[phrase] = {'table': [], 'column': []}
schema_linkings[phrase][schema_item[1]].append(schema_item[0])
# ⑤ 拼接 CoT 字串(這就是 prompt 裡 assistant 回應的格式!)
example['cot'] = "Let's think step by step.\n"
for phrase in schema_linkings:
example['cot'] += f'According to "{phrase}",'
if schema_linkings[phrase]['table']:
example['cot'] += f' tables [{", ".join(schema_linkings[phrase]["table"])}]'
if schema_linkings[phrase]['column']:
if example['cot'].endswith(']'):
example['cot'] += ' and'
example['cot'] += f' columns [{", ".join(schema_linkings[phrase]["column"])}]'
example['cot'] += ' may be used.\n'
if values:
example['cot'] += f'Values [{", ".join(values)}] may be used.\n'
example['cot'] += 'So the final answer is:\n'
example['cot'] += ' '.join(example['query'].strip('\t ;').split())
CoT 模板就這四種句型:
• Let's think step by step.
• According to "{phrase}", tables [...] and columns [...] may be used.
• Values [...] may be used.(若 SQL 含值)
• So the final answer is: {完整 SQL}
沒有 LLM 介入生成 CoT —— 整段是純規則 + sentence embedding 拼出來的字串,這是「1 次 API 呼叫」省錢的關鍵:CoT 在訓練前就離線預先生成好,存在 train.json 的 cot 欄位裡,推論時直接當 assistant 範例貼上去。
get_prompt_remove_dependency()SParC / CoSQL 上把多輪對話「壓平」成單輪問句的兩階段法,第一階段用這個 prompt:
# util/prompt.py · get_prompt_remove_dependency()
@staticmethod
def get_prompt_remove_dependency(gpt, questions, shots):
def preprocess(q_list):
result = ''
for i, q in enumerate(q_list):
result += str(i + 1) + '. ' + q + '\n'
return result.strip()
if gpt in GPT_CHAT_MODELS:
prompt = [{'role': 'system',
'content': 'Given the list of questions, you need to '
'rewrite them to remove the context dependency.'}]
for shot in shots: # 論文:每個資料集人工標 10 個改寫範例
prompt.append({'role': 'user', 'content': preprocess(shot['q_multiturn'])})
prompt.append({'role': 'assistant', 'content': preprocess(shot['q'])})
prompt.append({'role': 'user', 'content': preprocess(questions)})
return prompt
原始多輪問句以 "1. ... \n2. ... \n3. ..." 編號丟給 LLM,期望它輸出同樣編號但去掉上下文依賴的獨立問句版本。出錯就是這裡出錯(論文 Table 10 案例)—— LLM 偶爾沒消除依賴或漏資訊。
把上面所有片段組合,下面是 GPT-3.5-turbo 在 zero-shot + Create(EoT) + 3 列範例下會看到的訊息陣列:
[
{
"role": "system",
"content": "Given the database schema, you need to translate the question into the SQL query."
},
{
"role": "user",
"content": "Database schema:\ncreate table stadium (\n Stadium_ID number,\n Location text,\n Name text,\n ...\n primary key (Stadium_ID)\n)\n/*\n3 example rows from table stadium:\nStadium_ID Location Name ...\n1 Raith Rovers Stark's Park ...\n2 Ayr United Somerset Park ...\n3 East Fife Bayview Stadium...\n*/\ncreate table singer (\n ...\n)\n...\nQuestion: How many singers do we have?"
}
]
Few-shot CoT 模式下,system 後會多出 2(static) + 2(dynamic) = 4 組 (user / assistant) 對話;每組 assistant 內容就是 cot.py 預先生成的那段「Let's think step by step. ...」字串。
總結 ACT-SQL 的 prompt 哲學: 不靠精雕細琢的指令、不靠多次 API call,而是把工夫花在「把好的範例放對位置」 —— 靜態+動態混合範例提供格式參照,自動生成的 CoT 提供「schema linking → 寫 SQL」的思考軌跡。整個 prompt 簡單到可以用一頁程式碼產生完。
① 範例選擇策略偏簡單:靜態/動態範例數 ns、nd 是超參數,仍需人工決定;混合策略本身相對樸素,有改進空間。
② 穩健性變體成績仍偏弱:在部分 Spider 穩健性變體上分數相對不佳,值得後續針對性探索。
③ 多輪表現是主要待解題:兩階段「改寫 → 單輪」會因改寫品質不佳造成 schema linking 連鎖錯誤。改善 LLM 在多輪 text-to-SQL 的表現,是作者明確點名的挑戰性未來工作 —— 本文僅完成初步探索。
ACT-SQL 用「自動產生、形似 schema linking 的思維鏈」,在單次 API 呼叫、零人工標註的前提下,於 Spider dev set 取得脈絡學習方法的 SOTA,並把方法初步延伸到多輪場景 —— 兼顧效能、速度與成本。