快速搭建自己的RAG应用(三)

前面的章节,我们已经完成了可用的基于知识库回答的ai助手,尽管RAG容易上手,但是要真正掌握其精髓却颇有难度,实际上,建立一个的有效的RAG系统不仅仅是将文档放入向量数据库并叠加一个llm模型那么简单,这种方式知识时而有效而已。比如我们问些复杂点问题:

可以看llm的回答的确是相当不如意,提示词的内容并不全面。

在揭开解决方案的神秘面纱之前,我们先来探索一下这个问题的核心。想象一下,你有一个巨大的图书馆,里面有数十亿本书,而你的任务是在这个图书馆中找到与你的研究主题最相关的资料。这就是RAG(Retrieval-Augmented Generation)模型的工作——在大规模的文本海洋中进行语义搜索。

为了在这个巨大的图书馆中快速找到答案,我们使用了一种叫做向量搜索的技术。这就像是在一个多维空间中,把每本书的内容压缩成一个小小的向量,然后通过计算这些向量与你的查询向量之间的距离(比如用余弦相似度),来找到最接近的那本书。但是,这里有个小问题。当我们把书的内容压缩成向量时,就像是把一个丰富多彩的故事变成了黑白照片,总会有一些细节丢失。所以,有时候即使是最接近的三本书,也可能遗漏了一些关键的线索。如果那些排名靠后的书里藏着宝藏般的信息,我们该怎么办呢?

一个直观的想法是,把更多的书带回家(增加top_k值),然后把它们一股脑儿地交给我们的“大语言模型”。但是,我们真正关心的是召回率,也就是“我们找到了多少真正相关的书”。召回率并不在乎我们带回了多少本书,它只关心我们是否找到了所有相关的书。理论上,如果我们把图书馆里的每一本书都带回家,我们就能达到完美的召回率。然而,现实是残酷的。我们的“大语言模型”就像是一个只能装下有限信息的背包,我们称之为上下文窗口。即使是最先进的模型,比如Anthropic的Claude,它的背包可以装下100K Token(可以想象成100K个信息块),我们还是不能把所有的书都塞进去。可以参考下面这个图:

图表达的意思是如果信息被放置在上下文窗口的中间位置,那么模型回忆或检索这些信息的能力会降低,其效果甚至不如这些信息从未被提供给模型。这里的“上下文窗口”指的是模型在处理语言时能够考虑的文本范围,通常是一个固定长度的序列。

这种现象可能是因为在上下文窗口中间的信息相比于靠近窗口开始或结束位置的信息,更容易被后续输入的信息所覆盖或干扰,从而导致模型在需要时难以准确地回忆起这些信息。这表明,在设计或使用大型语言模型时,信息在上下文中的位置可能会影响模型的性能,特别是对于需要长期依赖或记忆的任务。、

举个例子:

假设我们有一个大型语言模型(LLM),它的上下文窗口长度为10个句子。我们想要模型根据一段对话来回答问题。对话内容如下:

  1. 小明说:“我昨天去了图书馆。”
  2. 小华问:“你借了什么书?”
  3. 小明回答:“我借了一本关于历史的书。”
  4. 小华又问:“那本书是关于哪个时期的?”
  5. 小明说:“是关于古罗马的。”
  6. 小华说:“听起来很有趣。”
  7. 小明补充:“是的,书中有很多关于罗马帝国的细节。”
  8. 小华问:“你打算什么时候还书?”
  9. 小明回答:“下周三。”
  10. 小华说:“我可能也会去借那本书。”

现在,我们要求模型回答问题:“小明借的书是关于什么的?”

如果我们将这个问题放在上下文窗口的中间(例如,在第5句和第6句之间),模型可能会因为后续的对话内容(如小华对书的兴趣、还书日期等)而分散注意力,导致它回忆起小明借的书是关于古罗马的能力降低。相比之下,如果问题紧跟在第3句或第5句之后,模型可能更容易直接关联到小明借的书的内容,因为它还没有被后续的对话内容所干扰。

这个例子说明了在上下文窗口中间存储的信息可能会受到后续信息的干扰,从而影响模型回忆这些信息的能力。这也强调了在设计交互式或连续对话系统时,合理安排信息在上下文中的位置对于提高模型性能的重要性。

LLM(大型语言模型)的回忆能力指的是它从其上下文窗口内的文本中检索信息的能力。研究表明,随着我们在上下文窗口中放置更多的令牌(tokens),LLM的回忆能力会下降。当我们过度填充上下文窗口时,LLM也更不可能遵循指令——因此,过度填充上下文窗口是一个糟糕的想法。

我们可以通过增加向量数据库返回的文档数量来提高检索回忆率,但我们不能在不损害LLM回忆能力的情况下将这些文档传递给LLM。

解决这个问题的方法是,通过检索大量文档来最大化检索回忆率,然后通过最小化传递给LLM的文档数量来最大化LLM的回忆能力。为了做到这一点,可以采用以下方案:

1. 使用合适的切分器

在前面的例子中,我使用了

ini
复制代码
# 创建文本分割器
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=30)

RecursiveCharacterTextSplitter:此文本拆分器是推荐用于通用文本的拆分器。它通过一个字符列表参数化,并尝试按顺序在这些字符上拆分,直到块足够小。默认的字符列表是[“\n\n”, “\n”, " ", “”]。这样做的效果是尽可能长时间地保持所有段落(然后是句子,然后是单词)在一起,因为这些通常看起来是最具有语义相关性的文本部分。它不仅基于一系列预设的字符进行拆分,而且能够递归地处理文本,确保按照给定的chunk_size(块大小)和chunk_overlap(块重叠)参数优化拆分结果。这种方法特别适用于需要保持文本内在结构和语义连贯性的场景,如处理段落、句子等结构化文本。

合理地分割文档需要考虑以下因素:

  • 块大小:块的大小应该适中,既要足够小以适应模型的上下文长度限制,又要足够大以包含足够的信息。通常,块的大小在几百个单词左右。
  • 语义完整性:分割时应尽量保持每个块内的语义完整性。这意味着应该避免将一个句子或一个概念分割到两个不同的块中。
  • 重复内容:在分割时应该避免在不同的块中重复相同的内容,除非这是文档结构的一部分(例如,法律文件中的条款可能会在多个部分重复)。
  • 文档结构:考虑文档的结构,如章节、子章节、段落等,可以帮助确定分割点。通常,可以在章节或段落边界处进行分割。
  • 关键词和实体:在分割时,可以考虑文档中的关键词和实体,确保它们不会被分割到不同的块中,以便在检索时能够准确地匹配到相关信息。

总之,合理地分割文档是构建高效、准确的RAG AI助手的关键步骤之一。通过考虑文档的内容、结构和模型的限制,可以创建出既高效又准确的文档分割策略。这个过程是不断调试的过程。为了更加贴合中文文档的格式,我们可以尝试使用重写过的切分器:

python
复制代码
from langchain.text_splitter import CharacterTextSplitter
import re
from typing import List

#该方案出自qanyting开源项目

class ChineseTextSplitter(CharacterTextSplitter):
    
    def __init__(self, pdf: bool = False, sentence_size: int = 100, **kwargs):
        super().__init__(**kwargs)
        self.pdf = pdf
        self.sentence_size = sentence_size

    def split_text(self, text: str) -> List[str]:
        if self.pdf:
            text = re.sub(r"\n{3,}", r"\n", text)
            text = re.sub('\s', " ", text)
            text = re.sub("\n\n", "", text)

        text = re.sub(r'([;;.!?。!??])([^”’])', r"\1\n\2", text)  # 单字符断句符
        text = re.sub(r'(.{6})([^"’”」』])', r"\1\n\2", text)  # 英文省略号
        text = re.sub(r'(\…{2})([^"’”」』])', r"\1\n\2", text)  # 中文省略号
        text = re.sub(r'([;;!?。!??]["’”」』]{0,2})([^;;!?,。!??])', r'\1\n\2', text)
        # 如果双引号前有终止符,那么双引号才是句子的终点,把分句符\n放到双引号后,注意前面的几句都小心保留了双引号
        text = text.rstrip()  # 段尾如果有多余的\n就去掉它
        # 很多规则中会考虑分号;,但是这里我把它忽略不计,破折号、英文双引号等同样忽略,需要的再做些简单调整即可。
        ls = [i for i in text.split("\n") if i]
        for ele in ls:
            if len(ele) > self.sentence_size:
                ele1 = re.sub(r'([,,.]["’”」』]{0,2})([^,,.])', r'\1\n\2', ele)
                ele1_ls = ele1.split("\n")
                for ele_ele1 in ele1_ls:
                    if len(ele_ele1) > self.sentence_size:
                        ele_ele2 = re.sub(r'([\n]{1,}| {2,}["’”」』]{0,2})([^\s])', r'\1\n\2', ele_ele1)
                        ele2_ls = ele_ele2.split("\n")
                        for ele_ele2 in ele2_ls:
                            if len(ele_ele2) > self.sentence_size:
                                ele_ele3 = re.sub('( ["’”」』]{0,2})([^ ])', r'\1\n\2', ele_ele2)
                                ele2_id = ele2_ls.index(ele_ele2)
                                ele2_ls = ele2_ls[:ele2_id] + [i for i in ele_ele3.split("\n") if i] + ele2_ls[
                                                                                                       ele2_id + 1:]
                        ele_id = ele1_ls.index(ele_ele1)
                        ele1_ls = ele1_ls[:ele_id] + [i for i in ele2_ls if i] + ele1_ls[ele_id + 1:]

                id = ls.index(ele)
                ls = ls[:id] + [i for i in ele1_ls if i] + ls[id + 1:]
        return ls

对比效果,几乎全做到了按照段落去切分

2. 对检索到的文档进行重新排序,并只为我们的LLM保留最相关的文档——为了做到这一点,我们使用重新排序(reranking)技术。

重排序模型,是能够针对一个查询和文档对,输出它们的相似度分数。我们利用这个分数对文档按照与查询的相关性进行重新排序。简单理解的意思就是:对embedding检索器出来的chunks再次通过重排序模型rerank按照分数排序后,筛选出相似度最高的chunks作为提示词输入。这也叫两阶段检索系统。

rerank模型与embedding模型的区别
embedding模型rerank模型
检索原理1、把文档A向量化2、把问题B向量化3、对比问题B与文档A的向量值,检索出值与B问题类似的文档,并得出分数1、将查询和某个文档直接输入到Transformer中,进行一整个推理步骤,并最终生成一个相似度分数。
优点检索速度快准确性高,1v1的vip服务,能更准确理解上下文的意思
缺点准确性低,将高维的文本数据压缩到较低维度的向量空间中,这无疑导致了信息的丢失。此外,由于查询是在收到后才知道的,对查询的上下文一无所知(我们是在用户提出查询之前就已经创建了嵌入)。检索速度慢
总结:Embedding模型可以提供有用的信息(粗排),但Rerankers模型通过考虑更多的上下文信息、用户意图和复杂的特征交互,能够提供更精确的排序结果(细排)。在实际应用中,这两种模型通常是互补的,结合使用可以提高整个信息检索系统的性能。

rerank模型有:

模型名称Reranking平均
bge-reranker-base57.7857.78
bge-reranker-large59.6959.69
bce-reranker-base_v160.0660.06

模型可以在魔搭社区下载

代码示例

ini
复制代码
import os
from typing import List

import nltk
from langchain_community.document_loaders import UnstructuredWordDocumentLoader
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_text_splitters import RecursiveCharacterTextSplitter
from pypinyin import pinyin, Style

from LlmClient import LlmClient
from RerankModel import RerankerModel
from configs import rerank_model_path, embedding_path, filepath
from splitter.chinese_text_splitter import ChineseTextSplitter

nltk_data_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'zhipu_chat/nltk_data')
nltk.data.path.insert(0, nltk_data_path)



# 创建嵌入模型
embeddings = HuggingFaceEmbeddings(model_name=embedding_path)

# 获取文件名
file_name = os.path.basename(filepath)
# 将文件名转换为拼音
pinyin_names = pinyin(file_name, style=Style.NORMAL)
# 生成数据库id
kb_id = ''.join([item[0] for item in pinyin_names]).replace('.', '_')

faiss_index_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), kb_id, 'faiss_index')


def merge_splits(docs) -> List:
    new_docs = []
    for doc in docs:
        if not new_docs:
            new_docs.append(doc)
        else:
            last_doc = new_docs[-1]
            if len(last_doc.page_content) + len(doc.page_content) < 200:
                last_doc.page_content += '\n' + doc.page_content
            else:
                new_docs.append(doc)
    splitter = RecursiveCharacterTextSplitter(
        separators=["\n\n", "\n", "。", "!", "!", "?", "?", ";", ";", "……", "…", "、", ",", ",", " ", ""],
        chunk_size=400,
        chunk_overlap=100,
    )
    end_docs = splitter.split_documents(new_docs)
    return end_docs


if os.path.exists(faiss_index_path):
    print("Index loaded from:", faiss_index_path)
    index = FAISS.load_local(folder_path=faiss_index_path, embeddings=embeddings, allow_dangerous_deserialization=True)
else:
    loader = UnstructuredWordDocumentLoader(filepath)

    # 创建文本分割器
    text_splitter = ChineseTextSplitter()
    # 分割文本
    splits = loader.load_and_split(text_splitter)
    # 再次分割处理
    splits = merge_splits(splits)
    # 创建索引
    index = FAISS.from_texts(
        texts=[doc.page_content for doc in splits],
        embedding=embeddings
    )
    # 保存索引
    index.save_local(folder_path=faiss_index_path)
    print("Index saved to:", faiss_index_path)

# 基于问题检索出类似的文档段落,喂给llm,llm经过推理后获取答案
llm_client = LlmClient()
while True:
    user_input = input("请输入文字,按回车键确认:")
    # 检查用户是否想要退出
    if user_input.lower() == 'exit':
        print("程序退出。")
        break
    # 执行相似性搜索,并返回与给定查询最相似的前k个结果。
    doc_score = index.similarity_search_with_score(user_input, k=30)

    for doc, score in doc_score:
        doc.metadata['score'] = score
    docs = [doc for doc, score in doc_score]
    retrieval_documents = sorted(docs, key=lambda x: x.metadata['score'], reverse=True)

    reranker_model = RerankerModel(rerank_model_path)

    scores = reranker_model.score_pairs([(user_input, doc.page_content) for doc in retrieval_documents])
    for doc, score in zip(retrieval_documents, scores):
        doc.metadata['reranker_score'] = score.tolist()
    # 排序
    rerank_documents = sorted(retrieval_documents, key=lambda x: x.metadata['reranker_score'], reverse=True)
    # 删除分数小于0.35的文档
    rerank_documents = [doc for doc in rerank_documents if doc.metadata['reranker_score'] > 0.35]

    # 只拿前面7个
    rerank_documents = retrieval_documents[: 7]

    # 调用llm优化提示词
    llm_client.query(prompt=';'.join(doc.page_content for doc in rerank_documents),
                     user_input=user_input)

    # 调用llm回答
    llm_client.query(prompt=';'.join(doc.page_content for doc in rerank_documents),
                     user_input=user_input)

下面我们再来问问:提供了那些岗位?

写在最后

在探索人工智能的征途上,我们刚刚揭开了冰山0.00001角。上述三篇文章,不过是引玉之砖,为初学者铺设了入门的阶梯。然而,要真正驾驭这股智能的洪流,我们还需深入langchain的奥秘,多探索魔搭社区huggingfaceollama。更进一步,我们需要潜心研究大模型的底层调优,洞悉不同算法间的微妙差异。

如何系统的去学习大模型LLM ?

作为一名热心肠的互联网老兵,我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。

但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的 AI大模型资料 包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来

😝有需要的小伙伴,可以V扫描下方二维码免费领取🆓

在这里插入图片描述

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

img

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

img

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

img

在这里插入图片描述

四、AI大模型商业化落地方案

img

阶段1:AI大模型时代的基础理解

  • 目标:了解AI大模型的基本概念、发展历程和核心原理。
  • 内容
    • L1.1 人工智能简述与大模型起源
    • L1.2 大模型与通用人工智能
    • L1.3 GPT模型的发展历程
    • L1.4 模型工程
      - L1.4.1 知识大模型
      - L1.4.2 生产大模型
      - L1.4.3 模型工程方法论
      - L1.4.4 模型工程实践
    • L1.5 GPT应用案例

阶段2:AI大模型API应用开发工程

  • 目标:掌握AI大模型API的使用和开发,以及相关的编程技能。
  • 内容
    • L2.1 API接口
      - L2.1.1 OpenAI API接口
      - L2.1.2 Python接口接入
      - L2.1.3 BOT工具类框架
      - L2.1.4 代码示例
    • L2.2 Prompt框架
      - L2.2.1 什么是Prompt
      - L2.2.2 Prompt框架应用现状
      - L2.2.3 基于GPTAS的Prompt框架
      - L2.2.4 Prompt框架与Thought
      - L2.2.5 Prompt框架与提示词
    • L2.3 流水线工程
      - L2.3.1 流水线工程的概念
      - L2.3.2 流水线工程的优点
      - L2.3.3 流水线工程的应用
    • L2.4 总结与展望

阶段3:AI大模型应用架构实践

  • 目标:深入理解AI大模型的应用架构,并能够进行私有化部署。
  • 内容
    • L3.1 Agent模型框架
      - L3.1.1 Agent模型框架的设计理念
      - L3.1.2 Agent模型框架的核心组件
      - L3.1.3 Agent模型框架的实现细节
    • L3.2 MetaGPT
      - L3.2.1 MetaGPT的基本概念
      - L3.2.2 MetaGPT的工作原理
      - L3.2.3 MetaGPT的应用场景
    • L3.3 ChatGLM
      - L3.3.1 ChatGLM的特点
      - L3.3.2 ChatGLM的开发环境
      - L3.3.3 ChatGLM的使用示例
    • L3.4 LLAMA
      - L3.4.1 LLAMA的特点
      - L3.4.2 LLAMA的开发环境
      - L3.4.3 LLAMA的使用示例
    • L3.5 其他大模型介绍

阶段4:AI大模型私有化部署

  • 目标:掌握多种AI大模型的私有化部署,包括多模态和特定领域模型。
  • 内容
    • L4.1 模型私有化部署概述
    • L4.2 模型私有化部署的关键技术
    • L4.3 模型私有化部署的实施步骤
    • L4.4 模型私有化部署的应用场景

学习计划:

  • 阶段1:1-2个月,建立AI大模型的基础知识体系。
  • 阶段2:2-3个月,专注于API应用开发能力的提升。
  • 阶段3:3-4个月,深入实践AI大模型的应用架构和私有化部署。
  • 阶段4:4-5个月,专注于高级模型的应用和部署。
这份完整版的大模型 LLM 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

😝有需要的小伙伴,可以Vx扫描下方二维码免费领取🆓

在这里插入图片描述

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值