对应:Stanford CS224V Fall 2026 Lecture 9(10/26 A Structured / Unstructured Query Language)。 关于材料来源:2026 年这次课的幻灯片尚未公开。本讲依据:2025 年同主题的幻灯片(Lam & Liu,SUQL: Structured/Unstructured Query Language);SUQL 论文(Liu 等,Findings of NAACL 2024,arXiv 2311.09818);课程表的描述(第一个把信息检索和数据库查询结合起来的查询语言;语言设计;评估方法)。 配套代码:
code/lecture07/(只用 Python 标准库;05 号文件可选,需要anthropic包和 API 密钥) 预计阅读时间:3 小时左右(含运行代码)
0. 本讲导航
0.1 学完本讲,你应该能够
- 说明为什么”把文本变成表格”或”把表格变成文本”都会损失能力,以及 SUQL 为什么选择在 SQL 上加两个函数。
- 写出 SUQL 查询:
answer()和summary()可以出现在 SELECT、WHERE、聚合、ORDER BY 里,回答 HybridQA 的六类问题。 - 解释 SUQL 编译器的三个优化(谓词排序、检索排序、惰性求值),以及”相似 ≠ 满足条件”为什么要求检索之后再让 LLM 判断。
- 设计一个基于 SUQL 的对话智能体:输入分类、语义解析、执行、“把查询读回给用户”、“没有结果”的回复。
- 在没有标准答案的真实任务上评估一个查询系统(精确率、假阴性分析),并解释为什么 HybridQA 上”没有完全匹配”的题有一大半不是系统的错。
0.2 先修知识
| 知识点 | 用在哪里 | 最低要求 |
|---|---|---|
| 第 6 讲 | 全讲 | 语义解析、关系代数、大枚举类型与 classify、混合数据的四种以往做法 |
| 第 5 讲 | 第 3 节 | 稠密检索;“检索到的不一定支持答案”;看错误 |
| 第 3 讲 | 第 3、4 节 | 工具调用;函数调用式的智能体;LLM 的输出要校验 |
| 第 1 讲 | 第 3 节 | “核查是一个过滤器”;模拟 LLM 的出错率 |
| SQL | 全讲 | SELECT … FROM … WHERE … ORDER BY … LIMIT、MAX()、IN (…)、OR 和括号 |
0.3 本讲地图
第1节 回顾与动机:混合数据需要"任意组合"
第2节 SUQL 语言:answer() 和 summary() ← 代码 01
第3节 编译器:谓词排序、检索排序、惰性求值 ← 代码 02
第4节 基于 SUQL 的对话智能体 ← 代码 03、05
第5节 评估一:真实的餐厅问题(Yelp) ← 代码 03
第6节 评估二:HybridQA,以及"分数低不一定是系统错了" ← 代码 04
第7节 讨论与边界
第8–12节 小结、与项目的关系、练习、延伸阅读、下一讲预告
0.4 本讲的玩具世界
继续用第 6 讲的 600 家虚构餐厅(restaurant_world.py),做两处改动(suql.py 的 make_suql_world,只在本讲的进程里生效):
- 给
restaurants表加一列reviews:这家餐厅的全部评论拼成的一段文字。论文的表结构里,评论是餐厅的一列(reviews TEXT[])。 - 加一个隐藏特征”停车方便”(15% 的餐厅)。第 6 讲的评论里本来就有一句闲话 “Parking can be tricky.”(停车不方便),两者放在一起,用来演示”相似 ≠ 满足条件”(第 3.4 节)。
answer() 和 summary() 由一个模拟的 LLM 执行(SimLLM)。它认得各种说法(“our children loved it” 就是”适合带孩子”),也有一点常识(cacio e pepe 是一种 pasta),但会出错:文字提到了相关的话题时,有 3% 的概率判断错;根本没提时,出错的概率只有 0.3%。
关于本讲的数字:幻灯片和论文里的真实数字会注明出处;模拟实验的数字都来自 01
04 号代码的实际输出(固定随机种子,每个脚本 12 秒)。模拟 LLM 的出错率、模拟耗时(每次answer()1 秒、summary()2 秒)、解析器的出错率都是设定的参数。05 号代码需要 API 密钥,编写讲义时没有用真实的 API 运行过。
1. 回顾与动机:混合数据需要”任意组合”
1.1 第 6 讲留下的问题
第 6 讲的结论(幻灯片第 2 页的小结):
- 很多问题同时需要结构化数据和自由文本。“Palo Alto 适合带孩子的餐厅”——地点在数据库的一列里,“适合带孩子”只出现在评论里。论文的统计:众包来的 100 个餐厅问题里,55 个需要两者;对话里 62 个查找餐厅的轮次中,25 个需要两者;合起来超过 49%。
- 以往的做法都不够:“结构或文本”(先分类、事后挑一个)每次只用一边;“把结构变成文本”(线性化)丢掉了 SQL 的计算能力;“两边各检索一跳再合并”只能组合一次。
幻灯片的结论只有一句:我们需要任意地组合两种来源的答案。
1.2 两个目标
幻灯片给处理混合数据定了两个目标:
- 最大化检索能力:文本保持为文本,知识库保持为知识库。把知识库转成文本(线性化),就不能再做排序、比较、计数;把文本转成表格(抽取成固定的列),就只能回答事先想到的那几个问题——“评论里有没有提到停车方便”这一列,谁会事先建好?幻灯片的说法是:任何一种转换都是”取最小公分母”,两边的长处都丢了。
- 混合多跳:检索和数据库访问可以任意组合。对文本用高效的检索(向量相似度);对表格用关系代数(聚合、排序……);两者的结果可以互相作为对方的输入,次数不限。
1.3 小测验:怎样支持”完全的组合”?
想一想你学过的东西里,什么具有”任意组合”的能力?编程语言。函数的结果可以作为另一个函数的参数,表达式可以无限嵌套。SQL 也是这样:子查询可以嵌套子查询,Codd(1972)证明了 SQL 对关系代数是完备的——任何关系代数的组合都能写成 SQL。
所以 SUQL 的答案是:发明一种语言。但不是从头发明,而是在 SQL 上加两个函数。为什么是 SQL?
- LLM 熟悉 SQL。网上有海量的 SQL 代码,LLM 写 SQL 的能力很强;新发明的语言它没见过,要靠大量示例才能学会。
- 组合能力是现成的。SQL 的语法规则本身就允许无限嵌套;只要新加的函数能出现在”任何能写一个文本值的地方”,它就自动获得了全部组合能力。
- 把问题分解开(幻灯片第 6 页)。和第 6 讲的 Text-to-SQL 一样,系统分成两半:LLM 语义解析器只负责把自然语言翻译成 SUQL;优化编译器负责高效地执行它。LLM 不需要知道怎样执行才快,编译器也不需要理解自然语言。
这又是第 1 讲”计算思维”的一个例子:把 LLM 不擅长的部分(大规模的过滤、排序、计数,执行的优化)交给程序,只把它擅长的部分(理解自然语言、读一段文字)留给它。
1.4 开发者需要提供什么?
基于 SUQL 的 Genie 智能体框架(幻灯片第 7 页),开发者只需要提供:
- 数据库的表结构(其中有些列是自由文本,例如
reviews TEXT[]、popular_dishes TEXT[]); - 自由文本本身(SUQL 框架会自动为它们建向量索引);
- 少样本示例(可选),例如:
User: Show me a family-friendly restaurant that has burgers
Target: SELECT *, summary(reviews), answer(reviews, 'is this restaurant family-friendly?') FROM restaurants
WHERE answer(reviews, 'do you find this restaurant to be family-friendly?') = 'Yes'
AND answer(popular_dishes, 'does this restaurant serve burgers') = 'Yes' LIMIT 1;
不需要为每个领域写代码——领域由表结构定义。
2. SUQL 语言:answer() 和 summary()
2.1 两个函数
| 函数 | 含义 | 例子 |
|---|---|---|
answer(t, q) | 读文本 t(一段文字或一组文字),回答问题 q,返回一个文本值 | answer(reviews, 'is this restaurant family-friendly?') |
summary(t) | 返回 t 的摘要 | summary(reviews) |
summary(t) 其实是 answer(t, 'what is the summary of this document?') 的”语法糖”——answer 已经是一个万能的函数,只要问对问题,就能从文本里取出任何信息。单独提供 summary 是为了方便:解析器很容易学会用它。
关键的设计:这两个函数的结果是普通的文本值,可以出现在任何允许文本值的地方——和数字运算符可以出现在任何允许数字的地方一样。于是:
-- "I want a family-friendly restaurant in Palo Alto"
SELECT *, summary(reviews) FROM restaurants
WHERE location = 'Palo Alto' AND answer(reviews, 'is it a family friendly restaurant?') = 'Yes'
LIMIT 1;
answer(…) = 'Yes' 就是一个普通的过滤条件,可以和 location = 'Palo Alto' 用 AND 连起来。
小测验:怎样实现 answer 和 summary? 最直接的办法:把这一行的文本和问题一起放进一个提示,让 LLM 回答。文本很长时(一家餐厅有 20 条评论),先用检索取出和问题最相关的几段,再交给 LLM。这正是第 5 讲 WikiChat 的”检索 + 阅读”,只是被封装成了一个函数。
2.2 例子:餐厅、笔记本电脑
幻灯片上的两个对话例子:
-- "Do you have a recommendation for a first date restaurant in Palo Alto? We're thinking sushi but not sure what's good around here."
SELECT *, summary(reviews) FROM restaurants
WHERE 'sushi' = ANY (cuisines) AND location = 'Palo Alto' AND rating >= 4.0
AND answer(reviews, 'is this restaurant good for a first date?') = 'Yes'
ORDER BY num_reviews DESC LIMIT 1;
-- "I need a laptop with a Thunderbolt 3 port and at least 16GB RAM for my workstation setup."
SELECT *, summary(reviews) FROM laptops
WHERE ram >= 16 AND (answer(about, 'does this laptop have Thunderbolt 3?') = 'Yes'
OR answer(description, 'does this laptop have Thunderbolt 3?') = 'Yes')
LIMIT 3;
第二个例子说明了一个实际的问题:答案在哪一段文字里往往不清楚(产品简介?详细描述?评论?),所以把所有可能相关的文本列都查一遍,用 OR 连起来。这个技巧在第 6 节会再出现。
代码 01 的演示 1 在本讲的餐厅数据库上执行了幻灯片的三条 SUQL(菜系写成本讲的 has(cuisines, 'sushi bars')),并用一个不会出错的 LLM 逐行执行同一个 WHERE,得到”标准答案”来核对:
| 查询 | 结果 | LLM 调用 | 核对 |
|---|---|---|---|
| Palo Alto 适合带孩子的餐厅 | Golden Fig Grill | answer 2 次,summary 1 次 | ✓(全库满足条件的 21 家) |
| Palo Alto、寿司、评分 ≥ 4.0、适合第一次约会,按评论数排序 | Jade Dragon Grill(4.5 分,1972 条评论) | answer 2 次 | ✓(满足条件的只有 1 家) |
| 适合带孩子、有汉堡(没有结构化条件) | Golden Bowl Grill(招牌菜 double cheeseburger) | answer 60 次 | ✓(满足条件的 5 家) |
前两条只调用了 2 次 LLM(全库 600 家餐厅)——编译器先做了结构化过滤,第 3 节细讲。第三条没有任何结构化条件,两个条件都要读文本,又很少有餐厅同时满足(5 家),所以调用多一些。
2.3 answer() 可以写在哪里?——HybridQA 的六类问题
HybridQA(第 6 节)的每道题有一张维基百科的表格,表格的单元格链接到维基百科的段落。SUQL 把链接的段落存进同名的 _Info 列。下面这张表(虚构国家 Veloria 的历届奥运旗手;奥运会的届次和举办城市是真实的):
CREATE TABLE Flag ("Name" TEXT, "Event year" TEXT, "Event year_Info" TEXT,
"Flag Bearer" TEXT, "Flag Bearer_Info" TEXT, "Gender" TEXT);
-- "Event year_Info" :这届奥运会的介绍("... were held in Rio de Janeiro, Brazil.")
-- "Flag Bearer_Info" :旗手的介绍("Lina Varga (born on 1994-08-09) ... competed in the women's 400 m hurdles event.")
HybridQA 论文把问题分成六类(T = 表格,P = 段落)。幻灯片把每一类写成了 SUQL(代码 01 的演示 2 逐条执行):
| 类型 | 问题 | SUQL | answer() 在哪里 | 结果 | LLM 调用 |
|---|---|---|---|---|---|
| I(T→P) | XXXI 届奥运会在哪里举办? | SELECT answer("Event year_Info", 'where is this event held?') FROM Flag WHERE "Name" = 'XXXI' | SELECT | Rio de Janeiro | 1 |
| II(P→T) | 在 Rio de Janeiro 举办的是哪一届? | SELECT "Name" FROM Flag WHERE answer("Event year_Info", 'where is this event held?') = 'Rio de Janeiro' | WHERE | XXXI | 5 |
| III(P→T→P) | Rio 那届的旗手是哪天出生的? | SELECT answer("Flag Bearer_Info", 'when is this person born?') FROM Flag WHERE answer("Event year_Info", …) = 'Rio de Janeiro' | 两处 | 1994-08-09 | 6 |
| IV(T&P) | 哪位男旗手参加了男子 100 公斤级? | … WHERE "Gender" = 'Male' AND answer("Flag Bearer_Info", 'what event did this person compete in?') = 'men''s 100 kg event' | WHERE + 结构化条件 | Andros Kell、Darin Moskel | 3 |
| V(比较) | 2012 年和 2016 年两届,年轻的那位旗手哪天出生? | SELECT MAX(answer("Flag Bearer_Info", 'when is this person born?')) FROM Flag WHERE "Event year" IN ('2016', '2012') | 聚合函数里 | 1994-08-09 | 2 |
| VI(最高级) | 最年轻的旗手是哪一年举旗的? | SELECT "Event year" FROM Flag ORDER BY answer("Flag Bearer_Info", 'when is this person born?') DESC LIMIT 1 | ORDER BY | 2016 | 5 |
几点观察:
- 同一个函数,写在不同的位置,就回答了不同类型的问题。不需要为每种问题设计一个模块——这就是”组合”的力量。
- 多跳检索被自然地包含了。类型 III 是”段落 → 表格 → 段落”:先从段落里找到举办城市,用它在表格里定位到那一行,再从另一段落里取出生日。NLP 文献里专门研究的”多跳检索”,在 SUQL 里只是一个普通的嵌套。
- 调用次数取决于
answer()在哪里。类型 I 先用表格找到行,只读一段;类型 II 要读每一行的段落才知道哪一届在 Rio;类型 IV 先用Gender = 'Male'筛掉一半,只读 3 段;类型 VI 必须读每一行——不知道每个人的生日,就没法排序。 - 论文用 PostgreSQL 的
::date把answer()的结果转成日期再比较;本讲把日期写成YYYY-MM-DD,按字符串比较和按日期比较的结果一样。
2.4 LLM 的常识 vs 字符串匹配
“Palo Alto 有卖 pasta 的地方吗?“——数据库的招牌菜一栏写的是 “cacio e pepe”(一种罗马的意大利面),没有 “pasta” 这个词。代码 01 的演示 3:
- SQL 的
popular_dishes LIKE '%pasta%':0 家; - SUQL 的
answer(popular_dishes, 'does this restaurant serve pasta?') = 'Yes':10 家(LLM 知道 cacio e pepe 是 pasta),Palo Alto 的 95 家每家问一次。
这就是”文本保持为文本”的意义:文本的含义不能靠字符串匹配来判断。第 6 讲的 classify 解决的是同一类问题在枚举类型上的版本(SUQL 论文第 5.2 节:取值不超过 10 个的枚举直接写进提示;更多的,把 = 重载成 classify)。
2.5 组合与可解释性
代码 01 的演示 4:“Mountain View 适合约会的地方——浪漫,或者景色好,按评分排前 3”:
SELECT name, rating FROM restaurants WHERE location = 'Mountain View' AND
(answer(reviews, 'is it romantic?') = 'Yes' OR answer(reviews, 'does it have a great view?') = 'Yes')
ORDER BY rating DESC LIMIT 3
编译器先把 WHERE 化成析取范式(DNF,“或的与”):
(location = 'Mountain View' AND answer(…romantic…) = 'Yes')
OR
(location = 'Mountain View' AND answer(…view…) = 'Yes')
每个”与”子句可以单独决定先算哪个条件(第 3 节)。结果是 Happy Kitchen Bistro(5.0 分)、Lucky Pearl Eatery、Green Table,LLM 调用 22 次(Mountain View 一共 101 家)。
SUQL 是形式化的,所以可以读回成自然语言:
I searched for restaurants in Mountain View, (where the reviews say it is romantic or where the reviews say it has a great view), sorted by rating.
用户看到这句话,就知道智能体是怎样理解他的问题的。第 4 节会说明,这是对付语义解析错误的一道重要防线。
2.6 SUQL 的优点,以及一个小测验
幻灯片总结的优点:
- 形式化地表示了检索和数据库查询的组合:简洁、表达能力强、与领域无关;
- 完全的组合能力:检索和关系代数可以任意组合;
- 可解释:可以把查询读回给用户;
- 是一种高级语言:怎样执行才快,交给编译器,写查询的人(和 LLM)不用操心。
小测验:给定一个形式化的查询,答案一定是对的吗? 数据库的部分是对的——SQL 的语义是精确的。但自由文本的部分可能出错:检索可能没找到相关的段落(检索错误),LLM 可能读错(回答错误)。代码 01 的模拟 LLM 有 3% 的出错率;第 5、6 节的评估里,这一类错误一直存在。SUQL 把错误局限在了 answer() 里——它出错时,你知道去哪里找原因。
2.7 SUQL 是新东西,怎样评估它?
幻灯片列了要回答的问题:
- 准确率:语义解析器的准确率(能不能写对 SUQL)?执行的准确率(执行结果对不对)?
- 速度:调用 LLM 很慢、很贵,执行一条查询要多久?
第 3 节讲速度,第 5、6 节讲准确率。
3. 编译器:谓词排序、检索排序、惰性求值
3.1 SQL 不加修改就能执行 SUQL——但很慢
answer 和 summary 只是两个外部函数。任何 SQL 引擎都允许注册自定义函数——本讲就是用 SQLite 的 create_function 注册的。所以 SUQL 程序可以被 SQL 引擎原样执行。
问题是 SQL 引擎不知道 answer() 很贵:location = 'Palo Alto' 几乎不花时间,而 answer() 要调用一次 LLM(约 1 秒,要花钱)。幻灯片第 29 页给了一个”朴素实现”:
- 取出每一行的评论;
- 对每一行调用 LLM 回答问题;
- 按地点过滤;
- 对全部通过的行生成摘要;
- 报告前几个。
小测验:怎样优化这个查询?
3.2 三个优化
1. 谓词排序(predicate ordering)。结构化的条件便宜得多,永远先算。编译器把 WHERE 化成 DNF,在每个”与”子句里先执行不含 answer() 的条件,只对剩下的行调用 LLM。
2. 只返回需要的结果。对推荐这类应用,用户不需要全部答案。用检索模型(向量相似度)给候选行排序,最可能满足条件的先交给 LLM。有多个文本条件时,论文用一个”聚合的相似度”:
其中 是一个文本条件(例如”适合带孩子吗?”), 是这一行里的一段文字(一条评论)。:一家餐厅有很多条评论,只要有一条提到就够了;:同时和几个条件都像的行排在前面。
3. 惰性求值(lazy evaluation)。只在需要的时候才计算——有 LIMIT 1 时,凑够 1 行就停,后面的行不再调用 LLM。惰性求值是编程语言里由来已久的概念(Hudak 1989)。
注意惰性求值什么时候是正确的:候选行必须按最终的顺序检查。查询有 ORDER BY rating DESC 时,按评分从高到低检查,先选中的行一定排在后选中的行前面,凑够了就可以停。如果 ORDER BY 里有 answer()(例如 2.3 节的类型 VI),就不能惰性求值——不读完每一行,不知道谁排第一。
3.3 一条查询、六种执行方式
代码 02 的演示 1 用幻灯片的例子,并且故意把 answer() 写在 location 前面(语义解析器完全可能这样写):
SELECT name, summary(reviews) FROM restaurants
WHERE answer(reviews, 'is it family-friendly?') = 'Yes' AND location = 'Palo Alto' LIMIT 1
| 执行方式 | answer | summary | 模拟耗时 | 结果 |
|---|---|---|---|---|
| A 朴素实现(幻灯片:先逐行问 LLM) | 600 | 22 | 644 秒 | ✓ |
| B SQLite 原样执行 | 50 | 1 | 52 秒 | ✓ |
| B′ SQLite 原样执行(条件换个顺序写) | 4 | 1 | 6 秒 | ✓ |
| C 编译器:谓词排序 | 95 | 1 | 97 秒 | ✓ |
| D 编译器:排序 + 惰性求值 | 4 | 1 | 6 秒 | ✓ |
| E 编译器:排序 + 惰性求值 + 检索排序 | 1 | 1 | 3 秒 | ✓ |
几点值得细看:
- A 的 22 次
summary:朴素实现先给全部通过的 22 行写摘要(21 家真的适合带孩子,加上一家被 LLM 误判的),最后才取 1 行。 - B 只调用了 50 次,不是 600 次:SQLite 按书写顺序求值、遇到 AND 的第一个 False 就短路,而且凑够
LIMIT就停止扫描——它自带了”惰性求值”。但它不会重排条件:answer()写在前面,排在第一家 Palo Alto 适合带孩子的餐厅之前的 49 家(大部分不在 Palo Alto)都被问了一遍。B′ 只是把两个条件换了个顺序,就降到 4 次。 - 编译器(C~E)不依赖书写顺序:不管解析器怎么写,都先做结构化过滤。C 没有惰性求值,把 Palo Alto 的 95 家全问了一遍;D 凑够 1 行就停;E 先问检索得分最高的那家——第一家就满足。
3.4 60 条查询的平均
代码 02 的演示 2 生成了 60 条随机的 SUQL(一个地点;一半带菜系;一两个文本条件;40% 是 LIMIT 1,30% 是 LIMIT 3,30% 是”列出全部”;一半把 answer() 写在前面):
| 执行方式 | answer/条 | summary/条 | 模拟耗时/条 | 精确率 | 召回率* |
|---|---|---|---|---|---|
| A 朴素实现 | 623.6 | 5.5 | 634.5 秒 | 0.98 | 0.96 |
| B SQLite 原样执行 | 196.8 | 1.2 | 199.2 秒 | 0.98 | 0.96 |
| C 谓词排序 | 66.9 | 1.2 | 69.3 秒 | 0.98 | 0.96 |
| D 排序 + 惰性 | 33.9 | 1.0 | 35.9 秒 | 0.98 | 0.96 |
| E 排序 + 惰性 + 检索 | 30.4 | 1.0 | 32.5 秒 | 0.98 | 0.96 |
| F E + 只检查检索得分最高的 20 行 | 12.7 | 1.0 | 14.8 秒 | 0.98 | 0.92 |
* 召回率只对”列出全部”的查询计算。
- 谓词排序省掉了约 2/3 的调用(相对于 B),惰性求值又省掉一半。
- 检索排序在这组查询上只多省了一点:对”列出全部”的查询,每个候选都要问,顺序无关紧要;它的作用主要在”只要一两家”的查询上。
- 精确率在各种方式之间完全一样:执行顺序不改变每一行的判断,只改变”问了哪些行、问了几行”。精确率由 LLM 自己的判断错误决定。
- F 给检索设了上限:更快,但”列出全部”的查询漏掉了一些答案(召回率 0.96 → 0.92)——检索得分排在后面的行,也可能满足条件。这是速度和完整性的权衡(练习 2)。
小测验:这些优化有必要吗? 算一笔账:论文的 Yelp 数据有 1,828 家餐厅、每家 20 条评论。朴素实现每个问题要调用上千次 LLM,按每次 1 秒算,用户要等半个多小时。对一个对话机器人来说,没有这些优化就根本不能用。
3.5 相似 ≠ 满足条件
检索排序让人很想再进一步:既然检索已经把最像的行排在前面,直接把前几行当答案,不就不用调用 LLM 了吗?
论文第 5.1 节的例子回答了这个问题:用户要”停车方便”的餐厅,一条评论写着”停车很难”。如果没有别的评论提到停车,这条评论的相似度会最高——它和问题说的是同一件事,只是结论相反。代码 02 的演示 3:
| 句子 | 和 “is parking easy?” 的相似度 |
|---|---|
| Parking can be tricky.(停车不方便) | 0.50 |
| Parking was a breeze.(停车很方便) | 0.50 |
| Plenty of free parking out front.(停车很方便) | 0.50 |
| Easy to park in their lot.(停车很方便) | 1.00 |
| Great place to bring the kids. | 0.00 |
- 评论里提到停车的(检索得分 > 0)有 249 家,其中真正停车方便的只有 68 家:只靠检索,精确率 27%;
- 检索之后再让 LLM 判断,保留 74 家,其中 68 家真的停车方便:精确率 92%(代价是 249 次调用)。
所以 SUQL 编译器用检索排序候选行,但每一行仍然要由 LLM 执行完整的条件 answer(reviews, 'is parking easy?') = 'Yes'。相似度回答的是”说的是不是同一件事”,LLM 回答的是”说的是什么”。第 5 讲讲过同一个道理:检索到的段落不一定支持答案。
3.6 编译器怎样实现?
幻灯片第 33~34 页:SUQL 编译器是修改 SQL 编译器实现的,以支持 SQL 和 summary / answer 的任意组合:
- 递归地处理 SQL 的语法树:从没有子查询的最底层开始,向上处理;
- 对每一个含有
answer的 SELECT 语句,应用上面的优化,把结果存进一张临时表; - 把这条语句替换成”从临时表里 SELECT”;
- 最后的处理(排序、投影、聚合)交给 SQL 编译器。
本讲的 SUQLCompiler 是一个简化版:只处理单表、没有子查询的查询;选中的行由 SQL 完成最后的排序和投影(summary() 只对最终输出的行计算)。
3.7 小测验:用”函数调用”的智能体不行吗?
幻灯片的另一个小测验:如果用一个函数调用式的智能体(第 3 讲),让它分别调用检索和数据库,会怎样?
代码 02 的演示 4 实现了这样一个智能体:它有 search_reviews(问题, k) 和 run_sql(条件) 两个工具,先检索评论取前 k 家,再用 SQL 过滤地点和菜系,最后逐家确认。在 42 条带 LIMIT 的查询上(返回至少一家正确的餐厅就算成功):
| 做法 | 成功率 | answer/条 |
|---|---|---|
| 函数调用:检索前 10 家 → SQL 过滤 | 57% | 1.7 |
| 函数调用:检索前 30 家 → SQL 过滤 | 74% | 4.0 |
| 函数调用:检索前 100 家 → SQL 过滤 | 95% | 11.9 |
| SUQL 编译器 | 98% | 11.9 |
问题出在检索在全库范围内进行,不知道用户只要 Mountain View 的。k 小了,前 k 家多半不在用户要的地点;k 大了,要确认的行多了(检索结果也要塞进智能体的上下文)。合适的 k 取决于数据库有多大、结构化条件有多严格——智能体事先不知道。这里只有 600 家餐厅,k = 100 已经是全库的 1/6;论文的 Yelp 数据有 1,828 家,真实的数据库还要大得多。
幻灯片的回答是:这些优化之所以可能,是因为编译器在优化整条查询。它看得见”先过滤地点、再在范围内检索”的机会;两个分开调用的工具看不见。当然,你可以给检索工具加一个”只在这些 id 里检索”的参数——那你就在一步一步地重新发明 SUQL。
4. 基于 SUQL 的对话智能体
4.1 从 SQL 智能体到 SUQL 智能体
第 6 讲的 SQL 智能体(幻灯片第 25 页):
用户输入 → 输入分类器 ─(不需要查数据库)─────────────────────────────► 回复生成
│(需要查数据库)
▼
语义解析器 → SQL → classify(枚举值)→ SQL 编译器 → 数据库 → 结果 → 回复生成
└─(没有结果)→ "没有结果"的回复
换成 SUQL 之后(幻灯片第 26 页),结构几乎不变,只多了两个由 LLM 实现的函数:answer 和 summary。整个系统是 7 个带少样本示例的提示词(用 jinja 模板写的)。
论文把对话状态追踪(第 10 讲会细讲)形式化地定义为:给定表结构 、之前各轮的智能体回复和用户输入(以及用户输入对应的 SUQL 查询),预测最新一轮用户输入对应的 SUQL 查询 (如果这一轮需要查询的话)。解析分两步,都用 LLM 加少样本提示实现:第一步判断要不要查知识库(问候、闲聊不需要);第二步生成 SUQL。
4.2 防御式编程:语义解析器错了怎么办?
解析器一定会犯错。论文的两条对策:
-
永远把查询读回给用户。智能体根据这一轮的 SUQL,明确告诉用户它查了什么(“I searched for Italian restaurants with a romantic atmosphere.”)。用户能从这句话里发现误解;即使没有结果,这句话也让用户知道该怎样换个说法。幻灯片上的例子:
I searched for 5-star restaurants in Sunnyvale that serve kids food. Unfortunately, I couldn’t find any search results. Is there anything else I can help you with?
-
没有结果时,明确告诉 LLM”没有找到”。论文特别指出:只要没有给 LLM 提供答案,它就倾向于编一个。所以”没有结果”必须作为一种单独的情况处理(第 5 讲的”我不知道”、第 6 讲的”没有结果”的回复)。
代码 03 的演示 5 给了四个例子:
[解析正确] 用户:Can you find a place in Mountain View romantic enough for a date?
智能体:I searched for restaurants in Mountain View, where the reviews say it is good for a date.
I found: Lucky Pearl Eatery, Old Lotus Bistro, Lucky Garden.
[解析出错] 用户:Any chinese restaurants in Mountain View that are open late?
SUQL:… AND opening_hours > '10pm' LIMIT 3
智能体:I searched for restaurants in Mountain View, serving chinese food, whose opening hours come
after '10pm' in alphabetical order. I found: Jade Fig, Green Lotus Bistro, Red Kitchen House.
[解析出错] 用户:Where can I get good pasta in Bernal Heights?
SUQL:… AND answer(reviews, 'does this restaurant serve pasta?') = 'Yes' LIMIT 3
智能体:I searched for restaurants in Bernal Heights, where the reviews say it serves pasta.
Unfortunately, I couldn't find any results. Is there anything else I can help with?
第二个例子里,营业时间是 '5pm-11pm' 这样的文字,> '10pm' 是按字母顺序比较的('5pm-11pm' > '10pm' 成立,因为字符 '5' 排在 '1' 后面),几乎每一行都满足——返回的三家不一定开到很晚。如实地读回查询,用户一眼就能看出问题。第三个例子里,解析器去评论里找菜品(菜品其实写在招牌菜一栏),读回的句子说明了它查的是”评论”。
5. 评估一:真实的餐厅问题(Yelp)
5.1 实验设置
论文的设置(幻灯片第 37~38 页,论文第 6.2 节):
- 真实的大数据集:Yelp 上 San Francisco、Palo Alto、Cupertino、Sunnyvale 四个城市的 1,828 家餐厅,每家前 20 条评论和前 20 个招牌菜;列:name、cuisines、price、rating、num_reviews、address、phone_number、opening_hours、location、reviews、popular_dishes。
- 组件:LLM 用 gpt-3.5-turbo;文本列用 COCO-DR(一个现成的稠密检索模型)建索引;一个新的、做优化的 SUQL 编译器。
- 用户问题:在 Prolific 上众包,不告诉工作者数据库里有哪些字段(免得问题被字段”带偏”):
- 只有问答:100 个关于餐厅的问题;
- 对话:20 段对话,96 轮(其中 62 轮在找餐厅)。
| 单轮 | 对话 | |
|---|---|---|
| 只需要结构化数据 | 45 | 37 |
| 需要结构化 + 文本 | 55 | 25 |
| 合计 | 100 | 62 |
5.2 没有标准答案时,怎样评估?
小测验:怎样评估它的准确率? 真实任务里很难标出标准答案:对一个问题,满足条件的餐厅可能有很多家。论文的做法:
- 人工逐家检查返回的每一家餐厅是否满足用户的全部条件;
- 指标是查询精确率(turn accuracy):所有问题返回的餐厅里,真阳性 / (真阳性 + 假阳性)。
基线是线性化 + 检索(Oguz 等 2022 的 UniK-QA 做法):把每家餐厅同一行的所有格子用逗号拼起来,用同一个稠密检索模型索引;对话时用少样本 LLM 根据对话历史写一个简短的检索查询,取最像的 m 家。
论文结果(表 5):
| 单轮 | 对话 | |
|---|---|---|
| 线性化 @1 | 57.0% | 63.4% |
| 线性化 @3 | 49.7% | 61.9% |
| SUQL(最多返回 3 家) | 93.8% | 90.3% |
幻灯片第 41 页的批注是:“基于向量的匹配不擅长多条件的查询”——“Palo Alto 的、意大利菜、浪漫的”三个条件压进一个向量,检索只能找到”大致像”的餐厅。
5.3 模拟实验
代码 03 按同样的比例生成了 100 个”用户问题”(45 个结构化、55 个混合),每个问题背后有一个真实意图(地点、菜系、价格、评分、开到很晚、特征、菜品),用隐藏的真实特征”人工检查”。模拟的解析器按设定的概率犯论文错误分析里的几类错误;线性化用第 1 讲的 BM25:
| 系统 | 结构化 | 混合 | 全部 |
|---|---|---|---|
| 线性化 @1 | 51.1% | 50.9% | 51.0% |
| 线性化 @3 | 40.7% | 40.0% | 40.3% |
| SUQL(最多 3 家) | 97.9% | 96.6% | 97.2% |
和论文的模式一致:线性化在结构化问题上就已经吃力——BM25 不会比较”评分 ≥ 4.5”,也不知道”开到很晚”指的是 11pm;混合问题上,它找到的是”提到这个词”的餐厅,不一定在用户要的地点。取前 3 家比取前 1 家更差:排在后面的更可能不满足条件。
5.4 没有返回结果的问题:假阴性分析
精确率只看返回了的餐厅,看不到”该返回却没返回”的情况。论文专门分析了这一类:
-
单轮的 100 个问题里,21 个没有返回结果,人工检查发现其中 7 个确实没有满足条件的餐厅;对话的 62 轮里,8 轮没有结果,其中 2 轮确实没有。所以假阴性率是单轮 14%、对话 9%。
-
幻灯片第 46 页给出了单轮 14 个假阴性的原因:
- 解析:2 个(一个语法错误,一个枚举字段混淆);
- 结构化查询:4 个”不支持的位置服务”(例如”离我近的”),2 个营业时间的错误,2 个菜品(在
popular_dishes里找,结果其实在评论里); - 自由文本:4 个
answer()(ChatGPT)的假阴性。
幻灯片在其中几类旁边标着”可以改进”。
代码 03 的演示 3(模拟):11 个问题没有返回结果,其中 8 个确实没有,3 个是假阴性(枚举值写错 1 个、answer() 漏判 1 个、菜品查错了列 1 个)。模拟的假阴性比论文少得多——模拟里没有”离我近的”这类数据库根本不支持的问题,解析器的出错率也是设定的;这里要看的是分析方法:把”没有结果”分成”真的没有”和”漏掉了”,再把”漏掉了”按原因归类,每一类对应一个改进方向。
5.5 召回率重要吗?
幻灯片第 44 页的小测验:答案找全了吗?——不全,完全不全。这样可以吗?
代码 03 的演示 4:有答案的 92 个问题里,满足条件的餐厅数中位数是 4 家,最多 27 家;智能体每次最多返回 3 家。对”推荐一家餐厅”这样的任务,用户要的是几个好的选择,不是完整的清单——返回 3 家正确的就够了,这也正是惰性求值能大量省钱的原因。但如果用户问”一共有几家?""全部列出来”,就必须执行完整的查询(去掉 LIMIT),召回率才重要(第 3.4 节的 F 方式就会在这里吃亏)。评估指标要和任务对齐。
5.6 用户的反馈
正面的(幻灯片第 47 页):“这个聊天机器人实际上没有什么我不喜欢的。如果它公开可用,我真的会经常用”;“回复很快、很详细,给出选项之后我几乎没有什么还想问的”;“推荐的餐厅好得让我吃惊……我问了一种菜系是什么,它也能解释。现在我饿了。”
负面的(第 48 页):偶尔很慢;没有链接和图片;不够友好,回复有时太长,用要点列出会更好。
注意负面反馈里没有一条是”它给了我错误的信息”——都是体验上的问题。这和第 5 讲 WikiChat 的用户研究很像:先把”不幻觉”做到,用户的关注点才会转到体验上。
6. 评估二:HybridQA,以及”分数低不一定是系统错了”
6.1 HybridQA 数据集
HybridQA(Chen 等 2020)基于维基百科(幻灯片第 51~52 页):
- 在 Amazon Mechanical Turk 上发了 13,000 个任务:每个任务给一张维基百科的表格和它链接的段落,要求写 6 个问题和答案;每个任务平均 12 分钟,报酬 2.3 美元;由 5 位 CS 研究生决定是否接受。最后得到约 7 万个问答对。
- 每个问题都必须同时用到表格和段落。
- 表格上的推理:按 =、>、< 过滤行(“第 XXXI 届”);对一列求最高级(“最早的一届”);在两个格子之间跳转;从表格里取信息。
- 文本上的推理:按提及选段落(“那位柔道运动员旗手”);从段落里抽取一段作为答案。
HybridQA 比 Yelp 难得多:每道题有自己的表结构,解析器没法为每张表准备示例——只能零样本:提示里只有这张表的表结构。
6.2 两个改进
初步的实验暴露了两个问题,于是有了两个改进(幻灯片第 54~55 页):
- 解析器很难判断该读哪一个文本列(一张表往往有好几个
_Info列)。改进:如果在原来的列里找不到答案,自动到其他文本列里找。 - 执行时有一个清楚的反馈信号:没有返回结果。改进:重新试一次(最多 2 次)。
完整的系统是三个简单的提示:解析(不到 10 个示例)→ 执行(没有结果就重新解析,最多 2 次)→ 用 LLM 把数据库的结果转成短答案(HybridQA 的标准答案都很短;标准答案只有一个实体,所以即使结果有多个,也只取一个)。论文用 GPT-4(gpt-4-1106-preview)解析和执行,用 GPT-3.5 做最后的转换。
6.3 论文结果
评估指标是原始文本上的完全匹配(EM)和 F1(论文表 3,节选):
| 方法 | 训练数据 | 开发集 EM / F1 | 测试集 EM / F1 |
|---|---|---|---|
| HYBRIDER(Chen 等 2020) | HybridQA(6.2 万) | 44.0 / 50.7 | 43.8 / 50.6 |
| MAFiD | 同上 | 66.2 / 74.1 | 65.4 / 73.6 |
| S³HQA(当时的最好成绩) | 同上 | 68.4 / 75.3 | 67.9 / 75.5 |
| LLaMA2 (7B) | 零样本 | 20.7 / – | – |
| TableLlama | TableInstruct(260 万) | 27.6 / – | – |
| HPROPRO(GPT-4) | 少样本 | 48.0 / 54.6(200 题) | 48.7 / 57.7 |
| SUQL | 少样本 | 59.3 / 68.3 | 59.0 / 68.4 |
- SUQL 超过了所有上下文学习(不训练)的方法,测试集上距离用 6.2 万个样本训练出来的最好成绩只差 8.9 个点 EM、7.1 个点 F1。
- 另一篇工作(Sui 等 2023)把整张表用不同的格式(JSON、HTML、Markdown……)交给 GPT-4,只在 1,000 道开发集题目上报告了结果,用的指标是”预测是标准答案的子串,或者反过来”,最好是 56.68%;用同样的指标,SUQL 在完整的开发集上是 72.5%。
- 最重要的一点(幻灯片第 56 页):HybridQA 的表格平均只有 4.4 列、15.7 行。最好的方法 S³HQA 等都依赖”把整张表(或整列)交给模型”,不能推广到大的表格;SUQL 的做法可以用于任意大小的表格。代码 04 的演示 4 算了一笔粗略的账:每行的格子加链接的段落约 180 个 token,16 行的表约 2,880 个 token,1,000 行约 18 万,10 万行约 1,800 万;SUQL 的解析提示里只有表结构(约 100 个 token),读文本的调用次数由结构化条件筛完之后剩下的行数决定。
6.4 错误分析:“真实的准确率”可能是 84.2%
幻灯片第 59 页的小测验:下一步该做什么?——看错误。论文随机抽了 72 个没有完全匹配的题:
| 类别 | 比例 |
|---|---|
| 评估的问题(61%) | |
| 格式不一致(预测 “Johnson City, Tenessee”,标准答案 “Johnson City”) | 37.5% |
| 标准答案错了或不完整(只有一个标准答案,其实应该有好几个) | 23.6% |
| 真正的错误(39%) | |
| 语义解析错误 | 22.2% |
| LLM 函数(answer、枚举分类)出错 | 11.1% |
| 类型转换错误(HybridQA 的表没有标注类型,SUQL 需要有类型的表结构) | 5.6% |
开发集 EM 是 59.3%,没有完全匹配的 40.7% 里只有 38.8% 是真正的错误,所以真实的准确率可能达到 84.2%()。
6.5 模拟实验
代码 04 生成了 50 张 HybridQA 式的小表(虚构国家的历届奥运旗手,8~16 行),每张出 6 道题(六类各一道),共 300 道。模拟的解析器按设定的概率选错文本列(15%)、写出查不到结果的条件(6%,例如把 'Male' 写成 'M')、犯语义错误(10%,例如”最年轻”写成”最年长”);answer() 有 6% 的概率抽错值(拿了段落里的另一个日期);“转成短答案”那一步有 30% 的概率换一种格式;标准答案有 4% 写错,有多个正确答案的题只写一个。
两个改进的消融(演示 2):
| 配置 | EM | F1 | 人工判断 |
|---|---|---|---|
| 基础 | 70.0% | 73.5% | 83.3% |
| + 改进 1:换文本列 | 75.7% | 79.3% | 91.3% |
| + 改进 2:没结果就重试 | 73.7% | 77.5% | 89.3% |
| 两个都加 | 76.3% | 80.2% | 92.7% |
- 改进 1 救回了”选错文本列”的题:在错的列里读不出答案,执行器自动换一列。注意它是在执行器里实现的(代码里把
answer(列, 问题)改写成answer_any(列, 其他列, 问题)),解析器不需要知道。 - 改进 2 救回了”查不到结果”的题:重新采样一次,多半就写对了。选错文本列也会导致查不到结果,所以改进 2 也能救回一部分这类题;两个改进有重叠,加在一起的收益小于各自收益之和。
- EM 和 F1 一直比人工判断低十几个点。
没有完全匹配的题,错在哪里?(演示 3,两个改进都加):
| 类别 | 题数 | 比例 |
|---|---|---|
| 格式不一致(答案其实对了) | 27 | 38% |
| 标准答案错了或不完整 | 22 | 31% |
| 系统:语义解析错误 | 11 | 15% |
系统:answer() 抽取出错 | 11 | 15% |
没有完全匹配的 71 题里,49 题(69%)是评估本身的问题;把它们算作答对,“真实的准确率”是 92.7%。模拟的比例和论文的 61% 相近——因为模拟里的格式变化和标注错误就是按论文描述的机制设定的。演示 1 里有一个典型的”标准答案不完整”的例子:
[IV] Which female flag bearer competed in the women's 200 m freestyle event?
系统:'Dana Arden' 标准答案:'Asha Kell' EM=0 人工判断:对(两人都参加了这个项目)
6.6 教训:评估方法本身需要验证
第 5、6 讲已经反复出现过同一个教训,这里又一次:
- 自动指标会系统性地低估基于 LLM 的系统:LLM 的答案往往比标准答案更完整(“Johnson City, Tennessee”),而 EM 惩罚了这一点。这个问题在评估 LLM 生成的回答时普遍存在(Kamalloo 等 2023)。
- 众包的标准答案有错,而且”每题只有一个标准答案”的规定本身就会制造错误。
- 所以分数之外,一定要抽样看错误,把”评估的问题”和”系统的问题”分开。只有系统的问题才指向下一步该改进什么。
7. 讨论与边界
7.1 SUQL 没有解决什么
answer()本身的错误。SUQL 把错误局限在了answer()里,但没有消除它。LLM 的判断错误直接决定了精确率(第 3.4 节);answer()的假阴性是 Yelp 实验里假阴性的一个来源。- 调用次数仍然可能很多。结构化条件不严格时(“适合带孩子、有汉堡”,没有地点),要读的行数仍然很多(代码 01 演示 1 的第三条查询调用了 60 次)。检索排序和上限可以缓解,但会牺牲召回率。
- 语义解析的错误。解析器写错了条件,执行再精确也没用。“把查询读回给用户”是一道防线,不是根治。
- 表结构以外的能力。“离我近的”需要位置服务,“开到很晚”需要理解营业时间的格式——这些是数据库本身缺的能力,查询语言补不上。
7.2 SUQL 和后面几讲的关系
SUQL 是后面好几讲的基础:
- 第 9 讲 SLIDERS:把一批长文档抽取成一张数据库表,再用 SUQL 回答问题(幻灯片:“Leverages SUQL: text → databases”)。
- 第 10 讲 Genie Worksheet:任务型智能体的知识查询部分,用的就是 SUQL(知识解析器把自然语言翻译成 SUQL)。
- 第 11 讲 DataSTORM:在大型数据库上做研究,执行器把自然语言问题翻译成 SQL 并执行。
7.3 相关的工作
把”让 LLM 处理文本”加进数据库查询,是近年数据库界和 NLP 界共同关注的方向。例如 LOTUS(Patel 等 2024)在 Pandas 风格的数据框上定义了一组”语义算子”(按自然语言条件过滤、排序、连接……),也研究了怎样优化它们的执行。它们和 SUQL 的共同点是:用一个形式化的、可组合的框架包住 LLM 的调用,让编译器 / 优化器负责效率。
8. 本讲小结
- 混合数据需要任意组合结构化查询和文本检索。线性化和抽取都是”取最小公分母”;SUQL 选择文本保持为文本,表格保持为表格,在 SQL 上加两个由 LLM 执行的函数:
answer(文本, 问题)和summary(文本)。 - 这两个函数的结果是普通的文本值,可以出现在任何允许文本值的地方(SELECT、WHERE、聚合、ORDER BY),所以 SQL 的全部组合能力都可以和”读文本”组合起来;HybridQA 的六类问题、多跳检索都只是普通的 SUQL。SUQL 形式化、可以读回给用户,但
answer()本身可能出错。 - SQL 引擎不加修改就能执行 SUQL,但很慢。编译器的三个优化:谓词排序(先做便宜的结构化过滤)、检索排序(最可能满足条件的先交给 LLM)、惰性求值(凑够 LIMIT 就停)。模拟实验里,一条查询从 600 次 LLM 调用降到 1 次。检索只用来排序,每一行仍要由 LLM 判断——相似 ≠ 满足条件。这些优化之所以可能,是因为编译器看得见整条查询;分别调用检索和数据库的函数调用式智能体只能猜 k。
- 智能体:输入分类 → 语义解析 → 编译执行 → 回复。两条防御:永远把查询读回给用户;没有结果时明确说没有。
- 评估:真实任务没有标准答案时,用人工检查的精确率,再单独分析”没有返回结果”的问题(论文:SUQL 93.8% / 90.3%,线性化 57.0% / 63.4%;假阴性率 14% / 9%)。HybridQA 上,零样本的 SUQL 离用 6.2 万样本训练的最好成绩差 8.9 个点 EM,但没有完全匹配的题里 61% 是评估本身的问题——真实的准确率可能达到 84.2%;而且 SUQL 能用于任意大小的表格。
9. 与项目的关系
- 如果你的数据同时有表格和文本(病历、产品目录、财报、评论),先统计用户问题里有多少需要两者。比例高的话,考虑 SUQL 式的设计:表结构 +
answer(),而不是把一切都塞进向量库。 - 设计查询时想清楚哪些条件便宜、哪些贵,让执行器先做便宜的。如果你在用函数调用式的智能体,问问自己:它能把结构化的条件”下推”到检索里吗?
- 永远把查询读回给用户,没有结果时明确说没有。这两条几乎不花成本,却能挡住很多解析错误带来的伤害。
- 评估时:区分精确率和召回率,按任务决定哪个重要;单独分析”没有结果”的情况;抽样检查不匹配的例子,把”评估的问题”和”系统的问题”分开。
10. 练习
练习 1(写 SUQL,看调用次数)。为”Palo Alto 适合约会、停车方便、评分最高的餐厅”写一条 SUQL,用 SUQLCompiler 执行。然后把两个 answer() 条件换个顺序再执行一次。调用次数一样吗?你能想出一个规则,让编译器自动决定几个 answer() 条件的先后吗?
参考答案
SELECT name, rating FROM restaurants WHERE location = 'Palo Alto'
AND answer(reviews, 'is it good for a date?') = 'Yes' AND answer(reviews, 'is parking easy?') = 'Yes'
ORDER BY rating DESC LIMIT 1
实测:结果是 Happy Bowl House(4.0 分;全库满足条件的只有这 1 家)。“约会”在前时调用 74 次,“停车”在前时调用 70 次。编译器按评分从高到低检查 Palo Alto 的 95 家,对每家依次问两个条件;第一个条件不满足就不问第二个(短路)。所以更少见的条件放在前面更省:“停车方便”只有约 11% 的餐厅满足,“适合约会”约 15%。一个自动的规则:用检索估计每个条件的”选择率”(有多少行可能满足),选择率低的先问——这正是数据库查询优化器排列条件的经典思路(按选择率和代价排序)。本讲的编译器和论文都没有做这一步,你可以在 SUQLCompiler.execute 里试着加上。
(另一个例子:把”停车方便”换成”意大利菜”——has(cuisines, 'italian') 是结构化条件——只调用 1 次就找到了 Sunny Garden Eatery,5.0 分。结构化条件越严格,调用越少。)
练习 2(动手改代码:检索的上限)。在 02_compiler_optimizations.py 里,把 F 方式的 max_candidates 改成 5 和 50,60 条查询的平均调用次数、精确率和召回率怎样变化?
参考答案
实测(answer/条,精确率,召回率):上限 5:8.4、0.98、0.66;上限 20:12.7、0.98、0.92;上限 50:19.6、0.98、0.96;不设上限:30.4、0.98、0.96。精确率不变(检查过的行,判断都一样);召回率随上限下降——“列出全部”的查询里,检索得分排在后面的行也可能满足条件,没被检查就漏掉了。对”推荐几家”的查询,小上限几乎没有损失;对”列出全部”或计数的查询,不能设上限。所以上限应该由查询的形状决定(有没有 LIMIT、是不是聚合),而不是一个全局常数。
练习 3(思考:SQLite 自带的”惰性求值”)。代码 02 的演示 1 里,SQLite 原样执行只调用了 50 次,而不是 600 次。用 SUQLCompiler(w, llm, order=False, lazy=True, retrieve=False) 执行同一条查询,调用次数是多少?为什么?如果把 lazy 也关掉呢?
参考答案
实测:order=False, lazy=True 调用 50 次,和 SQLite 原样执行一模一样;再关掉 lazy,调用 600 次。原因:SQLite 逐行扫描,对每一行按书写顺序求值 WHERE 的各个条件(AND 遇到 False 就短路),扫描到凑够 LIMIT 行就停——它天然就有”惰性求值”和”短路”,缺的只是谓词排序。order=False, lazy=True 正是在模仿这种行为。这也说明了优化的分工:惰性求值很多 SQL 引擎本来就有;谓词排序和检索排序才是 SUQL 编译器真正要加的(SQL 引擎不知道 answer() 有多贵)。
练习 4(动手改代码:格式)。在 04_hybridqa.py 里把 P_FORMAT 改成 0(“转成短答案”时总是用和段落一样的写法),EM、F1、人工判断分别怎样变化?这说明了什么?
参考答案
实测:EM 从 76.3% 升到 85.3%,F1 从 80.2% 升到 85.8%,人工判断不变(92.7%)。系统的能力一点没变,只是答案的写法变了,EM 就差了 9 个点。这说明 EM 衡量的有很大一部分是”格式是否和标注者一致”,而不是”答案对不对”。格式问题当然可以通过在训练集上微调来”解决”(学会标注者的写法),但这对真实用户没有意义——“Johnson City, Tennessee”对用户来说是更好的答案。
练习 5(思考:summary 的实现)。真实的 Yelp 数据每家餐厅有 20 条评论,加起来可能有几千个词。summary(reviews) 和 answer(reviews, q) 应该怎样实现,才能既准确又不太贵?
参考思路
answer(reviews, q):先在这家餐厅的 20 条评论里检索和 q 最相关的几条(向量相似度),只把它们交给 LLM——和第 5 讲 WikiChat 的”检索 + 阅读”一样;为了防止”只看到停车很难那一条”,可以多取几条,并让 LLM 在不确定时回答 “Unknown”。summary(reviews):评论太长时分块,每块先摘要,再把各块的摘要合起来(第 9 讲会看到,分块处理再合并时要注意重复和冲突);也可以离线预先算好,存成一列(摘要不依赖用户的问题)。两者都可以缓存:同一家餐厅、同一个问题,答案不变。
练习 6(思考:什么时候用函数调用的智能体)。第 3.7 节说明了函数调用式的智能体在混合查询上的问题。反过来想:什么情况下,你会更愿意用函数调用式的智能体,而不是 SUQL?
参考思路
(1) 数据源不能用一个查询语言统一:例如一边是数据库,另一边是只能通过 API 访问的外部服务(天气、地图、预订);(2) 问题需要边查边决定下一步:下一步查什么取决于上一步看到了什么,事先写不出一条完整的查询——第 8 讲的 SPINACH 就是这样,它在知识图谱上一步一步探索,而不是一次写出完整的 SPARQL;(3) 要执行有副作用的动作(下单、提交表单),这超出了查询语言的范围——第 10 讲的 Genie Worksheet 把查询(用 SUQL)和动作(用工作表)分开处理。一个常见的折中是:智能体负责多步的规划和动作,每一步的”查询”由 SUQL 这样的形式化语言完成。
11. 延伸阅读
课程相关
- Liu 等. SUQL: Conversational Search over Structured and Unstructured Data with Large Language Models. Findings of NAACL 2024. arXiv 2311.09818. 代码:github.com/stanford-oval/suql
- Chen 等. HybridQA: A Dataset of Multi-Hop Question Answering over Tabular and Textual Data. Findings of EMNLP 2020.
- Oguz 等. UniK-QA: Unified Representations of Structured and Unstructured Knowledge for Open-Domain Question Answering. Findings of NAACL 2022.(线性化基线)
- Yu 等. COCO-DR: Combating Distribution Shifts in Zero-Shot Dense Retrieval with Contrastive and Distributionally Robust Learning. EMNLP 2022.(论文用的检索模型)
- Lei 等. S³HQA: A Three-Stage Approach for Multi-hop Text-Table Hybrid Question Answering. ACL 2023.
背景
- Codd. Relational Completeness of Data Base Sublanguages. 1972.(SQL 对关系代数的完备性)
- Hudak. Conception, Evolution, and Application of Functional Programming Languages. ACM Computing Surveys 1989.(惰性求值)
- Kamalloo 等. Evaluating Open-Domain Question Answering in the Era of Large Language Models. ACL 2023.(EM 低估 LLM 的问题)
- Patel 等. LOTUS: Enabling Semantic Queries with LLMs Over Tables of Unstructured and Structured Data. 2024.(语义算子)
12. 下一讲预告
SUQL 处理的是表格:每张表有固定的列,语义解析器在提示里就能看到完整的表结构。如果数据是一张没有固定结构的大图呢?第 8 讲讲知识图谱:
- Wikidata:150 亿条事实、上亿个实体、上万种属性,没有固定的表结构;查询语言是 SPARQL;
- 为什么一次写出完整的 SPARQL 几乎不可能:属性太多、相似的属性太多(“住在哪里”可能是 residence、place of birth、headquarters location……),要先看看这个实体实际有哪些属性;
- SPINACH:一个模仿人类专家的智能体——搜索实体和属性、查看实体页面、查看属性的用法示例、执行查询、看结果、改查询,一步一步搭出正确的 SPARQL;
- 来自 Wikidata “Request a Query” 论坛的真实难题:为什么众包和合成的数据集都太简单。