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

750 阅读10分钟
<https://github.com/hyy10086/zhipu_chat>

大型语言模型(LLM)的文本推理能力,宛如一位博学的公民,其智慧之源来自于互联网上公开的文献宝库。想象一下,这位名为LLM的公民,如同一位勤奋的学者,借阅了图书馆中所有的书籍,并将这些知识深深地烙印在自己的记忆之中。然而,若有一本新书问世,尚未公开发行,那么LLM自然无法将其纳入自己的知识体系。因此,当我向LLM询问我的新书内容时,它显然无法给出答案。

解决之道显而易见——将我的新书“喂”给LLM,让它成为LLM知识库的一部分。这一过程,如同将新知注入智慧之泉,只能由我亲自完成。关键在于,何时将这份知识传递给LLM。

以考试为例,当被问及“1+1等于多少?”时,我们会在脑海中搜寻以往学过的知识,得出答案是3。或者,我们可能会偷偷翻阅书籍,现场查找答案。这两种方法都能帮助我们回答问题,同样地,让LLM回答问题也需要这样的过程。我们需要确保LLM能够获取最新的知识,以便它能够准确无误地回答问题,就像我们在考试中运用所学知识一样。

在大型语言模型(如GPT系列)的背景下,"预训练"和"知识提示"是两种不同的方法,用于准备模型以回答问题或执行特定任务。

    • 预训练(Pre-training):

预训练是指在大量文本数据上训练模型,使其学习语言的通用模式和结构。这个过程不针对特定任务,而是让模型掌握语言的一般知识。预训练通常涉及一个大规模的神经网络,如Transformer,它在无监督的情况下学习预测文本中的下一个词或掩盖的词。预训练的模型可以捕捉到丰富的语言表示,包括语法、常见的短语、事实知识等。

预训练完成后,模型可以通过微调(Fine-tuning)来适应特定任务,如问答、文本分类或翻译。微调是在特定任务的数据集上进行的,目的是调整模型的参数,使其更好地适应这些任务的要求。

    • 检索增强生成(Retrieval-augmented Generation【RAG】):

RAG是一种在推理或回答问题时向模型提供额外信息的技术。这通常涉及将相关知识或上下文信息作为输入的一部分提供给模型。例如,如果模型需要回答一个关于特定历史事件的问题,可以将该事件的摘要或关键事实作为提示输入给模型。

RAG可以是在线查询外部知识库,也可以是预先准备的知识片段,如维基百科文章的摘要。这种方法允许模型在回答问题时利用外部知识,即使这些知识没有在预训练过程中直接学习到。

总结来说,预训练是让模型学习通用语言知识的过程,而RAG是在特定任务中为模型提供额外信息的方法。两者可以结合使用,以提高模型在特定任务上的性能。预训练提供了基础的语言理解能力,而知识提示则提供了任务相关的具体信息,帮助模型做出更准确的回答。

确实,预训练模型方案对于个人和大多数企业而言,无疑是一座高耸的技术山峰,其难度和成本令人望而却步。它不仅需要巨额的算力资源和专业人才的投入,还对数据的数量与质量有着近乎苛刻的要求。此外,模型的调优过程漫长而复杂,如同在迷雾中寻找正确的航道,需要耐心和细致的探索。对于那些有志于深入这一领域的小伙伴们,这无疑是一场智力和耐力的双重考验。

然而,在技术的海洋中,我们并非只有一条航道。面对预训练模型的重重困难,我们可以选择一条更为平坦的道路前行。正如在知识的海洋中,我们更倾向于在有参考资料的情况下答题,而不是在没有任何辅助的闭卷考试中挣扎。因此,我们转向了“知识提示”方案,这是一种更为灵活、成本更低的方法。

实际上,几乎所有的个人问答聊天系统都采用了“RAG”方案。这种方法通过提供关键信息或提示,引导模型生成更准确的回答。它不需要庞大的预训练模型,也不需要海量的数据和长时间的调优,而是通过巧妙的提示设计,让模型在有限的资源下发挥出最大的效能。这种方法的灵活性和实用性,使其成为了个人和企业构建智能问答系统的理想选择。

在技术的探索之路上,我们不必拘泥于一种方法,而是要根据实际情况,选择最适合自己的道路。无论是攀登预训练模型的高峰,还是选择“知识提示”的平坦小径,我们的目标始终是让技术更好地服务于人类。

from zhipuai import ZhipuAI

from info import key

client = ZhipuAI(api_key=key)  # 请填写您自己的APIKey
response = client.chat.completions.create(
    model="glm-4",  # 填写需要调用的模型名称
    messages=[
        {"role": "system", "content": "你是一个乐于解答各种问题的助手,你的任务是为用户提供专业、准确、有见地的建议。"},
        {"role": "user","content": "已知小头的爸爸叫做大头,大头的爸爸叫做老王。请问小头的爷爷叫什么名字?"},
    ],
    stream=True,
)
for chunk in response:
    print(chunk.choices[0].delta.content,end='')

所以,作弊的老王你慌了么?

向量数据库

设想一下,若您的著作洋洋洒洒,涵盖两千万字的浩瀚篇章,共分2000章。此刻,您好奇地探询:“第1000章究竟描绘了何种精彩?”显然,我们无法将整部巨著作为提示,一股脑儿地输入系统。这不仅会使理解过程变得繁琐,而且冗余的信息可能会扰乱分析,更不用说,过长的提示将触及模型的长度限制,可能导致信息截断,使得答案失去意义,同时,这也意味着成本的急剧上升,经费在无声中燃烧。理想的做法就是只要把第1000章检索出来,当做提示词喂给llm即可。

文档格式的无规则,大体量导致了不能使用常用的数据库存储检索,这时候就需要用到向量数据库(Vector Database),也叫矢量数据库,主要用来存储和处理向量数据。图像、文本及音视频这类非结构化数据,皆可通过特定的转换或嵌入学习技术,被映射为向量形式,进而存储于向量数据库之中。这一过程使得我们能够对这些数据进行基于相似性的搜索与检索。

简言之,向量数据库赋予了我们依据数据的语义或上下文关联性,而非传统的精确匹配或固定标准,来发现最接近或最相关信息的能力。

向量数据库的核心优势在于其卓越的存储与检索效率。通过采用先进的索引策略和向量搜索算法,它能够在高维数据海洋中迅速定位所需信息。尽管向量数据库以管理向量数据见长,它同样能够处理传统的结构化数据。在实际应用中,许多场景要求同时对向量和结构化字段进行筛选与检索,这对向量数据库而言,既是其功能的展现,也是对其性能的考验。

把一份文档存进向量数据库主要是分为四步(ps:上图来自langchain,下面的也会大量使用langchain的包,langchain后面再说)

    1. LOAD:加载文件
from langchain_community.document_loaders import TextLoader
loader = TextLoader("./index.md")
loader.load()
    1. SPLIT:分割,选择合适的分割器将大文本分割Documents成更小的文本块。这对于索引数据和将其传递到模型都很有用,因为大块更难搜索并且不适合模型的有限上下文窗口。
    2. ENBED:嵌入,需用用到向量化模型,把上面分割出来的小文本块处理为数字向量。嵌入技术之所以至关重要,是因为它能够捕捉并表达词汇或句子的深层语义。通过将单词映射到连续的实值向量空间,嵌入模型能够揭示单词之间的内在联系。这种映射是基于单词在大量文本语料库中的共现模式来学习的,即单词如何频繁地在相似的语境中一同出现。例如,如果两个单词在多种语境中常常相伴出现,它们的嵌入向量在多维空间中将彼此靠近。这种空间上的邻近性反映了它们在语义上的相似性,表明这两个单词共享相近的意义和语境关联。因此,嵌入向量不仅代表了单个单词的含义,还揭示了单词间复杂的语义关系,为自然语言处理任务提供了强大的语义理解基础。

一个好的embed模型对后续的检索至关重要,下面是一张目前对中文语义的模型排行榜

下面的模型都能在魔搭社区找到

    1. STORE:存储,常用向量数据库有的有Faiss,Milvus,vectorstore,通过pip命令安装即可

基于文档提问

把文档切分,向量化后存储在向量数据库中的目的就是为了在提问的时候,检索相似的文档,把文档组装成为提示器,给到llm推理。

送给llm的提示词结构大概的语法就是:

根据历史会话和新已知:{检索出来的文档}。回答以下问题:" + {问题}

  1. Retrieve :给定用户输入,使用Retriever(检索器)从存储中检索相关的分割文档,也叫召回。
  2. PROMPT:把文档和问题组装成为提示词,让llm使用包含问题和检索到的数据的提示生成答案

案例

📎招聘信息.docx进行嵌入并保存在Faiss中,根据提问的问题检索出对应的文档当做提示词,发送给llm模型回答

  1. 下载embedding模型
# 大文件通过git命令可能无法下载,或需自己去仓库中下载
git clone https://www.modelscope.cn/maidalun/bce-embedding-base_v1.git
  1. 让llm基于文档回答问题
import os

from docx import Document
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

# 第一步:割分文档
filepath = '/Users/yy/Downloads/招聘信息.docx'
# 打开Word文档
doc = Document(filepath)

# 读取文档内容
content = ' '.join([paragraph.text for paragraph in doc.paragraphs])

# 创建文本分割器
text_splitter = RecursiveCharacterTextSplitter(chunk_size=100, chunk_overlap=30)
# 分割文本
splits = text_splitter.split_text(content)

# 第二步:文档嵌入
# 获取文件名
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')

# 创建嵌入模型
embedding_path = "/Users/yy/Documents/yy_work/pywork/bce-embedding-base_v1"
embeddings = HuggingFaceEmbeddings(model_name=embedding_path)

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:
    # 创建索引
    index = FAISS.from_texts(
        texts=[doc 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个结果。
    result_list = index.similarity_search(user_input, k=3)
    llm_client.query(prompt=';'.join(doc.page_content for doc in result_list),
                     user_input=user_input)