Redis 之父的 LLM 编程之旅 !

Antirez,原名Salvatore Sanfilippo,是Redis数据库的创始人,他通过创建高性能的键值存储系统,实现了数据库领域的重要突破,同时接受开发者的贡献和回馈,促进了Redis社区的形成和发展,推动了开源文化的发展,对整个行业产生了积极影响。2020年辞去Redis项目开发和维护者的职务,担任 Redis Labs 的顾问。

a304d22def764e525bea2a15b86731e9.png

Redis数据库的创始人Antirez

对于 Antirez 来说,编程是一种表达自我的方式,一种艺术形式。每个字符和换行符都必须精心制作,类似于写作的艺术形式,它必须美观、优雅且易于理解。


这篇文章聚焦antirez在人工智能编程中利用LLMs的经验。目的不仅在于加速编码,更在于避免在不值得努力的方面浪费时间。作者回顾了使用ChatGPT和本地操作的LLMs,强调对于有意愿有能力的人而言,LLMs是有帮助的。然而,作者也担忧其可能仅服务于那些已经具有优势的人。文章突出2023年在人工智能领域的特殊性,寻找有用内容变得更加困难的现状。

无所不知or学舌鹦鹉  

在机器学习的这股新奇和进步浪潮中,最令人担忧的现象之一是人工智能专家接受其有限知识的能力受到限制。智人发明了神经网络,更为关键的是,发明了一种自动优化神经网络参数的算法。硬件已经能够训练越来越大的模型,并且通过对待处理数据的统计知识(先验)以及通过大量的试错来进行连续逼近,已经发现了比其他模型更有效的架构。但总的来说,神经网络仍然相当难以理解。

面对解释LLMs的某些新兴能力的无能为力,人们本可以期望科学家更加谨慎。相反,许多人深刻低估了LLMs,说这些模型不过是略微先进的马尔可夫链,最多只能复述它们在训练集中看到的极其有限的变化。然后,在面对证据时,这种关于学舌鹦鹉的观念几乎被普遍撤回。

与此同时,许多热情洋溢的人群将超自然的力量归功于LLMs,而这在现实中是不存在的。不幸的是,LLMs最多只能在它们在训练中看到的数据所代表的空间中进行插值:这已经很多了。实际上,它们的插值能力是有限的(但仍然令人惊讶,而且也是意外的)。哦,如果今天最大的LLMs能够在它们见过的所有代码所界定的空间中连续插值!即使它们不能产生真正的新颖性,它们也能替代99%的程序员。然而,现实更为谦逊,几乎总是如此。LLM确实能够编写在精确形式上未曾见过的程序,显示出一定的能力,将训练集中以某种频率出现的不同思想融合在一起。同时,很明显,这种能力目前存在深刻的限制,每当需要微妙的推理时,LLMs会惨败。然而,它们代表着人工智能从诞生到今天的最大成就。这似乎是不可否认的。

愚蠢却无所不知

LLMs最多只能进行基本的推理,通常是不准确的,经常夹杂着关于不存在事实的幻觉。但它们拥有广泛的知识。在编程领域,以及其他有高质量数据的领域,LLMs就像是愚蠢的天才,知道很多事情。与这样的伙伴进行配对编程可能会很糟糕(对我来说,即使在最一般的情况下,配对编程也是可怕的):他们可能会有荒谬的想法,我们不得不不断努力去坚持我们自己的想法。但是如果这个博学的傻瓜在我们的处置,并回答我们提出的所有问题,情况就不同了。当前的LLMs不会带领我们超越知识的范畴,但如果我们想处理一个我们不太了解的主题,它们通常能够将我们从绝对无知的状态提升到足以让我们自行前进的程度。

在编程领域,也许它们的能力在二三十年前会显得无足轻重。那时,你只需要了解一些编程语言,经典算法,以及那十个基本的库。其余的你需要自己添加,包括自己的智慧,专业技能和设计能力。如果你拥有这些要素,你就是一位专业的程序员,能够做更多或更少的一切。随着时间的推移,我们目睹了框架、编程语言、各种库的爆炸性增长。这种复杂性的爆炸通常是完全不必要和不合理的,但事实就是事实。在这样的背景下,一个无知却无所不知的人是一个宝贵的盟友。

让我给你一个例子:我在机器学习方面的实验至少进行了一年,一直在使用Keras。然后由于种种原因,我转向了PyTorch。我已经知道嵌入或残差网络是什么,但我不想逐步学习PyTorch的文档(就像ChatGPT还不存在时我用Keras时那样)。有了LLMs,编写使用Torch的Python代码非常容易。我只需要对我想要构建的模型有清晰的想法,并提出正确的问题。

390c51828b9c38efd7195d569e138a8a.jpeg

举几个例子说明

我并不是在讨论像:“嘿,类X的做Y的方法是什么”这样的简单问题。如果只是为了这个,人们可能会倾向于同意对于LLMs持怀疑态度的人。更复杂的模型所能做的要复杂得多。直到几年前,这几乎是纯粹的魔法。我可以告诉GPT4:看,这是我在PyTorch中实现的神经网络模型。这些是我的批次数据。我想要调整张量的大小,使得生成批次的函数与神经网络的输入兼容,并且我想以这种特定的方式表示事物。你能给我展示进行重塑所需的代码吗?GPT4编写了代码,而我所要做的就是在Python CLI中测试张量是否真的具有我需要的维度,以及数据布局是否正确。

这里有另一个例子。一段时间以前,我不得不为某些基于ESP32的设备实现一个BLE客户端。经过一番研究,我意识到多平台蓝牙编程绑定基本上都无法使用。解决方案很简单,使用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格式,因此我可以轻松提取输入和输出的列表,以及它们的分配名称。我只知道关于这个卷积神经网络的一件事:它可以检测图像中的某些特征。我不知道输入图像的格式和大小,尤其是网络的输出比我想象的复杂得多(我以为它是一个二元分类器:观察的图像是否正常或是否有问题?两个输出,但有数百个)。我开始将ONNX网络元数据的输出剪切粘贴到ChatGPT中。我向助手解释我对网络了解的很少。ChatGPT猜测输入是如何组织的,以及输出可能是指示与潜在缺陷对应的图像部分的规范化框,以及其他指示这些缺陷的可能性的输出。在经过一些来回的交流几分钟后,我得到了一个能够进行网络推理的Python脚本,以及将起始图像转换为适用于输入的张量等所需的代码。令我惊讶的是,ChatGPT最终在观察了测试图像上的原始输出值(基本上是对数)后“理解”了网络的功能:一系列浮点数为识别确切的输出细节提供了上下文,包括归一化,框是否居中,或者左上角是否被指定,等等。

c4e499e008f3ff480ae439bb3ddb2dd4.jpeg

一次性程序

我可以记录数十个我上面叙述的类似情况。这没有意义,因为故事都是以更多或更少相同的方式重复的。我有一个问题,我需要迅速了解一些东西,我可以验证LLM是否在给我提供无意义的信息。在这种情况下,我使用LLM加快对知识的需求。

然而,还有不同的情况,我让LLM编写所有的代码。例如,每当我需要编写一个更多或更少是一次性程序时。就像这个:

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

我需要在小型神经网络学习过程中可视化损失曲线。我向GPT4展示了PyTorch程序在学习过程中生成的CSV文件的格式,然后我要求,如果我在命令行上指定了多个CSV文件,我不再想要相同实验的训练和验证损失曲线,而是想要比较不同实验的验证损失曲线。以上是GPT4生成的结果。总共30秒。

同样,我需要一个程序,读取AirBnB的CSV报告,并按月份和年份对我的公寓进行分组。然后,考虑清洁费用和每次预订的夜晚数量,它将对一年中不同月份的平均租金价格进行统计。这个程序对我来说非常有用。与此同时,编写它非常无聊:没有什么有趣的。所以我拿了CSV文件的一部分好玩地粘贴在GPT4上。我向LLM解释了问题所在:程序在第一次尝试中就成功了。我在下面完整展示给你。

python

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在训练集中各种程序轮廓勾勒出的空间内可能会有一定程度的插值能力。

在我的看法中,编写这类型的程序是不明智的时间使用方式。LLM能够替我完成这项工作肯定能够直接加速我编写代码的能力:我可以专注于重要的事物。

当事情不如预期进行时:系统编程

尽管我在使用大型语言模型(LLMs)和编程方面取得了成功,但当我用C语言编写程序时,我几乎总是将LLMs仅用作更方便的文档形式。我是系统编程的专家,在这个背景下,LLMs只有在它们能够进行复杂推理时对我有用:在这种情况下,它们几乎总是失败的。

让我们从这个实验的提示开始:

概述一个良好、小巧但有效的C语言布隆过滤器实现的要求。重点放在哈希函数的重要方面。然后用高质量的C语言编写它。

还要考虑到,您的示例实现应该大小适中,能够存储100,000个元素,误判率最多为5%。添加的元素是以空字符结尾的字符串。

以下是输出:

```c
#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生成的实现并不是特别出色。布隆过滤器是相当常见的数据结构,不属于太过奇异的范畴。然而,编写一个体面的布隆过滤器需要更强的抽象能力:例如,存在一个问题,即找到一种有效的方法对相同的字符串进行N次哈希,并确保这些哈希值足够不相关。

然而,如果你明确要求GPT-4修改哈希函数以产生N个不相关的输出,它提出的解决方案就显得更为可接受:

```c

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

因此,在系统编程环境中宣称LLMs没有用途是明显不准确的。

有趣而且也是可以预期的是,在这些情况下,较小模型和较大模型之间的差异确实是显著的。让我们看看两个本地模型的表现。

例如,Mistral的混合专家(MoE)Mixtral对相同问题的回答如下:

```c

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对于许多用途来说是一种优秀的模型,但对于已经存在的LLM弱推理能力而言,迄今为止的经验规则无疑是:规模越大越好。

以下是与本地LLM 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;

}

也许我使用的salting strategy不太好?我该如何更改它?

[深度搜索编码师回复]

您的实现可能存在一个问题,即您只是将 hash_id 简单地添加到哈希的末尾。这可能导致分布不均匀,特别是如果 hash_id 太大或不均匀分布。

考虑使用按位操作来混合 hash_id。以下是使用异或(XOR)的示例:

```c

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 上运行的模型来说,效果还不错。在这种情况下,模型显然受益于我提供了解决问题线索的事实,但确切的问题源头是模型确定的,提出的替代方案是有效的。像上述情况一样,这样的结果不在任何书籍、文档或谷歌搜索的范围之内。无论是插值的结果,还是你想以何种方式理解,模型在某种程度上进行了推理,如果在这个具体案例中,我们接受问题起源的识别和其潜在解决方案作为推理的一种形式的话。不管你怎么看待LLM(Large Language Models),说它们对程序员没有帮助是极为草率的。

然而,与此同时,我在过去几个月的经验表明,对于已经是经验丰富的程序员,LLM在系统编程方面几乎从不提供可接受的解决方案。让我给你展示另一个真实的例子。我的当前项目,ggufflib,涉及编写一个库,用于读取和写入GGUF格式文件,这是llama.cpp加载量化模型的格式。最初,为了理解量化编码的工作原理(出于速度原因,每个量化的位都以独特的方式存储),我尝试使用ChatGPT,但后来我决定对llama.cpp的代码进行逆向工程:速度更快。一个能够像样地协助系统程序员的LLM,如果看到数据编码的“struct”声明和解码函数,应该能够重建数据格式文档。llama.cpp的函数足够小,完全适应GPT4的上下文,但输出完全无用。在这些情况下,一切都像过去一样:纸和笔,阅读代码,并查看解码器提取位的位置。

让我更好地解释上述用例,以便你如果愿意的话,可以自己尝试。我们有来自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位/高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共事足够长时间的人,虽然接受它们的局限,却确信事实不可能是这样:它们将所见过的东西融合在一起的能力远远超出了随机重复单词的范围。尽管它们的训练主要是在预训练期间完成的,以预测下一个标记,但这个目标迫使模型创建某种形式的抽象模型。这个模型是薄弱的、片面的和不完美的,但如果我们观察到了我们观察到的情况,它必须存在。如果我们的数学确定性是可疑的,而最大的专家经常持相反的立场,相信眼见为实似乎是一种明智的方法。

1bd37de68abab964a9391205d3c8c8c5.jpeg

最后的话

今天不使用LLMs进行编程是毫无意义的吗?向LLMs提出正确的问题是一项基本技能。这种技能练习得越少,就越难通过人工智能改进工作。而且,开发对问题的描述能力在与其他人交流时也是有用的。LLMs并不是唯一有时不理解我们要说什么的人。沟通不良是一个巨大的限制,尽管许多程序员在自己的特定领域非常有能力,但却沟通得非常差。而如今谷歌已经无法使用:即使只是将LLMs用作压缩形式的文档也是一个好主意。就我个人而言,我将继续广泛使用它们。我从未喜欢学习一个晦涩的通信协议的细节,或者一个想要展示自己有多么出色的人编写的库的复杂方法。这对我来说似乎是"垃圾知识"。LLMs每天都在越来越多地解救我免于这一切。


欢迎关注微软 智汇AI 官方账号

一手资讯抢先了解

119bf8ab46438dee9b4c8e0c61f32f74.jpeg

f7916c8bd379cb9cb252ec7af975a974.jpeg

感谢喜欢,点击一下 在看 吧 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值