Streamlit 构建 GenAI 应用的最佳实践

使用 Streamlit 构建稳健、可扩展和负责任的 GenAI 应用的关键策略

发布于 LLMs,
Best Practices for Building GenAI Apps with Streamlit

Streamlit 让您可以非常轻松地将 Python 脚本转化为交互式 Web 应用,从而有机会展示您强大的 GenAI 应用。当您的应用从一个酷炫的概念验证阶段走向生产就绪阶段时,一些最佳实践可以在成本、性能、可维护性和用户信任方面带来天壤之别。

本文将指导您了解使用 Streamlit 构建更稳健、可扩展和负责任的 GenAI 应用的关键策略。

让我们深入了解吧!

1. 合理构建您的应用,以保证正常运行和可扩展性

随着您的 GenAI 应用不断增长,良好的结构是您最好的帮手。当您构建和维护多个应用时,这一点尤为重要,因为拥有一致的结构或框架有助于您快速完成应用维护。对于更复杂的应用,采用更模块化的方法至关重要。

保持您的主 Streamlit 应用脚本(streamlit_app.py)干净整洁,专注于 UI 和工作流。 

对于其他所有内容,例如 LLM 交互逻辑、检索增强生成(RAG)和代理行为,以及提示管理,请考虑使用 utils/ 目录。  

  • 创建如 utils/llm.py 这样的文件来处理 API 调用、客户端设置和错误处理
  • 使用 utils/prompt.py 来存储和格式化您的提示模板。

推荐的项目结构可能如下所示:  

my_genai_app/
├── .streamlit/
│   └── config.toml    # App configuration and styling
├── assets/             # Reusable UI assets (images, custom CSS, etc.)
├── utils/
│   └── llm.py         # LLM utility functions
│   └── prompt.py      # Prompt utility functions
├── README.md           # Describes the repo and its usage
├── requirements.txt    # Python dependencies
├── streamlit_app.py    # Main Streamlit app logic

或者,您可以创建语义文件夹,而不是使用 utils/ 文件夹来存放多个实用脚本文件,这些文件夹会根据脚本的功能进行命名。例如,创建一个 data/ 文件夹用于数据摄取脚本,一个 llm/ 文件夹用于 LLM 调用等。

2. 定制聊天元素

Streamlit 的聊天元素 st.chat_message()st.chat_input() 是构建会话式 GenAI 应用的基础。定制这些元素,特别是头像,可以显著改善聊天机器人的用户体验和品牌形象。

st.chat_message() 函数会在应用中插入一个用于单个聊天消息的容器。 

将 name 参数设置为 "user""assistant""human""ai" 可以启用预设样式和默认头像。此外,任何其他字符串也可以用于指定自定义角色。

# Name using AI
with st.chat_message("AI"):
    st.write("How can I help you?")

# Name using user
with st.chat_message("user"):
    st.write("What is Python?")

# Name using assistant
with st.chat_message("assistant"):
    st.write("How can I help you?")

# Name using human
with st.chat_message("human"):
    st.write("What is Streamlit?")

avatar 参数允许开发者指定一个自定义头像图片,该图片将显示在聊天消息容器中。如下例所示,可以通过指定 "path/to/image.png" 来使用表情符号、Material Icons 图标,甚至是一张图片。

# Avatar using emoji
with st.chat_message("assistant", avatar="🦖"):
    st.write("How can I help you?")

# Avatar using materials icon
with st.chat_message("user", avatar=":material/thumb_up:"):
    st.write("What is Python?")

# Avatar using an image
with st.chat_message("user", avatar="path/to/image.png"):
    st.write("What is Streamlit?")

3. 有效地显示 LLM 输出

LLM 可以输出纯文本、Markdown、JSON 等。Streamlit 都能支持:  

  • st.json(): 非常适合以交互式、可折叠的格式显示 LLM 输出的结构化 JSON。  
  • st.markdown(): 如果您的 LLM 输出 Markdown(许多 LLM 都支持输出 Markdown 来生成标题、列表或代码块等富文本),st.markdown() 将会完美地将其渲染出来。
  • st.write_stream(): 这非常适合从流式响应中逐个令牌地输出文本。
# Generate response using your favorite LLM model
def response_generator():
    # Code for response generation goes here
    return response

# Display assistant response in chat message container
with st.chat_message("assistant"):
    response = st.write_stream(response_generator())

4. 精通 Streamlit 中的提示工程

您的提示质量严重影响 LLM 的输出。Streamlit 的交互性非常适合这一点。  

  • 动态且上下文感知提示:构建包含来自组件(如 st.text_input())的用户输入、来自 st.session_state 的聊天历史记录或来自上传文件的数据的提示。  这本质上允许您为 RAG 工作流动态生成提示。
  • 使用提示模板:不要硬编码复杂的提示。使用 f-strings,或者更好地在 utils/prompt.py 中使用函数来创建可重用的提示模板,您可以使用动态数据填充这些模板。
  • 提供调试工具:使用侧边栏组件(st.selectbox() 用于选择模型,st.slider() 用于调整温度)来调整 LLM 参数或甚至提示的一部分。Streamlit 的快速刷新周期使得您可以轻松地对提示进行实验和迭代。  

在下面的示例中,您将看到可以将组件值插入到 prompt 变量中,以便动态生成提示。

prompt = f"""
            You are a helpful AI chat assistant. 
            Please use <context> tag to answer <question> tag.
            
            <context>
            {prompt_context}
            </context>
            
            <question>
            {user_question}
            </question>
            
            Answer:
        """

5. 安全地处理 API 密钥

建议不要将 API 密钥或其他密钥直接硬编码在您的脚本中。 请改用 Streamlit 内置的 st.secrets 密钥处理功能。

在本地,在 $CWD/.streamlit/secrets.toml 创建一个 TOML 文件(注意:务必将此文件添加到您的 .gitignore 中!否则凭据可能会泄露到公共仓库中)。

或者,当您部署到 Streamlit Community Cloud 时,您将通过应用内的 Secrets 管理功能以 TOML 格式输入这些密钥。随后,您可以通过 st.secrets 在代码中访问它们。

这些存储的密钥在应用中作为环境变量可用,可以通过 st.secrets["key_name"] 进行访问。

6. 使用 st.session_state 维护上下文

对于 LLM 应用,特别是会话式应用,维护上下文至关重要。st.session_state 通过存储对话历史记录来提供这种“记忆”。

LLM 通常是无状态的,独立处理每个输入。为了创建连贯的对话,st.session_state 存储用户和 LLM 之间的消息历史记录。

基本流程如下

  1. 初始化:存储一个空列表(例如, st.session_state.messages = [])。
  2. 消息存储:将每个用户输入和 LLM 输出追加到 st.session_state 中的历史记录。
  3. 上下文输入:st.session_state 中检索相关的历史记录,并将其包含在发送给 LLM 的提示中。
  4. 显示历史记录:使用 st.session_state 中存储的历史记录来显示对话。

Streamlit 的回调函数会响应用户的组件交互来更新 st.session_state。例如,st.button()st.text_input()st.selectbox() 等组件会触发回调(使用 on_clickon_change 参数),Streamlit 使用这些回调来修改 st.session_state。这反过来会动态更新对话历史记录或其他应用状态。

要清除对话历史记录,您可以删除 st.session_state 中存储历史记录的键,或者为该键分配一个空列表。这也可以在按钮触发的回调函数中完成。示例如下所示

def clear_chat_history():
    st.session_state.messages = []  # Clear the message history

st.button("Clear History", on_click=clear_chat_history)

在 LLM 应用中使用 st.session_state 和回调函数可以实现以下功能

  • 连贯的对话:LLM 响应变得更加相关。
  • 上下文感知:LLM 理解对话历史记录。
  • 复杂的交互:可以进行后续问题和澄清。
  • 个性化:LLM 响应可以根据用户历史记录进行定制。
  • 动态更新:应用实时响应用户输入。
  • 交互式控制:用户可以影响 LLM 的行为(例如,提供上下文、指定 LLM 模型参数等)
  • 状态管理:存储用户偏好和应用设置。

简而言之,st.session_state 为 LLM 应用提供了“记忆”,而回调函数允许用户与该记忆进行交互和修改,从而创建更复杂、更具交互性的应用。没有这些功能,与 LLM 应用的交互将仅仅是一次性的输入-输出提交。

7. 实现速率限制以控制 API 成本

生成式 AI 模型,特别是通过 API 访问的模型,如果不加以监控,使用成本会迅速上升。 

假设您在周末发布的应用非常受欢迎并一夜爆红。结果,该应用可能会由于高 API 消耗而产生巨额费用。实施速率限制是控制这种情况的解决方案。 

速率限制可以通过两种方法解决:(1)客户端和/或(2)服务器端。

客户端速率限制

好的,让我们为您的 GenAI 应用性能提速,并让您的用户满意!直接管理您的应用调用外部 API 的频率对于您的预算和用户体验来说都是一个明智的举动;我们可以将客户端请求限制视为您的第一道防线。

可以这样想:您可以为每个用户提供针对这些外部服务的“请求额度”。一个巧妙的技巧是使用 st.session_state 在一定时间内(例如,每分钟五次请求)监控外部 API 的调用。如果超出限制,友好的 st.warning() 可以告知他们暂停。为了更流畅的控制,可以尝试使用基于 st.session_state 的令牌桶系统,用户会获得“API 调用令牌”,这些令牌会随着时间补充。

服务器端速率限制

除了客户端策略之外,这里还有一个控制 API 成本的强大技巧:为您的 API 使用设置硬性美元限额。您可以设置每日最高 $5、每周最高 $20 或每月最高 $50。为什么这很棒?如果您的应用突然流行起来,积累了很高的使用量和成本,它可以保护您的 API 令牌免受意外收费。这是防止意外账单飙升的直接方法!

8. 使用缓存提高性能并节省成本

Streamlit 的缓存是优化的强大工具!对于外部 API 调用,您可以显著减少访问这些外部服务获取相同信息的频率。

@st.cache_data 是您的首选!将其应用于从外部 API 获取数据的函数。这会缓存响应,因此如果发出相同的请求,您的应用会以闪电般的速度提供缓存数据,从而大大减少您实际的外部 API 调用量和相关成本。如果外部数据可能会频繁更改,请记住设置 ttl(生存时间)。

如果您的应用执行计算密集型的本地任务(例如,将大型 LLM 模型直接从 Hugging Face 加载到其运行环境中),此过程会产生显著的计算负载,需要大量的本地资源。对于这类资源密集型的本地初始化,@st.cache_resource 可能会大有帮助。它确保大型对象(如您的 LLM)只在内存中加载一次——在应用启动或配置更改时,而不是在每一次用户交互或脚本重新运行时。这会极大地加快您的应用在使用这些本地模型时的响应速度。

Streamlit 的两个缓存装饰器 @st.cache_data@st.cache_resource 对于优化应用性能至关重要。@st.cache_data 对于高效管理返回数据的函数(例如 dataframe 转换、数据库查询结果或 ML 推理输出)至关重要,并且通过缓存获取的数据,在节省外部 API 配额和降低成本方面特别有效。@st.cache_resource 擅长优化计算密集型全局资源的初始化和重用,例如数据库连接或大型机器学习模型,确保它们只加载一次并在会话间共享。

9. 使用异步操作保持应用流畅

没人喜欢冻结的 UI!长时间运行的 LLM API 调用可能会导致应用无响应。为了确保应用具有响应性,让我们探讨如何在长时间等待期间向用户提供及时更新,以及如何利用 asyncio 进行异步执行。

让用户知道您正在处理

当您的应用正在执行耗时的计算或长时间等待的任务时,在等待期间提供反馈总是有帮助的(请参阅文档中关于 显示进度和状态 的部分):  

  • st.spinner("Thinking..."): 将其用作上下文管理器,以便在代码块执行时显示消息和加载图标。  
  • st.progress(0): 如果可以量化进度,请使用进度条并更新它(例如,my_bar.progress(percent_complete))。  
  • st.status("Working on it..."): 此容器可以显示消息,您可以随着多步骤任务的进行更新这些消息。

示例如下

import time
import streamlit as st

with st.status("Downloading data...", expanded=True) as status:
    st.write("Searching for data...")
    time.sleep(2)
    st.write("Found URL.")
    time.sleep(1)
    st.write("Downloading data...")
    time.sleep(1)
    status.update(
        label="Download complete!", state="complete", expanded=False
    )

st.button("Rerun")
  • st.toast("Done!"): 用于快速、非阻塞通知,例如后台任务完成时。 

现在让我们看看如何实现异步操作。

使用 asyncio 进行非阻塞 LLM 调用

Python 的 asyncio 库及其 async/await 语法,可以让您的应用处理长时间的 I/O 操作(如 API 调用)而不会冻结主线程。这对于流畅的用户体验至关重要。  

  • 基本的异步调用:使用 async def 定义您的 API 调用函数。在函数内部,当调用您的 LLM 提供商的异步客户端(例如 AsyncOpenAI)时,使用 await,您可以在此 AsyncOpenAI 代码片段 中看到实际应用。在您的主 Streamlit 脚本(同步运行)中,您将使用 asyncio.run() 来执行您的异步函数。  
  • 流式响应以获得更好的体验:对于聊天等任务,流式传输 LLM 的响应(在生成时显示单词)会使您的应用感觉快得多,而不是等待整个响应生成完毕。由于许多 LLM 库提供了用于流式传输的异步生成器,您可以使用 st.write_stream() 将文本流发送到应用,并在生成时显示。

10. 通过可观察性了解您的 GenAI 模型在做什么

GenAI 模型有时感觉像“黑匣子”。可观察性工具可帮助您了解其行为、调试问题并确保质量。这不仅仅是为了修复错误;更是为了理解模型为什么会这样说,这对于迭代和建立信任至关重要。  

TruLens

TruLens 是一个很棒的开源工具,用于评估和跟踪您的 LLM 应用。  

  • 开始使用:安装 Trulens (pip install trulens),初始化 TruSession,然后使用 Trulens 记录器(例如 TruLlama 或 TruChain)包装您的 LLM 应用(如 LangChain 代理或 LlamaIndex RAG 引擎)。这让 TruLens 可以记录输入、输出以及所有那些重要的中间步骤。Streamlit cookbook 中也有一个示例演示了在此 TruLens 秘籍 中 TruLens 的实际应用。
  • 在 Streamlit 中嵌入洞察:Trulens 提供了 Streamlit 组件,您可以直接将其添加到您的应用中以获取实时反馈:  
    • 使用 trulens_leaderboard() 显示不同应用版本的反馈结果和成本摘要。
    • 使用 trulens_feedback(record=your_record_object),您可以显示特定交互的可点击反馈分数(如相关性或情感)。
    • 此外,trulens_trace(record=your_record_object) 为您提供了详细的执行跟踪,用于调试。
  • 通过 Dashboard 深入了解:为了获得更全面的视图,run_dashboard() 会启动一个完整的 TruLens dashboard,它本身就是一个 Streamlit 应用!

LangSmith

LangSmith,由 LangChain 的创建者开发,是另一个优秀的 LLM 可观察性平台。  

  • 开始使用:安装 Langsmith (pip install langsmith)
  • 跟踪 URL:一个非常实用的技巧是在您的 Streamlit UI 中显示 LangSmith 跟踪的直接链接。当发生 LLM 交互时,使用 LangChain 的 tracing_v2_enabled 上下文管理器来捕获运行。然后,使用 cb.get_run_url()(其中 cb 是回调处理程序)获取跟踪 URL,并在应用中显示。这使得调试速度快得多!请参阅此 示例
  • LangSmith cookbook 有更多示例,包括如何在 Streamlit 应用中添加用户反馈按钮,并将反馈记录回 LangSmith。

其他开源可观察性工具

除了 TruLensLangSmith 之外,还可以考虑其他开源工具,例如用于 LLM 可观察性的 HeliconeLogfire,后者还提供 Pydantic 集成和通过 OpenTelemetry 进行自动插桩等功能。  

可观察性不仅仅用于调试代码;它是负责任的 AI 开发的基石。您可以通过检查您的应用是否公平对待不同的群体来程序化地评估其公平性和偏见。通过检查单个跟踪和评估结果,您可以获得透明度,从而深入了解模型的运行情况。可观察性工具还可以通过检测输出中的 PII 泄露或有害语言来帮助实现安全和隐私。此外,包含广泛的评估器可以进行全面的评估,涵盖基础性(答案是否基于提供的上下文?)、相关性和安全性等方面,从而帮助您构建更值得信赖的应用。

结论

使用 Streamlit 构建高效的 GenAI 应用需要一种周全的方法,而不仅仅是简单地连接到 LLM 模型。

在本文中,我们探讨了使用 Streamlit 构建稳健可扩展负责任的 GenAI 应用的 10 个最佳实践

稳健 💪

  • 使用 st.session_state 维护上下文:使用 st.session_state 和回调函数管理对话历史和状态,从而实现连贯、交互式的 LLM 应用。
  • 精通提示工程:使用 Streamlit 的交互式组件创建动态、上下文感知的提示,并利用提示模板。
  • 有效显示 LLM 输出:使用 st.json()st.markdown()st.write_stream() 清晰地呈现各种输出格式。
  • 定制聊天元素:通过使用自定义头像定制 st.chat_message() 来增强用户体验和品牌形象。

可扩展 📈

  • 逻辑地构建您的应用:将代码组织到实用模块中,以提高可维护性和可扩展性。
  • 利用缓存:使用 @st.cache_data 缓存外部 API 响应,使用 @st.cache_resource 缓存重要的本地资源,以提高性能并降低成本。
  • 保持应用流畅:在长时间运行的任务期间,使用 asyncio 和诸如 st.spinner()st.progress()st.status()st.toast() 之类的元素提供进度更新,以防止 UI 冻结。

负责任 🛡️

  • 使用可观察性工具:使用 Trulens 和 LangSmith 等工具深入了解 GenAI 应用模型的行为、调试问题并确保质量。
  • 安全地处理 API 密钥:利用 st.secrets 管理敏感凭据,避免将它们硬编码在脚本中。
  • 实施速率限制:通过客户端和服务器端策略控制 API 成本并防止滥用。

采纳这些最佳实践,您可以将令人印象深刻的概念验证转化为生产就绪的 LLM 应用。

开心使用 Streamlit 吧!👑

分享此文章

评论

在我们的论坛中继续讨论 →

在 LLMs 中还有...

查看更多 →