想象你接管了一座城市档案库。这里没有写满“第 1 步取号、第 2 步填表、第 3 步排队”的操作手册,取而代之的是两类资料:记录世界真相的“事实卡片”,和写着“如果……那么……”的“规则手册”。市民来办事时,你不需要指挥档案员“先翻哪个抽屉、失败了再换哪条路”,你只需要把问题单递进去:“这个结论能成立吗?”档案员会自己比对卡片、套用规则、沿着每一条可能的岔道搜索,直到给出答案或说一句“查无此证”。 Prolog 就是这样一门“档案馆语言”。它属于声明式逻辑编程:程序员把知识写成事实与规则,计算则由查询驱动。程序运行的本质,是引擎在知识库中做合一——把问题和档案对上号;做回溯——此路不通就退回岔路口重走;还会顺着递归规则一层层追查。这篇教程会用“城市档案库”的故事,带你从零看懂 Prolog 的语法、推理机制与经典写法。
对 AI 从业者而言,Prolog 是知识表示、规则引擎与自动推理的经典范式,理解它能让你在构建专家系统、语义网和可解释 AI 时,拥有一种“描述问题而非编写步骤”的思维武器。
可视化演示
Prolog是基于一阶逻辑的声明式编程语言,通过事实与规则描述问题。本文详解Prolog的项与谓词、合一与回溯... 切换模式查看不同维度。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Prolog:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现
声明式范式
想象你刚被任命为城市档案库的馆长。前任留下一句话:“别给员工写步骤,把城市规则写清楚,他们自己会查。”于是档案库里有了“事实卡片”和“规则手册”。当一个市民来问“我能否进入市政厅?”,档案员不是按某个固定流程执行,而是翻找能证明答案的卡片和规则。
Prolog 就是这样一门“档案馆语言”。它不提供 for 循环或赋值语句,而是让你把领域知识写成事实与规则,再通过查询来获得答案。程序员负责描述“世界是什么样”,Prolog 引擎负责“如何推出你要的答案”。这种范式称为声明式逻辑编程。
Prolog 中的每个知识条目都是一个子句(clause)。事实是无条件成立的关系描述;规则是带条件的逻辑蕴含。程序整体可以看成一个一阶逻辑子集的知识库。用一条最简单的祖先规则来说明:
你不必告诉档案员“先查哪个抽屉、再翻哪页”,规则册已经写明方向。同样,Prolog 程序员把精力放在知识结构上,把搜索路径交给引擎。这种“只问是什么、不管怎么做”的观念,是整个逻辑编程的起点。
- 谓词
ancestor表达“……是……的祖先”这条关系,X、Y 是待填充的参数。 - 左箭头
\leftarrowProlog 中以:-书写,表示“只要右边成立,左边就成立”。 - 前提
parent(X,Y)先证明 X 是 Y 的父母,这是知识库中事实或其它规则提供的证据。 - 全称含义X、Y 都是全称变量,所有满足前提的对象对都能推出祖先关系。
事实与规则
档案库里最常见的卡片是事实。比如“艾米的父亲是鲍勃”可以写成 parent(amy, bob).。它不需要任何条件,永远成立。另一类是规则,像“如果 X 是 Y 的父母,且 Y 是 Z 的父母,那么 X 是 Z 的祖父母”。规则是档案库的推理能力。
在 Prolog 中,事实就是一个原子语句。常量以小写字母开头,变量以大写字母或下划线开头。规则形如 head :- body.,body 中的逗号表示逻辑与。一个知识库把事实和规则都放在程序文件里,引擎会统一索引,不依赖书写顺序。例如:
事实描述证据,规则描述证据之间的因果。两者都以子句形式存在,引擎在查询时会把它们都看作候选。就像档案馆里事实是卷宗、规则是法条:证据越全、法条越严谨,推理结论越可靠。学习 Prolog,先要学会把领域知识整理成这样的知识库。
- 事实
parent(amy,bob)这是无条件成立的基本断言,表示艾米是鲍勃的父母。 - 规则头
grandparent(X,Z)结论部分,说明我们想定义“祖父母”关系。 - 规则体
parent(X,Y), parent(Y,Z)前提条件,两个谓词用逗号连接,表示必须同时成立。 - 变量复用两个子目标中的 Y 必须是同一个人,这是连接条件的桥梁。
parent(amy, bob).grandparent(X, Z) :- parent(X, Y), parent(Y, Z).
查询与谓词
当市民来办事,档案员先看到他写的问题单。问题单以 ?- 开头,比如 ?- parent(amy, X).,意思是:“请告诉我,谁是艾米的孩子?”查询会让引擎在知识库中搜索所有能让 parent(amy,X) 成立的 X。
这里 parent 是一个谓词(predicate),amy 是原子,X 是变量。谓词名加参数个数决定一个子句的身份;同一个名字不同元数会被视为不同谓词。查询的每个目标是需要被证明的子目标。
除了单个子目标,查询还可以用逗号连接多个子目标,比如 ?- parent(amy, X), parent(X, Y).。匿名变量 _ 表示不关心的位置。这是谓词组合成复合查询的方式。问题单上的变量会被答案一一填上;若引擎找不到任何证据,它会回答 false。就像档案员翻遍全库后说:“查无此证。”查询驱动推理,这一动作贯穿了整个 Prolog 程序的生命周期。
- 查询符
?-表示向知识库提出一个待证明的问题。 - 谓词
parent这是一个二元关系,第一个参数是父母,第二个参数是孩子。 - 常量
amy具体对象,以小写字母开头,表示“艾米”。 - 变量
X以大写字母开头,查询时会搜索所有可能的绑定。
合一机制
档案员拿到问题单后,第一件事是把问题单和卷宗里的卡片进行比对。这个比对不是简单的“相等”,而是双向的模式匹配:如果一边是未知数,另一边是具体信息,就把未知数填上;如果两边都缺失,就让它们彼此挂上钩。这套规则叫合一。
合一的目标是让两个项完全相同。匹配过程中产生的变量绑定叫替换(substitution)。例如查询 parent(amy,X) 与事实 parent(amy,bob) 合一,成功并生成绑定 X=bob。
如果两个参数都是变量,比如 X 和 Y,合一会让 X 与 Y 绑定在一起,之后任一绑定都会同步影响另一个。这使它比普通赋值更强大,也更容易出现“双向绑定”的直觉。档案员比对的不是“一份档案是否等于另一份”,而是“两份档案能不能互相补全”。合一让 Prolog 能够在不知道答案时,“猜出”答案。
- 比较首层结构首先检查两个项的外层谓词都是
parent,元数都是 2,因此可以继续。 - 比较第一个参数
amy与amy完全相同,匹配成功。 - 比较第二个参数一侧是变量 X,另一侧是常量
bob,变量被绑定为bob。 - 生成替换将绑定记录为 θ,完成本次合一。
回溯机制
档案员查案并不总是一击即中。他会先在第一个岔路口选一本手册,走到底;如果后续条件和证据对不上,就回到最近的岔路口,拿起下一本手册继续试。Prolog 正是这样:深度优先搜索 + 回溯。
每成功选择一个匹配子句,引擎就留下一个选择点。当前面的目标失败,就会撤销最近的变量绑定,退回选择点尝试下一条候选子句。这个过程会一直继续,直到求出全部答案或所有路径都失败。
回溯对应搜索树上的分支回退:失败的路径被剪掉,引擎回到最近的岔路口继续下一个分支。子句书写顺序和子目标顺序会影响搜索顺序与性能,但不影响知识库中已存在的逻辑结论。这就像档案员手中的毛线团:进死胡同时顺着线退回岔口,再换一条走廊。你用 ; 可以要求他继续找下一个答案,而回溯正是他不断折返的动力引擎。
- 目标 G当前要证明的查询或子目标。
- 候选子句集
clauses(G)知识库中所有能与 G 合一的子句。 - 规则体
body(c)事实的规则体为空,规则的规则体是若干子目标的列表。 - 求并集
\bigcup把每条候选路径得到的解都收集起来,就是所有答案。
递归与列表
档案库中许多关系是层层递进的:要查祖先,先查父母;要查父母的父母,再往上……Prolog 用递归描述这种链条。递归规则必须包含一个最直接的“底”和一条把问题缩小一步的“梯子”。
以家族祖先为例:祖先可以被定义为父母,或父母的祖先。这就是两个子句:一个终止条件(base case)和一个递归条件。列表是 Prolog 最常用的递归结构,用 [H|T] 把头 H 和尾列表 T 分开。计算列表长度可以这样写:
递归时每次都要先写边界条件,否则查询会无限循环。列表处理也常依靠递归:访问每个元素、拼接两个列表、查找元素,本质都是在 [H|T] 上反复拆分。档案员查祖先时,从当前卷宗出发,沿“父母栏”找到下一个卷宗,再次翻看。列表递归就像这条卷宗链:你只需要知道如何处理当前的这一格,然后放心地把剩下的链条交给下一层。Prolog 的递归之美就在于此。
- 空列表边界
length([], 0)是递归的终止条件,避免无限下降。 - 列表分解
[H|T]把非空列表拆成头部元素 H 和剩余列表 T。 - 递归调用先求尾列表 T 的长度 M。
- 汇总长度整体长度 N 等于 M 加 1,完成一个递推步骤。
截断与否定
档案库偶尔会遇到“此案不必再审”的情况。比如规则写明:某人的父亲一旦确定,就不再尝试其他候选人。Prolog 用截断 ! 表示:当执行到它时,冻结当前已经做出的选择,丢弃之后的选择点。
截断常用在规则体里,用来告诉引擎:此处之前的选择已经确定,不要再回来尝试其它子句。比如 p(X) :- q(X), !, r(X). 当 q(X) 成功后就执行 !,后续 r(X) 即使失败也不会再尝试其它 p 子句。
否定即失败(negation as failure)是另一个重要机制。\+ G 表示“如果能证出 G 就失败,如果证不出 G 就成功”。它基于封闭世界假设:知识库里没有证据,就默认不成立。可以写成:
注意,截断能大幅提升性能,但也可能把本应找到的答案剪掉;否定同样依赖知识库的完整程度。档案库中这一切都很直观:“盖棺定论”的截断和“查无此证”的否定,都是带着前提的高效工具,用错就会误判。
- 语法
\\+ GProlog 中对目标 G 的否定形式,常读作“否定即失败”。 - 证明失败引擎搜索整个知识库后找不到任何证明路径。
- 封闭世界假设没有记载的证据,就按不成立处理。
- 与经典否定区别它不是逻辑上的“G 为假”,而是“当前知识库证明不了 G”。
AI 应用场景
法律条款自动审查
阶段:知识表示与推理
解决的问题:传统法务审查需要人肉翻法规、比对多个例外条款,法规量增大后容易遗漏。AI 把条款写成事实和规则,让查询端口自动完成合一、回溯,并给出可解释的推理链。
具体案例:某合规系统把“未备案的数据传输”定义为违规:violation(X) :- transfer(X), not(exempted(X)). 业务人员输入一个操作编号,系统通过回溯返回违规结论以及触发该结论的条款,就像档案员翻出对应的卷宗和法条。
大学选课规划器
阶段:约束求解与路径搜索
解决的问题:课程依赖关系复杂,涉及先修课、时间冲突、学分限制,命令式代码要写大量 if 分支。用 Prolog 声明式地描述依赖,学生提问后引擎会自动生成可选方案。
具体案例:把“人工智能需要先修离散数学”写成 requires(ai, discrete_math).,把“已修课程”写成事实,查询 ?- can_take(me, ai). 时,引擎通过合一和回溯检查先修链是否全部满足。
Python 演示
#!/usr/bin/env python3# -*- coding: utf-8 -*- from itertools import count FACTS = [ ("parent", "amy", "bob"), ("parent", "bob", "cara"), ("parent", "cara", "dan"),] RULES = [ (("ancestor", "?x", "?y"), [("parent", "?x", "?y")]), (("ancestor", "?x", "?y"), [("parent", "?x", "?z"), ("ancestor", "?z", "?y")]),] _counter = count(1) def is_var(t): return isinstance(t, str) and t.startswith("?") def walk(t, env): while is_var(t) and t in env: t = env[t] return t def unify(a, b, env): a = walk(a, env) b = walk(b, env) if a == b: return env if is_var(a): env[a] = b return env if is_var(b): env[b] = a return env if isinstance(a, tuple) and isinstance(b, tuple) and len(a) == len(b): for x, y in zip(a[1:], b[1:]): m = unify(x, y, env) if m is None: return None return env return None def rename(t, c): if is_var(t): return t + "_" + str(c) if isinstance(t, tuple): return tuple(rename(x, c) for x in t) return t def prove(goals, env): if not goals: yield env return goal, rest = goals[0], goals[1:] for fact in FACTS: m = unify(goal, fact, env) if m is not None: yield from prove(rest, m) for head, body in RULES: c = next(_counter) h = rename(head, c) b = [rename(x, c) for x in body] m = unify(goal, h, env) if m is not None: yield from prove(b + rest, m) def solve(query): seen = set() for env in prove([query], {}): key = tuple(sorted(env.items())) if key not in seen: seen.add(key) yield env if __name__ == "__main__": for env in solve(("ancestor", "amy", "?x")): print(env["?x"])
FACTS = [用元组模拟 Prolog 事实表,每个三元组是一条亲子断言。RULES = [模拟规则:第一条直接证明祖先是父母,第二条通过中间人递归。def unify(a, b, env):合一函数,返回新的绑定或None,处理变量和常量的双向匹配。if isinstance(a, tuple) and isinstance(b, tuple):递归比较复合项的每个参数,相当于比对谓词的内部结构。for fact in FACTS:遍历事实数据库,逐个尝试合一。for head, body in RULES:尝试用规则匹配目标,再证明规则体。yield from prove(b + rest, m)把规则体子目标加入待证明队列,执行深度优先搜索与回溯。if __name__ == "__main__":程序入口,运行查询并打印所有祖先答案。
常见误区
误区我写 `parent(amy, bob).` 和下一行 `grandparent(X,Z) :- ...`,程序会像 Python 一样从上往下执行,那我只要调整行顺序就能让结果完全不同。
正解Prolog 是声明式语言,事实和规则描述知识库,查询触发引擎自动搜索。子句顺序会影响搜索顺序和性能,但真正决定结论的是合一、规则体和回溯。就像档案库里卷宗摆放顺序不影响事实本身,只影响查找快慢。
为何常见:绝大多数人先学命令式语言,习惯把代码看作步骤;看到 ?- 查询后,仍会用“执行”理解推理。
误区合一就是 `X = bob`,和 Python 里 `x = 'bob'` 一样,只允许右边的值赋给左边的变量。
正解合一允许两边都有变量:X = Y 会把两个变量绑在一起,之后任一边的具体值都会同步更新;常量只能与相同常量匹配,否则失败。这是双向匹配,不是单向赋值。档案员比对两份残缺档案时,两边都可以补全对方。
为何常见:Prolog 用 = 写合一,长得像赋值,但和过程式语言的赋值语义完全不同。
误区`!` 只是让程序跑得更快,加上它以后答案数量不会变,所以可以随意加。
正解截断会剪掉选择点,从而剪掉后续可能产生的答案。它把“所有解”变成“当前分支的解”,所以会改变程序的逻辑结果。正规用途是定位唯一解或防止无效搜索,但必须理解剪枝范围。就像档案员“盖棺定论”后,可能真的错过了另一份证据。
为何常见:新手看到 ! 能终止递归就以为它只是性能开关,忽略了选择点正是答案的来源。
误区`\+ goal` 就是“goal 在数学上为假”。如果知识库里没有 `sky_green.`,那么 `\+ sky_green.` 一定为真。
正解它只表示“在当前知识库中无法证明 goal”,即封闭世界假设。如果知识库不完整,“查无此证”不等于“事实为假”。比如档案没登记某人的出生证明,不能说此人没出生。
为何常见:经典逻辑的否定基于真值表,而 Prolog 的否定基于证明失败,初学者容易把两者混为一谈。
面试题
Q1. Prolog 中 parent(tom, bob). 在知识库里属于什么?
- A.规则
- B.事实← 正确答案
- C.查询
- D.截断
答案:B。它没有 :-,也没有条件,是一个无条件成立的基本断言,相当于档案库里一张事实卡片。规则必须包含 :-,查询以 ?- 开头,截断是 !。
Q2. 查询 ?- parent(tom, X). 中的 X 是什么?
- A.原子常量
- B.未绑定变量← 正确答案
- C.谓词名
- D.子句
答案:B。X 以大写字母开头,是查询中等待合一填充的变量。原子常量以小写字母开头,parent 是谓词名。档案员会把 X 当作问题单上的空栏等待填写。
Q3. Prolog 搜索多个答案时主要采用什么策略?
- A.随机搜索
- B.深度优先加回溯← 正确答案
- C.广度优先
- D.贪心搜索
答案:B。引擎从第一个匹配子句开始向下证明,失败后回到最近的选择点,这是深度优先加回溯。档案员沿着岔路一条道走到底,发现死胡同再退回。
Q4. 关于合一,下列说法正确的是?
- A.只能绑定常量,不能绑定变量
- B.允许两个变量互相绑定← 正确答案
- C.通过匹配让两个项变得完全一致← 正确答案
- D.两个不同常量也能合一
答案:BC。合一是双向模式匹配,两个变量可以互相绑定,目标是让两个项完全一致。不同常量无法合一,否则匹配失败,因此 A、D 错误。
Q5. 递归规则必须首先定义什么?
- A.截断
- B.边界条件← 正确答案
- C.列表
- D.副作用
答案:B。没有边界条件,递归会一直调用自身直到栈溢出。边界条件是递归链的末端,档案员必须有一个“查到底”的停止点。
Q6. 截断 ! 的效果是?
- A.只加快速度不改变答案
- B.剪掉选择点← 正确答案
- C.可能改变程序逻辑结果← 正确答案
- D.相当于逻辑否定
答案:BC。! 冻结之前的选择并剪掉选择点,可能剪掉本可以找到的答案,因此不是单纯的优化,更不是逻辑否定。像档案员下令“不再翻旧账”。
Q7. Prolog 中 \+ goal 的意思是?
- A.知识库中无法证明 goal← 正确答案
- B.goal 在现实世界一定为假
- C.执行剪枝
- D.启动调试
答案:A。否定即失败。它基于封闭世界假设:当前档案库没有登记,就当作不成立;但“查无此证”不等于现实为假。
Q8. 知识库为 parent(a,b). parent(b,c).,查询 ancestor(a,c) 要获得答案,通常需要?
- A.只有截断
- B.合并且递归规则← 正确答案
- C.列表处理
- D.否定即失败
答案:B。ancestor 需要规则:直接父母关系是一条规则,祖先的祖先是一条递归规则。查询通过合一匹配 parent(a,b)、parent(b,c) 并回溯得到答案。
Q9. 列表 [H|T] 中 H 和 T 分别表示?
- A.头部元素和剩余列表← 正确答案
- B.两个列表
- C.头部列表和尾部元素
- D.随机元素
答案:A。[H|T] 把列表拆成头部元素 H 和剩余列表 T,是 Prolog 递归处理列表的基础。档案员拿到一沓卷宗,先取最上面一份,再把剩下的链给下一层。
Q10. Prolog 属于哪种编程范式?
- A.命令式编程
- B.面向对象编程
- C.声明式逻辑编程← 正确答案
- D.数据库查询语言
答案:C。Prolog 用事实和规则描述关系,查询驱动推理,属于声明式逻辑编程。C/Python 主要是命令式;虽然它常用于知识库,但并不只是一门查询语言。