本文由群友哈伯投稿,DualMono代发。
本文较长,阅读时间需要约1小时。
本文目录
当我们谈论自然语言编程时,我们在谈论什么?我们要如何定义“自然语言编程”?我们要如何实现“自然语言编程”?
阅读本文可能需要少量的编程基础,代码演示会以Python进行,但完全不懂代码其实也没关系。
本章本体不涉及任何有关AI/LLM/Agent相关技术的讨论,但文章内容本身可能会给你带来一些关于AI编程的新视角。
此外还期望读者对集合、形式证明、映射等概念还有最基本的模糊印象,同时还会涉及到少量语言学上的知识与概念。
本文会极力避免触及晦涩难懂的形式化术语以便扩大覆盖受众,代价是要多讲些啰嗦又不严谨的大白话,在此提前致歉。
缘溪行,忘路之远近
所有人在最初学习计算机时,应该都或多或少碰到过“自然语言编程”这个概念。它就像是一种遥不可及的“魔法”,向我们描述了一种简单又先进的可能性。比如,对着计算机说一句:“帮我做个记账软件”,电脑就会根据你的个人信息、生活近况、行为习惯等,做出一个独属于你的记账软件。而现在的大语言模型以及agent的概念,似乎正在逐渐让这种“许愿式自然语言编程”成为可能,也即是所谓"Vibe Coding",就像是只需要告诉ai你想要什么,接下来的一切都由ai帮你搞定。那我们似乎可以很高兴地宣布“自然语言编程的时代到来了!”然后结束所有讨论?当然不,否则我也就不会书写这篇文章了。
让我们回到最初,几乎是所有程序员学习编程的开始:Hello World.
设想我们面对一个最基础的任务:用一段自然语言描述向计算机发出指令。
输出 hello world
这句话看起来足够简单。任何程序员读到这句话都能理解它的含义:让电脑在屏幕上用任何方式显示这样一串字符,字符内容是"Hello World"。
就从这个最简单的要求开始。假设存在一个“真正的”自然语言编程系统,如果我们把这条指令,或者说把这条自然语言代码交给它,它应该做些什么?
答案似乎也很简单:系统首先需要理解这句话。它要识别出"输出"是一个动作,"hello world"是这个动作的目标。然后,系统需要用一个具体的方式来实现这个动作——在屏幕上显示出这几个字符。
看起来顺理成章。但当我们开始思考系统内部究竟发生了什么,令人背后恶寒的一大堆问题就会出现浮现出来。
忽逢桃花林
在"输出hello world"这个例子里,我们可以区分出三个不同层面的东西。
第一层是语句本身字面含义的分析与理解。它构成了一个完整通顺的句子,算是一个基本的祈使句。
第二层是语句所指的操作。现代计算机配备了屏幕、扬声器、打印机等各种输出设备,显示字符可以算是最基本的功能。
第三层是执行的结果。程序运行结束后的结果应符合用户的期待,比如要在屏幕上显示出一些图像,图像内容是hello world。
来一段最简单的真实代码:
print("hello world")
上面就是Python中最简单的hello world程序。“输出”这个单词被映射成了 print 这个函数。括号表示print需要一个输入,输入的是被引号包裹的,"hello world"这一个字符串。
基于此,我们开始提出问题。
-
问题1. 为什么选择Python?以本文写作的角度讲,我为了书写简单,也考虑到它的广泛使用,于是这门语言的选用是“我”作为作者的决策。但对于计算机,对于一个假想中的自然语言执行器来说,它应该做何选择?它应该直接把自然语言本身作为载体,还是应该先把自然语言转化成一段通常意义上的代码?如果是后者,它要选择一门什么样的语言,又为什么要这样选择?
-
问题2. 为什么要把“输出”二字映射为print?从人类视角,hello world是一个初学者必然接触的东西,我们几乎可以说所有人初学时一定都会写下一段大同小异的print("hello world"),然后以此为基础去学习更多概念,什么是函数,什么是变量,什么是字符串,什么是类型,等等。可对于一个自然语言执行器,我们凭什么断定“输出”要被映射为print?它并不是人类程序员,它可以用屏幕画画,可以调用扬声器用英语说出hello world,它甚至还可以用指示灯闪出一段摩尔斯电码报文,我们可以说,对于一段程序的运行过程,只要中间产生的某种信息可以被明确地解释为hello world,我们就不能说它没有成功输出hello world。
-
问题3. "输出hello world."的语言含义到底是什么?这不是一个废话疑问。"输出hello world",用户需要的可能是一个简单的print语句,但也可能是想初步测试系统有没有正常启动,能不能正常工作,软件硬件状态是否正常,那么在hello world之外到底要不要输出别的额外信息,要不要说你好?往更过分和无厘头的情况想象和考虑,hello world在别的某个世界上可能不是指一串字符串,而是某种极端复杂的概念的代称。举例来说,就像是当我们要求“输出黎曼猜想”的时候,假如这个自然语言执行器真的原封不动地把“黎曼猜想”这四个大字放到显示屏上,你一定会笑出声——但我们也确实不能说这个执行结果是错误的。那么,当“输出黎曼猜想”和“输出hello world”两句话放在一起的时候,为什么两个在语言结构、语法构成、表达形式等各方面因素都几乎完全一致的句子,在我们的想法和需求里却会指向多种不同的要求和可能的结果?
当然,当我们要讨论所谓的自然语言编程时,问题远不止这三个,我最终也做不到真正给出绝对且正确的答案,只能对上述问题进行逐步拆解和深入,以窥“自然语言编程”这一概念的冰山一角。
林尽水源,便得一山
我们可以先尝试利用直觉对前面的问题做出一些最粗浅简陋的拆解,找出一些问题背后的本质,并尝试初步回答。
问题1有两个部分,自然语言编程的载体应该是什么,直接执行还是要转换成别的代码? 第二个部分则是说,如果要转化成别的代码,应该怎么选择。我们现在先关注前者,也即载体问题。
从例子和假设起步。假如把本文作者关进小黑屋,在小黑屋外留下传话筒,再挂一个屏幕,小黑屋里也有屏幕和键盘,屏幕和外界联通且内容一致。作者作为具备一定的编程常识的人,被要求根据听到的内容在外界屏幕上打字。这时我在小黑屋里听到有人说“输出hello world”,于是我在键盘上敲下对应的字母。
这个时候,我们是否应该把这个小黑屋整体当作是一个“自然语言编程系统”来看待?总体来看答案偏向于否定。
首先作者的知识有限,也许可以打出hello world,但我绝对无力给出关于黎曼猜想的严谨的形式化表述,因此我面对“输出黎曼猜想”的这个要求时真的就只能输出黎曼猜想这四个大字。更关键的是,小黑屋里的作者会饿,会累,面对同一个问题可能会给出不同的回答甚至拒绝工作——你总不能拿鞭子抽我……不能吧?——作为一个编程系统来说没有任何稳定性与可信度可言,具体工作行为无法分析,无法控制,无法复现。所以,把这个小黑屋当作自然语言编程的根本载体并不完全合理。
这里可以给出一个我个人的简单的小结论:一个自然语言编程系统,如果希望具有稳定性、可复现、可实用,那就应该首先考虑把自然语言代码转化成一个确定性的目标编程语言代码,能看到过程,能操作,能分析,能改写。也即是,自然语言编程系统的载体不太应该是一个直接的执行器,自然语言编程系统更像是一种把自然语言要求转化为一段目标代码的转换器。
所以,先初步给出一个粗浅的定义以方便接下来的写作与讨论:
- 定义 – 自然语言编程:用具备模糊性的朴素自然语言进行程序功能描述,而后转化为一段确定性的、可运行的代码并运行而得到结果的过程。
从程序员的视角来看,这个时候再讨论如何实现自然语言编程的时候可能就开始有一点点眉目:如何实现一种从自然语言到目标编程语言的编译器?(我们之后会称为自然语言编译器)
至此,我们首先针对问题1的第一部分,也即自然语言编程的载体问题做出了初步的回答。关于问题1的第二个部分:如何选择目标编程语言。它在本文的重要性很低,且在后续还会出现,此处暂时按下不表。
作者附注:
读者可能会自然想到现在的各种agent体系,它们确实像是某种自然语言执行器。
姑且不论算力和成本问题,这种执行载体事实上就是刚才关于小黑屋的比喻,它并不是完全稳定且始终可复现的,其内部机理也更接近黑盒,训练只能对齐语言模型的行为趋势,却不能真正地直观控制行为。
且更重要的一点在于,即便是agent体系,也依赖于bash或者mcp等技术,来作为操作上的介质,而bash、mcp等依赖于模型进行结构化输出,且其功能仍然取决于计算机内已有的确定性代码,实质上仍然是在把自然语言转化为确定性的计算机操作。
山有小口,仿佛若有光
接下来来到问题2,同样的,先对问题的本质做一些拆解,做一些假设,举一些例子,再考虑回答。
由于我们已经对自然语言编程给出了一个粗浅的定义,于是问题2也可以换一个表述:
- 在一个自然语言编译器里,在分析句子的构成,把句子中的每一个元素映射并且转化为一段确定性的代码的过程中,如何面对自然语言用语到实际代码的映射的模糊性?
要回答它,可以首先考虑回到人们的日常交流,以及最初学习编程时的情景中去。
当一个老师在给学生上第一节编程课的时候,基本会直接默认这个输出就是对应了一个print,或者一个stdout,这一类的东西,且为了避免学生的理解困难,老师绝不会也不应该长篇大论地去讲解什么标准输出流之类的问题,也不会让一无所知的学生得知还有一些什么控制音频的api库之类的事情。学生只需要知道,print函数怎么用,用了之后会在屏幕上看到什么,别的东西一概屏蔽且不讨论,当作它们不存在,等待未来学习自然揭示。而"hello world"更像是一种惯例,只是从计算机的发展历史中偶然出现,并且流传至今的,并没有什么特殊含义的句子。
当教师讲授的不是一门编程语言入门课程,而是在讲解计算机组成原理之类的东西,"hello world"可能就会被展开为一些底层硬件的体系结构层面的内容,讲解cpu怎么执行相关指令,键盘、屏幕、主机之类的部件如何连接如何通信,等。此时hello world自身就不再成为重点,只是一个切入点,具体代码内容可能也无关紧要。
因此,日常的自然语言交流是有环境因素的。人与人交流时所处的场景,人们得到的过往教育,历史发展的偶然,等。语言本身很难直接独立自持存在,语言的使用是和外界环境息息相关的,一个句子的含义一定要在其特定所处的情景下才能够被解释出来,一句话的含义无法自己浮现。这是所谓的语义非自足性,以及语境依赖性。再直观一点说:语言需要依赖于一个外部的环境/模型存在,语句自身不能独立存在。
在这里,我想引入一个语义学里的概念:世界模型。
为了现阶段的文章流畅性,这里不深入语义学里关于世界模型的精确、形式化说明,所以要注意下面的内容是牺牲了大量的严谨性的。此外,文章里提到的“世界模型”和现在ai领域的“世界模型”也有很大区别,二者并不是同一个东西,切记。
现在想象存在一个世界,这个世界只有张三和李四这两个人,没有其它任何东西。
我们说张三和李四就是这个世界模型里的元素。
张三和李四各自具备一些属性:张三身高1.8m,李四身高1.6m,张三体重80kg,李四体重100kg
张三和李四之间存在一些相互关系:李四喜欢张三,张三认识李四。
这时候我们进行一些语言描述:
a. 张三的身高体重比例是 1.8m / 80kg
b. 李四比张三瘦
c. 张三喜欢李四
d. 张三认识李四
我们可以通过直觉对上述四个句子做出判定:a为真、b为假、c为假、d为假
这是在讲什么废话,是什么无聊的文字游戏吗?
实际上,此处想要表达的并不是某种逻辑推理,我们是在说明:一句话的含义判断,或者说一句话的“真”与“假”,在不同的世界模型内是会不同的。假如有另一个世界,李四比张三高,或者有诸如此类的不同,那么上面四句话的真假判断就会发生变化。甚至还有一种可能,另一个世界里不存在张三,那么这几个句子就会彻底失去其含义,连真假都无法辨别。
我们把话题扯回自然语言编程。
在引入了世界模型的模糊概念后,我们可以尝试来设计一个提供给自然语言编译器的不严谨的世界模型:
世界模型的元素:
朴素的日常用词用语
编程初学者第一堂课会接触到的Python语法规则与基本功能
比如说:
你好、输出、英语、中文、单词hello、单词world 等等
print语法、字符串语法、函数语法 等等
其中,有一些元素具备下面的属性/互相关系:
输出 在Python里 是 print函数
中文单词你好翻译为英文 是 hello
等等诸如此类。
接下来,我们在这一个世界模型的基础上,书写一段中文自然语言代码:
用英语输出:你好世界!
根据世界模型的定义内容,我们可以很自然地把上面这段自然语言转化为py代码。“用英语”表示我们应该从翻译关系中找到单词之间的对应关系,目标语言是Python,输出应该用print函数,最后明确得到:
print("hello world")
我们好像得到了一条完整的链条:因为自然语言要考虑外部环境,所以我们引入了“世界模型”的建模,随后,在世界模型里,我们需要定义一些元素,定义元素的属性以及元素之间的互相关系。
当上面这些内容定义完毕后,我们似乎就有了设计自然语言编译器的条件,这个假想中的自然语言编译器能够准确无误且无歧义地把“用英语输出:你好世界!”这句话编译为可以运行的py代码。
现在也许可以尝试初步回答问题2:为什么把“输出”映射为“print”?因为我们明确定义了一个把“输出”映射为“print”的世界模型,我们必须要首先有这样一个世界模型定义,才能够避免映射里的歧义。
好,我知道,把你的刀放下来,讲了这么一大堆弯弯绕绕乱七八糟的东西,我们居然得到了一个似乎是狗屎废话的答案:
- 自然语言编译器能够无歧义地工作是因为我们一开始定义的世界模型里就没有歧义。
我可以在这里剧透一下,这个作答本身是错误的,但这个回答也算是一个非常重要的阶段性答案。
首先,它是帮助我们打破“许愿式自然语言编程”这一幻想的关键所在:
一个只存在于想象中的自然语言编程系统,它可以在没有任何前提条件、没有任何假设、没有任何预先知识储备的情况下,仅通过几句最朴素的语言描述,就真正做出完备、合理、可用、符合用户需求且没有任何歧义的程序。这样的自然语言编程系统,极大概率是不会存在的。自然语言编程始终有一个预设,有一个外部的世界模型,如果世界模型都不存在,那么自然语言编程也就是无稽之谈——没有世界模型,我们甚至无法定义什么是代码,什么是编程。
其次,这个粗糙的回答里藏着一个重要的信息:自然语言代码的歧义消解,或者说自然语言表述中的模糊性的消解,大概率与世界模型有高度相关性。朴素自然语言的模糊性很难自消解,而我们现在至少找到了一个可能的线索,这个线索隐约指向世界模型。
于是为了进一步挖掘世界模型会给我们带来什么,我们要开始深入世界模型这个想法本身,对当前模糊的定义做出纠偏,给出稍微更准确一点的合理的形式化定义,随后我们便会看到这个简陋模型自身的局限性,也能够知道这个简陋作答的错误出现在什么地方。
作者附注:
接下来要涉及代码和一些符号表示,看不懂不要紧,大概浏览然后略过即可。再之后主要还是以直观朴素的自然表达进行书写。
便舍船,从口入
就像之前提到的,从形式语义学的角度来看,最初最简单的世界模型可以认为包含着两个主要部分:世界模型中所包含的所有个体、每一个个体各自的属性和它们之间的关系。
个体可以是“任何东西”,人物、物件、概念、专有名词,等等。
我们会把所有个体的集合称作:论域。也就是说,论域是一个世界模型中所有存在的个体的集合,我们用大写字母 D 表示。
而个体的属性,以及个体之间的关系则属于另一个集合,我们把所有的这些属性、关系称作谓词。个体的属性只和其自身有关系,所以称为一元谓词,而涉及两个及以上n个个体之间关系的谓词则被称为n元谓词。比如“高的”“100kg”“留长发”之类的属性是一元谓词,而“喜欢”“认识”之类的关系则是二元谓词。我们用大写字母 F 代表所有谓词的集合。
同时为了方便代码上的处理,我们还需要引入一个名为常量的概念,用以方便在代码书写时进行查表操作。所有内容如下所示:
# M 由 D、F构成,M暂不直接在代码中起作用。我们用 M = 表示这个世界模型
D = {"张三", "李四", "王五", "小兵", "大力", "陈规"} # 论域:世界模型里的个体
F = {
# ── 常量到个体的映射 ──
"a": "张三",
"b": "李四",
"c": "王五",
"d": "小兵",
"e": "大力",
"h": "陈规",
# ── 一元谓词 ──
"HAPPY": {"张三", "小兵", "大力"}, # "高兴者"的集合
"TALL": {"张三", "王五"}, # "高个子"的集合
"FAT": {"李四", "小兵"}, # "胖子"的集合
# ── 二元谓词 ──
"LOVE": {("张三","李四"), ("王五","张三"), ("大力","陈规"),
("张三","王五"), ("小兵","大力")} # "喜欢"关系的集合
}
# 现在,我们可以通过查表直接来书写表达式
print(F["a"]) # 输出个体张三,张三这个个体存在于这个世界中
print(F["c"] in F["HAPPY"]) # 输出: False,王五不是快乐的
print(F["b"] in F["TALL"]) # 输出: False,李四不是高个子的
print((F["a"], F["b"]) in F["LOVE"]) # 输出: True,张三喜欢李四
在上面的叙述中,我们需要注意几件事:
首先,我们通过 M = <D, F> 这个式子来描述这个世界模型的构成。M,也即代表世界模型的字母,当前充当一个背景板,只在注释里出现。同时,我们虽然定义了集合 D,但为了表达形式的统一性,所有查表操作都通过 F 进行。
随后,对于这个世界M,没有“伤心”“矮个子”“瘦子”的概念,在这个世界里我们甚至不能说“张三是瘦子”这句话,我们只能说“张三不胖”。因为这个世界只有“胖”而没有“瘦”的概念。更进一步讲,所谓的“开心”“高”“胖”在M中只是一个“符号”,它们事实上是没有含义的,只是一个纯粹的判断,且目前判断的结果只能是“真”或“假”,“王五不是快乐的”这句话的含义只是由读者的世界观出发,然后自然地附加在了句子上,“王五不是快乐的”和“王五是伤心的”这两句话在当前世界模型中没有任何关系,我们并没有定义“伤心”这个谓词符号。
比如我定义一个名为"ABCDEFG"的一元谓词,里面有一些个体或者没有个体,我们也就可以在这个世界模型下表达 “某人在/不在ABCDEFG这个谓词对应的集合里”,但"ABCDEFG"到底是什么,其实是完全无所谓的。这也就回答了另一个可能存在的问题:“为什么谓词里的这些概念不在D中?”因为这些概念只是一个谓词,只是一个符号,在推导过程中不具备任何实际意义,世界模型不关心这些谓词被我们称作什么,我们只关心查表结果的真与假。
再之后,多元谓词输入是有顺序的,张三喜欢李四,李四喜欢张三,但是小兵喜欢大力,不意味着大力喜欢小兵。“张三李四互相喜欢”完全等价于“张三喜欢李四 且 李四喜欢张三”
最后,上面所有这些表示,这些在谓词对应的集合中进行查表的操作,这些句子可以被称为“谓词表达式”。
这些谓词表达式,其实还可以换一种写法:
F( HAPPY ) ( F(c) ) -> False
F( LOVE ) ( (F(a), F(b)) ) -> True
它们很微妙地隐含着“柯里化”或者“函数式编程”的想法。当然,具体表达取决于读者的个人喜好或着对于 F 的理解方式。此处不做重点讨论。
当我们认识到了一个相对来讲更严谨一点的世界模型后,我们需要回头注意此前出现过的一些内容:
现在想象存在一个世界,这个世界只有张三和李四这两个人,没有其它任何东西。
我们说张三和李四就是这个世界模型里的元素。
张三和李四各自具备一些属性:张三身高1.8m,李四身高1.6m,张三体重80kg,李四体重100kg
张三和李四之间存在一些相互关系:李四喜欢张三,张三认识李四。
a. 张三的身高体重比例是 1.8m / 80kg
b. 李四比张三瘦
c. 张三喜欢李四
d. 张三认识李四
上面的这个关于世界模型的初步讨论里其实存在一些小问题。首先,准确来讲我们不可以描述“李四比张三瘦”,这句话的真假无法被判定,因为张三和李四的身高、体重数据只是一个“一元谓词”,在这个模型里我们根本没有定义什么是胖什么是瘦。其次,要注意谓词无含义,“李四喜欢张三”并不代表“李四认识张三”。
用代码的话来讲,我们没有定义比较高矮胖瘦的函数,就无法进行相关计算。我们没有定义谓词之间的从属关系,就无法直接对谓词本身进行计算与判断。
当我们基本厘清了一个简单的世界模型应该怎么定义,又要注意一些什么,我们就可以回过头来继续考虑我们对于问题2的最初作答了。
为什么把“输出”映射为“print”?因为我们明确定义了一个把“输出”映射为“print”的世界模型,我们必须要首先有这样一个世界模型定义,才能够避免映射里的歧义。
此时可以肯定地说,这个作答是错误的。考虑下面的谓词定义以及其谓词表达式:
输出设备 = {显示器,音响}
显示器是输出设备 -> 真
音响是输出设备-> 真
自然语言编译器遇到“输出hello world”,在世界模型里查表“输出设备”这个谓词对应的集合,也即是在询问:“x是输出设备”这个表达式的真值结果,把世界模型里的所有元素都带入x进行谓词表达式的计算,最后会得到两条结果为真的路径。
换句话来说,“输出hello world”这句话的映射仍然有歧义,可以通过显示器输出,也可以通过音响输出,且这两种操作都是正确的!当我们说“输出hello world”的时候,从世界模型中能找出多条不一样的实现路径,且这些实现路径都没有问题,都可以被认为是合理的路径。
我们当然可以考虑提出最直接粗暴的解决方案:根据当前需求情况,直接修改世界模型的定义。
世界A,输出设备 = {显示器}
世界B,输出设备 = {音响}
定义多个世界,在不同的世界模型下可以把输出二字对某一条代码进行精确映射,但这只意味着问题会从“到底如何处理文本到世界模型多个目标个体的映射”转化成“到底如何判断怎么修改世界模型”
这种转化不能带来任何收益,问题会在递归中回到原点,假如我们要判断什么时候用世界模型A还是B,实质上是隐性地暗示了一个事实:我们需要世界模型之外的世界模型,这个外层的世界模型包含了所有的子世界模型。这种递归会在推导和计算操作中带来灾难性后果。
此外,随意修改并同时保存多个世界模型会导致一些相悖的事实表达——“张三是胖的”和“张三不是胖的”——同时存在。更多问题在这里不做展开,只需要知道我们不把直接修改世界模型当作一个合理选项。
问题到这里貌似回到了原点:世界模型不能消解歧义,语言自身似乎也无法消解自己的歧义。
但我们经过上面的论述,得到了一个很自然且符合直觉的新信息:世界模型通常不应该发生巨大变动,世界模型应当充当一个相对稳定的背景板,一个完整的世界模型里会包含我们所需要的所有个体元素以及谓词数据,而我们的自然语言编译器要做的事情大概率是要在世界模型里进行查表判断。
更重要的是,只包含了个体与谓词符号的世界模型也并不能满足我们的需求,这个世界模型不能在没有定义高矮胖瘦等谓词的情况下判断个体之间的相对关系,张三如果喜欢李四则张三也应当认识李四,前面的纯粹符号化描述根本不符合我们的日常生活用语经验,我们的语言不是符号,我们的语言是有明确的环境以及上下文含义的,谓词之间也是有互相联系的。
所以我们的探索到目前远没有结束,我们目前也无法对问题2做出进一步的新回答,那不妨再往前走几步。
初极狭,才通人
总结一下我们截至目前得到的关键拼图线索:
- 引入世界模型后,我们能够通过谓词对个体的属性或者相互关系进行一些真假判断,世界模型本身不应该被随意修改。
- 自然语言编译器要在世界模型的谓词集合中查表,直接查表的结果很可能是无穷多的,且每一个查表结果最终都会是合理的,我们需要一个查表规则来进行筛选。
- 世界模型中的谓词不应该局限于只作为纯形式化符号存在,谓词表达个体的属性、个体之间的关系,我们还需要一种工具来表达谓词之间的关系。
根据线索1和2,我们得到推测:世界模型和自然语言表达之间也许还存在一个什么东西:定义了查表规则,可实时修改,会使查表结果收敛为唯一路径。
根据此推测和线索3,我们得到新推测:这个东西大概率会触及谓词之间的关系,毕竟在世界模型里,某一个具备特定含义的事实不应该和另一个具备与之相悖的含义的事实同时存在
毫无头绪?其实还有一个线索已经在此前的一个例子里被埋下,在刚才详细讨论世界模型的时候我们刻意忽略了它:
世界模型的元素:
朴素的日常用词用语
编程初学者第一堂课会接触到的Python语法规则与基本功能
比如说:
你好、输出、英语、中文、单词hello、单词world 等等
print语法、字符串语法、函数语法 等等
其中,有一些元素具备下面的属性/互相关系:
输出 在Python里 是 print函数
中文单词你好翻译为英文 是 hello
等等诸如此类。
用英语输出:你好世界!
我们重点注意下面三段文本:
朴素的日常用词用语
编程初学者第一堂课会接触到的Python语法规则与基本功能
输出 在Python里 是 print函数
有一个很奇妙的事情,我为了“书写方便”,没有把世界模型的元素真正完整展开,只举了几个典型例子“你好、输出、英语…”后一笔带过。
而“输出 在Python里 是 print函数”这句话表达了一个嵌套的结构:查表所有编程语言里可以作为“输出”的函数,查表目标编程语言。
从更严谨的角度来看,“朴素的日常用词用语”这句话不应该被拿来描述一个世界模型里的元素,它应该是一个谓词,这个谓词对应的集合内容是所有朴素的日常用词用语。而世界模型的元素集合应该是一个“全集”,包含了所有用词用语,所有朴素和不朴素的用语,所有基础和不基础的Python语法规则和功能。同时,Python这门编程语言自身也是世界模型中的一个元素,世界模型里不应该只有Python,还应该有汇编,有C/C++,有RUST,有JS,有PHP,等等等等。
所以,我们在书写这个例子里的中文自然语言代码时,实际上并不是只写了“用英语输出:你好世界!”这么一句话,我们的“朴素用语”“编程初学者的第一堂课”“Python课堂”这三个条件实际上应该出现在世界模型和自然语言代码之间,这会让我们的完整链条变成下面的情况:
世界模型 -> “朴素用语” “编程初学者的第一堂课” “Python课堂” -> 中文自然语言代码:"用英语输出:你好世界!" -> 自然语言编译器: print("hello world")
也就是说,在自然语言代码与世界模型之间,出现了一些“前提条件”。这些前提条件会使自然语言代码在世界模型里的查表结果收敛到唯一一条路径。更有趣的是,这些前提条件自身并没有脱离世界模型:
-
“朴素日常用语”,相当于在世界模型的所有用词用语中进行查表,然后收敛到“朴素日常用语”这个谓词对应的集合中的用词用语,所有日常朴素的表达都在这个谓词对应的集合中。
-
“Python课堂”相当于在世界模型的所有编程语言中查找可能会使用的目标语言,那结果自然是Python。
-
“编程初学者的第一堂课”意味着我们在对“输出”这个谓词进行查表的时候,不会去考虑什么日志、音频设备调用之类的东西,这里的输出会被直接限定到唯一一个目标函数:print。
是的,这三个前提条件自身也是世界模型里的谓词/谓词表达式!
而且,这些前提条件真的满足了我们前面提到的两个推测:前提条件存在于世界模型和自然语言表达之间,前提条件会使查表结果收敛为唯一路径,前提条件会涉及到一些谓词之间的关系,它能够对自然语言代码里涉及到的谓词做出新的限定。我们可以说,“前提条件”几乎就是我们要找的那个关键且重要的中间层,而前提条件的构成中,谓词表达式占据一个非常重要的地位。
既然我们找到了前提条件这个中间层,就不妨来再对其存在和特性做一点点分析。
最直观的一点,中间层自身既然涉及到谓词表达式,那就能够进行谓词表达式的真值推导,一个最简单的例子:
编程语言 = {Python, C/C++, RUST, PHP, ......}
这时候我们在前提条件里说:
“编程初学者第一节课”
“这是一个Python课堂”
“教师使用PHP授课”
很显然,此时连前提条件自身都不能合理成立。就更别谈论在此前提条件下进行自然语言编译了。
此外,前提条件需要随时变化:
“编程初学者第一节课” 换成 “公司内代码,公司要求使用LOG进行输出记录”
我们可以通过随时修改前提条件,在不修改自然语言代码本体的情况下,修改最终自然语言编译器的输出。也就是说,比起直接书写“通过LOG进行文本记录”这种自然语言代码,不如把这种限制书写在前提条件这个中间层里。自然语言代码里就始终只需要一句“输出/记录xxx内容”就行了
最后,前提条件需要有人参与交互:
在书写特定代码时,或者说实现特定需求时,程序员自己往往有多种不同的工具、多种不同的可行路径去解决同一个问题。有些时候,我们在真正书写出最后能够运行的代码之前,其实并不知道哪一条路径更合适。或者我们以数学猜想的证明为例:同一个数学猜想可能会有完全不同的证明路径,研究者不可能在这些路径出现之前就确定它们各自需要些什么前提,需要一些什么样的数学工具。
因此,“前提条件”不是一个在自然语言编程初期就被彻底固定的东西,很可能仍然需要编程人员在自然语言代码书写时,甚至是在自然语言编译器在开始工作后,才能意识到当前“前提条件”的限定仍然不充足,结果仍然无法被收敛到唯一路径,此时就需要自然语言编译器提出疑问,再由编程人员进行决策。
这个中间层可能存在的别的一些特征或者规律,在此就不继续深入了,有关于这个中间层的探索至此其实已经基本足够。
现在,我们可以重新考虑问题2:
- 为什么把“输出”映射为“print”?因为我们给自然语言编译器在一个已知的世界模型中的查表行为限定了一些前提条件,这些前提条件会使自然语言编译器的查表结果收敛到唯一可行路径。我们无法消除自然语言在世界模型中的歧义,我们只能通过书写限定条件来消解这些歧义。
同时,问题1的第二部分也顺带着一起解决了:
- 目标编程语言的选择是前提条件中的一个决策。
毫不意外,这其实是一个比较令人沮丧的回答,我们最终没能逃出这个根本的限制条件:即便是假想中的自然语言编程,即便是假想中的自然语言编译器,我们也无法在目标不明确,前提条件不明确的情况下,书写“魔法般的自然语言代码”。以截至目前的讨论来看,我们不得不再次给“许愿式自然语言编程”的棺材打上一根更长、更坚固的钉子。
不过,没有时间为“许愿式自然语言编程”哀悼,因为接下来赶赴战场的将是同样艰难,甚至有过之而无不及的问题3:
- "输出hello world"的语言含义到底是什么?
作者附注:
再强调一遍,我们截至目前没有对LLM和Agent等人工智能相关技术做任何讨论。因为这篇文章的主线内容是“自然语言编程”而非“Agent编程”更不是“Vibe Coding”,更进一步讲,本文关心“自然语言编程”这个概念的合理定义甚至远超过关心某种具体的技术实现。
就像最开始关于小黑屋的比喻,假如把那个小黑屋当作是一个合理的自然语言编程系统,那几乎是在说世界上的每一个人都可以被“自然语言编程”。同时,假如因认为现在的LLM、Agent已经,或即将要解决世界上的一切问题而拒绝去认真看待事物自身的内在逻辑,那么本文截至目前的1万多字也就都没有任何意义了。对于怀揣着此类想法的读者朋友,在这里作者也不建议你再耐着性子看下去,我确实无法带给你任何有价值的东西。
复行数十步
回到我们最初提出的疑问:
"输出hello world."的语言含义到底是什么?这不是一个废话疑问。"输出hello world",用户需要的可能是一个简单的print语句,但也可能是想初步测试系统有没有正常启动,能不能正常工作,软件硬件状态是否正常,那么在hello world之外到底要不要输出别的额外信息,要不要说你好?往更过分和无厘头的情况想象和考虑,hello world在别的某个世界上可能不是指一串字符串,而是某种极端复杂的概念的代称。举例来说,就像是当我们要求“输出黎曼猜想”的时候,假如这个自然语言执行器真的原封不动地把“黎曼猜想”这四个大字放到显示屏上,你一定会笑出声——但我们也确实不能说这个执行结果是错误的。那么,当“输出黎曼猜想”和“输出hello world”两句话放在一起的时候,为什么两个在语言结构、语法构成、表达形式等各方面因素都几乎完全一致的句子,在我们的想法和需求里却会指向多种不同的要求和可能的结果?
整理一下,问题3的具体核心是最后一段:
- 为什么两个在语言结构、语法构成、表达形式等各方面因素都几乎完全一致的句子,在我们的想法和需求里却会指向多种不同的要求和可能的结果?
它在询问:如何分辨“使用”与“提及”?
“使用”指的是,我们要把一个词拆开,要进一步挖掘它指向的“内部含义”,要做语言和分析理解上的展开,然后去使用那些展开后的东西。
“提及”指的是,我们要把一个词当作是词汇本身使用,我们提到它也就是提到它的表面字符本身,不将其展开。
我们也许可以用前提条件的想法来对这个问题先做出一些简单尝试:
前提条件
黎曼猜想是一个具备特殊指向性的专有名词。
在涉及到“黎曼猜想”时应该将其展开,提供黎曼猜想的形式化语言版本。
这种做法能稍微解决一些小问题,比如,当“输出黎曼猜想”和“输出hello world”两句话放在一起的时候,为什么前者的输出内容可以不一样,为什么自然语言编译器可以处理这种差异,世界模型里有一些元素、一些谓词,或者一些别的什么东西,能够组成“黎曼猜想”的完整语言表达,因此自然语言编译器就可以从世界模型里把这个完整表达通过某条唯一路径提取出来,最终呈现给用户。
然而它会带来一些显而易见的糟糕后果:
- 自然语言编译器接下来要怎么区分:输出黎曼猜想;输出“黎曼猜想”。这两句话?
- 黎曼猜想的表达有多个不同的版本,涉及到不同的数学工具,甚至涉及到不同的历史时代,我们在书写前提条件时难道真的要把这些东西全都加上?它们真的都应该出现在前提条件表达式中吗?
- 难道我们要给世界上的每一个专有名词都写一个前提条件,告诉编译器,面对专有名词时要展开?那我们到底是在写自然语言代码,还是在写百科全书?
- 假如我们在前提条件中给"hello world"也赋予一个新的指代和引申含义,那么未来是不是所有遇到hello world的地方又反过来要考虑新的问题:这时候的hello world到底应该用其原始文本,还是前提条件里定义的引申意?我们是不是要书写前提条件的前提条件?
所以,前提条件不是万能的,理想情况下,前提条件自身应该精简易懂,在整个自然语言编译器进行工作时可以进行实时附加,试图把所有自然语言编程中遇到的问题都放到前提条件这个中间层去解决是不可行的。
那么,从自然语言中来,也要到自然语言中去,从世界模型中来,也要回到世界模型中去。我们再一次将目光投向我们生活中的世界。
例1:书店对话
我走进一家书店,对店员说:“请问有《活着》吗?”
店员会走向文学区,找余华的那本小说递给我。我不会希望店员理解成“这里有活人吗?”这里不是瓜摊,我也不是来找茬的,虽然在这种情况下更像是店员在找我的茬;我也不会指望店员从柜台底下掏出一本封面上印着“活着”两个大字的空白笔记本——虽然从最极端的字面意义上讲,“有《活着》吗”这句话确实可以被解释为“是否存在一个物体,其表面刻有‘活着’这两个字符”。
现在,如果我说的是:“请问有’活着’这两个字的书法字帖吗?”店员会走向书法区。
同样的两个字符"活着",在第一句话里是一个作品的名称,是一个专有名词,指向余华的小说;在第二句话里是被提及的文本本身,指向两个汉字的字形。在日常聊天语境里,我说的话里没有标点符号,但店员能自然地在脑海里给这两句不同的要求打上合适的符号。
例2:课堂点名提问
老师问:“谁知道‘三角形’的定义?”
我举手回答:“三条线段首尾相连构成的封闭图形。”
老师又问:“谁能把‘三角形’三个字写在黑板上?”
我走上前,用粉笔写下“三角形”三个汉字。
老师说:“请画一个‘三角形’。”
我此时在黑板上画出一个三角形图案。
第一句话里,“三角形”是一个数学概念,我需要展开它的内涵。第二句话里,“三角形”是一个字符串,我只需要复现它的字面形态。第三句话里,“三角形”指的是一类图像,任何从视觉上看属于三角形的图像在这里都是可行的。哪怕我画了一个两边边长极长且横向占满了整个黑板,而第三边远小于另外两个边的,视觉上看简直是一根针的三角形,教师也不能说这是错误的——当然,这种行为也是在找茬,接下来我可能会被教师叫出全名并且被“礼貌地”请回座位上去。
第二个例子比起第一个例子可能会更棘手一些,因为即便把教师说的三句话都写下来,“三角形”这个词汇旁边的标点符号也没有任何区别,我们无法从句子的标点中直接看出来三个“三角形”到底有什么区别。
一个截至目前显而易见的特点是,当名词指代其自身文本时,相关语句都存在某种加强描述特别强调了名词自身,“活着这两个字”,以及“三角形这三个汉字”,这些句子里的强调内容非常像是我们的“前提条件”中间层,只是它们直接出现在句子内部,和句子融合在一起。
做一些猜测:“前提条件中间层”可能并不真的独立存在,前提条件和自然语言代码描述内容自身本来就是一体的,说不定只是作者为了多在文章里水几千字而硬造出来的概念?
有这种想法是正常的,但这只是文章书写过程中故意埋下的小陷阱,让我们把三角形这个例子里的句子改写:
“请写下三角形。”
“请画出三角形。”
这两句话里没有出现自我强调,唯一的区别是句子里的动词:写下、画出。不同的动词让同一个“三角形”的释义取得不同的结果,句型结构本身都是一样的祈使句,连标点符号都没有区别。
那么问题到底是出在我们对句子的理解本身,还是出现在我们的世界模型建模里,亦或者二者都有?不妨再认真核查一些细节,从已有信息挖掘些新线索。让我们把下面四句话放在一起审视:
“请写下三角形”
“请画出三角形”
“请输出hello world”
“请输出黎曼猜想”
前二者的动词不一致,目标词语一致,后二者动词一致,目标词语不一致。同时,这四句话的结构基本上一样,祈使句,一个动词,一个名词。没有中间层,没有前提条件,它们依赖的也完全是对应于同一个现实世界的同一个世界模型。它们共同发出了一个警告:我们现在构建的世界模型还是太简陋了。
回顾已有建模:
D = {"张三", "李四", "王五", "小兵", "大力", "陈规"} # 论域:世界模型里的个体
F = {
# ── 常量到个体的映射 ──
"a": "张三",
"b": "李四",
"c": "王五",
"d": "小兵",
"e": "大力",
"h": "陈规",
# ── 一元谓词 ──
"HAPPY": {"张三", "小兵", "大力"}, # "高兴者"的集合
"TALL": {"张三", "王五"}, # "高个子"的集合
"FAT": {"李四", "小兵"}, # "胖子"的集合
# ── 二元谓词 ──
"LOVE": {("张三","李四"), ("王五","张三"), ("大力","陈规"),
("张三","王五"), ("小兵","大力")} # "喜欢"关系的集合
}
记得我们在之前的文章内容里不断强调的问题:这个世界里,张三、李四、HAPPY、TALL,都只是符号,不管把其中哪一个直接换成“ABCDEFG”,世界的本质都不会出现变化。“前提条件中间层”做出的操作并不是改写这些符号,中间层只是在谓词对应的集合里面做取舍,取交集、并集、补集等,试图让真值解析收敛到唯一路径。
所以,我们现在需要考虑给这些纯粹的符号添加“语言含义”了,当符号具备内在含义,符号所处的场景才能开始出现区别。还是那个例子,当符号不具备内在含义,我们就只能说“张三是/不是高兴的”而不能说“张三是/不是悲伤的”;只有“高兴”一词意味着心情状态,我们才能说:张三的情绪好/张三的情绪坏,我们也才能说“张三是高兴的,所以张三不是悲伤的”。
于是,为了给词汇增加“含义”,我们需要暂时抛弃此前的 M = <D, F> 形式的建模,转向一种大多数程序员都相对更熟悉,日常用得也更多的一种建模手段,从而方便后续书写与讨论(不意味着我们要永远抛弃lambda化的表达)。非程序员读者不用着急,不需要完全看懂。后文会有朴素化的说明。
class Item:
"""用类来描述所有名词词汇,每一个名词都是一个类实例,每个实例涉及一些自身自带的属性以及相关的方法"""
def __init__(self, text: str):
self.text = text
self._roles = {} # 存储该词在不同语境下的语义展开
def define_role(self, context: str, meaning):
"""定义该词在特定语境下的含义内容。"""
self._roles[context] = meaning
def resolve(self, context: str):
"""根据当前语境解析出实际语义。"""
if context not in self._roles:
raise ValueError(f"词 '{self.text}' 在语境 '{context}' 下未定义语义。")
return self._roles[context]
# ── 构建词汇表 ──
triangle = Item("三角形")
triangle.define_role("mention", "三角形")
triangle.define_role("concept", "三条线段首尾相连构成的封闭图形")
triangle.define_role("visual", "任意三角形图案")
hello = Item("hello world")
hello.define_role("mention", "hello world")
riemann = Item("黎曼猜想")
riemann.define_role("mention", "黎曼猜想")
riemann.define_role("formal", "ζ(s) = Σ(1/n^s), Re(s)>0 的解析延拓...")
这里结合前文再从头开始大概解释一下。
这之前的世界模型看待句子是一种查表模式,判断一句话真假,主要手段是分析这句话对应的谓词表达式,分析张三是否高兴,张三的高矮胖瘦,主要是分析张三这个元素到底在不在“HAPPY、TALL、FAT”的这些词汇对应的表里,说“张三是高兴的”,完全等价于“张三在‘高兴’这张表中所列出的所有元素中”,等价于“张三属于高兴者所对应的集合”。
class Item形式的描述,是在说:每个单词对应的某个物体某个元素,在世界模型中自己存储自己对应的所有信息,而非依赖于另一张表,又或者说,世界上的东西首先是一个类,任何东西首先要知道自己是什么。如果想知道张三的情绪,张三的高矮胖瘦,应该问张三自己。存储了“张三”这个个体的所有信息的东西,我们称作类实例,类实例会回应你的一系列询问:我是高兴的,我是高的,我不胖,这些东西是类实例自己的属性,是类实例自己的内部数据。张三、李四,又或者黎曼猜想或者hello world,每一个东西都是一个类实例,世界模型由无数个类实例组合构成,世界模型具有所有的类实例。在这个世界模型里,说“张三是高兴的”,完全等价于“张三这个类实例的情绪属性是高兴”。
也就是说,现在我们的词汇和句子在class Item形式的描述下,略微脱离了“形式化的无意义符号”状态,转而开始具备一定的内在含义了。
我们把世界模型里的元素创建成Item实例,三角形就是 Item("三角形")。我们给三角形书写它在不同情况下的内部含义,在"concept"的情况下,也即是用到它的概念含义时,其内容是“三条线段首尾相连构成的封闭图形”,相对应的,mention的情况下,用到的就是“三角形”这三个字本身。相应的,我们对于hello world就只赋予唯一一个mention属性,内容就是"hello world"字符串自身。
在这样一个建模里,一个词的使用与提及其实没有太大本质区别,仅仅只是在不同场景下根据需求访问一个类实例的不同内部属性参数,也即是视情况访问resolve函数。在提及场景下,就获取其mention的内容,在使用场景下,还可以根据使用场景的不同,获取不同使用场景对应的内容。
于是,既然Item中已经自己存储了自己在不同场景下的含义,既然世界模型里的名词是class Item,那么我们也可以有动词的class Verb。
注意!!这里必须强调,class Verb并不对应 M = <D, F> 中的谓词表达式,M = <D, F> 这个世界模型中的谓词信息几乎可以认为已经被完整地存储在了class Item中,因为class Item会完整存储每一个Item自己内在的高矮胖瘦喜怒哀乐等属性(只不过目前的代码省略了没有直接写出来),可以认为 M = <D, F> 模型下,谓词表达式所查找的就是这些属性。而下面动词的 class Verb 在此前的 M = <D, F> 模型中暂时不具备任何对应关系,这会是一个展开来讲甚至又可以再写出去两三万字的内容。
class Verb:
"""动词:通过内置的优先级列表,决定如何从 Item 中提取含义"""
def __init__(self, name: str, priority: list):
self.name = name
self.priority = priority # 优先级列表,例如 ['concept', 'visual', 'mention']
def resolve_scene(self, item: Item):
"""
场景解析:按照优先级,挨个向 Item 索要含义。
命中即返回,全都没命中则报错。
"""
for role in self.priority:
if role in item._roles:
return {
"predicate": self.name,
"target": item.text,
"resolved_meaning": item._roles[role],
"matched_role": role
}
# 如果某个 Item 不具备 Predicate 下可能对应的任何场景,说明类型不匹配,直接报错
raise TypeError(f"错误:'{item.text}' 无法在 '{self.name}' 场景中运行。")
# ── 1. 定义谓词及其优先级 ──
# “输出”很宽容,优先要概念,其次要视觉,最后才要字面文本
output = Predicate("输出", priority=["concept", "visual", "mention"])
# “画出”和“写下”很挑剔,只要视觉或字面文本
draw = Predicate("画出", priority=["visual"])
write = Predicate("写下", priority=["mention"])
让我们再来解释一下上面的这部分代码。class Verb 内部存储了关于“场景”的信息,且“场景”之间具备优先级关系。
比如"输出"这个词,它具备的priority有三个:["concept", "visual", "mention"],draw和write也即是画出和写下这两个词,只具备visual或者mention的场景。
看一下实际的使用:
- “输出黎曼猜想”:黎曼猜想这个Item具备concept和mention,而在“输出”这个动词中,concept的优先级高于mention,因此碰到黎曼猜想四个字时会优先输出其概念或者其形式化表达。
- “输出hello world”:hello world 只有一个指向自身的mention属性,所以这个指令只会让我们得到hello world这几个字符。
- “输出三角形”:三角形具备"concept", "visual", "mention",而在“输出”中,concept同样高于visual,所以我们不会得到三角形的图像。
- “画出三角形”:“画出”只要求visual一种场景,因此“画出三角形”的指令是在要求直接给出三角形的图像。
- “画出hello world”:系统无法工作。因为hello world不具备visual场景属性。
- “画出黎曼猜想”:同上,我们的建模体系的数据内容里目前没定义黎曼猜想的图像化表达形式,所以系统无法工作。
好像很不错!总结一下我们截至目前到底都做了什么:
我们提到,M = <D, F> 体系下,世界的元素成员以及世界的谓词表达式只有符号,没有含义,而没有含义的符号就没有情景上的区别。同时,我们明确说明了“前提条件中间层”在问题3里的不可用性,解释了直接用中间层去处理词汇使用义与提及义时会遇到的问题。因此,为了能够直接通过句子的字面成分来提取出一个句子的情景信息,我们开始尝试为无意义的符号体系赋予含义,为此,我们暂时抛开了 M = <D, F> 体系,转而使用class Item、class Verb体系,便于为词汇赋予其内在含义,随后我们成功地利用这个体系构建了一个简单的优先级与情景匹配系统。
诶呀,有没有发现一件事,“前提条件中间层”这个概念,在现在的class Item、class Verb体系下好像没有出现?没关系,其实很简单。
举个例子,注意到我们现在定义的这个class Verb中,不可能出现“同级别”的情景,"concept" > "visual" > "mention",这个排序永远成立。可假如我们要求一个新的class Verb2,这个class Verb2中可以允许同级别情景的存在,比如“输出”一词的"concept", "visual"是同级别的,这个时候,同级别情景带来的歧义就要用前提条件层来解决了。我们就会得到下面的链条:
前提条件:
“输出”一词的"visual"优先级高于"concept"
“输出黎曼猜想”:黎曼猜想没有visual,仍然输出其形式化定义
“输出三角形”:三角形有visual,我们会得到三角形图案
而且,非常有趣的一点是:前提条件中间层在这里解决的问题,和在 M = <D, F> 体系下解决的问题几乎是同一个!
- 当同一级别的合理解释路径有多条的时候,对合理的路径进行有条件的收敛,使之只剩下唯一一条合理的正确路径
它们的性质也很像:前提条件表达式也是自然语言代码;可以高效、简洁地影响多条自然语言代码;完全可以随时更改,对于同一段自然语言代码,可以用前提条件表达来快速且明确地更改其行为。
这为我们提供了一个非常非常重要的暗示: M = <D, F> 体系,class Item、class Verb体系,两者可能具备某种深层次的一致性。它们在表达习惯上可能有区别,但其逻辑内核的本质很大概率是相似、甚至可能是相同的。(本章节无法论述和证明,这是个过分复杂的话题)
好了,到这里,我们几乎可以很高兴地宣布,问题3也解决了!在我们构想中的自然语言编译器中,“输出黎曼猜想”和“输出hello world”两句话也有了其自身的合理解决路径,我们基本上成功处理了这两句话在自然语言编译器中可能存在的歧义问题,同时没有引入灾难性的递归/冲突。
让我们常试着阶段性地总结“自然语言编译器”的概念描述吧:
- 定义 – 自然语言编译器是一种转换器。它不直接执行自然语言指令,它会将具备模糊性的朴素自然语言描述,转化为一段确定性的、可运行的目标编程语言代码。自然语言编译器的核心工作机制包含三个层面:世界模型、前提条件消歧、场景解析与优先级匹配
- 世界模型:编译器必须预设一个相对稳定的“世界模型”作为背景板。该模型由具备内在含义的个体(如名词、概念)及其属性与关系构成,为自然语言提供语义解释的基础环境。
- 前提条件消歧:当世界模型内的查表结果存在多条合理路径时,编译器依赖一层可实时修改、可交互的“前提条件”进行约束。这些前提条件本身也是世界模型内的合法表达,用于限定查表范围,使模糊的自然语言最终收敛为唯一确定的代码实现路径。
- 场景解析与优先级匹配:编译器通过分析句子中的词汇与句法,在世界模型中进行数据检索。它依据特定词汇与句法预设的“场景优先级”,从词汇的多种潜在含义(如字面文本、视觉图像、抽象概念等)中自动提取出当前语境下最匹配的语义,以此区分词汇的“使用”与“提及”。
简言之,我们目前得到的最基础自然语言编译器的工作模式是:在世界模型的基础上,通过场景优先级和前提条件的共同作用,将模糊的自然语言文本“编译”为精确计算机代码。
豁然开朗……吗?
捕鱼的武陵人在游览一个未曾见过的美丽桃花林时,在桃花林的尽头意外遇到了一个小山,山体上有一个隐约透出光亮的狭窄入口。他抛下渔船,在这段只有一人宽的隐秘山洞里行走了几十步,而后视野豁然开朗:“土地平旷,屋舍俨然,有良田美池桑竹之属。阡陌交通,鸡犬相闻。其中往来种作,男女衣着,悉如外人。黄发垂髫,并怡然自乐。”
多美丽的世外桃源啊,然:“太守即遣人随其往,寻向所志,遂迷,不复得路。南阳刘子骥,高尚士也,闻之,欣然规往。未果,寻病终。后遂无问津者。”
这世外桃源存在吗?也许存在,也许不存在。无论如何,《桃花源记》总归是向我们展示了一个美好的世界。只需走进那个山洞,只需复行数十步,便可得豁然开朗。
然而可以明确的是,我现在向各位展示的这个名为“自然语言编程”的世界,不可能存在这种便宜的好事。复行数十步后,前方等待着我们的只会是百步、千步、千千万万步。
文章写到现在,零零总总加起来也一万多两万个字了。把演示用的代码算上,23000多个字符。可别忘了,截至目前真正意义上参与到我们这个想象中的自然语言编程系统中的“自然语言代码”,实际只有一条:
让我们回到最初,几乎是所有程序员学习编程的开始:Hello World.
设想我们面对一个最基础的任务:用一段自然语言描述向计算机发出指令。
"输出hello world."
天啊!“输出hello world”?!这是冰山一角吗?可别说笑,我们目前看到的充其量只能算是海水上破碎的浮冰,莫说是冰山一角,文章到此可连冰山的影子都没见到。
M = <D, F> 体系接下来要怎么演进?class Item、class Verb体系接下来又要怎么演进?这两个甚至以后的很多个体系会在什么时候会合?我作为讨论工具使用的“前提条件中间层”究竟意味着什么?为什么我一定要构建这样一个中间层?我截至目前提供的这些概念,这些设计,这些代码,它们真的是“合理正确”的吗?在代码的世界,在语言学的世界,在数学的世界,我们的“自然语言编译器”究竟还要触碰到多少新概念,还要融入多少新想法,未来要推翻多少已经被书写的内容?
还有一点,我相信很多读者看到这里已经憋坏了:在整篇文章里,除了几次题外话的强调,我几乎完全没碰任何AI相关的讨论,少量几次,也无不透露出我对于“许愿式编程”的谨慎,可能有少量读者已经在摩拳擦掌,准备着冲到评论区教教我怎么用Codex或者Claude Code之类的东西,然后把我的文章批判一番了。
其实还真不是我不想写不想聊,我准备了很多东西,本想把关于“LLM在自然语言编程中意味着什么”的部分放在现在的这个小节,可能大概也会有至少4000多接近5000字,小节名都想好了,就叫做“复行数十步……再行数十步”。但受限于精力,想着至少在8月份发出来,而且最后假如一不小心写嗨了很有可能又要搞出去10000多字,那就不好了,这篇文可能9月份都发不出来。
总之,人力有尽,仅仅是为了“输出hello world”这么一句话,只是正文内容本身就已经能让我花去2万字,让我花去七天几十个小时时间来斟字酌句,如果算上选题、提炼大纲、确定书写模式等工作的时间,那就远不止如此了。而我就在这种书写、删除、又书写的纠结中,浑然无知地度过了我的28岁生日,8月25日——顺带一提,也是Linux的生日——其实我想赶在8月25日发的来着,但更早的时候甚至没有想好到底应该怎么写,写到多深,确实没救,尽力了。
未来的系列文章也会在这个仓库同步更新,欢迎star:https://github.com/haberzero/Notes-of-Natural-Language-Programming
很显然,这会是一个超长超长的系列文章,就像我说的,连冰山的影子都还没看见呢,不可能在这里结束。写得不会很快,每过一到两个月能有一篇相同字数相同质量的都已经是万幸了。至于做成视频?想过,这个系列就还是别了,我会死在这的。这个系列如果最后做视频也必然是充电视频。
借着这篇文章还要宣传点东西:
https://github.com/haberzero/intent-behavior-code-inter
交互式意图行为代码(Intent Behavior Code – Interactive)。我设计的一种实验性编程语言,最主要的灵感是一个问题:“假如我们把LLM交互当作编程语言的基本要素,这门编程语言会是什么样的?”
这门语言里,我们把LLM当作一种表达式,一种函数,一种可调用的类看待。书写结构借鉴Python,一些函数式编程的概念则借鉴Rust。当我们把LLM当作表达式看待时,编程语言里的其它所有概念:变量、函数、类,等等,到底应该如何与LLM表达式进行交互,同时我们要如何面对LLM的不确定性。这就是IBC-Inter想要实验与探讨的东西。同时,它也与本系列关于自然语言编程的分析讨论紧密相关。
实验阶段由Python实现内核逻辑,因为目前来说与LLM交互本来也不需要语言自身的性能,未来会迁移。目前99.9%的代码都是AI生成,主力模型是 GLM 5.2 / DS V4 Flash。
到这里就彻底结束了。真的非常非常感谢你的阅读观看。到这里估摸着得有40~60分钟的阅读时间,可以站起来喝杯水、走一走(笑)。那我们,下一篇文章再见!
