大模型和编程--写在 2024 年初

本文翻译自:http://antirez.com/news/140
作者: Antirez(本名为 Salvatore Sanfilippo,Redis 作者)
发布时间:2024 年 1 月 1 日。

我想先明确一点,这篇文章并不旨在回顾大语言模型(LLMs)。毫无疑问,2023年对人工智能领域而言是具有里程碑意义的一年,但重复这一点似乎没有太多意义。这篇文章更多是一名程序员个人的体验分享。自从 ChatGPT 面世,以及后来我开始使用能在本地运行的大语言模型(LLMs),我就深入地应用了这一新技术。我的目标是提高编程效率,但这不是唯一的理由。另一个重要的目的是避免在编程中那些不值得投入精力的部分浪费心智。无数个小时被花在搜索那些乏味、缺乏智力挑战的细节文档上;努力去理解一个复杂得无谓的 API;编写一些几小时后就被弃用的即用型程序。这些都是我特别想避免的,特别是在如今这个时代,谷歌搜索结果中充斥着大量无用信息,要在其中找到真正有价值的内容变得越来越困难。

LLM

同时,我也绝非编程的初学者。我完全有能力在没有任何外部帮助的情况下编写代码,而事实上我也经常这样做。随着时间的推移,我越来越频繁地使用大语言模型(LLMs)来编写高层次的代码,尤其是 Python 语言,在 C 语言中则使用得较少。通过我的个人经验,我明白了何时应该借助大语言模型来提高效率,以及何时这样做反而会降低我的工作速度。我还意识到,大语言模型有点像维基百科和 YouTube 上的视频教程:对于那些有意愿、能力和自律的人,它们是极好的帮助工具,但对于那些已经落后的人来说,它们的帮助可能就显得有限。我担忧的是,至少在初始阶段,这些工具可能主要惠及那些本就具备一定优势的人。

让我们一步一步地来看。

全知全能还是鹦鹉学舌?

在机器学习新浪潮带来的创新和进步中,最引人关注的现象之一是AI专家们难以接受自己知识上的局限性。人类发明了神经网络,并且更关键地,发明了一种自动优化神经网络参数的算法。硬件的发展使得训练越来越大型的模型成为可能,借助对待处理数据的统计先验知识,以及大量的尝试和逐步优化,人们发现了一些比其它架构更高效的模型。但总体而言,神经网络的内部机制仍然相对不透明。

面对大语言模型(LLM)一些新兴能力难以解释的现状,科学家们本应表现出更多的谨慎。然而,相反的是,许多人严重低估了LLM的能力,认为它们不过是一些略微高级的马尔可夫链,顶多能重复训练集中见过的极为有限的变体。但随着证据的出现,这种将LLM视为“鹦鹉”的看法几乎被普遍放弃。

与此同时,许多热情的观众过分夸大了LLM的能力,赋予了它们在现实中根本不存在的超自然力量。遗憾的是,LLM最多只能在其训练过程中接触到的数据所构成的空间内进行插值:即便如此,这已经相当了不起。实际上,它们的插值能力是有限的(尽管这依然令人惊叹,并且超出了预期)。哦,如果当今最大的LLM能在它们接触过的所有代码构成的空间内连续进行插值就好了!即使它们不能创造真正的新颖事物,也足以取代99%的程序员。然而,现实总是更为平凡。LLM确实能编写它们以前未曾完全见过的程序,展现出将训练集中频繁出现的不同思想融合的能力。但这种能力目前还有其深刻的局限性,一旦涉及到微妙的推理,LLM就会遭遇彻底的失败。尽管如此,它们仍代表了从人工智能诞生至今的最伟大成就,这一点似乎是毋庸置疑的。

愚蠢但无所不知

确实,大语言模型(LLM)在推理方面的能力有其局限性,这些推理往往不够精确,有时甚至会出现一些基于虚构事实的错误判断。但它们却拥有海量的知识储备。在编程领域,以及其他有高质量数据支持的领域,LLM就像是拥有丰富知识却缺乏深度思考能力的“书呆子”。与这样的合作伙伴一起编程(对我而言,双人编程本身就有些棘手)可能会令人头疼:它们可能提出一些不切实际的想法,我们不得不不断斗争,坚持自己的观点。但如果这样一位博学却简单思考的“伙伴”听从我们的指挥,回答我们提出的所有问题,情况就截然不同。目前的LLM无法带我们探索知识的未知领域,但当我们面对自己不太熟悉的主题时,它们常常能帮助我们从一无所知到拥有足够的知识独立前行。

在编程领域,或许直到二三十年前,LLM的这种能力还不那么引人注目。那时候,你只需要掌握一两种编程语言、经典算法以及一些基础库。剩下的部分需要靠你自己的智慧、专业知识和设计能力去补充。如果你具备这些条件,你就能成为一名能够应对各种任务的专家级程序员。但随着时间的推移,我们见证了框架、编程语言和各种库的爆炸式增长。这种复杂性往往是不必要且无理由的,但这就是现实。在这样的环境中,一个知识渊博却思维简单的“助手”成为了宝贵的帮手。

举个例子:我在机器学习领域的实验至少持续了一年,使用的是Keras。后来由于种种原因,我转向了PyTorch。我已经了解了嵌入(embedding)和残差网络(residual network)等概念,但我不想像之前学习Keras时那样逐步钻研PyTorch的文档(那时ChatGPT还未出现)。有了LLM的帮助,编写使用Torch的Python代码变得非常容易。我所需要做的,就是对我想构建的模型有一个清晰的构思,并提出恰当的问题。

举个例子

我要讲述的不是像“X 类的方法用来做什么?”这样简单的问题。如果只是这些,可能有人会赞同那些对大语言模型(LLMs)持怀疑态度的观点。但实际上,更高级的模型能够完成的任务远比这复杂得多。直到几年前,这些都还像是不可思议的魔法。我可以这样指示 GPT4: 看这里,这是我用 PyTorch 实现的神经网络模型。这些是我处理的数据批次。我想要调整张量(tensors)的大小,使得输出数据批次的函数能与神经网络的输入相兼容,同时我希望以这种特殊的方式来表示数据。你能展示给我如何编写实现这种数据重塑的代码吗?GPT4 会编写出这样的代码,我只需要在 Python 命令行界面(CLI)中测试一下,看看这些张量的维度是否符合我的需求,数据布局是否正确。

这里有个例子。之前,我需要为一些基于 ESP32 的设备开发 BLE 客户端。经过调研,我发现大多数跨平台的蓝牙编程接口基本上都难以使用。解决方案其实很直接:用 Objective C 语言,利用 macOS 的原生 API 来编写代码。这让我面临了两个挑战:一是要学习 Objective C 中复杂的 BLE API,这些 API 充满了我认为完全不合理的设计模式(作为一个崇尚简洁的人,我觉得这些设计违背了我对“好设计”的理解);二是要回忆起如何使用 Objective C 编程。我上一次用 Objective C 写程序还是十年前,对于事件循环、内存管理等许多细节都已经记不清了。

最终的代码如下,虽不完美,但确实实现了预期功能。我在非常短的时间内就编写完成了。否则根本不可能实现。

https://github.com/antirez/freakwan/blob/main/osx-bte-cli/SerialBTE.m

这段代码的编写过程主要依赖于我在 ChatGPT 上进行的一系列剪切和粘贴操作,因为有些我想实现但不太懂的功能不能正常工作。大语言模型(LLM)帮我分析了问题所在并提供了解决方案。虽然大部分代码不是直接由 LLM 编写的,但 LLM 的确大幅提高了我的编码效率。没有 ChatGPT 我也能完成吗?当然,但更重要的不是它可能耗费更多的时间:实际上,若无 ChatGPT 的帮助,我可能连尝试都不会去做,因为那并不划算。这一点非常关键。对于一个与我的主要项目关联不大的程序,投入与产出的比例本来是不理想的。此外,这还带来了一个额外的好处,比程序本身更有价值:为了这个项目,我还对 linenoise(我的一个行编辑库)进行了修改,使其支持多路复用。

这是另一个例子,不过这次更偏重于数据解析,而不是编程。我曾计划利用我在网上找到的一个卷积神经网络来编写一个 Python 脚本,但这个网络的文档非常缺乏。这个网络的一个优势是它使用了 ONNX 格式,这让我能轻松地提取出输入和输出的列表及其命名。我对这个卷积网了解的只有一点:它能在图像中检测特定特征。我不清楚输入图像的格式和大小,更难以理解的是,网络的输出远比我预想的复杂(我原本以为它是个二元分类器,用来判断图像是否正常或有问题,但实际上输出有数百种)。我开始的步骤是把 ONNX 网络的元数据输出复制到 ChatGPT。我向助手解释了我所知的关于网络的有限信息。ChatGPT 推测了输入的组织方式,以及输出可能是标准化的框,用于指示图像中与潜在缺陷相对应的部分,还有其他表示这些缺陷可能性的输出。经过几分钟的讨论,我得到了一个能够进行网络推理的 Python 脚本,以及将初始图像转换为适合输入的张量所需的所有代码。最让我印象深刻的是,当 ChatGPT 观察到测试图像的原始输出值(基本上是 logits)时,它终于“理解”了网络的工作原理:一系列浮点数提供了识别确切的输出细节、标准化过程、框是居中还是指定左上角等信息的上下文。

一次性程序

我可以举出许多我之前提到的类似案例。但这并没有太大意义,因为它们大同小异,不过是重复同样的故事。当我遇到问题,急需确认一些信息,特别是当我不确定大语言模型 (LLM) 是否提供了准确信息时,我就会用它来快速获取我所需的知识。

但在其他情况下,我会完全依赖大语言模型来编写所有代码。就比如当我需要编写一个基本上只用一次的程序时,像这样的一个程序:

https://github.com/antirez/simple-language-model/blob/main/plot.py

我需要制作一个小型神经网络在学习过程中损失曲线的可视化图。我向 GPT4 输入了 PyTorch 程序在学习过程中产生的 CSV 文件格式,并请求如果在命令行中指定多个 CSV 文件,不要显示同一个实验的训练和验证损失曲线,而是要展示不同实验的验证损失曲线进行比较。GPT4 生成的结果正是我所需,整个过程只花了三十秒。

同理,我还需要一个程序来分析 AirBnB 的 CSV 报告,根据月份和年份对我的公寓进行分类。接着,该程序会考虑清洁费和每次预订的夜数,统计不同月份的平均租金价格。这个程序对我非常有用。但编写它又极其枯燥:没有任何趣味性。因此,我把 CSV 文件的一部分内容复制粘贴到 GPT4 中。我向大语言模型说明了需要解决的问题,结果这个程序一试就成功了。下面我将完整展示这个程序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pandas as pd
pd.set_option('display.max_rows', None)
df = pd.read_csv('listings.csv')
reservations = df[df['Type'] == 'Reservation']
reservations['Start Date'] = pd.to_datetime(reservations['Start Date'])
reservations['Year'] = reservations['Start Date'].dt.year
reservations['Month'] = reservations['Start Date'].dt.month
reservations['Nightly Rate'] = (reservations['Amount'] - reservations['Cleaning Fee']) / reservations['Nights']
all_listings = reservations['Listing'].unique()
all_years = reservations['Year'].unique()
all_months = range(1, 13)
index = pd.MultiIndex.from_product([all_listings, all_years, all_months], names=['Listing', 'Year', 'Month'])
all_data = pd.DataFrame(index=index).reset_index()
merged_data = pd.merge(all_data, reservations, on=['Listing', 'Year', 'Month'], how='left')
average_nightly_rates = merged_data.groupby(['Listing', 'Year', 'Month'])['Nightly Rate'].mean().fillna(0)
print(average_nightly_rates)

要准确地理解怎样对这些本来分散无序的数据进行分类,我们需要简单的推理:这不是大语言模型(LLM)仅凭借复述其所学过的语言就能轻易解决的问题。虽然 GPT4 在训练过程中肯定遇到过类似的程序,但它几乎肯定没有见过需要按照我所提出的特定方式在 CSV 文件中对数据进行分类的程序。这表明(我个人认为),大语言模型能够在训练集中涵盖的各种程序范围内做到一定程度的推断和应用。

在我看来,亲自编写这样的程序是对时间的浪费。大语言模型能够帮我完成这项工作,无疑直接加速了我的编码效率:我可以把精力集中在更重要的事务上。

系统编程中的意外挑战

虽然我在将大语言模型(LLM)应用于编程方面有所成就,但每当我编写 C 语言程序时,我通常只把大语言模型当作一种更便捷的文档工具。作为系统编程领域的专家,我发现只有在大语言模型能进行复杂的逻辑推理时,它们才真正对我有帮助。然而,在这种情况下,大语言模型的表现通常不尽如人意。

首先,让我们来确定一个优质且实用的 C 语言布隆过滤器(Bloom Filter)实现所需的基本条件:

重点应放在哈希函数的设计上,确保它既有效又高效。接下来,用精良的 C 语言技术来编写这个程序。

同时,确保你的布隆过滤器示例能够存储 100,000 个元素,并且误报率不超过 5%。需要添加的元素是以空字符结尾的字符串。

下面是实验的输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

#define N 100000 // Number of elements
#define P 0.05 // False positive probability
#define M (int)(-(N * log(P)) / (log(2) * log(2))) // Size of bit array
#define K (int)(M / N * log(2)) // Number of hash functions

unsigned char bitArray[M / 8 + 1];

void setBit(int n) {
bitArray[n / 8] |= 1 << (n % 8);
}

int getBit(int n) {
return bitArray[n / 8] & (1 << (n % 8));
}

// Basic hash functions (for demonstration purposes)
int hash1(char *str) {
int hash = 5381;
int c;
while ((c = *str++))
hash = ((hash << 5) + hash) + c;
return abs(hash % M);
}

int hash2(char *str) {
unsigned long hash = 0;
int c;
while ((c = *str++))
hash = c + (hash << 6) + (hash << 16) - hash;
return abs(hash % M);
}

void add(char *str) {
setBit(hash1(str));
setBit(hash2(str));
// Add more hash functions if needed
}

int is_in(char *str) {
return getBit(hash1(str)) && getBit(hash2(str));
// Check more hash functions if used
}

int main() {
char *testStr = "testString";
add(testStr);

if (is_in(testStr)) {
printf("%s is probably in the set.\n", testStr);
} else {
printf("%s is definitely not in the set.\n", testStr);
}

return 0;
}

GPT4 所实现的布隆过滤器并没有达到非常出色的水平。布隆过滤器其实非常常见,并不是什么稀奇古怪的数据结构。但显而易见,要编写一个合格的布隆过滤器,需要较高的抽象思维能力:比如,如何有效地对同一字符串进行 N 次哈希,并确保这些哈希值彼此之间足够独立。

不过,如果你明确指导 GPT4 对哈希函数进行修改,让它能产生 N 个独立的输出,那么它提出的方案就会明显更加符合要求:

1
2
3
4
5
6
7
8
unsigned int hash(const char* str, unsigned int hash_id) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c + hash_id;
}
return hash % BF_SIZE;
}

如果 GPT4 能够自主提出这种方法,它编写布隆过滤器的方式将会有所不同,比如一次性使用单个哈希函数来设定 K 个位点。

在单独完成编写适当且更通用哈希函数的任务方面,GPT4 表现出了能力。但在更大范围的布隆过滤器编写项目中,它没有展示出足够的逻辑推理能力。相反,它仅仅提供了两个不同但相似的哈希函数。这种做法,恰好符合人们对于布隆过滤器相关的一些低质量文章中的示例所展现出的水平。

大语言模型当前在推理方面的能力有限,加上针对此类主题的高质量资料相对稀缺,更不幸的是,存在大量低质量的信息源,这些因素共同导致了这种不尽如人意的结果。事实上,这并非孤例。在我尝试将大语言模型应用于算法或系统编程的极少数尝试中,结果普遍不尽人意。这种情况甚至发生在像 Python 编程这类看似大语言模型更为擅长的领域。

同时,令人惊讶的是,GPT-4 却能够在另一个会话中对其生成的函数进行反编译,并准确理解该函数的目的:

https://chat.openai.com/share/f54b64d7-d19a-4be4-8f10-08553aa812f1

因此,认为大语言模型在系统编程领域毫无用处是不准确的。

有趣的是,这些情形下,小型与大型模型之间的表现差异非常明显。我们不妨来观察两个不同的本地模型是如何应对的。

举个例子,Mistral 的多专家系统 (MoE) 模型 Mixtral 对这个问题给出了以下回答:

1
2
3
4
5
6
7
8
unsigned int hash(const char* str, unsigned int hash_id) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash + hash_id;
}

显然,只在最后添加 hash_id 是一个相当差劲的方法。尽管 Mixtral 在许多方面都表现出色,但对于大语言模型通常较弱的推理能力而言,目前的经验法则似乎是:模型越大,效果越好。

这是我与本地大型语言模型 deepseek-coder 的一次对话。由于我的 RAM 不足以支持更高精度的模型运行,因此只能将其量化到 4 位。尽管如此,作为一个拥有 340 亿参数的庞大模型,它在这个问题上的推理能力显得相当出色。以下是我使用 llama.cpp 进行的对话记录:

输入如下:

我设计了一个哈希函数,目的是为相同的数据生成 N 个不同的哈希值。但问题在于,当我调整 hash_id 的值时,产生的哈希值分布效果很不理想:

1
2
3
4
5
6
7
8
unsigned int hash(const char* str, unsigned int hash_id) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) + c;
}
return hash + hash_id;
}

可能是我目前采用的加盐(Salting)策略不太合适?我应该怎样调整这个策略呢?

输出如下:

你目前实现中的一个潜在问题是,你只是将 hash_id 直接加在哈希值的尾部。这种做法可能导致哈希值的分布不均匀,尤其是当 hash_id 值过大或者分布本身就不均匀时。

你可以考虑采用位运算来更好地融合 hash_id。这里有一个使用异或(XOR)运算的示例:

1
2
3
4
5
6
7
8
unsigned int hash(const char* str, unsigned int hash_id) {
unsigned int hash = 5381;
int c;
while ((c = *str++)) {
hash = ((hash << 5) + hash) ^ c + hash_id; // Using XOR to mix in the hash_id
}
return (hash ^ hash_id); // Mixing the hash_id one more time at the end
}

这种方法会比仅仅将 hash_id 直接加入哈希值中效果更好,因为它能更有效地将 hash_id 分散到整个哈希值中,从而实现更均匀的分布。[……实际的回复中还包含了更多详细信息]

对于在我的 MacBook M1 Max 上运行的模型来说,这是一个相当不错的成果。它成功地将求和操作和异或(XOR)运算结合使用。虽然模型在这个过程中确实受益于我提供的关于问题的线索,但重要的是,它能够自行识别问题的真正根源,并提出了一个有效的解决方案。像这样的内容不是简单通过阅读书籍、查阅文档或进行 Google 搜索就能找到的。无论你如何理解这个模型的工作方式,它在这个特定场景下展现了某种形式的推理能力,这里的推理是指识别问题的根本原因及其可能的解决方案。因此,断言大语言模型对程序员毫无帮助是非常轻率的。

然而,根据我过去几个月的经验,对于系统编程而言,如果你已经是一名经验丰富的程序员,大语言模型很少能提供令人满意的解决方案。来看一个实际例子。我目前的项目 ggufflib,涉及编写一个可以读写 GGUF 格式文件的库,这正是 llama.cpp 用来加载量化模型的格式。起初,为了弄清楚量化编码的运作方式(每个量化位由于速度原因以独特方式存储),我试图利用 ChatGPT 来帮助理解,但最终我选择了反向工程 llama.cpp 的代码,这样做更加迅速。理想情况下,如果一个大语言模型能够看到数据编码的“结构体”声明和解码函数,它应当能够重新构建数据格式的文档,以此来有效地协助系统程序员。尽管 llama.cpp 的功能足够简单,可以完全放入 GPT4 的上下文中,但输出结果却毫无用处。在这些情况下,我们还是得像以前那样,用纸笔来记录,阅读代码,并检查解码器提取的位点是如何被记录的。

为了更好地阐述上述用例,你如果愿意,可以试着自己来尝试一下。以下是从 llama.cpp 实现中取得的一个结构。

1
2
3
4
5
6
7
8
9
10
// 6-bit quantization
// weight is represented as x = a * q
// 16 blocks of 16 elements each
// Effectively 6.5625 bits per weight
typedef struct {
uint8_t ql[QK_K/2]; // quants, lower 4 bits
uint8_t qh[QK_K/4]; // quants, upper 2 bits
int8_t scales[QK_K/16]; // scales, quantized with 8 bits
ggml_fp16_t d; // super-block scale
} block_q6_K;

接下来是一个用来进行反量化处理的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void dequantize_row_q6_K(const block_q6_K * restrict x, float * restrict y, int k) {
assert(k % QK_K == 0);
const int nb = k / QK_K;

for (int i = 0; i < nb; i++) {

const float d = GGML_FP16_TO_FP32(x[i].d);

const uint8_t * restrict ql = x[i].ql;
const uint8_t * restrict qh = x[i].qh;
const int8_t * restrict sc = x[i].scales;
for (int n = 0; n < QK_K; n += 128) {
for (int l = 0; l < 32; ++l) {
int is = l/16;
const int8_t q1 = (int8_t)((ql[l + 0] & 0xF) | (((qh[l] >> 0) & 3) << 4)) - 32;
const int8_t q2 = (int8_t)((ql[l + 32] & 0xF) | (((qh[l] >> 2) & 3) << 4)) - 32;
const int8_t q3 = (int8_t)((ql[l + 0] >> 4) | (((qh[l] >> 4) & 3) << 4)) - 32;
const int8_t q4 = (int8_t)((ql[l + 32] >> 4) | (((qh[l] >> 6) & 3) << 4)) - 32;
y[l + 0] = d * sc[is + 0] * q1;
y[l + 32] = d * sc[is + 2] * q2;
y[l + 64] = d * sc[is + 4] * q3;
y[l + 96] = d * sc[is + 6] * q4;
}
y += 128;
ql += 64;
qh += 32;
sc += 8;
}
}
}

当我请求 GPT4 描述我们所用格式的大致结构时,它难以清楚地解释数据块是如何依据权重位置在“ql”的低/高 4 位被存储的。为了这篇博客文章,我还尝试让它编写一个简化的函数,用以展示数据的存储方法(可能它无法用文字解释,但可以通过代码来表达)。然而,生成的函数存在许多问题,例如索引错误,以及6位到8位的符号扩展处理不当(它仅仅是将其转换为 uint8_t 类型),等等。

顺便说一下,这是我最终自己编写的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
 } else if (tensor->type == GGUF_TYPE_Q6_K) {
uint8_t *block = (uint8_t*)tensor->weights_data;
uint64_t i = 0; // i-th weight to dequantize.
while(i < tensor->num_weights) {
float super_scale = from_half(*((uint16_t*)(block+128+64+16)));
uint8_t *L = block;
uint8_t *H = block+128;
int8_t *scales = (int8_t*)block+128+64;
for (int cluster = 0; cluster < 2; cluster++) {
for (uint64_t j = 0; j < 128; j++) {
f[i] = (super_scale * scales[j/16]) *
((int8_t)
((((L[j%64] >> (j/64*4)) & 0xF) |
(((H[j%32] >> (j/32*2)) & 3) << 4)))-32);
i++;
if (i == tensor->num_weights) return f;
}
L += 64;
H += 32;
scales += 8;
}
block += 128+64+16+2; // Go to the next block.
}
}

在上述函数中,我移除了代码的一个重要部分:详细注释,这些注释详细记录了 llama.cpp 中 Q6_K 编码的具体格式。如果 GPT 能够帮我完成这类注释工作,那将极大地提高效率。我相信这只是时间问题,可能几个月之内就能实现,因为这种任务在现有技术水平下,通过适当扩展模型能力,即可实现,无需任何技术突破。

放眼未来

不得不承认,但事实如此:当今大多数编程工作基本上是在不断复制相同的东西,只是形式略有变化。这并不需要太高层次的逻辑推理。大语言模型在这方面表现出了不错的能力,尽管它们的表现仍然受限于其处理上下文的最大大小。这确实应该引发程序员的深思。是否值得花时间编写这类程序?当然,这能带来可观的收入,但如果一个大语言模型能够完成其中的部分工作,那么从长远来看,五到十年后,这可能并不是最佳的职业发展方向。