redis作者博文,因为网上看到了很多译文我都觉得差强人意,所以我决定自己来翻译一下,原文:LLMs and Programming in the first days of 2024,采用ChatGPT翻译我进行校对和意译的方式。

首先我得说,这篇文章不是对LLMs(大语言模型)进行复盘,很明显再怎么强调2023年对人工智能很特别已经没多大意义。实际上,这篇文章更像是分享我作为程序员的亲身体验。从ChatGPT面世,到后来在本地运行的LLMs,我深入的使用了这项新技术。最初的目标是加快我的编码能力,但这不是唯一的目的。我还想避免浪费精力和时间在编程中那些不值得操心的地方。比如:花无数小时在寻找奇怪又让人提不起兴趣的文档上;努力学习一个过于复杂得没有必要的API;编写一些用一会就会扔的程序。这些都是我不想做的事情,尤其是现在,谷歌已经成了一个充满垃圾信息的海洋,只能在其中寻找少数有用的东西。

不过,我绝对不是编程的新手。我能够在没有任何帮助的情况下写代码,而且也经常这样做。不过随着时间的推移,我开始越来越多地使用LLMs来编写高级代码,特别是在Python中,而在C语言中则较少。个人的经验告诉我,我已经学会了何时使用它们,以及它们什么时候会拖慢我的步伐。我还搞明白了,LLMs有点像 百度百科 维基百科和 Bilibili YouTube上散布的视频课程:它对那些有意愿、有能力和自律的人有帮助,但对那些已经掉队的人来说帮助有限。我担心至少在最初阶段,它们只会使那些已经具有优势的人受益。

所以让我们一步步来看吧。

全知全能还是鹦鹉学舌?

在这个崭新的机器学习浪潮中,最令人担心的一点是,很多人工智能专家难以承认他们知识的局限性。人类发明了神经网络,更重要的是,发明了自动优化这些网络参数的算法。我们的硬件已经可以训练越来越大的模型了,并且通过统计数据(先验知识)和大量的尝试和错误,找到了比其他模型更好的架构。但总的来说,神经网络还是很难搞懂的。

面对LLMs的一些新功能,我们理解不了,本该更谨慎。但许多人严重低估了LLMs,说它们最多就是个稍微高级点的马尔科夫链,能做的也就是重复训练集里的内容。然后,一旦有了证据,这种鹦鹉学舌的观点就被普遍撤回了。

与此同时,有些人过分夸大了LLMs的能力,归功于它们有超自然的能力,但实际上并不存在。不幸的是,LLMs最多只能在它们训练过程中接触到的数据表示的空间里进行填充:虽然这已经很厉害了。实际上,它们的填充能力是有限的(但还是很惊人的)。如果今天最大的LLMs能在它们所接触到的所有代码空间里进行平滑填充,即使它们不能创造真正的新东西,也足以取代99%的程序员了。但实际情况要谨慎得多。LLMs确实可以编写它们之前没以精确形式见过的程序,显示出训练集中以某种频率出现的不同思想的一定能力。但很明显,这种能力现在还有很大的限制,每当需要微妙的推理时,LLMs都会遭遇灾难性的失败。不过,它们代表了人工智能从诞生到今天的最伟大成就。这点似乎是毋庸置疑的。

愚蠢但无所不知

确实,LLMs最多只能进行基础的推理,而且往往不太准确,常常会有些关于不存在事实的幻觉。但它们拥有大量的知识。在编程领域,以及其他有高质量数据的领域,LLMs就像是知道很多事情的愚蠢的学者。和这样的伙伴一起进行结对编程简直是噩梦(对我来说,结对编程本身在任何情况下都很糟糕):他们可能会有一些荒谬的想法,我们得不断地努力推动自己的思路。但如果这个博学的傻瓜能够顺从我们的指导,回答我们提出的问题,情况就会不同。现在的LLMs并不能带我们超越已知的路径,但如果我们想解决一个我们不太熟悉的问题,它们通常能够帮助我们从一无所知到拥有足够的知识来自己解决问题。

在编程这一领域,也许二三十年前,LLMs的能力几乎不会引起任何兴趣。那时候,你只需要懂几种编程语言、掌握经典算法,还有那十个基本的库。其他的东西,你需要依靠自己的智慧、专业知识和设计技能来完成。如果你具备这些条件,你就是个能胜任几乎所有任务的专业程序员。随着时间的推移,我们目睹了框架、编程语言以及各种库的蓬勃发展。这种复杂性的激增通常是不必要的,也不合理,但事实就是如此。在这种情况下,一个知晓一切的傻瓜成为了宝贵的合作伙伴。

举个例子:我进行了至少一年的机器学习实验,使用的是Keras。后来由于各种原因,我转向了PyTorch。我已经了解了嵌入层和残差网络的概念,但我不想像之前学习Keras那样(当时还没有ChatGPT),一步步研究PyTorch的文档。有了LLMs,用Torch写Python代码变得非常简单。我只需要对我想构建的模型有一个清晰的理念,然后提出正确的问题。

实际操作时间

在实际操作中(我说的不是那些简单的事情),比如:“嘿,X类的Y方法是什么?”如果只是这样的话,人们可能会更倾向于怀疑LLMs的价值。LLMs可以做的事情要复杂得多。直到几年前,这还像是纯粹的魔法。我可以告诉GPT4:“看,这是我在PyTorch中实现的神经网络模型。这些是我的批次数据。我想调整张量的大小,以使它们与神经网络的输入兼容,还要以特定的方式表示数据。你能为我生成相应的代码吗?”GPT4会生成代码,我所要做的就是在Python命令行界面测试一下张量是否真的具有对我有用的尺寸,以及数据布局是否正确。

另一个例子。不久前,我需要为某些基于ESP32的设备实现一个BLE客户端。经过一番研究,我意识到多平台的蓝牙编程绑定或多或少都不可用。解决方案很简单,就是使用macOS的原生API,用Objective C编写代码。所以,我发现自己同时面临着两个问题:一方面是学习Objective C繁琐的BLE API,充满了我认为毫无意义的代码模式(我是个极简主义者,那种API正好与我认为的“好设计”背道而驰),另一方面是要记住如何用Objective C编程。我上次用Objective C编写程序已经是十年前的事了:我不记得事件循环、内存管理等细节了。

最终,我得到了这段代码,虽然它不太优雅,但它完成了我需要的任务。我在极短的时间内编写了它。没有LLMs,这几乎是不可能的。

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

这段代码主要是通过在ChatGPT上剪切和粘贴我想做的但不太了解如何实现的内容来编写的,所以它并没有完全正确运行。但LLMs告诉我了问题所在以及如何解决。当然,LLMs并没有完全编写整段代码,但它显著加速了编写过程。没有ChatGPT我能做到吗?当然可以,但最有趣的是,它会花费我更多的时间,而且我甚至可能不会尝试,因为那不值得。这一点至关重要。对于我的项目来说,编写这种程序的工作量与回报之比是不划算的。而且,这项工作还带来了更有用的次要效应:在那个项目中,我修改了linenoise(我的一个用于行编辑的库),使其能在多路复用中工作。

还有另一个例子,这次涉及更多的是数据解释而不是代码编写。我想使用我在网上找到的一个卷积神经网络来设置一个Python脚本,但它的文档相当缺乏。这个网络的一个优点是它采用了ONNX格式,因此我可以轻松地提取输入和输出列表以及它们的名称。我只知道这个卷积网络有一点:它可以在图像中检测某些特征。我不知道输入图像的格式和大小,尤其是网络的输出比我想象的要复杂得多(我以为它是一个二元分类器:观察到的图像是正常,或者有问题。本应该只有两种输出,但实际上有数百个)。我开始将ONNX网络的元数据输出复制粘贴到ChatGPT中,并向助手解释了我对网络的一些了解。ChatGPT推测输入是如何组织的,输出可能是标准化的框,指示图像中与潜在缺陷相对应的部分,以及其他指示这些缺陷可能性的输出。经过几分钟的来回对话,我得到了一个能够进行网络推理的Python脚本,以及将起始图像转换为适合输入的张量所需的代码等等。让我印象深刻的是,一旦ChatGPT在测试图像上观察到了原始输出值(基本上是logits),它最终“理解”了网络的功能:一系列浮点数提供了上下文来确定确切的输出细节,标准化,框是否居中,或者是否指定了左上角等等。

一次性程序

有很多类似我刚刚提到的例子,虽然细节有所不同,但故事基本一样。当我遇到问题需要迅速获取验证性信息时,我会求助于LLMs,以确保它们提供的信息是可靠的。在这些情况下,我使用LLMs来快速填补我的知识缺口。

然而,在其他情况下,我会让LLM编写所有代码。例如,当我需要创建一个几乎是一次性的程序时,就像这个:

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

我需要可视化一个小型神经网络学习过程中的损失曲线。我向GPT4展示了PyTorch程序在学习过程中产生的CSV文件格式,然后询问如果我提供多个CSV文件的路径,我想比较它们的验证损失曲线,而不是同一实验的训练和验证损失曲线。GPT4生成了上述结果,整个过程只花了三十秒。

同样,我需要一个程序来读取AirBnB的CSV报告,并按月和年对我的公寓进行分组。然后,根据清洁费和每次预订的晚数,它会计算不同月份的平均租金价格。这个程序对我来说非常有用,但编写它又极其无聊。所以,我只复制粘贴了CSV文件的一部分到GPT4中。我告诉LLM要解决的问题是什么:程序第一次尝试就成功了。下面是完整的代码:

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)

要理解如何对这些混乱的数据进行分组,需要一些简单的推理能力,这不是LLMs能通过简单地复述已知语言来解决的问题。虽然GPT4在训练中可能遇到过类似的问题,但几乎可以肯定它没有见过要按照我指定的精确方式在特定格式的CSV文件中进行分组的程序。这展示了在我看来,LLMs可以在不同程序之间进行一定程度的填充,处于训练集所绘制的程序空间内。

对我来说,编写这些程序是不明智的时间投资。LLMs可以以相当直接的方式为我生成代码,无疑提高了我的工作效率,让我可以更专注于更重要的任务。

事与愿违:系统编程

虽然我在LLMs和编程方面都有一些成功经验,但每当我写C程序的时候,我基本上都只是把LLMs当作更方便的文档工具。我是系统编程方面的专家,在这个领域除非LLMs能够进行复杂的推理,它们对我才有用。但很遗憾,通常情况下它们都不能满足我的需求。

好了,现在让我们来看一下这个实验的要求吧:

首先,你需要概述一个高效的C语言布隆过滤器的实现要求,要求这个过滤器是小巧但功能强大的。特别关注哈希函数的设计和性能。然后,你需要以高质量的C代码来实现这个过滤器。

另外,需要考虑到你的示例实现要能够存储100,000个元素,并且假阳性概率最多不超过5%。这些元素都是以null结尾的字符串。

以下是LLMs的输出:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#define N 100000  // 元素数量
#define P 0.05    // 误报概率
#define M (int)(-(N * log(P)) / (log(2) * log(2)))  // 位数组大小
#define K (int)(M / N * log(2))  // 哈希函数数量
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));
}
// 基本哈希函数(仅用于演示)
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));
    // 如果需要,添加更多的哈希函数
}
int is_in(char *str) {
    return getBit(hash1(str)) && getBit(hash2(str));
    // 如果使用更多的哈希函数,请检查它们
}
int main() {
    char *testStr = "testString";
    add(testStr);
    if (is_in(testStr)) {
        printf("%s 可能在集合中。\n", testStr);
    } else {
        printf("%s 肯定不在集合中。\n", testStr);
    }
    return 0;
}

GPT-4生成的实现并不完美。布隆过滤器在数据结构中相当常见,不算什么特别的东西。不过,毫无疑问,要编写一个像样的布隆过滤器需要更高级的思维能力。比如,要找到一种有效地对同一个字符串进行N次哈希处理的方法,同时要确保这些哈希值之间没有关联。

但是,如果你明确要求GPT-4来修改哈希函数,以产生N个没有关联的输出,那么它提出的解决方案可能更加可行:

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;
}

如果GPT-4能够自己想到这个主意,那它就会以一种不同的方式来编写布隆过滤器,一次性设置K个比特,只用一个哈希函数。

GPT-4能够独立完成编写合适且更通用的哈希函数的任务。但在编写布隆过滤器这个更大的项目中,它未能展现出足够的逻辑推理能力,而是生成了两个不同但相似的哈希函数,这与人们对于布隆过滤器的预期有些不符,但这在关于布隆过滤器的糟糕文章示例中并不罕见。

目前大型语言模型在推理方面仍有待加强,再加上关于这个主题的资源可能相对有限,甚至可能存在很多低质量的资源,这都导致了成果相对有限。但这并不是个例。我在尝试使用LLMs进行算法或系统编程时,通常结果都不太理想,即使期望LLMs在像Python编程这样的上下文中拥有的能力也并不一定能够完全体现出来。

同时,GPT-4能够解析它生成的函数(在另一个会话中),也能精确理解其目的:

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

因此,声称在系统编程上下文中没有用处是肯定不准确的。

有趣的是,这些情况下,小型大语言模型和大型大语言模型之间的差异确实显著。让我们来看看两个本地模型的表现。

例如,Mistral的MoE Mixtral,对同一个问题的回应如下:

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 在许多情况下都表现出色,但对于本来就不太强的LLMs来说,规模似乎是关键,越大越好。

我与本地的 LL 模型 deepseek-coder 进行了对话,它的规模仅有 4 比特,因为我的 RAM 不够用来更高精度的模型。但是,作为一个拥有 340 亿参数的大型模型,它在这个问题上的推理能力似乎相当先进。以下是聊天记录(使用 llama.cpp 运行):

我有一个哈希函数,本应该为相同的数据输出 N 个不同的哈希值。然而,当我改变 hash_id 的值时,我发现生成的哈希值分布非常不均匀:

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 添加到哈希值的末尾。这可能会导致分布不均匀,特别是如果 hash_id 太大或者分布不均匀的话。

考虑使用位操作来混合 hash_id。以下是一个使用异或运算的示例:

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 分散到你的哈希值中,而不像简单地添加它那样可能导致分布不均匀。【省略…实际回答包含了更多信息】。

Deep-seek coder reply

作为运行在我的MacBook M1 Max上的模型来说,效果还不错。这个模型在处理求和和XOR混合的问题上表现出色。在这种情况下,模型肯定是受到了我提供的问题线索的帮助,但它也能够嗅出实际问题并提供一些有用的替代方案。像上面的这种情况,书籍、文档或谷歌搜索都无法提供帮助。无论你怎么看待LLMs,不管是它是基于填充的原始输出,还是以其他方式呈现,这个模型都已经做了某种程度的推理工作。如果我们承认在这种情况下它能够找到问题的根源并提供潜在解决方案,那么说LLMs对程序员毫无用处,那未免太武断了。

然而,最近几个月的经验告诉我,在系统编程方面,如果你已经是一位有经验的程序员,那么大型语言模型(LLMs)几乎不会提供可行的解决方案。让我给你分享一个真实的例子。我正在进行的项目叫做ggufflib,涉及编写一个读写GGUF格式文件的库,这是llama.cpp加载量化模型时使用的格式。一开始,我尝试使用ChatGPT来理解量化编码的工作原理(因为每个量化位都以独特的方式存储,以提高速度)。但后来我决定采用逆向工程的方式来研究llama.cpp的代码,因为这种方法更加高效。如果LLM能够像识别数据编码的“struct”声明和解码函数一样,帮助系统程序员重新构建数据格式文档,那将非常有用。llama.cpp中的函数非常小,可以完全包含在GPT-4的上下文中,但LLM的输出毫无帮助。在这些情况下,情况回到了传统的方式:拿出纸笔,仔细阅读代码,然后查看解码器提取的位是如何映射的。

如果你有兴趣的话,可以看看我更详细地解释上述情况,你能容易地理解并尝试它。我们有一个数据结构,是从llama.cpp中实现的:

// 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;

并且有一个用于执行反量化的函数:

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),等等。

可以顺便看看,这是最终我自己编写的代码:

    } 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可以帮我来做这个任务,那真的会非常有帮助,我敢说这只是时间的问题,因为这种任务是可行的,无需任何重大突破,只需要一些改进。

正确认识问题

真的很抱歉,但实际情况如下:现在的大部分编程工作都是在稍微不同的情境下反复执行相同的任务。这些任务通常不需要太多的推理能力。虽然LLMs在这方面表现得相当出色,但它们仍然受到它们上下文的最大尺寸的限制。这应该让程序员们深思,编写这种类型的程序是否值得呢?当然,你会得到相当丰厚的报酬,但也许在未来五到十年内,LLM可以完成其中的一部分工作,这是否仍然是最佳选择呢?(译注:我看到作者的评论区对他的这一观点展开了激烈的讨论,讨论核心是作者在劝说程序员改行,我认为作者只是在让程序员们停止进行没有意义的工作而去寻找更有意义的,大家也可以发表自己的看法)

此外,LLMs是否真的具备某种推理能力,还是一切都只是虚张声势呢?也许有时候,它们似乎只是因为符号学家会谈到的”能指”而给人一种实际上并不存在的深层含义。那些与LLMs密切合作的人,尽管了解它们的局限性,但他们确信LLMs不可能仅仅是虚张声势:它们在模仿以前见过的内容方面表现出了惊人的能力,远超过了简单的随机单词重复。尽管它们在大部分时间里是通过预测下一个标记来进行训练的,但这个目标迫使模型构建某种抽象模型。虽然这个模型可能不够强大、有缺陷且不完美,但如果我们观察到的世界就是这样,那么这个模型必然存在。当我们的数学定理不够可靠,即使是最顶尖的专家们也会持不同的观点时,相信我们亲眼所见似乎是一个明智的选择。

最后,如今在编程中不使用LLMs还有意义吗?提出正确问题给LLMs是一项基本技能,这项技能练习得越少,通过人工智能提高工作质量就越困难。此外,提高对问题的清晰描述能力也对与他人沟通非常有用。有时,不仅仅是LLMs,其他人也难以理解我们要表达的意思。沟通困难是一个很大的限制,尽管许多程序员在自己的领域非常擅长,但他们的沟通技巧不足。甚至将LLMs仅用于文档的压缩形式也是个不错的主意,现在连谷歌都在这方面遇到了困难。对我来说,我将继续广泛使用它们。我从来没有喜欢过学习晦涩的通信协议细节或编写复杂库的方式,那只是浪费时间。对我来说,这些都是”垃圾知识”。LLMs越来越多地帮助我摆脱这些烦恼。


0 条评论

发表回复

Avatar placeholder

您的电子邮箱地址不会被公开。 必填项已用 * 标注