《Advanced RAG》-05-探索语义分块(Semantic Chunking)

摘要

文章首先介绍了语义分块在 RAG 中的位置和作用,并介绍了常见的基于规则的分块方法。

然后,阐述了语义分块的目的是确保每个分块包含尽可能多的独立语义信息。

接着,文章分别介绍了三种语义分块方法的原理和实现方法,并对每种方法进行了总结和评估。

文章观点

  1. 语义分块是 RAG 中的一种重要技术,将文档分解成更小的块以提取详细的特征。
  2. 常见的分块方法是基于规则的方法,如固定块大小或相邻块重叠等。
  3. 语义分块的目的是确保每个分块包含尽可能多的独立语义信息。
  4. 基于嵌入的方法利用嵌入模型对文本进行语义分块,其中 LlamaIndex 和 Langchain 都提供了语义分块器。
  5. 基于模型的方法利用模型对文本进行语义分块,包括 Naive BERTCross Segment AttentionSeqModel
  6. 基于 LLM 的方法利用 LLM 构建命题进行语义分块,形成一个小到大的索引结构,提供了一种新的语义分块思路。

解析文档后,我们可以获得结构化或半结构化数据。现在的主要任务是将它们分解成更小的块来提取详细的特征,然后嵌入这些特征来表示它们的语义。其在 RAG 中的位置如图 1 所示。

在这里插入图片描述

大多数常用的分块方法都是基于规则的,采用固定分块大小或相邻分块重叠等技术。

对于多级文档,我们可以使用 Langchain 提供的 RecursiveCharacterTextSplitter。这允许定义多级分隔符。

然而,在实际应用中,由于预定义的规则(块大小或重叠部分的大小)过于死板,基于规则的分块方法很容易导致检索上下文不完整或包含噪声的块大小过大等问题。

因此,对于分块来说,最有效的方法显然是根据语义进行分块。语义分块旨在确保每个分块包含尽可能多的独立语义信息。

本文将探讨语义分块的方法,解释其原理和应用。我们将介绍三种方法:

  1. 基于嵌入式(Embedding-based)
  2. 基于模型(Model-based)
  3. 基于 LLM(LLM-based)

基于嵌入的方法(Embedding-based)

LlamaIndex 和 Langchain 都提供了基于嵌入的语义分块器。算法的思路大致相同,我们将以 LlamaIndex 为例进行说明。

请注意,要访问 LlamaIndex 中的语义分块器,您需要安装最新版本。我安装的前一个版本 0.9.45 并不包含这种算法。因此,我创建了一个新的 conda 环境,并安装了更新版本 0.10.12:

pip install llama-index-core
pip install llama-index-readers-file
pip install llama-index-embeddings-openai
pip install httpx[socks]

值得一提的是,LlamaIndex 0.10.12 可以灵活安装,因此这里只安装了一些关键组件。已安装的版本如下:

(llamaindex_010) Florian:~ Florian$ pip list | grep llama
llama-index-core              0.10.12
llama-index-embeddings-openai 0.1.6
llama-index-readers-file      0.1.5
llamaindex-py-client          0.1.13

测试代码如下

from llama_index.core.node_parser import (
    SentenceSplitter,
    SemanticSplitterNodeParser,
)
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.core import SimpleDirectoryReader


import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPEN_AI_KEY"# load documents
dir_path = "YOUR_DIR_PATH"
documents = SimpleDirectoryReader(dir_path).load_data()


embed_model = OpenAIEmbedding()
splitter = SemanticSplitterNodeParser(
    buffer_size=1, breakpoint_percentile_threshold=95, embed_model=embed_model
)

nodes = splitter.get_nodes_from_documents(documents)
for node in nodes:
    print('-' * 100)
    print(node.get_content())

我跟踪了 de>splitter.get_nodes_from_documents 函数,其主要过程如图 2 所示:

在这里插入图片描述

图 2 中提到的 "sentences"是一个 python 列表,其中每个成员都是一个字典,包含四个(键,值)对,键的含义如下:

  • sentence: 当前句子
  • **index****:**当前句子的序列号
  • **combined_sentence****:**一个滑动窗口,包括 [index -self.buffer_size, index, index + self.buffer_size] 3 个句子(默认情况下,self.buffer_size = 1)。它是用于计算句子间语义相关性的工具。合并前后句子的目的是减少噪音,更好地捕捉连续句子之间的关系。
  • **combined_sentence_embedding****:**合并句子的嵌入内容

从以上分析可以看出,基于嵌入的语义分块主要是根据滑动窗口(合并句子)计算相似度。那些相邻且符合阈值的句子会被归入一个语义块。

目录路径只包含一份 BERT 纸质文件。下面是一些运行结果:

(llamaindex_010) Florian:~ Florian$ python /Users/Florian/Documents/june_pdf_loader/test_semantic_chunk.py 
...
...
----------------------------------------------------------------------------------------------------
We argue that current techniques restrict the
power of the pre-trained representations, espe-
cially for the fine-tuning approaches. The ma-
jor limitation is that standard language models are
unidirectional, and this limits the choice of archi-
tectures that can be used during pre-training. For
example, in OpenAI GPT, the authors use a left-to-right architecture, where every token can only at-
tend to previous tokens in the self-attention layers
of the Transformer (Vaswani et al., 2017). Such re-
strictions are sub-optimal for sentence-level tasks,
and could be very harmful when applying fine-
tuning based approaches to token-level tasks such
as question answering, where it is crucial to incor-
porate context from both directions.
In this paper, we improve the fine-tuning based
approaches by proposing BERT: Bidirectional
Encoder Representations from Transformers.
BERT alleviates the previously mentioned unidi-
rectionality constraint by using a “masked lan-
guage model” (MLM) pre-training objective, in-
spired by the Cloze task (Taylor, 1953). The
masked language model randomly masks some of
the tokens from the input, and the objective is to
predict the original vocabulary id of the maskedarXiv:1810.04805v2  [cs.CL]  24 May 2019----------------------------------------------------------------------------------------------------
word based only on its context. Unlike left-to-right language model pre-training, the MLM ob-
jective enables the representation to fuse the leftand the right context, which allows us to pre-
train a deep bidirectional Transformer. In addi-
tion to the masked language model, we also use
a “next sentence prediction” task that jointly pre-
trains text-pair representations. The contributions
of our paper are as follows:
• We demonstrate the importance of bidirectional
pre-training for language representations. Un-like Radford et al. (2018), which uses unidirec-
tional language models for pre-training, BERT
uses masked language models to enable pre-
trained deep bidirectional representations. This
is also in contrast to Peters et al. 
----------------------------------------------------------------------------------------------------
...
...

测试结果表明,数据块的粒度相对较粗。

图 2 还显示,这种方法是基于页面的,并不能直接解决跨多个页面的块的问题。

一般来说,基于嵌入的方法的性能在很大程度上取决于嵌入模型。实际效果需要今后进行评估。

基于模型的方法(Model-based methods)

回顾 BERT 的预训练过程。设计了一个二元分类任务,即下一句预测(Next Sentence Prediction, NSP),用以教导模型理解两个句子之间的关系。在这里,将两个句子同时输入BERT,模型预测第二个句子是否紧接着第一个句子。

我们可以运用这一原理来设计一种直接的分块方法。对于一篇文档,将其分割成若干句子。然后,使用滑动窗口将相邻的两个句子输入 BERT 模型进行 NSP 判断,如图 3 所示:

在这里插入图片描述

**如果预测得分低于预设阈值,则表明这两个句子之间的语义关系较弱。**这可以作为文本分割点,如图 3 中的句子 2 和句子 3。

这种方法的优点是可以直接使用,无需培训或微调。

但是,这种方法在确定文本分割点时只考虑了前后句子,忽略了后面句子的信息。此外,这种方法的预测效率相对较低。

跨领域关注(Cross Segment Attention)

如图 4 所示,论文《Text Segmentation by Cross Segment Attention》提出了三种关于跨段注意力的模型:

在这里插入图片描述

如图中所示:

  1. 在跨句段 BERT 模型(左)中,我们向模型输入围绕潜在句段断裂的局部上下文:左边 k 个词组,右边 k 个词组。
  2. 在 BERT+Bi-LSTM 模型(中)中,我们首先使用 BERT 模型对每个句子进行编码,然后将句子表示输入 Bi-LSTM 中。
  3. 在分层 BERT 模型(右图)中,我们首先使用 BERT 对每个句子进行编码,然后将输出的句子表示输入另一个基于转换器的模型。资料来源通过跨段关注进行文本分割。

图 4 (a) 显示了跨分段 BERT 模型,该模型将文本分段定义为逐句分类任务。潜在断句的上下文(两侧的 k 个token)被输入到模型中。与 [CLS] 相对应的隐藏状态被传递给 softmax 分类器,由其决定是否在潜在断句处进行分割。

本文还提出了另外两个模型。其中一个使用 BERT 模型获得每个句子的向量表示。然后将多个连续句子的这些向量表示输入 Bi-LSTM 模型(图 4 (b))或另一个 BERT 模型(图 4 ©),以预测每个句子是否是文本分割边界。

如图 5 所示,当时这三个模型取得了最先进的结果:

在这里插入图片描述

然而,迄今为止,人们只发现了本文中的训练实现。目前还没有发现公开可用的推理模型。

序列模型(SeqModel)

Cross-Segment 模型对每个句子进行独立矢量化,不考虑任何更广泛的上下文信息。SeqModel 中提出了进一步的改进方案,详见论文 “Sequence Model with Self-Adaptive Sliding Window for Efficient Spoken Document Segmentation”。

SeqModel 采用 BERT 同时对多个句子进行编码,在计算句子向量之前对较长上下文中的依赖关系进行建模。然后,它会预测是否在每个句子之后进行文本分割。

此外,该模型还利用自适应滑动窗口法来提高推理速度,而不会影响准确性。SeqModel 的示意图如图 6 所示。

在这里插入图片描述

SeqModel 可通过 ModelScope 框架 使用。代码如下:

from modelscope.outputs import OutputKeys
from modelscope.pipelines import pipeline
from modelscope.utils.constant import Tasks

p = pipeline(
    task = Tasks.document_segmentation,
    model = 'damo/nlp_bert_document-segmentation_english-base'
)

print('-' * 100)

result = p(documents='We demonstrate the importance of bidirectional pre-training for language representations. Unlike Radford et al. (2018), which uses unidirectional language models for pre-training, BERT uses masked language models to enable pretrained deep bidirectional representations. This is also in contrast to Peters et al. (2018a), which uses a shallow concatenation of independently trained left-to-right and right-to-left LMs. • We show that pre-trained representations reduce the need for many heavily-engineered taskspecific architectures. BERT is the first finetuning based representation model that achieves state-of-the-art performance on a large suite of sentence-level and token-level tasks, outperforming many task-specific architectures. Today is a good day')

print(result[OutputKeys.TEXT])

测试数据在末尾添加了一句 “Today is a good day”,但结果并没有以任何方式将 "Today is a good day"分开。

(modelscope) Florian:~ Florian$ python /Users/Florian/Documents/june_pdf_loader/test_seqmodel.py 
2024-02-24 17:09:36,288 - modelscope - INFO - PyTorch version 2.2.1 Found.
2024-02-24 17:09:36,288 - modelscope - INFO - Loading ast index from /Users/Florian/.cache/modelscope/ast_indexer
...
...
----------------------------------------------------------------------------------------------------
...
... 
We demonstrate the importance of bidirectional pre-training for language representations.Unlike Radford et al.(2018), which uses unidirectional language models for pre-training, BERT uses masked language models to enable pretrained deep bidirectional representations.This is also in contrast to Peters et al.(2018a), which uses a shallow concatenation of independently trained left-to-right and right-to-left LMs.• We show that pre-trained representations reduce the need for many heavily-engineered taskspecific architectures.BERT is the first finetuning based representation model that achieves state-of-the-art performance on a large suite of sentence-level and token-level tasks, outperforming many task-specific architectures.Today is a good day

总体而言,基于模型的语义分块法仍有很大的提升空间。

我建议的一种改进方法是创建针对特定项目的训练数据,以便进行领域微调。这可以提高模型的性能。此外,优化模型结构也是一个改进点。

如果我们能找到一个在特定业务数据上表现良好的模型,那么基于模型的方法仍然有效。

基于 LLM 的方法(LLM-based methods)

论文《Dense X Retrieval: What Retrieval Granularity Should We Use?》引入了一种新的检索单元,称为命题

命题被定义为文本中的原子表达式,每个命题封装了一个独特的事实片段,并以简洁、自包含的自然语言格式呈现。

那么,我们如何获得这个所谓的命题呢?本文通过构建提示和与 LLM 交互来实现。

LlamaIndex 和 Langchain 都实现了相关算法,下面使用 LlamaIndex 进行演示。

LlamaIndex 的实施思路包括使用论文中提供的提示来生成命题:

PROPOSITIONS_PROMPT = PromptTemplate(
    """Decompose the "Content" into clear and simple propositions, ensuring they are interpretable out of
context.
1. Split compound sentence into simple sentences. Maintain the original phrasing from the input
whenever possible.
2. For any named entity that is accompanied by additional descriptive information, separate this
information into its own distinct proposition.
3. Decontextualize the proposition by adding necessary modifier to nouns or entire sentences
and replacing pronouns (e.g., "it", "he", "she", "they", "this", "that") with the full name of the
entities they refer to.
4. Present the results as a list of strings, formatted in JSON.

Input: Title: ¯Eostre. Section: Theories and interpretations, Connection to Easter Hares. Content:
The earliest evidence for the Easter Hare (Osterhase) was recorded in south-west Germany in
1678 by the professor of medicine Georg Franck von Franckenau, but it remained unknown in
other parts of Germany until the 18th century. Scholar Richard Sermon writes that "hares were
frequently seen in gardens in spring, and thus may have served as a convenient explanation for the
origin of the colored eggs hidden there for children. Alternatively, there is a European tradition
that hares laid eggs, since a hare’s scratch or form and a lapwing’s nest look very similar, and
both occur on grassland and are first seen in the spring. In the nineteenth century the influence
of Easter cards, toys, and books was to make the Easter Hare/Rabbit popular throughout Europe.
German immigrants then exported the custom to Britain and America where it evolved into the
Easter Bunny."
Output: [ "The earliest evidence for the Easter Hare was recorded in south-west Germany in
1678 by Georg Franck von Franckenau.", "Georg Franck von Franckenau was a professor of
medicine.", "The evidence for the Easter Hare remained unknown in other parts of Germany until
the 18th century.", "Richard Sermon was a scholar.", "Richard Sermon writes a hypothesis about
the possible explanation for the connection between hares and the tradition during Easter", "Hares
were frequently seen in gardens in spring.", "Hares may have served as a convenient explanation
for the origin of the colored eggs hidden in gardens for children.", "There is a European tradition
that hares laid eggs.", "A hare’s scratch or form and a lapwing’s nest look very similar.", "Both
hares and lapwing’s nests occur on grassland and are first seen in the spring.", "In the nineteenth
century the influence of Easter cards, toys, and books was to make the Easter Hare/Rabbit popular
throughout Europe.", "German immigrants exported the custom of the Easter Hare/Rabbit to
Britain and America.", "The custom of the Easter Hare/Rabbit evolved into the Easter Bunny in
Britain and America." ]

Input: {node_text}
Output:"""
)

在 "基于嵌入的方法"中,我们已经安装了 LlamaIndex 0.10.12 的关键组件。

但如果要使用 DenseXRetrievalPack,我们还需要运行 pip install llama-index-llms-openai。安装完成后,当前的 LlamaIndex 相关组件如下:

(llamaindex_010) Florian:~ Florian$ pip list | grep llama
llama-index-core                    0.10.12
llama-index-embeddings-openai       0.1.6
llama-index-llms-openai             0.1.6
llama-index-readers-file            0.1.5
llamaindex-py-client                0.1.13

在 LlamaIndex 中,DenseXRetrievalPack 是一个需要单独下载的软件包。这里直接在测试代码中下载。测试代码如下:

from llama_index.core.readers import SimpleDirectoryReader
from llama_index.core.llama_pack import download_llama_pack

import os
os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY"# Download and install dependencies
DenseXRetrievalPack = download_llama_pack(
    "DenseXRetrievalPack", "./dense_pack"
)

# If you have already downloaded DenseXRetrievalPack, you can import it directly.# from llama_index.packs.dense_x_retrieval import DenseXRetrievalPack# Load documents
dir_path = "YOUR_DIR_PATH"
documents = SimpleDirectoryReader(dir_path).load_data()


# Use LLM to extract propositions from every document/node
dense_pack = DenseXRetrievalPack(documents)

response = dense_pack.run("YOUR_QUERY")

通过测试代码可以发现,类 DenseXRetrievalPack 的构造函数主要在使用中。分析 de>class DenseXRetrievalPack 的源代码似乎很有必要。

class DenseXRetrievalPack(BaseLlamaPack):
    def __init__(
        self,
        documents: List[Document],
        proposition_llm: Optional[LLM] = None,
        query_llm: Optional[LLM] = None,
        embed_model: Optional[BaseEmbedding] = None,
        text_splitter: TextSplitter = SentenceSplitter(),
        similarity_top_k: int = 4,
    ) -> None:
        """Init params."""
        self._proposition_llm = proposition_llm or OpenAI(
            model="gpt-3.5-turbo",
            temperature=0.1,
            max_tokens=750,
        )

        embed_model = embed_model or OpenAIEmbedding(embed_batch_size=128)

        nodes = text_splitter.get_nodes_from_documents(documents)
        sub_nodes = self._gen_propositions(nodes)

        all_nodes = nodes + sub_nodes
        all_nodes_dict = {n.node_id: n for n in all_nodes}

        service_context = ServiceContext.from_defaults(
            llm=query_llm or OpenAI(),
            embed_model=embed_model,
            num_output=self._proposition_llm.metadata.num_output,
        )

        self.vector_index = VectorStoreIndex(
            all_nodes, service_context=service_context, show_progress=True
        )

        self.retriever = RecursiveRetriever(
            "vector",
            retriever_dict={
                "vector": self.vector_index.as_retriever(
                    similarity_top_k=similarity_top_k
                )
            },
            node_dict=all_nodes_dict,
        )

        self.query_engine = RetrieverQueryEngine.from_args(
            self.retriever, service_context=service_context
        )

如代码所示,构造函数的思路是首先使用 text_splitter 将文档划分为原始节点,再调用 self._gen_propositions 通过生成命题获得相应的子节点。然后,它会使用 nodes + sub_nodes 建立一个 VectorStoreIndex,并通过 RecursiveRetriever 进行检索。递归检索器可以使用小块检索,但会将相关的大块传递给生成阶段。

该目录路径只包含一个 BERT 论文文档。通过调试,我们发现 sub_nodes[].text 并非原始文本,而是被改写过的:

> /Users/Florian/anaconda3/envs/llamaindex_010/lib/python3.11/site-packages/llama_index/packs/dense_x_retrieval/base.py(91)__init__()
     90 
---> 91         all_nodes = nodes + sub_nodes
     92         all_nodes_dict = {n.node_id: n for n in all_nodes}


ipdb> sub_nodes[20]
IndexNode(id_='ecf310c7-76c8-487a-99f3-f78b273e00d9', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Our paper demonstrates the importance of bidirectional pre-training for language representations.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[21]
IndexNode(id_='4911332e-8e30-47d8-a5bc-ed7cbaa8e042', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Radford et al. (2018) uses unidirectional language models for pre-training.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[22]
IndexNode(id_='83aa82f8-384a-4b06-92c8-d6277c4162bf', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='BERT uses masked language models to enable pre-trained deep bidirectional representations.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[23]
IndexNode(id_='2ac635c2-ccb0-4e62-88c7-bcbaef3ef38a', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Peters et al. (2018a) uses a shallow concatenation of independently trained left-to-right and right-to-left LMs.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)
ipdb> sub_nodes[24]
IndexNode(id_='e37b17cf-30dd-4114-a3c5-9921b8cf0a77', embedding=None, metadata={}, excluded_embed_metadata_keys=[], excluded_llm_metadata_keys=[], relationships={}, text='Pre-trained representations reduce the need for many heavily-engineered task-specific architectures.', start_char_idx=None, end_char_idx=None, text_template='{metadata_str}\n\n{content}', metadata_template='{key}: {value}', metadata_seperator='\n', index_id='8deca706-fe97-412c-a13f-950a19a594d1', obj=None)

子节点和节点之间的关系如图 7 所示,这是一个从小到大的索引结构。

在这里插入图片描述
由小到大的索引结构是通过 self._gen_propositions 建立的,代码如下:

  async def _aget_proposition(self, node: TextNode) -> List[TextNode]:
        """Get proposition."""
        inital_output = await self._proposition_llm.apredict(
            PROPOSITIONS_PROMPT, node_text=node.text
        )
        outputs = inital_output.split("\n")

        all_propositions = []

        for output in outputs:
            if not output.strip():
                continueif not output.strip().endswith("]"):
                if not output.strip().endswith('"') and not output.strip().endswith(
                    ","
                ):
                    output = output + '"'
                output = output + " ]"if not output.strip().startswith("["):
                if not output.strip().startswith('"'):
                    output = '"' + output
                output = "[ " + output

            try:
                propositions = json.loads(output)
            except Exception:
                # fallback to yamltry:
                    propositions = yaml.safe_load(output)
                except Exception:
                    # fallback to next outputcontinueif not isinstance(propositions, list):
                continue

            all_propositions.extend(propositions)

        assert isinstance(all_propositions, list)
        nodes = [TextNode(text=prop) for prop in all_propositions if prop]

        return [IndexNode.from_text_node(n, node.node_id) for n in nodes]

    def _gen_propositions(self, nodes: List[TextNode]) -> List[TextNode]:
        """Get propositions."""
        sub_nodes = asyncio.run(
            run_jobs(
                [self._aget_proposition(node) for node in nodes],
                show_progress=True,
                workers=8,
            )
        )

        # Flatten listreturn [node for sub_node in sub_nodes for node in sub_node]

对于每个原始节点,异步调用 self._aget_proposition,通过 PROPOSITIONS_PROMPT 获取 LLM 返回的 inital_output,然后根据 inital_output 获取命题并构建 TextNode。最后,将这些 TextNode 与原始节点关联起来,即 [IndexNode.from_text_node(n, node.node_id) for n in nodes]

值得一提的是,原论文使用 LLM 生成的命题作为训练数据,以进一步微调文本生成模型。文本生成模型现已公开。

总的来说,这种利用 LLM 构建命题的分块方法实现了更精细的分块。它与原始节点形成了一个从小到大的索引结构,从而为语义分块提供了一种新的思路。

不过,这种方法依赖于 LLM,而 LLM 的成本相对较高。

结论

本文探讨了三种语义分块方法的原理和实现方法,并提供了一些评论。

一般来说,语义分块是一种更优雅的方法,也是优化 RAG 的关键点。

本文为翻译,原文地址:https://pub.towardsai.net/advanced-rag-05-exploring-semantic-chunking-97c12af20a4d

  • 10
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

静愚 AGI

你的善意终将流回自己,加油!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值