RAG实战:用Gemini和Langchain打造你的PDF对话机器人

4

在人工智能领域,检索增强生成(RAG)技术正逐渐成为一种强大的工具,它允许大型语言模型(LLMs)访问外部知识库,从而实现更加精准和全面的信息检索与生成。本文将深入探讨如何利用RAG方法,结合谷歌的Gemini模型和Langchain框架,快速构建一个能够与PDF文件进行交互的智能机器人。选择Gemini模型的原因在于其API目前提供免费调用,这为广大的开发者提供了一个低成本的实验平台。RAG技术的核心在于其能够将检索模型与生成模型相结合,从而实现对私有或专有数据源信息的有效利用。一个典型的RAG检索过程包含多个关键步骤,例如,从用户问题出发,检索相关信息,并最终生成用户友好的答案。

一个典型的RAG系统由两个主要组件构成:检索器组件和生成器组件。检索器组件负责从外部数据源(如PDF、Word、Text等)检索与用户问题相关的信息,并将其提供给LLM,以便LLM能够更好地回答问题。生成器组件则利用检索到的相关信息,生成准确、完整且用户友好的答案。

RAG检索过程

本文将重点介绍如何利用Langchain框架的父文档检索器来构建检索器组件,以及如何利用谷歌的Gemini大模型来构建生成器组件。对于那些希望快速上手机器人应用程序开发的读者,可以参考之前撰写的关于使用Python快速开发各种聊天机器人应用的博客。此外,对于不熟悉谷歌Gemini模型的读者,可以查阅之前撰写的关于谷歌Gemini API应用的基础应用博客,以便快速了解Gemini API的使用方法。

在本文的最后,将分享完整的代码,供读者在此基础上进行完善,开发出符合自身需求的机器人。

环境配置

在开始之前,需要配置必要的开发环境。这里主要使用panellangchain这两个Python包。可以使用pip命令进行安装:

pip install google-generativeai
pip install panel
pip install langchain
pip install chroma

如果在安装过程中遇到任何错误,请根据错误信息安装其他必要的Python包。

组件介绍

本文将使用谷歌Gemini模型组件以及基于Langchain的检索器组件,其中包括文档分割器组件、父文档检索器组件和向量数据库组件。此外,还需要使用Python的Web框架组件Panel。下面是这些组件的Python包导入:

import tempfile
import panel as pn
import param
from langchain.document_loaders import PyPDFLoader
from langchain.embeddings import HuggingFaceBgeEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.retrievers import ParentDocumentRetriever
from langchain.storage import InMemoryStore
from langchain.vectorstores import Chroma
from panel_chat_examples import EnvironmentWidgetBase
import google.generativeai as genai

这里需要强调的是,使用的是谷歌原生的Gemini模型组件,而非Langchain封装的Gemini组件。这是因为Langchain的Gemini组件存在一些Bug,例如无法设置谷歌模型的安全策略,导致机器人在回答问题时经常出现安全性问题。这可能是由于Langchain的Gemini组件仍处于早期版本,预计后续会逐步完善。

功能模块介绍

Embedding模型的选择

Embedding模型在RAG系统中扮演着至关重要的角色,它负责将文本数据转换为向量表示,从而使得计算机能够理解和处理文本信息。在选择Embedding模型时,需要根据具体的应用场景和数据特点进行权衡。

考虑到本次实验使用的是中文内容的文档,因此选择了BAAI的"bge-small-zh-v1.5"模型。如果使用的是英文PDF文档,则可以将Embedding模型切换为BAAI的"bge-small-en-v1.5"模型:

bge_embeddings = HuggingFaceBgeEmbeddings(
    model_name="BAAI/bge-small-zh-v1.5", cache_folder="D:\models"
)

cache_folder参数用于指定Embedding模型存储的文件夹。当首次执行机器人程序时,系统会自动从Hugging Face的网站下载Embedding模型,并将其存储到默认的cache_folder文件夹下(通常是C盘)。为了避免C盘空间不足,可以指定cache_folder参数的路径,将Embedding模型存储在其他磁盘上。

Gemini模型的安全策略

Gemini模型在回答用户问题时,采用了一套严格的内容安全审查机制,以防止出现违反道德伦理、色情暴力等内容。由于本次实验使用的PDF文档是《阿凡提故事大全.pdf》,它是一个中文文档,当机器人在回答问题时,有时会出现内容违规的提示。这可能是由于Gemini模型的默认安全审查策略的阈值设置过高导致的。因此,将安全审查策略的阈值调整为“无”后,就不再出现违规提示:

generation_config = {
    "temperature": 0.0,
    "top_p": 1,
    "top_k": 1,
    "max_output_tokens": 2048,
}

safety_settings = [
    {
        "category": "HARM_CATEGORY_HARASSMENT",
        "threshold": "BLOCK_NONE",
    },
    {
        "category": "HARM_CATEGORY_HATE_SPEECH",
        "threshold": "BLOCK_NONE",
    },
    {
        "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
        "threshold": "BLOCK_NONE",
    },
    {
        "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
        "threshold": "BLOCK_NONE",
    },
]

这里设置了模型参数generation_config和安全策略参数safety_settings。在模型参数中,将temperature设置为0。temperature参数的取值范围为0到1,参数值越低,LLM给出的答案越精准,参数值越高,给出的答案变化性越大。为了让LLM不要产生多样化的结果,应尽量调低temperature的值,可以设置为0或0.1。另外,将所有安全策略的阈值都设置为BLOCK_NONE,即不做安全性审查。

父文档检索器

父文档检索器是Langchain框架中的一个重要组件,它能够将大型文档分割成更小的块,并建立父子关系,从而实现更精准的检索。

下面是在Panel中定义父文档检索器的一些组件:

@pn.cache(ttl=TTL)
def _get_docs(pdf):
    # load documents
    with tempfile.NamedTemporaryFile("wb", delete=False) as f:
        f.write(pdf)
    file_name = f.name
    loader = PyPDFLoader(file_name)
    docs = loader.load()
    return docs


@pn.cache(ttl=TTL)
def _get_parent_splitter():
    return RecursiveCharacterTextSplitter(chunk_size=1000)


@pn.cache(ttl=TTL)
def _get_child_splitter():
    return RecursiveCharacterTextSplitter(chunk_size=400)


@pn.cache(ttl=TTL)
def _get_MemoryStore():
    return InMemoryStore()


@pn.cache(ttl=TTL)
def _get_vector_db():
    vectorstore = Chroma(
        collection_name="split_parents", embedding_function=bge_embeddings
    )
    return vectorstore


@pn.cache(ttl=TTL)
def _get_retriever(pdf, number_of_chunks: int):
    docs = _get_docs(pdf)
    vectorstore = _get_vector_db()
    store = _get_MemoryStore()
    child_splitter = _get_child_splitter()
    parent_splitter = _get_parent_splitter()
    # 创建父文档检索器
    retriever = ParentDocumentRetriever(
        vectorstore=vectorstore,
        docstore=store,
        child_splitter=child_splitter,
        parent_splitter=parent_splitter,
        search_kwargs={"k": number_of_chunks},
    )
    # 添加文档集
    retriever.add_documents(docs)
    return retriever

创建Gemini模型对象

下面是在Panel中定义Gemini模型对象和模型生成内容的方法:

@pn.cache(ttl=TTL)
def _get_model():
    # 创建gemini model
    model = genai.GenerativeModel(
        model_name="gemini-pro",
        generation_config=generation_config,
        safety_settings=safety_settings,
    )

    return model



def _get_response(contents):
    retriever = _get_retriever(state.pdf, state.number_of_chunks)
    model = _get_model()

    relevant_docs = retriever.get_relevant_documents(contents)
    contexts = "\n\n".join([w.page_content for w in relevant_docs])

    # prompt模板
    template = f"""请根据下面给出的上下文来回答下面的问题,并给出完整的答案:
上下文:{contexts}

问题: {contents}
"""
    response = model.generate_content(template)

    chunks = []
    for chunk in relevant_docs:
        name = f"Chunk {chunk.metadata['page']}"
        content = chunk.page_content
        chunks.insert(0, (name, content))
    return response.text, chunks

创建Panel页面组件

为了构建用户友好的交互界面,需要创建Panel的页面组件,并设置环境变量等全局变量,以便设置谷歌的API Key和回调函数。这里使用Panel的chat_interface作为页面聊天组件:

class EnvironmentWidget(EnvironmentWidgetBase):
    GOOGLE_API_KEY: str = param.String()


class State(param.Parameterized):
    pdf: bytes = param.Bytes()
    number_of_chunks: int = param.Integer(default=2, bounds=(1, 5), step=1)


environ = EnvironmentWidget()
state = State()

pdf_input = pn.widgets.FileInput.from_param(state.param.pdf, accept=".pdf", height=50)
text_input = pn.widgets.TextInput(placeholder="First, upload a PDF!")


def _get_validation_message():
    pdf = state.pdf
    google_api_key = environ.GOOGLE_API_KEY
    if not pdf and not google_api_key:
        return "请在左侧侧边栏中输入谷歌api key 然后上传PDF文件!"
    if not pdf:
        return "请先上传pdf 文件"
    if not google_api_key:
        return "请先输入谷歌api key"
    genai.configure(api_key=google_api_key, transport='rest')
    return ""


def _send_not_ready_message(chat_interface) -> bool:
    message = _get_validation_message()

    if message:
        chat_interface.send({"user": "System", "object": message}, respond=False)
    return bool(message)


async def respond(contents, user, chat_interface):
    if _send_not_ready_message(chat_interface):
        return
    if chat_interface.active == 0:
        chat_interface.active = 1
        chat_interface.active_widget.placeholder = "在这里输入您的问题!"
        yield {"user": "Gemini", "object": "现在可以开始和pdf对话了!"}
        return

    response, documents = _get_response(contents)
    pages_layout = pn.Accordion(*documents, sizing_mode="stretch_width", max_width=800)
    answers = pn.Column(response, pages_layout)

    yield {"user": "Gemini", "object": answers}


chat_interface = pn.chat.ChatInterface(
    callback=respond,
    sizing_mode="stretch_width",
    widgets=[pdf_input, text_input],
    disabled=True,
)


@pn.depends(state.param.pdf, environ.param.GOOGLE_API_KEY, watch=True)
def _enable_chat_interface(pdf, google_api_key):
    if pdf and google_api_key:
        chat_interface.disabled = False
    else:
        chat_interface.disabled = True


_send_not_ready_message(chat_interface)

## Wrap the app in a nice template

template = pn.template.BootstrapTemplate(
    title="PDF文档对话机器人",
    sidebar=[environ, state.param.number_of_chunks],
    main=[chat_interface],
)
template.servable()

机器人使用方法介绍

要在命令行窗口中执行机器人的源代码程序,可以使用以下命令:

panel serve gemini_pdf_bot.py

命令行执行机器人程序

然后在浏览器中打开访问机器人的链接。完成输入谷歌API Key和上传PDF文件后,就可以开始和机器人聊天了:

机器人聊天界面

总结

本文详细介绍了如何开发一个简单的RAG系统,即基于PDF文档问答的机器人应用。在这个过程中,应用了Langchain的父文档检索策略、Panel的页面聊天组件chat_interface以及谷歌的Gemini大模型。希望本文的内容能够对大家学习RAG和聊天机器人程序有所帮助。

通过本文,读者可以了解到RAG技术的基本原理和应用方法,并能够利用Gemini模型和Langchain框架快速构建一个能够与PDF文件进行交互的智能机器人。这种机器人可以应用于各种场景,例如智能客服、文档分析、知识问答等,为人们提供更加便捷和高效的信息服务。