RAG技术全解析:打造下一代智能问答系统

一、RAG简介

大型语言模型(LLM)已经取得了显著的成功,尽管它们仍然面临重大的限制,特别是在特定领域或知识密集型任务中,尤其是在处理超出其训练数据或需要当前信息的查询时,常会产生“幻觉”现象。为了克服这些挑战,检索增强生成(RAG)通过从外部知识库检索相关文档chunk并进行语义相似度计算,增强了LLM的功能。通过引用外部知识,RAG有效地减少了生成事实不正确内容的问题。RAG目前是基于LLM系统中最受欢迎的架构,有许多产品基于RAG构建,使RAG成为推动聊天机器人发展和增强LLM在现实世界应用适用性的关键技术。

二、RAG架构

2.1 RAG实现过程

RAG在问答系统中的一个典型应用主要包括三个步骤:

  • Indexing(索引):将文档分割成chunk,编码成向量,并存储在向量数据库中。
  • Retrieval(检索):根据语义相似度检索与问题最相关的前k个chunk。
  • Generation(生成):将原始问题和检索到的chunk一起输入到LLM中,生成最终答案。

2.2 RAG在线检索架构

三、RAG流程

接下来,我们将深入探讨RAG各个流程,并为RAG构建技术路线图。

3.1 索引

索引是将文本分解成可管理的chunk的过程,是组织系统的关键步骤,面临三个主要挑战:

  • 不完整的内容表示:chunk的语义信息受到分割方法的影响,导致在更长的上下文中重要信息的丢失或隐藏。
  • 不准确的chunk相似性搜索:随着数据量的增加,检索中的噪声增多,导致频繁与错误数据匹配,使检索系统变得脆弱和不可靠。
  • 不明确的引用轨迹:检索到的chunk可能来源于任何文档,缺乏引用路径,可能导致存在来自多个不同文档的chunk,尽管这些chunk在语义上相似,但包含的内容完全不同的主题。
3.1.1 Chunking

Transformer模型有固定的输入序列长度,即使输入上下文窗口很大,一个句子或几个句子的向量也比几页文本的平均向量更能代表它们的语义意义。所以我们需要对数据进行分块,将初始文档分割成一定大小的chunk,同时不丢失它们的意义(将文本分割成句子或段落,而不是将一个句子分成两部分)。

有多种文本切分策略能够完成这项任务,我们在实践中采用了以下3种策略:

  • 直接分段:将文本按照一定的规则进行分段处理后,转成可以进行语义搜索的格式。这里不需要调用模型进行额外处理,成本低,适合绝大多数应用场景。
  • 生成问答对:根据一定的规则,将文本拆成一段主题文本,调用LLM为该段主题文本生成问答对。这种处理方式有非常高的检索精度,但是会丢失部分文本细节,需要特别留意。
  • 增强信息:通过子索引以及调用LLM生成相关问题和摘要,来增加chunk的语义丰富度,更加有利于后面的检索。不过需要消耗更多的存储空间和增加LLM调用开销。

chunk的大小是一个需要重点考虑的参数,它取决于我们使用的Embedding模型及其token的容量。标准的Transformer编码器模型,如基于BERT的Sentence Transformer最多处理512个token,而OpenAI的text-embedding-3-small能够处理更长的序列(8191个token)。

为了给LLM提供足够的上下文以进行推理,同时给搜索提供足够具体的文本嵌入,我们需要一些折衷策略。较大的chunk可以捕获更多的上下文,但它们也会产生更多的噪音,需要更长的处理时间和更高的成本。而较小的chunk可能无法完全传达必要的上下文,但它们的噪音较少。

以网页https://www.openim.io/en的文本内容为输入,按照上面3种策略进行文本分割。

  1. 直接分段

    切分后的chunk信息,总共10个chunk:

     def split_long_section(section, max_length=1300):
         lines = section.split('\n')
         current_section = ""
         result = []
         for line in lines:
             # Add 1 for newline character when checking the length
             if len(current_section) + len(line) + 1 > max_length:
                 if current_section:
                     result.append(current_section)
                     current_section = line  # Start a new paragraph
                 else:
                     # If a single line exceeds max length, treat it as its own paragraph
                     result.append(line)
             else:
                 if current_section:
                     current_section += '\n' + line
                 else:
                     current_section = line
  2. 生成问答对

    切分后的chunk信息,总共28个chunk,每个chunk包含一对问答:


    切分后的某个chunk的问答对信息:
  3. 增强信息

    切分后的chunk信息,总共6个chunk,每个chunk都包含一批数据索引信息:


    切分后的某个chunk的数据索引信息:
3.1.1.1 滑动窗口

平衡这些需求的一种简单方法是使用重叠的chunk。通过使用滑动窗口,可以增强语义过渡。然而,也存在一些限制,包括对上下文大小的控制不精确、有截断单词或句子的风险,以及缺乏语义考虑。

final_result = []
ast_lines = ""
for section in result:
    lines = section.split('\n')
    last_two_lines = "\n".join(lines[-2:])  # Extract the last two lines
    combined_section = last_lines + "\n" + section if last_lines else section
    final_result.append(combined_section)
    last_lines = last_two_lines
3.1.1.2 上下文丰富化

这里的概念是为了获得更好的搜索质量而检索较小的chunk,并添加周围的上下文供LLM进行推理。
有两个选项:通过在较小的检索chunk周围添加句子来扩展上下文,或者将文档递归地分成多个较大的父chunk,其中包含较小的子chunk。

句子窗口检索

在这个方案中,文档中的每个句子都被单独嵌入,这提供了查询与上下文余弦距离搜索的高准确性。
为了在获取到最相关的单个句子后更好地推理出找到的上下文,我们通过在检索到的句子之前和之后添加k个句子来扩展上下文窗口,然后将这个扩展后的上下文发送给LLM。

from llama_index import ServiceContext, VectorStoreIndex, StorageContext
from llama_index.node_parser import SentenceWindowNodeParser

def build_sentence_window_index(
    document, llm, vector_store, embed_model="local:BAAI/bge-small-en-v1.5"
):
    # create the sentence window node parser w/ default settings
    node_parser = SentenceWindowNodeParser.from_defaults(
        window_size=3,
        window_metadata_key="window",
        original_text_metadata_key="original_text",
    )
    sentence_context = ServiceContext.from_defaults(
        llm=llm,
        embed_model=embed_model,
        node_parser=node_parser
    )
    storage_context = StorageContext.from_defaults(vector_store=vector_store)
    sentence_index = VectorStoreIndex.from_documents(
        [document], service_context=sentence_context, storage_context=storage_context
    )

    return sentence_index

父文档检索器

文档被分割成一个层次结构的chunk,然后最小的叶子chunk被发送到索引中。在检索时,我们检索k个叶子chunk,如果有n个chunk引用同一个父chunk,我们将它们替换为该父chunk并将其发送给LLM进行答案生成。

关键思想是将用于检索的chunk与用于合成的chunk分开。使用较小的chunk

### RAG 技术与大型语言模型 (LLM) 的融合应用 #### 基础概念介绍 RAG(Retrieval-Augmented Generation)是一种结合检索增强生成的方法,旨在提升自然语言处理任务的效果。通过引入外部知识库中的信息来辅助生成过程,这种方法不仅提高了生成内容的质量,还增强了系统的灵活性和适应能力。 #### 实现方式概述 为了实现这一目标,通常采用如下架构: 1. **数据预处理阶段** 数据集被分割成多个片段并存储于索引结构中以便快速访问。这些片段可能来自文档集合或其他形式的知识源[^1]。 2. **查询理解与扩展** 用户输入经过初步解析后形成向量表示,用于后续相似度计算;同时借助 LLM 对原始请求进行语义扩充,增加同义词或关联词汇以扩大搜索范围[^2]。 3. **多轮次检索机制** 利用上述特征向量,在预先构建好的索引内执行多次迭代式的近似最近邻查找操作,逐步缩小候选答案空间直至收敛至最优解附近。 4. **结果合成优化** 收集到的相关片段经由另一实例化的 LLM 进行二次加工——即根据具体应用场景调整语气风格、补充背景资料等细节部分,最终产出连贯完整的回复文本。 ```python from transformers import RagTokenizer, RagRetriever, RagSequenceForGeneration tokenizer = RagTokenizer.from_pretrained("facebook/rag-token-nq") retriever = RagRetriever.from_pretrained( "facebook/dpr-question_encoder-single-nq-base", index_name="exact", use_dummy_dataset=True, ) model = RagSequenceForGeneration.from_pretrained("facebook/rag-sequence-nq", retriever=retriever) input_dict = tokenizer.prepare_seq2seq_batch(contexts=["What is the capital of France?"], return_tensors="pt") generated_ids = model.generate(input_ids=input_dict["input_ids"]) print(tokenizer.batch_decode(generated_ids, skip_special_tokens=True)) ``` 此代码展示了如何加载预训练的 RAG 模型,并针对给定问题获取最佳匹配的回答。这里选择了 Facebook 开发的一套工具链作为示范案例,实际部署时可根据需求替换相应组件。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值