Streamlit + 大模型:10 分钟搭建企业内部 AI 数据分析平台

8 阅读9分钟

告别 BI 工具的高昂授权费和繁琐配置。用 Streamlit + 大模型 API,不到 200 行代码就能搭建一个"自然语言查数据"的 AI 分析平台。业务人员说人话,AI 帮你出图表。

为什么选 Streamlit?

做数据分析和 AI 应用的前端,Streamlit 是 Python 生态里最省力的选择。

数据分析前端框架对比

特性StreamlitGradioDashJupyter + 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 CloudGitHub 仓库连接免费(公开项目)
# 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 走向了生产可用。核心优势:

  1. 开发极快:200 行代码搞定从前端到后端全链路
  2. 对业务友好:非技术人员也能通过自然语言自助分析
  3. 灵活扩展:换个模型、加个认证、接个新数据源都很简单
  4. 部署简单:一台 2C4G 的云服务器就能跑起来

如果你需要云服务器来部署,腾讯云/阿里云轻量应用服务器 2C4G 才几十块一个月,性能完全够用。


👤 作者简介

一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~

博客:yunduancloud.icu