告别 BI 工具的高昂授权费和繁琐配置。用 Streamlit + 大模型 API,不到 200 行代码就能搭建一个"自然语言查数据"的 AI 分析平台。业务人员说人话,AI 帮你出图表。
为什么选 Streamlit?
做数据分析和 AI 应用的前端,Streamlit 是 Python 生态里最省力的选择。
数据分析前端框架对比
| 特性 | Streamlit | Gradio | Dash | Jupyter + Voila |
|---|---|---|---|---|
| 学习成本 | 极低 | 低 | 中 | 低 |
| 代码量 | 50-200 行 | 50-150 行 | 300-800 行 | 100-300 行 |
| 图表支持 | 原生 + Plotly | 支持 | 原生 | 需配置 |
| 实时更新 | 支持 | 支持 | 支持 | 有限 |
| 部署难度 | 一行命令 | 一行命令 | 中等 | 中等 |
| 适合场景 | 快速原型、内部工具 | ML Demo | 企业级 Dashboard | 交互式分析 |
核心优势:纯 Python 编写、零前端知识、迭代极速。
第一步:环境搭建
# 创建虚拟环境
python -m venv ai_analytics
source ai_analytics/bin/activate # Windows: ai_analytics\Scripts\activate
# 安装依赖
pip install streamlit pandas plotly openai sqlalchemy psycopg2-binary python-dotenv
项目结构:
ai_analytics/
├── app.py # Streamlit 主应用
├── llm_service.py # 大模型服务封装
├── database.py # 数据库连接
├── requirements.txt # 依赖清单
├── .env # 环境变量配置
└── config.py # 配置管理
第二步:数据库连接层
"""
database.py - 数据库连接与查询执行
支持 PostgreSQL / MySQL / SQLite
"""
import os
import pandas as pd
from sqlalchemy import create_engine, text, inspect
from dotenv import load_dotenv
load_dotenv()
class DatabaseManager:
"""数据库管理器"""
def __init__(self, connection_string: str = None):
self.conn_str = connection_string or os.getenv("DATABASE_URL", "sqlite:///sample.db")
self.engine = create_engine(self.conn_str, pool_pre_ping=True, pool_size=5)
def get_tables(self) -> list[str]:
"""获取所有表名"""
inspector = inspect(self.engine)
return inspector.get_table_names()
def get_table_schema(self, table_name: str) -> list[dict]:
"""获取表结构"""
inspector = inspect(self.engine)
columns = inspector.get_columns(table_name)
return [
{
"name": col["name"],
"type": str(col["type"]),
"nullable": col.get("nullable", True),
"default": str(col.get("default", "")),
}
for col in columns
]
def get_full_schema(self) -> str:
"""获取完整的数据库 schema 描述(给大模型用)"""
tables = self.get_tables()
schema_parts = []
for table in tables:
columns = self.get_table_schema(table)
col_desc = ", ".join([f"{c['name']} ({c['type']})" for c in columns])
schema_parts.append(f"表 {table}: {col_desc}")
# 获取示例数据(前 3 行)
try:
df = pd.read_sql(f"SELECT * FROM {table} LIMIT 3", self.engine)
sample = df.to_string(index=False)
schema_parts.append(f"示例数据:\n{sample}")
except Exception:
pass
schema_parts.append("")
return "\n".join(schema_parts)
def execute_query(self, sql: str) -> pd.DataFrame:
"""执行 SQL 查询并返回 DataFrame"""
try:
df = pd.read_sql(text(sql), self.engine)
return df
except Exception as e:
raise ValueError(f"SQL 执行错误: {str(e)}")
def execute_write(self, sql: str) -> int:
"""执行写操作,返回影响行数"""
with self.engine.connect() as conn:
result = conn.execute(text(sql))
conn.commit()
return result.rowcount
# 初始化示例数据
def init_sample_data(db: DatabaseManager):
"""创建示例数据表"""
create_sql = """
CREATE TABLE IF NOT EXISTS employees (
id INTEGER PRIMARY KEY,
name VARCHAR(100),
department VARCHAR(50),
position VARCHAR(50),
salary DECIMAL(10, 2),
hire_date DATE,
city VARCHAR(50)
);
CREATE TABLE IF NOT EXISTS sales (
id INTEGER PRIMARY KEY,
employee_id INTEGER,
product_name VARCHAR(100),
amount DECIMAL(10, 2),
quantity INTEGER,
sale_date DATE,
region VARCHAR(50)
);
CREATE TABLE IF NOT EXISTS departments (
name VARCHAR(50) PRIMARY KEY,
budget DECIMAL(12, 2),
headcount INTEGER,
location VARCHAR(50)
);
"""
with db.engine.connect() as conn:
conn.execute(text(create_sql))
conn.commit()
# 检查是否有数据
df = pd.read_sql("SELECT COUNT(*) as cnt FROM employees", db.engine)
if df.iloc[0]['cnt'] == 0:
import random
from datetime import date, timedelta
departments = ["技术部", "销售部", "市场部", "财务部", "人力资源部"]
cities = ["北京", "上海", "郑州", "深圳", "杭州"]
products = ["云服务器", "数据库", "CDN加速", "对象存储", "AI模型服务"]
employees = []
for i in range(1, 51):
hire = date(2020, 1, 1) + timedelta(days=random.randint(0, 2000))
employees.append((
i,
f"员工{i}",
random.choice(departments),
random.choice(["初级", "中级", "高级", "专家"]),
random.randint(8000, 35000),
hire,
random.choice(cities),
))
sales = []
for i in range(1, 201):
sale_date = date(2025, 1, 1) + timedelta(days=random.randint(0, 365))
sales.append((
i,
random.randint(1, 50),
random.choice(products),
random.randint(500, 50000),
random.randint(1, 20),
sale_date,
random.choice(["华东", "华南", "华北", "华中", "西部"]),
))
dept_data = []
for dept in departments:
dept_data.append((
dept,
random.randint(100000, 500000),
random.randint(8, 15),
random.choice(cities),
))
for emp in employees:
with db.engine.connect() as conn:
conn.execute(text(
"INSERT INTO employees VALUES (?,?,?,?,?,?,?)"
), emp)
conn.commit()
for sale in sales:
with db.engine.connect() as conn:
conn.execute(text(
"INSERT INTO sales VALUES (?,?,?,?,?,?,?)"
), sale)
conn.commit()
for d in dept_data:
with db.engine.connect() as conn:
conn.execute(text(
"INSERT INTO departments VALUES (?,?,?,?)"
), d)
conn.commit()
第三步:大模型服务封装
"""
llm_service.py - 大模型服务封装
支持 OpenAI / DeepSeek / 通义千问等兼容 OpenAI API 的模型
"""
import os
import json
from openai import OpenAI
from dotenv import load_dotenv
load_dotenv()
class LLMService:
"""大模型服务"""
def __init__(
self,
api_key: str = None,
base_url: str = None,
model: str = None,
):
self.client = OpenAI(
api_key=api_key or os.getenv("LLM_API_KEY"),
base_url=base_url or os.getenv("LLM_BASE_URL", "https://api.openai.com/v1"),
)
self.model = model or os.getenv("LLM_MODEL", "deepseek-chat")
SYSTEM_PROMPT = """你是一个数据分析专家。你的任务是根据用户的自然语言描述,生成对应的 SQL 查询。
数据库 schema 如下:
{schema}
规则:
1. 只生成 SELECT 查询,不要生成 INSERT/UPDATE/DELETE
2. 使用标准的 SQL 语法
3. 如果用户的问题模糊,选择最合理的解释
4. 直接返回 SQL,不要有任何解释文字
5. 日期比较使用字符串比较或函数
"""
ANALYSIS_PROMPT = """你是数据分析专家。根据以下查询结果,用简洁专业的中文进行分析总结。
查询问题:{question}
查询结果:
{data}
请从以下角度分析:
1. 关键数据指标和趋势
2. 异常值和值得关注的点
3. 2-3 条可执行的业务建议
"""
def generate_sql(self, question: str, schema: str) -> str:
"""根据自然语言生成 SQL"""
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": self.SYSTEM_PROMPT.format(schema=schema)},
{"role": "user", "content": question},
],
temperature=0.1, # 低温度,确保生成稳定的 SQL
max_tokens=500,
)
sql = response.choices[0].message.content.strip()
# 清理可能的 markdown 格式
if sql.startswith("```sql"):
sql = sql[6:]
if sql.startswith("```"):
sql = sql[3:]
if sql.endswith("```"):
sql = sql[:-3]
return sql.strip()
def analyze_data(self, question: str, data_df) -> str:
"""分析查询结果"""
data_str = data_df.to_string(max_rows=50)
response = self.client.chat.completions.create(
model=self.model,
messages=[
{"role": "system", "content": "你是数据分析专家,用中文回答。"},
{"role": "user", "content": self.ANALYSIS_PROMPT.format(
question=question, data=data_str
)},
],
temperature=0.5,
max_tokens=1500,
)
return response.choices[0].message.content
第四步:Streamlit 主应用
"""
app.py - AI 数据分析平台主界面
"""
import streamlit as st
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from database import DatabaseManager, init_sample_data
from llm_service import LLMService
# ==================== 页面配置 ====================
st.set_page_config(
page_title="AI 数据分析平台",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded",
)
# ==================== 初始化 Session State ====================
if "messages" not in st.session_state:
st.session_state.messages = []
if "query_history" not in st.session_state:
st.session_state.query_history = []
# ==================== 侧边栏配置 ====================
with st.sidebar:
st.title("配置中心")
st.subheader("大模型配置")
llm_model = st.selectbox(
"选择模型",
["deepseek-chat", "gpt-4o-mini", "qwen-turbo"],
index=0,
)
llm_api_key = st.text_input("API Key", type="password")
llm_base_url = st.text_input(
"API Base URL",
value="https://api.deepseek.com/v1",
)
st.divider()
st.subheader("数据库")
db_choice = st.radio("数据源", ["示例数据库", "自定义连接"], index=0)
if db_choice == "自定义连接":
db_url = st.text_input("数据库连接串", placeholder="postgresql://user:pass@host:5432/db")
else:
db_url = "sqlite:///sample.db"
st.divider()
if st.button("清除历史", use_container_width=True):
st.session_state.messages = []
st.session_state.query_history = []
st.rerun()
# ==================== 初始化服务 ====================
@st.cache_resource
def get_db(url: str):
"""获取数据库连接(带缓存)"""
db = DatabaseManager(url)
init_sample_data(db)
return db
@st.cache_resource
def get_llm(key: str, base_url: str, model: str):
"""获取 LLM 服务(带缓存)"""
return LLMService(api_key=key, base_url=base_url, model=model)
db = get_db(db_url)
# ==================== 主界面 ====================
st.title("AI 数据分析平台")
st.caption("用自然语言查询数据,AI 帮你生成 SQL、执行查询、分析结果")
# 显示数据概览
with st.expander("数据库表结构", expanded=False):
tables = db.get_tables()
for table in tables:
with st.container():
cols = st.columns([3, 1])
cols[0].markdown(f"**{table}**")
cols[1].text("")
schema = db.get_table_schema(table)
schema_df = pd.DataFrame(schema)
st.dataframe(schema_df, use_container_width=True, hide_index=True)
# ==================== 查询历史标签页 ====================
tab_chat, tab_history, tab_explore = st.tabs(["AI 对话查询", "查询历史", "数据探索"])
with tab_chat:
# 聊天消息显示
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
if msg["role"] == "user":
st.markdown(msg["content"])
else:
# AI 回复包含 SQL + 数据 + 图表 + 分析
if msg.get("sql"):
st.code(msg["sql"], language="sql")
if msg.get("data") is not None:
st.dataframe(msg["data"], use_container_width=True)
if msg.get("fig"):
st.plotly_chart(msg["fig"], use_container_width=True)
if msg.get("analysis"):
st.info(msg["analysis"])
if msg.get("error"):
st.error(msg["error"])
# 用户输入
if prompt := st.chat_input("输入你想查询的问题,例如:各部门的平均薪资是多少?"):
# 显示用户消息
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
# AI 处理
if not llm_api_key:
response_msg = {
"role": "assistant",
"error": "请先在左侧配置 API Key",
}
else:
with st.spinner("AI 正在思考..."):
try:
llm = get_llm(llm_api_key, llm_base_url, llm_model)
schema = db.get_full_schema()
# 1. 生成 SQL
sql = llm.generate_sql(prompt, schema)
# 2. 执行查询
df = db.execute_query(sql)
# 3. 自动生成图表
fig = None
if not df.empty:
numeric_cols = df.select_dtypes(include="number").columns.tolist()
non_numeric_cols = df.select_dtypes(exclude="number").columns.tolist()
if numeric_cols:
x_col = non_numeric_cols[0] if non_numeric_cols else df.index.name or df.columns[0]
y_col = numeric_cols[0] if len(numeric_cols) == 1 else None
if y_col:
fig = px.bar(df, x=x_col, y=y_col, title=f"{prompt}")
elif len(numeric_cols) >= 2:
fig = px.scatter(
df,
x=numeric_cols[0],
y=numeric_cols[1],
color=non_numeric_cols[0] if non_numeric_cols else None,
title=f"{prompt}",
)
else:
fig = px.bar(df, x=df.columns[0], y=numeric_cols[0], title=f"{prompt}")
# 4. 分析结果
analysis = llm.analyze_data(prompt, df)
response_msg = {
"role": "assistant",
"sql": sql,
"data": df,
"fig": fig,
"analysis": analysis,
}
# 保存到历史
st.session_state.query_history.append({
"question": prompt,
"sql": sql,
"rows": len(df),
})
except Exception as e:
response_msg = {
"role": "assistant",
"error": f"出错了:{str(e)}",
}
st.session_state.messages.append(response_msg)
with st.chat_message("assistant"):
if response_msg.get("sql"):
st.code(response_msg["sql"], language="sql")
if response_msg.get("data") is not None:
st.dataframe(response_msg["data"], use_container_width=True)
if response_msg.get("fig"):
st.plotly_chart(response_msg["fig"], use_container_width=True)
if response_msg.get("analysis"):
st.info(response_msg["analysis"])
if response_msg.get("error"):
st.error(response_msg["error"])
with tab_history:
if st.session_state.query_history:
history_df = pd.DataFrame(st.session_state.query_history)
st.dataframe(history_df, use_container_width=True)
else:
st.info("暂无查询历史,去 AI 对话标签页开始查询吧")
with tab_explore:
# 快速数据探索
selected_table = st.selectbox("选择表", db.get_tables())
if selected_table:
df = pd.read_sql(f"SELECT * FROM {selected_table} LIMIT 100", db.engine)
st.dataframe(df, use_container_width=True)
# 快速统计
st.subheader("统计概览")
st.dataframe(df.describe(), use_container_width=True)
第五步:运行与部署
本地运行
# 创建 .env 文件
cat > .env << EOF
LLM_API_KEY=your-deepseek-api-key
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat
DATABASE_URL=sqlite:///sample.db
EOF
# 启动应用
streamlit run app.py --server.port 8501
云服务器部署
| 部署方式 | 操作步骤 | 成本 |
|---|---|---|
| 裸机部署 | SSH 到服务器,pip install + streamlit run | 最低配 2C4G(约 ¥50/月) |
| Docker 部署 | Dockerfile 打包镜像,docker compose up | 同上 |
| 腾讯云 Web 函数 | 打包为函数 + API 网关触发 | 按调用量计费 |
| Streamlit Cloud | GitHub 仓库连接 | 免费(公开项目) |
# Docker 部署方式
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
COPY . .
EXPOSE 8501
CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
# docker-compose.yml
version: '3.8'
services:
ai-analytics:
build: .
ports:
- "8501:8501"
env_file:
- .env
restart: unless-stopped
常见问题与优化
| 问题 | 解决方案 |
|---|---|
| SQL 生成不准确 | 在 system prompt 中提供更完整的 schema 和示例 |
| 查询超时 | 增加 max_tokens,或对大表添加 LIMIT |
| 图表不美观 | 自定义 Plotly 主题色和布局 |
| 并发用户多 | 使用 Gunicorn + 多 worker 运行 |
| 数据安全 | 加上身份认证中间件,按部门控制数据权限 |
总结
Streamlit + 大模型这个组合,让"自然语言查数据"这件事从 POC 走向了生产可用。核心优势:
- 开发极快:200 行代码搞定从前端到后端全链路
- 对业务友好:非技术人员也能通过自然语言自助分析
- 灵活扩展:换个模型、加个认证、接个新数据源都很简单
- 部署简单:一台 2C4G 的云服务器就能跑起来
如果你需要云服务器来部署,腾讯云/阿里云轻量应用服务器 2C4G 才几十块一个月,性能完全够用。
👤 作者简介
一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~
博客:yunduancloud.icu