第 8 讲:知识图谱上的反应式智能体——SPINACH

Agentic AI · Lecture 08。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应:Stanford CS224V Fall 2026 Lecture 10(10/28 Reactive Agents for Parsing Knowledge Graph Queries)。 关于材料来源:2026 年这次课的幻灯片尚未公开。本讲依据:2025 年的幻灯片(Lam、Liu & Semnani,Agentic AI for Knowledge Base Queries);SPINACH 论文(Liu、Semnani 等,Findings of EMNLP 2024,arXiv 2407.11417);课程表的描述(Wikidata 有 150 亿条事实;用 SPARQL 写查询很难;反应式的智能体工作流;部署在 Wikimedia)。第 3 讲已经介绍过 SPINACH 的策略提示词和”循环检测与回滚”,本讲讲完整的系统和它的实验。 配套代码:code/lecture08/(只用 Python 标准库;04 号文件可选,需要 anthropic 包和 API 密钥) 预计阅读时间:3 小时左右(含运行代码)


0. 本讲导航

0.1 学完本讲,你应该能够

  1. 解释知识图谱的基本概念(三元组、QID、PID、限定词),读懂和写出简单的 SPARQL。
  2. 说出在 Wikidata 上写 SPARQL 难在哪里:没有固定的表结构、属性太多太像、数据不完整、结构不统一。
  3. 比较四类以往的方法(微调的语义解析、提示 LLM 直接写、子图检索、逐边探索),说出各自的局限。
  4. 描述 SPINACH 智能体:五个动作、状态、策略提示词、循环检测与回滚、动作预算;解释为什么”模仿人类专家”是设计的出发点。
  5. 说明评估知识图谱问答的几个问题:多列答案怎样打分(按行的 F1)、为什么众包和合成的数据集都不够好、数据集要多大。

0.2 先修知识

知识点用在哪里最低要求
第 3 讲第 3 节ReAct 循环;工具调用;循环检测与回滚;步数上限
第 6 讲第 1、2 节语义解析;关系代数(选择、连接、聚合、排序)
第 1 讲第 2 节LLM 不知道长尾知识;“查出来的”比”记住的”可靠
第 5、7 讲第 5 节评估方法本身需要验证;EM 会低估

0.3 本讲地图

第1节  知识图谱与 SPARQL:为什么在 Wikidata 上写查询这么难   ← 代码 01
第2节  以往的方法:微调、提示、子图检索、逐边探索               ← 代码 02 演示 2
第3节  SPINACH:模仿人类专家的智能体                          ← 代码 02 演示 1、4、5,代码 04
第4节  实验结果                                              ← 代码 02
第5节  评估:指标、数据集、规模                               ← 代码 03
第6节  从知识图谱到关系数据库;讨论与边界
第7–11节 小结、与项目的关系、练习、延伸阅读、下一讲预告

0.4 本讲的玩具世界

代码 kg_world.py 生成一个虚构的 Wikidata 式知识图谱:6 个国家、12 个省、37 座城市、6 所大学(3 所有音乐学院)、19 种乐器(4 个大类)、约 200 个人(乐手、物理学家、数学家、导演、演员、企业家)、25 家公司、30 座山、40 部电影、5 个奖项,一共约 2,500 条三元组。实体的编号(QID)是虚构的(从 Q1000 开始);属性的编号(PID)沿用真实 Wikidata 的编号(P31 instance of、P1303 instrument……),这样你在真实的 Wikidata 上看到的也是同样的 PID。

图谱里刻意保留了 Wikidata 的七种”怪癖”(第 1.4 节)。配套的迷你 SPARQL 引擎(sparql.py)支持三元组模式、属性路径、限定词、FILTER、计数、排序等常用写法。

关于本讲的数字:幻灯片和论文里的真实数字会注明出处;模拟实验的数字都来自 01~03 号代码的实际输出。特别注意:SPINACH 的”策略”本来是一个 LLM,代码里用一个有随机性的”专家流程”代替——工具在图谱上真实执行,但”看一眼实体页能发现问题的概率”这类效果是设定的参数。模拟的问题也比真实的论坛问题简单得多(平均 3.2 个子句,论文的数据集是 8.89 个),模拟的绝对分数比论文高得多,不能和论文的数字直接比较;要看的是方向和机制。


1. 知识图谱与 SPARQL:为什么在 Wikidata 上写查询这么难

1.1 知识图谱

很多信息不适合放进表格。知识图谱(knowledge graph,也叫知识库、语义网)把知识表示成一张图:节点是实体,边是关系。

Wikidata 是最大的、实时更新的公开知识图谱(幻灯片第 6 页):

  • 150 亿条事实,每年新增约 10 亿条三元组;
  • 约 1 亿个实体;约 1 万种属性(其中约 3,000 种常用);
  • 约 2.5 万名贡献者;
  • 维基百科里的每个实体都在 Wikidata 里,Wikidata 还有多得多的实体。

它对数学、生物、教育、语言学、社会科学等许多领域的研究都很有价值(SPINACH 论文第 1 节)。

1.2 三元组、QID 和 PID

Wikidata 用 RDF 三元组表示:(主语, 属性, 宾语)。

  • 实体用唯一的 QID 表示:斯坦福大学是 Q41506。同名的实体很多(叫 Springfield 的城市就有几十个),所以必须用 QID;同一个 QID 在所有语言的维基百科里都一样。
  • 属性用唯一的 PID 表示:P112 是 “founded by”(创办者)。
  • 宾语可以是另一个实体,也可以是一个字面值(数字、日期、字符串)。
  • 一条陈述还可以带限定词(qualifier):“某人获得某奖”这条陈述上,挂着”时间:2011 年”。

“斯坦福大学是谁创办的?“写成 SPARQL:

SELECT ?x WHERE { wd:Q41506 wdt:P112 ?x . }

代码 01 的演示 1 打开了虚构图谱里几个实体的”页面”(SPINACH 的 get_wikidata_entry 动作)。一位乐手的页面:

label: Gaspar Underhill
description: musician from Orundi (born 1959)
P31 (instance of): human (Q1000)
P106 (occupation): musician (Q1012)
P19 (place of birth): Harrowgate (Q1053)
P1303 (instrument): sitar (Q1026)
P1416 (affiliation): Northgate University School of Music (Q1093)
P69 (educated at): Kingsley College (Q1099)
...

一位科学家的页面上,获奖那一行带着限定词:

P166 (award received): Orund Prize in Physics (Q1191) [P585 (point in time): 2023-01-01]

搜索 “Ashford” 会得到两座城市(一座在 Kestavia、一座在 Tirenne)——只能靠说明来区分。

1.3 SPARQL 入门

一条 SPARQL 查询描述一个”图案”,引擎找出图谱里所有能套上这个图案的方式。和第 6 讲的关系代数对照:

SPARQL含义关系代数 / SQL
?p wdt:P1416 wd:Q1093 .隶属于这所学院的人选择
?p wdt:P1416 wd:Q1093 . ?p wdt:P1303 ?i .共用变量 ?p连接(JOIN)
?i wdt:P279* wd:Q1018 .沿”子类”走零步或多步SQL 里很难写(递归查询)
(COUNT(?m) AS ?n)、ORDER BY DESC(?h) LIMIT 1、FILTER(?d < "1950-01-01")计数、排序、过滤聚合、排序、选择
ASK WHERE { … }有没有匹配?EXISTS

代码 01 的演示 2 逐条执行了这些写法。幻灯片强调:SPARQL 允许在整个知识图谱上做关系代数运算——过滤、投影、连接、计数……;而且查询引擎会做查询优化。这是”把知识图谱当文本检索”做不到的。

1.4 为什么写 SPARQL 这么难

幻灯片第 12 页列了几个原因。代码 01 的演示 3~5 在虚构的图谱上把它们一一重现:

1. 没有固定的表结构。Neo4j 这类有类型的图数据库要求同一类节点有同样的属性;Wikidata 是无类型的,为了让各个领域都能扩展(增加新的属性需要经过许可)。小测验:能把这些数据表示成表格吗? 演示 3 把虚构图谱里的 210 个人放进一张”人”表,每种属性一列:13 种属性 × 210 人 = 2,730 个格子,47% 是空的——乐手有”乐器”,科学家有”导师”,导演什么都没有。真实的 Wikidata 有上万种属性,这张表会有上万列、几乎全空,而且随时会加新列。

2. 属性太多、太相似。3,000 种常用属性根本记不住;而且常常不清楚该用哪一个。幻灯片的例子:“Isaac Newton 住在哪里?""Salesforce 在哪里?“——可能的属性有 administrative territorial entity、residence、state、country、place of birth、place of death、headquarters location、location of formation……用哪个取决于这个节点实际有哪些属性。所以必须先看看节点的属性,才能写出正确的查询。

3. 查询可以很复杂:很多连接、分组、排序、属性路径、限定词。

演示 5 列出了六种”怪癖”下,按字面意思写的查询和正确的查询差多少:

问题朴素的写法正确的写法朴素的写法错在哪里
演奏打击乐器的乐手0 行23 行具体的乐器(tabla)是打击乐器的子类(P279),不是它的实例(P31);还有人直接记了大类
出生在 Kestavia 的乐手10 行12 行一半的城市没有直接记国家(P17),只记了所在的省(P131):要写 wdt:P131*/wdt:P17
Delia Isenberg 的博士生2 行3 行学生那边一定记了”导师”(P184),导师那边的”学生”(P185)只记了一部分
隶属于 Northgate University(包括它的学院)的乐手1 行5 行很多人隶属于下面的音乐学院,学院”属于”(P361)大学
某人哪一年获奖0 行1 行年份是这条陈述的限定词(pq:P585),不是这个人的属性
Kestavia 的山有多高0 行4 行山的高度是”海拔”(P2044);“height”(P2048)是人和建筑的高度

注意:每条朴素的写法都是合法的 SPARQL,一半还能返回结果——只是不对或不全。返回 0 行的,至少给了一个”出错了”的信号;返回了一部分结果的,什么信号也没有。第 4 节会看到,这类”沉默的错误”最难对付。

1.5 执行查询的四种反馈

SPINACH 论文第 4.1 节:执行一条查询,会得到四种反馈之一(演示 4):

反馈例子智能体能学到什么
有结果4 行看看结果合不合理(问”最高的”,却返回一长串?)
没有结果用 P2048 查山某个条件写错了:逐个子句排查
语法错误少了一个 }按错误信息改
超时两条毫不相干的模式(结果是所有组合)查询太宽了:加条件、简化

Wikidata 把每条查询的执行时间限制在 60 秒。论文观察到:LLM 能根据非空的结果推理下一步;没有结果时,它常常逐个子句调试,或者换一种做法;遇到语法错误,它能根据错误信息改正;超时之后,它会设法简化查询。

这四种反馈是写 SPARQL 时最有价值的信息——一个只写一次、从不执行的系统,完全得不到它们。


2. 以往的方法

知识库问答(KBQA)以往有几类做法(幻灯片第 14~18 页;论文第 2.2 节)。

2.1 微调的语义解析器

Xu 等(EMNLP 2023)用少量样本微调 LLaMA,把问题翻译成 SPARQL(WikiSP);实体由一个单独的实体链接模块识别。它在 WikiWebQuestions 上是当时最好的成绩,而且比 GPT-3 直接回答少幻觉:幻灯片的例子——问 “Where did A Bronx Tale take place?”,GPT-3 猜”纽约的布朗克斯”;而 Wikidata 里这部电影的拍摄地包括新泽西和纽约。

局限:它只会训练数据里见过的那些写法。

2.2 提示 LLM 直接写 SPARQL

第 6 讲的 Yelp 例子里,LLM 在简单的表上直接写 SQL 就很准。LLM 也知道 SPARQL 的语法——但它只记得少数几个 PID 和 QID(P31、P279、斯坦福大学……),写不出”研究型”的问题(很多连接、分组、排序)。

2.3 子图检索

不写 SPARQL:根据问题从图谱里检索出一小块子图(Zhang 等,ACL 2022),再让模型在子图上推理。

小测验:它能提供 SPARQL 的全部表达能力吗?它能找到”最高的山”吗? 不能。“最高的山”需要比较图谱里所有的山,任何一块”和问题相关”的子图都装不下。而且整个 Wikidata 太大了,嵌入整个图谱的代价高得无法承受,所以子图检索的方法通常只在一小块子图上做实验——SPINACH 论文举的例子只覆盖了 Wikidata 的 0.01%(1.7 万个 QID,而全部是 1.1 亿多个)。

2.4 LLM 逐边探索图谱

Think-on-Graph(ToG,Sun 等 ICLR 2024):让 LLM 从问题里的实体出发,一次走一条边地探索图谱,探索完了再根据走到的子图回答。

同样的小测验:它能提供 SPARQL 的全部表达能力吗?SPINACH 论文指出两个问题:

  • 状态是一张子图:走过的节点都要放在状态里。答案有很多实体时(“哪些演员毕业于哈佛?”),或者需要计算时(“最高的山是哪座?”),这种做法从根本上就做不到。
  • 答案在探索结束后才生成:探索时不写查询、不执行、不看反馈,走错了路也不会回头修正。

2.5 模拟实验:四类方法的表现

代码 02 的演示 2 在两套问题上比较了它们(简单问题 40 道:一个实体、一个属性、一列答案,像 WebQuestions;论坛式问题 56 道:8 类,每类踩中一种怪癖,很多要返回两列、要计数或排序)。指标是第 5 节的”按行”EM 和 F1:

做法简单 EM简单 F1论坛式 EM论坛式 F1
直接问 LLM(闭卷)2.52.53.64.2
LLM 一次写出 SPARQL0.00.00.00.0
微调的解析器(给定正确实体)82.582.50.015.9
逐边探索(给定正确实体)75.075.00.018.9
SPINACH(5 次运行的平均)95.595.571.880.3
  • 图谱是虚构的,LLM 的记忆里没有这些事实:直接问只能编;一次写出 SPARQL 时,QID 全是编的,什么也查不到。
  • 微调的解析器在简单问题上很好,在论坛式问题上几乎全错:它只会训练数据里的写法(一列、没有排序和计数),也不知道图谱的怪癖。
  • 逐边探索在简单问题上还行;论坛式问题上,答案很多、要计数、要排序时都做不到。
  • 两种基线都拿到了正确的实体(论文也这样做,让它们的任务更容易),SPINACH 要自己找实体。

3. SPINACH:模仿人类专家的智能体

3.1 出发点:“如果人类专家能做到,我们就能做到”

幻灯片第 22 页:SPINACH 结合了语义解析(SPARQL)、子图检索和 LLM 的长处:

  • 语义解析到 SPARQL:表达能力完整,还能利用查询引擎的优化;
  • 图检索:查看数据里实际存在的属性;
  • LLM 智能体把两者结合起来。

更重要的是设计原则:自动化人类专家的做法。人类专家并不记住所有的节点和属性——所以我们也不需要微调,只用上下文学习。

人类专家怎样写一条复杂的 SPARQL(幻灯片第 23 页)?

  1. 从写简单的 SPARQL 开始;
  2. 需要时去看 Wikidata 上实体或属性的页面——了解图谱的结构,看看一个节点实际有哪些属性;
  3. 一个子句一个子句地往上加,最终搭出完整的查询;一路执行中间的查询、看看结果,验证自己的工作;如果查询失败了,回头检查自己的假设。

LLM 智能体把知识查询、写查询、执行并评估结果(子图检索)这三件事交织在一起。

3.2 五个动作

每个动作都对应人类专家会做的一件事(论文第 4.1 节;幻灯片第 24 页):

动作相当于作用
search_wikidata(文字)在 wikidata.org 的搜索框里搜按名字和别名找 QID 和 PID(这一步就是用 LLM 做实体消歧)
get_wikidata_entry(QID)打开一个实体的页面看它的全部出边:实际有哪些属性、值是什么、有哪些限定词
get_property_examples(PID)打开一个属性的页面看几个用法示例,弄清这个属性是怎样用的
execute_sparql(查询)在 query.wikidata.org 上执行得到四种反馈之一
stop()—把最后执行的那条查询当作最终答案

为了限制智能体要读的信息量:搜索最多返回 8 个实体和 4 个属性;查询结果很多时只显示前 5 行和后 5 行;实体页用一个单独的少样本提示修剪,只保留和问题相关的条目。

3.3 和以往的智能体有什么不同

论文第 4 节列了三点:

  1. 用 SPARQL 的全部表达能力来探索,而不是一次走一条边。SPINACH 把”探索”和”写 SPARQL”交织在一起:被要求多试、早失败(“try many SPARQLs and fail early”),从查询的结果(或者没有结果)里了解图谱的结构,发现自己错误的假设,然后回头修正。
  2. 状态是完整的”动作—观察”历史,而不是一张子图。所以答案有成千上万个实体、或者需要计算时,都不成问题——那是查询引擎的事。
  3. 不假设事先知道实体。以往的智能体从问题里识别出的实体出发;SPINACH 可以先搜索关系,如果那样更合适的话。

3.4 策略:一个零样本的提示词

SPINACH 是一个 ReAct 智能体(第 3 讲):第 nn 步,它根据状态 Sn={t1,a1,o1,…,tn−1,an−1,on−1}S_n = \{t_1, a_1, o_1, \dots, t_{n-1}, a_{n-1}, o_{n-1}\}(之前所有的想法、动作、观察),输出一个想法 tnt_n 和一个动作 ana_n;动作在外部执行,结果作为观察 ono_n 加进状态。

策略是一个零样本的提示词,只有高层的原则(第 3 讲已经展示过它的大意):从非常简单的片段开始写;执行每个片段,验证它是对的;在继续之前,确认你对 Wikidata 结构的每一个假设;一次加一块;不要重复同一个动作;问题保证有答案,继续直到找到为止;最后的投影里要返回实体本身,而不只是名字……

为什么只给高层的原则? 论文说,他们最初试过用更严格的规则控制每一步的动作,但并没有提高准确率:智能体可能遇到的状态太多了,为每一种状态编程出最好的动作很难(尤其是零样本、没有大量策略数据可以学习时);考虑各种边界情况反而会把智能体弄糊涂。所以选择每一步做什么,留给 LLM 自己判断;只通过策略提示词施加高层的控制。这样能利用 LLM 的常识和世界知识——例如,论文观察到 LLM 能判断一条查询的结果看起来不合理(问”最老的”,却返回一长串实体)。

3.5 循环与回滚、动作预算

为了鼓励探索,策略用温度 1.0 采样——同一个状态下重新采样,会得到不同的动作。代价是:LLM 有时会陷入循环,一遍又一遍地重复同一个动作;或者偏离指示,在最后一条查询没有结果时就调用 stop。

对策(第 3 讲第 4 节讲过):检测到这两种情况时,把探索状态重置到重复或偏离之前,从那里继续(因为是高温采样,重新运行不会重复同样的动作序列)。循环在三种情况下结束:调用了 stop;回滚之后用了 15 个动作;或者总共用了 30 个动作(每道题有 15 个动作的预算,另外 15 个可以花在回滚上)。

3.6 一条完整的轨迹

代码 02 的演示 1:模拟的 SPINACH 回答 “What is the highest mountain in Pallasca, and how high is it?”(节选):

[ 1] search_wikidata('Pallasca')            → Q1039 Pallasca
[ 2] search_wikidata('mountain')            → Q1007 mountain
[ 3] search_wikidata('instance of')         → P31
[ 4] search_wikidata('country')             → P17; P27 country of citizenship
[ 5] search_wikidata('height')              → P2048 height; P2044 elevation above sea level
[ 6] execute_sparql(... ?m wdt:P2048 ?h ... ORDER BY DESC(?h) LIMIT 1)   ← 少了一个 }
                                            → Syntax error: Unrecognized term: ORDER
[ 7] execute_sparql(... ?m wdt:P2048 ?h ... }  ORDER BY DESC(?h) LIMIT 1)
                                            → No results.
[ 8] Thought: The result looks wrong (empty). Debug by running a simpler fragment.
     execute_sparql(SELECT ?m WHERE { ?m wdt:P31 wd:Q1007 . } LIMIT 5)    → 5 rows(山是有的)
[ 9] get_property_examples(P2048)           → Talia Underhill -> 170; Bruna Grimaldi -> 193; …(都是人)
[10] execute_sparql(SELECT ?x WHERE { ?x wdt:P31 wd:Q1007 . } LIMIT 1)    → Mount Castellane 0
[11] get_wikidata_entry(Q1341)              → … P2044 (elevation above sea level): 3747
[12] execute_sparql(... ?m wdt:P2044 ?h ... ORDER BY DESC(?h) LIMIT 1)   → Mount Castellane 14 | 6163
[13] stop()

这条轨迹包含了论文图 2 描述的几个阶段:先查找相关的 QID 和 PID;试图一次写出完整的查询,失败了(这里是因为用了错误的属性;论文的例子里是一个没有核实过的、编出来的 QID);开始调试——执行更简单的查询、复查自己的假设;看了一个实体的页面之后发现了错误;最后写出正确的查询。

3.7 消融:每个动作都有用吗?

论文表 4(SPINACH 验证集,每次去掉一个动作;execute_sparql 和 stop 不能去掉,否则智能体根本不会返回查询):

EMF1
SPINACH21.446.4
去掉 get_wikidata_entry11.736.4
去掉 get_property_examples10.429.4
去掉 search_wikidata4.625.3

幻灯片的结论:“所有的动作都对智能体的表现有实质的贡献”。

代码 02 的演示 4(模拟,5 次运行的平均;论坛式问题):

配置EMF1平均动作数用光预算
SPINACH(全部动作,有回滚)71.880.38.30.4%
去掉 get_wikidata_entry67.977.38.10.0%
去掉 get_property_examples65.475.98.61.8%
去掉 search_wikidata48.656.210.011.8%
关掉循环检测与回滚68.978.510.05.0%
预算 8 个动作56.865.37.432.5%
预算 25 个动作(总共 50)71.880.58.30.0%
  • 去掉搜索掉得最多(和论文一致):实体的 QID 只能用 rdfs:label 写查询去猜(要猜对确切的名字才查得到),属性的 PID 只能凭记忆。
  • 去掉实体页或属性示例,各掉三四个点:它们揭示的问题不同(模拟里设定:实体页更擅长揭示结构上的怪癖,属性示例更擅长揭示限定词和”属性用错了”)。
  • 关掉回滚:重复的动作照样消耗预算,平均动作数从 8.3 升到 10.0,5% 的题用光了预算。练习 3 会把重复的概率调高,差别会大得多。
  • 预算太少(8 个),三分之一的题来不及调试完;预算加到 25 个几乎没有帮助——预算是”上限”,不是”目标”。

再次提醒:模拟里每个工具”能揭示什么”是设定的参数,所以消融的方向有意义,具体的点数没有。


4. 实验结果

4.1 在 SPINACH 数据集上

论文表 3(SPINACH 数据集;GPT-4o;按行的 EM / F1):

方法验证集 EM / F1测试集 EM / F1
直接让 GPT-4o 回答0.0 / 3.90.0 / 4.0
让 GPT-4o 写 SPARQL1.3 / 5.40.6 / 3.9
微调的 WikiSP(给定正确实体)1.3 / 3.51.2 / 7.1
零样本 ToG(GPT-4;给了 20 个正确答案里的实体作为起点)3.9 / 9.81.8 / 7.2
零样本 SPINACH(GPT-4o)21.4 / 46.416.4 / 45.3
  • 直接回答几乎全错:论文指出,这说明数据集里主要是 LLM 不熟悉的长尾知识(作为对比,GPT-4 在 WebQuestionsSP 上直接回答能达到 90.5% EM)。
  • 为了让基线的任务更容易,论文给 WikiSP 直接提供了正确的实体;ToG 用正确的实体作为起点时,前 80 道验证题一条探索路径也找不到、只能退回到 GPT-4 直接回答,于是改成从正确答案里抽最多 20 个实体作为起点。即使这样,SPINACH 在测试集上仍比最好的基线高 38.1 个点 F1。
  • 但绝对的分数并不高(测试集 EM 16.4%、F1 45.3%):这些问题还有很大的改进空间。好在 SPINACH 会显示它的每一步,用户可以接着对话、修改查询,帮智能体找到答案。

4.2 在以往的数据集上

论文表 2(零样本 SPINACH,GPT-4o):

数据集SPINACH EM / F1以往最好
QALD-7(Task 4)62.2 / 74.6微调的 WikiSP 38.0 / 43.6
QALD-9 Plus58.3 / 71.6—
QALD-10(ToG 用的子集)64.7 / 72.4ToG(GPT-4)EM 54.7
WikiWebQuestions 测试集59.9 / 70.3在它上面微调的 WikiSP 65.5 / 71.9

SPINACH 在三个 QALD 数据集上创造了新的最好成绩;在 WikiWebQuestions 上,离在这个数据集上微调过的 WikiSP 只差 1.6 个点 F1。在 QALD-7 上,零样本的 SPINACH 比 WikiSP 高 24.2 个点 EM、31.0 个点 F1——说明零样本的智能体跨数据集的泛化能力远强于微调的模型。

4.3 每道题用了几个动作

论文表 5(SPINACH 验证集,回滚之后的动作数):35 个 28.6%,68 个 33.8%,911 个 11.0%,1214 个 7.1%,用满 15 个的 19.5%——近五分之一的题,智能体是被预算”叫停”的。

代码 02 的演示 5(模拟,一次运行):35 个 7.1%,68 个 57.1%,911 个 28.6%,1214 个 5.4%,15 个 1.8%;平均每道题回滚 1.0 次。模拟的问题简单得多,所以很少用满预算。

4.4 错误分析

论文随机抽了 20 道 F1 < 0.05 的题(第 5.4 节):

原因比例例子
属性相关的问题40%没找到正确的属性;或者用错了写法(该用 ps: / pq: 的限定词,却用了 wdt:)
复杂的 SPARQL30%写不出复杂的查询,例如”生于某个月份的人”需要复杂的过滤
探索不够15%用到动作上限时还没探索完
语义解析不准确10%多加了一个子句:问”……出版了哪些东西”,LLM 假设结果都是”书”,可真实的结果还有”印刷品""法律文件”
格式问题5%只问年份(“2021”),却返回了完整的日期

一个有趣的观察(论文脚注):有两次,GPT-4o 在前一条 SPARQL 没有结果之后写道”我会取出所有相关的记录,再用 Python 过滤”——提示用 LLM 更熟悉的编程语言可能有好处。

代码 02 的演示 3(模拟,5 次运行里 F1 < 0.5 的 50 题次):复杂的子句写错了 40%、属性 / 结构用错了 34%、格式 16%、多加了一个条件 8%、动作用完了 2%。每一类问题的平均 F1:

类型F1类型F1
C1 乐器的子类95.5C5 获奖年份(限定词)95.0
C2 出生国(P131 链)64.4C6 隶属(含学院)85.8
C3 最高的山(海拔)60.0C7 公司 + 日期过滤75.0
C4 学术谱系(只记一边)78.0C8 计数92.5

两个最低的类各有原因。C2 是”沉默的错误”:朴素的写法也有结果,执行时没有任何信号;模拟的策略又对 P17(country)这样的常用属性很有把握,从不去确认它(练习 2 和代码 README 的小实验 5 会改变这一点)。C3 要排序取第一(复杂的子句),格式和排序的错误都会让它直接得 0 分。会”报错”的怪癖(C1 子类、C5 限定词:朴素的写法查不到结果)反而大多能调试出来。

4.5 部署:Wikidata 论坛上的真实对话

SPINACH 部署在 spinach.genie.stanford.edu,也作为 Wikidata 上的一个机器人(SpinachBot)参与 “Request a Query” 论坛的讨论(幻灯片第 52 页):

  • 600 多段真实的对话——都是真实而且困难的查询;
  • 随机抽了 198 段:成功 78%(154 段),失败 22%(44 段);
  • 失败的里面,一半(22 段)和数据集里的问题相似;另一半不相似:问题本身没说清楚、要求修改已有的查询、字符串处理。

幻灯片的批注:“实际使用中的成功率更高!“——真实的对话里,用户可以补充信息、纠正方向。


5. 评估:指标、数据集、规模

5.1 以往的数据集

论文表 1 比较了 KBQA 数据集的平均复杂度:

数据集类型平均子句数平均投影列数
WikiWebQuestions自然的问题(搜索引擎 / 众包)2.631
QALD-9 Plus同上3.141
QALD-10同上2.381
SimpleQuestionsWikidata同上2.001
ComplexWebQuestions合成的逻辑形式(问题经过改写)5.191
GrailQA同上7.101
KQA Pro同上6.341
SPINACH自然的逻辑形式 + 人工标注的问题8.892.50

两类以往的数据集各有问题(幻灯片第 42~43 页):

  • 众包的(问题来自搜索引擎或众包工人):问题太简单。小测验:“这样可以吗?“——真实用户需要的远不止”某人生于哪里”。
  • 合成的(先生成逻辑形式,再生成或改写问题):复杂,但自然语言的变化有限、查询的模式单一。训练数据和评估数据的说法太像,模型可以考出虚高的分数:在 KQA Pro 上,一个简单的 BART 语义解析器不做实体链接就能超过 90%。在合成数据上表现好的系统,不一定能处理自然的复杂问题(Oren 等 2021;Campagna 等 2022)。

代码 03 的演示 3 重现了这个陷阱:用 5 个模板、每个模板 2~3 种说法生成一个合成数据集,训练一个”背模板”的解析器(把实体名换成 X,在训练集里找最像的问题,照抄它的属性)。它在同模板的合成测试集上选对属性 45/45 = 100%;在论坛式问题上(给它正确的实体),平均 F1 0.0。

演示 2 统计了本讲两套模拟问题的复杂度:简单问题平均 1.00 个子句、1.00 列;论坛式问题 3.21 个子句、1.57 列——比 SPINACH 数据集(8.89 个子句、2.50 列)简单得多。

5.2 SPINACH 数据集:来自 Wikidata 论坛的真实难题

Wikidata 有一个 “Request a Query” 论坛(幻灯片第 44 页):用户提出问题,别人回复 SPARQL,大家讨论、修改,直到问题解决。这些对话真实、自然,反映了实际使用者关心的问题——但很难。

构建过程(论文第 3 节):

  1. 论坛 2016 年 7 月到 2024 年 5 月的存档里,2,780 段讨论至少包含一条有效的 SPARQL;保留最后一条 SPARQL 有结果(且结果小于 10 MB)的,剩下 2,171 段;从中随机抽 920 段。
  2. 三位 Wikidata 专家(论文作者)人工处理。删掉几类子句:分析或改进 Wikimedia 项目本身的查询;用户在调试过于复杂的 SPARQL(意思本身很难用自然语言说清);为了不超时而加的优化子句;只负责格式的子句。
  3. 为每条 SPARQL 标注一个自足的、去语境化的自然语言问题:
    • 消歧实体和属性:不问”电影在哪里发生”,而是区分”故事发生地”和”拍摄地”;不问”某人来自哪里”,而是区分”国籍”和”出生国”;
    • 自然的说法:在不引入歧义的前提下,不直接用属性名(不说”进球的时间点是什么”,而说”进球是什么时候”);
    • 准确地反映可选的子句和投影:用”如果有的话”表示 OPTIONAL;用”对每个结果,返回它的名字和位置”说明要哪几列。
  4. 最后得到 155 道验证题、165 道测试题;专家之间独立地交叉验证彼此的标注。

论文图 1 的例子:用户想要 Leonhard Euler 的学术谱系——他的导师、导师的导师……,以及他的学生、徒孙、徒孙的学生……;每一行要包含学生和他的导师。对应的 SPARQL 用到了属性路径 wdt:P184* 和 wdt:P185/(wdt:P185?)/…。

5.3 多列答案怎样打分:按行的 F1

以往的数据集每题只返回一列;SPINACH 数据集平均 2.5 列。论文的原则:从用户的角度,多给一些有用的信息是好事——问”南达科他州人口最多的县”,系统顺便返回人口,不应该扣分。所以设计了按行的 EM 和 F1(第 5.1 节):

  1. 标准答案的第 ii 行 yiy_i 和预测的第 jj 行 yj′y'_j 之间的召回:recall(yi,yj′)=∣yi∩yj′∣/∣yi∣\text{recall}(y_i, y'_j) = |y_i \cap y'_j| / |y_i|——预测的行多出来的列不扣分;
  2. 用匈牙利算法(论文用 SciPy 的 linear_sum_assignment)把标准行和预测行一一配对,使召回之和最大(召回为 0 的不配);
  3. 配上的召回之和是 tptp;没配上的标准行、以及配上的行里缺的部分是 fnfn;没配上的预测行是 fpfp;
  4. F1=2 tp2 tp+fp+fnF1 = \dfrac{2\,tp}{2\,tp + fp + fn};EM = 1 当且仅当 F1 = 1。只有一列时,它们和传统的 EM、F1 完全一样。

代码 03 的演示 1 对比了按行的 F1 和一个朴素的做法(把表里所有的值倒进一个集合,算集合的 F1):

情况按行 F1集合 F1
多返回了一列(人口)1.000.67
两列,配对正确1.001.00
两列,张冠李戴(Ann-violin、Bob-piano)0.671.00
少了一行(3 行里对了 2 行)0.800.80
多了一行0.670.67
一行只对了一半0.670.50

集合 F1 有两个毛病:多给信息被扣分;行和行的对应关系丢了,张冠李戴也能拿满分。

论文的”局限”一节也承认:自动指标(EM、F1)常常不能准确衡量基于 LLM 的系统(第 7 讲刚见过)。他们的对策是尽量准确地把 SPARQL 翻译成问题,缩小自动指标和人工检查之间的差距。

5.4 数据集够大吗?

幻灯片第 45 页的小测验:“155 道验证题、165 道测试题——这些数字看起来很小,够吗?”

代码 03 的演示 4 用自助法(bootstrap:从题目里有放回地重新抽样 2,000 次)估计置信区间。在 56 道模拟的论坛式问题上:

平均 F195% 置信区间
SPINACH81.2[71.9, 89.6]
逐边探索18.9[12.5, 26.2]
两者之差62.3[50.5, 72.9]

一个系统自己的分数有十来个点的不确定性;但比较两个系统时,要看的是差距的置信区间——差距远大于不确定性时,几十道题就足以说明问题。粗略地,一个准确率为 pp 的系统,95% 置信区间的半宽约为 1.96p(1−p)/n1.96\sqrt{p(1-p)/n}:p=0.5p = 0.5 时,n=56n = 56 约 ±13 个点,n=165n = 165 约 ±7.6 个点,n=1000n = 1000 约 ±3.1 个点。SPINACH 和基线差了 38 个点,165 道题绰绰有余;两个只差一两个点的系统,几百道题也分不出高下。论文附录 C.2 专门讨论了数据集的规模和统计功效。真实的论坛问题需要专家花大量时间标注,质量比数量更重要。


6. 从知识图谱到关系数据库;讨论与边界

6.1 同样的思路用在 SQL 数据库上:DataTalk

幻灯片的后半部分讲了同一个思路在关系数据库上的应用(第 3 讲已经介绍过 DataTalk):数据记者要分析美国联邦选举委员会(FEC)和 OpenElections 的竞选资金数据——36 个很大的关系数据库。《纽约时报》的一篇关于夏威夷承包商竞选捐款的报道,有经验的数据记者要花 100 多个小时。能不能让记者直接和数据”对话”,不需要数据科学家?

DataTalk 的动作集和 SPINACH 一一对应:

知识图谱(SPINACH)关系数据库(DataTalk)
search_wikidata(文字)get_tables()、entity_linking(文字, [列…])、location_linking(文字)
get_wikidata_entry(QID)retrieve_tables_details([表…])
get_property_examples(PID)—
execute_sparql(查询)execute_SQL(查询)
stop()stop()

真实的经验(幻灯片第 61 页):智能体式的做法是必需的——表太多了,需要检索表的知识;结果很难解读,数据有很多附加说明(例如有些表不包括 200 美元以下的捐款),需要数据专家——要像学徒一样把专家的经验记下来。第 11 讲的 DataSTORM 会在大型数据库上更进一步:从数据里自动发现洞见。

6.2 智能体式方法的问题

幻灯片第 64 页总结了智能体式方法的两个问题:

  • 可能无限循环、没有进展(第 3.5 节的回滚和预算);
  • 不会规划需要多步的动作:它是一个”增量的决策过程”,每一步只根据观察决定下一个动作。

第 3 讲的”计算思维引擎”就是针对第二点的:把复杂的问题递归地分解,而不是让智能体一步一步地摸索。

6.3 代价

SPINACH 每道题要调用很多次 LLM,延迟和成本都比简单的系统高(论文的”局限”一节)。这似乎是更准确的代价;论文指出,蒸馏(第 5 讲的 WikiChat 做过)可能是提高效率的方向。

6.4 本讲模拟的边界

  • 模拟的 SPINACH 策略不是 LLM:它按 SPINACH 策略提示词的原则行事,每一步对不对由设定的概率决定。真实的 LLM 会犯模拟里没有的错误(比如误读一个实体页),也会有模拟里没有的能力(比如从查询的结果里自己发现图谱的结构)。
  • 模拟的图谱只有 7 种怪癖,问题平均 3.2 个子句;真实的 Wikidata 和论坛问题复杂得多。
  • 可选的 04 号文件把同样的工具交给真实的 Claude,你可以亲自观察真实 LLM 的探索过程。

7. 本讲小结

  1. 知识图谱用三元组表示知识:实体用 QID、属性用 PID,陈述可以带限定词。Wikidata 有 150 亿条事实、约 1 万种属性,没有固定的表结构。SPARQL 能在整个图谱上做关系代数运算,还有属性路径。
  2. 在 Wikidata 上写 SPARQL 很难:属性太多、太像;用哪个属性取决于节点实际有哪些属性;数据不完整、结构不统一(子类 vs 实例、只记一边、限定词、要沿着 P131 走……)。朴素的写法大多是合法的,只是不对或不全——不看一眼图谱,写不出正确的查询。
  3. 以往的方法:微调的解析器只会训练数据里的写法;LLM 直接写 SPARQL 只记得少数 PID 和 QID;子图检索和逐边探索的状态是一张子图,做不到大答案集、计数、排序(“最高的山”)。
  4. SPINACH 模仿人类专家:五个动作(搜索、看实体页、看属性示例、执行、停止);状态是完整的动作—观察历史;零样本的策略提示词只给高层原则(从简单的开始、确认每个假设、一次加一块);温度 1.0 鼓励探索,循环检测与回滚 + 15/30 个动作的预算防止失控。它在 SPINACH 数据集上比最好的基线高 38.1 个点 F1,在三个 QALD 数据集上创造了新纪录,部署在 Wikidata 论坛上成功率 78%;每个动作都有实质的贡献。
  5. 评估:多列答案用按行的 F1(多给的列不扣分、行要配对);众包的数据集太简单,合成的数据集会被”背模板”——需要来自真实论坛的、自然而复杂的问题;数据集的规模要看你想分辨多大的差距。

8. 与项目的关系

  • 如果你的数据在知识图谱(或者很多张表)里,不要指望 LLM 一次写对查询。给它和人类专家一样的工具:搜索实体和属性、查看实体、查看属性的用法、执行查询;让它边写边执行。
  • 找出你的数据的”怪癖”:哪些属性只记了一边?哪些值是大类、哪些是具体的?哪些信息在限定词里?把它们写进工具的说明或策略提示词。
  • 区分”会报错的错误”和”沉默的错误”。后者(结果不全)执行时没有信号,只能靠事先确认假设、或者人工抽查。
  • 给智能体设动作预算和循环检测;记录每一步,失败时看轨迹找原因。
  • 评估时:如果答案是表格,用按行的指标;用真实用户的问题,而不是你自己想出来的模板;算一算你的测试集能分辨多大的差距。

9. 练习

练习 1(写 SPARQL)。在虚构的图谱上写一条 SPARQL:“哪些电影的故事发生在 Kestavia 的城市里?返回电影和城市。“它用到了本讲的哪两个怪癖?再把它改成”在 Kestavia 的城市里拍摄的电影”。(Kestavia 的 QID 是 Q1037。)

参考答案
SELECT ?f ?c WHERE { ?f wdt:P840 ?c . ?c wdt:P131*/wdt:P17 wd:Q1037 . }

实测 7 行。两个怪癖:(1) “故事发生地”是 P840(narrative location),“拍摄地”是 P915(filming location),两者在图谱里经常不同;(2) 一半的城市没有直接记国家,要写 wdt:P131*/wdt:P17——如果只写 ?c wdt:P17 wd:Q1037,只有 5 行。改成拍摄地:把 P840 换成 P915,实测 11 行。

练习 2(动手改代码:确认假设)。在 02_spinach_agent.py 演示 4 的 configs 里加上 params={"p_verify": 0} 和 params={"p_verify": 1}(执行前”确认假设”的概率)。论坛式问题的 F1、平均动作数怎样变化?分别看 C4(学术谱系)和 C2(出生国)两类,有什么不同?为什么?

参考答案

实测(5 次运行的平均):p_verify = 0:F1 79.2、平均 8.1 个动作;0.6(默认):80.3、8.3 个;1:80.8、8.7 个。总体只差一两个点,但分开看很有意思:C4(学术谱系)从 68.2 → 78.0 → 90.0;C2(出生国)始终是 64.4。

C4 的错误是”沉默的”:用 P185(导师那边记的学生)查,结果不全但不为空,执行时没有信号;只有事先看一个学生的页面(发现学生那边记了 P184),才能发现。所以确认假设对它帮助很大。C2 的错误也是沉默的,但模拟的策略对 P17(country)这样的常用属性很有把握,从来不去确认它(代码里:P_MEMORY ≥ 0.8 的属性跳过确认)——这正是真实 LLM 的一种典型失误:越熟悉的东西越不检查。把 P_MEMORY["P17"] 改成 0.5,C2 的 F1 从 64.4 升到 81.4(代码 README 的小实验 5)。确认假设也有代价:每次一两个动作,在别的题上会挤占调试的预算——所以总体的提升不大。

练习 3(动手改代码:循环)。在 configs 里加上 params={"p_repeat": 0.3}(重复上一个动作的概率从 0.12 提到 0.3),有回滚和没有回滚(rollback=False)各运行一次。结果说明了什么?

参考答案

实测(5 次运行的平均):有回滚时 F1 80.0(和默认的 80.3 几乎一样),平均 8.3 个动作,0.7% 的题用光预算;没有回滚时 F1 56.8,平均 12.4 个动作,36.4% 的题用光预算。回滚把重复的动作”撤销”了:它只消耗”总共 30 个”的额度,不消耗”回滚之后 15 个”的预算,而且状态回到重复之前,模型重新采样时多半会换一个动作。没有回滚时,一次重复常常引发一连串重复(重复的动作和观察留在上下文里,让模型继续”相信”这条路),预算很快耗尽。温度越高、越鼓励探索,回滚就越重要。

练习 4(手算按行的 F1)。标准答案 [(A, x), (B, y), (C, z)],预测 [(A, x), (B, z), (D, w)]。手算按行的 F1,再用 row_f1 验证。

参考答案

召回矩阵(标准行 × 预测行):(A,x) 和 (A,x) 是 1;(B,y) 和 (B,z) 是 0.5(共有 B);(C,z) 和 (B,z) 是 0.5(共有 z);(D,w) 和谁都是 0。最优配对:(A,x)↔(A,x)、再从 (B,y) 或 (C,z) 里选一个配 (B,z),总召回 1.5。所以 tp = 1.5;配上 2 行,fn = (3 − 2) + (1 − 1) + (1 − 0.5) = 1.5;fp = 3 − 2 = 1。F1 = 2 × 1.5 / (2 × 1.5 + 1 + 1.5) = 3 / 5.5 ≈ 0.545。row_f1 的结果是 0.5454…,EM = 0。

练习 5(思考:状态是什么)。ToG 的状态是一张子图,SPINACH 的状态是完整的动作—观察历史。“哪些演员毕业于哈佛?“(答案有几百人)和”最高的山是哪座?“这两个问题,为什么前者做不到、后者能做到?SPINACH 的状态有什么代价?

参考思路

子图式的状态要把答案相关的节点都放进来:几百个演员的子图装不下(也不应该装进 LLM 的上下文);“最高的山”要比较所有的山,子图里只有”和问题相关”的那一小块。SPINACH 的状态里只有查询和查询的结果摘要(结果很多时只显示前 5 行和后 5 行)——几百个答案由查询引擎处理,智能体只需要知道”这条查询是对的”。代价是:状态随着步数增长(每一步的想法、动作、观察都留着),上下文越来越长、越来越贵;这正是第 3 讲”上下文管理”的问题。SPINACH 用”限制每个观察的大小”(8 个实体、4 个属性、前后各 5 行、修剪实体页)来控制。

练习 6(思考:为你的数据设计动作集)。假设你要为一个医院的电子病历数据库(几百张表、很多编码系统,例如 ICD 诊断码)做一个 SPINACH 式的智能体。你会给它哪些动作?怎样对应 SPINACH 的五个动作?策略提示词里你会写哪几条原则?

参考思路

对应 DataTalk 的设计:list_tables() / search_schema(文字)(找表和列,相当于搜索属性);describe_table(表)(列、类型、几行样例,相当于看属性示例);lookup_code(文字, 编码系统)(把”糖尿病”映射到 ICD 码,相当于实体链接);get_patient_record(id)(看一个具体病人的全部记录,相当于看实体页——最能揭示”这个信息实际记在哪里”);execute_sql(查询);stop()。策略原则:从小查询开始,先在少量行上验证;确认每个编码和每张表的含义(例如”诊断”表和”账单”表里的诊断码含义不同);数一数结果的行数,和常识比较;注意时间范围和单位;所有结论引用执行过的查询。还需要考虑权限(只读、去标识化)——第 3 讲的”工具的权限”。


10. 延伸阅读

课程相关

  • Liu、Semnani 等. SPINACH: SPARQL-Based Information Navigation for Challenging Real-World Questions. Findings of EMNLP 2024. arXiv 2407.11417. 代码与数据:github.com/stanford-oval/spinach;在线演示:spinach.genie.stanford.edu
  • Xu 等. Fine-tuned LLMs Know More, Hallucinate Less with Few-Shot Sequence-to-Sequence Semantic Parsing over Wikidata(WikiSP、WikiWebQuestions). EMNLP 2023.
  • Sun 等. Think-on-Graph: Deep and Responsible Reasoning of Large Language Model on Knowledge Graph. ICLR 2024.
  • Zhang 等. Subgraph Retrieval Enhanced Model for Multi-hop Knowledge Base Question Answering. ACL 2022.
  • Yao 等. ReAct: Synergizing Reasoning and Acting in Language Models. ICLR 2023.

数据集与评估

  • Usbeck 等. QALD-10 — The 10th Challenge on Question Answering over Linked Data. 2023.
  • Cao 等. KQA Pro: A Dataset with Explicit Compositional Programs for Complex Question Answering over Knowledge Base. ACL 2022.
  • Oren 等. Finding Needles in a Haystack: Sampling Structurally-diverse Training Sets from Synthetic Data for Compositional Generalization. EMNLP 2021.

背景

  • Vrandečić & Krötzsch. Wikidata: A Free Collaborative Knowledgebase. Communications of the ACM 2014.
  • Wikidata 的 SPARQL 教程:wikidata.org/wiki/Wikidata:SPARQL_tutorial;“Request a Query” 论坛:wikidata.org/wiki/Wikidata:Request_a_query

11. 下一讲预告

第 6~8 讲的数据都是结构化的(表格、知识图谱),或者是”表格 + 短文本”(第 7 讲的评论)。如果知识藏在一大批长文档里呢?——几百份财报(每份 10 万个 token)、几千篇论文、一个州的全部保险备案。第 9 讲讲 SLIDERS:

  • 为什么 LLM 的上下文”永远不够长”,以及为什么即使够长,精确度也会随长度下降;
  • “分块”的方法有什么问题:块的边界把表格切断、块里看不到文档的标题和章节、各块抽出的信息重复、残缺、互相矛盾;
  • SLIDERS 的做法:根据问题自动设计表结构、语义分块、带上下文的抽取、对账合并(reconciliation),最后用 SQL 回答——把文本变成数据库,再用第 6、7 讲的工具。
← 返回课程目录