前途科技
  • 科技
  • AI
    • AI 前沿技术
    • Agent生态
    • AI应用场景
    • AI 行业应用
  • 初创
  • 报告
  • 学习中心
    • 编程与工具
    • 数据科学与工程
我的兴趣
前途科技前途科技
Font ResizerAa
站内搜索
Have an existing account? Sign In
Follow US
Copyright © 2024 AccessPath.com, 前途国际科技咨询(北京)有限公司,版权所有。 | 京ICP备17045010号-1 | 京公网安备 11010502033860号
大模型与工程化

Streamlit与Chainlit:快速构建与部署智能聊天机器人

NEXTECH
Last updated: 2025年9月22日 上午10:21
By NEXTECH
Share
121 Min Read
SHARE

快速原型开发(Rapid Prototyping)是指构建产品简易版本并持续收集用户反馈的过程,旨在快速验证重要的假设,评估关键风险。这种方法与敏捷软件开发实践、“精益创业”(Lean Startup)方法论中的“构建-测量-学习”循环紧密结合,能够显著降低开发成本并缩短产品上市时间。鉴于人工智能相关技术、用例和用户期望仍处于早期阶段,快速原型开发对于成功交付AI产品尤为重要。

Contents
端到端聊天机器人演示实用指南

为此,Streamlit于2019年发布,作为一个Python框架,它简化了需要用户界面(UI)的AI应用原型开发过程。数据科学家和工程师可以专注于后端部分(例如,训练机器学习模型并通过API暴露预测接口),而Streamlit仅需几行Python代码即可生成一个用户友好且可定制的UI。Chainlit同样是一个Python框架,于2023年发布,专门用于解决会话式AI应用(即聊天机器人)原型开发中的痛点。虽然Streamlit和Chainlit在某些方面相似,但它们之间也存在重要的差异。本文将通过构建端到端的聊天机器人演示应用,探讨这两个框架的优缺点,并提供实用的建议。

注意:以下各节中的所有图表均为示意图。

端到端聊天机器人演示

本地环境设置

为了简化起见,演示应用程序的构建旨在便于在本地环境中测试,采用通过Ollama访问的开源大型语言模型(LLM)。Ollama是一个用户友好的工具,用于在本地机器上下载、管理和交互开源LLM。

当然,这些演示后期可以修改以用于生产环境,例如,通过利用OpenAI或Google等公司提供的最新LLM,并将聊天机器人部署在AWS、Azure或GCP等常用超大规模云平台上。以下所有实现步骤均已在macOS Sequoia 15.6.1上测试通过,在Linux和Windows上的操作应大致相似。

请点击此处下载并安装Ollama。通过在终端中运行以下命令检查安装是否成功:

You Might Also Like

AI驱动供应链网络优化:连接MCP服务器与FastAPI微服务的实践
构建一个真正高效的KPI监控系统:实用策略与挑战应对
深入Triton:从向量加法看高性能GPU编程,为大模型优化提速
Qwen3 Omni 的“全模态”:与多模态大模型的本质差异解析

ollama --version
本文将使用谷歌轻量级Gemma 2模型,该模型具有2B参数,可通过以下命令下载:

ollama pull gemma:2b
模型文件大小约为1.7 GB,因此下载可能需要几分钟,具体取决于网络连接速度。使用以下命令验证模型是否已下载:

ollama list
此命令将显示通过Ollama已下载的所有模型。

接下来,将使用uv来设置项目目录,uv是一个快速且用户友好的Python项目管理工具。请按照此处的说明安装uv,并通过以下命令验证安装:

uv --version
在本地机器上的合适位置初始化一个名为chatbot-demos的项目目录,操作如下:

uv init --bare chatbot-demos
如果未指定--bare选项,uv会在初始化期间创建一些标准文件,例如main.py、README.md和一个Python版本锁定文件,但这些文件在本次演示中并不需要。最小化过程仅创建一个pyproject.toml文件。

在chatbot-demos项目目录中,创建一个requirements.txt文件,包含以下依赖项:

chainlit==2.7.2
ollama==0.5.3
streamlit==1.49.1

现在,在项目目录内创建一个Python 3.12虚拟环境,激活该环境并安装依赖项:

uv venv --python=3.12 
source .venv/bin/activate
uv add -r requirements.txt

检查依赖项是否已安装:

uv pip list
将实现一个名为LLMClient的类,用于处理后端功能,该功能可以与以UI为中心的功能解耦,这是Streamlit和Chainlit等框架的关键区别所在。例如,LLMClient可以负责选择LLM提供商、执行LLM调用、与外部数据库交互以实现检索增强生成(RAG),以及记录对话历史以供后续分析。以下是LLMClient的一个示例实现,保存在名为llm_client.py的文件中:

import logging
import time
from datetime import datetime, timezone
from typing import List, Dict, Optional, Callable, Any, Generator
import os
import ollama

LOG_FILE = os.path.join(os.path.dirname(__file__), "conversation_history.log")

logger = logging.getLogger("conversation_logger")
logger.setLevel(logging.INFO)

if not logger.handlers:
    fh = logging.FileHandler(LOG_FILE, encoding="utf-8")
    fmt = logging.Formatter("%(asctime)s - %(message)s")
    fh.setFormatter(fmt)
    logger.addHandler(fh)

class LLMClient:
    def __init__(
        self,
        provider: str = "ollama",
        model: str = "gemma:2b",
        temperature: float = 0.2,
        retriever: Optional[Callable[[str], List[str]]] = None,
        feedback_handler: Optional[Callable[[Dict[str, Any]], None]] = None,
        logger: Optional[Callable[[Dict[str, Any]], None]] = None
    ):
        self.provider = provider
        self.model = model
        self.temperature = temperature
        self.retriever = retriever
        self.feedback_handler = feedback_handler
        self.logger = logger or self.default_logger

    def default_logger(self, data: Dict[str, Any]):
        logging.info(f"[LLMClient] {data}")

    def _format_messages(self, messages: List[Dict[str, str]]) -> str:
        return "
".join(f"{m['role'].capitalize()}: {m['content']}" for m in messages)

    def _stream_provider(self, prompt: str, temperature: float) -> Generator[str, None, None]:
        if self.provider == "ollama":
            for chunk in ollama.generate(
                model=self.model,
                prompt=prompt,
                stream=True,
                options={"temperature": temperature}
            ):
                yield chunk.get("response", "")
        else:
            raise ValueError(f"Streaming not implemented for provider: {self.provider}")

    def stream_generate(
        self,
        messages: List[Dict[str, str]],
        on_token: Callable[[str], None],
        temperature: Optional[float] = None
    ) -> Dict[str, Any]:
        start_time = time.time()

        if self.retriever:
            query = messages[-1]["content"]
            docs = self.retriever(query)
            if docs:
                context_str = "
".join(docs)
                messages = [{"role": "system", "content": f"Use this context:
{context_str}"}] + messages

        prompt = self._format_messages(messages)
        assembled_text = ""
        temp_to_use = temperature if temperature is not None else self.temperature

        try:
            for token in self._stream_provider(prompt, temp_to_use):
                assembled_text += token
                on_token(token)
        except Exception as e:
            assembled_text = f"Error: {e}"

        latency = time.time() - start_time

        result = {
            "text": assembled_text,
            "timestamp": datetime.now(timezone.utc),
            "latency": latency,
            "provider": self.provider,
            "model": self.model,
            "temperature": temp_to_use,
            "messages": messages
        }

        self.logger({
            "event": "llm_stream_call",
            "provider": self.provider,
            "model": self.model,
            "temperature": temp_to_use,
            "latency": latency,
            "prompt": prompt,
            "response": assembled_text
        })

        return result

    def record_feedback(self, feedback: Dict[str, Any]):
        if self.feedback_handler:
            self.feedback_handler(feedback)
        else:
            self.logger({"event": "feedback", **feedback})

    def log_interaction(self, role: str, content: str):
        logger.info(f"{role.upper()}: {content}")

Streamlit基础演示

在项目目录中创建一个名为st_app_basic.py的文件,并粘贴以下代码:

import streamlit as st
from llm_client import LLMClient

MAX_HISTORY = 5
llm_client = LLMClient(provider="ollama", model="gemma:2b")

st.set_page_config(page_title="Streamlit Basic Chatbot", layout="centered")
st.title("Streamlit Basic Chatbot")

if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat history
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

# User input
if prompt := st.chat_input("Type your message..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    st.session_state.messages = st.session_state.messages[-MAX_HISTORY:]
    llm_client.log_interaction("user", prompt)

    with st.chat_message("assistant"):
        response_container = st.empty()
        state = {"full_response": ""}

        def on_token(token):
            state["full_response"] += token
            response_container.markdown(state["full_response"])

        result = llm_client.stream_generate(st.session_state.messages, on_token)
        st.session_state.messages.append({"role": "assistant", "content": result["text"]})
        llm_client.log_interaction("assistant", result["text"])

通过以下命令在localhost:8501启动应用程序:

streamlit run st_app_basic.py
如果应用程序未自动在默认浏览器中打开,请手动导航至该URL(http://localhost:8501)。读者将看到一个基础的聊天界面。在输入框中输入以下问题并按回车键:

如何将摄氏度转换为华氏度?

图1显示了结果:

图1:Streamlit初始问答界面

图1:Streamlit初始问答界面

现在,提出以下追问:

你能用Python实现这个公式吗?

由于演示实现会跟踪最多5条之前的对话历史,聊天机器人能够将“这个公式”与前一个提示中的公式关联起来,如下图2所示:

图2:Streamlit后续问答界面

图2:Streamlit后续问答界面

读者可以随意尝试更多提示。要关闭应用程序,请在终端中执行Control + c。

Chainlit基础演示

在项目目录中创建一个名为cl_app_basic.py的文件,并粘贴以下代码:

import chainlit as cl
from llm_client import LLMClient

MAX_HISTORY = 5
llm_client = LLMClient(provider="ollama", model="gemma:2b")

@cl.on_chat_start
async def start():
    await cl.Message(content="Welcome! Ask me anything.").send()
    cl.user_session.set("messages", [])

@cl.on_message
async def main(message: cl.Message):
    messages = cl.user_session.get("messages")
    messages.append({"role": "user", "content": message.content})
    messages[:] = messages[-MAX_HISTORY:]
    llm_client.log_interaction("user", message.content)

    state = {"full_response": ""}

    def on_token(token):
        state["full_response"] += token

    result = llm_client.stream_generate(messages, on_token)
    messages.append({"role": "assistant", "content": result["text"]})
    llm_client.log_interaction("assistant", result["text"])

    await cl.Message(content=result["text"]).send()

通过以下命令在localhost:8000(注意端口不同)启动应用程序:

chainlit run cl_app_basic.py
为了进行比较,将运行与之前相同的两个提示。结果如下图3和图4所示:

图3:Chainlit初始问答界面

图3:Chainlit初始问答界面

图4:Chainlit后续问答界面

图4:Chainlit后续问答界面

与之前一样,在尝试更多提示后,通过在终端中执行Control + c来关闭应用程序。

Streamlit高级演示

现在,将扩展基础Streamlit演示,增加一个左侧的持久侧边栏,其中包含一个用于切换LLM温度参数的滑块小部件、一个用于下载聊天历史的按钮,以及每个聊天机器人回复下方的反馈按钮(“有帮助”、“无帮助”)。在Streamlit中,自定义应用程序布局和添加全局小部件相对容易,但在Chainlit中复制这些功能可能较为繁琐——感兴趣的读者可以尝试复现,亲身体验其中的挑战。

以下是扩展后的Streamlit应用程序,保存在名为st_app_advanced.py的文件中:

import streamlit as st
from llm_client import LLMClient
import json

MAX_HISTORY = 5
llm_client = LLMClient(provider="ollama", model="gemma:2b")

st.set_page_config(page_title="Streamlit Advanced Chatbot", layout="wide")
st.title("Streamlit Advanced Chatbot")

# Sidebar controls
st.sidebar.header("Model Settings")
temperature = st.sidebar.slider("Temperature", 0.0, 1.0, 0.2, 0.1)  # min, max, default, increment size
st.sidebar.download_button(
    "Download Chat History",
    data=json.dumps(st.session_state.get("messages", []), indent=2),
    file_name="chat_history.json",
    mime="application/json"
)

if "messages" not in st.session_state:
    st.session_state.messages = []

# Display chat history
for msg in st.session_state.messages:
    with st.chat_message(msg["role"]):
        st.markdown(msg["content"])

# User input
if prompt := st.chat_input("Type your message..."):
    st.session_state.messages.append({"role": "user", "content": prompt})
    st.session_state.messages = st.session_state.messages[-MAX_HISTORY:]
    llm_client.log_interaction("user", prompt)

    with st.chat_message("assistant"):
        response_container = st.empty()
        state = {"full_response": ""}

        def on_token(token):
            state["full_response"] += token
            response_container.markdown(state["full_response"])

        result = llm_client.stream_generate(
            st.session_state.messages,
            on_token,
            temperature=temperature
        )
        llm_client.log_interaction("assistant", result["text"])
        st.session_state.messages.append({"role": "assistant", "content": result["text"]})

        # Feedback buttons
        col1, col2 = st.columns(2)
        if col1.button("Helpful"):
            llm_client.record_feedback({"rating": "up", "comment": "User liked the answer"})
        if col2.button("Not Helpful"):
            llm_client.record_feedback({"rating": "down", "comment": "User disliked the answer"})

图5显示了一个示例截图:

图5:Streamlit高级功能演示

图5:Streamlit高级功能演示

Chainlit高级演示

接下来,将扩展基础Chainlit演示,增加每个消息的交互式操作和多模态输入处理(本文中为文本和图像)。Chainlit框架的“聊天原生”原语使得实现这些类型的功能比在Streamlit中更容易。同样,鼓励感兴趣的读者尝试使用Streamlit复制这些功能,以体验其中的差异。

以下是扩展后的Chainlit应用程序,保存在名为cl_app_advanced.py的文件中:

import os
import json
from typing import List, Dict
import chainlit as cl
from llm_client import LLMClient

MAX_HISTORY = 5
DEFAULT_TEMPERATURE = 0.2
SESSIONS_DIR = os.path.join(os.path.dirname(__file__), "sessions")
os.makedirs(SESSIONS_DIR, exist_ok=True)

llm_client = LLMClient(provider="ollama", model="gemma:2b", temperature=DEFAULT_TEMPERATURE)

def _session_file(session_name: str) -> str:
    safe = "".join(c for c in session_name if c.isalnum() or c in ("-", "_"))
    return os.path.join(SESSIONS_DIR, f"{safe or 'default'}.json")

def _save_session(session_name: str, messages: List[Dict]):
    with open(_session_file(session_name), "w", encoding="utf-8") as f:
        json.dump(messages, f, ensure_ascii=False, indent=2)

def _load_session(session_name: str) -> List[Dict]:
    path = _session_file(session_name)
    if os.path.exists(path):
        with open(path, "r", encoding="utf-8") as f:
            return json.load(f)
    return []

@cl.on_chat_start
async def start():
    cl.user_session.set("messages", [])
    cl.user_session.set("session_name", "default")
    cl.user_session.set("last_assistant_idx", None)

    await cl.Message(
        content=(
            "欢迎!有任何问题都可以问我。"
        ),
        actions=[
            cl.Action(name="set_session_name", label="设置会话名称", payload={"turn": None}),
            cl.Action(name="save_session", label="保存会话", payload={"turn": "save"}),
            cl.Action(name="load_session", label="加载会话", payload={"turn": "load"}),
        ],
    ).send()

@cl.action_callback("set_session_name")
async def set_session_name(action):
    await cl.Message(content="请键入:/name 您的会话名称").send()

@cl.action_callback("save_session")
async def save_session(action):
    session_name = cl.user_session.get("session_name")
    _save_session(session_name, cl.user_session.get("messages", []))
    await cl.Message(content=f"会话已保存为 '{session_name}'。").send()

@cl.action_callback("load_session")
async def load_session(action):
    session_name = cl.user_session.get("session_name")
    loaded = _load_session(session_name)
    cl.user_session.set("messages", loaded[-MAX_HISTORY:])
    await cl.Message(content=f"已加载会话 '{session_name}',包含 {len(loaded)} 轮对话。 ").send()

@cl.on_message
async def main(message: cl.Message):
    if message.content.strip().startswith("/name "):
        new_name = message.content.strip()[6:].strip() or "default"
        cl.user_session.set("session_name", new_name)
        await cl.Message(content=f"会话名称已设置为 '{new_name}'。").send()
        return

    messages = cl.user_session.get("messages")

    user_text = message.content or ""
    if message.elements:
        for element in message.elements:
            if getattr(element, "mime", "").startswith("image/"):
                user_text += f" [图片:{element.name}]"

    messages.append({"role": "user", "content": user_text})
    messages[:] = messages[-MAX_HISTORY:]
    llm_client.log_interaction("user", user_text)

    state = {"full_response": ""}
    msg = cl.Message(content="")

    def on_token(token: str):
        state["full_response"] += token
        cl.run_sync(msg.stream_token(token))

    result = llm_client.stream_generate(messages, on_token, temperature=DEFAULT_TEMPERATURE)
    messages.append({"role": "assistant", "content": result["text"]})
    llm_client.log_interaction("assistant", result["text"])

    msg.content = state["full_response"]
    await msg.send()

    turn_idx = len(messages) - 1
    cl.user_session.set("last_assistant_idx", turn_idx)

    await cl.Message(
        content="这有帮助吗?",
        actions=[
            cl.Action(name="thumbs_up", label="是", payload={"turn": turn_idx}),
            cl.Action(name="thumbs_down", label="否", payload={"turn": turn_idx}),
            cl.Action(name="save_session", label="保存会话", payload={"turn": "save"}),
        ],
    ).send()

@cl.action_callback("thumbs_up")
async def thumbs_up(action):
    turn = action.payload.get("turn")
    llm_client.record_feedback({"rating": "up", "turn": turn})
    await cl.Message(content="感谢您的反馈!").send()

@cl.action_callback("thumbs_down")
async def thumbs_down(action):
    turn = action.payload.get("turn")
    llm_client.record_feedback({"rating": "down", "turn": turn})
    await cl.Message(content="感谢您的反馈。").send()

图6显示了一个示例截图:

图6:Chainlit高级功能演示

图6:Chainlit高级功能演示

实用指南

正如前文所示,Streamlit和Chainlit都可以快速构建简单的聊天机器人应用程序。在实现的基础演示中,存在一些架构上的相似之处:对Ollama的调用和对话日志记录通过LLMClient类进行了抽象,上下文大小通过一个名为MAX_HISTORY的常量变量进行限制,并且历史记录被序列化为纯文本聊天格式。然而,高级演示表明,每个框架的范围有所不同,这根据用例带来了特定的优缺点以及相关的实用建议。

Streamlit是一个用于以数据为中心的交互式Web应用程序的通用框架,而Chainlit则专注于构建和部署会话式AI应用程序。因此,如果聊天机器人在原型中是核心组件,使用Chainlit可能更具意义;正如上述代码示例所示,Chainlit处理了多个样板操作细节(例如,内置的聊天功能支持原生打字指示器、消息流式传输和Markdown/代码渲染)。但如果聊天机器人嵌入在更大的AI产品中,Streamlit可能能更好地应对更大的应用程序范围(例如,将聊天界面与数据可视化、仪表板、全局小部件和自定义布局相结合)。

此外,AI应用程序中的对话元素可能需要以异步方式处理,以确保良好的用户体验(UX),因为消息可以随时到达,并且需要在其他任务(例如,调用另一个API或流式传输模型输出)可能正在进行时快速处理。Chainlit利用Python的async和await关键字,可以轻松地进行异步聊天逻辑原型开发,确保应用程序能够处理并发操作而不会阻塞UI。该框架处理了管理WebSocket连接和自定义轮询的底层细节,因此每当触发事件(例如,消息发送、令牌流式传输、状态改变)时,Chainlit的事件处理逻辑会自动触发所需的UI更新。相比之下,Streamlit使用同步通信,这导致应用程序脚本在每次用户交互时重新运行;对于需要处理多个并发进程的复杂应用程序,Chainlit可能比Streamlit提供更流畅的用户体验。

TAGGED:ChainlitStreamlit大模型快速原型聊天机器人
Share This Article
Email Copy Link Print
Previous Article 利用API函数调用进行生产计划的n8n工作流 – (图片由Samir Saci提供) n8n数据分析:从Python到JavaScript的实战攻略与性能优化
Next Article 无状态检索式问答聊天机器人的示例 打破“失忆”僵局:LLM如何赋能检索式聊天机器人实现多轮对话
Leave a Comment

发表回复 取消回复

您的邮箱地址不会被公开。 必填项已用 * 标注

最新内容
LinkedIn小游戏帖子截图1
500天深度体验:从产品数据科学视角,拆解LinkedIn小游戏的设计与实验
数据科学与工程
潘通2026年度色云舞者概念图
潘通2026年度色“云舞者”:是宁静的承诺,还是经济衰退的无声信号?
科技
MyQ车库门控制器连接示意图
智能家居生态再遭打击:Chamberlain新平台封锁第三方车库门集成方案
科技
GMM在Excel中的初始化步骤
机器学习“降临日历”第五天:在Excel中实现高斯混合模型(GMM)
未分类

相关内容

图1:高级LangGraph工作流示例
未分类

使用LangGraph构建高效智能体系统:深度解析与实战

2025年10月1日
智能体AI金融应用演示
大模型与工程化

金融业的智能体AI:印尼机遇与挑战深度解析

2025年10月23日
大模型与工程化

系统思维:在AI时代驾驭复杂LLM应用与智能体的核心策略

2025年10月31日
SQL数据库设计图
大模型与工程化

图数据库RAG与SQL数据库RAG:大型语言模型性能深度比较

2025年11月2日
Show More
前途科技

前途科技是一个致力于提供全球最新科技资讯的专业网站。我们以实时更新的方式,为用户呈现来自世界各地的科技新闻和深度分析,涵盖从技术创新到企业发展等多方面内容。专注于为用户提供高质量的科技创业新闻和行业动态。

分类

  • AI
  • 初创
  • 学习中心

快速链接

  • 阅读历史
  • 我的关注
  • 我的收藏

Copyright © 2025 AccessPath.com, 前途国际科技咨询(北京)有限公司,版权所有。 | 京ICP备17045010号-1 | 京公网安备 11010502033860号

前途科技
Username or Email Address
Password

Lost your password?

Not a member? Sign Up