Gradio 代理 & MCP 黑客马拉松

获奖者
Gradio logo
  1. 聊天机器人
  2. 快速创建聊天机器人

如何使用 Gradio 创建聊天机器人

介绍

聊天机器人是大型语言模型(LLMs)的流行应用。使用 Gradio,你可以轻松构建聊天应用并与用户分享,或者通过直观的 UI 自行尝试。

本教程使用 gr.ChatInterface(),这是一个高级抽象,允许你快速创建聊天机器人 UI,通常只需*几行 Python 代码*。它可以轻松适应支持多模态聊天机器人或需要进一步自定义的聊天机器人。

先决条件:请确保你使用的是最新版本的 Gradio

$ pip install --upgrade gradio

OpenAI-API 兼容端点注意事项

如果你有一个提供 OpenAI-API 兼容端点(例如 Ollama)的聊天服务器,你可以用一行 Python 代码启动一个 ChatInterface。首先,运行 pip install openai。然后,使用你自己的 URL、模型和可选令牌

import gradio as gr

gr.load_chat("https://:11434/v1/", model="llama3.2", token="***").launch()

文档中了解 gr.load_chat。如果你有自己的模型,请继续阅读,了解如何使用 Python 为任何聊天模型创建应用!

定义聊天函数

要使用 gr.ChatInterface() 创建聊天应用,你需要做的第一件事是定义你的聊天函数。在最简单的情况下,你的聊天函数应该接受两个参数:messagehistory(参数名称可以任意,但必须按此顺序)。

  • message:一个 str 类型的值,表示用户最近的消息。
  • history:一个包含 rolecontent 键的 openai 风格字典列表,表示先前的对话历史。也可能包含表示消息元数据的附加键。

例如,history 可能如下所示

[
    {"role": "user", "content": "What is the capital of France?"},
    {"role": "assistant", "content": "Paris"}
]

而下一条 message 将是

"And what is its largest city?"

你的聊天函数只需返回

  • 一个 str 类型的值,即聊天机器人根据聊天 history 和最新 message 给出的响应,例如在此情况下
Paris is also the largest city.

让我们看几个聊天函数示例

示例:一个随机回复“是”或“否”的聊天机器人

让我们编写一个随机回复“是”或“否”的聊天函数。

这是我们的聊天函数

import random

def random_response(message, history):
    return random.choice(["Yes", "No"])

现在,我们可以将其插入 gr.ChatInterface() 并调用 .launch() 方法来创建 Web 界面

import gradio as gr

gr.ChatInterface(
    fn=random_response, 
    type="messages"
).launch()

提示: 在 `gr.ChatInterface` 中始终将 `type="messages"`。默认值(`type="tuples"`)已被弃用,并将在 Gradio 的未来版本中移除。

就是这样!这是我们正在运行的演示,快来试试吧

示例:一个交替表示同意和不同意的聊天机器人

当然,前面的示例非常简单,它没有考虑用户输入或之前的历史记录!这里有另一个简单的示例,展示了如何结合用户输入和历史记录。

import gradio as gr

def alternatingly_agree(message, history):
    if len([h for h in history if h['role'] == "assistant"]) % 2 == 0:
        return f"Yes, I do think that: {message}"
    else:
        return "I don't think so"

gr.ChatInterface(
    fn=alternatingly_agree, 
    type="messages"
).launch()

我们将在下一份指南中介绍更真实的聊天函数示例,该指南展示了如何将 `gr.ChatInterface` 与流行的大型语言模型一起使用

流式聊天机器人

在你的聊天函数中,你可以使用 yield 生成一系列部分响应,每个响应都会替换前一个。这样,你将得到一个流式聊天机器人。就这么简单!

import time
import gradio as gr

def slow_echo(message, history):
    for i in range(len(message)):
        time.sleep(0.3)
        yield "You typed: " + message[: i+1]

gr.ChatInterface(
    fn=slow_echo, 
    type="messages"
).launch()

当响应正在流式传输时,“提交”按钮会变成“停止”按钮,可用于停止生成器函数。

提示: 即使你在每次迭代中都生成最新消息,Gradio 也只将每条消息的“差异”从服务器发送到前端,这减少了网络延迟和数据消耗。

自定义聊天界面

如果你熟悉 Gradio 的 gr.Interface 类,那么 gr.ChatInterface 包含了许多相同的参数,你可以用它们来自定义聊天机器人的外观和感觉。例如,你可以

  • 使用 titledescription 参数在聊天机器人上方添加标题和描述。
  • 分别使用 themecss 参数添加主题或自定义 CSS。
  • 添加 examples 甚至启用 cache_examples,这使得用户更容易尝试你的聊天机器人。
  • 自定义聊天机器人(例如更改高度或添加占位符)或文本框(例如添加最大字符数或添加占位符)。

添加示例

你可以使用 examples 参数向 gr.ChatInterface 添加预设示例,该参数接受一个字符串示例列表。任何示例都将以“按钮”的形式出现在聊天机器人中,在发送任何消息之前。如果你想将图片或其他文件作为示例的一部分,可以使用以下字典格式代替字符串:{"text": "这张图片里有什么?", "files": ["cheetah.jpg"]}。每个文件都将作为一条独立的消息添加到你的聊天机器人历史记录中。

你可以使用 example_labels 参数更改每个示例的显示文本。你还可以使用 example_icons 参数为每个示例添加图标。这两个参数都接受一个字符串列表,其长度应与 examples 列表相同。

如果你想缓存示例,使其预先计算并即时显示结果,请设置 cache_examples=True

自定义聊天机器人或文本框组件

如果你想自定义构成 ChatInterfacegr.Chatbotgr.Textbox,那么你可以传入你自己的聊天机器人或文本框组件。这里有一个示例,展示了如何应用本节讨论的参数

import gradio as gr

def yes_man(message, history):
    if message.endswith("?"):
        return "Yes"
    else:
        return "Ask me anything!"

gr.ChatInterface(
    yes_man,
    type="messages",
    chatbot=gr.Chatbot(height=300),
    textbox=gr.Textbox(placeholder="Ask me a yes or no question", container=False, scale=7),
    title="Yes Man",
    description="Ask Yes Man any question",
    theme="ocean",
    examples=["Hello", "Am I cool?", "Are tomatoes vegetables?"],
    cache_examples=True,
).launch()

这里是另一个示例,它为你的聊天界面添加了一个“占位符”,该占位符在用户开始聊天之前显示。gr.Chatbotplaceholder 参数接受 Markdown 或 HTML

gr.ChatInterface(
    yes_man,
    type="messages",
    chatbot=gr.Chatbot(placeholder="<strong>Your Personal Yes-Man</strong><br>Ask Me Anything"),
...

占位符在聊天机器人中垂直和水平居中显示。

多模态聊天界面

你可能希望为你的聊天界面添加多模态功能。例如,你可能希望用户能够将图片或文件上传到你的聊天机器人并提问相关问题。你可以通过向 gr.ChatInterface 类传递一个参数(multimodal=True)来使你的聊天机器人支持“多模态”。

multimodal=True 时,你的聊天函数的签名会略有改变:你的函数的第一个参数(我们上面提到的 message)应该接受一个字典,其中包含提交的文本和上传的文件,格式如下

{
    "text": "user input", 
    "files": [
        "updated_file_1_path.ext",
        "updated_file_2_path.ext", 
        ...
    ]
}

你的聊天函数的第二个参数 history 将与之前一样,采用 openai 风格的字典格式。但是,如果历史记录包含上传的文件,文件的 content 键将不是字符串,而是由文件路径组成的单元素元组。历史记录中的每个文件都将是一条独立的消息。因此,在上传两个文件并提问后,你的历史记录可能如下所示

[
    {"role": "user", "content": ("cat1.png")},
    {"role": "user", "content": ("cat2.png")},
    {"role": "user", "content": "What's the difference between these two images?"},
]

当设置 multimodal=True 时,你的聊天函数的返回类型*不会改变*(即,在最简单的情况下,你仍然应该返回一个字符串值)。我们将在下方讨论更复杂的情况,例如返回文件。

如果你正在自定义多模态聊天界面,你应该向 textbox 参数传入 gr.MultimodalTextbox 的实例。你可以通过传入 sources 参数进一步自定义 MultimodalTextbox,该参数是一个要启用的源列表。这里有一个示例,说明如何设置和自定义多模态聊天界面

import gradio as gr

def count_images(message, history):
    num_images = len(message["files"])
    total_images = 0
    for message in history:
        if isinstance(message["content"], tuple):
            total_images += 1
    return f"You just uploaded {num_images} images, total uploaded: {total_images+num_images}"

demo = gr.ChatInterface(
    fn=count_images, 
    type="messages", 
    examples=[
        {"text": "No files", "files": []}
    ], 
    multimodal=True,
    textbox=gr.MultimodalTextbox(file_count="multiple", file_types=["image"], sources=["upload", "microphone"])
)

demo.launch()

附加输入

你可能希望向你的聊天函数添加附加输入,并通过聊天 UI 将它们暴露给用户。例如,你可以为系统提示添加一个文本框,或者添加一个滑块来设置聊天机器人响应中的令牌数量。gr.ChatInterface 类支持 additional_inputs 参数,可用于添加附加输入组件。

additional_inputs 参数接受一个组件或一个组件列表。你可以直接传入组件实例,或使用它们的字符串快捷方式(例如 "textbox" 而不是 gr.Textbox())。如果你传入组件实例,并且它们*尚未*渲染,那么这些组件将显示在聊天机器人下方的一个 gr.Accordion() 中。

这是一个完整示例

import gradio as gr
import time

def echo(message, history, system_prompt, tokens):
    response = f"System prompt: {system_prompt}\n Message: {message}."
    for i in range(min(len(response), int(tokens))):
        time.sleep(0.05)
        yield response[: i + 1]

demo = gr.ChatInterface(
    echo,
    type="messages",
    additional_inputs=[
        gr.Textbox("You are helpful AI.", label="System Prompt"),
        gr.Slider(10, 100),
    ],
)

demo.launch()

如果你传入 additional_inputs 的组件已经在父 gr.Blocks() 中渲染,那么它们将*不会*在折叠面板中重新渲染。这为决定如何布局输入组件提供了灵活性。在下面的示例中,我们将 gr.Textbox() 定位在聊天机器人 UI 的顶部,同时将滑块保持在下方。

import gradio as gr
import time

def echo(message, history, system_prompt, tokens):
    response = f"System prompt: {system_prompt}\n Message: {message}."
    for i in range(min(len(response), int(tokens))):
        time.sleep(0.05)
        yield response[: i+1]

with gr.Blocks() as demo:
    system_prompt = gr.Textbox("You are helpful AI.", label="System Prompt")
    slider = gr.Slider(10, 100, render=False)

    gr.ChatInterface(
        echo, additional_inputs=[system_prompt, slider], type="messages"
    )

demo.launch()

带有附加输入的示例

你还可以为你的附加输入添加示例值。向 examples 参数传入一个列表的列表,其中每个内部列表表示一个样本,并且每个内部列表的长度应为 1 + len(additional_inputs)。内部列表的第一个元素应该是聊天消息的示例值,随后的每个元素都应该按顺序是其中一个附加输入的示例值。当提供附加输入时,示例将显示在聊天界面下方的表格中。

如果你需要创建更自定义的内容,那么最好使用低级 gr.Blocks() API 构建聊天机器人 UI。我们在此处提供一份专门的指南

附加输出

就像你可以接受额外的输入到你的聊天函数一样,你也可以返回额外的输出。只需在 gr.ChatInterface 中向 additional_outputs 参数传入一个组件列表,并从你的聊天函数中为每个组件返回额外的 U值。这里有一个示例,它提取代码并将其输出到一个单独的 gr.Code 组件中

import gradio as gr

python_code = """
def fib(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fib(n-1) + fib(n-2)
"""

js_code = """
function fib(n) {
    if (n <= 0) return 0;
    if (n === 1) return 1;
    return fib(n - 1) + fib(n - 2);
}
"""

def chat(message, history):
    if "python" in message.lower():
        return "Type Python or JavaScript to see the code.", gr.Code(language="python", value=python_code)
    elif "javascript" in message.lower():
        return "Type Python or JavaScript to see the code.", gr.Code(language="javascript", value=js_code)
    else:
        return "Please ask about Python or JavaScript.", None

with gr.Blocks() as demo:
    code = gr.Code(render=False)
    with gr.Row():
        with gr.Column():
            gr.Markdown("<center><h1>Write Python or JavaScript</h1></center>")
            gr.ChatInterface(
                chat,
                examples=["Python", "JavaScript"],
                additional_outputs=[code],
                type="messages"
            )
        with gr.Column():
            gr.Markdown("<center><h1>Code Artifacts</h1></center>")
            code.render()

demo.launch()

注意:与附加输入不同,传入 additional_outputs 的组件必须已在你的 gr.Blocks 上下文中定义——它们不会自动渲染。如果你需要在 gr.ChatInterface 之后渲染它们,你可以在首次定义时设置 render=False,然后像上面示例中那样,在 gr.Blocks() 的适当部分 .render() 它们。

返回复杂响应

我们前面提到,在最简单的情况下,你的聊天函数应该返回一个 str 响应,该响应将在聊天机器人中渲染为 Markdown。但是,你也可以返回更复杂的响应,如下所述

返回文件或 Gradio 组件

目前,以下 Gradio 组件可以显示在聊天界面中

  • gr.Image
  • gr.Plot
  • gr.Audio
  • gr.HTML
  • gr.Video
  • gr.Gallery
  • gr.File

只需从你的函数中返回这些组件中的一个,即可与 gr.ChatInterface 一起使用。这里有一个返回音频文件的示例

import gradio as gr

def music(message, history):
    if message.strip():
        return gr.Audio("https://github.com/gradio-app/gradio/raw/test/test_files/audio_sample.wav")
    else:
        return "Please provide the name of an artist"

gr.ChatInterface(
    music,
    type="messages",
    textbox=gr.Textbox(placeholder="Which artist's music do you want to listen to?", scale=7),
).launch()

类似地,你可以使用 gr.Image 返回图像文件,使用 gr.Video 返回视频文件,或使用 gr.File 组件返回任意文件。

返回多条消息

你可以通过返回一个消息 list 来从你的聊天函数返回多条助手消息,其中每条消息都是有效的聊天类型。例如,这允许你发送一条消息以及文件,如下例所示

import gradio as gr

def echo_multimodal(message, history):
    response = []
    response.append("You wrote: '" + message["text"] + "' and uploaded:")
    if message.get("files"):
        for file in message["files"]:
            response.append(gr.File(value=file))
    return response

demo = gr.ChatInterface(
    echo_multimodal,
    type="messages",
    multimodal=True,
    textbox=gr.MultimodalTextbox(file_count="multiple"),
)

demo.launch()

显示中间思考或工具使用

gr.ChatInterface 类支持在聊天机器人中直接显示中间思考或工具使用。

要做到这一点,你需要从你的聊天函数返回一个 gr.ChatMessage 对象。这里是 gr.ChatMessage 数据类的模式以及两个内部类型字典

@dataclass
class ChatMessage:
   content: str | Component
   metadata: MetadataDict = None
   options: list[OptionDict] = None

class MetadataDict(TypedDict):
   title: NotRequired[str]
   id: NotRequired[int | str]
   parent_id: NotRequired[int | str]
   log: NotRequired[str]
   duration: NotRequired[float]
   status: NotRequired[Literal["pending", "done"]]

class OptionDict(TypedDict):
   label: NotRequired[str]
   value: str

如你所见,gr.ChatMessage 数据类类似于 openai 风格的消息格式,例如,它有一个“content”键,指代聊天消息内容。但它还包含一个“metadata”键,其值是一个字典。如果这个字典包含一个“title”键,则生成的消息将作为中间思考显示,标题显示在思考的顶部。这里有一个示例展示了用法

import gradio as gr
from gradio import ChatMessage
import time

sleep_time = 0.5

def simulate_thinking_chat(message, history):
    start_time = time.time()
    response = ChatMessage(
        content="",
        metadata={"title": "_Thinking_ step-by-step", "id": 0, "status": "pending"}
    )
    yield response

    thoughts = [
        "First, I need to understand the core aspects of the query...",
        "Now, considering the broader context and implications...",
        "Analyzing potential approaches to formulate a comprehensive answer...",
        "Finally, structuring the response for clarity and completeness..."
    ]

    accumulated_thoughts = ""
    for thought in thoughts:
        time.sleep(sleep_time)
        accumulated_thoughts += f"- {thought}\n\n"
        response.content = accumulated_thoughts.strip()
        yield response

    response.metadata["status"] = "done"
    response.metadata["duration"] = time.time() - start_time
    yield response

    response = [
        response,
        ChatMessage(
            content="Based on my thoughts and analysis above, my response is: This dummy repro shows how thoughts of a thinking LLM can be progressively shown before providing its final answer."
        )
    ]
    yield response


demo = gr.ChatInterface(
    simulate_thinking_chat,
    title="Thinking LLM Chat Interface 🤔",
    type="messages",
)

demo.launch()

你甚至可以显示嵌套思考,这对于一个工具可能调用其他工具的代理演示很有用。要显示嵌套思考,请在“metadata”字典中包含“id”和“parent_id”键。请阅读我们关于显示中间思考和工具使用的专门指南,了解更真实的示例。

提供预设响应

当返回助手消息时,你可能希望提供用户可以选择的预设选项作为响应。要做到这一点,你将再次从你的聊天函数返回一个 gr.ChatMessage 实例。这次,请确保设置 options 键来指定预设响应。

如上文 gr.ChatMessage 模式所示,options 键对应的值应该是一个字典列表,每个字典都包含一个 value(一个字符串,当点击此响应时应发送到聊天函数的值)和一个可选的 label(如果提供,则是显示为预设响应的文本而不是 value)。

此示例演示了如何使用预设响应

import gradio as gr
import random

example_code = """
Here's an example Python lambda function:

lambda x: x + {}

Is this correct?
"""

def chat(message, history):
    if message == "Yes, that's correct.":
        return "Great!"
    else:
        return gr.ChatMessage(
            content=example_code.format(random.randint(1, 100)),
            options=[
                {"value": "Yes, that's correct.", "label": "Yes"},
                {"value": "No"}
            ]
        )

demo = gr.ChatInterface(
    chat,
    type="messages",
    examples=["Write an example Python lambda function."]
)

demo.launch()

直接修改聊天机器人值

你可能希望使用自己的事件修改聊天机器人的值,而不是 gr.ChatInterface 中预构建的事件。例如,你可以创建一个下拉菜单,用特定对话预填充聊天历史记录,或者添加一个单独的按钮来清除对话历史记录。gr.ChatInterface 支持这些事件,但你需要在这些事件中使用 gr.ChatInterface.chatbot_value 作为输入或输出组件。在这个示例中,我们使用 gr.Radio 组件来预填充聊天机器人与某些对话。

import gradio as gr
import random

def prefill_chatbot(choice):
    if choice == "Greeting":
        return [
            {"role": "user", "content": "Hi there!"},
            {"role": "assistant", "content": "Hello! How can I assist you today?"}
        ]
    elif choice == "Complaint":
        return [
            {"role": "user", "content": "I'm not happy with the service."},
            {"role": "assistant", "content": "I'm sorry to hear that. Can you please tell me more about the issue?"}
        ]
    else:
        return []

def random_response(message, history):
    return random.choice(["Yes", "No"])

with gr.Blocks() as demo:
    radio = gr.Radio(["Greeting", "Complaint", "Blank"])
    chat = gr.ChatInterface(random_response, type="messages")
    radio.change(prefill_chatbot, radio, chat.chatbot_value)

demo.launch()

通过 API 使用你的聊天机器人

一旦你构建了 Gradio 聊天界面并将其托管在 Hugging Face Spaces 或其他地方,你就可以在 `/chat` 端点使用简单的 API 查询它。该端点只需用户的消息,并会返回响应,内部会跟踪消息历史记录。

要使用该端点,你应该使用Gradio Python 客户端Gradio JS 客户端。或者,你可以将你的聊天界面部署到其他平台,例如

聊天历史记录

你可以为你的 ChatInterface 启用持久聊天历史记录,允许用户维护多个对话并轻松切换。启用后,对话将使用本地存储在用户的浏览器中本地私密保存。因此,如果你将 ChatInterface 部署到例如 Hugging Face Spaces,每个用户都将拥有自己独立的聊天历史记录,不会干扰其他用户的对话。这意味着多个用户可以同时与同一个 ChatInterface 交互,同时维护他们自己的私有对话历史记录。

要启用此功能,只需设置 gr.ChatInterface(save_history=True)(如下一节示例所示)。用户随后将在侧边面板中看到他们以前的对话,并可以继续任何以前的聊天或开始新的聊天。

收集用户反馈

为了收集关于你的聊天模型的反馈,请设置 gr.ChatInterface(flagging_mode="manual"),用户将能够对助手响应进行点赞或点踩。每个被标记的响应,连同整个聊天历史记录,都将保存在应用程序工作目录中的 CSV 文件中(这可以通过 flagging_dir 参数进行配置)。

你还可以通过 flagging_options 参数更改反馈选项。默认选项是“赞”和“踩”,它们分别显示为点赞和点踩图标。任何其他选项都显示在专门的标记图标下。此示例展示了一个同时启用了聊天历史记录(上一节中提到)和用户反馈的 ChatInterface。

import time
import gradio as gr

def slow_echo(message, history):
    for i in range(len(message)):
        time.sleep(0.05)
        yield "You typed: " + message[: i + 1]

demo = gr.ChatInterface(
    slow_echo,
    type="messages",
    flagging_mode="manual",
    flagging_options=["Like", "Spam", "Inappropriate", "Other"],
    save_history=True,
)

demo.launch()

请注意,在此示例中,我们设置了几个标记选项:“赞”、“垃圾邮件”、“不当”、“其他”。由于区分大小写的字符串“Like”是标记选项之一,用户将在每个助手消息旁边看到一个点赞图标。其他三个标记选项将出现在标记图标下的下拉菜单中。

下一步是什么?

既然你已经了解了 gr.ChatInterface 类以及如何快速创建聊天机器人 UI,我们建议你阅读以下内容之一