掘金 阅读 ( ) • 2024-04-23 07:59

RAG实操教程: langchain+Milvus向量数据库创建你的本地知识库

本篇文章是 Milvus 向量数据库学习的总结篇,打造自己的知识库系统。

RAG是什么

RAGretrieval-augmented-generation的缩写,翻译为中文的意思就检索增强,以基于最新,最准确的数据建立LLM的语料知识库。

LLM 有哪些痛点

我们知道 LLM的知识库是通过现有的网络公开的数据作为数据源来训练的,现在公开的很多模型他们基于的训练数据会比我们现在网络上公开的数据早很多,那自然就会产生一种问题,网络上最新的数据和知识 LLM是不知道。还有一种情况就是很多企业他们对自己的数据的安全做的很好,也就是私有化数据(这些数据是有价值的,也是企业的立足之本)。这些数据网络上肯定是不存在,那自然 LLM也是不知道的。

我们在提问LLM 对于一些不知道的知识时候,LLM 很多时候是不知道如何回答问题的。甚至会对我们的问题进行胡诌随机回答,也就是瞎说。

为什么要用 RAG

如果使用预训练好的 LLM 模型,应用在某些情景下势必会有些词不达意的问题,例如问 LLM 你个人的信息,那么它会无法回答;这种情况在企业内部也是一样,例如使用 LLM 来回答企业内部的规章条款等。

这种时候主要有三种方式来让 LLM 变得更符合你的需求:

  1. Promt Enginerring: 输入提示来指导 LLM 产生所需回应。 例如常见的 In-context Learning,通过在提示中提供上下文或范例,来形塑模型的回答方式。 例如,提供特定回答风格的示例或包含相关的情境信息,可以引导模型产生更合适的答案。

  2. Fine tuning:

    这个过程包括在特定数据集上训练 LLM,使其响应更符合特定需求。 例如,一个EDA公司会使用其内部文件 verilog code 进行 Fine tuning ,使其能够更准确地回答关于企业内部问题。 但是 Fine tuning需要代表性的数据集且量也有一定要求,且 Fine tuning 并不适合于在模型中增加全新的知识或应对那些需要快速迭代新场景的情况。

  3. RAG (Retrieval Augmented Generation) : 结合了神经语言模型和撷取系统。 撷取系统从数据库或一组文件中提取相关信息,然后由语言模型使用这些信息来生成答案。 我们可以把 RAG 想像成给模型提供一本书或者是文档、教程,让它根据特定的问题去找信息。 此方法适用于模型需要整合实时、最新或非常特定的信息非常有用。 但RAG并不适合教会模型理解广泛的信息或学习新的语言、格式。

img

目前的研究已经表明,RAG 在优化 LLM 方面,相较于其他方法具有显著的优势。

主要的优势可以体现在以下几点:

  1. RAG 通过外部知识来提高答案的准确性,有效地减少了虚假信息,使得产生的回答更加准确可信。
  2. 使用撷取技术能够识别到最新的信息(用户提供),这使得 LLM 的回答能保持及时性
  3. RAG 引用信息来源是用户可以核实答案,因此其透明透非常高,这增强了人们对模型输出结果的信任。
  4. 透过获取与特定领域数据,RAG能够为不同领域提供专业的知识支持,定制能力非常高。
  5. 安全性和隐私管理方面,RAG 通过数据库来存储知识,对数据使用有较好控制性。 相较之下,经过 Fine tuning 的模型在管理数据存取权限方面不够明确,容易外泄,这对于企业是一大问题。
  6. 由于 RAG 不需更新模型参数,因此在处理大规模数据集时,经济效率方面更具优势。

不过虽然RAG有许多优势在,但这3种方法并不是互斥的,反而是相辅相成的。 结合 RAGFine tuning ,甚至 Promt Enginerring 可以让模型能力的层次性得增强。 这种协同作用特别在特定情境下显得重要,能够将模型的效能推至最佳。 整体过程可能需要经过多次迭代和调整,才能达到最佳的成效。 这种迭代过程涵盖了对模型的持续评估和改进,以满足特定的应用需求。

RAG 实际应用的过程

如何解决上面的问题

那如何让 LLM 知道这些最新/私有的数据的知识呢❓

那就是 RAG。通过将模型建立在外部知识来源的基础上来补充回答。从而提高 LLM生成回答的质量。

在基于 LLM实现的问答系统中使用 RAG 有三方面的好处:

  • 确保 LLM 可以回答最新,最准确的内容。并且用户可以访问模型内容的来源,确保可以检查其声明的准确性并最终可信。
  • 通过将 LLM建立在一组外部的、可验证的事实数据之上,该模型将信息提取到其参数中的机会更少。这减少了 LLM 泄露敏感数据或“幻觉”不正确或误导性信息的机会。
  • RAG 还减少了用户根据新数据不断训练模型并随着数据的变化更新训练参数的需要。通过这种方式企业可以减低相关财务成本。

现在所有基础模型使用的是 transformerAI 架构。它将大量原始数据转换为其基本结构的压缩表示形式。这种原始的表示基础模型可以适应各种任务,并对标记的、特定于领域的知识进行一些额外的微调。

不过仅靠微调很少能为模型提供在不断变化的环境中回答高度具体问题所需的全部知识,并且微调的时间周期还比较长。所以科学家们提出了 RAG,让 LLM 能够访问训练数据之外的信息。RAG 允许 LLM 建立在专门的知识体系之上,以更准确的回答问题,减低幻觉的产生。

下面我将 langchain+Milvus 向量数据库创建你的本地知识库

安装 langchain

使用下面的命令可以快速安装 langchain 相关的依赖

 ​
 pip install langchain
 ​
 pip install langchain-community # 默认安装
 ​
 pip install langchain-core # 默认安装
 ​
 pip install langchain-cli # LangChain 命令行界面
 ​
 pip install --upgrade --quiet pypdf # 安装pdf 加载器的依赖
 ​

文档加载器 pdf

在这篇文章中我们使用pdf作为我们的知识库文档。

加载文档的过程一般需要下面的步骤:

  • 解析文档(txtpdfhtmlurlxmlmarkdownjson)等等为字符串(langchain已经分装了几十种文档加载器,感兴趣的可以去参考官网)。
  • 将字符串拆分为适合模型的对话窗口的大小,称为 chunk,chunk的大小需要依据模型的会话窗口设定。
  • 保存拆分好的文档保存到向量数据库中。
  • 设计向量数据库的数据库、集合、字段,索引等信息。
  • 从向量数据库中检索需要的数据

这些步骤 langchain 已经给结合自己的工具连做好了封装,所以我们直接使用 langchain 来构建RAG

 from langchain_text_splitters import CharacterTextSplitter
 ​
 from langchain_community.document_loaders import PyPDFLoader
 ​
 file_path = 'CodeGeeX 模型API.pdf'
 ​
 # 初始化pdf 文档加载器
 loader = PyPDFLoader(file_path=file_path)
 # 将pdf中的文档解析为 langchain 的document对象
 documents = loader.load()
 # 将文档拆分为合适的大小
 text_splitter = CharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
 docs = text_splitter.split_documents(documents)

上面有两个参数需要注意下:

chunk_size=1000

表示拆分的文档的大小,也就是上面所说的要设置为多少合适取决于所使用LLM 的窗口大小。

  • 如果设置小了,那么我们一次查询的数据的信息量就会少,势必会导致信息的缺失。
  • 如果设置大了,一次检索出来的数据就会比较大,LLM 产生的token就会多,费用贵,信息不聚焦等问题。

chunk_overlap=100

这个参数表示每个拆分好的文档重复多少个字符串。

  • 为什么要重复:如果拆分后的chunk没有重复,很有可能会产生语义错误。

    比如这样一段文本:

    ....小米汽车的续航里程为700公里....如果文本正好从 700的哪里拆分开的话,那给人展示的时候就会产生酒究竟是700m还是700公里才歧义。

  • chunk_overlap如果设置比较大的值也不合适。这样每个文档的重复度就会很大,导致有用的信息就会减少,从而 LLM 回复的内容有可能缺少关键的信息。

Miluvs 向量数据库

关于 Milvui 可以参考我的前两篇文章

下面我们安装 pymilvus

 pip install --upgrade --quiet  pymilvus

如果你使用的不是 Miluvs 数据库,那也没关系,langchain 已经给我们分装了几十种向量数据库,你选择你需要的数据库即可。本文中我们是系列教程中一篇,所以我们使用 Miluvs 向量库。

Embedding model

image.png

这里需要明确的两个功能是:

  • embedding Model所做中工作就是将 imageDocumentAudio等信息向量化.
  • vectorBD 负责保存多维向量

我这里使用 AzureOpenAIEmbeddings 是个收费的模型。有开源的 embedding Model可以部署在本地使用,如果你的机器性能足够好。如果要本地部署可以参考 docker 部署 llama2 模型 。

这里我使用 AzureOpenAIEmbeddings, 相关配置我放到了 .env 文件中,并使用 dotenv 加载。

 # AZURE config
 AZURE_OPENAI_ENDPOINT=''
 AZURE_OPENAI_API_KEY=''
 OPENAI_API_VERSION=2024-03-01-preview
 AZURE_DEPLOYMENT_NAME_GPT35 = "gpt-35-turbo"
 AZURE_DEPLOYMENT_NAME_GPT4 = "gpt-4"
 AZURE_EMBEDDING_TEXT_MODEL = "text-embedding-ada-002"

这里各位可以依据自己的情况设定即可。

向量化+存储

上面已经说明了向量库以及embedding model的关系。我们直接使用 langchain 提供的工具连完成 embeddingstore

 # 初始化 embedding model
 from langchain_openai import AzureOpenAIEmbeddings
 embeddings = AzureOpenAIEmbeddings()
 from langchain_community.vectorstores import Milvus
 vector = Milvus.from_documents(
     documents=documents, # 设置保存的文档
     embedding=embeddings, # 设置 embedding model
     collection_name="book", # 设置 集合名称
     drop_old=True,
     connection_args={"host": "127.0.0.1", "port": "19530"},# Milvus连接配置
 )

执行完成上面的代码,我们就将pdf中文档内容保存到 vector_db 中。

image.png

字段 vector 就是保存的多维向量。

image.png

Milvus search

虽然现在我们还没有使用 LLM 的任何能力,但是我们已经可以使用 vector 的搜索功能了。

 query = "CodeGeeX模型API参数有那些?"
 docs = vector.similarity_search(query)
 print(docs)
 # 带score搜索
 query = "CodeGeeX模型API参数有那些?"
 docs = vector.similarity_search_with_score(query, k=2)
 print(docs)

similarity_searchsimilarity_search_with_score 的区别就是 similarity_search_with_score 搜索出来会带有一个 score 分值的字段,某些情况下这个 score 很有用。

langchain 不仅仅提供了基础的搜索能力,还有其他的搜索方法,感兴趣的可以去研究下。

RAG Chat

准备工作我们已经就绪,接下来我们使用langchain 构建我们的chat。

既然是聊天也就是我们跟模型的一问一答的形式来体现。这两年LLM的出现,关于 LLM 的知识里面我们估计最熟悉就是角色设定了。

  • 什么是角色设定:下面 OpenAI 给出的回答:

在大型语言模型(LLM)中,角色设定指的是为AI助手创建一个特定的人格或身份。这个设定包括AI助手的说话风格、知识领域、价值观、行为方式等各个方面。通过这些设定,AI助手可以扮演不同的角色,比如专业的客服、风趣幽默的聊天对象,或是特定领域的专家顾问。

角色设定可以让AI助手的回答更加符合特定的场景和用户的期望。比如一个扮演医生的AI助手,会用专业术语解释病情,给出严谨的建议;而一个扮演朋友的AI助手,会用轻松的语气聊天,给出生活化的提示。

此外,角色设定还可以帮助限定AI助手的行为边界,避免其做出不恰当或有害的回应。设定明确的角色定位,有助于AI助手更好地理解自己的身份和职责,从而提供更加合适和有帮助的回答。

总的来说,角色设定让AI助手的对话更加自然和人性化,让用户获得更好的使用体验。同时它也是引导AI助手行为、确保其安全可控的重要手段。

在 chat中我们同样也需要以及简单的 prompt:

 template = """You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. 
 ​
 Question: {question} 
 ​
 Context: {context} 
 ​
 Answer:
 """

这个prompt中很明显我们设定了两个变量 question, context

question: 这个会在后面被替换为用户的输入,也就是用户的问题。

context: 这个变量我们在后面会替换为向量检索出来的内容。

请思考下: 我们最后提供给LLm的内容只是用户的问题呢还是问题连带内容一起给到LLM?

chat chain

基于上面的内容我们基本的工作已经完成,下面就是我们基于 langchain构建chat

 import os
 from langchain_core.prompts import ChatPromptTemplate
 prompt = ChatPromptTemplate.from_template(template)
 print(prompt)
 ​
 # 加载chat model
 from langchain_openai import AzureChatOpenAI
 from langchain_core.runnables import RunnableParallel,RunnablePassthrough
 from langchain_core.output_parsers import StrOutputParser
 llm = AzureChatOpenAI(
     azure_deployment=os.environ.get('AZURE_DEPLOYMENT_NAME_GPT35')
 )
 ​
 retriever = vector.as_retriever()
 ​
 chain = (
     RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
     | prompt
     | llm
     | StrOutputParser()
 )

对于初学者可能有个问题就是:为什么这里有个 AzureChatOpenAI() 的实例 llm

这是个好问题,对于初学者会被各种 LLM 搞晕😵‍💫。

  • AzureOpenAIEmbeddings() 这是一个负责将文本向化话的 model
  • AzureChatOpenAI() 是一个 chat 模型。负责聊天的 model。

基于 langchain 的链式调用构建 chat

 chain = (
     RunnableParallel({"context": retriever, "question": RunnablePassthrough()})
     | prompt
     | llm
     | StrOutputParser()
 )

这里看到 prompt中的两个变量context, question 会被替换。

为什么我们要写变量在 prompt中?

  • 工程化:我们在做LLM相关的工作最重要的就是prompt工程。这也是个重要的话题后面再说
  • 灵活:

测试

示例一:

 question = "CodeGeeX模型API参数有那些?"
 answer = chain.invoke(question)
 print(answer)

输出:

 CodeGeeX模型API的参数包括:
 - prompt:用户输入的提示词
 - max_tokens:模型输出最大tokens
 - temperature:采样温度,控制输出的随机性
 - top_p:另一种采样方法,模型考虑具有top_p概率质量tokens的结果
 - stream:用于同步调用时,控制模型生成内容的返回方式,可以设置为false或true

示例二:

 question = "请给一个chat模型非流式请求示例"
 answer = chain.invoke(question)
 print(answer)

输出:

 流式请求示例:
 curl --location 'http://{ip}/prod/model/api/infillingStreaming' \
 --header 'Content-Type: application/json' \
 --data '{
     "model": "codegeex",
     "prompt": "package problem1;\nclass Solution{\n    public int \nremoveDuplicates(int[] nums) {\n        int cnt = 1;\n        for (int i = 1; \ni < nums.length; ++i)\n            if (nums[i] != nums[i - 1]) {\n             \n   nums[cnt] = nums[i];\n                ++cnt;\n            }\n        return \ncnt;\n    }\n}",
     "max_tokens": 1024,
     "temperature": 0.2,
     "top_p": 0.95,
     "stream": true
 }'

对比pdf中的内容,很明显这个结果就是对的:

image-20240422171050777

总结:

本文主要是 Milvus 向量数据实战总结。

  • LLM 痛点以及解决方案
  • RAG 是什么,为什么选用RAG。
  • langchain 文档加载器,embedding modelchat model
  • 文档拆分的注意点,embedding modelchat model区别。
  • chat 示例代码。

相关文章