动手学大模型应用开发,第6天:前后端搭建

第一章 项目代码简析

根据前面讲解的内容,我们逐步学习了如何调用不同的 LLM API,如何处理知识库文档搭建向量数据库,再如何设计 Prompt 搭建基于数据库的检索问答链。现在,我们可以回顾前面学过的所有内容,结合项目设计思路,将上述学习内容实现成代码,并按项目层次封装,来支持后续部署的调用。在这一章,我们会结合项目代码与前面学习的内容,讲解我们是如何封装项目代码以接口的形式为部署层提供核心功能的。

一、LLM 调用

基于前文内容,我们可以将百度文心、讯飞星火、智谱 GLM 等国内大模型接口封装成自定义的 LLM,然后接入到 LangChain 架构中。由于我们的项目需要将上述三种大模型接口都进行封装来统一到我们的项目框架中,为了项目的简洁,我们首先定义了一个自定义 LLM 的基类:

python 复制代码
class Self_LLM(LLM):
    # 自定义 LLM
    # 继承自 langchain.llms.base.LLM
    # 原生接口地址
    url : str =  None
    # 默认选用 GPT-3.5 模型,即目前一般所说的百度文心大模型
    model_name: str = "gpt-3.5-turbo"
    # 访问时延上限
    request_timeout: float = None
    # 温度系数
    temperature: float = 0.1
    # API_Key
    api_key: str = None
    # 必备的可选参数
    model_kwargs: Dict[str, Any] = Field(default_factory=dict)

Self_LLM 类定义在 /llm/self_llm.py 文件中,抽出调用三种大模型共同需要的参数(例如模型调用网址、使用模型名字、温度系数、API_Key 等)。在 Self_LLM 类基础上,我们分别继承了三个大模型 API 的自定义 LLM:Wenxin_LLM(/llm/wenxin_llm.py)、Spark_LLM(/llm/spark_llm.py)、ZhipuAILLM(/llm/zhipuai_llm.py),三个子类分别定义了本 API 所独有的参数,并基于 API 调用方式重写了 _call 方法。如果对封装方法有所疑问,可以详细阅读第二章,我们有讲解每一种 API 的调用方法。封装的自定义 LLM,实则就是将各自的调用方法按照给定的结构重写到 _call 方法中。

通过如上封装,在上一层检索问答链搭建时可以直接调用各个自定义 LLM 类,从而无需关注不同 API 调用的细节。

同时,为调用方便,我们也封装了统一的 get_completion 函数(/llm/call_llm.py):

python 复制代码
def get_completion(prompt :str, model :str, temperature=0.1,api_key=None, 
                    secret_key=None, access_token=None, appid=None, 
                    api_secret=None, max_tokens=2048) -> str

该函数将四种模型 API 的原生接口封装在一起,旨在通过这一个函数来调用所有的模型。在这个函数内部,我们解析了 model 参数的值,分别映射到不同的 API 调用函数中:

python 复制代码
    if model in ["gpt-3.5-turbo", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-0613", "gpt-4", "gpt-4-32k"]:
        return get_completion_gpt(prompt, model, temperature, api_key, max_tokens)
    elif model in ["ERNIE-Bot", "ERNIE-Bot-4", "ERNIE-Bot-turbo"]:
        return get_completion_wenxin(prompt, model, temperature, api_key, secret_key)
    elif model in ["Spark-1.5", "Spark-2.0"]:
        return get_completion_spark(prompt, model, temperature, api_key, appid, api_secret, max_tokens)
    elif model in ["chatglm_pro", "chatglm_std", "chatglm_lite"]:
        return get_completion_glm(prompt, model, temperature, api_key, max_tokens)
    else:
        return "不正确的模型"

对于其中映射的每一个子函数(包括 get_completion_gpt、get_completion_wenxin 等),我们都以类似于第二章讲解的方式进行了封装。在后续调用中,可以直接使用 get_completion 函数,通过传入不同的模型参数和 API_KEY 认证,可以隐藏掉 API 调用细节。

二、数据库构建

我们将在第四章中讲解过的构建项目数据库方法封装成 create_db 函数,函数中封装了各种文件类型的源文件处理方法和最终向量数据库的构建。如果本地没有构建过向量数据库,可以直接调用该方法:

python 复制代码
def create_db(files=DEFAULT_DB_PATH, persist_directory=DEFAULT_PERSIST_PATH, embeddings="openai"):
    """
    该函数用于加载源数据文件,切分文档,生成文档的嵌入向量,创建向量数据库。

    参数:
    file: 存放文件的路径。
    embeddings: 用于生产 Embedding 的模型

    返回:
    vectordb: 创建的数据库。
    """

在该函数内部,我们构造了一个文件加载映射函数,该函数会针对源文件类型分配不同的文件加载器,从而实现对不通过文件的处理:

python 复制代码
def file_loader(file, loaders):
    # 对于多种文档的 FileLoader 映射
    if isinstance(file, tempfile._TemporaryFileWrapper):
        file = file.name
    if not os.path.isfile(file):
        [file_loader(os.path.join(file, f), loaders) for f in  os.listdir(file)]
        return
    file_type = file.split('.')[-1]
    if file_type == 'pdf':
        loaders.append(PyMuPDFLoader(file))
    elif file_type == 'md':
        loaders.append(UnstructuredMarkdownLoader(file))
    elif file_type == 'txt':
        loaders.append(UnstructuredFileLoader(file))
    return

同时,针对已构造向量数据库需要调用的情况,我们也封装了 get_vectordb 函数,在检索问答链中只需直接调用该函数获取已构建的数据库即可:

python 复制代码
def get_vectordb(file_path:str=None, persist_path:str=None, embedding = "openai",embedding_key:str=None):
    """
    返回向量数据库对象
    输入参数:
    question:
    llm:
    vectordb:向量数据库(必要参数),一个对象
    template:提示模版(可选参数)可以自己设计一个提示模版,也有默认使用的
    embedding:可以使用zhipuai等embeddin,不输入该参数则默认使用 openai embedding,注意此时api_key不要输错
    """

三、检索问答链

基于 LLM 层与 Database 层的封装,我们可以在应用层搭建自定义的检索问答链,封装实现项目的核心功能。

首先我们封装了一个 LLM 映射函数,该函数会根据传入 model 参数的不同映射到不同的 LLM 对象,从而实现不同 API 来源 LLM 的切换:

python 复制代码
def model_to_llm(model:str=None, temperature:float=0.0, appid:str=None, api_key:str=None,Spark_api_secret:str=None,Wenxin_secret_key:str=None):
        """
        星火:model,temperature,appid,api_key,api_secret
        百度文心:model,temperature,api_key,api_secret
        智谱:model,temperature,api_key
        OpenAI:model,temperature,api_key
        """
        if model in ["gpt-3.5-turbo", "gpt-3.5-turbo-16k-0613", "gpt-3.5-turbo-0613", "gpt-4", "gpt-4-32k"]:
            if api_key == None:
                api_key = parse_llm_api_key("openai")
            llm = ChatOpenAI(model_name = model, temperature = temperature , openai_api_key = api_key)
        elif model in ["ERNIE-Bot", "ERNIE-Bot-4", "ERNIE-Bot-turbo"]:
            if api_key == None or Wenxin_secret_key == None:
                api_key, Wenxin_secret_key = parse_llm_api_key("wenxin")
            llm = Wenxin_LLM(model=model, temperature = temperature, api_key=api_key, secret_key=Wenxin_secret_key)
        elif model in ["Spark-1.5", "Spark-2.0"]:
            if api_key == None or appid == None and Spark_api_secret == None:
                api_key, appid, Spark_api_secret = parse_llm_api_key("spark")
            llm = Spark_LLM(model=model, temperature = temperature, appid=appid, api_secret=Spark_api_secret, api_key=api_key)
        elif model in ["chatglm_pro", "chatglm_std", "chatglm_lite"]:
            if api_key == None:
                api_key = parse_llm_api_key("zhipuai")
            llm = ZhipuAILLM(model=model, zhipuai_api_key=api_key, temperature = temperature)
        else:
            raise ValueError(f"model{model} not support!!!")
        return llm

在该映射器的基础上,我们构建了我们的自定义检索问答链。我们分别构建了两种检索问答链,QA_chain_self(/qa_chain/QA_chain_self.py)和 Chat_QA_chain_self(/qa_chain/Chat_QA_chain_self.py),分别对应普通的检索问答链和加入历史会话的检索问答链。两种自定义检索问答链内部实现细节类似,只是调用了不同的 LangChain 链。

在自定义检索问答链内部,我们首先在构造函数中定义了长期参数的赋值,并且调用了 LLM 映射器和数据库构建(或加载)函数,从而实现了调用问答链的所有准备工作。然后我们定义了 answer 函数,该函数会对用户的问题调用检索问答链并输出回答,同时,在每一次调用 answer 的时候我们都可以动态改变温度系数和 top_k 参数:

python 复制代码
    def answer(self, question:str=None, temperature = None, top_k = 4):
        """"
        核心方法,调用问答链
        arguments: 
        - question:用户提问
        """

        if len(question) == 0:
            return ""
        
        if temperature == None:
            temperature = self.temperature
            
        if top_k == None:
            top_k = self.top_k

        result = self.qa_chain({"query": question, "temperature": temperature, "top_k": top_k})
        return result["result"]   

在完成上述封装之后,我们无需关注底层细节,只需实例化一个自定义检索问答链对象,并调用其 answer 方法即可实现本项目的核心功能。同时,实例化时无需关注不同 API 的调用差异,直接传入需要调用的模型参数即可。

后续的服务层部署,我们会直接在上述封装代码的基础上进行调用,即直接调用自定义检索问答链,而不再从头实现全部过程。

第二章、Gradio 的介绍与前端界面的搭建 💬

我们对知识库和LLM已经有了基本的理解,现在是将它们巧妙地融合并打造成一个富有视觉效果的界面的时候了。这样的界面不仅对操作更加便捷,还能便于与他人分享。

Gradio 是一种快速便捷的方法,可以直接在 Python 中通过友好的 Web 界面演示机器学习模型 。在本课程中,我们将学习如何使用它为生成式人工智能应用程序构建用户界面。在构建了应用程序的机器学习或生成式人工智能后,如果你想构建一个demo给其他人看,也许是为了获得反馈并推动系统的改进,或者只是因为你觉得这个系统很酷,所以想演示一下:Gradio 可以让您通过 Python 接口程序快速实现这一目标,而无需编写任何前端、网页或 JavaScript 代码。 加载 HF API 密钥和相关 Python 库

一、Gradio 简介

Gradio 可以包装几乎任何 Python 函数为易于使用的用户界面。

常用的基础模块构成如下:

  • 应用界面:gradio.Interface(简易场景), gradio.Blocks(定制化场景)
  • 输入输出:gradio.Image(图像), gradio.Textbox(文本框), gradio.DataFrame(数据框), gradio.Dropdown(下拉选项), gradio.Number(数字), gradio.Markdown(Markdown), gradio.Files(文件)
  • 控制组件:gradio.Button(按钮)
  • 布局组件:gradio.Tab(标签页), gradio.Row(行布局), gradio.Column(列布局)

大部分功能模块都可以通过以下三个参数进行初始化:

  • fn:包装的函数
  • inputs:输入组件类型,(例如:"text"、"image)
  • ouputs:输出组件类型,(例如:"text"、"image)

1.1 gradio.Interface() 搭建界面

python 复制代码
# 导入所需的库
import gradio as gr  # 用于创建 Web 界面
import os  # 用于与操作系统交互,如读取环境变量

# 定义一个函数来根据输入生成文本
def generate(input, temperature):
    """
    该函数用于根据输入生成文本。

    参数:
    input: 输入内容。
    temperature: LLM 的温度系数。

    返回:
    output: 生成的文本。
    """
    # 使用预定义的 client 对象的 predict 方法,从输入生成文本
    # slider 的值限制生成的token的数量
    output = llm.predict(input, temperature=temperature)
    return output  # 返回生成的文本

# 创建一个 Web 界面
# 输入:一个文本框和一个滑块
# 输出:一个文本框显示生成的文本
demo = gr.Interface(
    fn=generate, 
    inputs=[
        gr.Textbox(label="Prompt"),  # 文本输入框
        gr.Slider(label="Temperature", value=0,  maximum=1, minimum=0)  # 滑块用于选择模型的 temperature
    ], 
    outputs=[gr.Textbox(label="Completion")],  # 显示生成文本的文本框
    title="Chat Robot",  # 界面标题
    description="Local Knowledge Base Q&A with llm",  # 界面描述
    # allow_flagging="never", 
)

# 关闭可能已经启动的任何先前的 gradio 实例
gr.close_all()

# 启动 Web 界面
# 使用环境变量 PORT1 作为服务器的端口号
# demo.launch(share=True, server_port=int(os.environ['PORT1']))
demo.launch() # 直接启动页面
python 复制代码
Running on local URL:  http://127.0.0.1:7860

To create a public link, set `share=True` in `launch()`.
  • fn=generate: 这是用于处理输入的函数,即文本生成函数 generate。
  • inputs=[ gr.Textbox(label="Prompt"), gr.Slider(label="Temperature", value=0, maximum=1, minimum=0) ]: 这定义了模型的输入。
    使用 gr.Textbox 部件来以文本框的形式显示输入的内容描述,label 参数设置了输入部件的标签为 prompt。
    使用 gr.Slider 部件以滑动条的形式来显示输入的内容描述,label 参数设置了输入部件的标签为 temperature。
  • outputs=[gr.Textbox(label="Caption")]: 这定义了输出部分。使用 gr.Textbox 部件来显示生成的内容描述,label 参数设置了输出部件的标签。
  • title="Chat Robot": 这是界面的标题,将显示在界面的顶部。
  • description="Local Knowledge Base Q&A with llm ": 这是界面的描述,提供有关界面功能的更多信息。
  • allow_flagging="never": 这设置了不允许标记内容,确保不会显示标记不恰当内容的选项。

通过 demo.launch() 启动整个可视化前端界面。

我们可以对demo.launch 中的参数进行配置:

demo.launch(share=True, server_port=8080))

  • share=True 表示生成外部可访问的链接
  • server_port=8080 表示运行的端口

这样,外部的用户也可以通过生成的链接直接访问我们的界面。

现在我们已经搭建了一个非常简单的 Gradio 界面,它有一个文本框输入和一个输出。我们已经可以非常简单地向 LLM 提问。但我们还是不能对话,因为如果你再问一个后续问题,它就无法理解或保留上下文。

因此,基本上我们要做的是,向模型发送我们之前的问题、它自己的回答以及后续问题。但建立所有这些都有点麻烦。这就是 Gradio 聊天机器人组件的作用所在,因为它允许我们简化向模型发送对话历史记录的过程。

因此,我们要解决这个问题。为此,我们将引入一个新的 Gradio 组件--Gradio Chatbot。

二、使用 gradio.Chatbot() 来助力聊天!

让我们开始使用 Gradio Chatbot 组件。这里实例化了一个带有文本框 prompt 和提交按钮的 Gradle ChatBot 组件,是一个非常简单的用户界面。但我们现在还不是在和 LLM 聊天。

我们必须格式化聊天 prompt。此处正在定义这个格式化聊天 prompt 函数。 在这里,我们要做的就是使其包含聊天历史记录,这样 LLM 就能知道上下文。 但这还不够。我们还需要告诉它,哪些信息来自用户,哪些信息来自 LLM 本身,也就是我们正在调用的助手。 因此,我们设置了格式聊天 prompt 功能,在聊天记录的每一轮中,都包含一条用户信息和一条助手信息,以便我们的模型能准确回答后续问题。 现在,我们要将格式化的 prompt 传递给我们的 API。

相比 Interface,Blocks 提供了一个低级别的 API,用于设计具有更灵活布局和数据流的网络应用。Blocks 允许控制组件在页面上出现的位置,处理复杂的数据流(例如,输出可以作为其他函数的输入),并根据用户交互更新组件的属性可见性。可以定制更多组件。

python 复制代码
# 定义一个函数,用于格式化聊天 prompt。
def format_chat_prompt(message, chat_history):
    """
    该函数用于格式化聊天 prompt。

    参数:
    message: 当前的用户消息。
    chat_history: 聊天历史记录。

    返回:
    prompt: 格式化后的 prompt。
    """
    # 初始化一个空字符串,用于存放格式化后的聊天 prompt。
    prompt = ""
    # 遍历聊天历史记录。
    for turn in chat_history:
        # 从聊天记录中提取用户和机器人的消息。
        user_message, bot_message = turn
        # 更新 prompt,加入用户和机器人的消息。
        prompt = f"{prompt}\nUser: {user_message}\nAssistant: {bot_message}"
    # 将当前的用户消息也加入到 prompt中,并预留一个位置给机器人的回复。
    prompt = f"{prompt}\nUser: {message}\nAssistant:"
    # 返回格式化后的 prompt。
    return prompt

# 定义一个函数,用于生成机器人的回复。
def respond(message, chat_history):
    """
    该函数用于生成机器人的回复。

    参数:
    message: 当前的用户消息。
    chat_history: 聊天历史记录。

    返回:
    "": 空字符串表示没有内容需要显示在界面上,可以替换为真正的机器人回复。
    chat_history: 更新后的聊天历史记录
    """
    # 调用上面的函数,将用户的消息和聊天历史记录格式化为一个 prompt。
    formatted_prompt = format_chat_prompt(message, chat_history)
    # 使用llm对象的predict方法生成机器人的回复(注意:llm对象在此代码中并未定义)。
    bot_message = llm.predict(formatted_prompt,
                                max_new_tokens=1024,
                                stop_sequences=["\nUser:", ""])
    # 将用户的消息和机器人的回复加入到聊天历史记录中。
    chat_history.append((message, bot_message))
    # 返回一个空字符串和更新后的聊天历史记录(这里的空字符串可以替换为真正的机器人回复,如果需要显示在界面上)。
    return "", chat_history

# 下面的代码是设置Gradio界面的部分。

# 使用Gradio的Blocks功能定义一个代码块。
with gr.Blocks() as demo:
    # 创建一个Gradio聊天机器人组件,设置其高度为240。
    chatbot = gr.Chatbot(height=240) 
    # 创建一个文本框组件,用于输入  prompt。
    msg = gr.Textbox(label="Prompt")
    # 创建一个提交按钮。
    btn = gr.Button("Submit")
    # 创建一个清除按钮,用于清除文本框和聊天机器人组件的内容。
    clear = gr.ClearButton(components=[msg, chatbot], value="Clear console")

    # 设置按钮的点击事件。当点击时,调用上面定义的respond函数,并传入用户的消息和聊天历史记录,然后更新文本框和聊天机器人组件。
    btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
    # 设置文本框的提交事件(即按下Enter键时)。功能与上面的按钮点击事件相同。
    msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot]) 

# 关闭所有已经存在的 Gradio 实例。
gr.close_all()
# 启动新的 Gradio 应用,设置分享功能为 True,并使用环境变量 PORT1 指定服务器端口。
# demo.launch(share=True, server_port=int(os.environ['PORT1']))
demo.launch()
python 复制代码
Closing server running on port: 7860
Running on local URL:  http://127.0.0.1:7860

To create a public link, set `share=True` in `launch()`.

现在,我们的问答机器人可以回答后续问题了。 我们可以看到,我们向它发送了上下文。我们向它发送了信息,然后要求它完成。一旦我们进入另一个迭代循环,我们就会向它发送我们的整个上下文,然后要求它完成。这很酷。但是,如果我们一直这样迭代下去,那么模型在一次对话中所能接受的信息量就会达到极限,因为我们总是给它越来越多的之前对话的内容。

这里,我们创建了一个简单但功能强大的用户界面,用于与LLM聊天。如果需要进一步Gradio 所能提供的最佳功能,我们可以创建一个包含更多功能的用户界面。

三、 接入本地知识库进行回答

3.1 绑定已封装的函数

现在我们可以将本地知识库的内容接入进来,让 llm 运用本地数据库进行回答。

我们之前已经学习过如何运用 LLM 对本地知识库进行问答,现在让我们将这个功能加入到我们的前端界面中。 回忆一下,我们学习了两种运用本地知识库进行问答的方式:

  • Chat_QA_chain_self:记忆历史记录的问答
  • QA_chain_self:直接进行问答,即没有历史记录

我们需要创建两个按钮,分别与对应的函数相绑定。

初始化按钮

python 复制代码
db_with_his_btn = gr.Button("Chat db with history")
db_wo_his_btn = gr.Button("Chat db without history")

将按钮与函数进行绑定,并且配置相应的输入和输出

python 复制代码
# 设置按钮的点击事件。当点击时,调用上面定义的 Chat_QA_chain_self 函数,并传入用户的消息和聊天历史记录,然后更新文本框和聊天机器人组件。
db_with_his_btn.click(Chat_QA_chain_self.answer, inputs=[msg, chatbot,  llm, embeddings, history_len, top_k, temperature], outputs=[msg, chatbot])
# 设置按钮的点击事件。当点击时,调用上面定义的 QA_chain_self 函数,并传入用户的消息和聊天历史记录,然后更新文本框和聊天机器人组件。
db_wo_his_btn.click(QA_chain_self.answer, inputs=[msg, chatbot, llm, embeddings, top_k, temperature], outputs=[msg, chatbot])

四、 丰富前端界面

4.1 gradio.File() 上传文件

这里我们是直接运用前面章节生成好的向量数据库。

当用户想要在前端页面上传自己的文件生成新的数据库时,gradio.File 可以很方便的完成这部分功能。

File 用于创建一个文件组件,允许用户上传通用文件(用作输入)或显示通用文件(用作输出)。

  • 作为输入时,File 模块文件按照 file_count 以 tempfile._TemporaryFileWrapper 或 List[tempfile._TemporaryFileWrapper] 的格式传递给绑定的函数,或者按照 type 的值转化为 bytes/List[bytes]。
  • 作为输出时,File 模块希望函数返回一个文件的路径/包含文件路径的列表(str / List[str])。

file_count 允许用户上传文件的数量,返回类型将根据 "multiple" 或 "directory" 的情况为每个文件返回一个列表。

  • "single",允许用户上传一个文件。
  • "multiple",允许用户上传多个文件。
  • "directory",允许用户上传所选目录中的所有文件。

file_types: 要上传的文件扩展名或文件类型的列表(例如['image','.json','.mp4'])。字符串表示支持上传的文件类型,格式后缀表示支持上传的文件格式。

  • "file": 允许上传任何文件
  • "image": 仅允许上传图像文件
  • "audio": 仅允许上传音频文件
  • "video": 仅允许上传视频文件
  • "text": 仅允许上传文本文件
  • ".pdf": 仅允许上传 pdf 文件
  • ".md": 仅允许上传 txt 文件

注意:当 file_count 为 "directory" 时,会忽略 "file_types" 的参数配置。

python 复制代码
gr.File(label='请选择知识库目录',file_count='directory',
                file_types=['.txt', '.md', '.docx', '.pdf'])

4.2 gradio.Slider() 配置滑动条

对于本项目来说,存在很多可以配置的参数,比如 LLM 的温度系数(temperature),这个参数的取值范围为 0-1,控制着 LLM 生成内容的稳定性。

温度基本上就是希望模型的变化程度。因此,如果将温度设为零,模型就会倾向于始终对相同的输入做出相同的反应。所以同样的问题,同样的答案。温度越高,信息的变化就越多。但如果温度过高,它就会开始给出无意义的答案。

我们想通过前端来进行参数的配置,但同时希望将参数限制在一个区间内,这时 gr.text 无法满足我们的需求。gradio.Slider 是可以胜任这个任务的组件。

gradio.Slider 允许用户通过滑动滑块在指定的范围内选择一个值

  • minimum:滑块的最小值,默认为 0。
  • maximum:滑块的最大值,默认为 100。
  • value: 滑块的默认值,即作为可调用对象时的默认值。
  • step:滑块的步长,即每次滑动的增量,默认为None。
  • label:组件在界面中显示的名称,默认为None。
  • interactive: 滑块是否可调节。如果未提供,则根据组件是用作输入还是输出进行推断。
python 复制代码
temperature = gr.Slider(0,
        1,
        value=0.00,
        step=0.01,
        label="llm temperature",
        interactive=True)

我们可以将其他类似的参数采用相同的构建方式,如:

  • 向量检索的数量(top_k):从向量数据库返回的最相关文档的数量,LLM 会根据返回的文档生成回答。
  • 聊天历史的长度(history_len):使聊天历史保持一定的长度,防止过大消耗过多的 token。

4.3 gradio.Dropdown() 建立下拉列表

刚刚我们介绍了 gradio 对于连续值的选择方法,现在我们来介绍下对于离散值的选择方法。

我们可以切换不同的模型,尝试不同模型的效果。我们用 gradio.Dropdown 来建立一个下拉列表,让用户从提供的模型中选择一个模型。

  • choices:可供选择的选项列表。
  • value:默认选中的值。如果为 None,则没有默认选中值。如果为可调用对象,则在应用程序加载时调用该函数以设置组件的初始值。
  • type:组件返回的值的类型。"value(返回选择的字符串),"index"(返回选择的索引)。
  • multiselect:是否可以选择多个选项。
  • max_choices:可选择的最大选项数。如果为None,则不限制数量。
  • interactive: 滑块是否可调节。如果未提供,则根据组件是用作输入还是输出进行推断。
python 复制代码
llm = gr.Dropdown(
    llm_model_list,
    label="large language model",
    value=init_llm,
    interactive=True)

同样,我们可以对生成 Embedding 的模型进行对应的配置。

将组件作为输入绑定在对应的函数中,即可完成对应参数的切换。

4.4 gradio.Accordion() 可折叠组件

对于 llm 和 Embedding 来说,通常我们选择一次后就不会再做调整,一直将整个组件展示在界面中显得占位置,我们可以用 gradio.Accordion 组件来将其折叠。

  • open:控制默认展开折叠组件。

gradio.Accordion 默认是展开的,对于组件内容提供折叠按钮。对于不需要的组件我们可以配置 open="False", 将其设置为默认折叠的状态。

我们先初始化 gradio.Accordion 组件,再将需要折叠的内容用 with 包起来即可。

python 复制代码
model_select = gr.Accordion("模型选择")
with model_select:
    llm = gr.Dropdown(...)
    embedding = gr.Dropdown(...)

4.5 gradio.Markdown() 编写 Markdown 模块

之前介绍的都是交互式组件,我们可以用 markdown 为我们的界面加一些说明,使整个界面看起来更加美观。同时可以增加一些辅助信息。

  • value:要在Markdown组件中显示的值。如果可调用,则每当加载应用程序以设置组件的初始值时,都会调用该函数。
  • rtl(bool):是否从右到左呈现文本。如果为True,则将渲染文本的方向设置为从右到左。默认值为False,它从左到右呈现文本。
  • latex_delimiters(list[dict[str,str|bool]]):将用于呈现 LaTeX 表达式的形式{"left":打开分隔符(str)、"right":关闭分隔符(str)、"display":是否以换行符(bool)显示}形式的dict列表。如果未提供,则将latex_delimitters'设置为","right":"","right":"[{ "left": "","right":"","right":"", "display": False }]`,因此只有包含在 $ 分隔符中且在同一行中的表达式才会呈现为 latex。传入一个空列表以禁用 LaTeX 渲染。有关更多信息,请参阅KaTeX文档
python 复制代码
gr.Markdown("""<h1><center>Chat Robot</center></h1>
<center>Local Knowledge Base Q&A with llm</center>
""")

4.6 gradio.Row() 和 gradio.Column() 调整布局

现在我们已经有了足够多的组件,是时候将他们按照一定的布局格式进行调整了。

gradio.Row() 和 gradio.Column() 分别是新建一行和新建一列的组件。我们将界面划分成不同的行列区域,将所需组件摆放在对应位置即可。

gradio.Row() 的常用参数

  • equal_height(bool): 是否将每个子元素的高度设置为相等。
  • variant(Literal[('default', 'panel', 'compact')]): 行类型。"default"(无背景)、"panel"(灰色背景色和圆角)或"compact"(圆角和无内部间隙)。

gradio.Column() 的常用参数:

  • scale:与相邻列相比的相对宽度。例如,如果列 A 的 scale 为 2,而列 B 的 scale 为1,则 A 的宽度将是 B 的两倍。
  • min_width: Column 的最小像素宽度,如果没有足够的屏幕空间,则将换行。如果某个 scale 值导致列比 min_width 窄,则首先考虑 min_widght 参数。
  • variant: 同 gradio.Row()

例如,我们可以将所有的对话组件放在一行中。将所有参数配置放在一列, 并将 chatBot 和参数配置以 4:1 的比例进行布置。

python 复制代码
with gr.Row():
    # 创建提交按钮。
    db_with_his_btn = gr.Button("Chat db with history")
    db_wo_his_btn = gr.Button("Chat db without history")
    llm_btn = gr.Button("Chat with llm")
python 复制代码
with gr.Column(scale=4):
    chatbot = gr.Chatbot(height=480) 
    ...
with gr.Column(scale=1):
    ...
    model_argument = gr.Accordion("参数配置", open=False)
    with model_argument:
        ...
    model_select = gr.Accordion("模型选择")
    with model_select:
        ...

当项目部署时,可能同一时间有多个用户进行访问,这时我们可以将 demo.queue(concurrency_count=3) 进行配置,表示用三个线程来并行。

现在是时候将我们的界面分享给别人了

下面是 demo.launch() 的几种场景分享配置

  1. 如果你的运行环境是在容器中运行,需要指定与当初创建容器时的端口一致,才能将端口映射出来 假设容器端口是8080,
  • demo.launch(server_name="0.0.0.0", server_port=8080)
  1. 如果是外部环境,非容器内部,则任意端口均可。
  • demo.launch(server_name="0.0.0.0", server_port="任意端口")
  1. 若想分享给局域网之外的其他人,则设置 share=True,可免费分享3天
  • demo.launch(share=True)

现在我们已经实现了 《llm 通过本地数据库进行回答》的基本功能和界面。快去进行自己的尝试吧。

第三章、Fast api 进行前后端分离 💬

目前我们已经完成了基本的可视化页面,并且可以实现对应的功能。

为了方便整个项目的管理,现有的项目通常采用前后端分离的方式搭建,前后端数据通过 json 的格式进行传输。

FastAPI 是一个用于构建 API 的现代、快速(高性能)的 web 框架,非常方便用于搭建我们的前后端分离的应用。

我们首先需要将我们用到的后端函数进行 FastAPI 的封装。封装 API 与前文中讲过将大模型 API 封装成本地 API 的方法类似,我们首先导入第三方库并创建一个 API 对象:

python 复制代码
from fastapi import FastAPI
from pydantic import BaseModel
import os

app = FastAPI() # 创建 api 对象

本地 API 一般通过 POST 方式进行访问,即参数会附加在 POST 请求中,我们需要定义一个数据模型来接收 POST 请求中的数据:

python 复制代码
# 定义一个数据模型,用于接收POST请求中的数据
class Item(BaseModel):
    prompt : str # 用户 prompt
    model : str = "gpt-3.5-turbo"# 使用的模型
    temperature : float = 0.1# 温度系数
    if_history : bool = False # 是否使用历史对话功能
    # API_Key
    api_key: str = None
    # Secret_Key
    secret_key : str = None
    # access_token
    access_token: str = None
    # APPID
    appid : str = None
    # APISecret
    api_secret : str = None
    # 数据库路径
    db_path : str = "../../data_base/vector_db/chroma"
    # 源文件路径
    file_path : str = "../../data_base/knowledge_db"
    # prompt template
    prompt_template : str = template
    # Template 变量
    input_variables : list = ["context","question"]
    # Embdding
    embedding : str = "openai"
    # Top K
    top_k : int = 5
    # embedding_key
    embedding_key : str = api_key

在上面的类中,我们定义了要调用我们已封装的 QA_chain 所需要传入的参数,对于非必须参数,我们都设置了默认参数来保证调用的简洁性,接下来我们就可以创建一个 POST 请求的 API 端点:

python 复制代码
@app.post("/answer/")
async def get_response(item: Item):

    # 首先确定需要调用的链
    if not item.if_history:
        # 调用 Chat 链
        chain = QA_chain_self(model=item.model, temperature=item.temperature, top_k=item.top_k, file_path=item.file_path, persist_path=item.db_path, 
                                appid=item.appid, api_key=item.api_key, embedding=item.embedding, template=template, api_secret=item.api_secret, embedding_key=item.embedding_key)

        response = chain.answer(question = item.prompt)
    
        return response
    
    # 由于 API 存在即时性问题,不能支持历史链
    else:
        return "API 不支持历史链"

上述端点的业务逻辑很简单,即调用我们已封装的 QA_chain_self 对象进行实例化与回答即可。通过这一个端点启动,我们便可通过访问本地 8000 端口来调用个人知识库助手的服务啦,我们只需要通过下列命令启动:

python 复制代码
uvicorn app:app 

到此介绍完毕!

相关推荐
剑盾云安全专家5 小时前
如何用AI轻松制作PPT,让演示更智能!
人工智能·科技·aigc·powerpoint·软件
墨风如雪5 小时前
Cloudflare 推出「AI迷宫」:用AI废话忽悠爬虫机器人的新策略
aigc
晨航7 小时前
南京审计大学:《 面向工程审计行业的DeepSeek大模型应用指南》.pdf(免费下载)
人工智能·ai·aigc
蓝胖子的小叮当9 小时前
搭建一个属于自己的本地大模型AI知识库
aigc·deepseek
程序员X小鹿1 天前
告别Excel公式:1句话分析复杂数据,10倍提效,打工人速存!(附保姆级教程)
aigc
百里香酚兰1 天前
【AI学习笔记】Coze平台实现将Excel文档批量导入数据库全过程
人工智能·笔记·大模型·aigc·工作流·pe·coze
正宗咸豆花1 天前
【PromptCoder + Trae】三分钟复刻 Spotify
人工智能·ai·prompt·aigc·个人开发
win4r1 天前
🚀Cursor降低智商!WindSurf零代码开发MCP Server!五分钟轻松实现LightRAG+MCP为Claude和AutoGen挂载知识库!
aigc·openai·cursor
yugasun1 天前
🌟 初探 MCP:给大模型插上 USB-C 接口?🌟
llm·aigc·mcp