用 Streamlit 構建一個簡易對話機器人 UI

在這篇文章中,我將演示如何用 Streamlit 快速構建一個輕量的對話機器人 UI,并通過 LangChain / LangGraph 調用 LLM,實現簡單的對話功能。通過將前端和后端分離,你可以單獨測試模型調用和 UI 顯示。

為什么選擇 Streamlit?

Streamlit 是一個專為 Python 數據應用設計的前端框架,特點是:

  • 極簡化前端開發,只需 Python 代碼即可構建 Web 應用。

  • 與 Python 生態兼容,方便集成機器學習、LLM 等工具。

  • 交互組件豐富,如表單、滑塊、下拉框等。

通過 Streamlit,我們可以專注于業務邏輯,而不用寫復雜的 HTML/CSS/JS。

系統架構

我們將系統拆分為兩部分:

  1. 后端 (backend.py)

    • 管理對話狀態(狀態圖)

    • 調用 LLM(如 ChatTongyi)

    • 處理對話記憶和存儲(SQLite)

  2. 前端 (app.py)

    • 使用 Streamlit 顯示對話界面

    • 負責收集用戶輸入

    • 調用后端生成模型回復

這種架構有幾個好處:

  • UI 與后端解耦,可單獨測試

  • 后端邏輯可復用到其他應用或接口

  • 數據存儲統一管理,方便擴展

后端實現 (backend.py)

import sqlite3
from typing import Annotated, Listfrom langchain_core.messages import AnyMessage, HumanMessage
from langchain_community.chat_models import ChatTongyifrom langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.sqlite import SqliteSaver
from typing_extensions import TypedDict# ============ 狀態定義 ============
class ChatState(TypedDict):messages: Annotated[List[AnyMessage], add_messages]# ============ 節點函數 ============
def make_call_model(llm: ChatTongyi):"""返回一個節點函數,讀取 state -> 調用 LLM -> 更新 state.messages"""def call_model(state: ChatState) -> ChatState:response = llm.invoke(state["messages"])return {"messages": [response]}return call_model# ============ 后端核心類 ============
class ChatBackend:def __init__(self, api_key: str, model_name: str = "qwen-plus", temperature: float = 0.7, db_path: str = "memory.sqlite"):self.llm = ChatTongyi(model=model_name, temperature=temperature, api_key=api_key)self.builder = StateGraph(ChatState)self.builder.add_node("model", make_call_model(self.llm))self.builder.set_entry_point("model")self.builder.add_edge("model", END)# SQLite 檢查點conn = sqlite3.connect(db_path, check_same_thread=False)self.checkpointer = SqliteSaver(conn)self.app = self.builder.compile(checkpointer=self.checkpointer)def chat(self, user_input: str, thread_id: str):config = {"configurable": {"thread_id": thread_id}}final_state = self.app.invoke({"messages": [HumanMessage(content=user_input)]}, config)ai_text = final_state["messages"][-1].contentreturn ai_textif __name__ == '__main__':from backend import ChatBackendbackend = ChatBackend(api_key="YOUR_KEY")print(backend.chat("你好", "thread1"))

前端實現 (app_ui.py)

import os
import uuid
import streamlit as st
from backend import ChatBackendst.set_page_config(page_title="Chatbot", page_icon="💬", layout="wide")# ======== Streamlit CSS =========
st.markdown("""<style>.main {padding: 2rem 2rem;}.chat-card {background: rgba(255,255,255,0.65); backdrop-filter: blur(8px); border-radius: 20px; padding: 1.25rem; box-shadow: 0 10px 30px rgba(0,0,0,0.08);} .msg {border-radius: 16px; padding: 0.8rem 1rem; margin: 0.35rem 0; line-height: 1.5;}.human {background: #eef2ff;}.ai {background: #ecfeff;}.small {font-size: 0.86rem; color: #6b7280;}.tag {display:inline-block; padding: .25rem .6rem; border-radius: 9999px; border: 1px solid #e5e7eb; margin-right:.4rem; cursor:pointer}.tag:hover {background:#f3f4f6}.footer-note {color:#9ca3af; font-size:.85rem}[data-testid="stSidebarHeader"] {margin-bottom: 0px}/* 讓主標題上移,和側邊欄對齊 */.block-container {padding-top: 1.5rem !important;}</style>""",unsafe_allow_html=True,
)# ======== 側邊欄配置 =========
with st.sidebar:st.markdown("## ?? 配置")if "thread_id" not in st.session_state:st.session_state.thread_id = str(uuid.uuid4())if "chat_display" not in st.session_state:st.session_state.chat_display = []api_key = st.text_input("DashScope API Key", type="password", value=os.getenv("DASHSCOPE_API_KEY", ""))model_name = st.selectbox("選擇模型", ["qwen-plus", "qwen-turbo", "qwen-max"], index=0)temperature = st.slider("Temperature", 0.0, 1.0, 0.7, 0.05)if st.button("? 新建會話"):st.session_state.thread_id = str(uuid.uuid4())st.session_state.chat_display = []st.rerun()if st.button("🗑? 清空當前會話"):st.session_state.chat_display = []st.rerun()# ======== 主區域 =========
st.markdown("# 💬 Chatbot")
if not api_key:st.warning("請先在左側輸入 DashScope API Key 才能開始對話。")
else:backend = ChatBackend(api_key=api_key, model_name=model_name, temperature=temperature)st.markdown('<div class="chat-card">', unsafe_allow_html=True)if not st.session_state.chat_display:st.info("開始對話吧!")else:for role, content in st.session_state.chat_display:css_class = "human" if role == "user" else "ai"avatar = "🧑?💻" if role == "user" else "🤖"st.markdown(f"<div class='msg {css_class}'><span class='small'>{avatar} {role}</span><br/>{content}</div>", unsafe_allow_html=True)with st.form("chat-form", clear_on_submit=True):user_input = st.text_area("輸入你的問題/指令:", height=100, placeholder="比如:幫我寫一個二分查找函數。")submitted = st.form_submit_button("發送 ?")if submitted and user_input.strip():st.session_state.chat_display.append(("user", user_input))with st.spinner("思考中..."):ai_text = backend.chat(user_input, st.session_state.thread_id)st.session_state.chat_display.append(("assistant", ai_text))st.rerun()st.markdown('</div>', unsafe_allow_html=True)st.markdown(f"<div class='footer-note'>Session: <code>{st.session_state.thread_id}</code></div>", unsafe_allow_html=True)

運行效果

  1. 運行后端服務:無需額外啟動,backend.pyapp.py 調用即可。

  2. 在瀏覽器中訪問 Streamlit 頁面:

streamlit run app_ui.py
  1. 左側輸入 API Key,選擇模型和溫度。

  2. 在主區域輸入問題或指令,點擊“發送”,AI 回復將顯示在聊天窗口。

效果示例:

總結與擴展

通過這套架構,我們實現了:

  • 前后端解耦:UI 與 LLM 調用分離,可單獨測試

  • 對話記憶管理:使用 SQLite 保存會話狀態

  • 可擴展性:后端可替換不同 LLM 或添加多輪對話邏輯

其它

如果你的模型支持流式輸出

backend_stream.py
import os
import sqlite3
import time
from typing import Annotated, Listfrom dotenv import load_dotenv
from langchain_core.messages import AnyMessage, HumanMessage
from langchain_community.chat_models import ChatTongyi
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.sqlite import SqliteSaver
from typing_extensions import TypedDict
from langchain.callbacks.base import BaseCallbackHandler# ===== 狀態定義 =====
class ChatState(TypedDict):messages: Annotated[List[AnyMessage], add_messages]# ===== 節點函數(流式調用) =====
def make_call_model(llm: ChatTongyi):def call_model(state: ChatState) -> ChatState:# ? 這里改為流式responses = []for chunk in llm.stream(state["messages"]):responses.append(chunk)return {"messages": responses}return call_model# ===== 流式回調 =====
class StreamCallbackHandler(BaseCallbackHandler):def __init__(self):self.chunks = []def on_llm_new_token(self, token: str, **kwargs):self.chunks.append(token)# ===== 后端核心 =====
class ChatBackend:def __init__(self, api_key: str, model_name: str = "qwen-plus", temperature: float = 0.7, db_path: str = "memory.sqlite"):self.llm = ChatTongyi(model=model_name,temperature=temperature,api_key=api_key,streaming=True,  # 開啟流式)self.builder = StateGraph(ChatState)self.builder.add_node("model", make_call_model(self.llm))self.builder.set_entry_point("model")self.builder.add_edge("model", END)conn = sqlite3.connect(db_path, check_same_thread=False)self.checkpointer = SqliteSaver(conn)self.app = self.builder.compile(checkpointer=self.checkpointer)def chat_stream_simulated(self, user_input: str):"""模擬流式輸出(調試用)"""response = f"AI 正在回答你的問題: {user_input}"for ch in response:yield chtime.sleep(0.05)def chat_stream(self, user_input: str, thread_id: str):"""真正的流式輸出"""config = {"configurable": {"thread_id": thread_id}}# 用 llm.stream() 獲取流式輸出for chunk in self.llm.stream([HumanMessage(content=user_input)]):token = chunk.text()if token:yield token
app_stream_ui.py
# app_stream_ui_no_thinking.py
import os
import time
import uuid
import streamlit as st
from backend_stream import ChatBackend  # 請確保 backend_stream.chat_stream 返回逐 token 字符串st.set_page_config(page_title="Chatbot", page_icon="💬", layout="wide")# ======== 原有 CSS(保持不變) =========
st.markdown("""<style>.main {padding: 2rem 2rem;}.chat-card {background: rgba(255,255,255,0.65); backdrop-filter: blur(8px); border-radius: 20px; padding: 1.25rem; box-shadow: 0 10px 30px rgba(0,0,0,0.08);} .msg {border-radius: 16px; padding: 0.8rem 1rem; margin: 0.35rem 0; line-height: 1.5;}.human {background: #eef2ff;}.ai {background: #ecfeff;}.small {font-size: 0.86rem; color: #6b7280;}.footer-note {color:#9ca3af; font-size:.85rem}[data-testid="stSidebarHeader"] { margin-bottom: 0px }.block-container { padding-top: 1.5rem !important; }</style>""",unsafe_allow_html=True,
)# ======== 側邊欄配置 =========
with st.sidebar:st.markdown("## ?? 配置")if "thread_id" not in st.session_state:st.session_state.thread_id = str(uuid.uuid4())if "chat_display" not in st.session_state:st.session_state.chat_display = []api_key = st.text_input("DashScope API Key", type="password", value=os.getenv("DASHSCOPE_API_KEY", ""))model_name = st.selectbox("選擇模型", ["qwen-plus", "qwen-turbo", "qwen-max"], index=0)temperature = st.slider("Temperature", 0.0, 1.0, 0.7, 0.05)if st.button("? 新建會話"):st.session_state.thread_id = str(uuid.uuid4())st.session_state.chat_display = []st.experimental_rerun()if st.button("🗑? 清空當前會話"):st.session_state.chat_display = []st.experimental_rerun()# ======== 主區域 =========
st.markdown("# 💬 Chatbot")if not api_key:st.warning("請先在左側輸入 DashScope API Key 才能開始對話。")
else:# 可以緩存 backend 到 session_state,但為簡單起見這里每次實例化(如需優化我可以幫你加緩存)backend = ChatBackend(api_key=api_key, model_name=model_name, temperature=temperature)# 聊天歷史占位(位于輸入上方)chat_area = st.container()history_ph = chat_area.empty()def render_history(ph):"""渲染 st.session_state.chat_display(使用你原來的 HTML + CSS 樣式)"""html = '<div class="chat-card">'for role, content in st.session_state.chat_display:css_class = "human" if role == "user" else "ai"avatar = "🧑?💻" if role == "user" else "🤖"html += f"<div class='msg {css_class}'><span class='small'>{avatar} {role}</span><br/>{content}</div>"html += "</div>"ph.markdown(html, unsafe_allow_html=True)# 初始渲染歷史render_history(history_ph)# 用戶輸入表單(表單在歷史下方,因此輸出始終在上面)with st.form("chat-form", clear_on_submit=True):user_input = st.text_area("輸入你的問題/指令:", height=100, placeholder="比如:幫我寫一個帶注釋的二分查找函數。")submitted = st.form_submit_button("發送 ?")if submitted and user_input.strip():# 1) 把用戶消息寫入歷史(立即可見)st.session_state.chat_display.append(("user", user_input))# 2) 插入 assistant 占位(空字符串),保證后續輸出顯示在上方歷史st.session_state.chat_display.append(("assistant", ""))ai_index = len(st.session_state.chat_display) - 1# 立刻渲染一次,使用戶看到自己的消息和空 assistant 氣泡(輸出會填充此氣泡)render_history(history_ph)# 3) 開始流式生成并實時寫回歷史full_text = ""try:for token in backend.chat_stream(user_input, st.session_state.thread_id):# token: 每次 yield 的字符串(可能是字符或片段)full_text += token# 更新 session 中的 assistant 占位內容(注意不要添加"思考中")st.session_state.chat_display[ai_index] = ("assistant", full_text)# 重新渲染歷史,保證輸出始終在輸入上方render_history(history_ph)# 小睡短暫時間幫助 Streamlit 刷新(按需調整或刪除)time.sleep(0.01)except Exception as e:# 如果生成出錯,把錯誤消息放到 assistant 氣泡里(替代原邏輯)st.session_state.chat_display[ai_index] = ("assistant", f"[生成出錯] {e}")render_history(history_ph)else:# 生成完成(full_text 已包含最終結果),已在循環中寫回,無需額外操作pass# 頁腳(session id)st.markdown(f"<div class='footer-note'>Session: <code>{st.session_state.thread_id}</code></div>", unsafe_allow_html=True)

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/920339.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/920339.shtml
英文地址,請注明出處:http://en.pswp.cn/news/920339.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【Redis 進階】Redis 典型應用 —— 緩存(cache)

一、什么是緩存 緩存&#xff08;cache&#xff09;是計算機中的一個經典的概念&#xff0c;在很多場景中都會涉及到。核心思路就是把一些常用的數據放到觸手可及&#xff08;訪問速度更快&#xff09;的地方&#xff0c;方便隨時讀取。 舉例&#xff1a;我需要去高鐵站坐高鐵…

RK3588 Ubuntu22.04 解決eth0未托管問題

在調試rk3588的Ubuntu的時候發現&#xff0c;網絡那里一直顯示eth0未托管&#xff0c;但是聯網功能又是正常的&#xff0c;猜測是某一個配置文件的問題修改如下&#xff1a;打開/etc/NetworkManager/NetworkManager.conf&#xff0c;將managed&#xff0c;修改成true即可然后重…

雷卯針對香橙派Orange Pi 3G-IoT-B開發板防雷防靜電方案

一、應用場景計算機、無線網絡服務器、游戲機、音樂播放器、高清視頻播放器、揚聲器、Android 設備、Scratch 編程平臺二、核心功能參數三、擴展接口詳情雷卯專心為您解決防雷防靜電的問題&#xff0c;有免費實驗室供檢測。開發板資料轉自深圳迅龍軟件。謝謝&#xff01;

Science Robotics 豐田研究院提出通過示例引導RL的全身豐富接觸操作學習方法

人類表現出非凡的能力&#xff0c;可以利用末端執行器&#xff08;手&#xff09;的靈巧性、全身參與以及與環境的交互&#xff08;例如支撐&#xff09;來縱各種大小和形狀的物體。 人類靈活性的分類法包括精細和粗略的作技能。盡管前者&#xff08;精細靈巧性&#xff09;已在…

趣丸游戲招高級業務運維工程師

高級業務運維工程師趣丸游戲 廣州職位描述1、負責公司AI業務線運維工作&#xff0c;及時響應、分析、處理問題和故障&#xff0c;保證業務持續穩定&#xff1b; 2、負責基于分布式、微服務、容器云等復雜業務的全生命周期的穩定性保障&#xff1b; 3、參與設計運維平臺、工具、…

2025通用證書研究:方法論、崗位映射與四證對比

本文基于公開材料與典型招聘描述&#xff0c;對常見通用型或準入型證書做方法論級別的比較&#xff0c;不構成培訓或報考建議&#xff0c;也不涉及任何招生、返現、團購等信息。全文采用統一術語與可復用模板&#xff0c;以減少“經驗之爭”&#xff0c;便于不同背景的讀者獨立…

在WSL2-Ubuntu中安裝Anaconda、CUDA13.0、cuDNN9.12及PyTorch(含完整環境驗證)

WSL 搭建深度學習環境&#xff0c;流程基本上是一樣的&#xff0c;完整細節可參考我之前的博客&#xff1a; 在WSL2-Ubuntu中安裝CUDA12.8、cuDNN、Anaconda、Pytorch并驗證安裝_cuda 12.8 pytorch版本-CSDN博客 之所以記錄下來&#xff0c;是因為CUDA和cuDNN版本升級后&#x…

OpenFOAM中梯度場的復用(caching)和生命期管理

文章目錄OpenFOAM中梯度場的復用(caching)和生命期管理一、緩存機制的目標二、如何實現緩存&#xff08;以 fvc::grad 為例&#xff09;1. 使用 IOobject::AUTO_WRITE 和注冊名2. 示例&#xff1a;fvc::grad 的緩存實現&#xff08;簡化邏輯&#xff09;三、生命期管理是如何實…

【Hot100】貪心算法

系列文章目錄 【Hot100】二分查找 文章目錄系列文章目錄方法論Hot100 之貪心算法121. 買賣股票的最佳時機55. 跳躍游戲45. 跳躍游戲 II763. 劃分字母區間方法論 Hot100 之貪心算法 121. 買賣股票的最佳時機 121. 買賣股票的最佳時機&#xff1a;給定一個數組 prices &#…

電子電氣架構 --- 軟件項目復雜性的駕馭思路

我是穿拖鞋的漢子,魔都中堅持長期主義的汽車電子工程師。 老規矩,分享一段喜歡的文字,避免自己成為高知識低文化的工程師: 做到欲望極簡,了解自己的真實欲望,不受外在潮流的影響,不盲從,不跟風。把自己的精力全部用在自己。一是去掉多余,凡事找規律,基礎是誠信;二是…

SSE實時通信與前端聯調實戰

1.SSE 原理機制 sse 類似websocket,但是sse是單向的&#xff0c;不可逆的&#xff0c;只能服務端向客戶端發送數據流 2.解決跨域問題 Access to XMLHttpRequest at http://127.0.0.1:8090/sse/doChat from origin http://127.0.0.1:3000 has been blocked by CORS policy: Re…

從傳統到創新:用報表插件重塑數據分析平臺

一、傳統 BI 平臺面臨的挑戰 在當今數字化時代&#xff0c;數據已成為企業決策的重要依據。傳統的商業智能&#xff08;BI&#xff09;平臺在數據處理和分析方面發揮了重要作用&#xff0c;但隨著數據量的爆炸式增長和用戶需求的日益多樣化&#xff0c;其局限性也逐漸顯現。 …

MySQL--MySQL中的DECIMAL 與 Java中的BigDecimal

1. 為什么需要 DECIMAL在數據庫中&#xff0c;常見的數值類型有&#xff1a;INT、BIGINT → 整數&#xff0c;存儲容量有限。FLOAT、DOUBLE → 浮點數&#xff0c;存儲效率高&#xff0c;但存在精度丟失問題。DECIMAL(M, D) → 定點數&#xff0c;存儲精確值。例子&#xff1a;…

低空無人機系統關鍵技術與應用前景:SmartMediaKit視頻鏈路的基石價值

引言&#xff1a;低空經濟的新興格局 低空經濟作為“新質生產力”的代表&#xff0c;正在從政策驅動、技術突破和市場需求的共振中走向產業化。2023年&#xff0c;中國低空經濟的市場規模已超過 5000 億元人民幣&#xff0c;同比增長超過 30%。無人機&#xff08;UAV&#xff…

在Windows系統上升級Node.js和npm

在Windows系統上升級Node.js和npm&#xff0c;我推薦以下幾種方法&#xff1a; 方法1&#xff1a;使用官網安裝包&#xff08;最簡單&#xff09; 訪問 nodejs.org 下載Windows安裝包&#xff08;.msi文件&#xff09; 運行安裝包&#xff0c;選擇"修復"或直接安裝新…

【Jetson】基于llama.cpp部署gpt-oss-20b(推理與GUI交互)

前言 本文在jetson設備上使用llama.cpp完成gpt-oss 20b的部署&#xff0c;包括后端推理和GUI的可視化交互。 使用的設備為orin nx 16g&#xff08;super&#xff09;&#xff0c;這個顯存大小推理20b的模型完全沒有問題。 使用硬件如下&#xff0c;支持開啟super模式。&#…

Matplotlib 可視化大師系列(一):plt.plot() - 繪制折線圖的利刃

目錄Matplotlib 可視化大師系列博客總覽Matplotlib 可視化大師系列&#xff08;一&#xff09;&#xff1a;plt.plot() - 繪制折線圖的利刃一、 plt.plot() 是什么&#xff1f;二、 函數原型與核心參數核心參數詳解三、 從入門到精通&#xff1a;代碼示例示例 1&#xff1a;最基…

第二階段Winfrom-8:特性和反射,加密和解密,單例模式

1_預處理指令 &#xff08;1&#xff09;源代碼指定了程序的定義&#xff0c;預處理指令&#xff08;preprocessor directive&#xff09;指示編譯器如何處理源代碼。例如&#xff0c;在某些情況下&#xff0c;我們希望編譯器能夠忽略一部分代碼&#xff0c;而在其他情況下&am…

【開題答辯全過程】以 微信小程序的醫院掛號預約系統為例,包含答辯的問題和答案

個人簡介一名14年經驗的資深畢設內行人&#xff0c;語言擅長Java、php、微信小程序、Python、Golang、安卓Android等開發項目包括大數據、深度學習、網站、小程序、安卓、算法。平常會做一些項目定制化開發、代碼講解、答辯教學、文檔編寫、也懂一些降重方面的技巧。感謝大家的…

鴻蒙ArkUI 基礎篇-06-組件基礎語法-Column/Row/Text

目錄 掌握組件寫法&#xff0c;使用組件布局界面 ArkUI與組件 先布局再內容 DevEco Studio代碼實戰 預覽效果 總結 練習 掌握組件寫法&#xff0c;使用組件布局界面 ArkUI與組件 ArkUI&#xff08;方舟開發框架&#xff09;&#xff1a;構建 鴻蒙 應用 界面 的框架 組件…