用 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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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;构建 鸿蒙 应用 界面 的框架 组件…