🔥 项目难度:⭐⭐⭐☆☆(适合有一定Linux/基于大模型+MySQLMySQL基础的开发者)
🎯 适用人群:DBA、运维工程师、Python后端、高校数据库实训、毕业设计
📌 核心技术:MySQL 8.0 + Python 3.11 + 大模型API + Streamlit + 提示词工程
写在前面
这篇文章不是简单的Demo演示,而是一个真正能落地生产的完整项目 。项目来自InnoAI SQL助手官方文档,我在原文基础上补充了大量踩坑经验、原理解析、安全加固细节,确保你跟着做一遍就能真正掌握。
这个项目最大的价值在于:它把大模型的自然语言能力和传统数据库的严谨性结合起来,既降低了使用门槛,又不牺牲数据安全。整套架构完全可以迁移到企业内部系统中使用。
一、项目全景解析
1.1 项目到底解决了什么问题?
| 痛点场景 | 传统做法 | 本方案 |
|---|---|---|
| 业务人员不会SQL | 找DBA帮忙写,排队等半天 | 直接说人话,AI自动生成SQL |
| DBA调优效率低 | 手动EXPLAIN分析,经验依赖强 | AI自动解析执行计划,输出可落地方案 |
| 报表统计繁琐 | 每次都要重新写SQL导出Excel | 一句话生成,AI顺带帮你做业务解读 |
| 安全顾虑 | 怕业务人员误操作删库 | 内置SQL拦截,只允许SELECT |
1.2 项目组织架构
InnoAI SQL助手
├── 接入交互层(终端CLI + Web可视化)
├── Python程序核心层(4个py文件 + 1个env配置)
│ ├── main.py # 总调度入口,核心业务逻辑
│ ├── web_main.py # Streamlit Web界面
│ ├── mysql_client.py # 数据库封装+安全拦截
│ ├── prompts.py # 提示词工程统一管理
│ └── .env # 敏感配置(权限600)
├── 底层数据层(MySQL 8.0.45)
└── AI服务层(腾讯云TokenHub,兼容OpenAI接口)

二、环境搭建(每一步都有坑)
⚠️ 重要提示 :以下所有命令在 openEuler 22.03 SP4 环境下实测通过。其他发行版(如CentOS 7)可能需要调整部分参数。
2.1 系统初始化(最容易忽略的一步)
# 关闭SELinux(不改这个后面MySQL可能起不来)
sed -i '7s/enforcing/disabled/' /etc/selinux/config
# 关闭防火墙(生产环境请改为放行3306/8501端口)
systemctl disable --now firewalld
systemctl status firewalld
# 修改主机名(养成好习惯,别用localhost)
hostnamectl set-hostname server
bash
# 配置时间同步(日志时间不准排查问题会很痛苦)
vim /etc/chrony.conf
# 将原有server行替换为:
server ntp.aliyun.com iburst
systemctl restart chronyd
chronyc sources
# 看到 ^* 203.107.6.88 说明同步成功
2.2 安装编译依赖(缺一个都会翻车)
dnf install -y gcc gcc-c++ make cmake \
zlib-devel bzip2-devel openssl-devel \
ncurses-devel sqlite-devel readline-devel \
libffi-devel tk-devel wget tar vim \
tree net-tools openssh-server
为什么需要这么多?
-
openssl-devel:Python的SSL模块依赖,没有它pip装不了任何https源的包 -
libffi-devel:大模型API调用时需要加密库支持 -
ncurses-devel:后面MySQL启动可能依赖这个
2.3 Python 3.11.9 源码编译安装(重点!)
为什么要源码编译?
系统自带的Python版本太低(通常是3.6或3.9),而LangChain等新库要求3.10+。另外,源码编译可以精确控制安装路径,避免污染系统Python。
cd /usr/local/src
# 下载(如果官网慢,可以用国内镜像)
wget https://www.python.org/ftp/python/3.11.9/Python-3.11.9.tgz
# 解压
tar -zxvf Python-3.11.9.tgz
cd Python-3.11.9
# 配置(--enable-shared 是关键!)
./configure --prefix=/usr/local/python3.11 --enable-shared
# 编译(-j$(nproc) 启用多核加速,8核机器能快好几倍)
make -j$(nproc) && make install
⚠️ 踩坑预警:
-
--enable-shared必须加 :否则后面import ssl会报错ImportError: libpython3.11.so.1.0: cannot open shared object file -
编译完要配置动态库:
echo "/usr/local/python3.11/lib" > /etc/ld.so.conf.d/python311.conf
ldconfig -
建立软链接(不要覆盖系统Python!):
ln -s /usr/local/python3.11/bin/python3.11 /usr/local/bin/python3
ln -s /usr/local/python3.11/bin/pip3.11 /usr/local/bin/pip3 -
验证安装:
python3 --version # 应该输出 Python 3.11.9
python3 -c "import ssl; print(ssl.OPENSSL_VERSION)" # 验证SSL模块
pip3 --version
2.4 配置pip国内镜像(不然下载慢到怀疑人生)
mkdir -p ~/.pip
vim ~/.pip/pip.conf
[global]
index-url = http://mirrors.aliyun.com/pypi/simple/
[install]
trusted-host = mirrors.aliyun.com
2.5 安装Python依赖
pip3 install --upgrade pip
pip3 install pymysql python-dotenv tabulate \
langchain langchain-openai sqlparse streamlit
各包的作用:
| 包名 | 用途 |
|---|---|
| pymysql | MySQL数据库连接驱动 |
| python-dotenv | 读取.env配置文件 |
| tabulate | 终端表格美化输出 |
| langchain-openai | 大模型API调用(兼容OpenAI标准) |
| sqlparse | SQL语句解析和格式化 |
| streamlit | Web可视化界面 |
验证依赖是否正常:
/usr/local/python3.11/bin/python3 -c "import langchain_openai; print('3.11依赖全部正常')"

三、MySQL 8.0.45 部署(生产级)
3.1 下载并解压
cd /
# 假设已经通过XFTP上传了mysql-8.0.45-linux-glibc2.28-x86_64.tar.xz到根目录
tar -xvf mysql-8.0.45-linux-glibc2.28-x86_64.tar.xz
# 移动到标准位置
mv mysql-8.0.45-linux-glibc2.28-x86_64 /usr/local/mysql
cd /usr/local/mysql
3.2 创建用户和目录
# 创建mysql用户组
groupadd mysql
# 创建mysql系统用户(不可登录,提高安全性)
useradd -r -g mysql -s /sbin/nologin mysql
# 创建数据目录
mkdir data
chmod -R 750 data
# 修改属主
chown -R mysql:mysql /usr/local/mysql
3.3 初始化数据库(记住初始密码!)
# 初始化(--initialize 会生成随机密码,--initialize-insecure 是无密码)
bin/mysqld --initialize --user=mysql \
--basedir=/usr/local/mysql \
--datadir=/usr/local/mysql/data
⚠️ 重点:初始化完成后,终端会输出一串随机密码,类似:
A temporary password is generated for root@localhost: kX!p9qR#2mZz
务必复制保存!
3.4 启动MySQL并登录
# 后台启动
bin/mysqld_safe --user=mysql &
# 另开一个终端,查看进程
ps -ef | grep mysql
# 登录(粘贴刚才的初始密码)
bin/mysql -u root -p
如果出现 libncurses.so.5: cannot open shared object file 错误:
ln -s /usr/lib64/libncurses.so.6.3 /usr/lib64/libncurses.so.5
ln -s /usr/lib64/libtinfo.so.6.3 /usr/lib64/libtinfo.so.5
3.5 修改root密码
-- 修改密码(MySQL 8.0必须用mysql_native_password插件才能被pymysql连接)
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';
FLUSH PRIVILEGES;
-- 验证
SELECT user, host, plugin FROM mysql.user;
-- 应该看到 root@localhost 的 plugin 是 mysql_native_password
3.6 创建测试数据库和表
CREATE DATABASE testdb;
USE testdb;
-- 创建订单表
CREATE TABLE order_info(
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID',
user_id INT COMMENT '用户ID',
order_name VARCHAR(200) COMMENT '商品名称',
pay_amount DECIMAL(10,2) COMMENT '支付金额',
create_time DATETIME COMMENT '下单时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电商订单业务表';
-- 插入20条标准测试数据
INSERT INTO order_info(user_id, order_name, pay_amount, create_time) VALUES
(1001, '智能手机', 2999.00, '2026-05-01 10:20:00'),
(1001, '有线入耳耳机', 199.00, '2026-05-02 14:10:00'),
(1002, '14英寸轻薄笔记本电脑', 5499.00, '2026-05-03 09:30:00'),
(1002, '无线蓝牙鼠标', 89.00, '2026-05-03 09:35:00'),
(1003, '平板学习机', 1799.00, '2026-05-04 11:05:00'),
(1003, '平板专用保护壳', 49.00, '2026-05-04 11:08:00'),
(1004, '机械游戏键盘', 349.00, '2026-05-05 16:42:00'),
(1004, '电竞头戴耳机', 459.00, '2026-05-05 16:48:00'),
(1005, '大屏智能电视', 3299.00, '2026-05-06 08:15:00'),
(1005, '电视壁挂支架', 129.00, '2026-05-06 08:20:00'),
(1006, '无线快充充电器', 129.00, '2026-05-03 13:22:00'),
(1006, '降噪蓝牙耳机', 399.00, '2026-05-03 13:25:00'),
(1007, '电竞显示器', 1899.00, '2026-05-07 10:10:00'),
(1007, '显示器增高支架', 79.00, '2026-05-07 10:15:00'),
(1008, '折叠平板支架', 39.00, '2026-05-04 15:30:00'),
(1008, '便携充电宝', 159.00, '2026-05-04 15:33:00'),
(1009, '台式游戏主机', 6999.00, '2026-05-08 09:05:00'),
(1009, '电竞防滑鼠标垫', 59.00, '2026-05-08 09:08:00'),
(1010, '手机钢化膜', 29.00, '2026-05-05 17:12:00'),
(1010, '桌面收纳支架', 45.00, '2026-05-05 17:16:00');
-- 验证数据
SELECT COUNT(*) FROM order_info; -- 应该返回20
SELECT * FROM order_info LIMIT 5;
3.7 配置systemd托管MySQL(推荐)
vim /etc/systemd/system/mysqld.service
[Unit]
Description=MySQL Server
After=network.target remote-fs.target nss-lookup.target
[Service]
Type=notify
User=mysql
Group=mysql
ExecStart=/usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf
LimitNOFILE=65535
LimitNPROC=65535
Restart=on-failure
RestartPreventExitStatus=1
TimeoutSec=0
[Install]
WantedBy=multi-user.target
# 创建my.cnf配置文件
vim /etc/my.cnf
[client]
port=3306
socket=/tmp/mysql.sock
[mysqld]
port=3306
basedir=/usr/local/mysql
datadir=/usr/local/mysql/data
tmpdir=/tmp
socket=/tmp/mysql.sock
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
default-storage-engine=INNODB
log_error=error.log
max_connections=200
# 重新加载并启动
systemctl daemon-reload
systemctl enable --now mysqld.service
systemctl status mysqld
3.8 配置环境变量(方便命令行使用)
vim ~/.bash_profile
# 在末尾添加
export PATH=$PATH:/usr/local/mysql/bin
source ~/.bash_profile
mysql -V # 验证


四、申请大模型API Key
4.1 腾讯云TokenHub平台
访问 腾讯云 产业智变·云启未来 - 腾讯 → 进入「TokenHub大模型平台」→ 完成实名认证 → API Key管理 → 新建API密钥
4.2 模型选择建议(非常重要!)
| 模型 | 是否推荐 | 原因 |
|---|---|---|
| DeepSeek V4 Pro | ✅ 强烈推荐 | 输出稳定,SQL生成准确率高 |
| Qwen3.5-Plus | ✅ 推荐 | 速度快,成本低 |
| 带"思考链"的模型 | ❌ 不推荐 | 会把思考过程一起返回,导致SQL提取失败 |
为什么不能用带思考链的模型?
# 带思考链的模型可能返回:
好的,我来分析这个需求。用户想要查询2026年5月的消费数据...
首先我需要确认表结构...
然后构造SQL...
sql
SELECT ...
这样返回的内容里有大量非SQL文本,我们的正则提取规则会失效。

五、项目代码详解(逐文件讲解)
创建项目目录:
mkdir -p /opt/mysql_ai_tools
cd /opt/mysql_ai_tools
5.1 .env 配置文件(权限600!)
vim /opt/mysql_ai_tools/.env
# ============================ MySQL数据库连接配置 ============================
MYSQL_HOST=127.0.0.1
MYSQL_PORT=3306
MYSQL_USER=root
MYSQL_PASSWORD=123456
MYSQL_DB=testdb
# ============================ 腾讯云TokenHub大模型配置 ======================
LLM_API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
LLM_BASE_URL=https://tokenhub.tencentmaas.com/v1
LLM_MODEL_NAME=deepseek-v4-pro
LLM_TEMPERATURE=0
权限加固(必须做!):
chmod 600 /opt/mysql_ai_tools/.env
为什么要设600?
-
600 = rw-------,只有所有者能读写,其他用户无权访问
-
防止其他用户通过
cat .env看到数据库密码和API Key
5.2 mysql_client.py(数据库封装+安全拦截)
这是整个项目最关键的防线,负责拦截危险SQL。
# -*- coding: utf-8 -*-
# 文件名: mysql_client.py
# 功能: MySQL 8.0 数据库统一封装类
# 核心能力: 连接管理、查询执行、EXPLAIN获取、SQL安全拦截
import pymysql
import os
import re
from dotenv import load_dotenv
# 加载环境变量
load_dotenv()
class Mysql80Client:
"""MySQL 8.0 数据库操作封装类"""
def __init__(self):
"""初始化时读取配置并建立连接"""
self.host = os.getenv("MYSQL_HOST", "127.0.0.1")
self.port = int(os.getenv("MYSQL_PORT", "3306"))
self.user = os.getenv("MYSQL_USER", "root")
self.password = os.getenv("MYSQL_PASSWORD", "")
self.database = os.getenv("MYSQL_DB", "testdb")
self.conn = None
self.connect()
def connect(self):
"""建立数据库连接"""
try:
self.conn = pymysql.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
charset='utf8mb4', # 支持中文和emoji
cursorclass=pymysql.cursors.DictCursor # 返回字典格式,方便按字段名取值
)
except pymysql.MySQLError as e:
raise Exception(f"数据库连接失败,请检查地址/账号/密码: {e.args[1]}")
except Exception as e:
raise Exception(f"数据库连接异常: {str(e)}")
@staticmethod
def _check_sql_safety(sql: str) -> None:
"""
静态方法:SQL安全校验
核心防护:拦截所有非SELECT操作,防止AI生成危险SQL
"""
sql_trim = sql.strip().upper()
# 危险操作关键字黑名单
danger_keywords = [
"INSERT", "UPDATE", "DELETE", "DROP",
"ALTER", "CREATE", "TRUNCATE", "REPLACE"
]
for kw in danger_keywords:
# 使用单词边界匹配,避免字段名包含关键字时误拦截
if re.search(r'\b' + re.escape(kw) + r'\b', sql_trim):
raise Exception(f"安全拦截: 禁止执行{kw}类型语句,仅支持SELECT查询")
def execute_query(self, sql: str):
"""
执行普通SELECT查询
返回: (字段名列表, 数据行列表)
"""
# 先校验安全性
self._check_sql_safety(sql)
try:
with self.conn.cursor() as cursor:
cursor.execute(sql)
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
return columns, rows
except pymysql.MySQLError as e:
raise Exception(f"SQL执行失败(错误码{e.args[0]}): {e.args[1]}")
except Exception as e:
raise Exception(f"查询异常: {str(e)}")
def get_explain_plan(self, sql: str):
"""
获取SQL执行计划(EXPLAIN)
用于性能调优分析
"""
self._check_sql_safety(sql)
explain_sql = f"EXPLAIN {sql}"
try:
with self.conn.cursor() as cursor:
cursor.execute(explain_sql)
columns = [desc[0] for desc in cursor.description]
rows = cursor.fetchall()
return columns, rows
except pymysql.MySQLError as e:
raise Exception(f"获取执行计划失败: {e.args[1]}")
except Exception as e:
raise Exception(f"执行计划异常: {str(e)}")
def close(self):
"""关闭数据库连接"""
if self.conn and not self.conn._closed:
self.conn.close()
这段代码的关键点:
-
安全拦截是第一位的 :
_check_sql_safety方法在每次执行前都会调用,哪怕是大模型生成的SQL也不例外 -
DictCursor的好处 :返回
{字段名: 值}的字典,前端展示时不用关心字段顺序 -
异常分类处理:区分MySQL专属错误和通用异常,方便排查问题
5.3 prompts.py(提示词工程)
提示词工程是这个项目AI能力的核心。好的提示词能让模型输出稳定、可解析的结果。
# -*- coding: utf-8 -*-
# 文件名: prompts.py
# 功能: 统一管理大模型提示词模板
import re
class UnifiedPrompt:
"""提示词统一管理类"""
# ====================== 全局共用表结构 ======================
TABLE_SCHEMA = """
表名: order_info (订单信息表)
字段说明:
- id: 订单ID (BIGINT, 主键)
- user_id: 用户ID (INT)
- order_name: 商品名称 (VARCHAR)
- pay_amount: 支付金额 (DECIMAL)
- create_time: 下单时间 (DATETIME)
"""
# ====================== 模板1: 自然语言转SQL ======================
NL_TO_SQL_PROMPT = f"""
你是一名严谨的MySQL 8.0数据库开发工程师。
[任务目标]
根据用户自然语言描述的业务需求,生成可直接执行、无语法错误的MySQL查询SQL。
[表结构参考]
{TABLE_SCHEMA}
[强制输出规则]
1. 只能生成SELECT查询语句,绝对不允许生成INSERT/UPDATE/DELETE/DROP等修改或删除数据的语句。
2. 只能使用上面列出的5个字段,禁止编造不存在的字段名。
3. 查询字段可使用中文别名,格式固定为: 字段 AS 别名(别名内部不能有空格)。
4. SQL语法遵循MySQL 8.0标准,所有关键字统一大写。
5. 最终SQL必须包裹在 ```sql ``` Markdown代码块内。
6. 禁止输出任何解释、说明文字,只返回纯SQL代码块。
7. 日期条件使用标准格式:'YYYY-MM-DD HH:MM:SS'。
[用户需求]
{{user_input}}
"""
# ====================== 模板2: SQL性能调优 ======================
SQL_TUNE_PROMPT = f"""
你是资深MySQL DBA性能优化专家。
[任务目标]
根据原始SQL + EXPLAIN执行计划数据,定位查询性能问题并给出可直接落地的优化方案。
[表结构参考]
{TABLE_SCHEMA}
[待分析SQL]
{{sql_input}}
[执行计划数据]
{{explain_data}}
[输出要求]
1. 先点明核心性能问题(全表扫描、无索引、索引失效、扫描行数过多等)。
2. 给出完整的CREATE INDEX建索引SQL语句,可直接复制执行。
3. 若原SQL写法存在缺陷,提供改写后的完整优化SQL。
4. 内容简洁、分点罗列,便于用户快速阅读。
"""
@staticmethod
def extract_sql(response_text: str) -> str:
"""
从大模型返回的文本中提取纯净SQL语句
三层匹配优先级,兼容不同模型的输出格式
"""
if not response_text:
return ""
# 优先级1: 匹配标准 ```sql ``` 代码块(首选格式)
match = re.search(r'```sql\s*(.*?)\s*```', response_text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
# 优先级2: 兼容 <sql> 标签格式(备用方案)
match = re.search(r'<sql>\s*(.*?)\s*</sql>', response_text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
# 优先级3: 兜底匹配,直接抓取以SELECT开头、分号结尾的SQL片段
match = re.search(r'(SELECT\s+.*?;)', response_text, re.DOTALL | re.IGNORECASE)
if match:
return match.group(1).strip()
return ""
# 全局单例,其他地方直接 import prompt_helper 使用
prompt_helper = UnifiedPrompt()
提示词设计的精髓:
| 技巧 | 作用 |
|---|---|
| 强制代码块格式 | 让SQL提取变得可靠 |
| 禁止输出解释文字 | 减少解析干扰 |
| 明确列出字段 | 防止模型"脑补"不存在的字段 |
| 关键字大写 | 统一格式,方便正则匹配 |
| 三层提取策略 | 兼容不同模型的输出习惯 |
5.4 main.py(核心业务逻辑)
这是项目的大脑,串联起数据库、大模型和提示词三个模块。
# -*- coding: utf-8 -*-
# 文件名: main.py
# 功能: 项目核心业务逻辑 + 终端交互入口
import os
import re
import logging
from dotenv import load_dotenv
from langchain_openai import ChatOpenAI
from mysql_client import Mysql80Client
from tabulate import tabulate
from prompts import UnifiedPrompt, prompt_helper
load_dotenv()
# 全局日志配置
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)
def check_config() -> None:
"""启动时校验所有必需的环境变量"""
required_vars = [
"MYSQL_HOST", "MYSQL_PORT", "MYSQL_USER",
"MYSQL_PASSWORD", "MYSQL_DB",
"LLM_API_KEY", "LLM_BASE_URL", "LLM_MODEL_NAME"
]
missing = []
for var in required_vars:
if not os.getenv(var):
missing.append(var)
if missing:
raise ValueError(f"缺少必需的环境变量: {', '.join(missing)}")
def get_llm():
"""初始化大模型客户端"""
return ChatOpenAI(
api_key=os.getenv("LLM_API_KEY"),
base_url=os.getenv("LLM_BASE_URL"),
model=os.getenv("LLM_MODEL_NAME"),
temperature=int(os.getenv("LLM_TEMPERATURE", 0))
)
def clean_sql_spacing(sql: str) -> str:
"""
SQL清洗函数:修复大模型输出中常见的格式问题
"""
if not sql:
return ""
# 1. 去除Markdown代码块标记
sql = re.sub(r'```sql\s*', '', sql, flags=re.IGNORECASE)
sql = re.sub(r'```\s*', '', sql)
# 2. 去除前后空白
sql = sql.strip()
# 3. 修复AS别名内部的空格(如"订单 ID" → "订单ID")
def _clean_alias_space(match):
prefix = match.group(1) # AS关键字
alias = match.group(2) # 别名内容
alias_clean = re.sub(r'\s+', '', alias) # 删除别名内部所有空格
return f"{prefix}{alias_clean}"
sql = re.sub(
r'\b(AS)\s+(.+?)(?=\s*,|\s+FROM\b|\s+WHERE\b|\s+ORDER\b|\s+GROUP\b|\s+LIMIT\b|\s*;)',
_clean_alias_space,
sql,
flags=re.IGNORECASE
)
# 4. 关键字大写标准化
keywords = [
"SELECT", "FROM", "WHERE", "ORDER BY", "GROUP BY", "HAVING",
"AND", "OR", "LIMIT", "DESC", "ASC", "AS", "INNER JOIN",
"LEFT JOIN", "RIGHT JOIN", "ON", "INSERT INTO", "UPDATE",
"SET", "DELETE FROM", "VALUES", "LIKE", "IN", "BETWEEN",
"IS NULL", "COUNT", "SUM", "AVG", "MAX", "MIN", "OVER"
]
for kw in keywords:
# 字母+关键字粘连拆分
pattern = r'([a-z_])\s*(' + re.escape(kw.lower()) + r')'
sql = re.sub(pattern, r'\1 \2', sql, flags=re.IGNORECASE)
# 中文+关键字粘连拆分
pattern_cn = r'([\u4e00-\u9fa5])\s*(' + re.escape(kw.lower()) + r')'
sql = re.sub(pattern_cn, r'\1 \2', sql, flags=re.IGNORECASE)
# 统一大写
sql = re.sub(r'\b' + re.escape(kw.lower()) + r'\b', kw, sql, flags=re.IGNORECASE)
# 5. 清理多余空格
sql = re.sub(r'\s+', ' ', sql).strip()
return sql
def nl2sql_query(user_input: str) -> dict:
"""
核心业务1: 自然语言转SQL查询
返回标准字典,供终端和Web共用
"""
llm = get_llm()
db = Mysql80Client()
try:
logger.info("正在生成SQL语句...")
# 1. 调用大模型生成SQL
prompt = UnifiedPrompt.NL_TO_SQL_PROMPT.format(user_input=user_input)
response = llm.invoke(prompt)
raw_content = response.content.strip()
# 2. 提取纯净SQL
extracted_sql = prompt_helper.extract_sql(raw_content)
if not extracted_sql:
raise Exception("大模型未返回有效SQL,请重新描述需求")
# 3. 清洗SQL格式
clean_sql = clean_sql_spacing(extracted_sql)
logger.info(f"生成SQL: {clean_sql}")
# 4. 执行查询
columns, rows = db.execute_query(clean_sql)
# 5. 生成业务总结(如果有数据)
summary = ""
if rows:
logger.info("正在生成数据总结...")
summary_prompt = f"""
你是电商数据分析师。以下是SQL查询结果,请给出简练的业务总结。
SQL语句: {clean_sql}
查询数据: {str(rows)}
重点说明数据反映的业务含义,如有异常值请指出。
"""
summary_resp = llm.invoke(summary_prompt)
summary = summary_resp.content.strip()
return {
"success": True,
"sql": clean_sql,
"columns": columns,
"rows": rows,
"summary": summary,
"raw_llm": raw_content
}
except Exception as e:
logger.error(f"查询处理失败: {str(e)}")
return {
"success": False,
"error": str(e),
"raw_llm": raw_content if 'raw_content' in locals() else ""
}
finally:
db.close()
def sql_tune_analyze(raw_sql: str) -> dict:
"""
核心业务2: SQL性能调优分析
"""
llm = get_llm()
db = Mysql80Client()
try:
# 1. 清洗SQL
clean_sql = clean_sql_spacing(raw_sql)
logger.info("正在获取执行计划...")
# 2. 获取EXPLAIN执行计划
columns, plan_rows = db.get_explain_plan(clean_sql)
# 3. 调用大模型分析
logger.info("正在分析性能瓶颈...")
prompt = UnifiedPrompt.SQL_TUNE_PROMPT.format(
sql_input=clean_sql,
explain_data=str(plan_rows)
)
response = llm.invoke(prompt)
return {
"success": True,
"sql": clean_sql,
"plan_columns": columns,
"plan_rows": plan_rows,
"suggestion": response.content.strip()
}
except Exception as e:
logger.error(f"调优分析失败: {str(e)}")
return {
"success": False,
"error": str(e)
}
finally:
db.close()
def main_cli():
"""终端交互入口"""
try:
check_config()
except ValueError as e:
print(f"❌ {e}")
return
while True:
print("\n" + "=" * 60)
print(" 🚀 InnoAI SQL 助手")
print("=" * 60)
print("1. 自然语言生成SQL,自动查询并AI总结数据")
print("2. 输入SQL语句,AI分析执行计划并给出调优方案")
print("0. 退出程序")
print("-" * 60)
choice = input("请输入功能序号: ").strip()
if choice == '1':
query = input("\n请输入你的数据查询需求: ").strip()
if not query:
print("⚠️ 请输入有效需求")
continue
result = nl2sql_query(query)
if not result["success"]:
print(f"\n❌ 处理失败: {result['error']}")
if result.get("raw_llm"):
print(f"大模型原始回复:\n{result['raw_llm']}")
continue
print(f"\n✅ 生成SQL:")
print(result["sql"])
if result["rows"]:
print(f"\n📊 查询结果 (共{len(result['rows'])}条):")
print(tabulate(result["rows"], headers="keys", tablefmt="pretty"))
if result["summary"]:
print(f"\n💡 业务总结:")
print(result["summary"])
else:
print("\n📭 未查询到匹配数据")
elif choice == '2':
sql_input = input("\n请输入需要分析的SQL语句: ").strip()
if not sql_input:
print("⚠️ 请输入有效SQL")
continue
result = sql_tune_analyze(sql_input)
if not result["success"]:
print(f"\n❌ 分析失败: {result['error']}")
continue
print(f"\n📋 执行计划详情:")
print(tabulate(result["plan_rows"], headers="keys", tablefmt="pretty"))
print(f"\n🔧 调优建议:")
print(result["suggestion"])
elif choice == '0':
print("\n👋 程序已安全退出。")
break
else:
print("⚠️ 无效输入,请重试。")
if __name__ == "__main__":
main_cli()
5.5 web_main.py(Streamlit Web界面)
Web界面完全复用main.py的业务逻辑,不重复造轮子。
# -*- coding: utf-8 -*-
# 文件名: web_main.py
# 功能: Streamlit网页可视化界面
import streamlit as st
import os
import re
from main import check_config, nl2sql_query, sql_tune_analyze
# 页面配置
st.set_page_config(
page_title="InnoAI SQL 助手",
layout="wide",
initial_sidebar_state="expanded"
)
# ====================== 全局CSS样式 ======================
st.markdown("""
<style>
/* 全局字体 */
html, body, [class*="css"] {
font-size: 15px;
line-height: 1.6;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
}
/* 主容器 */
.block-container {
padding-top: 2.5rem;
padding-bottom: 3rem;
max-width: 1100px;
margin: 0 auto;
}
/* 标题 */
h1 { font-size: 2rem !important; font-weight: 600; margin-bottom: 1.8rem; }
h3 { font-weight: 600; margin-top: 1.5rem; margin-bottom: 1rem; }
/* 输入框 */
.stTextArea label p { font-size: 15px; font-weight: 500; color: #1f2937; margin-bottom: 0.5rem; }
textarea { font-size: 15px !important; line-height: 1.6 !important; border-radius: 8px !important; padding: 12px 14px !important; }
/* 按钮 */
.stButton > button { font-size: 15px !important; font-weight: 500; padding: 0.65rem 2.2rem !important; border-radius: 8px !important; min-width: 160px; }
/* 侧边栏 */
section[data-testid="stSidebar"] { font-size: 14.5px; }
section[data-testid="stSidebar"] h1 { font-size: 1.5rem !important; white-space: nowrap; margin-bottom: 1.2rem; }
section[data-testid="stSidebar"] .stRadio > div { gap: 0.8rem; }
/* 代码块复制按钮修复 */
.stCodeBlock { position: relative !important; border-radius: 8px !important; }
.stCodeBlock button { opacity: 1 !important; visibility: visible !important; pointer-events: auto !important; z-index: 999 !important; width: 36px; height: 36px; top: 10px; right: 10px; border-radius: 6px; background: #f3f4f6 !important; color: #374151 !important; border: 1px solid #e5e7eb !important; }
.stCodeBlock button:hover { background: #e5e7eb !important; }
.stCodeBlock code, .stCodeBlock pre { font-size: 14.5px !important; line-height: 1.7 !important; font-family: "JetBrains Mono", Consolas, Monaco, monospace; }
/* 数据表格 */
.stDataFrame { font-size: 14.5px; border-radius: 8px; overflow: hidden; }
.stDataFrame [data-testid="table"] td { padding: 10px 12px; }
</style>
""", unsafe_allow_html=True)
# ====================== 配置校验 ======================
if "config_checked" not in st.session_state:
try:
check_config()
st.session_state.config_checked = True
except ValueError as e:
st.error(f"配置错误: {e}")
st.stop()
# ====================== 侧边栏 ======================
with st.sidebar:
st.title("🚀 InnoAI SQL 助手")
st.info(f"当前模型: `{os.getenv('LLM_MODEL_NAME', '未知')}`")
page = st.radio("功能导航", ["🔍 数据查询与总结", "⚙️ SQL 性能调优"])
# ====================== 页面1: 数据查询 ======================
if page == "🔍 数据查询与总结":
st.header("💬 自然语言转 SQL 查询")
user_input = st.text_area(
"请输入你的业务查询需求:",
height=150,
placeholder="例如:查询用户1001最近30天的所有订单,按金额从高到低排序"
)
if st.button("🚀 生成并执行", type="primary"):
input_trim = user_input.strip()
if not input_trim:
st.warning("请输入有效的业务查询需求后再提交")
elif re.match(r'(?i)^\s*SELECT\s+', input_trim):
st.warning("此处请输入自然语言描述的需求,请勿直接粘贴SQL语句。如需分析SQL,请切换到「SQL性能调优」页面。")
else:
with st.spinner("AI 正在生成 SQL 并查询数据..."):
result = nl2sql_query(input_trim)
if not result["success"]:
st.error(f"处理失败: {result['error']}")
if result.get("raw_llm"):
with st.expander("查看大模型原始回复"):
st.code(result["raw_llm"], language="text")
else:
st.success("✅ SQL 生成并执行成功")
st.markdown("### 📝 生成的 SQL")
st.code(result["sql"], language="sql")
if result["rows"]:
st.markdown("### 📊 查询结果")
st.dataframe(result["rows"], use_container_width=True)
else:
st.info("未查询到匹配的数据")
if result["summary"]:
st.markdown("### 💡 业务总结")
st.info(result["summary"])
# ====================== 页面2: SQL调优 ======================
else:
st.header("🔧 SQL 性能调优分析")
raw_sql = st.text_area(
"请输入待分析的 SQL 语句:",
height=200,
placeholder="例如:SELECT * FROM order_info WHERE user_id = 1001"
)
if st.button("📊 开始分析", type="primary"):
input_trim = raw_sql.strip()
if not input_trim:
st.warning("请输入有效的 SQL 语句后再提交")
elif not re.match(r'(?i)^\s*SELECT\s+', input_trim):
st.warning("此处请输入 SELECT 类型的 SQL 语句进行分析。")
else:
with st.spinner("正在获取执行计划并分析..."):
result = sql_tune_analyze(input_trim)
if not result["success"]:
st.error(f"分析失败: {result['error']}")
else:
st.success("✅ 执行计划获取成功")
if result["plan_rows"]:
st.markdown("### 📋 执行计划")
st.dataframe(result["plan_rows"], use_container_width=True)
st.markdown("### 💡 调优建议")
st.markdown(result["suggestion"])
st.markdown("---")
st.caption("InnoAI SQL 助手 · 基于 LangChain + Streamlit · Python 3.11")

六、功能测试(真实用例)
6.1 终端模式测试
python3 /opt/mysql_ai_tools/main.py
测试用例1:多用户消费汇总
输入需求: 1001、1002、1003每个用户的订单总消费金额与订单笔数,按总消费从高到低排序
实际输出:
SELECT user_id AS 用户ID, SUM(pay_amount) AS 总消费金额, COUNT(id) AS 订单笔数
FROM order_info
WHERE user_id IN (1001, 1002, 1003)
GROUP BY user_id
ORDER BY 总消费金额 DESC
AI业务总结:
用户价值分层显著:用户1002为核心高价值用户(总消费5588元),其客单价(约2794元)约为用户1003(约924元)的3倍,消费能力差异巨大,建议对1002实施VIP维系策略。
购买行为同步性高:样本内3位用户订单笔数均为2单,显示该群体具有相似的复购节奏。
异常提示:不同用户订单笔数完全一致较为罕见,需确认是否为特定筛选条件所致,避免样本偏差误导分析。
测试结果:
| 用户ID | 总消费金额 | 订单笔数 |
|---|---|---|
| 1002 | 5588.00 | 2 |
| 1001 | 3198.00 | 2 |
| 1003 | 1848.00 | 2 |
6.2 SQL调优测试
输入SQL:
SELECT user_id AS 用户ID, SUM(pay_amount) AS 总消费金额, COUNT(id) AS 订单笔数
FROM order_info
WHERE user_id IN (1001, 1002, 1003)
GROUP BY user_id
ORDER BY 总消费金额 DESC
执行计划:
| id | select_type | table | type | key | rows | Extra |
|---|---|---|---|---|---|---|
| 1 | SIMPLE | order_info | ALL | NULL | 20 | Using where; Using temporary; Using filesort |
AI调优建议:
核心性能问题
全表扫描(type: ALL) :
user_id字段无索引,导致查询扫描全表临时表与文件排序 :Extra显示
Using temporary; Using filesort,分组和排序未在索引上完成无索引命中:key为None,查询未利用任何索引加速
建索引SQL
ALTER TABLE order_info ADD INDEX idx_user_id_pay_amount(user_id, pay_amount);
优化后SQL(建议将COUNT(id)改为COUNT(*)以符合最佳实践)
SELECT user_id AS 用户ID, SUM(pay_amount) AS 总消费金额, COUNT(*) AS 订单笔数
FROM order_info
WHERE user_id IN (1001, 1002, 1003)
GROUP BY user_id
ORDER BY 总消费金额 DESC;
七、Web服务部署(生产级)
7.1 创建systemd服务
vim /etc/systemd/system/mysql-ai-web.service
[Unit]
Description=InnoAI SQL Web Tool
After=network.target mysqld.service
[Service]
Type=simple
User=root
WorkingDirectory=/opt/mysql_ai_tools
ExecStart=/usr/local/python3.11/bin/python3 -m streamlit run web_main.py \
--server.address 0.0.0.0 \
--server.port 8501 \
--server.headless true
Restart=always
RestartSec=3
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
systemctl daemon-reload
systemctl enable --now mysql-ai-web
systemctl status mysql-ai-web
# 查看日志
journalctl -u mysql-ai-web -f
7.2 访问测试
hostname -I
# 假设输出 192.168.88.130
浏览器访问:http://192.168.88.130:8501
八、安全加固清单(生产必做)
| 项目 | 操作 | 原因 |
|---|---|---|
| .env文件权限 | chmod 600 .env |
防止其他用户读取密码 |
| MySQL用户权限 | 创建专用只读账号 | 即使SQL拦截失效也有最后一道防线 |
| 防火墙 | 只放行8501端口 | 减少攻击面 |
| Streamlit访问控制 | 配合nginx反向代理+Basic Auth | Streamlit原生无登录认证 |
| API Key轮换 | 定期更换TokenHub密钥 | 防止Key泄露造成资损 |
创建MySQL只读账号(推荐):
CREATE USER 'readonly'@'localhost' IDENTIFIED WITH mysql_native_password BY 'ReadOnly@123';
GRANT SELECT ON testdb.* TO 'readonly'@'localhost';
FLUSH PRIVILEGES;
然后修改.env中的MYSQL_USER和MYSQL_PASSWORD为只读账号。
九、常见问题排查
Q1: pip安装时报SSL错误
pip._vendor.urllib3.exceptions.SSLError
原因 :Python编译时--enable-shared没加,或者ldconfig没执行
解决:重新编译Python并确保动态库配置正确
Q2: 大模型返回的不是纯SQL
好的,我来帮你生成SQL...```sql SELECT ... ```
原因:用了带思考链的模型
解决:换成DeepSeek V4 Pro或Qwen3.5-Plus
Q3: Streamlit代码块复制按钮不显示
原因:Streamlit版本问题或CSS被覆盖
解决:使用文中提供的CSS修复代码,强制显示复制按钮
Q4: MySQL连接报Authentication plugin错误
Authentication plugin 'caching_sha2_password' cannot be loaded
原因:pymysql不支持MySQL 8.0默认的认证插件
解决 :将root用户改为mysql_native_password(文中已包含此步骤)
十、项目扩展思路
| 扩展方向 | 实现方式 |
|---|---|
| 多数据源支持 | 在.env中配置多个数据库,通过参数切换 |
| 查询历史记录 | 增加SQLite记录每次查询的SQL和结果 |
| 导出Excel | 使用pandas将查询结果导出为Excel文件 |
| 定时报表 | 结合cron+邮件,每天自动发送报表 |
| 多模型切换 | Web界面增加下拉框,运行时切换不同模型 |
| 企业微信/钉钉机器人 | 通过webhook接收自然语言,返回查询结果 |
| 查询限流 | 增加Redis记录用户查询次数,防止滥用 |
总结
这个项目看似简单,实则涵盖了Linux运维、MySQL调优、Python开发、大模型应用、Web部署五个领域的核心技能。做完这个项目,你对AI+数据库落地的理解会上升一个台阶。
最关键的三点经验:
-
安全永远是第一位的:SQL拦截 + 只读账号 + 文件权限,三层防护缺一不可
-
提示词决定上限:好的提示词能让70分的模型输出90分的结果
-
工程化思维:代码分层、异常处理、日志记录,这些才是生产级系统的标配
**如果这篇教程对你有帮助,欢迎点赞+收藏+关注!** 有任何问题可以在评论区留言,我看到会回复。
标签:#MySQL #Python #人工智能 #Streamlit #运维自动化 #NL2SQL #SQL优化 #毕业设计 #数据库实训 #大模型应用
版权声明:本文基于InnoAI SQL助手官方文档编写,代码已获得授权分享,仅供学习交流使用。