從零搭建大模型問答系統-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_URLdef 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 請求到后端 APIresponse = 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_dataelse: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 = usernamereturn "登錄成功!", 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 而不是 Blocksgr.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 requestsfrom 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_historydef process_user_input(user_input, chat_history):"""處理用戶輸入并更新聊天記錄。"""try:# 構造請求數據payload = {"user_input": user_input,"chat_history": chat_history # 發送 JSON 格式正確的 chat_history}# 發送 POST 請求到后端 APIresponse = 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_historydef 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 textdef 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 請求到后端 APIresponse = 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:# 插入自定義 CSSgr.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_URLdef send_verification_code(phone_number, status_text):"""調用后端發送驗證碼接口"""if not phone_number.isdigit() or len(phone_number) != 11:return gr.update(value="?? 手機號格式不正確"), status_texttry:# 模擬調用后端發送驗證碼接口response = requests.post(f"{BASE_URL}/send-code", # 使用 BACKEND_URLjson={"phone_number": phone_number})if response.status_code == 200:# 返回成功消息return gr.update(value=f"? 驗證碼已發送至 {phone_number}"), status_textelse:# 返回錯誤消息error_message = response.json().get("message", "未知錯誤")return gr.update(value=f"? 發送失敗:{error_message}"), status_textexcept Exception as e:# 捕獲網絡錯誤return gr.update(value=f"? 網絡錯誤:{str(e)}"), status_textdef register_user(phone, code, username, password, confirm_pwd, status_text):"""調用后端注冊接口"""# 驗證密碼一致性if password != confirm_pwd:return gr.update(value="?? 兩次輸入的密碼不一致"), status_texttry:# 調用后端注冊接口response = requests.post(f"{BASE_URL}/register", # 使用 BACKEND_URLjson={# "phone_number": phone,# "code": code,"username": username,"password": password,# "confirm_password": confirm_pwd})if response.status_code == 200:# 注冊成功return gr.update(value="🎉 注冊成功!"), status_textelse:# 注冊失敗,返回錯誤信息error_message = response.json().get("message", "注冊失敗")return gr.update(value=f"? {error_message}"), status_textexcept Exception as e:# 捕獲網絡錯誤return gr.update(value=f"? 網絡錯誤:{str(e)}"), status_textdef 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 = usernameprint(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_historysearch_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為知識庫問答系統前端提供后端功能接口