想象你接管了一座城市档案库。这里没有写满“第 1 步取号、第 2 步填表、第 3 步排队”的操作手册,取而代之的是两类资料:记录世界真相的“事实卡片”,和写着“如果……那么……”的“规则手册”。市民来办事时,你不需要指挥档案员“先翻哪个抽屉、失败了再换哪条路”,你只需要把问题单递进去:“这个结论能成立吗?”档案员会自己比对卡片、套用规则、沿着每一条可能的岔道搜索,直到给出答案或说一句“查无此证”。 Prolog 就是这样一门“档案馆语言”。它属于声明式逻辑编程:程序员把知识写成事实与规则,计算则由查询驱动。程序运行的本质,是引擎在知识库中做合一——把问题和档案对上号;做回溯——此路不通就退回岔路口重走;还会顺着递归规则一层层追查。这篇教程会用“城市档案库”的故事,带你从零看懂 Prolog 的语法、推理机制与经典写法。

岗位关联

对 AI 从业者而言,Prolog 是知识表示、规则引擎与自动推理的经典范式,理解它能让你在构建专家系统、语义网和可解释 AI 时,拥有一种“描述问题而非编写步骤”的思维武器。

目录
  1. 可视化演示
  2. 声明式范式
  3. 事实与规则
  4. 查询与谓词
  5. 合一机制
  6. 回溯机制
  7. 递归与列表
  8. 截断与否定
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

Prolog是基于一阶逻辑的声明式编程语言,通过事实与规则描述问题。本文详解Prolog的项与谓词、合一与回溯... 切换模式查看不同维度。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Prolog:核心原理 动态展示关键流程与数据流动 交互控件调节参数实时渲染 f(x) = transform(x) 核心概念的可视化呈现

声明式范式

想象你刚被任命为城市档案库的馆长。前任留下一句话:“别给员工写步骤,把城市规则写清楚,他们自己会查。”于是档案库里有了“事实卡片”和“规则手册”。当一个市民来问“我能否进入市政厅?”,档案员不是按某个固定流程执行,而是翻找能证明答案的卡片和规则。

Prolog 就是这样一门“档案馆语言”。它不提供 for 循环或赋值语句,而是让你把领域知识写成事实与规则,再通过查询来获得答案。程序员负责描述“世界是什么样”,Prolog 引擎负责“如何推出你要的答案”。这种范式称为声明式逻辑编程。

Prolog 中的每个知识条目都是一个子句(clause)。事实是无条件成立的关系描述;规则是带条件的逻辑蕴含。程序整体可以看成一个一阶逻辑子集的知识库。用一条最简单的祖先规则来说明:

你不必告诉档案员“先查哪个抽屉、再翻哪页”,规则册已经写明方向。同样,Prolog 程序员把精力放在知识结构上,把搜索路径交给引擎。这种“只问是什么、不管怎么做”的观念,是整个逻辑编程的起点。

ancestor(X,Y)←parent(X,Y)\mathrm{ancestor}(X,Y) \leftarrow \mathrm{parent}(X,Y)
这条公式读作:如果 X 是 Y 的父母,那么 X 是 Y 的祖先。左箭头左边是结论,右边是前提。
  1. 谓词 ancestor表达“……是……的祖先”这条关系,X、Y 是待填充的参数。
    ancestor(X,Y)\mathrm{ancestor}(X,Y)
  2. 左箭头 \leftarrowProlog 中以 :- 书写,表示“只要右边成立,左边就成立”。
    A←BA \leftarrow B
  3. 前提 parent(X,Y)先证明 X 是 Y 的父母,这是知识库中事实或其它规则提供的证据。
    parent(X,Y)\mathrm{parent}(X,Y)
  4. 全称含义X、Y 都是全称变量,所有满足前提的对象对都能推出祖先关系。
    ∀X ∀Y (parent(X,Y)→ancestor(X,Y))\forall X\,\forall Y\,(\mathrm{parent}(X,Y)\rightarrow \mathrm{ancestor}(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)\mathrm{parent}(\text{amy},\text{bob})\quad\text{与}\quad\mathrm{grandparent}(X,Z) \leftarrow \mathrm{parent}(X,Y) \wedge \mathrm{parent}(Y,Z)
行末句点表示该子句结束;规则的逗号相当于“并且”。
  1. 事实 parent(amy,bob)这是无条件成立的基本断言,表示艾米是鲍勃的父母。
    parent(amy,bob)\mathrm{parent}(\text{amy},\text{bob})
  2. 规则头 grandparent(X,Z)结论部分,说明我们想定义“祖父母”关系。
    grandparent(X,Z)\mathrm{grandparent}(X,Z)
  3. 规则体 parent(X,Y), parent(Y,Z)前提条件,两个谓词用逗号连接,表示必须同时成立。
    parent(X,Y)∧parent(Y,Z)\mathrm{parent}(X,Y) \wedge \mathrm{parent}(Y,Z)
  4. 变量复用两个子目标中的 Y 必须是同一个人,这是连接条件的桥梁。
    ∃Y (parent(X,Y)∧parent(Y,Z))\exists Y\,(\mathrm{parent}(X,Y) \wedge \mathrm{parent}(Y,Z))
text2 行
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)\text{?-}\, \mathrm{parent}(\text{amy},X)
查询问的不是“计算什么”,而是“哪些证据能让这个关系成立”。
  1. 查询符 ?-表示向知识库提出一个待证明的问题。
    ?- G\text{?-}\,G
  2. 谓词 parent这是一个二元关系,第一个参数是父母,第二个参数是孩子。
    parent(⋅,⋅)\mathrm{parent}(\cdot,\cdot)
  3. 常量 amy具体对象,以小写字母开头,表示“艾米”。
    amy\text{amy}
  4. 变量 X以大写字母开头,查询时会搜索所有可能的绑定。
    XX

合一机制

档案员拿到问题单后,第一件事是把问题单和卷宗里的卡片进行比对。这个比对不是简单的“相等”,而是双向的模式匹配:如果一边是未知数,另一边是具体信息,就把未知数填上;如果两边都缺失,就让它们彼此挂上钩。这套规则叫合一。

合一的目标是让两个项完全相同。匹配过程中产生的变量绑定叫替换(substitution)。例如查询 parent(amy,X) 与事实 parent(amy,bob) 合一,成功并生成绑定 X=bob。

如果两个参数都是变量,比如 X 和 Y,合一会让 X 与 Y 绑定在一起,之后任一绑定都会同步影响另一个。这使它比普通赋值更强大,也更容易出现“双向绑定”的直觉。档案员比对的不是“一份档案是否等于另一份”,而是“两份档案能不能互相补全”。合一让 Prolog 能够在不知道答案时,“猜出”答案。

parent(amy,X)∧parent(amy,bob)⟹θ={X/bob}\mathrm{parent}(\text{amy},X) \wedge \mathrm{parent}(\text{amy},\text{bob})\quad\Longrightarrow\quad\theta = \{X/\text{bob}\}
θ 是合一的产物:一个变量到值的替换。匹配时,模式中的变量被实例化。
  1. 比较首层结构首先检查两个项的外层谓词都是 parent,元数都是 2,因此可以继续。
    parent(⋅,⋅)\mathrm{parent}(\cdot,\cdot)
  2. 比较第一个参数amy 与 amy 完全相同,匹配成功。
    amy=amy\text{amy} = \text{amy}
  3. 比较第二个参数一侧是变量 X,另一侧是常量 bob,变量被绑定为 bob。
    X↦bobX \mapsto \text{bob}
  4. 生成替换将绑定记录为 θ,完成本次合一。
    θ={X/bob}\theta = \{X/\text{bob}\}

回溯机制

档案员查案并不总是一击即中。他会先在第一个岔路口选一本手册,走到底;如果后续条件和证据对不上,就回到最近的岔路口,拿起下一本手册继续试。Prolog 正是这样:深度优先搜索 + 回溯。

每成功选择一个匹配子句,引擎就留下一个选择点。当前面的目标失败,就会撤销最近的变量绑定,退回选择点尝试下一条候选子句。这个过程会一直继续,直到求出全部答案或所有路径都失败。

回溯对应搜索树上的分支回退:失败的路径被剪掉,引擎回到最近的岔路口继续下一个分支。子句书写顺序和子目标顺序会影响搜索顺序与性能,但不影响知识库中已存在的逻辑结论。这就像档案员手中的毛线团:进死胡同时顺着线退回岔口,再换一条走廊。你用 ; 可以要求他继续找下一个答案,而回溯正是他不断折返的动力引擎。

prove(G)=⋃c∈clauses(G)prove(body(c))\mathrm{prove}(G) = \bigcup_{c \in \mathrm{clauses}(G)} \mathrm{prove}(\mathrm{body}(c))
它的意思是:要证明 G,就遍历所有能匹配 G 的子句,逐一证明每个子句的规则体,把所有成功路径合并起来。
  1. 目标 G当前要证明的查询或子目标。
    GG
  2. 候选子句集 clauses(G)知识库中所有能与 G 合一的子句。
    c∈clauses(G)c \in \mathrm{clauses}(G)
  3. 规则体 body(c)事实的规则体为空,规则的规则体是若干子目标的列表。
    body(c)\mathrm{body}(c)
  4. 求并集 \bigcup把每条候选路径得到的解都收集起来,就是所有答案。
    prove(G)=⋃cprove(body(c))\mathrm{prove}(G) = \bigcup_c \mathrm{prove}(\mathrm{body}(c))

递归与列表

档案库中许多关系是层层递进的:要查祖先,先查父母;要查父母的父母,再往上……Prolog 用递归描述这种链条。递归规则必须包含一个最直接的“底”和一条把问题缩小一步的“梯子”。

以家族祖先为例:祖先可以被定义为父母,或父母的祖先。这就是两个子句:一个终止条件(base case)和一个递归条件。列表是 Prolog 最常用的递归结构,用 [H|T] 把头 H 和尾列表 T 分开。计算列表长度可以这样写:

递归时每次都要先写边界条件,否则查询会无限循环。列表处理也常依靠递归:访问每个元素、拼接两个列表、查找元素,本质都是在 [H|T] 上反复拆分。档案员查祖先时,从当前卷宗出发,沿“父母栏”找到下一个卷宗,再次翻看。列表递归就像这条卷宗链:你只需要知道如何处理当前的这一格,然后放心地把剩下的链条交给下一层。Prolog 的递归之美就在于此。

length([],0)且length([H∣T],N)←length(T,M)∧N=M+1\mathrm{length}([],0)\quad\text{且}\quad\mathrm{length}([H|T],N) \leftarrow \mathrm{length}(T,M) \wedge N = M + 1
第一条规则说空列表长度为 0;第二条规则说非空列表的长度等于尾列表长度加 1。
  1. 空列表边界length([], 0) 是递归的终止条件,避免无限下降。
    length([],0)\mathrm{length}([],0)
  2. 列表分解[H|T] 把非空列表拆成头部元素 H 和剩余列表 T。
    [H∣T][H|T]
  3. 递归调用先求尾列表 T 的长度 M。
    length(T,M)\mathrm{length}(T,M)
  4. 汇总长度整体长度 N 等于 M 加 1,完成一个递推步骤。
    N=M+1N = M + 1

截断与否定

档案库偶尔会遇到“此案不必再审”的情况。比如规则写明:某人的父亲一旦确定,就不再尝试其他候选人。Prolog 用截断 ! 表示:当执行到它时,冻结当前已经做出的选择,丢弃之后的选择点。

截断常用在规则体里,用来告诉引擎:此处之前的选择已经确定,不要再回来尝试其它子句。比如 p(X) :- q(X), !, r(X). 当 q(X) 成功后就执行 !,后续 r(X) 即使失败也不会再尝试其它 p 子句。

否定即失败(negation as failure)是另一个重要机制。\+ G 表示“如果能证出 G 就失败,如果证不出 G 就成功”。它基于封闭世界假设:知识库里没有证据,就默认不成立。可以写成:

注意,截断能大幅提升性能,但也可能把本应找到的答案剪掉;否定同样依赖知识库的完整程度。档案库中这一切都很直观:“盖棺定论”的截断和“查无此证”的否定,都是带着前提的高效工具,用错就会误判。

p(X)←q(X)∧!∧r(X)p(X) \leftarrow q(X) \wedge ! \wedge r(X)
它不是说“G 在现实世界一定为假”,而是“当前知识库中证明不了 G”。
  1. 语法 \\+ GProlog 中对目标 G 的否定形式,常读作“否定即失败”。
    not(G)\mathrm{not}(G)
  2. 证明失败引擎搜索整个知识库后找不到任何证明路径。
    prove(G)⇒⊥\mathrm{prove}(G) \Rightarrow \bot
  3. 封闭世界假设没有记载的证据,就按不成立处理。
    未记载⇒不成立\text{未记载} \Rightarrow \text{不成立}
  4. 与经典否定区别它不是逻辑上的“G 为假”,而是“当前知识库证明不了 G”。
    not(G)≠¬G经典\mathrm{not}(G) \neq \neg G^{\text{经典}}

AI 应用场景

法律条款自动审查

阶段:知识表示与推理

解决的问题:传统法务审查需要人肉翻法规、比对多个例外条款,法规量增大后容易遗漏。AI 把条款写成事实和规则,让查询端口自动完成合一、回溯,并给出可解释的推理链。

具体案例:某合规系统把“未备案的数据传输”定义为违规:violation(X) :- transfer(X), not(exempted(X)). 业务人员输入一个操作编号,系统通过回溯返回违规结论以及触发该结论的条款,就像档案员翻出对应的卷宗和法条。

大学选课规划器

阶段:约束求解与路径搜索

解决的问题:课程依赖关系复杂,涉及先修课、时间冲突、学分限制,命令式代码要写大量 if 分支。用 Prolog 声明式地描述依赖,学生提问后引擎会自动生成可选方案。

具体案例:把“人工智能需要先修离散数学”写成 requires(ai, discrete_math).,把“已修课程”写成事实,查询 ?- can_take(me, ai). 时,引擎通过合一和回溯检查先修链是否全部满足。

Python 演示

python80 行
#!/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"])
目的:用 Python 实现一个极简的 Prolog 推理引擎,展示事实、规则、合一、递归和回溯如何协作,最终打印出“艾米的所有祖先”。
  1. FACTS = [用元组模拟 Prolog 事实表,每个三元组是一条亲子断言。
  2. RULES = [模拟规则:第一条直接证明祖先是父母,第二条通过中间人递归。
  3. def unify(a, b, env):合一函数,返回新的绑定或 None,处理变量和常量的双向匹配。
  4. if isinstance(a, tuple) and isinstance(b, tuple):递归比较复合项的每个参数,相当于比对谓词的内部结构。
  5. for fact in FACTS:遍历事实数据库,逐个尝试合一。
  6. for head, body in RULES:尝试用规则匹配目标,再证明规则体。
  7. yield from prove(b + rest, m)把规则体子目标加入待证明队列,执行深度优先搜索与回溯。
  8. 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). 在知识库里属于什么?

单选题 · easy

  • A.规则
  • B.事实← 正确答案
  • C.查询
  • D.截断

答案:B。它没有 :-,也没有条件,是一个无条件成立的基本断言,相当于档案库里一张事实卡片。规则必须包含 :-,查询以 ?- 开头,截断是 !。

Q2. 查询 ?- parent(tom, X). 中的 X 是什么?

单选题 · easy

  • A.原子常量
  • B.未绑定变量← 正确答案
  • C.谓词名
  • D.子句

答案:B。X 以大写字母开头,是查询中等待合一填充的变量。原子常量以小写字母开头,parent 是谓词名。档案员会把 X 当作问题单上的空栏等待填写。

Q3. Prolog 搜索多个答案时主要采用什么策略?

单选题 · mid

  • A.随机搜索
  • B.深度优先加回溯← 正确答案
  • C.广度优先
  • D.贪心搜索

答案:B。引擎从第一个匹配子句开始向下证明,失败后回到最近的选择点,这是深度优先加回溯。档案员沿着岔路一条道走到底,发现死胡同再退回。

Q4. 关于合一,下列说法正确的是?

多选题 · mid

  • A.只能绑定常量,不能绑定变量
  • B.允许两个变量互相绑定← 正确答案
  • C.通过匹配让两个项变得完全一致← 正确答案
  • D.两个不同常量也能合一

答案:BC。合一是双向模式匹配,两个变量可以互相绑定,目标是让两个项完全一致。不同常量无法合一,否则匹配失败,因此 A、D 错误。

Q5. 递归规则必须首先定义什么?

单选题 · mid

  • A.截断
  • B.边界条件← 正确答案
  • C.列表
  • D.副作用

答案:B。没有边界条件,递归会一直调用自身直到栈溢出。边界条件是递归链的末端,档案员必须有一个“查到底”的停止点。

Q6. 截断 ! 的效果是?

多选题 · hard

  • A.只加快速度不改变答案
  • B.剪掉选择点← 正确答案
  • C.可能改变程序逻辑结果← 正确答案
  • D.相当于逻辑否定

答案:BC。! 冻结之前的选择并剪掉选择点,可能剪掉本可以找到的答案,因此不是单纯的优化,更不是逻辑否定。像档案员下令“不再翻旧账”。

Q7. Prolog 中 \+ goal 的意思是?

单选题 · mid

  • A.知识库中无法证明 goal← 正确答案
  • B.goal 在现实世界一定为假
  • C.执行剪枝
  • D.启动调试

答案:A。否定即失败。它基于封闭世界假设:当前档案库没有登记,就当作不成立;但“查无此证”不等于现实为假。

Q8. 知识库为 parent(a,b). parent(b,c).,查询 ancestor(a,c) 要获得答案,通常需要?

单选题 · hard

  • A.只有截断
  • B.合并且递归规则← 正确答案
  • C.列表处理
  • D.否定即失败

答案:B。ancestor 需要规则:直接父母关系是一条规则,祖先的祖先是一条递归规则。查询通过合一匹配 parent(a,b)、parent(b,c) 并回溯得到答案。

Q9. 列表 [H|T] 中 H 和 T 分别表示?

单选题 · easy

  • A.头部元素和剩余列表← 正确答案
  • B.两个列表
  • C.头部列表和尾部元素
  • D.随机元素

答案:A。[H|T] 把列表拆成头部元素 H 和剩余列表 T,是 Prolog 递归处理列表的基础。档案员拿到一沓卷宗,先取最上面一份,再把剩下的链给下一层。

Q10. Prolog 属于哪种编程范式?

单选题 · easy

  • A.命令式编程
  • B.面向对象编程
  • C.声明式逻辑编程← 正确答案
  • D.数据库查询语言

答案:C。Prolog 用事实和规则描述关系,查询驱动推理,属于声明式逻辑编程。C/Python 主要是命令式;虽然它常用于知识库,但并不只是一门查询语言。