逐步指南-在-Streamlit-中构建和部署具有记忆功能的-LLM-聊天

逐步指南:在 Streamlit 中构建和部署具有记忆功能的 LLM 聊天

原文:towardsdatascience.com/step-by-step-guide-to-build-and-deploy-an-llm-powered-chat-with-memory-in-streamlit/

在这篇文章中,我将逐步向您展示如何在 Streamlit 中构建和部署一个由 LLM(Gemini)驱动的聊天,并在 Google Cloud 控制台上监控 API 的使用情况。Streamlit 是一个 Python 框架,它使得将 Python 脚本转换为交互式 Web 应用变得非常容易,几乎不需要前端工作。

最近,我开发了一个项目,bordAI — 一个由 LLM(大型语言模型)驱动的聊天助手,集成了我开发的用于支持刺绣项目的工具。之后,我决定开始这个系列帖子,分享我在过程中学到的技巧。

下面是这篇文章的简要总结:

1 到 6 — 项目设置

7 到 13 — 构建聊天

14 到 15— 部署和监控应用


1. 创建一个新的 GitHub 仓库

前往 GitHub 并创建一个新的仓库。


2. 在本地克隆仓库

→ 在你的终端中执行此命令以克隆它:

git clone <your-repository-url>

3. 设置虚拟环境(可选)

虚拟环境就像你电脑上的一个独立空间,你可以在这里安装特定版本的 Python 和库,而不会影响你的整个系统。这很有用,因为不同的项目可能需要同一库的不同版本。

→ 创建虚拟环境:

pyenv virtualenv 3.9.14 chat-streamlit-tutorial

→ 激活它:

pyenv activate chat-streamlit-tutorial

4. 项目结构

项目结构只是组织项目所有文件和文件夹的一种方式。我们的结构将如下所示:

chat-streamlit-tutorial/
│
├── .env
├── .gitignore
├── app.py
├── functions.py
├── requirements.txt
└── README.md
  • .env→ 存储 API 密钥的文件(不会推送到 GitHub)

  • .gitignore → 列出 git 需要忽略的文件或文件夹的文件

  • app.py → 主 streamlit 应用

  • functions.py → 用于更好地组织代码的自定义函数

  • requirements.txt → 列出项目需要的库

  • README.md → 解释你的项目是关于什么的文件

→ 在你的项目文件夹中执行此操作以创建这些文件:

touch .env .gitignore app.py functions.py requirements.txt

→ 在文件 .gitignore 中添加:

.env
__pycache__/

→ 将以下内容添加到 requirements.txt

streamlit
google-generativeai
python-dotenv

→ 安装依赖项:

pip install -r requirements.txt

5. 获取 API 密钥

API 密钥就像一个密码,告诉服务你有权使用它。在这个项目中,我们将使用 Gemini API,因为他们有一个免费层,所以你可以免费尝试它,而不必花钱。

  • 前往 aistudio.google.com/

  • 创建或登录到你的账户。

  • 点击“创建 API 密钥”,创建它,并复制它。

如果你只想使用免费层,不要设置账单。在“计划”下应该显示“免费”,就像这里一样:

图片由作者提供

我们将在本项目中使用gemini-2.0-flash。它提供了一个免费套餐,如以下表格所示:

图片

作者截图自aistudio.google.com/plan_information

  • 15 RPM = 每分钟请求 15 次

  • 1,000,000 TPM = 每分钟 1 百万个令牌

  • 1,500 RPD = 每天请求 1,500 次

注意:这些限制截至 2025 年 4 月准确无误,可能会随时间变化。

提前提醒:如果你正在使用免费套餐,谷歌可能会使用你的提示来改进他们的产品,包括人工审查,因此不建议发送敏感信息。如果你想了解更多关于这一点的内容,请查看这个链接


6. 存储你的 API 密钥

我们将 API 密钥存储在.env文件中。一个.env文件是一个简单的文本文件,用于存储秘密信息,因此你不需要直接将其写入代码。我们不希望它出现在 GitHub 上,因此我们必须将其添加到我们的.gitignore文件中。此文件确定在将更改推送到存储库时 git 应实际忽略哪些文件。我在第四部分“项目结构”中已经提到过这一点,但以防你错过了,我在这里再次重复。

这一步非常重要,不要忘记它!

→ 将此添加到.gitignore文件中:

.env
__pycache__/

→ 将 API 密钥添加到.env文件中:

API_KEY= "your-api-key"

如果你是在本地运行,.env文件可以正常工作。然而,如果你稍后使用 Streamlit 部署,你将不得不使用st.secrets。这里我包含了一段可以在两种情况下工作的代码。

→ 将此函数添加到你的functions.py文件中:

import streamlit as st
import os
from dotenv import load_dotenv

def get_secret(key):
    """
    Get a secret from Streamlit or fallback to .env for local development.

    This allows the app to run both on Streamlit Cloud and locally.
    """
    try:
        return st.secrets[key]
    except Exception:
        load_dotenv()
        return os.getenv(key)

→ 将此添加到你的app.py文件中:

import streamlit as st
import google.generativeai as genai
from functions import get_secret

api_key = get_secret("API_KEY")

7. 选择模型

我选择gemini-2.0-flash用于这个项目,因为它是一个很好的模型,提供了慷慨的免费套餐。然而,你可以探索其他提供免费套餐的模型选项,并选择你喜欢的。

图片

作者截图自aistudio.google.com/plan_information

  • Pro:为高质量输出设计的模型,包括推理和创造力。通常用于复杂任务、问题解决和内容生成。它们是多模态的——这意味着它们可以处理文本、图像、视频和音频作为输入和输出。

  • Flash:针对速度成本效率设计的模型。与 Pro 相比,在复杂任务中可能提供较低质量的答案。通常用于聊天机器人、助手和实时应用,如自动短语完成。它们是多模态输入,输出目前仅为文本,其他功能正在开发中。

  • Lite:比 Flash 更快、更便宜,但功能有所减少,例如它仅支持多模态输入和纯文本输出。其主要特点是比 Flash更经济,在成本限制内生成大量文本的理想选择。

这个链接提供了关于模型及其差异的详细信息。

在这里,我们正在设置模型。只需将“gemini-2.0-flash”替换为你选择的模型。

→ 将以下内容添加到你的 app.py

genai.configure(api_key=api_key)
model = genai.GenerativeModel("gemini-2.0-flash")

8. 构建聊天

首先,让我们讨论我们将使用的关键概念:

  • st.session_state:这就像是你应用的记忆。Streamlit 每次发生变化时(例如发送消息或点击按钮)都会从头到尾重新运行你的脚本,所以通常,所有变量都会重置。这允许 Streamlit 在重新运行之间记住值。然而,如果你刷新网页,你将丢失 session_state

  • st.chat_message(name, avatar):在界面中创建一个消息的聊天气泡。第一个参数是消息作者的名称,可以是 “user”,“human”,“assistant”,“ai”,或 str。如果你使用 user/human 和 assistant/ai,它已经具有默认的用户和机器人图标。如果你想改变,可以这样做。更多详情请查看文档

  • st.chat_input(placeholder):在底部显示一个输入框,供用户输入消息。它有许多参数,所以我建议你查看文档

首先,我将分别解释代码的每一部分,然后我会展示整个代码。

这个初始步骤初始化了你的 session_state,应用的“记忆”,以保持会话中所有的消息。

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

接下来,我们将设置第一条默认消息。这是可选的,但我喜欢添加它。如果你适合你的上下文,可以添加一些初始指令。每次 Streamlit 运行页面且 st.session_state.chat_history 为空时,它都会将此消息以“assistant”的角色追加到历史记录中。

if not st.session_state.chat_history:
    st.session_state.chat_history.append(("assistant", "Hi! How can I help you?"))

在我的应用 bordAI 中,我添加了这条初始信息,以提供应用上下文和说明:

图片由作者提供

对于用户部分,第一行创建输入框。如果 user_message 包含内容,它将内容写入界面,并将其追加到 chat_history

user_message = st.chat_input("Type your message...")

if user_message:
    st.chat_message("user").write(user_message)
    st.session_state.chat_history.append(("user", user_message))

现在,让我们添加助手部分:

  • system_prompt 是发送给模型的提示。你可以用 user_message 替换 full_input(查看下面的代码)。然而,输出可能不会很精确。提示提供了关于如何你想模型表现出的上下文和指令,而不仅仅是什么你想它回答。一个好的提示可以使模型响应更准确、一致,并与你的目标保持一致。此外,如果不告诉模型应该如何表现,它就很容易受到提示注入的攻击。

提示注入是指有人试图操纵模型的提示以改变其行为。一种减轻这种影响的方法是清晰地结构化提示,并在三重引号内限定用户的消息。 *

我们将从简单且不明确的system_prompt开始,在下一节课中我们将使其变得更好以便比较差异。

  • full_input:在这里,我们正在组织输入,用三重引号(“””)限定用户消息。这并不能防止所有的提示注入,但这是创建更好和更可靠的交互的一种方法。

  • response:向 API 发送请求,将输出存储在响应中。

  • assistant_reply:从响应中提取文本。

最后,我们使用st.chat_message()结合write()来显示助手的回复,并将其附加到st.session_state.chat_history中,就像我们处理用户信息一样。

if user_message:
    st.chat_message("user").write(user_message)
    st.session_state.chat_history.append(("user", user_message))

    system_prompt = f"""
    You are an assistant.
    Be nice and kind in all your responses.
    """
    full_input = f"{system_prompt}\n\nUser message:\n\"\"\"{user_message}\"\"\""

    response = model.generate_content(full_input)
    assistant_reply = response.text

    st.chat_message("assistant").write(assistant_reply)
    st.session_state.chat_history.append(("assistant", assistant_reply))

现在让我们一起看看所有这些内容!

→ 将此添加到你的app.py中:

import streamlit as st
import google.generativeai as genai
from functions import get_secret

api_key = get_secret("API_KEY")
genai.configure(api_key=api_key)
model = genai.GenerativeModel("gemini-2.0-flash")

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

if not st.session_state.chat_history:
    st.session_state.chat_history.append(("assistant", "Hi! How can I help you?"))

user_message = st.chat_input("Type your message...")

if user_message:
    st.chat_message("user").write(user_message)
    st.session_state.chat_history.append(("user", user_message))

    system_prompt = f"""
    You are an assistant.
    Be nice and kind in all your responses.
    """
    full_input = f"{system_prompt}\n\nUser message:\n\"\"\"{user_message}\"\"\""

    response = model.generate_content(full_input)
    assistant_reply = response.text

    st.chat_message("assistant").write(assistant_reply)
    st.session_state.chat_history.append(("assistant", assistant_reply))

要在本地运行和测试你的应用程序,首先导航到项目文件夹,然后执行以下命令。

→ 在你的终端中执行:

cd chat-streamlit-tutorial
streamlit run app.py

Yay! 你现在在 Streamlit 中运行了一个聊天程序!


9. 提示工程

提示工程是一个编写指令以从 AI 模型获得最佳可能输出的过程。

提示工程有许多技巧。以下有 5 个提示:

  1. 写出清晰和具体的指令。

  2. 定义一个角色,期望的行为和助手的规则。

  3. 提供适当数量的上下文。

  4. 使用分隔符来表示用户输入(如我在第八部分中解释的)。

  5. 以指定的格式请求输出。

这些提示可以应用于system_prompt或当你编写与聊天助手交互的提示时。

我们当前的系统提示是:

system_prompt = f"""
You are an assistant.
Be nice and kind in all your responses.
"""

它非常模糊,并为模型提供不了任何指导。

  • 没有明确的方向给助手,它应该提供什么样的帮助

  • 没有指定角色或辅助的主题是什么

  • 没有关于如何结构化输出的指南

  • 没有关于它应该是技术性还是非正式性的上下文

  • 缺乏边界

我们可以根据上面的提示来改进我们的提示。以下是一个例子。

→ 在app.py中更改system_prompt

system_prompt = f"""
You are a friendly and a programming tutor.
Always explain concepts in a simple and clear way, using examples when possible.
If the user asks something unrelated to programming, politely bring the conversation back to programming topics.
"""
full_input = f"{system_prompt}\n\nUser message:\n\"\"\"{user_message}\"\"\""

如果我们用旧的提示问“什么是 python?”它只会给出一个通用的简短答案:

图片由作者提供

使用新的提示,它提供了更详细的响应,并附有示例:

图片由作者提供

图片由作者提供

尝试自己更改system_prompt以查看模型输出的差异,并为你的上下文制定理想的提示!


10. 选择生成内容参数

在生成内容时,你可以配置许多参数。在这里,我将演示temperaturemaxOutputTokens是如何工作的。查看文档获取更多详细信息。

  • temperature:控制输出的随机性,范围从 0 到 2。默认值为 1。较低的值产生更确定的输出,而较高的值产生更具创造性的输出。

  • maxOutputTokens:输出中可以生成的最大标记数。一个标记大约是四个字符。

要动态更改温度并测试它,你可以创建一个侧边栏滑块来控制这个参数。

→ 将以下内容添加到app.py中:

temperature = st.sidebar.slider(
    label="Select the temperature",
    min_value=0.0,
    max_value=2.0,
    value=1.0
)

→ 将response变量更改为:

response = model.generate_content(
    full_input,
    generation_config={
        "temperature": temperature,
        "max_output_tokens": 1000
    }
)

侧边栏将看起来像这样:

图片由作者提供

尝试调整温度以查看输出如何变化!


11. 显示聊天历史

这一步确保你跟踪所有在聊天中交换的消息,这样你就可以看到聊天历史。如果没有这个,每次你发送消息时,你只会看到助手和用户的最新消息。

这段代码访问了附加到chat_history的所有内容,并在界面上显示它。

→ 在app.py中的if user_message之前添加以下内容:

for role, message in st.session_state.chat_history:
    st.chat_message(role).write(message)

现在,一个会话中的所有消息都保存在界面上可见:

图片由作者提供

Obs:我尝试提出一个非编程问题,助手试图将话题转回编程。我们的提示正在起作用!


12. 使用记忆进行聊天

除了在chat_history中存储消息外,我们的模型并不了解我们对话的上下文。它是无状态的,每次交易都是独立的。

图片由作者提供

为了解决这个问题,我们必须在提示中传递所有这些上下文,以便模型可以参考之前交换的消息。

创建context,这是一个包含直到那一刻交换的所有消息的列表。最后添加最新的用户消息,这样它就不会在上下文中丢失。

system_prompt = f"""
You are a friendly and knowledgeable programming tutor.
Always explain concepts in a simple and clear way, using examples when possible.
If the user asks something unrelated to programming, politely bring the conversation back to programming topics.
"""
full_input = f"{system_prompt}\n\nUser message:\n\"\"\"{user_message}\"\"\""

context = [
    *[
        {"role": role, "parts": [{"text": msg}]} for role, msg in st.session_state.chat_history
    ],
    {"role": "user", "parts": [{"text": full_input}]}
]

response = model.generate_content(
    context,
    generation_config={
        "temperature": temperature,
        "max_output_tokens": 1000
    }
)

现在,我告诉助手我正在做一个分析天气数据的项目。然后我问我的项目主题是什么,它正确地回答了“天气数据分析”,因为它现在有了之前消息的上下文。

图片由作者提供

如果你的上下文太长,你可以考虑总结它以节省成本,因为你发送给 API 的标记越多,你将支付的费用就越多。


13. 创建重置按钮(可选)

我喜欢添加一个重置按钮,以防万一出问题或者用户只想清除对话。

你只需要创建一个函数来将chat_history设置为空列表。如果你创建了其他会话状态,也应该在这里将它们设置为 False 或空。

→ 将以下内容添加到functions.py中:

def reset_chat():
    """
    Reset the Streamlit chat session state.
    """
    st.session_state.chat_history = []
    st.session_state.example = False # Add others if needed

→ 如果你想要它在侧边栏中,请将以下内容添加到app.py中:

from functions import get_secret, reset_chat

if st.sidebar.button("Reset chat"):
    reset_chat()

它看起来会是这样:

图片由作者提供

所有的东西放在一起:

import streamlit as st
import google.generativeai as genai
from functions import get_secret, reset_chat

api_key = get_secret("API_KEY")
genai.configure(api_key=api_key)
model = genai.GenerativeModel("gemini-2.0-flash")

temperature = st.sidebar.slider(
    label="Select the temperature",
    min_value=0.0,
    max_value=2.0,
    value=1.0
)

if st.sidebar.button("Reset chat"):
    reset_chat()

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

if not st.session_state.chat_history:
    st.session_state.chat_history.append(("assistant", "Hi! How can I help you?"))

for role, message in st.session_state.chat_history:
    st.chat_message(role).write(message)

user_message = st.chat_input("Type your message...")

if user_message:
    st.chat_message("user").write(user_message)
    st.session_state.chat_history.append(("user", user_message))

    system_prompt = f"""
    You are a friendly and a programming tutor.
    Always explain concepts in a simple and clear way, using examples when possible.
    If the user asks something unrelated to programming, politely bring the conversation back to programming topics.
    """
    full_input = f"{system_prompt}\n\nUser message:\n\"\"\"{user_message}\"\"\""

    context = [
        *[
            {"role": role, "parts": [{"text": msg}]} for role, msg in st.session_state.chat_history
        ],
        {"role": "user", "parts": [{"text": full_input}]}
    ]

    response = model.generate_content(
        context,
        generation_config={
            "temperature": temperature,
            "max_output_tokens": 1000
        }
    )
    assistant_reply = response.text

    st.chat_message("assistant").write(assistant_reply)
    st.session_state.chat_history.append(("assistant", assistant_reply))

14. 部署

如果你的仓库是公开的,你可以免费使用 Streamlit 进行部署。

确保您在公共存储库上没有 API 密钥。

首先,保存并将您的代码推送到存储库。

→ 在您的终端中执行:

git add .
git commit -m "tutorial chat streamlit"
git push origin main

直接推送到 main 并不是最佳实践,但由于这是一个简单的教程,我们将为了方便而这样做。

  1. 前往您在本地上运行的 streamlit 应用。

  2. 点击右上角的“部署”。

  3. 在 Streamlit Community Cloud 中,点击“立即部署”。

  4. 填写信息。

图片由作者提供

  1. 点击“高级设置”并写入 API_KEY="your-api-key",就像你在 .env 文件中做的那样。

  2. 点击“部署”。

完成了!如果您愿意,可以查看我的应用 这里!🎉


15. 在 Google Console 上监控 API 使用情况

这篇帖子的最后一部分展示了如何在 Google Cloud Console 上监控 API 使用情况。如果您公开部署应用,这很重要,这样您就不会有任何意外。

  1. 访问 Google Cloud Console.

  2. 前往“API 和服务”。

  3. 点击“生成语言 API”。

图片由作者提供

  • 请求次数:您的 API 被调用的次数。在我们的例子中,每次我们运行 model.generate_content(context) 时都会调用 API。

  • 错误(%):请求失败的百分比。错误可能有 4xx 代码,这通常是用户/请求者的错误——例如,400 表示 输入错误,而 429 表示您 太频繁地调用 API。此外,代码为 5xx 的错误通常是系统/服务器的错误,并且不太常见。Google 通常会内部重试或建议几秒后重试——例如,500 表示 内部服务器错误503 表示 服务不可用

  • 中值延迟,单位:毫秒:这显示了您的服务响应所需的时间(以毫秒为单位),在 50 分位百分比的响应时间——意味着一半的请求比这个时间快,一半比这个时间慢。这是衡量您服务速度的良好一般指标,回答了问题,“它通常有多快?”

  • 95% 分位延迟,单位:毫秒:这显示了 95 分位百分比的响应时间——意味着 95% 的请求比这个时间快,只有 5% 比这个时间慢。这有助于识别系统在重负载或较慢情况下如何表现,回答问题,“对于某些用户来说,情况有多糟糕?”

中值延迟和 p95 延迟之间的快速示例:

想象一下,您的服务通常在 200ms 内响应:

  • 中值延迟 = 200ms(很好!)

  • p95 延迟 = 220ms(也很好)

现在在重负载下:

  • 中值延迟 = 220ms(仍然看起来不错)

  • p95 延迟 = 1200ms(不好

指标 p95 显示 5% 的用户等待时间超过 1.2 秒——这是一个非常糟糕的体验。如果我们只看中值,我们会认为一切正常,但 p95 显示了隐藏的问题。

在“指标”页面继续,你会找到图表,底部是 API 调用的方法。此外,在“配额和系统限制”中,你可以监控 API 使用情况与免费层限制的比较。

图片

图片由作者提供

点击“显示使用图表”来逐日比较使用情况。

图片

图片由作者提供


希望你喜欢这个教程。

你可以在我的 GitHub 上找到这个项目的所有代码。

我很乐意听听你的想法!请在评论中告诉我你的看法。

关注我:

posted @ 2026-03-29 09:50  布客飞龙III  阅读(81)  评论(0)    收藏  举报