Scaffolded LLM 即自然语言计算机
来源:beren.io · 2023-04-11 · Beren Millidge
最近,基于 LLM 的 agent 火得一塌糊涂——AutoGPT 这类项目展示了把 LLM 套进一个简单的 agentic loop、再用 prompt 驱使它去完成现实任务有多容易。更宽泛地,我们可以谈论一类"scaffolded(带支架的)"1 LLM 系统——在 LLM 内核外包一层程序化的支架,把若干次独立的 LLM 调用串起来,去完成一个超出单条 prompt 能力范围的更大、更复杂的任务。Scaffolded LLM 这个想法并不新,但有了 GPT4 之后,底层 LLM 的可靠性和指令遵循能力可能已经过线,agent 之类的方案才得以规模化跑通。然而,缺失且紧迫的,是对全局的理解。Scaffolded LLM 不只是好玩的玩具,它实际上是一种新型通用自然语言计算机的底层基板。

'generative agent' 的架构。一个 scaffolded LLM 程序。
举个例子,看看最近一篇论文 里的 'generative agent' 架构。架构核心是一个接收指令、执行自然语言任务的 LLM。外面有一组指定这些任务和数据的 prompt 模板,供 LLM 操作。还有一块 memory,存储远超 LLM 单次能消化的上下文,并且可以由计算单元读写。简而言之,这玩意儿造出来长得像下面这个样子:

冯·诺依曼计算机架构。
我们干的事情,本质上是重新发明了冯·诺依曼架构,更进一步说,我们重新发明了通用计算机。这种趋同进化并不奇怪——冯·诺依曼架构对设计计算机来说是一个非常自然的抽象。不过,如果我们造出来的是一台计算机,那它是非常特殊的一种。和数字计算机一样,它是完全通用的,但它操作的对象不是 bit,而是 文本。我们造出了一台 自然语言 计算机,它以自然语言文本为单位输入,产出经过更进一步加工的自然语言文本。和数字计算机一样,我们这台自然语言(NL)计算机理论上完全通用——图灵机的一切操作都可以用自然语言写下来——而且极其有用:现实世界里的许多系统,包括人类自己,更愿意以自然语言运转。很多任务用代码精确描述很困难,但用一两句自然语言就能讲清楚。
带着这个类比,我们尽量把它推到极限,看看会得出什么结论。
先来把 scaffolded LLM 的各个组件和数字计算机的硬件架构对应清楚。LLM 本身显然对应 CPU,是系统中真正发生 "计算" 的地方。但和 CPU 不同,它操作的单位是上下文窗口里的 token,而不是寄存器里的 bit。如果 CPU 的自然类型签名是 bits -> bits,那自然语言处理单元(NLPU)的自然类型就是 strings -> strings。Prompt 和 "context" 直接对应 RAM——CPU 能快速访问、快速操作的那块易访存储。第三层是 memory。在数字计算机里有显式的存储库或者 "disk" 这种慢速存储。它直接对应 scaffolded LLM 里的向量数据库 memory。我们目前用的那些启发式(比如基于 embedding 的向量搜索)来决定何时取出特定记忆,等同于数字计算机里的 memory controller(内存控制器)固件,由它处理 CPU 对特定内存地址的访问。最后,CPU 还得跟外部世界交互。在数字计算机里,这是通过 "驱动" 或者特殊的硬件/软件模块实现的,让 CPU 能控制显示器、打印机、鼠标等外设。对 scaffolded LLM,则是 插件 以及类似机制。最后,还有围绕 LLM 内核的 "scaffolding(支架)" 代码。这层代码实现了把单次 LLM 调用串起来的协议,比如一个 ReAct agent loop,或者一个 递归式书籍摘要器。这些协议就是跑在我们这台自然语言计算机上的 "程序"。
有了这些等价关系,我们也可以来谈核心的性能单位。对数字计算机来说,是 CPU 每秒能执行的操作数量(FLOPs)和系统可用的 RAM。这两个单位在自然语言计算机里都有精确对应。RAM 就是 context length。GPT4 当前的 8K 上下文等价于 8kbit 的 RAM(理论上很快会扩展到 32kbit)。这相当于把我们带回到数字计算机里的 Commodore 64,时间坐标在 80 年代初。FLOP 计数也有等价物。每次 LLM 调用/生成可以视作完成一次计算任务——一次自然语言操作(NLOP)。简单起见,假设从一个 prompt 生成约 100 个 token 算作一次 NLOP,那我们就能算出不同 LLM 的 NLOPs/秒。GPT4 大概是 1 NLOP/秒,GPT3.5 turbo 快约 10 倍,所以是 10 NLOPs/秒。这跟动辄数十亿 FLOPs/秒的 CPU 比相差悬殊。但单次 NLOP 比一条 CPU 指令复杂得多,直接对比并不公平。话虽如此,NLOP 这个数仍然是关键指标。任何认真玩过 GPT4 的人都知道,GPT4 响应的迟缓本身才是核心瓶颈,而不是钱。
有了性能单位之后,下一个问题是:我们是否该期待出现摩尔定律式或其他指数级的能力提升?显然,整个 LLM 范式才 3 年,下定论还太早。但我们已经看到不少翻倍。GPT3 到现在 3 年里,上下文长度翻了 4 倍(2K 到 8K)。底层 LLM 的能力和 NLOP 的速度也大幅上升(GPT3 到 GPT4 至少翻倍),尽管缺乏精确量化。这一切都被 LLM 及其训练 run 的指数级规模与成本驱动着——GPT4 的训练估计花了 1 亿美元,而最大规模的 run 预计在未来两年摸到 10 亿美元。我的预测是:指数级提升至少还会持续几年,可能更久。但 5 到 10 年内,单次训练 run 的可投入资金大概率会撞上天花板(10B 这个量级几乎没有玩家能再往上走)。再之后,重要的就不是堆资源,而是参数和数据的高效利用,以及底层 GPU 硬件的进步。
把 scaffolded LLM 概念化为自然语言计算机,除了定义性能单位,还能给出哪些预测或洞察?
编程语言
谈数字计算机编程,自然就要谈编程语言。NL 计算机能有编程语言吗?长什么样?显然能有。我们已经在搭最初的原语了。Chain of thought(思维链)。Selection-inference(选择-推断)。Self-correction loops(自纠循环)。Reflection(反思)。这些都比单次 NLOP 抽象层级更高。我们达到了汇编语言的水平。CoT、SI、reflection 就是汇编里大家熟悉的 mov、leq 和 goto。借助 langchain 这类库和复杂的 prompt 模板,我们或许已经在搭最早的编译器,尽管目前非常原始。我们还远没到 C 语言。我们甚至对它会长什么样都没什么把握。再往上走还有大把抽象层我们连边都没摸到。要解锁这些抽象,需要时间,也需要远超当前水平的 NL 算力。因为搭出不漏抽象(non-leaky abstraction)本身有根本成本。函数式或动态语言永远比裸金属 C 慢,这是有原因的——抽象有开销。在 NLOP 像今天这么贵的当下,我们没法真正地用或者拿这些抽象做实验;但我们终会能。
不仅是编程语言,整个为这台自然语言计算机写好 "软件" 的空间,目前也几乎完全没人探。我们还在摸索什么样的硬件合适、最基础的汇编是什么样。我们已经开始开发简单算法——比如递归文本摘要——以及简单的数据结构,比如 "memory stream",但这些只是最初步。还有整片自然语言算法和数据结构的世界,潜伏在可能性的边界之外,对我们而言完全未知。
理论
数字计算机出现 之前,就已经积累了大量理论。图灵、哥德尔等人在计算机还不存在时就奠定了算法的基础。Lambda 演算 30 年代起步,到 50 年代已经成为高度发达的逻辑分支,那时计算机还又贵又稀有。硬件设计方面,布尔逻辑在它成为数字电路核心之前已被人类掌握了一百年。算法复杂度理论、类型论、编程语言设计这些极其精致的理论与摩尔定律并行了几十年。相比之下,NL 计算机几乎不存在与之对应的形式理论。仅有 simulators frame 这类最初步的尝试在去年发表。
举例来说,NLOP 这个概念本身几乎完全未定义。除了"任意自然语言变换"以外,我们对单次 NLOP 的边界没有任何概念。我们没有等价于数字逻辑里 NAND 门那样的最小自然语言电路,能用来表达任意 NL 程序。我们对一门由 NLOP 组成的编程语言会怎么运作、能产生什么算法,也没什么真切的概念。我们也没有底层电路正确行为规范的真值表。
执行模型
考虑一下自然语言程序的"执行模型"。CPU 经典上是线性执行:指令一条条读入再串行执行。然而,你想并行调用多少次 LLM 都行。我们这台 NL 计算机的自然执行模型,更像是一个不断扩张的并行 NLOPs DAG,受限于程序本身固有的串行性,而 不 受限于 "硬件"。事实上,我们重新发明了 dataflow architecture(数据流架构)。
计算机硬件本质上是同像(homoiconic)的——CPU 的 opcode 跟其他东西一样都是 bit,可以像 "data" 一样被操作。"指令" 与 "数据" 之间没有原则性区别,只有约定。自然语言计算机也是这样。对一次 NLOP 来说,prompt 就是全部——没有 "context" 和 "instruction" 的区分。但和数字计算机一样,我们正开始在 prompt 里发展把命令与语义内容分开的约定。比如 GPT4 引入 "system prompt" 暗示我们在演化 RAM 里的受保护内存区。在常见用法里,人们经常把 "context" 从 "prompt" 里分离出来,这时 prompt 就更显式地承担 op-code 的角色。比如 prompt 可能是:"请总结这些文档:…[文档列表]"。这里,"summary" 命令是 opcode,文档列表是 RAM 中其余位置的 context。这样一次 LLM 调用就是一次 NLOP。
存储层级
当今数字计算机有复杂的存储层级,不同层级在容量、便宜程度与延迟之间做权衡。从 disk(极大、便宜但慢)到 RAM(各方面中等)到片上 cache(极快但极贵且受限)。当前的 scaffolded LLM 只有两层 "cache/RAM"——直接喂给 LLM 的 prompt context,以及 "memory"——通常是个向量数据库或一组外部事实。随着设计成熟,存储层级里大概率会增加更多层。可能包括 LLM 架构 内部 的额外 cache 层——比如稠密上下文 vs 稀疏/局部注意上下文;也可能在外部,把一次 NLOP 拆成一组 LLM 子调用,让它们用并选择长期记忆里不同的 context。一种初始做法是:用 LLM 给长期记忆里的各段 context 做相关性排序,只把最相关的塞进真正执行 NLOP 那次 LLM 调用的 context。这里就是用执行排序步骤的成本和时间,去换延迟与容量之间的平衡。
基础模型即认知硬件
虽然 scaffolded LLM 整个 stack 严格来说每一层都是软件,但核心 LLM 与 CPU 硬件之间的关系比一般类比要强。基础模型在很多方面更具有经典硬件而非软件的属性——可以把它们看作支撑 "软件" 支架的 "认知硬件"。基础模型本质上是巨大的 I/O 黑盒,坐在围绕它的支架中央。在没有强力可解释性或控制工具的情况下,我们没法轻易拆开它、调试它,乃至修复已知 bug。它没有版本控制,几乎也没有针对其行为的测试。我们只有一个不可解释、又极其昂贵的黑盒。从 ML 模型生产方角度看,基础模型也有相似特征。它们脆弱、昂贵,迭代周期长2。如果一次训练搞砸了,没有 push-to-github 这种简单修复,可能要等几个月才能重启训练。一旦模型上线,它的很多行为基本被锁定。你可以用 fine-tuning、RLHF 和其他后训练手段做一些控制,但大量行为和性能在预训练阶段就已经被烙进去了。这一切都很像硬件公司在部署上面对的难题。
而且和硬件一样,基础模型也是高度通用的。一个模型可以完成许多不同任务,并且像 CPU 那样跑各种各样的 NLOP 和程序。此外,基础模型与跑在其上的 "程序" 已经具有一定可移植性,未来很可能更强。理论上换个模型只要改 API 调用。实务里很少这么省事。为了应对某个特定 LLM 的不可靠和各种失败模式,大量 prompt、保险机制和隐含知识最终都被硬写进程序里。这都限制了即时可移植性。但这本质上只是抽象层不够发达、写代码离金属(离神经元?)太近的症状。早期计算机程序也是针对某种具体硬件架构写的,互相之间不可移植——这种状况一直持续到 90 年代才大幅改善。随着 LLM 变强、变可靠,人们围绕它发展出更好的抽象,可移植性也会改善,硬件-软件解耦与模块化会越来越明显,越来越有用。
scaffolded LLM 中其他 "硬件" 部件在较弱程度上也是这样。比如 memory 通常是 faiss 这类向量数据库,对大多数人同样是个难替换、难适配的黑盒 API 调用。而 memory-controller "固件"——也就是写出来的、用于寻址和管理 LLM 长期记忆的启发式逻辑——则容易理解、更新和替换。这意味着,一旦自然语言程序与 "软件" 开始普及,我们应该能预期它会出现今天硬件与软件之间相同的动态:生产 NL 程序会比生产 "硬件" 便宜得多、入门门槛低得多,"硬件" 几乎对所有人都贵得不切实际。NL 软件迭代速度远快于硬件,会成为分布式创新的主要发生地。
与数字计算机的本质差异
把 scaffolded LLM 与数字计算机的类比推得很远,但这个类比也在若干重要方向上发散,几乎所有发散都围绕 NLOP 这个概念以及把 LLM 当作 NLPU 这件事。和数字 CPU 不同,LLM 有些不太顺手的属性,让我们当下很难用它构建高度可靠的链式程序。NLOP 的昂贵和缓慢已经显而易见,且严重约束当前的程序设计。这些问题随时间应能缓解。其他关键差异是当前 NLOP 的不可靠性、不充分定义(underspecifiability)和非确定性。
拿 NLOP 的一个经典例子:文本摘要。摘要看起来是个有用的自然语言原语,对人类有内在用途,并且开始在自然语言数据结构里扮演关键角色——把记忆和上下文压成能塞进有限 context 的样子。但和 CPU 操作不同,摘要是不充分定义的:从输入到输出是一对多映射。同一段文本有许多有效的摘要,质量参差。我们没有一张通往 "最优" 摘要的地图,甚至连这个概念在多目标多约束下意味着什么都不清楚。摘要也是不可靠的:不同的 LLM3、不同的 prompt(甚至同一个 prompt 在高 temperature 下)能给出质量与价值差异巨大的摘要。LLM 甚至连零 temperature 下都不是确定性的(这点听起来反常但属实,自己跑就能验证。原因是为加速推理而启用的非确定性 CUDA 优化)。这一切都和数字硬件极其不一样——后者极度可靠,I/O 规范固定且已知。
这意味着,在我们能开始搭起强大的抽象和抽象语言之前,单次 NLOP 的可靠性必须被显著提高。抽象需要可靠的底座。数字计算机适合在上面盖抽象塔,正是因为这种可靠性。如果你能高度信任系统的所有组件,就可以构造精巧的组合链。否则,你就一直在和混沌发散对抗。提升可靠性的方法包括:更好的 prompting、更好的 LLM 组件、更好的 tuning,以及叠加大量纠错层。纠错本身在硬件领域并非新事——大量研究花在了修复 bit-flip 的纠错码上。我们大概率也需要类似的 "语义纠错码" 来对 LLM 的输出做处理,才能把一长串 NLOP 高度连贯一致地拼接起来。
不过,虽然 NLOP 的不可靠和不充分定义难以承重,但也带来巨大的机会。LLM 的灵活性无可匹敌。和有固定指令集和已知 op-code 的 CPU 不同,理论上你可以 prompt 一个 LLM 去尝试几乎任意一个自然语言任务。op-code 集合不再固定,而是不断膨胀。就像我们一直在发现新的逻辑门。任务原语的总量到底多大、是否最终会像逻辑电路那样存在完整分解,目前尚不清楚。不止于此,把 prompt(或者 op-code)合并、串联起来,得到的行为是半组合(且不可靠)的——这非常顺手。我们可以基于 prompt 模板方案造出整套语言。从指令集架构的视角看,CPU 那边 RISC 看上去赢了,但基于 LLM 的"计算机"内在地像在 CISC 模式下运转。未来(甚至现在)大概会有一个与 RISC vs CISC 同构的争论:是把许多简单 prompt 复杂地串起来更好,还是用更少但更复杂的 prompt 更好。
脚注
原文链接: https://www.beren.io/2023-04-11-Scaffolded-LLMs-natural-language-computers/
-
我在这里说 "scaffolded" LLM 而不是某篇 近期文章 里那种 "agentized" LLM,是因为 agent 现在虽然热门,但这个想法的范畴更广。不是所有自然语言程序都需要做成 agent。Agent 是对某一类任务很自然的抽象,但还有别的。 ↩︎
-
这个类比一个有趣的副产品是它澄清了 OpenAI 这类基础模型供应商当前所处的角色和经济地位。它们基本与数字计算机时代的大芯片厂(如 Intel)占据同一个经济生态位。商业结构非常相似:训练基础模型有巨额固定资本支出(建新晶圆厂也是);它们都面对一个不断改进、新一代远比旧一代强的技术(摩尔定律 vs 当代 AI scaling);都以大批量、高毛利但伴随显著边际成本的方式售卖商品化产品(芯片 vs API 调用)(实际制造每块芯片 vs 每次模型推理)。如果这些等价关系成立,我们就能大致猜到这个行业长期会长成什么样——也就是当前以及历史上的半导体业。我们应该预期它会整合成几家寡头巨头,每家都背着巨额固定成本、保持激烈竞争,而不会像 SaaS 或纯软件公司那样以极高利润率印钞。 ↩︎
-
NLOP 与标准 FLOP 还有一个关键差别:它们的"内在难度"分级不同。一个小语言模型能完成某些任务,另一些则需要最先进的大模型。随着 NL 程序变得越来越复杂精巧,对各类 op 难度的理解也会加深,每个 op 都会被委派给"能可靠完成它的最小最便宜的语言模型"。这样,NL 程序不会有一个统一的 "CPU"(LLM)内核,而是会由对许多不同规模、不同专长的语言模型的异构调用组成。 ↩︎