昨天(10 月 27 日)量子位报道了一条挺炸裂的新闻:OpenAI 在发新模型 GPT-6.1 Terra 的同一天,顺手公布了三项 AI 系统产出的科学成果,横跨数学、计算机科学和物理:
- 证明了局部平滑猜想(数学家 Sogge 1991 年提出,30 多年没人完全拿下),还顺带解决了 Kakeya 猜想和 Stein 限制猜想
- 宣称证伪了 ETH——指数时间假说。这个假说大致认为,像 3-SAT 这类复杂问题很难找到比穷举快得多的算法,过去几十年一大堆计算机科学结论都建立在它成立的基础上。现在 OpenAI 说,它的 AI 系统证明了这个地基是错的
- 给出了包括量子色动力学(QCD)在内的某些四维量子场论的存在性证明——千禧年大奖难题"杨-米尔斯"的一部分(质量间隙还没解决,百万美元暂时还领不走)
两个限定必须先钉死,免得成谣言:第一,这是 OpenAI 自己宣称的结果,数学界还在审查确认中(物理部分正由诺奖得主 David Gross 等外部专家把关);第二,证伪 ETH 不等于解决了 P vs NP,也不意味着明天就有实用新算法。
但今天我想聊的不是这三个成果本身,是报道里一个容易被划过去的细节:这三项成果不是只甩出一段自然语言推导就完事——数学与计算机科学结果的 Lean 形式化证明已经一并公开,谁都可以拿去机器复验。
这个细节才是重点。
Lean 是什么?数学的编译器
Lean 是一个形式化证明系统。你把数学证明写成 Lean 代码,它像编译器一样逐行检查每一步推导是否严格合法——公理用错了、跳步了、类型对不上,编译直接报错,一个符号都糊弄不过去。
这就是为什么 OpenAI 敢把"证伪 ETH"这种动摇地基的结论放出来:AI 写的证明过了 Lean 的编译,等于拿到了机器背书。没人需要"相信"这个 AI,大家只需要相信编译器。
再说回咱们日常干活。你让 AI 写周报、写促销文案、写行业快讯——它写得一样流畅、一样自信、一样"看起来对"。但它写"3 天后是周五"(实际是周六)、“12 万比 8 万多了 5 万”(明明是多 4 万)的时候,没有任何东西会报错。
差别就在这儿:
数学有编译器,自然语言没有。 AI 在 Lean 里错一步就编译失败;在你的文档里错一百处,排版照样精美。所谓幻觉检测,说白了就是给自然语言补一个编译器。
下面我用 176 行标准库代码搭一个最小版本。思路直接抄编译器:把文本里的声明分三类,能算的当场算,要查资料的走"链接",判不了的标出来转人工。
核心设计:三种声明,三条流水线
编译器不是万能的,它只管它能判的。这个"文本编译器"也一样,先把声明分三类:
| 类型 | 类比 | 例子 | 怎么处理 |
|---|---|---|---|
| COMPILE | 编译期检查 | “12 万比 8 万多 5 万”、“3 天后是周五” | 机器可判,当场复算,有错就报 ERROR |
| LINK | 链接外部库 | “《科学》创刊于 1880 年” | 编译器管不了,交给"链接器"查外部符号表;查不到就是 undefined reference |
| COMMENT | 注释 | “这大概是今年最重要的新闻” | 观点/预测句,编译器不判真伪,跳过、不放行 |
这里有个关键观念:查不到 ≠ 没问题。链接器找不到符号时会直接报错停下,不会假装链接成功。放到文本核验上就是一句话:检索不到证据的事实声明,不能放行。
代码 1:安全求值——编译器的第一课是别用 eval
AI 文本里抽出的算术式要算一遍。第一反应是 eval("300 + 45")——打住,eval 能执行任意代码,文本里混入 __import__("os") 你就完了。正确做法是用 AST(抽象语法树)做白名单,只放行数字和四则运算:
"""给自然语言装一个「编译器」。
数学有 Lean:证明里错一步,编译就过不去。
自然语言没有编译器:AI 写错一个数,句子照样通顺。
这个脚本演示「声明编译器」的三段式:
解析(从文本抽出声明)→ 编译(机器可判的,确定性复算)→ 链接(事实类,检索核验)
链接不到的事实就是 undefined reference;观点句是注释,编译器不查。
纯标准库,Python 3.10+。保存为 nlcompile.py 后 `python nlcompile.py` 跑 demo。
"""
from __future__ import annotations
import ast
import re
from dataclasses import dataclass
from datetime import date, timedelta
# ---------- 前端:安全求值 ----------
# 编译器第一课:别用 eval()。eval 能执行任意代码,
# 这里用 AST 白名单——只允许数字和 +-*/,其它语法树节点直接拒。
_BIN = {ast.Add: lambda a, b: a + b, ast.Sub: lambda a, b: a - b,
ast.Mult: lambda a, b: a * b, ast.Div: lambda a, b: a / b}
_UNARY = {ast.USub: lambda a: -a, ast.UAdd: lambda a: +a}
def safe_eval(expr: str) -> float:
def walk(node: ast.AST) -> float:
if isinstance(node, ast.Expression):
return walk(node.body)
if isinstance(node, ast.Constant) and isinstance(node.value, (int, float)):
return float(node.value)
if isinstance(node, ast.BinOp) and type(node.op) in _BIN:
return _BIN[type(node.op)](walk(node.left), walk(node.right))
if isinstance(node, ast.UnaryOp) and type(node.op) in _UNARY:
return _UNARY[type(node.op)](walk(node.operand))
raise ValueError(f"非法语法:{ast.dump(node)}")
return walk(ast.parse(expr.strip(), mode="eval"))
思路跟编译器一样:别信输入文本,只认白名单里的语法结构。一句话:文本是数据,不是代码。凡是处理 AI 输出的系统,都得守住这条。
代码 2:解析——从文本里抽出"能判的"声明
# ---------- 声明:三种「可判定性」 ----------
# COMPILE:机器可判(算术、日期)——有标准答案,当场复算
# LINK: 检索可判(事实、年份)——编译器管不了,交给链接器查外部符号表
# COMMENT:判不了(观点、预测)——编译器眼里的注释,跳过但不放行
@dataclass
class Claim:
phase: str
raw: str
data: tuple
def _wan(s: str, unit: str | None) -> float:
return float(s) * (10_000 if unit == "万" else 1)
def parse_claims(text: str) -> list[Claim]:
"""lexer + parser:只收结构完整的声明,收不进来的句子不参与编译。"""
claims: list[Claim] = []
for m in re.finditer(r"([\d.]+)\s*(万)?[^\d。;]{1,15}?比[^\d。;]{0,10}?"
r"([\d.]+)\s*(万)?[^\d。;]{0,6}?多(?:了)?\s*([\d.]+)\s*(万)?", text):
a = _wan(m.group(1), m.group(2)); b = _wan(m.group(3), m.group(4))
c = _wan(m.group(5), m.group(6))
claims.append(Claim("COMPILE", m.group(0), ("sub", a, b, c)))
for m in re.finditer(r"([\d.+\-*/x×()()][\d.+\-*/x×()()\s]*?)\s*=\s*([\d.]+)", text):
claims.append(Claim("COMPILE", m.group(0),
("expr", m.group(1).replace("×", "*").replace("x", "*"),
float(m.group(2)))))
for m in re.finditer(r"(\d+)\s*天后是(周[一二三四五六日天])", text):
claims.append(Claim("COMPILE", m.group(0), ("date", int(m.group(1)), m.group(2))))
for m in re.finditer(r"《([^》]+)》[^\d。;]{0,4}?创刊于\s*(\d{4})\s*年", text):
claims.append(Claim("LINK", m.group(0), ("founded", m.group(1), int(m.group(2)))))
# 带观点词的句子一律按注释处理(放最后,机器可判的优先)
for s in re.split(r"[。;]", text):
if any(w in s for w in ("最", "大概", "可能", "将会", "无疑")) \
and not any(c.raw in s for c in claims):
claims.append(Claim("COMMENT", s.strip(), ()))
return claims
两个设计决定要说清楚:
先抓谁,有讲究。 机器能判的模式先抓,观点判断放最后。比如"这大概是最大的 3 + 4 = 7 的增长"这种句子,只要里面有能复算的算式,就先按算式判,不能因为有"大概"俩字就整句放行。
观点句不判对错。 编译器不检查注释写得好不好,咱们这个编译器也不判观点的真伪——标出来,转人工。这不是功能缺失,是这事儿本来就不归机器管。
代码 3:编译 + 链接——三类声明各走各的门
# ---------- 编译 + 链接 ----------
_WEEKDAYS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
@dataclass
class Diag: # 诊断信息,对齐编译器的 error / warning
level: str # OK / ERROR / LINKED / UNDEFINED / SKIPPED
raw: str
msg: str
def render(self) -> str:
icon = {"OK": "✅", "ERROR": "❌", "LINKED": "🔗",
"UNDEFINED": "⚠️", "SKIPPED": "💤"}[self.level]
return f"{icon} {self.level:<9} {self.raw} -- {self.msg}"
def compile_text(text: str, *, anchor: date, corpus: dict[str, int]) -> list[Diag]:
"""anchor:「N 天后」的锚定日期。corpus:外部符号表(检索结果的替身)。"""
diags: list[Diag] = []
for c in parse_claims(text):
if c.phase == "COMMENT":
diags.append(Diag("SKIPPED", c.raw, "观点/预测句,编译器不判真伪,转人工"))
continue
if c.phase == "LINK":
_, name, year = c.data
if name not in corpus:
diags.append(Diag("UNDEFINED", c.raw,
f"undefined reference to 「{name}」:语料查不到,别放行"))
elif corpus[name] == year:
diags.append(Diag("LINKED", c.raw, f"符号表核对一致({name} = {year})"))
else:
diags.append(Diag("ERROR", c.raw,
f"声称 {year},符号表是 {corpus[name]}——事实性幻觉"))
continue
# COMPILE
kind = c.data[0]
if kind == "sub":
_, a, b, claimed = c.data
actual = a - b
if abs(actual - claimed) < 1e-9:
diags.append(Diag("OK", c.raw, f"{a:g} - {b:g} = {actual:g},编译通过"))
else:
diags.append(Diag("ERROR", c.raw,
f"左边 {a:g} - {b:g} = {actual:g},声称 {claimed:g},"
f"差 {abs(actual - claimed):g}"))
elif kind == "expr":
_, expr, claimed = c.data
try:
actual = safe_eval(expr)
except (ValueError, SyntaxError) as e:
diags.append(Diag("ERROR", c.raw, f"表达式无法解析:{e}"))
continue
if abs(actual - claimed) < 1e-9:
diags.append(Diag("OK", c.raw, f"{expr.strip()} = {actual:g},编译通过"))
else:
diags.append(Diag("ERROR", c.raw,
f"{expr.strip()} = {actual:g},声称 {claimed:g}"))
elif kind == "date":
_, n, claimed_wd = c.data
claimed_wd = claimed_wd.replace("周天", "周日")
real = _WEEKDAYS[(anchor + timedelta(days=n)).weekday()]
if real == claimed_wd:
diags.append(Diag("OK", c.raw, f"{anchor} + {n}天 = {real},编译通过"))
else:
diags.append(Diag("ERROR", c.raw,
f"{anchor} + {n}天 = {real},声称 {claimed_wd}"))
return diags
注意日期复算里的 anchor:「3 天后是周几」必须锚定一个绝对日期才能判。AI 写日期爱用"明天"“三天后"这种相对词,而它的上下文里往往没有"今天几号”——日期幻觉的根源就在这。所以 anchor 是强制传入的关键字参数,藏不起来、漏不掉。
UNDEFINED 这一档得多说两句:「《柳叶刀》创刊于 1823 年」这句话本身没毛病(1823 年创刊是真的),但符号表里没收录《柳叶刀》——真的也不放行。为啥?系统验证不了它,放行它的理由就只剩"看着像真的",幻觉就是这么混过去的。编译器也是这个脾气:符号表里查不到,就是 undefined reference,链接直接失败,哪怕那个函数在别的库里真的存在。
串起来跑一遍:一段 AI 快讯的"编译报告"
demo 直接拿 ETH 新闻编一段 AI 生成的快讯:五句话、6 条声明,每种命运都埋了一条。_main 完整如下(含打印逻辑),复制就能跑:
# ---------- demo ----------
def _main() -> None:
# 一段 AI 写的「科研快讯」,五句话五种命运
ai_text = (
"OpenAI 同日甩出三项科学成果,相关 Lean 证明 12 万行,比上一版的 8 万行多了 5 万行。"
"其中复杂性理论部分用的引理合计 300 + 45 = 355 条。"
"《科学》杂志创刊于 1880 年,《柳叶刀》创刊于 1823 年。"
"这大概是今年最重要的 AI 新闻,没有之一。"
"外部审查 3 天后是周五公布初步意见。"
)
corpus = {"科学": 1880} # 符号表里只有《科学》,没有《柳叶刀》
anchor = date(2026, 10, 28) # 周三
print("=" * 66)
print("输入(AI 生成的快讯):")
print(f" {ai_text}")
print(f"锚定日期:{anchor}(周三) 符号表:{corpus}")
print("=" * 66)
diags = compile_text(ai_text, anchor=anchor, corpus=corpus)
for d in diags:
print(d.render())
print("=" * 66)
n_err = sum(1 for d in diags if d.level == "ERROR")
n_ok = sum(1 for d in diags if d.level in ("OK", "LINKED"))
n_skip = sum(1 for d in diags if d.level in ("UNDEFINED", "SKIPPED"))
print(f"nlcompile: {len(diags)} 个声明 → {n_ok} 通过,{n_err} 编译错误,{n_skip} 未放行")
print("结论:有错就拦,查不到就不放,观点不替人下结论——像编译器一样。")
print("=" * 66)
if __name__ == "__main__":
_main()
python nlcompile.py 的实际输出(本地实测):
==================================================================
输入(AI 生成的快讯):
OpenAI 同日甩出三项科学成果,相关 Lean 证明 12 万行,比上一版的 8 万行多了 5 万行。其中复杂性理论部分用的引理合计 300 + 45 = 355 条。《科学》杂志创刊于 1880 年,《柳叶刀》创刊于 1823 年。这大概是今年最重要的 AI 新闻,没有之一。外部审查 3 天后是周五公布初步意见。
锚定日期:2026-10-28(周三) 符号表:{'科学': 1880}
==================================================================
❌ ERROR 12 万行,比上一版的 8 万行多了 5 万 -- 左边 120000 - 80000 = 40000,声称 50000,差 10000
❌ ERROR 300 + 45 = 355 -- 300 + 45 = 345,声称 355
❌ ERROR 3 天后是周五 -- 2026-10-28 + 3天 = 周六,声称 周五
🔗 LINKED 《科学》杂志创刊于 1880 年 -- 符号表核对一致(科学 = 1880)
⚠️ UNDEFINED 《柳叶刀》创刊于 1823 年 -- undefined reference to 「柳叶刀」:语料查不到,别放行
💤 SKIPPED 这大概是今年最重要的 AI 新闻,没有之一 -- 观点/预测句,编译器不判真伪,转人工
==================================================================
nlcompile: 6 个声明 → 1 通过,3 编译错误,2 未放行
结论:有错就拦,查不到就不放,观点不替人下结论——像编译器一样。
==================================================================
6 条声明,三条流水线,各归各的:算术错的当场编译错误(“12 万比 8 万多 5 万”,差了整整 1 万,人眼扫过去毫无感觉);事实对上的链接成功;符号表没有的(哪怕它是真的)undefined reference;观点句跳过不背锅。
这就是"给自然语言装编译器"的意思:AI 该怎么写还怎么写,但写完能不能发出去,先过机器这道关。
边界也得说清楚
- 覆盖范围取决于模式库。 没收录的句式抓不到,抓不到就不参与编译——宁可漏,不可错抓。生产系统里这层是 LLM 抽取 + 规则兜底,比这里的正则宽得多。
- 符号表必须可信。 corpus 是"编译器信任的外部库",它错了,LINKED 就是给错误盖章。生产里这一层是搜索核验,要多来源交叉。
- 判不了不等于没用。 把观点句、查不到的声明明确标出来,读者至少知道哪几句没人担保——这比"全文看起来都很可信"诚实得多。
现成的"完整版编译器":AIHCC
上面是 176 行的教学版。把三条流水线做到生产级,是 AIHCC 的 /detect 正在做的事:提交一段文本,它逐句抽声明、分类——数字和日期走确定性复算,事实声明走搜索核验(多源取证,相当于一个真符号表),最后逐句标注并给出存疑句数(summary.red)。注册即得 API Key,免费 3 次/天,也有 MCP 可以直接挂进你的 Agent 工具链。
它的 Duet 则把这道"编译"放在了定时任务的出口:每天自动生成的晨报先过核验,有存疑句、或者没核验成的,一律转人工审批,你点头才标记可发布。说白了:AI 负责写,"编译器"负责拦,你负责点最后那个头。
写在最后
这次 OpenAI 的三项成果,我觉得最值得琢磨的不是"AI 又证出了什么",而是它默认了一条规矩:AI 给出的重大结论,必须附一份机器能校验的证明,不然不作数。 数学界有 Lean,AI 证伪 ETH 才有人敢听;自然语言没这东西,AI 写错你周报里的日期,你只能自己扛。
在"自然语言的编译器"普及开来之前,每次要把 AI 写的东西发出去时,都值得多问一句:
这段话,编译过吗?
代码说明:全文代码合并为
nlcompile.py,基于 Python 3.10+ 标准库(ast / re / dataclasses / datetime),无第三方依赖,demo 输出为本地实测结果(Python 3.12.2);demo 中的快讯文本为演示用合成内容(其中《柳叶刀》1823 年创刊属实,此处用于演示"语料查不到不放行")。ETH 证伪等三项成果为 OpenAI 宣称、Lean 证明已公开、尚待学界最终确认,且与 P vs NP 无关;报道细节以量子位原文及 OpenAI 官方公告为准。AIHCC/detect与 Duet 的功能以官方页面为准。
#OpenAI #大模型 #AI幻觉 #Python #Agent安全
本文提出将形式化验证思想引入AI文本生成流程,借鉴Lean证明系统的设计理念,构建面向自然语言的‘编译器’:通过解析、求值、链接三阶段流水线,对AI输出中的算术声明、事实声明和观点声明分别处理;支持安全AST求值、日期/数值复算、符号表驱动的事实核验,并明确标出不可判定内容。核心目标是用确定性机制抑制AI幻觉,提升生成内容可信度。
&spm=1001.2101.3001.5002&articleId=169367669&d=1&t=3&u=b07cc3d042fb448f99e3e52d5203baad)
429

被折叠的 条评论
为什么被折叠?



