Chapter 50
6. 与任意LLM交流 Chat with any LLM
第六章 与任何大语言模型聊天! 💬
在最后这一课中,我们将构建一个与开源 LLM 聊天的应用程序。我们将使用最好的开源模型之一,Falcon 40B。
我们将使用开源 LLM 构建一个聊天机器人应用程序。你可能已经用 ChatGPT 聊过天了,但运行它不仅成本高昂,交互的模式还较为死板。自定义 LLM 可以在本地运行,在自己的数据中进行微调,或者在云上运行,成本更低。在本课中,我们将使用运行 "falcon-40B-Instruct "的推理端点。
使用文本生成推理库在本地运行很方便。当然,也可以使用 Gradio 创建接口。Gradio 是基于 API 的 LLM,因此不仅支持开源的当然,你也可以使用 Gradio 为基于 API 的 LLM 创建接口,所以不仅仅是开源 LLM。但在本课程中,我们将重点介绍开源 LLM Falcon 40B。
加载 HF API 密钥和相关 Python 库
# 导入必要的库
import os # 用于操作系统相关的操作,例如读取环境变量
import io # 用于处理流式数据(例如文件流)
import IPython.display # 用于在IPython环境中显示数据,例如图片
from PIL import Image # 用于处理图像数据
import base64 # 用于处理base64编码,通常用于编码图像数据
import requests # 用于进行HTTP请求,例如GET和POST请求
# 设置请求的默认超时时间为60秒
requests.adapters.DEFAULT_TIMEOUT = 60
# 导入dotenv库的函数
# dotenv允许您从.env文件中读取环境变量
# 这在开发时特别有用,可以避免将敏感信息(如API密钥)硬编码到代码中
from dotenv import load_dotenv, find_dotenv
# 寻找.env文件并加载它的内容
# 这允许您使用os.environ来读取在.env文件中设置的环境变量
_ = load_dotenv(find_dotenv())
# 从环境变量中读取'HF_API_KEY'并将其存储在hf_api_key变量中
hf_api_key = os.environ['HF_API_KEY']我们在这里设置token和辅助函数。你可以看到,在这里我们使用了不同的库。我们使用的是文本生成库,这是一个用于处理开源 LLM 的精简库,可以让你同时加载 API(就像我们在这里做的一样),也可以在本地运行你自己的 LLM。
# 助手函数
# 导入必要的库
import requests # 用于进行HTTP请求
import json # 用于处理JSON数据
# 导入自定义模块中的Client类
# 假设这个类是用于与FalcomLM-instruct端点通信的
from text_generation import Client
# 使用os.environ从环境变量中获取'HF_API_FALCOM_BASE'的值,这应该是FalcomLM的基础URL
# 使用hf_api_key作为身份验证的一部分创建一个客户端实例
# 设置请求超时时间为120秒
client = Client(os.environ['HF_API_FALCOM_BASE'],
headers={"Authorization": f"Basic {hf_api_key}"},
timeout=120)建立一个应用程序,与任何LLM聊天!
这里我们将使用一个 推理端点 来调用 falcon-40b-instruct , 其在🤗 开源LLM榜单上名列前茅。
如果要本地调用它, 可以使用 Transformers 库 或者 文本生成推理库
# 设置一个中文提示,该提示将被送到模型中用于生成文本
prompt_chinese = "数学是被发明还是被发现的?"
# 调用client的generate方法
# 使用先前设置的中文提示 prompt_chinese
# max_new_tokens 参数用于限制生成的文本长度
# 然后从返回的结果中获取生成的文本
client.generate(prompt_chinese, max_new_tokens=256).generated_textOutput
'\n数学是被发现的,因为它是自然界的基本规律和模式。人类只是发现了这些规律和模式,并将它们用符号和公式表达出来。'
prompt = "Has math been invented or discovered?"
client.generate(prompt, max_new_tokens=256).generated_textOutput
'\nMath has been both invented and discovered. It is a human invention in the sense that it is a system of rules and concepts that we have created to help us understand the world around us. However, it is also a discovery in the sense that it is a fundamental aspect of the universe that we have uncovered through our observations and experiments.'
在第 2 课中,我们使用了一个非常简单的 Gradio 界面,它有一个文本框输入和一个输出。在这里,我们也可以用类似的方式与 LLM 聊天。再次复制我们的prompt。在这里,我们可以决定需要多少token。这就是非常简单地向 LLM 提问的方法。但我们还是不能聊天,因为如果你再问一个后续问题,它就无法理解或保留上下文。
# 回到第 2 课,时间过得真快!
# 导入所需的库
import gradio as gr # 用于创建Web界面
import os # 用于与操作系统交互,如读取环境变量
# 定义一个函数来根据输入生成文本
def generate(input, slider):
# 使用预定义的client对象的generate方法,从输入生成文本
# slider的值限制生成的token的数量
output = client.generate(input, max_new_tokens=slider).generated_text
return output # 返回生成的文本
# 创建一个Web界面
# 输入:一个文本框和一个滑块
# 输出:一个文本框显示生成的文本
demo = gr.Interface(
fn=generate,
inputs=[
gr.Textbox(label="Prompt"), # 文本输入框
gr.Slider(label="Max new tokens", value=20, maximum=1024, minimum=1) # 滑块用于选择生成的token的最大数量
],
outputs=[gr.Textbox(label="Completion")] # 显示生成文本的文本框
)
# 关闭可能已经启动的任何先前的gradio实例
gr.close_all()
# 启动Web界面
# 使用环境变量PORT1作为服务器的端口号
demo.launch(share=True, server_port=int(os.environ['PORT1']))因此,基本上我们要做的是,向模型发送我们之前的问题、它自己的回答以及后续问题。但建立所有这些都有点麻烦。这就是 Gradio 聊天机器人组件的作用所在,因为它允许我们简化向模型发送对话历史记录的过程。
因此,我们要解决这个问题。为此,我们将引入一个新的 Gradio 组件--Gradio Chatbot。

使用 gr.Chatbot() 来助力!
让我们开始使用 Gradio Chatbot 组件。这里实例化了一个带有文本框提示和提交按钮的 Gradle ChatBot 组件,是一个非常简单的用户界面。但我们现在还不是在和 LLM 聊天。
只需随机选择三个预设回复,然后将我的信息和机器人的信息添加到聊天记录中。所以在这里,你可以看到我可以说任何话,它基本上会随机查看这三个回复。
# 导入random库,用于随机选择消息
import random
import gradio as gr # 用于创建Web界面
import os # 用于与操作系统交互,如读取环境变量
# 定义机器人的响应函数
def respond(message, chat_history):
# 随机选择一个中文回复
bot_message_chinese = random.choice(["告诉我更多信息",
"酷,但我不感兴趣",
"嗯,那好吧"])
# 随机选择一个英文回复 (此回复未在界面中使用)
bot_message = random.choice(["Tell me more about it",
"Cool, but I'm not interested",
"Hmmmm, ok then"])
# 将用户的消息和机器人的中文回复添加到聊天历史记录中
chat_history.append((message, bot_message_chinese))
# 返回一个空字符串和更新后的聊天历史记录
return "", chat_history
# 使用gr.Blocks()上下文管理器来定义用户界面
with gr.Blocks() as demo:
# 定义一个聊天机器人窗口,高度为240
chatbot = gr.Chatbot(height=240)
# 定义一个用户输入消息的文本框
msg = gr.Textbox(label="Prompt")
# 定义一个提交按钮
btn = gr.Button("Submit")
# 定义一个清除文本框和聊天窗口内容的按钮
clear = gr.ClearButton(components=[msg, chatbot], value="Clear console")
# 当用户点击提交按钮时,调用响应函数
btn.click(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
# 当用户在文本框中按下Enter键时,也调用响应函数
msg.submit(respond, inputs=[msg, chatbot], outputs=[msg, chatbot])
# 关闭可能已经启动的任何先前的gradio实例
gr.close_all()
# 启动Web界面
# 使用环境变量PORT2作为服务器的端口号
demo.launch(share=True, server_port=int(os.environ['PORT2']))
我们必须格式化聊天prompt。此处正在定义这个格式化聊天prompt函数。 在这里,我们要做的就是使其包含聊天历史记录,这样 LLM 就能知道上下文。 但这还不够。我们还需要告诉它,哪些信息来自用户,哪些信息来自 LLM 本身,也就是我们正在调用的助手。 因此,我们设置了格式聊天prompt功能,在聊天记录的每一轮中,都包含一条用户信息和一条助手信息,以便我们的模型能准确回答后续问题。 现在,我们要将格式化的prompt传递给我们的 API。
# 定义一个函数,用于格式化聊天提示。
def format_chat_prompt(message, chat_history):
# 初始化一个空字符串,用于存放格式化后的聊天提示。
prompt = ""
# 遍历聊天历史记录。
for turn in chat_history:
# 从聊天记录中提取用户和机器人的消息。
user_message, bot_message = turn
# 更新提示,加入用户和机器人的消息。
prompt = f"{prompt}\nUser: {user_message}\nAssistant: {bot_message}"
# 将当前的用户消息也加入到提示中,并预留一个位置给机器人的回复。
prompt = f"{prompt}\nUser: {message}\nAssistant:"
# 返回格式化后的提示。
return prompt
# 定义一个函数,用于生成机器人的回复。
def respond(message, chat_history):
# 调用上面的函数,将用户的消息和聊天历史记录格式化为一个提示。
formatted_prompt = format_chat_prompt(message, chat_history)
# 使用client对象的generate方法生成机器人的回复(注意:client对象在此代码中并未定义)。
bot_message = client.generate(formatted_prompt,
max_new_tokens=1024,
stop_sequences=["\nUser:", ""]).generated_text
# 将用户的消息和机器人的回复加入到聊天历史记录中。
chat_history.append((message, bot_message))
# 返回一个空字符串和更新后的聊天历史记录(这里的空字符串可以替换为真正的机器人回复,如果需要显示在界面上)。
return "", chat_history
# 下面的代码是设置Gradio界面的部分。
# 使用Gradio的Blocks功能定义一个代码块。
with gr.Blocks() as demo:
# 创建一个Gradio聊天机器人组件,设置其高度为240。
chatbot = gr.Chatbot(height=240)
# 创建一个文本框组件,用于输入提示。
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,并使用环境变量PORT3指定服务器端口。
demo.launch(share=True, server_port=int(os.environ['PORT3']))

现在,我们的聊天机器人应该可以回答后续问题了。 我们可以看到,我们向它发送了上下文。我们向它发送了信息,然后要求它完成。一旦我们进入另一个迭代循环,我们就会向它发送我们的整个上下文,然后要求它完成。这很酷。但是,如果我们一直这样迭代下去,那么模型在一次对话中所能接受的信息量就会达到极限,因为我们总是给它越来越多的之前对话的内容。
为了让模型发挥最大作用,我们可以在这里将最大token数max_new_tokens设置为 1024、 这是我们在 API 中运行的硬件条件下,该模型所能接受的最大值。
可以尝试以下prompt:
- 哪些动物生活在热带草原?
- 这之中哪种动物最强壮?
这里,我们创建了一个简单但功能强大的用户界面,用于与LLM聊天。如果需要进一步Gradio 所能提供的最佳功能,我们可以创建一个包含更多功能的用户界面。
添加其他高级功能
# 定义一个函数,用于格式化聊天提示。
def format_chat_prompt(message, chat_history, instruction):
# 初始化提示,加入系统指令。
prompt = f"System:{instruction}"
# 遍历聊天历史记录。
for turn in chat_history:
# 从聊天记录中提取用户和机器人的消息。
user_message, bot_message = turn
# 更新提示,加入用户和机器人的消息。
prompt = f"{prompt}\nUser: {user_message}\nAssistant: {bot_message}"
# 将当前的用户消息也加入到提示中,并预留一个位置给机器人的回复。
prompt = f"{prompt}\nUser: {message}\nAssistant:"
# 返回格式化后的提示。
return prompt
# 定义一个函数,用于生成机器人的回复。
def respond(message, chat_history, instruction, temperature=0.7):
# 调用上面的函数,将用户的消息、聊天历史记录和系统指令格式化为一个提示。
prompt = format_chat_prompt(message, chat_history, instruction)
# 更新聊天历史记录,先加入用户的消息(机器人的回复部分先为空)。
chat_history = chat_history + [[message, ""]]
# 使用client对象的generate_stream方法生成机器人的回复(注意:client对象在此代码中并未定义)。
stream = client.generate_stream(prompt,
max_new_tokens=1024,
stop_sequences=["\nUser:", ""],
temperature=temperature) # 设置生成回复的温度,决定回复的随机性。
acc_text = ""
# 使用流式处理获取机器人的回复。
for idx, response in enumerate(stream):
text_token = response.token.text
# 如果有任何详情信息,直接返回。
if response.details:
return
# 如果是第一个令牌并且它以空格开始,则去除该空格。
if idx == 0 and text_token.startswith(" "):
text_token = text_token[1:]
# 累积生成的文本。
acc_text += text_token
# 更新最后一轮的聊天记录。
last_turn = list(chat_history.pop(-1))
last_turn[-1] += acc_text
chat_history = chat_history + [last_turn]
yield "", chat_history
acc_text = ""
# 设置Gradio界面部分。
with gr.Blocks() as demo:
# 创建一个Gradio聊天机器人组件,并设置其高度。
chatbot = gr.Chatbot(height=240)
# 创建一个文本框组件,用于输入提示。
msg = gr.Textbox(label="Prompt")
# 创建一个可折叠组件,用于显示高级选项。
with gr.Accordion(label="Advanced options", open=False):
# 在可折叠组件内创建一个文本框,用于输入系统消息。
system = gr.Textbox(label="System message", lines=2, value="一段用户和基于大语言模型的法律助手的对话. 助手会给出真实且有帮助的回答.")
# 创建一个滑块,用于调整回复的温度。
temperature = gr.Slider(label="temperature", minimum=0.1, maximum=1, value=0.7, step=0.1)
# 创建一个提交按钮。
btn = gr.Button("Submit")
# 创建一个清除按钮,用于清除文本框和聊天机器人组件的内容。
clear = gr.ClearButton(components=[msg, chatbot], value="Clear console")
# 设置按钮的点击事件。当点击时,调用上面定义的respond函数,并传入用户的消息、聊天历史记录和系统消息,然后更新文本框和聊天机器人组件。
btn.click(respond, inputs=[msg, chatbot, system], outputs=[msg, chatbot])
# 设置文本框的提交事件(即按下Enter键时)。功能与上面的按钮点击事件相同。
msg.submit(respond, inputs=[msg, chatbot, system], outputs=[msg, chatbot])


在这里,我们有高级选项,包括系统消息,它可以设置 LLM 与你聊天的模式。 因此,在系统消息中,你可以说,例如,你是一个乐于助人的助手,或者你可以给它一个特定的语气,一个特定的语调, 你希望它更有趣一点,更严肃一点,你真的可以反复调试系统消息提示,看看它对你的消息有什么影响。
有些人甚至会想给 LLM 一个角色,比如你是一个提供法律建议的律师,或者你是一个提供医疗建议的医生, 但要注意的是,众所周知,LLM 会以一种听起来很真实的方式提供虚假信息。 因此,尽管使用Falcon 40B 进行实验和探索会很有趣,但在现实世界的应用场景中,必须为此类使用案例制定进一步的保障措施。
还有其他高级参数,比如这里的温度。 温度基本上就是你希望模型的变化程度。因此,如果将温度设为零,模型就会倾向于始终对相同的输入做出相同的反应。 所以同样的问题,同样的答案。温度越高,信息的变化就越多。但如果温度过高,它就会开始给出无意义的答案。 因此,0.7 是一个不错的默认参数,但我们鼓励你多做尝试。
除此之外,这个用户界面还能让我们进行流式传输回复。 它逐个token发送,我们可以看到它实时完成。因此,我们不需要等到整个答案都准备好了。在这里,我们可以看到它是如何完成的。如果你不理解这里的所有内容,也不用担心,因为我们的目的是用一个非常完整的用户界面来结束课程,并提供LLM方面的所有功能。
在格式聊天提示中,也就是我们之前使用的功能中,我们添加了一个新元素,那就是系统指令。因此,在开始用户助手对话之前,我们在系统顶部添加了一个指令。因此,基本上在发送给模型的每条信息的开头,都会有我们设置的系统信息。在这里,我们调用文本生成库的generate_stream函数。而 generate_stream函数的作用就是逐个生成响应标记。因此,在这个循环中,发生的事情就是按标记生成响应标记,将其添加到聊天记录中,然后将其返回给函数。
# 关闭可能已经启动的任何先前的gradio实例
gr.close_all()