从零搭建大模型问答系统-Gradio+Ollama+Qwen2.5实现全流程(一)
- 前言
- 一、界面设计(计划)
- 二、模块设计
- 1.登录模块
- 2.注册模块
- 3. 主界面模块
- 4. 历史记录模块
- 三、相应的接口(前后端交互)
- 四、实现前端界面的设计
- config.py
- History_g1.py
- Login.py
- Main.py
- Register.py
- App.py
- 五、效果展示
压抑的气氛,疲惫的身躯,干涩的眼眶,闲暇的周末~
不甘沉浸在打瓦与go学长的对抗较量,不愿沉迷于开麦与队友间的加密通信,
我便默默地打开电脑,选择换个活法度过周末。
前言
随着人工智能 AI 的快速发展,基于大语言模型(LLM)的应用逐渐成为软件开发中的热点。今天就学习一下如何设计和实现一个前后端交互的问答系统。在这里先进行需求分析,从以下三个角度去考虑:
技术需求 :
- 前端使用 Gradio 构建用户界面
- 后端使用 Ollama(本地化模型部署工具)
- 框架调用 Qwen2.5 大模型
- 通信协议选择RESTful API(JSON格式)
通过初始设计,我们需要从需求分析开始入手,再去系统实现,最后完成开发的整个过程。
功能需求 :
- 用户可以通过前端界面输入问题。
- 系统能够基于大模型生成准确的回答。
- 支持多轮对话。
- 提供简单的用户操作反馈(如加载状态、错误提示等)。
非功能性需求 :
- 系统响应时间控制在合理范围内。
- 界面简洁直观,用户体验友好。
通信流程图
通信流程
用户->>+前端: 输入问题
前端->>+后端: POST /api/generate
后端->>+Ollama: 模型调用请求
Ollama->>-后端: 生成响应
后端->>-前端: JSON响应
前端->>-用户: 显示回答
一、界面设计(计划)
在设计的时候将预期的功能都设计出来,之后再逐个去实现,往往设计的太简单,当后续需添加功能的时候,前端界面的布局反复修改也比较麻烦。先照葫芦画瓢,根据deepseek以及kimi等问答系统的界面进行模仿学习,修改设计。
这里登录方式分为三种 : 一种是微信扫码,手机号登录,账号密码登录。在实现过程中,暂时计划实现一种登录方式即可,完成整个项目的逻辑。
- 界面1 ,登陆界面
| 验证码登录 | 密码登录 |
————————————
这块就使用到我们前面学习到的多页面布局
还有Row 和 Column的组合拳
-
界面2 ,注册界面
-
界面3,主界面
4.历史记录界面
界面设计不用太细节有个草图模样就行,重点还是需要理解如何使用容器来布局这些设计,以及添加组件,绑定什么事件。你也可以自己设计一些更加丰富多彩的界面,有的设计在gradio中要想完美的显示,还是需要费很大的功夫(别问为什么)。
二、模块设计
预期有以下四个界面,先将每个界面写出来,再去考虑整合在一块。
1.登录模块
核心功能
发送验证码 :
用户输入手机号或邮箱后,点击“发送验证码”按钮。
前端调用后端接口 /send_verification_code 发送验证码。
如果输入格式错误(如手机号不是11位数字),前端会直接提示错误信息。
验证码登录验证 :
用户输入验证码后,点击“登录”按钮。
前端调用后端接口 /verify_code_login 验证验证码是否正确。
如果验证成功,返回“登录成功”;否则提示错误信息。
密码登录验证 :
用户输入用户名和密码后,点击“登录”按钮。
前端调用后端接口 /login 进行密码验证。
如果验证成功,返回用户ID和成功信息;否则提示错误信息。
用户注册 :
用户输入用户名和密码后,点击“注册”按钮。
前端调用后端接口 /register 提交注册信息。
如果注册成功,提示用户“注册成功,请登录”。
忘记密码 :
提示用户联系管理员重置密码。
界面设计
使用 Gradio 的 Tabs 组件实现验证码登录和密码登录的切换。
左侧为扫码登录区域,右侧为登录选项卡区域。
提供清晰的输入框、按钮和状态提示。
2.注册模块
核心功能
发送验证码 :
用户输入手机号后,点击“获取验证码”按钮。
前端调用后端接口 /send-code 发送验证码。
如果手机号格式错误或发送失败,前端会提示错误信息。
用户注册 :
用户输入手机号、验证码、用户名、密码和确认密码后,点击“立即注册”按钮。
前端调用后端接口 /register 提交注册信息。
如果注册成功,提示用户“注册成功!”;否则提示错误信息。
返回登录 :
提供“返回登录”按钮,用户可以跳转回登录界面。
界面设计
提供清晰的输入框和按钮。
实时显示状态提示(如验证码发送成功或失败)。
3. 主界面模块
核心功能
用户输入处理 :
用户输入问题后,点击“提交”按钮或按 Enter 键。
前端调用后端接口 /chat 获取模型的回答。
将用户输入和模型回答更新到聊天历史记录中。
新建对话 :
用户点击“开启新对话”按钮。
前端调用后端接口 /save_conversation 保存当前对话记录,并清空聊天历史。
切换侧边栏 :
用户可以点击“切换侧边栏”按钮展开或收起侧边栏。
二维码窗口 :
用户点击“手机端下载”按钮,显示二维码窗口。
点击关闭按钮隐藏二维码窗口。
快捷问题 :
提供三个快捷问题按钮,用户点击后自动填充到输入框。
4. 历史记录模块
核心功能
获取历史记录 : 前端调用后端接口 /get_conversation 获取历史记录。
如果后端不可用,使用本地模拟数据。
动态更新 :
用户选择时间范围或输入搜索关键词后,前端动态更新历史记录。
返回主界面 :
提供“返回主界面”按钮,用户可以跳转回主界面。
界面设计
提供时间范围选择器和搜索框。 使用 HTML 动态生成历史记录列表。
三、相应的接口(前后端交互)
在接口设计的过程中,一定要设计好请求参数与相应参数,如果未确定好请求响应的参数,在与后端交互的时候会有很多不必要的麻烦。以下是我设计的相关参数,读者也可以自己去设计添加更多的参数以优化自己系统的功能。
接口概览表
模块 | 接口地址 | 请求方法 | 功能说明 | 调用位置 |
---|---|---|---|---|
登录模块 | /send_verification_code | POST | 发送验证码 | 登录界面发送按钮 |
/verify_code_login | POST | 验证码登录 | 验证码登录按钮 | |
/login | POST | 密码登录 | 密码登录按钮 | |
注册界面 | /send-code | POST | 发送注册验证码 | 注册界面发送按钮 |
/register | POST | 提交注册信息 | 注册按钮 | |
主界面 | /chat | POST | 处理用户提问 | 聊天消息提交 |
/save_conversation | POST | 保存对话记录 | 新建对话按钮 | |
历史记录 | /get_conversation | POST | 获取历史对话 | 历史记录页面加载 |
- 登录模块
1.1 发送验证码
请求地址
/send_verification_code
请求参数
{"phone_email": "用户输入的手机号或邮箱"}
响应示例
{"message": "验证码已发送,请查收!"}
{ "detail": "错误信息(如手机号格式错误)"}
1.2 验证码登录
请求地址
/verify_code_login
请求参数
{
"phone_email": "用户输入的手机号或邮箱",
"code": "用户输入的验证码"
}
响应示例
{
"status": "success",
"message": "登录成功!"
}
{
"status": "error",
"detail": "验证码错误,请重新输入!"
}
1.3 密码登录
请求地址
/login
请求参数
{
"username": "用户名",
"password": "密码"
}
响应示例
{
"status": "success",
"message": "登录成功!",
"user_id": 12345
}
{
"status": "error",
"detail": "用户名或密码错误!"
}
- 注册模块
2.1 发送注册验证码
请求地址
/send-code
请求参数
{"phone_number": "用户输入的手机号"}
响应示例
# 成功
{ "message": "验证码已发送至 {phone_number}"}
# 失败
{"message": "发送失败:错误信息"}
2.2 提交注册
请求地址
/register
请求参数
{
"username": "用户名",
"password": "密码"
}
响应示例
# 成功
{ "message": "注册成功!"}
# 失败
{ "message": "注册失败:错误信息"}
- 主界面
3.1 处理用户提问
请求地址
/chat
请求参数
{
"user_input": "用户输入的问题",
"chat_history": [
{"role": "user", "content": "用户输入"},
{"role": "assistant", "content": "模型回答"}
]
}
响应示例
# 成功
{
"status": "success",
"response": "模型生成的回答",
"chat_history": [
{"role": "user", "content": "用户输入"},
{"role": "assistant", "content": "模型回答"}
]
}
# 失败
{
"status": "error",
"detail": "无法处理请求,请稍后再试!"
}
3.2 保存对话记录
请求地址
/save_conversation
请求参数
{
"user_id": "用户ID",
"conversation": [
{"user_input": "用户输入"},
{"bot_response": "模型回答"}
]
}
响应示例
# 成功
{ "message": "对话记录保存成功"}
# 失败
{"message": "保存失败:错误信息"}
- 历史记录模块
4.1 获取历史对话
请求地址
/get_conversation
请求参数
{
"user_id": "用户ID",
"time_period": "时间范围(本周/本月/本年/全部)",
"search_query": "搜索关键词"
}
响应示例
# 成功
{
"status": "success",
"chat_history": [
{
"title": "会话标题",
"content": "会话内容",
"date": "会话日期"
},
...
]
}
# 失败
{
"status": "error",
"detail": "无法获取历史记录,请稍后再试!"
}
四、实现前端界面的设计
当确定好接口,设计好界面,我们就可以进行编程了。
这是文件目录:
config.py
# config.py
# 后端 API 地址
BASE_URL = "http://localhost:8000"
History_g1.py
import gradio as gr
import requests
import json
from config import BASE_URL
def fetch_history(user_id, time_period="全部", search_query=""):
"""
从后端接口或 mock_history_data 获取历史记录。
:param user_id: 用户的唯一标识
:param time_period: 时间范围("本周", "本月", "本年", "全部")
:param search_query: 搜索关键词
:return: 历史记录数据
"""
try:
# 构造请求数据
payload = {
"user_id": user_id,
"time_period": time_period,
"search_query": search_query,
}
# 发送 POST 请求到后端 API
response = requests.post(f"{BASE_URL}/get_conversation", json=payload)
# 检查响应状态码
if response.status_code == 200:
# 解析后端返回的数据
history_data = response.json().get("chat_history", [])
# 数据适配:确保返回的是嵌套列表,每个元素是一个字典
formatted_data = []
for index, item in enumerate(history_data):
# print(f"处理第 {index + 1} 条记录: {item}") # 输出当前处理的记录
if isinstance(item, list): # 判断是否为嵌套列表
valid_conversation = []
for record in item:
# print(f" 当前记录: {record}, 类型: {type(record)}") # 输出当前记录及其类型
# 如果记录是字符串形式的 JSON,尝试解析为 Python 对象
if isinstance(record, str) and (record.startswith("[") or record.startswith("{")):
try:
record = json.loads(record.replace("'", '"')) # 替换单引号为双引号
# print(f" 成功解析字符串 JSON: {record}")
except json.JSONDecodeError:
# print(f" 无法解析字符串 JSON: {record}")
continue
# 如果解析后的对象是列表,则遍历其中的每个元素
if isinstance(record, list):
for sub_record in record:
if isinstance(sub_record, dict): # 判断是否为字典
# print(f" 子记录字典内容: {sub_record.keys()}") # 输出子记录字典的键
if "user_input" in sub_record or "bot_response" in sub_record:
# 添加默认 date 字段
sub_record.setdefault("date", "未知日期")
valid_conversation.append(sub_record)
else:
print(f" 非字典子记录: {sub_record}")
elif isinstance(record, dict): # 判断是否为字典
# print(f" 字典内容: {record.keys()}") # 输出字典的键
if "user_input" in record or "bot_response" in record:
# 添加默认 date 字段
record.setdefault("date", "未知日期")
valid_conversation.append(record)
else:
print(f" 非字典记录: {record}")
if valid_conversation: # 只保留有效的对话记录
formatted_data.append(valid_conversation)
else:
print(f"非嵌套列表记录: {item}")
print(f"后端返回的原始数据: {history_data}")
print(f"格式化后的数据: {formatted_data}")
return formatted_data
else:
print(f"后端错误: {response.status_code}")
raise Exception("后端返回错误状态码")
except Exception as e:
# 如果后端调用失败,回退到本地模拟数据
print(f"调用后端接口失败: {str(e)}")
def update_history(user_id, time_period="全部", search_query=""):
"""
根据时间范围和搜索关键词动态更新历史记录。
:param user_id: 用户的唯一标识
:param time_period: 时间范围("本周", "本月", "本年", "全部")
:param search_query: 搜索关键词
:return: 历史记录 HTML 内容
"""
# 获取所有历史记录
all_conversations = fetch_history(user_id, time_period, search_query)
# 构造 HTML 输出内容
if not all_conversations:
return "<div style='color: gray;'>暂无历史记录</div>"
html_content = []
for conversation in all_conversations:
# 第一条提问作为超链接标题
first_user_input = conversation[0].get("user_input", "无标题")
first_date = conversation[0].get("date", "未知日期")
# 超链接部分
html_content.append(
f'''
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
<button style="background: none; border: none; color: blue; text-decoration: underline; cursor: pointer;"
onclick="handleButtonClick('{first_user_input}')">{first_user_input}</button>
<span style="margin-left: 10px; color: gray;">{first_date}</span>
</div>
'''
)
return "\n".join(html_content)
def show_conversation_details(conversation_title, user_id, time_period="全部", search_query=""):
"""
根据超链接标题显示对应对话的详细信息。
:param conversation_title: 对话标题(第一条提问的内容)
:param user_id: 用户的唯一标识
:param time_period: 时间范围("本周", "本月", "本年", "全部")
:param search_query: 搜索关键词
:return: 对话详细信息的 HTML 内容
"""
# 获取所有历史记录
all_conversations = fetch_history(user_id, time_period, search_query)
# 找到对应标题的对话
for conversation in all_conversations:
if conversation[0].get("user_input") == conversation_title:
html_content = []
for record in conversation:
if "user_input" in record:
content = f"用户提问: {record['user_input']}"
elif "bot_response" in record:
content = f"机器人回复: {record['bot_response']}"
html_content.append(
f'<div style="margin-left: 20px;">{content} <span style="margin-left: 10px; color: gray;">{record["date"]}</span></div>'
)
return "<br>".join(html_content)
return "<div style='color: gray;'>未找到相关对话</div>"
def history_interface(user_id_state):
"""历史记录界面的封装函数"""
with gr.Column() as history_content:
gr.Markdown("# 历史会话")
with gr.Row(elem_classes="highlight-border"):
# 添加返回按钮
back_to_main_btn = gr.Button("返回主界面", elem_classes="send-btn4", elem_id="back-to-main-btn")
change_btn = gr.Button("刷新", elem_classes="send-btn4", elem_id="change_btn")
# 时间范围选择器
with gr.Row(elem_classes="highlight-border"):
time_period_dropdown = gr.Dropdown(
choices=["本周", "本月", "本年", "全部"],
label="选择时间范围",
value="全部"
)
# 搜索框
with gr.Row(elem_classes="highlight-border"):
search_box = gr.Textbox(label="搜索历史会话", placeholder="输入关键词搜索")
# 历史记录展示区域
with gr.Group(elem_classes="highlight-border"):
history_output = gr.HTML()
# # 隐藏的 Textbox 用于触发事件
# click_event_trigger = gr.Textbox(visible=False)
# 刷新按钮事件
change_btn.click(
fn=update_history,
inputs=[user_id_state, time_period_dropdown, search_box],
outputs=[history_output]
)
# 动态生成按钮并绑定事件
def on_link_click(link_text):
detailed_info = show_conversation_details(link_text, user_id_state.value, "全部", "")
return detailed_info
# 获取历史记录并动态生成按钮
all_conversations = fetch_history(user_id_state.value, "全部", "")
buttons = []
for conversation in all_conversations:
first_user_input = conversation[0].get("user_input", "无标题")
button = gr.Button(first_user_input, elem_classes="history-button",
style="background: none; border: none; color: blue; text-decoration: underline; cursor: pointer;")
button.click(
fn=on_link_click,
inputs=[gr.State(first_user_input)], # 使用 State 传递按钮的文本
outputs=[history_output]
)
buttons.append(button)
return history_content, back_to_main_btn, change_btn, history_output, search_box, time_period_dropdown
Login.py
import gradio as gr
import requests
from config import BASE_URL
import global_vars
# 后端 API 地址
# 发送验证码
def send_verification_code(phone_email):
"""发送验证码到后端"""
if len(phone_email) != 11 or not phone_email.isdigit():
return "手机号格式错误,请输入11位数字!"
url = f"{BASE_URL}/send_verification_code"
response = requests.post(url, json={"phone_email": phone_email})
if response.status_code == 200:
return response.json().get("message", "验证码已发送,请查收!")
else:
return response.json().get("detail", "没链接呢")
# 验证码登录验证
def login_with_code(phone_email, code):
"""验证码登录验证"""
url = f"{BASE_URL}/verify_code_login"
response = requests.post(url, json={"phone_email": phone_email, "code": code})
if response.status_code == 200:
return "登录成功!"
else:
return response.json().get("detail", "验证码错误,请重新输入!")
# 密码登录验证
'''
返回结果:
{
"status": "success",
"message": "登录成功!",
"user_id": 12345
}
{
"status": "error",
"detail": "用户名或密码错误!"
}
'''
def login_handler(username, password):
"""密码登录验证的前端逻辑"""
if not username or not password:
return "用户名和密码不能为空!", False # 返回错误信息和跳转标志
# 打印调试信息(可选)
print(f"正在验证:用户名={username}, 密码={password[:4]}")
# 调用后端接口进行验证
url = f"{BASE_URL}/login"
response = requests.post(url, json={"username": username, "password": password})
if response.status_code == 200:
data = response.json()
if data.get("status") == "success":
user_id = data.get("user_id", "")
global_vars.user_id_state = username
return "登录成功!", True # 返回成功信息和跳转标志
else:
return data.get("detail", "登录失败!"), False # 返回错误信息和跳转标志
else:
return response.json().get("detail", "服务器错误!"), False # 返回错误信息和跳转标志
# 用户注册
def register_handler(username, password):
"""用户注册的前端逻辑"""
if not username or not password:
return "用户名和密码不能为空!"
# 打印调试信息(可选)
print(f"正在注册:用户名={username}, 密码={password[:4]}****")
# 调用后端接口进行注册
url = f"{BASE_URL}/register"
response = requests.post(url, json={"username": username, "password": password})
if response.status_code == 200:
return "注册成功,请登录!"
else:
return response.json().get("detail", "注册失败!")
def forgot_password():
return "请联系管理员重置密码!"
def login_interface():
"""登录界面的封装函数"""
with gr.Column() as login_content: # 使用 Column 而不是 Blocks
gr.Markdown("# logo+名称", elem_classes="centered-containerL")
with gr.Row(elem_classes="gradio-containerL"):
# 左侧扫码登录区域
with gr.Column(scale=1,elem_classes="highlight-border"):
gr.Markdown("""<h1 style="font-size: 20px; color: #007BFF; text-align: center;">微信扫码 快速登录</h1>""")
gr.Markdown("""<h1 style="font-size: 20px; color: #000000; text-align: center;">——————————————</h1>""")
image = gr.Image("WX.jpg", elem_id="custom-image", height=300)
# 验证码登录选项卡
with gr.TabItem("验证码登录",elem_classes="highlight-border"):
phone_email = gr.Textbox(
label="手机号/邮箱",
placeholder="请输入手机号或邮箱",
info="区分大小写,不含空格",
elem_classes="input-field",
)
code = gr.Textbox(
show_label=False,
placeholder="输入验证码",
)
send_btn = gr.Button(
"发送验证码",
elem_classes="send-btnL",
)
send_status = gr.Markdown(value="", elem_classes="status-text")
login_code_button = gr.Button("登录", variant="primary", elem_id="login-confirm-btn")
login_result = gr.Textbox(label="操作结果", interactive=False)
# 密码登录选项卡
with gr.TabItem("密码登录",elem_classes="highlight-border"):
username = gr.Textbox(
show_label=False,
placeholder="用户名",
info="区分大小写,不含空格",
elem_classes="input-field",
)
password = gr.Textbox(
show_label=False,
type="password",
placeholder="输入密码",
)
forgot_btn = gr.Button("忘记密码?")
login_pwd_button = gr.Button("登录", variant="primary", elem_id="login-confirm-btn")
register_button = gr.Button("注册", elem_id="register-btn")
login_result = gr.Textbox(label="操作结果", interactive=False) # interactive=False 无法编辑
should_redirect = gr.State(False)
send_btn.click(
send_verification_code,
inputs=[phone_email],
outputs=[send_status]
)
login_code_button.click(
login_with_code,
inputs=[phone_email, code],
outputs=[login_result]
)
# 绑定登录按钮事件
login_pwd_button.click(
login_handler,
inputs=[username, password],
outputs=[login_result, should_redirect]
)
forgot_btn.click(
forgot_password,
inputs=[],
outputs=[login_result]
)
register_button.click(
register_handler,
inputs=[username, password],
outputs=[login_result]
)
return login_content, register_button, login_code_button, login_pwd_button,should_redirect,login_result,username
Main.py
import gradio as gr
import requests
from global_vars import user_id_state
from config import BASE_URL
from copy import deepcopy
'''
后端返回的类型:正确{
"status": "success",
"response": "你好!有什么可以帮您的吗?",
"chat_history": [
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮您的吗?"}
{"role": "user", "content": "你好"},
{"role": "assistant", "content": "你好!有什么可以帮您的吗?"}
]
错误:{"status": "error",
"detail": "无法处理请求,请稍后再试!"}
'''
#之前的
# def process_user_input(user_input, chat_history):
# """处理用户输入并更新聊天记录。"""
# try:
# #chat_history 发送格式 List[Dict[str, str]]
# formatted_chat_history = chat_history if isinstance(chat_history, list) else []
# formatted_chat_history.append({"role": "user", "content": user_input})
#
# # 构造请求数据
# payload = {
# "user_input": user_input,
# "chat_history": formatted_chat_history # 发送 JSON 格式正确的 chat_history
# }
# # 发送 POST 请求到后端 API
# response = requests.post(f"{BASE_URL}/chat", json=payload)
#
# # 检查响应状态码
# if response.status_code == 200:
# data = response.json()
# if data.get("status") == "success":
# bot_response = data.get("response", "后端未返回有效数据")
# #返回 Gradio Chatbot 需要 List[Dict[str, str]]
# chat_history.append({"role": "assistant", "content": bot_response})
# else:
# error_message = data.get("detail", "后端返回无效数据")
# chat_history.append({"role": "assistant", "content": error_message})
# else:
# error_message = f"后端错误: {response.status_code}"
# chat_history.append({"role": "assistant", "content": error_message})
# except Exception as e:
# error_message = f"通信失败: {str(e)}"
# chat_history.append({"role": "assistant", "content": error_message})
#
# return "", chat_history
def process_user_input(user_input, chat_history):
"""处理用户输入并更新聊天记录。"""
try:
# 构造请求数据
payload = {
"user_input": user_input,
"chat_history": chat_history # 发送 JSON 格式正确的 chat_history
}
# 发送 POST 请求到后端 API
response = requests.post(f"{BASE_URL}/chat", json=payload)
# 检查响应状态码
if response.status_code == 200:
data = response.json()
if data.get("status") == "success":
bot_response = data.get("response", "后端未返回有效数据")
chat_history.append({"role": "assistant", "content": bot_response})
else:
error_message = data.get("detail", "后端返回无效数据")
chat_history.append({"role": "assistant", "content": error_message})
else:
error_message = f"后端错误: {response.status_code}"
chat_history.append({"role": "assistant", "content": error_message})
except Exception as e:
error_message = f"通信失败: {str(e)}"
chat_history.append({"role": "assistant", "content": error_message})
return "", chat_history
def toggle_sidebar(expand):
"""切换侧边栏处理"""
if expand:
return gr.update(visible=True), gr.update(visible=False)
else:
return gr.update(visible=False), gr.update(visible=True)
def toggle_qrcode(show_qrcode):
"""显示或隐藏二维码窗口。"""
return gr.update(visible=show_qrcode)
def fill_input(text, user_input):
return text
def update_and_scroll(user_input, chat_history):
"""更新聊天记录并模拟滚动到底部"""
# Step 1: 立即更新用户输入到聊天记录中
if not chat_history:
chat_history = []
chat_history.append({"role": "user", "content": user_input})
# 返回清空的输入框和更新后的聊天记录(显示用户输入)
yield "", chat_history
# Step 2: 异步处理后端请求
_, updated_chat_history = process_user_input(user_input, chat_history)
# 返回最终结果(包含后端响应)
yield "", updated_chat_history
# def update_and_scroll(user_input, chat_history):
#
# # Step 1: 立即更新用户输入到聊天记录中
# if not chat_history:
# chat_history = []
# chat_history.append({"role": "user", "content": user_input})
#
# # 返回清空的输入框和更新后的聊天记录(显示用户输入)
# yield "", deepcopy(chat_history)
#
# # Step 2: 异步处理后端请求
# _, updated_chat_history = process_user_input(user_input, deepcopy(chat_history))
#
# # 返回最终结果(包含后端响应)
# yield "", deepcopy(updated_chat_history)
#调用后端
def save_and_clear_conversation(chat_history,user_id_state):
"""新建对话功能事件
1.保存当前对话记录到后端,并清空聊天记录。
:param chat_history: 当前的聊天记录(List[Dict[str, str]] 格式)
:param user_id_state: 用户 ID(用于标识用户)
:return: 清空后的聊天记录
"""
try:
# 将 chat_history 转换为后端所需的格式
formatted_conversation = []
for entry in chat_history:
role = entry.get("role", "")
content = entry.get("content", "")
if role == "user":
formatted_conversation.append({"user_input": content})
elif role == "assistant":
formatted_conversation.append({"bot_response": content})
# 构造请求数据
payload = {
"user_id": user_id_state,
"conversation": formatted_conversation
}
# 发送 POST 请求到后端 API
response = requests.post(f"{BASE_URL}/save_conversation", json=payload)
# 检查响应状态码
if response.status_code == 200:
print("对话记录保存成功")
else:
print(f"后端错误: {response.status_code}")
except Exception as e:
print(f"通信失败: {str(e)}")
# 清空聊天记录
return []
# # 定义全局变量用于存储聊天记录状态
# chat_history_state = gr.State([])
def main_interface(user_id_state):
"""主界面的封装函数"""
with gr.Column() as register_content:
# 插入自定义 CSS
gr.HTML("""
<style>
.custom-button {
width:50px;
height: 40px;
font-size: 14px;
}
/* 自定义 Chatbot 样式 */
.chatbot-wrap {
max-height: 1000px; /* 设置最大高度 */
overflow-y: auto; /* 启用垂直滚动条 */
border: 1px solid #ccc; /* 添加边框 */
padding: 10px; /* 内边距 */
border-radius: 8px; /* 圆角 */
}
/* 二维码窗口样式 */
.qrcode-window {
position: fixed; /* 固定定位 */
top: 20px;
right: 20px;
width: 250px;
background-color: white;
border: 1px solid #ccc;
padding: 15px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
z-index: 1000; /* 确保在最上层 */
}
.qrcode-window h3 {
margin-top: 0;
}
.qrcode-close-btn {
float: right;
cursor: pointer;
color: red;
}
</style>
""")
with gr.Row(elem_classes="highlight-border"):
with gr.Row(elem_classes="highlight-border"):
# toggle_button = gr.Button("切换侧边栏", elem_classes="send-btn2")
# 添加返回按钮
back_tologin_btn = gr.Button("退出", elem_classes="send-btn4", elem_id="back-to-login-btn")
with gr.Column(min_width=200, scale=1, visible=True,elem_classes="highlight-border") as sidebar_expanded:
# gr.Markdown("侧边栏展开")
gr.Image(value="panda.jpg", elem_classes="highlight-border")
new_chat_button = gr.Button("开启新对话", elem_classes="send-btn")
history_btn = gr.Button("历史记录", elem_classes="send-btn")
more_features_button = gr.Button("更多功能", elem_classes="send-btn")
favorites_button = gr.Button("收藏对话", elem_classes="send-btn")
settings_button = gr.Button("个人设置", elem_classes="send-btn")
mobile_download_button = gr.Button("手机端下载", elem_classes="send-btn")
desktop_download_button = gr.Button("电脑端下载", elem_classes="send-btn")
with gr.Column(min_width=100, scale=1, visible=False,elem_classes="highlight-border") as sidebar_collapsed:
gr.Markdown("缩小")
gr.Image(value="panda.jpg" ,elem_classes="highlight-border")
gr.Button("新对话", elem_classes="send-btn")
# 添加跳转按钮
history_button = gr.Button("查看历史记录", elem_id="history-btn")
gr.Button("更多", elem_classes="send-btn")
gr.Button("收藏", elem_classes="send-btn")
gr.Button("设置", elem_classes="send-btn")
gr.Button("手载", elem_classes="send-btn")
gr.Button("电载", elem_classes="send-btn")
# toggle_button.click(lambda: toggle_sidebar(True), outputs=[sidebar_expanded, sidebar_collapsed])
# toggle_button.click(lambda: toggle_sidebar(False), outputs=[sidebar_expanded, sidebar_collapsed])
with gr.Column(scale=4,elem_classes="highlight-border"):
gr.Markdown(
"""
<h1 style="font-size: 60px; color: #007BFF; text-align: center;">我是小希,很高兴与您交流</h1>
<p style="font-size: 24px; color: #333; text-align: center;">我可以帮你写代码、读文件、写作各种创意内容,请把你的任务交给我吧~</p>
"""
)
# 聊天历史记录组件
chat_history = gr.Chatbot(label="聊天框", elem_classes="chatbot-wrap",type="messages")
user_input = gr.Textbox(label="请输入您的问题", placeholder="宇宙超强大脑小希为您解忧消愁,摆脱一切烦恼!")
with gr.Row(elem_classes="highlight-border"):
# 左侧占位(可留空)
gr.HTML("")
gr.HTML("")
gr.HTML("")
gr.HTML("")
gr.HTML("")
submit_button = gr.Button("提交", elem_classes="send-btn3")
# 创建一个隐藏的文本框用于存储问题
hidden_textbox = gr.Textbox(visible=False)
# 使用 gr.Row 将三个按钮放在一行展示
with gr.Row(elem_classes="highlight-border"):
weather_question = gr.Button("贷款流程是什么?", elem_classes="send-btn2")
guide_question = gr.Button("贷款材料需要什么",elem_classes="send-btn2")
click_answer = gr.Button("点击就可解答",elem_classes="send-btn2")
weather_question.click(lambda: fill_input("贷款流程是什么?", hidden_textbox), outputs=hidden_textbox)
guide_question.click(lambda: fill_input("贷款材料需要什么", hidden_textbox), outputs=hidden_textbox)
click_answer.click(lambda: fill_input("点击就可解答", hidden_textbox), outputs=hidden_textbox)
# 将隐藏文本框的内容复制到用户输入框
hidden_textbox.change(lambda x: x, inputs=hidden_textbox, outputs=user_input)
# 手机端下载二维码窗口(悬浮窗口)
with gr.Column(visible=False, elem_classes="highlight-border") as qrcode_window:
gr.Markdown("### 扫码下载")
close_button = gr.Button("×", elem_classes="qrcode-close-btn")
gr.Image(value="WX.jpg", label="手机端下载二维码")
# 按钮绑定事件
mobile_download_button.click(lambda: toggle_qrcode(True), outputs=[qrcode_window])
close_button.click(lambda: toggle_qrcode(False), outputs=[qrcode_window])
# desktop_download_button.click(lambda: show_page("desktop_download"), outputs=[register_content, desktop_download_page])
# back_to_home_button.click(lambda: show_page("home"), outputs=[register_content, desktop_download_page])
# 其他按钮事件
# 在 main_interface 函数中绑定 new_chat_button 的事件
new_chat_button.click(
save_and_clear_conversation,
inputs=[chat_history,user_id_state],
outputs=[chat_history]
)
history_button.click()
history_btn.click()
more_features_button.click()
favorites_button.click()
settings_button.click()
# 将按钮和 Textbox 的 Enter 键绑定到同一个回调函数
submit_button.click(
update_and_scroll,
inputs=[user_input, chat_history],
outputs=[user_input, chat_history]
)
# 监听 Enter 键事件
user_input.submit(
update_and_scroll,
inputs=[user_input, chat_history],
outputs=[user_input, chat_history]
)
return register_content, history_btn, history_button, back_tologin_btn
Register.py
import gradio as gr
import requests
from config import BASE_URL
def send_verification_code(phone_number, status_text):
"""调用后端发送验证码接口"""
if not phone_number.isdigit() or len(phone_number) != 11:
return gr.update(value="⚠️ 手机号格式不正确"), status_text
try:
# 模拟调用后端发送验证码接口
response = requests.post(
f"{BASE_URL}/send-code", # 使用 BACKEND_URL
json={"phone_number": phone_number}
)
if response.status_code == 200:
# 返回成功消息
return gr.update(value=f"✅ 验证码已发送至 {phone_number}"), status_text
else:
# 返回错误消息
error_message = response.json().get("message", "未知错误")
return gr.update(value=f"❌ 发送失败:{error_message}"), status_text
except Exception as e:
# 捕获网络错误
return gr.update(value=f"❌ 网络错误:{str(e)}"), status_text
def register_user(phone, code, username, password, confirm_pwd, status_text):
"""调用后端注册接口"""
# 验证密码一致性
if password != confirm_pwd:
return gr.update(value="⚠️ 两次输入的密码不一致"), status_text
try:
# 调用后端注册接口
response = requests.post(
f"{BASE_URL}/register", # 使用 BACKEND_URL
json={
# "phone_number": phone,
# "code": code,
"username": username,
"password": password,
# "confirm_password": confirm_pwd
}
)
if response.status_code == 200:
# 注册成功
return gr.update(value="🎉 注册成功!"), status_text
else:
# 注册失败,返回错误信息
error_message = response.json().get("message", "注册失败")
return gr.update(value=f"❌ {error_message}"), status_text
except Exception as e:
# 捕获网络错误
return gr.update(value=f"❌ 网络错误:{str(e)}"), status_text
def register_interface():
"""注册界面的封装函数"""
with gr.Column() as register_content: # 移除 css 参数
gr.Markdown("# 用户注册", elem_classes="centered-containerR")
with gr.Column(elem_classes="gradio-containerR"):
with gr.Row(elem_classes="highlight-border"):
gr.Button("手机号注册", variant="secondary")
with gr.Row(elem_classes="highlight-border"):
username = gr.Textbox(label="用户名(必填)", placeholder="请输入用户名")
with gr.Row(elem_classes="highlight-border"):
phone = gr.Textbox(label="+86(中国) 手机号", placeholder="请输入手机号")
password = gr.Textbox(label="密码(必填)", type="password", placeholder="请输入密码")
with gr.Row(elem_classes="highlight-border"):
code_input = gr.Textbox(label="短信验证码", placeholder="请输入收到的6位验证码")
confirm_password = gr.Textbox(label="确认密码(必填)", type="password", placeholder="请确认密码")
with gr.Row(elem_classes="highlight-border"):
send_code_btn = gr.Button("获取验证码", variant="primary", elem_classes="send-btn3")
with gr.Row(elem_classes="highlight-border"):
gr.Markdown("已有账号,[去登录](#) 返回到初始界面", elem_classes="centered-containerR")
register_btn = gr.Button("立即注册", variant="success")
# 添加返回按钮
back_to_login_btn = gr.Button("返回登录", elem_id="back-to-login-btn")
status_text = gr.Textbox(
label="状态提示",
interactive=False
)
# # 绑定发送验证码事件
# send_code_btn.click(
# send_verification_code,
# inputs=[phone, status_text],
# outputs=[status_text]
# )
# 绑定注册事件
register_btn.click(
register_user,
inputs=[phone, code_input, username, password, confirm_password, status_text],
outputs=[status_text]
)
return register_content, send_code_btn, register_btn, back_to_login_btn
App.py
# 主文件,负责整合所有界面
import gradio as gr
from Login import login_interface
from Register import register_interface
from Main import main_interface
from History import history_interface,fetch_history
import global_vars
'''
通用返回值设计形式:
{
"status": "success/error", // 请求状态
"message": "操作成功的描述信息", // 成功时的提示信息
"data": { // 成功时的附加数据(可选)
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user_id": 12345
},
"detail": "失败时的具体原因" // 错误时的详细信息
}
'''
# 定义全局变量用于跟踪当前界面
current_page = "login"
# 定义跳转逻辑
def navigate_to_register():
"""从登录界面跳转到注册界面"""
return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
def navigate_to_login():
"""从注册界面跳转到登录界面"""
return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
def navigate_to_main2():
"""从登录界面跳转到主界面"""
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
def navigate_to_main1(login_result, should_redirect):
"""根据登录结果决定是否跳转到主界面。"""
if should_redirect:
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
else:
return gr.update(), gr.update() # 不跳转,保持当前界面
def navigate_to_history():
"""从主界面跳转到历史记录界面"""
# print(global_vars.user_id_state)
# # update_history(user_id_state)
# user_id_state.value = global_vars.user_id_state
# # history_content,back_to_main_btn,change_btn,history_output, search_box= history_interface(user_id_state)
# # print(global_vars.user_id_state)
# print(f"history_context: {history_content}")
# print(f"history_output: {history_output}")
# # history_context = "123456"
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True),gr.update(value=history_output.value)
def navigate_to_main_from_history():
"""从历史记录界面跳转到主界面"""
return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
def updateA_history(user_id_state):
history_data = fetch_history(user_id_state)
return gr.update(value=history_data)
# 定义全局 CSS 样式
css = """
.gradio-containerR{ /* Register的相关 */
max-width: 50%;
margin: 40px auto;
padding: 40px;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.centered-containerR{
text-align: center;
}
.input-rowR{
display: flex;
justify-content: space-between;
margin-bottom: 16px;
}
.input-rowR > * {
width: 48%;
}
.button-rowR {
display: flex;
justify-content: center;
margin-top: 16px;
}
.gradio-containerL { /*登录界面*/
width: 840px; /* 固定宽度 */
height: 550px; /* 固定高度 */
margin: auto; /* 水平居中 */
display: flex; /* 使用 Flexbox 实现内容居中 */
align-items: center; /* 垂直居中 */
justify-content: center; /* 水平居中 */
border: 6px solid #e0e0e0; /* 外边框 */
border-radius: 16px; /* 圆角 */
box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1); /* 阴影效果 */
background: linear-gradient(145deg, #ffffff, #f8f9fa); /* 渐变背景 */
}
.verification-boxL {height: 60px; margin-bottom: 15px;}
.send-btnL {width: 100%; background: #4CAF50; color: white; border: none; padding: 12px 0;}
#custom-galleryL {background-color: #f0feee !important; border-radius: 8px;}
/* 新增样式 */
.button-primary {
background-color: #4CAF50 !important;
color: white !important;
border: none !important;
padding: 10px 20px !important;
font-size: 16px !important;
cursor: pointer !important;
}
.button-secondar {
background-color: #f0f0f0 !important;
color: black !important;
border: none !important;
padding: 10px 20px !important;
font-size: 16px !important;
cursor: pointer !important;
}
.send-btn {
width: 80%;
height: 50%; /* 固定高度 */
background-color: #F4A460; /* */
color: black; /* 白色文字 */
font-size: 16px;
border: none;
border-radius: 5px;
cursor: pointer;
/* 居中对齐 */
display: block; /* 块级元素:将按钮设置为块级元素,以便可以使用 margin 属性进行居中 */
margin-left: 30px; /* 左外边距自动:水平方向左对齐 */
margin-right: 10px; /* 右外边距自动:水平方向右对齐 */
margin-top: 5px; /* 上外边距:设置按钮距离上方 10px 的间距(可选) */
margin-bottom: 5px; /* 下外边距:设置按钮距离下方 10px 的间距(可选) */
}
.send-btn2 {
# width: 50px;
# height: 50px; /* 较小的高度 */
background-color: #8B003; /* 木色 */
color: white;
font-size: 18px;
border: none;
border-radius: 5px;
cursor: pointer;
/* 居中对齐 */
display: block; /* 块级元素:将按钮设置为块级元素,以便可以使用 margin 属性进行居中 */
margin-left: 80px; /* 左外边距自动:水平方向左对齐 */
margin-right: 80px; /* 右外边距自动:水平方向右对齐 */
margin-top: 5px; /* 上外边距:设置按钮距离上方 10px 的间距(可选) */
margin-bottom: 5px; /* 下外边距:设置按钮距离下方 10px 的间距(可选) */
}
.send-btn3 {
width: 50px; /* 自动宽度:按钮宽度根据内容自动调整 */
height: 50px; /* 中等高度:设置按钮的固定高度为 35px */
background-color: #4CAF50; /* 蓝色背景:设置按钮的背景颜色为蓝色 (#2196F3) */
color: white; /* 白色文字:设置按钮的文字颜色为白色 */
font-size: 15px; /* 字体大小:设置按钮文字的字体大小为 15px */
border: none; /* 无边框:移除按钮的默认边框 */
border-radius: 20px; /* 圆角:设置按钮的圆角半径为 5px,使其看起来更柔和 */
cursor: pointer; /* 鼠标悬停时显示手型光标:提示用户该按钮是可点击的 */
/* 居中对齐 */
display: block; /* 块级元素:将按钮设置为块级元素,以便可以使用 margin 属性进行居中 */
margin-left: auto; /* 左外边距自动:将按钮推到右边 */
margin-right: 0; /* 右外边距为 0:确保按钮紧贴容器右边 */
}
.send-btn4 {
width: 50px; /* 自动宽度:按钮宽度根据内容自动调整 */
height: 50px; /* 中等高度:设置按钮的固定高度为 35px */
background-color: #808080; /* 蓝色背景:设置按钮的背景颜色为蓝色 (#2196F3) */
color: white; /* 白色文字:设置按钮的文字颜色为白色 */
font-size: 15px; /* 字体大小:设置按钮文字的字体大小为 15px */
border: none; /* 无边框:移除按钮的默认边框 */
border-radius: 5px; /* 圆角:设置按钮的圆角半径为 5px,使其看起来更柔和 */
cursor: pointer; /* 鼠标悬停时显示手型光标:提示用户该按钮是可点击的 */
/* 居中对齐 */
display: block; /* 块级元素:将按钮设置为块级元素,以便可以使用 margin 属性进行居中 */
margin-left: 0; /* 左外边距自动:水平方向左对齐 */
margin-right: 0; /* 右外边距自动:水平方向右对齐 */
margin-top: 0; /* 上外边距:设置按钮距离上方 10px 的间距(可选) */
margin-bottom: 0; /* 下外边距:设置按钮距离下方 10px 的间距(可选) */
}
.highlight-border {
border: 2px solid #007BFF; /* 蓝色边框 */
padding: 10px; /* 内边距 */
margin: 5px; /* 外边距 */
border-radius: 5px; /* 圆角 */
}
"""
# 创建主应用
with gr.Blocks(title="知识库问答系统", css=css) as demo:
# 创建一个标题
gr.Markdown("# 知识库问答系统")
Login_state = 0
# 登录界面
with gr.Row(visible=True) as login_row:
login_content, register_button, login_code_button, login_pwd_button ,should_redirect,login_result,username = login_interface()
# 定义全局变量用于存储用户信息
user_id_state = username
print(f"user_id_state: {global_vars.user_id_state}")
# 注册界面
with gr.Row(visible=False) as register_row:
register_content, send_code_btn, register_btn,back_to_login_btn = register_interface()
# 主界面
with gr.Row(visible=False) as main_row:
main_content,history_btn,history_button,back_tologin_btn= main_interface(user_id_state)
# 历史记录界面
with gr.Row(visible=False) as history_row:
history_content,back_to_main_btn,change_btn,history_output,search_box,time_period_dropdown = history_interface(user_id_state)
from History import update_history
search_box.change(
lambda search_query, user_id: update_history(user_id, search_query),
inputs=[search_box, user_id_state],
outputs=[history_output]
)
# 绑定按钮事件
register_button.click(
navigate_to_register,
inputs=[],
outputs=[login_row, register_row, main_row, history_row]
)
back_to_login_btn.click(
navigate_to_login,
inputs=[],
outputs=[login_row, register_row, main_row, history_row]
)
back_tologin_btn.click(
navigate_to_login,
inputs=[],
outputs=[login_row, register_row, main_row, history_row]
)
# 密码登录跳转绑定
login_pwd_button.click(
navigate_to_main1,
inputs=[login_result, should_redirect],
outputs=[login_row, register_row, main_row, history_row] # 假设 main_row 是主界面,login_row 是登录界面
)
# 验证码跳转绑定
login_code_button.click(
navigate_to_main2,
inputs=[],
outputs=[login_row, register_row, main_row, history_row]
)
history_btn.click(
navigate_to_history,
inputs=[],
outputs=[login_row, register_row, main_row, history_row, history_output]
).then(
fn=updateA_history,
inputs=[user_id_state],
outputs=[history_output]
)
# 跳转历史记录绑定
history_button.click(
navigate_to_history,
inputs=[],
outputs=[login_row, register_row, main_row, history_row, history_output]
)
back_to_main_btn.click(
navigate_to_main_from_history,
inputs=[],
outputs=[login_row, register_row, main_row, history_row]
)
# 监听时间范围选择器和搜索框的变化,动态更新历史记录
def on_change(user_id, time_period, search_query):
return update_history(user_id, time_period, search_query)
time_period_dropdown.change(
on_change,
inputs=[user_id_state, time_period_dropdown, search_box],
outputs=[history_output]
)
search_box.change(
on_change,
inputs=[user_id_state, time_period_dropdown, search_box],
outputs=[history_output]
)
# change_btn.click(
# change_function,
# inputs=[user_id_state],
# outputs=[login_row, register_row, main_row, history_row]
# )
# 启动应用
demo.launch(server_name="0.0.0.0")
五、效果展示
这是前端实现完与后端进行交互之后的结果,相应的注册信息,聊天记录都是存在数据库中。
-
首先进行注册: 这里手机号功能后端暂未实现,只用输入用户名和密码。点击注册,会与后端进行交互存储用户信息,返回一个结果,前端根据返回结果进行相应的提示(注册成功!)
-
登录过程,验证码、手机登录后端暂未实现,暂时支持密码登录(输入注册的用户名和密码),系统根据信息会给一个返回值,根据结果显示状态(登陆成功!):
-
历史记录,这块是为了记录我们历史对话过程,刚注册的账号没有对话记录:
- 一轮对话:当输入问题并且有回复就说明我们与后端的交互是没有问题的,后端处理请求是基于数据库回答,在数据库中没有的情况下基于千问大模型接口来进行回答。
第一轮对话首个问题: 贷款材料需要什么
多轮对话:在第一轮对话之后,提问回答过程中的相关内容,看它是否有分析检索的能力(这部分内容是数据库中没有的)
- 验证基于数据库与大模型的回答
以下问题是数据库中的问题,看是否可以根据数据库中的内容直接回答。
这是第二次会话: 你好,银行贷款的五级分类
同样的问题,这是在千问中请求的结果,对比来看,回答的形式不同。
再查看数据库中的内容,这部分是完全直接输出给用户请求了。说明首先还是基于数据库进行回复的。
下图为再次咨询回答中某一条相关信息的具体内容的时候,他回复的在数据库中并没有,是根据学习数据库中的内容以及借助千问大模型给出的回复。
再重新进行一轮对话,我们问问数据库中没有的,第三轮对话内容:我有个数学难题不会解决,1+2等于,是可以正常输出的,此时就是调用千问接口进行回复的。
6. 查看历史会话记录
根据我们前面三次会话的第一问作为超链接显示某个会话,如图所示。
前端基本实现了,与刚开始设计的界面多少有差距,但是整体交互逻辑没问题。部分内容没有更新到位,希望这个笔记能更好的促进我们使用gradio,也期待宝子们的实践成果。
后端的内容请学习以下文章内容:
ollama+qwen2.5+nomic本地部署及通过API调用模型示例
使用FastAPI为知识库问答系统前端提供后端功能接口