LLMs and Programming in the first days of 2024

Salvatore Sanfilippo

2024 年初的 LLMs 與程式設計

原文由 Salvatore Sanfilippo 發布,訂閱此部落格

首先要說,這篇文章並不是要回顧 LLMs。2023 年對人工智慧而言無疑是特別的一年,再去強調這點似乎沒什麼意義。相反地,這篇文章想呈現的,是一位程式設計師的個人見證。自從 ChatGPT 問世,後來又開始使用能在本地端運行的 LLMs 以來,我大量使用了這項新技術。目的固然是為了加快寫程式的速度,但不只如此。我也不想把心力浪費在那些不值得費神的程式細節上。為了查找那些冷僻、又毫無智識趣味的細節而花上無數小時翻文件;為了搞懂一個過度複雜、往往又沒什麼道理的 API 而費盡心思;寫一些寫完馬上就能用、幾小時後就丟掉的程式。這些事我都不想再做了,尤其現在 Google 已經淪為一片垃圾訊息的汪洋,得在其中大海撈針才能找到一點有用的東西。

同時,我絕對不是程式新手。我完全有能力在沒有任何輔助的情況下寫程式,而且也經常這麼做。久而久之,我越來越常讓 LLMs 幫我寫高階程式碼,特別是用 Python,C 就少得多。讓我印象最深的是,在與 LLMs 相處的個人經驗中,我已經確切學會了什麼時候該用它、什麼時候用了反而會拖慢進度。我也體悟到,LLMs 有點像維基百科和散落在 YouTube 上的各種影片課程:它能幫助那些有意願、有能力、也有紀律的人,但對於已經落後的人,幫助就很有限。我擔心,至少在一開始,它只會讓原本就占優勢的人更得利。

不過,我們還是一步一步來談吧。

全知還是鸚鵡?

這波機器學習帶來的新奇與進展中,最令人擔憂的現象之一,就是 AI 專家難以承認自己所知有限。智人發明了神經網路,然後更關鍵地,發明了一套能自動優化神經網路參數的演算法。硬體已經能夠訓練越來越大的模型,再藉由對要處理資料的統計知識(也就是先驗知識),以及透過大量試誤、逐步逼近,找出比其他架構更有效的設計。但說到底,神經網路仍然相當不透明。

面對這種無法解釋 LLMs 某些浮現能力的困境,原本會期待科學家能更謹慎一些。結果剛好相反,許多人大大低估了 LLMs,說它們充其量不過是稍微進階一點的馬可夫鏈,最多只能把訓練資料裡看過的東西,吐出極其有限的變化。後來在證據面前,這種「鸚鵡」的說法幾乎被全面收回。

同時間,許多熱情的群眾又賦予了 LLMs 實際上並不存在的超自然能力。遺憾的是,LLMs 充其量只能在訓練時看過的資料所構成的空間裡做內插——光是這點就已經很了不起了。實際上,它們內插的能力是有限的(但依然驚人,甚至出乎意料)。唉,要是當今最大的 LLMs 能在它們看過的所有程式碼所圍起的空間裡連續地內插就好了!就算無法創造出真正的新東西,也足以取代 99% 的程式設計師了。現實則要平淡得多,一如往常。LLM 當然能夠寫出它未曾以完全相同形式看過的程式,展現出某種將訓練資料中以一定頻率出現的不同想法加以融合的能力。同時也很清楚,這種能力目前有著深刻的限制,只要需要細膩的推理,LLMs 就會徹底失敗。但它們仍代表了 AI 從誕生至今最偉大的成就,這一點似乎無可否認。

愚蠢卻無所不知

沒錯:LLMs 充其量只具備非常初階、常常不準確、又時不時摻雜著關於不存在事實的幻覺的推理能力。但它們擁有龐大的知識。在程式設計領域,以及其他擁有高品質資料的領域裡,LLMs 就像是一群什麼都懂一點的白癡天才。跟這樣的夥伴一起做成對程式設計(pair programming)會很痛苦(對我來說,成對程式設計本身就已經夠痛苦了):他們會有一堆荒謬的想法,我們得不斷抗爭才能貫徹自己的想法。但如果這個博學的傻瓜任我們差遣、有問必答,情況就不一樣了。現階段的 LLMs 還無法帶我們走上未知的知識之路,但如果我們想挑戰一個不太熟悉的主題,它們往往能把我們從一無所知的狀態,拉拔到足以靠自己繼續前進的程度。

在程式設計領域,或許直到二十或三十年前,它們的能力還不會有什麼吸引力。那時候你只需要懂一兩種程式語言、經典的演算法,再加上那十個核心函式庫。剩下的就得靠你自己,用你的智慧、經驗與設計能力去補足。如果你具備這些,你就是個能應付各種狀況的專家程式設計師。久而久之,我們見證了框架、程式語言、各種函式庫的大爆炸。很多時候這種複雜度的爆炸完全是不必要、也不合理的,但現實就是如此。而在這樣的情境下,一個什麼都懂的傻瓜就是珍貴的盟友。

舉個例子:我用 Keras 做機器學習實驗至少做了一年,後來因為種種原因改用了 PyTorch。我本來就知道什麼是 embedding、什麼是殘差網路,但我實在不想像當初學 Keras 那樣,一步一步去啃 PyTorch 的文件(那時 ChatGPT 還不存在)。有了 LLMs,要寫使用 Torch 的 Python 程式就變得非常容易。我只需要對自己想組裝的模型有清楚的想法,然後問對問題就行了。

來舉些例子

我說的不是那種簡單的問題,像是:「嘿,類別 X 裡做 Y 的方法是什麼?」如果只是為了這個,或許會讓人傾向同意那些對 LLMs 持懷疑態度的人。更強大的模型能做到的,要複雜得多。就在幾年前,那根本像是魔法。我可以對 GPT-4 說:你看,這是我用 PyTorch 實作的神經網路模型,這是我的 batches。我想重新調整張量的尺寸,讓產生 batches 的函式能跟神經網路的輸入相容,而且我想用這種特定的方式來呈現資料。你能告訴我重塑所需的程式碼嗎?GPT-4 寫出了程式碼,我只需要在 Python CLI 裡測試張量的維度是不是真的符合需求、資料排列是否正確。

再舉一個例子。之前我得為某些基於 ESP32 的裝置實作一個 BLE 客戶端。研究了一番後,我發現跨平台的藍牙程式設計綁定(bindings)幾乎都沒法用。解決方法很簡單,直接用 macOS 的原生 API 以 Objective-C 來寫。於是,我同時得處理兩個問題:學習 Objective-C 那套繁瑣、充滿我認為毫無道理的模式的 BLE API(我是個極簡主義者,那種 API 跟我心目中「好的設計」完全背道而馳),同時還得回想怎麼用 Objective-C 寫程式。我上一次寫 Objective-C 程式已經是十年前了:我早就忘了事件迴圈、記憶體管理等等細節。

最後的成果就是這段程式碼,稱不上漂亮,但該做的事都有做到。我在極短的時間內就寫完了。否則根本不可能。

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

這段程式碼多半是靠把我想做、但又不太知道怎麼做的東西直接剪貼去問 ChatGPT,本來跑不起來,再讓 LLM 解釋問題出在哪、該怎麼解。誠然,LLM 並沒有幫我寫出大部分的程式碼,但它確實大幅加快了撰寫速度。沒有 ChatGPT,我有辦法做到嗎?當然可以,但最有趣的重點不在於會花更多時間:事實是我根本就不會去嘗試,因為那不值得。這一點至關重要。寫這樣一個對我的專案來說只是次要的程式,其投入與回報的比例本來就不划算。除此之外,它還帶來一個比程式本身更有用的附帶收穫:為了這個專案,我修改了 linenoise(我寫的行編輯函式庫之一),讓它能在多工模式下運作。

另一個例子,這次比較不是寫程式,而是解讀資料。我想用一個在網路上找到的卷積神經網路來架一個 Python 腳本,但它的文件非常缺乏。這個網路的好處是它是 ONNX 格式,所以我可以輕易擷取輸入與輸出的清單及其命名。關於這個 convnet,我只知道一件事:它能偵測影像中的某些特徵。我不知道輸入影像的格式與尺寸,更麻煩的是,網路的輸出比我想像的複雜得多(我原以為它是二元分類器:看到的影像是正常還是有問題?應該只有兩個輸出,結果卻有數百個)。我一開始把 ONNX 網路的中繼資料輸出直接貼給 ChatGPT,跟它說明我對這個網路僅有的那點了解。ChatGPT 推測輸入可能是如何組織的,並認為輸出很可能是標示影像中對應潛在缺陷區域的正規化框,以及其他表示這些缺陷可能性的輸出。經過幾分鐘的來回討論,我就得到了一個能做網路推論的 Python 腳本,外加把原始影像轉換成適合輸入的張量所需的程式碼等等。那次對話讓我印象深刻的是,ChatGPT 最後是在觀察了測試影像上的原始輸出值(基本上就是 logits)後才真正「理解」網路是如何運作的:一連串浮點數提供了上下文,得以辨識確切的輸出細節、正規化方式、框是以中心點為基準還是以左上角為基準等等。

用完即丟的程式

我可以列舉數十個像上面那樣的案例,但沒什麼意義,因為故事大同小異。我有一個問題,需要快速搞懂某件事,而且*我有能力驗證* LLM 是不是在胡說八道。好,在這種情況下,我就用 LLM 來加速我獲取知識的過程。

不過,也有些情況是我讓 LLM 把全部程式碼都寫完。例如,每當我需要寫一個差不多用完即丟的程式時。就像這個:

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

我需要視覺化一個小型神經網路在訓練過程中的損失曲線。我把 PyTorch 程式在訓練時產生的 CSV 檔格式拿給 GPT-4 看,然後要求如果在命令列上指定多個 CSV 檔,我不要再看到同一個實驗的訓練與驗證損失曲線,而是要比較不同實驗的驗證損失曲線。上面就是 GPT-4 產生的結果,總共花了三十秒。

類似地,我需要一個程式來讀取 Airbnb 的 CSV 報表,並依月份與年份將我的房源分組。然後考量清潔費與每次訂房的天數,統計一年中不同月份的平均租金。這個程式對我來說非常實用。同時,寫它卻無聊透頂:一點有趣的地方都沒有。所以我截了一段 CSV 檔直接貼給 GPT-4,跟 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)

要準確理解如何分組那些原本相當零散、混亂的資料,需要一些簡單的推理:這不是 LLM 光靠複誦它看過的語言的近似值就能解決的問題。GPT-4 在訓練時當然看過類似的程式,但幾乎可以肯定沒看過必須以我要求的那種精確方式、針對那種特定格式的 CSV 檔做分組的程式。這(在我看來)顯示 LLMs 能在訓練集中不同程式所勾勒出的空間內,在某種程度上進行內插。

在我看來,寫這類程式是不明智的時間運用。LLM 能幫我代勞,確實以相當直接的方式加快了我產出程式碼的能力:我可以專注在重要的事情上。

事與願違之時:系統程式設計

儘管我在 LLMs 與程式設計上頗有斬獲,但當我用 C 寫程式時,幾乎總是只把 LLMs 當成一種更方便的文件形式來用。我是系統程式設計的專家,在那個領域裡,LLMs 只有在具備複雜推理能力時才對我有用:而在這種情境下,它們幾乎總是失敗。

我們先從這個實驗的提示開始:

請概述一個小巧但可運作的 C 語言 bloom filter 實作的需求,聚焦在雜湊函式的重要面向。然後用高品質的 C 把它寫出來。

另外請考量,你的範例實作應該要能儲存 10 萬個元素,偽陽性機率最多 5%。加入的元素是 null 結尾的字串。

以下是輸出:

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

GPT-4 產生的實作稱不上出色。Bloom filter 算是相當普及的資料結構,並不是什麼冷門的東西。然而很明顯,要寫出一個像樣的 bloom filter 需要更強的抽象能力:例如,如何找到一種有效的方法把同一個字串雜湊 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;
}

如果它一開始就自己想到這個點子,它就會用不同的方式來寫 bloom filter,用單一雜湊函式一次設定 K 個位元。

GPT-4 能夠孤立地完成撰寫一個合適且更通用的雜湊函式的任務。但在撰寫 bloom filter 這個更大的專案中,它卻未能展現足夠的推理能力,反而吐出了兩個不同但相似的雜湊函式:這正是你會在寫得很差的 bloom filter 範例文章中預期看到的東西。

大型語言模型目前薄弱的推理能力,加上關於這個主題的資料來源可能稀少,甚至更糟的是,可能有大量低品質的來源,共同導致了這樣微薄的成果。然而,這並非特例。我在演算法或系統程式設計領域嘗試使用 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 已經很薄弱的推理能力而言,到目前為止的規則無疑是:越大越好。

這是我與本地端 LLM deepseek-coder 的對話,它被量化到 4 位元,因為我的記憶體不足以用更高精度來跑模型。儘管如此,它有 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;
}

也許我用的加鹽策略很差?我該怎麼改?

[DeepSeek Coder 回覆]

你的實作可能有一個問題,就是你只是在雜湊值的最後加上 `hash_id`。這可能會導致分布不佳,尤其是當 `hash_id` 太大或分布不均時。

可以考慮使用位元運算來混合 `hash_id`。以下是使用 XOR 的範例:

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` 分散到各個雜湊值中,單純相加可能會導致分布不夠均勻。[節錄……實際回覆中包含了更多有用的資訊]

對於一個跑在我 MacBook M1 Max 上的模型來說,表現不算差。它還把加法跟 XOR 混用了。在這個案例中,模型固然受惠於我提供了關於待解問題的線索,但確實是模型自己指出了真正造成困擾的根源,並且提出了有效的替代方案。像上面這樣的東西,不是任何書籍、文件或 Google 搜尋就能得到的。不管你把它看作多麼原始、內插的結果,或隨你怎麼看待,模型都展現了某種形式的推理——如果在此特定案例中,我們把「推理」接受為找出問題根源並提出可能解方的能力的話。不管人們怎麼看待 LLMs,說它們對程式設計師沒有幫助,是極為草率的。

不過同時,過去幾個月來的經驗也顯示,對於系統程式設計,如果你已經是經驗豐富的程式設計師,LLMs 幾乎從來不會提供可接受的解法。讓我再舉一個實際的例子。我目前的專案 ggufflib,是要寫一個讀寫 GGUF 格式檔案的函式庫,也就是 llama.cpp 載入量化模型所用的格式。一開始,為了搞懂量化編碼是如何運作的(為了速度,每個量化的位元是以很花俏的方式儲存的),我試著用 ChatGPT,但後來還是決定直接逆向工程 llama.cpp 的程式碼:那樣快得多。一個能 decent 地協助系統程式設計師的 LLM,如果看到資料編碼的「struct」宣告和解碼函式,應該要有能力重建出資料格式的文件。llama.cpp 的函式小到足以完全放進 GPT-4 的上下文視窗中,但輸出卻完全沒用。在這種情況下,就得像過去一樣土法煉鋼:紙和筆,讀程式碼,看解碼器擷取的位元都登記到哪裡去。

讓我把上面的使用情境解釋得更清楚一點,好讓你如果想自己試試看也能試。我們有這個來自 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;
        }
    }
}

如果我請 GPT-4 概述所使用的格式,它很難清楚解釋區塊是如何根據權重位置,儲存在「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 打交道夠久的人,在接受其限制的同時,也能確定事情並非如此:它們融合過去所見事物的能力,遠遠超出隨機吐出詞彙的程度。儘管它們的訓練大多是在預訓練階段、透過預測下一個 token 來完成,這個目標卻迫使模型建立某種形式的抽象模型。這個模型很薄弱、零碎、也不完美,但如果我們觀察到那些現象,它就必定存在。如果我們的數學確定性都令人懷疑、而最頂尖的專家們又常常立場對立,那麼相信自己親眼所見,似乎是明智的做法。

最後,現在不用 LLMs 來寫程式,到底有什麼道理?向 LLMs 提出正確的問題,是一項基本技能。越少練習,就越無法靠 AI 來提升自己的工作。況且,培養描述問題的能力,在跟其他人溝通時也很有用。不只有 LLMs 有時聽不懂我們想說什麼。表達不清是個很大的限制,而許多程式設計師儘管在自己的專業領域很強,表達能力卻很差。更何況現在 Google 已經變得不好用了:把 LLMs 當作一種壓縮過的文件形式來用,也是個好主意。就我而言,我會繼續大量使用它們。我從來就不喜歡去學習某個冷僻通訊協定的細節,或是某個想炫耀自己多厲害的人所寫的函式庫裡那些迂迴的方法。在我看來,那都是「垃圾知識」。LLMs 正一天比一天更幫我省去這些麻煩。

本文章由 muse-spark-1.2-contributor 進行翻譯

留言