大多数生成式 AI 项目都始于实验,而大多数最终都没有进入生产环境。快速原型开发非常重要,尤其是当你的团队缺少完整全栈工程支持时。你需要一种 setup,让数据科学家可以构建由 RAG 驱动、真实用户能够测试的应用,然后再决定是否投入生产级基础设施。
Streamlit 是一个用于快速 Web app 开发的 Python framework。它允许你用一个 script 构建包含 frontend 和 backend 的可用界面。该 framework 提供了可以直接组合的预构建 components,不需要底层 Web 开发;不过,你也会受限于 Streamlit 当前提供的组件能力。
Streamlit 会在每次用户交互时重新运行整个 script。这简化了开发,但也带来了开销,使其难以扩展到几十个并发用户以上。对于验证一个 GenAI app 是否真的提供价值而言,这种取舍反而是有利的。
对于拥有数千或数百万用户的生产应用,其他 frameworks 提供更好的可扩展性和性能。Django 提供完整功能能力,包括 authentication 和复杂 workflows。Flask 提供轻量且灵活的基础。FastAPI 则针对 API endpoints 做了优化。
本章的 recipes 会逐步构建。你会先创建一个基础 Streamlit app,以理解该 framework 的核心机制;然后构建一个可运行的 RAG chatbot;最后扩展它,使其能够处理文档上传和查询。
TIP
在 RAG 早期,许多应用默认做成 chatbots,即使更简单的界面可能更合适。Dashboards 和 search interfaces 往往更快、更直观。在选择 chat interface 之前,应考虑你的 use case 是否真的受益于对话式交互。
你可以在本书 GitHub repository 中找到本章所有代码示例。
11.1 构建第一个 Streamlit App
Problem
你不是 Web developer,但仍然想快速把想法变成 Web app 并部署出去。
Solution
Streamlit 是一个 Python Web framework,可以让你用一个 script 和几行代码构建一个功能完整的 Web app。安装 Streamlit 以跟随这个 recipe:
pip install streamlit
构建一个基础 Streamlit app,展示 title、text input,以及一个将用户输入打印到屏幕上的区域:
import streamlit as st
st.title("Simple Chatbot")
user_input = st.text_input("You:")
if user_input:
st.write(f"Bot: You said '{user_input}'!")
要运行 app,在 terminal 中执行以下命令:
streamlit run streamlit_app.py
这个命令会启动一个托管 app 的本地 Web server。除非你另外配置,否则 Streamlit 默认使用端口 8501,因此可以在 http://localhost:8501 打开 app。
图 11-1:最基础的 Streamlit app
这个示例展示了 Streamlit 的核心机制。后续 recipes 会展示如何构建更高级的 RAG-specific features,包括 conversation history、session state management,以及与 external APIs 的集成。
Discussion
Streamlit 的简单性是它最大的优势。Script rerun model 消除了 session handling、routing,以及 frontend-backend coordination。你写的是看起来像普通 script 的 Python 代码,而 Streamlit 会自动处理 Web interface。这使得 prototype 阶段的 state management 变得非常简单。
取舍是可扩展性。每次用户交互都会从头到尾重新运行整个 script,为每个用户带来 CPU overhead。超过 50 个并发用户后,这种模式会成为瓶颈。
Streamlit 适合 internal tools、proof-of-concept demos,以及需求收集阶段的快速迭代。不要将它用于拥有数千用户的 external apps,因为 FastAPI with React 扩展性更好;也不要用于需要 custom branding 的应用,因为 Django 提供更多 UI 控制;也不适合需要 fine-grained authentication 的应用,因为 FastAPI 更自然地集成 authentication middleware。
关键取舍是 development speed versus scalability。你可以在几小时内 prototype 一个可运行的 RAG app,但之后重构到生产基础设施可能需要数周。对于很可能不会进入生产的实验来说,Streamlit 是更好的选择,因为它最小化了前期投入。
你可以通过将 compute-heavy tasks 卸载到 microservices 来扩展 Streamlit 的适用范围。让 Streamlit 保持为轻量 frontend,同时把 authentication、ML inference 或 database operations 委托给专用 APIs。这种 hybrid approach 在 100–500 用户规模内可行,之后通常需要完整重写。
See Also
Streamlit documentation 全面覆盖 framework concepts 和 components。
Streamlit GitHub repository 包含源码和社区示例。
11.2 使用 Streamlit 构建 Chatbot App
Problem
你已经设计了一个 RAG workflow,并希望将它集成到 chatbot app 中。
Solution
Streamlit 提供了构建 conversational UIs 的功能。在这个 recipe 中,你会构建一个 chatbot,让用户询问天气。该 chatbot 使用 external API 检索天气信息,并使用 LLM 解释这些数据,回答用户问题。
用户在 Streamlit chat interface 中输入问题后,app 会执行以下步骤,见图 11-2:
- 用户发送请求后,RAG workflow 的第一步会从用户问题中提取 location,例如提到的城市名或国家。
- 使用提取出的 location 调用 external weather API,并返回当天的天气信息。
- Weather information 被发送回 LLM,LLM 会结合用户问题和提供的 context,生成 response。
图 11-2:RAG weather app 背后的流程
这个 recipe 使用 Streamlit library 构建 Web app,使用 Requests library 调用 external weather API。由于用户通常会询问城市或国家,但 API 需要 latitude 和 longitude,因此 app 使用 GeoPy 将地名转换成坐标。安装这些 libraries:
pip install streamlit openai requests geopy
在构建并运行 app 之前,使用 venv package 创建一个 Python virtual environment。Virtual environments 会隔离项目 dependencies,防止与系统中的其他 Python 项目冲突,也让你的 setup 更容易在不同机器上复现。
然后用 Streamlit 创建基础 chat interface 的初始框架。只有几行代码,但包含了将 messages 打印到屏幕上的关键函数。使用 Streamlit 的 chat message function,并将每条 message 标记为 user 或 assistant,会自动以不同格式展示消息:
import streamlit as st
with st.chat_message("user"):
st.write("Hello, I am a user message.")
with st.chat_message("assistant"):
st.write("Hello, I am an assistant message.")
要运行 app,在 app 目录中使用以下命令:
streamlit run my_app.py
图 11-3:基础 Streamlit chat app
下一步是实现接收用户输入并通过 Streamlit session state 维护 chat history 的逻辑。基础 chatbot interface 的代码模板会将对话存储在 messages list 中。
messages 中的每一项都是一个 dictionary,包含 role 和 content 两个 keys。role 要么是 user,要么是 assistant。content 通常是用户问题或 assistant response。通过持续追加到 messages,你可以构建所有用户输入和 assistant 回复的 session history。
你还会使用 st.chat_message 交替渲染用户 prompt 和 assistant response:
import streamlit as st
st.title("Simple RAG Chat App")
if "messages" not in st.session_state:
st.session_state.messages = []
def simple_rag(query):
if "hello" in query.lower():
return "Hello! How can I help you?"
elif "rag" in query.lower():
return (
"RAG combines retrieval and generation for better "
"responses."
)
else:
return (
f"You asked: '{query}'. A real RAG system would "
f"search documents and generate a response."
)
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("Ask a question"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
response = simple_rag(prompt)
st.session_state.messages.append(
{"role": "assistant", "content": response}
)
with st.chat_message("assistant"):
st.markdown(response)
到这个阶段,app logic 仍然是一个包含 if / else 逻辑的 dummy function。接下来,用真实 RAG system 的逻辑替换这个 dummy。
在这个 recipe 构建的基础 RAG system 中,retriever 由两个 API calls 表示:
- 第一个 API call 会为给定 location 检索 latitude 和 longitude。
- 第二个 API call 使用这些坐标检索该 location 的天气。
在调用这些 APIs 之前,你会使用 LLM 检查用户 query,并判断其中是否包含 location。Weather API 需要 location;没有 location 时,问题无法回答:
import openai
import json
def extract_the_city_and_country(user_query):
client = openai.OpenAI()
prompt = (
f"Extract the city and country from: '{user_query}'. "
"Return as JSON with 'city' and 'country' keys."
)
response = client.chat.completions.create(
model="gpt-5.2",
messages=[{"role": "user", "content": prompt}],
response_format={"type": "json_object"},
)
try:
return json.loads(response.choices[0].message.content)
except:
return None
图 11-4 展示了输出。Response 遵循请求的 JSON structure:一个包含 city 和 country keys 的 dictionary。
图 11-4:从用户 query 中提取 city 和 country
接下来,使用 Nominatim API 将 city 或 country 解析成精确坐标:
from geopy.geocoders import Nominatim
def get_coordinates_for_city(city, country):
geolocator = Nominatim(user_agent="city_coordinate_finder")
location = geolocator.geocode(f"{city}, {country}")
if location:
return location.latitude, location.longitude
else:
print(f"Could not find coordinates for '{city}, {country}'.")
return None, None
图 11-5 中的输出展示了 location 的 latitude 和 longitude;在这个例子中,是 Paris 的坐标。
图 11-5:根据 city 和 country 计算出的地理位置
接下来,调用 Open-Meteo API,为给定坐标获取当前天气数据,并返回完整 API response:
import requests
def get_current_weather_open_meteo(latitude, longitude):
"""
Fetches current weather data from Open-Meteo for a given location.
"""
base_url = "https://api.open-meteo.com/v1/forecast"
params = {
"latitude": latitude,
"longitude": longitude,
"current": (
"temperature_2m,relative_humidity_2m,precipitation,"
"weather_code,wind_speed_10m"
),
# Automatically determine timezone based on location
"timezone": "auto",
}
try:
response = requests.get(base_url, params=params)
# Raise HTTPError for bad responses (4xx/5xx)
response.raise_for_status()
weather_data = response.json()
return weather_data
except requests.exceptions.RequestException as e:
print(f"Error fetching weather data: {e}")
return None
图 11-6 展示了返回的 dictionary。它不仅包含 temperature,还包含 humidity、time zone、elevation 等更多信息。
图 11-6:从 Open-Meteo API 检索到的天气数据
下一步会把天气信息输入发送给 LLM 的 prompt。模型会结合 API results 和用户问题进行解释,并生成有用答案。
在这个例子中,你使用一个基础 prompt template,包含用户问题和天气数据,并要求模型为用户总结天气:
import openai
def create_weather_prompt(user_query, weather_data):
if not weather_data:
return "No weather data available."
prompt = (
"You are a helpful assistant. Based on the weather "
"data below, answer the user's question in a clear "
"and simple way.\n\n"
f"User question: {user_query}\n\n"
f"Weather data (JSON):\n{weather_data}\n"
"Summarize the weather information so that it is "
"easy for anyone to understand."
)
return prompt
def send_prompt_to_llm(prompt):
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-5.2",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": prompt},
],
)
return response.choices[0].message.content.strip()
图 11-7 展示了 LLM 生成的最终 response。
图 11-7:最终 RAG weather app response
最后,将前面定义的函数组合成一个 Streamlit app 可以随时调用的单一 RAG workflow。在这个 recipe 中,helper functions 被移动到独立 Python module app_helper_functions 中,以保持主 app 文件干净,并让 RAG flow 更容易理解。下面代码展示了应该加入 app_helper_functions module 的 perform_rag function:
# In app_helper_functions.py
def perform_rag(user_query):
location_data = extract_the_city_and_country(user_query)
latitude, longitude = get_coordinates_for_city(
location_data["city"], location_data["country"]
)
weather_data = get_current_weather_open_meteo(latitude, longitude)
prompt = create_weather_prompt(user_query, weather_data)
response = send_prompt_to_llm(prompt)
return response
现在将 perform_rag function 集成进 Streamlit chat app。下面是完整应用,它将 RAG workflow 与 Streamlit interface 结合起来:
import streamlit as st
from app_helper_functions import perform_rag
st.title("Weather RAG Chat App")
if "messages" not in st.session_state:
st.session_state.messages = []
for message in st.session_state.messages:
with st.chat_message(message["role"]):
st.markdown(message["content"])
if prompt := st.chat_input("Ask about the weather"):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
response = perform_rag(prompt)
st.session_state.messages.append(
{"role": "assistant", "content": response}
)
with st.chat_message("assistant"):
st.markdown(response)
图 11-8 展示了 app 回答几个 sample questions 时的样子,例如关于 Berlin 或 Rome 天气的问题。
图 11-8:RAG weather app interface
Discussion
Session state bugs 来自 Streamlit 的 rerun behavior。每次用户交互都会触发 script rerun,从而将所有变量重置为初始值。只有明确存入 st.session_state 的数据才会保留。访问前始终检查 "messages" not in st.session_state,以避免 KeyError exceptions。
当 conversation history 很重要时,可以为 chatbots 使用 session state。对于每次 query 独立的 stateless apps,例如 search interfaces 或 dashboards,不要使用 session state,因为它会增加不必要的复杂性。
主要风险是 state 在用户之间泄漏。Streamlit 会为每个 browser tab 创建一个 session,但单 tab 测试无法发现 multiuser bugs。如果用户 A 上传了一个文档,用户 B 刷新页面后是否会看到用户 A 的文档?应使用两个 browser windows 提前测试并发现这类问题。
对于复杂 workflows,应将 RAG logic 与 UI code 分离:将 Streamlit script 控制在 100 行以内,把 API calls 和 prompt assembly 移到独立 modules,并将 compute-heavy operations,例如 embeddings、大模型调用,部署为 Lambda functions 或 microservices。Single-script structure 只适合 demos。当多个开发者共同维护 codebase,或 RAG logic 会在 Streamlit 外复用时,应使用 module separation。
调试 Streamlit apps 很有挑战,因为 print statements 在 reruns 期间可能乱序出现。应使用 VS Code debugger。图 11-9 展示了 setup:创建一个 configuration,以 streamlit 作为 module,将 run 作为第一个参数,将 app 文件名作为第二个参数。设置 breakpoints,以在每次 rerun 时检查 st.session_state。
图 11-9:在 VS Code 中创建新的 debugger configuration
See Also
Streamlit API reference 记录了 chat UI components,包括 st.chat_message 和 st.chat_input。
Streamlit session state documentation 解释了 multiuser applications 中的 state management。
11.3 为 Chatbot 添加 PDF Analyzer 功能
Problem
你想让 chatbot 接收 PDF uploads,并使用 LLMs 和 multimodal models 分析它们。
Solution
这个 recipe 展示如何给 chatbot 添加上传功能,然后使用 multimodal models 提取和分析文档文本。
图 11-10 展示了高层流程。由于 multimodal models 处理的是 JPEG 或 PNG 等 images,第一步是将 PDF 拆成单独页面图像。然后遍历所有页面,并使用 multimodal model 从每页提取文本。最后,将提取出的页面文本拼接成一个完整 document text,并在 full text 上执行 entity / value extraction step。
图 11-10:Entity extraction web app 的流程
该 app 使用 OpenAI 的 multimodal models,并使用 pdf2image 和 Pillow 预处理 PDFs,将每页转换为 image。安装如下:
pip install streamlit openai pdf2image pillow pydantic
这个 recipe 构建一个支持 drag-and-drop PDF upload 的 chatbot。首先定义一个 function,通过 pdf2image library 将 PDF 转换为 images,将每一页保存为 PNG,并将生成的 image paths 存入列表:
from pdf2image import convert_from_bytes
def convert_pdf_to_images(pdf_file):
# Read the PDF file and convert each page to an image
# (PIL Image objects)
images = convert_from_bytes(pdf_file.read())
# Save each image to a temporary PNG file and return the file paths
image_paths = []
for idx, image in enumerate(images):
temp_path = f"images/temp_page_{idx}.png"
image.save(temp_path, "PNG")
image_paths.append(temp_path)
return image_paths
接下来,将每张 image 发送给 multimodal model,由它提取文本。对于这个 OCR / extraction step,你使用一个简单 prompt,将 multimodal model 变成专注文本的 OCR assistant:
from openai import OpenAI
import base64
from io import BytesIO
client = OpenAI()
def perform_ocr_and_extract_entities(image_paths):
extracted_text = []
# Prepare the system message for GPT-5.2 vision
system_message = {
"role": "system",
"content": (
"You are an OCR assistant. Extract all text from the "
"provided images. Do not summarize or skip any content."
),
}
for image_path in image_paths:
with open(image_path, "rb") as image_file:
# Read image data and base64 encode it
base64_image = base64.b64encode(
image_file.read()
).decode("utf-8")
# Create the image_url dictionary directly from the base64 string
image_content = {
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{base64_image}"},
}
messages = [
system_message,
{
"role": "user",
"content": [image_content],
},
]
response = client.chat.completions.create(
model="gpt-5.2",
messages=messages,
max_tokens=4096,
)
full_text = response.choices[0].message.content
extracted_text.append(full_text)
return extracted_text
从 extracted text 中,现在可以运行 entity extraction step。这里,你从完整文档文本中提取 person entities,字段包括 Name、Age 和 City。你可以根据需要调整 prompt,以提取任何其他类型的 entity 或 value:
from openai import OpenAI
import json
client = OpenAI()
def extract_entities_from_text(extracted_text):
system_message = (
"Extract all people entities from the text as a JSON array "
"of objects with fields: ID (int), Name (str), Age (int), "
"City (str)."
)
text = "\n".join(extracted_text)
response = client.chat.completions.create(
model="gpt-5.2",
messages=[
{"role": "system", "content": system_message},
{"role": "user", "content": text},
],
max_tokens=1024,
)
extracted_data = response.choices[0].message.content
return extracted_data
最后一步,是将这些函数组合成一个 chatbot application:
st.title("Chat with File Upload")
uploaded_pdf = st.file_uploader(
"Upload a PDF (optional)",
type=["pdf"],
)
prompt = st.chat_input("Ask a question about the uploaded PDF")
if prompt and prompt.files:
with st.spinner("Processing PDF..."):
for uploaded_file in prompt.files:
if uploaded_file.type == "application/pdf":
images = convert_pdf_to_images(uploaded_file)
extracted_text = perform_ocr_and_extract_entities(images)
st.write("Extracted Text from PDF Pages:")
for idx, page_text in enumerate(extracted_text, 1):
st.markdown(f"**Page {idx}:**")
st.write(page_text)
extracted_data = extract_entities_from_text(extracted_text)
st.write("Extracted Entities:")
st.write(extracted_data)
启用 uploads 后,你会在 chat UI 左下角看到 paperclip icon,表示可以通过 drag-and-drop 附加文件。图 11-11 中,你还可以看到一个 sample file 的第一个结果。该文件包含 survey participants 的信息,包括 age、city of origin 和 first name。App 会将这些提取为 entities,并返回 dictionaries 列表。
图 11-11:Entity extraction web app
Discussion
Multimodal models 通过 vision transformers 处理 images,可以同时识别 text、tables 和 layout。它们比传统 OCR 更擅长处理扫描文档和 forms,因为它们理解空间关系,例如“这个数字在 Total column 中”或“这个签名位于 approval line 下方”。
将 PDFs 转换为 images 会创建统一处理 pipeline。扫描版和数字版 PDFs 都变成 images,因此一条代码路径可以处理所有情况。这简化了开发,但会增加成本,因为你处理的是 pixels,而不是 text layers。
当文档混合 text、tables 和 images,例如 invoices、forms、research papers;扫描文档没有 text layer;或 layout context 会影响含义时,可以使用这种方法。不要把它用于 plain-text PDFs,因为它比 PyMuPDF extraction 贵 10 到 100 倍;也不要用于需要亚秒级延迟的 real-time workflows,或每天处理数千页的场景,因为成本会随 page count 线性增长。
关键取舍是 extraction quality versus cost。对于 100 页文档,text-layer extraction 成本约 0.05 美元,而 multimodal OCR 成本约 0.50–2.00 美元。Digital-native documents,也就是从 Word、LaTeX 或浏览器创建、且只包含段落和 headings 的 PDFs,并不会从 multimodal processing 中受益。
当页面旋转或质量较差时,multimodal models 偶尔会 hallucinate field values 或跳过 sections。将数据写入下游系统前,始终使用 Pydantic 根据 expected schemas 验证 extracted data。生产系统需要 field presence validation、value range checks,以及针对低置信度 extraction 的 manual review queues。
你可以扩展 prompt,以提取更丰富结构:JSON 格式的 table data、figures 和 diagrams 的 image descriptions、用于文档导航的 heading hierarchies,或用于结构化数据录入的 form field mappings。
See Also
pypdf 可以在不需要 OCR 时,从 digital PDFs 中提取文本。
LlamaIndex SimpleDirectoryReader 提供包括 PDFs 在内的多格式文档解析。
LangChain PDF loaders 支持从简单到高级的各种 PDF parsing approaches。
11.4 将 RAG App 连接到 SQL Database
Problem
你想将 RAG chatbot 连接到 SQL database,使用户可以针对数据提问。
Solution
为了让用户用自然语言询问 SQL data,系统必须生成有效 SQL queries。这种能力称为 text to SQL。
图 11-12 展示了 RAG 系统收到新用户请求时的流程。首先,将所有可用 database documentation 和 data models embedding 到 vector store 中。当用户提问时,系统执行 semantic similarity search,找到 database documentation 中相关部分,并用它让 LLM 生成合适 SQL query。然后针对 database 执行 SQL query。最后,将检索到的数据与 prompt 和 generation model 的 instructions 组合起来,模型使用这些数据作为回答用户问题的基础。
图 11-12:Vanna AI 回答用户问题的步骤
这个 recipe 使用 OpenAI LLMs 进行 SQL query generation 和 final answer generation,使用 Vanna AI framework 处理 text-to-SQL 功能,并使用 Chroma 作为 vector store backend。对于 sample SQL database,会生成一个新的 SQLite database,并填入 sample data。安装这些 components:
pip install streamlit openai vanna chromadb sqlite3
第一步是设置 SQL database 并填入 sample data。SQLite 是一个轻量级 in-file database,非常适合小型项目。因为它只是一个文件,所以成本低、维护少。不过,它不包含 session 和 user management 等更高级数据库功能。
在本书 repository 中,你会找到一个 Python script,其中包含创建 SQLite database 并填充 sample data 的 SQL statements。Sample database 代表一个在线书店,包含 genres、clients、staff、deliveries、authors、books 和 sales items 等表。
下一步是初始化 Vanna AI。创建一个新 class,组合所需 Vanna 功能,包括 OpenAI chat model 和 Chroma vector store:
class MyVanna(ChromaDB_VectorStore, OpenAI_Chat):
def __init__(self, config=None):
ChromaDB_VectorStore.__init__(self, config=config)
OpenAI_Chat.__init__(self, config=config)
要将 Vanna 连接到 SQLite database,只需要下面一行代码:
vn.connect_to_sqlite('bookstore.db')
下一步是“训练” Vanna engine。这并不是训练一个 ML model;而是预处理 documentation,并将其存入 vector store。
表 11-1 展示了可用于处理不同 documentation types 的 Vanna functions 示例。
表 11-1:Vanna AI training functions 与示例
| Documentation type | Description | Example |
|---|---|---|
| DDL statements | 描述 database structure 的 schema definitions,例如 tables、columns、keys。 | vn.train(ddl="CREATE TABLE customers (id INT PRIMARY KEY, name TEXT)") |
| Documentation strings | 人类编写的 tables、columns、业务含义、constraints 和 conventions 说明。 | vn.train(documentation="Revenue is calculated as sales minus returns.") |
| SQL statements | 反映常见 access patterns 和 business logic 的 example SQL queries。 | vn.train(sql="SELECT id, name FROM employees WHERE department = 'Sales'") |
| Question-SQL pairs | 用户问题与正确 SQL query 配对。这是最有效的 training method。 | vn.train(question="Show me all active users", sql="SELECT * FROM users WHERE status = 'active'") |
接下来,从 database 加载 DDL statements,并用它们训练 Vanna。第一部分会直接从 database 检索 DDL,第二部分遍历这些 DDL statements,并逐个调用 Vanna 的 training function:
# Initialize Vanna
if "vn" not in st.session_state:
st.session_state.vn = MyVanna(
{
"api_key": os.environ.get("OPENAI_API_KEY"),
"model": "gpt-5.2",
"path": "./chroma_db",
}
)
st.session_state.vn.connect_to_sqlite("bookstore.db")
# Train on database schema
ddl_df = st.session_state.vn.run_sql(
"SELECT sql FROM sqlite_master WHERE sql IS NOT NULL"
)
for ddl in ddl_df["sql"]:
st.session_state.vn.train(ddl=ddl)
对于这个直接示例,DDL statements 已经足够,因为 bookstore database 是自解释的。对于更复杂 data models,你需要额外 documentation,例如 table relationships、column meanings 和 value formats。
接下来,测试 Vanna 是否按预期工作。它会接收一个 sample question,生成 SQL query,并执行它。该问题要求系统找出五本最畅销图书:
user_input = "What are the top 5 best-selling books?"
sql_query = vn.generate_sql(user_input)
result = vn.run_sql(sql_query)
图 11-13 中,你可以看到 LLM 生成的 SQL query,以及 database 返回的数据。
图 11-13:Top five best-selling books 的 SQL query
到这里,你已经有了一个可运行的 retrieval step,以及可以输入 generation step 的数据。接下来,将这些函数嵌入 Streamlit chatbot。下面代码展示了 chatbot app 的完整实现,包括对 Vanna 的调用:
import streamlit as st
import os
from vanna.openai import OpenAI_Chat
from vanna.chromadb import ChromaDB_VectorStore
from openai import OpenAI
class MyVanna(ChromaDB_VectorStore, OpenAI_Chat):
def __init__(self, config=None):
ChromaDB_VectorStore.__init__(self, config=config)
OpenAI_Chat.__init__(self, config=config)
# Initialize Vanna
if "vn" not in st.session_state:
st.session_state.vn = MyVanna({
"api_key": os.environ.get("OPENAI_API_KEY"),
"model": "gpt-5.2",
"path": "./chroma_db"
})
st.session_state.vn.connect_to_sqlite("bookstore.db")
# Train on database schema
ddl_df = st.session_state.vn.run_sql(
"SELECT sql FROM sqlite_master WHERE sql IS NOT NULL"
)
for ddl in ddl_df["sql"]:
st.session_state.vn.train(ddl=ddl)
st.title("SQL Chat App")
# Chat interface
if prompt := st.chat_input("Ask about the bookstore"):
# Generate and run SQL
sql = st.session_state.vn.generate_sql(prompt)
result = st.session_state.vn.run_sql(sql)
# Generate answer
client = OpenAI()
answer = client.chat.completions.create(
model="gpt-5-mini",
messages=[{
"role": "user",
"content": (
f"Answer based on this data: {result}\n"
f"Question: {prompt}"
)
}]
).choices[0].message.content
# Display
st.chat_message("user").write(prompt)
st.chat_message("assistant").write(answer)
with st.expander("SQL Result"):
st.write(result)
图 11-14 展示了 chatbot 的样子。在这个示例中,SQL query 只返回两条结果,因为测试 database 只有两条 entries。它仍然只是一个 sample database。
图 11-14:Vanna AI Streamlit chatbot interface
Discussion
Text-to-SQL 通过让非技术用户以自然语言查询数据,民主化了数据库访问。当 database 文档良好、包含 5–20 张表且 relationships 清晰时,用户可以快速获得常见问题的答案,例如 top N queries、filters 和 aggregations,而无需自己写 SQL。
当用户可以在执行前审查 generated SQL 时,该方法效果最好。展示 query 可以让用户发现错误,并建立对系统的信任。这种透明性也能帮助用户随着时间学习 SQL patterns。
Text-to-SQL 有可预测的 failure modes。对于 “Show me trends” 这类模糊问题,模型会因为缺少业务上下文而猜错 metrics。跨 5 张以上 tables 的复杂 joins 经常 timeout 或返回错误结果。糟糕 documentation 会导致 hallucinated column names,模型会编造看似合理但不存在的 fields,例如当真实 column 是 clv 时,模型可能生成 customer_lifetime_value。
当用户不懂 SQL 且经常提出模糊问题时,不要使用 text-to-SQL,因为他们无法验证 generated queries;当 queries 需要 database 中不存在的 business logic 时,也不要使用;当 compliance 或 audit trails 要求 deterministic behavior 时,也不适合,因为 LLM output 会变化。
Agentic approach,也就是第 8 章的方法,更适合处理这些情况。你可以将 10–20 个 SQL query templates 定义为带参数的 tools。模型选择使用哪个 template,并填充参数。这既允许自然语言输入,又能提供 deterministic queries 和清晰 audit trails。
对于生产系统,应从 agentic approach 开始。只有当用户明确要求 free-form text-to-SQL,且 database documentation 很扎实时,再加入它。Hybrid model,也就是常见 queries 使用 agentic templates、ad hoc exploration 使用 text-to-SQL,非常适合 analytics teams。
See Also
LangChain SQL agent documentation 解释了如何构建 agents,将自然语言问题转换成 SQL queries,并在 relational databases 上执行。
LlamaIndex SQL Query Engine 支持针对 SQL databases 的自然语言查询。
Vanna AI GitHub repository 包含 text-to-SQL 实现的更多示例和配置选项。
11.5 使用 Docker 和 AWS 部署 Streamlit App
Problem
你需要以稳健、可复现且可扩展的方式部署 app。
Solution
这个 recipe 展示如何使用 Docker containers 在 AWS 上部署 Streamlit app。Docker 是一个容器化平台,会将你的应用及其所有 dependencies 打包成可移植 container image。关于 Docker 的更多信息,请参考 Docker 官方网站。
在 AWS 上,你会使用 Amazon Elastic Container Registry(ECR)存储 Docker images,并使用 Amazon Elastic Container Service(ECS)运行 containers。ECS 管理 container lifecycle,包括 scaling 和 health monitoring。
要跟随这个 recipe,你需要:
- 本地机器安装 Docker Desktop,用于构建和测试 images。Docker Desktop 个人使用免费,但企业用户可能需要付费 license。
- 一个 cloud provider account,用于部署 container。
这个 recipe 简要描述 AWS,但你也可以使用其他 providers。主流云厂商 AWS、Google Cloud 和 Azure 都提供 free tiers,其有限额度通常足够小型 side projects 使用。
为了复现 container build 和 deployment,使用包含以下文件的 folder structure:
Dockerfilerequirements.txt,即 Python dependencies- Streamlit app code
entrypoint.sh,用于启动 app
第一步是创建 Dockerfile,它指定所有 dependencies,内容如下:
# Dockerfile (example)
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8501
ENTRYPOINT ["bash", "entrypoint.sh"]
Dockerfile 定义了 Docker container 内部执行的步骤。它将 working directory 设置为 ./app。将文件复制到 image 时,请确保创建这个 working directory,也就是这里的 app folder,然后将 repository files 复制进去。文件复制完成后,image 会通过 pip 安装 requirements,并暴露端口 8501。
最后一个命令会在 container 启动时执行 entrypoint.sh script。该 script 会启动 Streamlit app,并设置 server port,使 app 可以通过已知地址访问。
现在可以使用 Dockerfile 创建实际 Docker image。打开 terminal 或 command prompt,进入 Dockerfile 所在目录,并执行以下命令:
docker build -t rag-streamlit-app:latest .
这会构建你的 image。部署前先在本地测试:
docker run -p 8501:8501 rag-streamlit-app:latest
打开 http://localhost:8501,验证 app 是否工作。如果你看到 Streamlit interface,说明 container 已准备好部署。
要部署到 AWS,执行以下步骤:
- 安装 AWS CLI。
- 创建 ECR repository:
aws ecr create-repository --repository-name rag-streamlit-app。 - 推送 image,按照 ECR console 中的 authentication commands 操作。
- 创建指向 ECR image 的 ECS task definition。
- 在 ECS Fargate,也就是 serverless container runtime,上部署 task。
要部署到 Google Cloud,可以使用 Cloud Run。将 image 上传到 Google Container Registry,然后用一条命令部署。
要部署到 Azure,可以使用 Container Apps。工作流与 GCP Cloud Run 类似。
WARNING
Fargate 按 vCPU-hour 和 memory 收费。一个 1 vCPU、2 GB RAM 的单 container 24/7 运行,每月成本大约 30–40 美元。启用 auto-scaling 时要谨慎。流量高峰期间不受限制的 scaling 可能产生意外成本。应在 ECS service configuration 中设置 maximum task count limits。
Discussion
Docker 会将应用和所有 dependencies 打包进可移植 container。这让部署与平台无关且可复现。任何开发者都可以从 Dockerfile 构建相同 image,确保 development、testing 和 production environments 之间一致。
这个 recipe 覆盖 container creation 和 manual deployment。生产系统还需要额外三层能力:
Infrastructure as code(IaC)
Terraform 或 AWS Cloud Development Kit(CDK)等 IaC tools 会用 version-controlled files 定义基础设施。如果没有 IaC,调试生产问题时,你只能手动点击 consoles 来回忆配置。IaC 可以防止 configuration drift。当你通过 console 手动更新生产环境时,dev environment 会逐渐偏离。IaC 强制两个环境都从相同 source files 构建。
Continuous integration and continuous deployment(CI/CD)pipeline
使用 GitHub Actions 或 GitLab CI 在每次 commit 到 main 时重建和重新部署。Pipeline 会运行 tests、构建 Docker image、推送到 ECR,并更新 ECS task definition。如果 tests 失败,pipeline 会阻止部署。Manual deployment 会引入人为错误。你可能忘记运行 tests、从错误 branch 构建,或跳过 migration script。CI/CD pipelines 会让这些错误无法发生。
Secrets management
不要在 Docker image 中硬编码 API keys。Images 存储在 registries 中,任何有访问权限的人都可以看到。使用 AWS Secrets Manager,或由 ECS 在 runtime 注入的 environment variables。这可以让 credentials 不进入 version control 和 container registries。如果 image 泄漏,例如 registry permissions 配置错误或 CI/CD credentials 被攻破,攻击者就能拿到你的 API keys。Rotating credentials 意味着必须重建并重新部署所有 images。使用 secrets management 时,只需要在一个地方轮换。
对于 prototypes,manual deployment 是可以接受的。当你确信 app 会长期运行、多名开发者需要部署,或 compliance 要求 audit trails 时,再添加这些层。
See Also
AWS ECS container image guide 提供了创建 container images 的分步说明。
Docker Get Started guide 覆盖 Docker 基础,包括构建和运行 containers。
Streamlit Docker deployment tutorial 演示了如何将 Streamlit applications 容器化。