跳到主内容
快讯直播
AI智模界
教程

用 Lean 4 + LLM 把数学猜想变成可验证证明

读完这篇你能拿到三个具体产物:一个装了 mathlib 的 Lean 4 工程;一个"命题被冻结、只让模型填证明"的半自动回路;一份 sorry 审计报告,能明确告诉你哪些定理是真证出来的、哪些还挂着洞。整个流程在一个下午能跑通,产出是一个陈述 100% 编译通过、证明还剩若干个 sorry 的文件——这本身就是有价值的中间成果,因为形式化领域公认最难的一半是"把猜想写对",不是"把证明写完"。

先把心智模型立住,后面所有操作都围绕它:

> Lean 验证的是"你给出的证明项,确实证明了你自己写的那个命题"。它验证命题是否为真,也验证命题是不是你心里想的那个。

所以"编译通过"和"猜想成立"是两件事。把这两件事分开,你才不会被 LLM 骗。

前置条件清单

动手前确认这些就位:

  • 一台能装 Linux/macOS 的机器,Node 与 Python 3 环境;
  • 磁盘预留若干 GB(mathlib 的源码加预编译缓存体积不小,具体以官方仓库说明为准);
  • 编辑器用 VS Code + Lean 4 官方扩展,能实时看到 goal 状态——这一步体验提升巨大,别省;
  • 一个能通过 API 调用的 LLM(本地部署也行),要求能接受长文本输入并稳定输出代码;
  • 命令行基础:cdgrepexport

不需要你懂 Lean 的证明策略细节。需要你有耐心看编译器报错。

第一步:装工具链,跑通"Hello World"

Lean 4 的标准做法是用 elan 管理工具链版本,用 lake 做构建。安装脚本地址以 Lean 官方页面为准,这里用占位符表示:

```bash

安装脚本的准确 URL 以 Lean 官方文档为准

curl -sSf <官方 elan 安装脚本 URL> | sh

source ~/.profile

elan --version

elan toolchain list

```

然后建一个依赖 mathlib 的工程:

```bash

模板名与参数以 lake 官方文档为准

lake new ns_demo math

cd ns_demo

lake update

lake exe cache get # 拉取 mathlib 预编译缓存,强烈建议先做这步

lake build

```

lake exe cache get 是关键。没有它,第一次编译 mathlib 会等到你怀疑人生。

验证环境:

```lean

import Mathlib

example (a b : ℕ) : a + b = b + a := by

exact Nat.add_comm a b

```

把这段存成 Scratch.lean,用 lake env lean Scratch.lean 单独跑。试验文件用 lake env lean,正式定理放进库目录再 lake build,这样编译速度快很多。

第二步:把猜想写成"冻结的命题"

这一节是整篇文章最重要的部分。

假设你想碰 Navier–Stokes 方向。三维 Navier–Stokes 全局正则性这种级别的问题,完整形式化是多年工程(社区里已有 PDE 形式化的持续努力,具体进展以官方仓库为准),不适合作为第一天的目标。正确做法是分层降级:先形式化一个真实但小得多的能量不等式玩具模型,把流水线跑通。

```lean

import Mathlib

set_option autoImplicit false

/-- 玩具模型:一维标量版本的"能量衰减"。

真实 NS 能量估计是 ‖u(t)‖² + 2ν∫₀ᵗ‖∇u‖² ≤ ‖u(0)‖²,

这里先砍掉空间维度与梯度项,只留下"耗散让能量不增"的骨架。 -/

theorem toy_energy_decay

(ν : ℝ) (hν : 0 < ν) (u : ℝ → ℝ)

(h : ∀ t, 0 ≤ t → HasDerivAt u (-(ν * u t)) t) :

∀ t, 0 ≤ t → (u t) ^ 2 ≤ (u 0) ^ 2 := by

sorry

```

再看离散几何方向。Erdős–Szekeres 是这块的经典热点,二维"凸位置"版本需要 orientation、凸包等一大堆几何定义,同样先降级到一维单调子序列版本:

```lean

/-- 一维 Erdős–Szekeres:长度 (r-1)(s-1)+1 的互异实数序列,

必有长度 r 的递增子序列或长度 s 的递减子序列。 -/

theorem erdos_szekeres_one_dim

{n r s : ℕ} (hr : 0 < r) (hs : 0 < s)

(hn : (r - 1) * (s - 1) + 1 ≤ n)

(a : Fin n → ℝ) (hinj : Function.Injective a) :

(∃ f : Fin r → Fin n, StrictMono f ∧ StrictMono (a ∘ f)) ∨

(∃ g : Fin s → Fin n, StrictMono g ∧ StrictAnti (a ∘ g)) := by

sorry

```

现在关键技巧来了:别把命题写在 theorem 的签名里。把它抽成一个 def,让命题变成不可篡改的常量:

```lean

def Conjecture_ES1D : Prop :=

∀ {n r s : ℕ}, 0 < r → 0 < s → (r - 1) * (s - 1) + 1 ≤ n →

∀ (a : Fin n → ℝ), Function.Injective a →

(∃ f : Fin r → Fin n, StrictMono f ∧ StrictMono (a ∘ f)) ∨

(∃ g : Fin s → Fin n, StrictMono g ∧ StrictAnti (a ∘ g))

theorem es_1d : Conjecture_ES1D := by

sorry

```

为什么要这样?因为 LLM 最容易犯的错,不是证不出来,而是偷偷把命题改弱——把 < 换成 、悄悄加一个前提、把量词顺序调一下。一旦命题被抽成 def Conjecture_ES1D,模型无论怎么改证明体,都不可能改到命题本身。它要是想改,只能去改 def,那你一眼就能看见。

顺带一句:def 定义 Prop 不需要证明,所以这段代码本身就能编译通过。sorry 也不是错误,只是警告——这是 Lean 的设计意图,但对 CI 是灾难,后面必须审计。

第三步:让 LLM 只填证明,不碰陈述

写一个 Python 回路:读文件 → 编译 → 把错误喂回模型 → 让模型只输出证明体 → 校验命题指纹没变 → 写回 → 再编译。

```python

import subprocess, pathlib, re, hashlib

ROOT = pathlib.Path(".").resolve()

TARGET = ROOT / "NsDemo" / "Target.lean"

def run_lean(timeout=600):

"""单独编译一个试验文件,比 lake build 快。"""

p = subprocess.run(

["lake", "env", "lean", str(TARGET)],

cwd=ROOT, capture_output=True, text=True, timeout=timeout,

)

return p.returncode, (p.stdout + p.stderr)

def count_sorry(text: str) -> int:

粗略计数,正式审计用 #print axioms

return len(re.findall(r"\bsorry\b", text))

def statement_of(text: str) -> str:

"""取 := 之前的部分当作命题指纹,防止模型悄悄改陈述。"""

return text.split(":=")[0].strip()

def fingerprint(text: str) -> str:

return hashlib.sha256(statement_of(text).encode()).hexdigest()[:12]

def extract_lean(reply: str) -> str:

m = re.search(r"``lean\s*(.*?)``", reply, re.S)

return (m.group(1) if m else reply).strip()

PROMPT = """你是一个 Lean 4 / mathlib 证明助手。

【硬性约束】

1. 只允许修改定理的证明体(by 后面的部分)。

2. 绝对不允许修改定理名、参数、假设、结论,一个字都不许动。

3. 不允许新增 axiom,不允许用 sorry 之外的占位。

4. 如果一步证不完,就输出多个子引理,每个子引理体用 sorry,

并在注释里写清楚这个子引理打算怎么证。

【当前文件】

{code}

【编译器输出】

{errors}

只输出一个 ```lean 代码块,里面是完整的、可直接落盘的文件内容。"""

这里接你自己的模型 SDK;以你所用服务的官方文档为准

def ask_llm(prompt: str) -> str:

raise NotImplementedError("接入你用的模型客户端")

def solve(max_rounds: int = 12):

base_fp = fingerprint(TARGET.read_text())

for i in range(max_rounds):

code = TARGET.read_text()

rc, err = run_lean()

n_sorry = count_sorry(code)

print(f"[round {i}] rc={rc} sorry={n_sorry}")

if rc == 0 and n_sorry == 0:

print("没有 sorry 且编译通过,进入人工复核。")

return True

reply = ask_llm(PROMPT.format(code=code, errors=err[-6000:]))

new_code = extract_lean(reply)

if fingerprint(new_code) != base_fp:

print("!! 检测到命题被修改,拒绝写回。")

continue

TARGET.write_text(new_code + "\n")

return False

if __name__ == "__main__":

solve()

```

几个细节值得强调:

  • 错误只截取尾部若干字符喂回去,不要贴整个编译日志,否则上下文立刻爆掉;
  • 第一次迭代几乎必然会报错,这很正常,报错本身是最有价值的信号;
  • 每次写回前校验指纹——这一步能挡掉绝大多数"模型自作聪明"事故。

第四步:用 #print axioms 做 sorry 审计

grep sorry 只看字面量,模型把漏洞藏进一个中间引理时你就漏了。真正的审计手段是 #print axioms

```lean

-- 放在文件末尾

#print axioms es_1d

#print axioms toy_energy_decay

```

编译时 Lean 会打印这个定理依赖的公理清单。正常情况下你会看到 propextClassical.choiceQuot.sound 这三条 mathlib 标准公理;一旦出现 sorryAx,说明证明里还有洞。这一步是"可验证"三个字的落点,务必写进 CI。

```bash

简易守门脚本:出现 sorryAx 就退出非零

lake env lean NsDemo/Target.lean | grep -q "sorryAx" && echo "存在未完成证明" && exit 1

```

常见坑与排错

sorry 当成"证完了"。 sorry 会让文件编译通过,CI 会绿。必须靠 #print axioms 兜底。

模型偷偷改弱命题。 前面 def Conjecture + 指纹校验就是治这个的。再加一道保险:另写一个 example : Conjecture_ES1D := es_1d,如果命题被改过,这行会直接报类型不匹配。

幻觉引理名。 模型会一本正经地写出不存在的 Mathlib.Foo.bar_lemma。不要跟它争,直接在文件里加 #check 那个名字,编译一次就知道真假。更好的办法是让 Lean 自己找:exact?apply?simp? 这类搜索式 tactic 会用当前 goal 去检索 mathlib,比模型猜准得多。

隐式变量带来的诡异错误。 在文件开头写 set_option autoImplicit false。否则模型打错的变量名会被 Lean 当成隐式自由变量默默接受,最后给你一个莫名其妙的类型错误。

版本漂移。 toolchain 和 mathlib 必须一起更新,具体版本约束以工程的 lakefile 为准。别手动改 toolchain 文件,用 elanlake update

大定理一步到位。 让模型一次证完 Erdős–Szekeres 是不现实的。正确姿势是让它先吐 3 到 5 个子引理的签名,每个都用 sorry,然后再逐个攻破。这一步"先规划后证明"的效果,比反复重试同一目标好得多。

超时与 tactic 爆炸。 复杂的 simp 会跑很久。可以临时 set_option maxHeartbeats 400000 抬高上限,但更该做的是把目标拆小。

上下文塞太多。 别把整个 mathlib 源码喂进去。给签名、给报错、给几条 #check 结果就够了。

下一步建议

先练陈述,再练证明。 把形式化工作量按 7:3 分配——七成时间花在把猜想精确地写成 Prop,三成给证明。你可以专门做一个"只写陈述、全部 sorry"的文件集合当 benchmark,这个练习对判断模型能力上限非常有用。

引入自动化 tactic 打底。 omega 管线性整数算术,linarith/nlinarith 管线性与多项式不等式,positivity 管正性,gcongr 管不等式链,aesop 做通用搜索。让模型先试这些,失败再写手工证明,成功率会明显提升。

从一维走向二维。 一维 Erdős–Szekeres 跑通后,二维版本的增量工作主要是定义"一般位置""凸位置"这类几何谓词,然后证明一维结论能提升上去。这条路径比直接冲三维 Navier–Stokes 现实得多。

把 sorry 数量做成指标。 每次提交记录剩余 sorry 数,只允许下降不允许上升。这个朴素规则比任何"证明质量评估"都管用。

人永远保留最后一道关。 LLM 负责生成证明草稿、搜索引理、搬运繁琐代数;你负责把命题写对、判断这个定理值不值得证、以及审查最终 #print axioms 的干净程度。这个分工在可预见的未来都不会变。

跨领域迁移。 这套"冻结命题 → 编译 → 报错回喂 → 审计公理"的回路,和领域无关。换成图论、组合优化、密码学协议的安全性证明,流程完全一样。真正的门槛从来不是 Lean 的语法,是你有没有能力把一句中文的数学直觉,翻译成一个编译器能接受的、不多不少刚刚好的 Prop

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。