本地 AI 数据分析助手:Qwen3.8 + DuckDB 实战

AI教程21小时前更新 程序员阿超
804 0 0

一、背景:数据分析不想把数据上传云端怎么办

做数据分析最尴尬的是:数据在本地 CSV 和 Parquet 文件里,却要复制粘贴到云端 AI 对话框里问帮我算一下各地区销售额。数据量一大就超限,敏感数据更不敢上传。

理想形态是:AI 在本地读数据库、用 SQL 查数、把结果解释给你听,全程数据不出内网。本文用 DuckDB(本地 OLAP 神器)加 Qwen3 系列本地模型(通过 LM Studio 运行),搭一个免费、可离线的 Agentic SQL 数据助手:你用中文提问,它自动写 SQL、执行、解读结果。

二、原理:Agentic SQL 是怎么转起来的

流程只有四步:

  1. Schema 感知:把 DuckDB 里有哪些表、字段名、字段类型、几行样例数据拼进 prompt,模型才知道能查什么。
  2. Text-to-SQL:LLM 根据问题生成一条 SQL,只允许 SELECT,不允许 DELETE 和 DROP。
  3. 执行与自修:用 DuckDB 执行,报错就把错误信息回抛给模型,让它修正重写,最多重试 2 到 3 次。
  4. 结果解读:把查询结果表格再交给模型,用中文总结出结论和建议。

为什么选 DuckDB:单文件、无服务、启动毫秒级,直接查询 CSV 和 Parquet,比 SQLite 更适合分析型聚合。为什么选 Qwen3:中文和 SQL 生成能力强,有从 4B 到 32B 多档尺寸,可按显存和内存挑选。为什么用 LM Studio:图形化管理本地模型,自带 OpenAI 兼容 API,Python 一行即可调用。

三、环境准备

需要一台 8GB 以上内存的电脑,Mac、Windows、Linux 均可,有无显卡都能跑(无显卡选小模型)。

pip install duckdb pandas openai tabulate
  1. 去 LM Studio 官网下载安装包并安装。
  2. 在 LM Studio 的 Discover 页搜索 Qwen3,按内存选模型:8GB 内存选 Qwen3 4B 量化版;16GB 内存选 Qwen3 8B 量化版;32GB 以上选 Qwen3 14B 或 32B,SQL 准确率明显更高。
  3. Load 模型后,在 Local Server 页点 Start Server,默认地址为 http://localhost:1234/v1,记下模型名。
  4. 准备一份示例数据 sales.csv(日期、地区、产品、金额、数量五列即可,无数据可先用下面代码生成)。

    import pandas as pd, numpy as np
    np.random.seed(0)
    dates = pd.date_range(“2025-01-01”, periods=90)
    df = pd.DataFrame({
    “order_date”: np.random.choice(dates, 2000),
    “region”: np.random.choice([“华东”, “华南”, “华北”, “西南”], 2000),
    “product”: np.random.choice([“键盘”, “鼠标”, “显示器”, “升降桌”], 2000),
    “amount”: np.random.randint(100, 5000, 2000),
    “qty”: np.random.randint(1, 10, 2000),
    })
    df.to_csv(“sales.csv”, index=False)
    print(df.head())

四、分步实战:从建库到中文问答

第 1 步:把 CSV 装进 DuckDB 并查看结构

import duckdb
con = duckdb.connect("sales.duckdb")
con.execute("CREATE OR REPLACE TABLE sales AS FROM 'sales.csv'")
print(con.execute("DESCRIBE sales").fetchall())
print(con.execute("SELECT * FROM sales LIMIT 5").fetchall())

DuckDB 的 FROM 读取 CSV 写法会自动推断表结构,非常适合临时导入数据。

第 2 步:封装 Schema 描述函数

def schema_text(con):
    rows = con.execute("DESCRIBE sales").fetchall()
    cols = ", ".join(f"{r[0]}({r[1]})" for r in rows)
    sample = con.execute("SELECT * FROM sales LIMIT 3").fetchall()
    return f"表sales字段:{cols}。样例行:{sample}。日期字段order_date为DATE类型。"

print(schema_text(con))

这段文字每次都要拼进 prompt,是 Text-to-SQL 准确率的关键。

第 3 步:连接本地模型并生成 SQL

from openai import OpenAI
client = OpenAI(base_url="http://localhost:1234/v1", api_key="lm-studio")
MODEL = "qwen3-8b"  # 改成你在LM Studio里加载的模型名

SYSTEM = "你是数据分析师,只写DuckDB SQL。规则:只允许SELECT;表名只能用sales;字段必须来自schema;日期用DATE比较;只输出SQL,不要解释,不要markdown。"

def text_to_sql(question, schema):
    r = client.chat.completions.create(model=MODEL, messages=[
        {"role": "system", "content": SYSTEM},
        {"role": "user", "content": f"Schema:{schema} 问题:{question}"},
    ], temperature=0.1)
    sql = r.choices[0].message.content.strip().strip("`")
    return sql

temperature 设低(0 到 0.2),SQL 更稳定。期望输出类似:SELECT region, SUM(amount) AS total FROM sales WHERE order_date BETWEEN DATE ‘2025-01-01’ AND DATE ‘2025-01-31’ GROUP BY region ORDER BY total DESC。

第 4 步:执行加报错自修(核心)

def ask(question, retries=2):
    schema = schema_text(con)
    sql = text_to_sql(question, schema)
    for i in range(retries + 1):
        try:
            df = con.execute(sql).fetch_df()
            return sql, df
        except Exception as e:
            if i == retries:
                raise
            fix = client.chat.completions.create(model=MODEL, messages=[
                {"role": "system", "content": SYSTEM},
                {"role": "user", "content": f"Schema:{schema} 问题:{question} 上次SQL:{sql} 报错:{e} 请修正,只输出SQL。"},
            ], temperature=0.1)
            sql = fix.choices[0].message.content.strip().strip("`")
    return sql, None

sql, df = ask("华东地区卖得最好的产品是什么?")
print(sql)
print(df)

实测大部分 SQL 报错(如字段名大小写、日期函数)一次重试就能修好。

第 5 步:结果转中文解读

def explain(question, sql, df):
    table = df.head(20).to_markdown(index=False)
    r = client.chat.completions.create(model=MODEL, messages=[
        {"role": "system", "content": "你是业务分析师,用中文解读数据,给出结论和一条建议,200字以内。"},
        {"role": "user", "content": f"问题:{question} SQL:{sql} 结果:{table}"},
    ])
    return r.choices[0].message.content

print(explain("华东地区卖得最好的产品是什么?", sql, df))

第 6 步:拼成命令行问答循环

while True:
    q = input("请输入问题(q退出):").strip()
    if q.lower() == "q":
        break
    try:
        sql, df = ask(q)
        print("SQL:", sql)
        print(df.head(10).to_string())
        print(explain(q, sql, df))
    except Exception as e:
        print("查询失败:", e)

至此,一个完全本地、数据不出内网的中文数据助手就跑起来了。

五、常见坑

  1. 模型选太大跑不动:LM Studio 加载时看内存占用,超了会疯狂 swap。8GB 机器老老实实 4B 起步。
  2. 字段名对不上:CSV 有中文列名或空格时,SQL 容易写错。建议建表后统一改成英文小写列名。
  3. 日期比较翻车:模型爱写 LIKE 匹配日期,DuckDB 更稳的是 BETWEEN DATE AND DATE,在 system prompt 里写死。
  4. 一次查全表:限制 LIMIT 100 或要求聚合后再解读,避免把几万行全塞进 prompt 撑爆上下文。
  5. 权限控制缺失:在执行层再加一道正则拦截 drop、delete、update、insert、alter,防止模型手滑写坏库。
  6. 中文值有空格:地区名华东前后多空格会导致 GROUP BY 出两组,建表时先 TRIM 清洗一遍。
  7. LM Studio 连不上:检查 Local Server 是否 Start,端口是否为 1234,模型名是否与加载的一致。

六、总结

DuckDB 解决本地极速查数,Qwen3 解决中文问题转 SQL,LM Studio 解决本地模型易用性,三者拼在一起就是零成本、可离线的 Agentic SQL。按内存选模型、做好 schema 描述和报错重试,准确率完全够日常分析用。下一步可以给它加图表输出(matplotlib)或接上 Parquet 数据湖,变成团队共用的本地 BI 助手。

七、深入:Text-to-SQL 的 Prompt 工程细节

Schema 序列化格式直接影响准确率。实测经验:CREATE TABLE 语句格式比自然语言描述更稳,因为模型训练时见过大量建表语句。建议这样拼 schema:把 DESCRIBE 结果转成 CREATE TABLE 文本,再加 3 行样例数据(INSERT 或 CSV 片段)。样例数据的作用是告诉模型字段值的真实形态(地区写华东还是 EAST-CN),能减少一半的 WHERE 条件写错。

Few-shot 示例:固定放 2 个你们业务的问答对(问题加正确 SQL),模型的方言错误(比如 DuckDB 日期写法)会大幅减少。示例要覆盖最常用的两种查询:分组聚合排名、带日期过滤的明细。不要放超过 4 个示例,否则 prompt 太长,schema 空间被挤占。

方言差异是 DuckDB 特有的坑:模型默认写 MySQL 方言(如 DATE_FORMAT),DuckDB 要用 strftime 或直接 DATE 比较。在 system prompt 里加一句只用 DuckDB 方言,日期比较用 BETWEEN DATE 起止,并给一个正确示例,基本根治。

八、安全与生产化:只读连接加三道闸

生产环境不能让 LLM 直连可写库。三道闸缺一不可。第一,DuckDB 用只读模式打开正式库,分析用副本:duckdb.connect(“sales.duckdb”, read_only=True),模型再手滑也写不坏。第二,正则拦截危险关键字,执行前检查 SQL 是否以 SELECT 或 WITH 开头,含 drop、delete、update、insert、alter、attach、copy 直接拒绝。第三,查询超时与行数限制:DuckDB 设置 PRAGMA query_timeout,做聚合解读前先 SELECT COUNT(*) 判断量级,超过 5 万行强制要求先聚合,禁止把全表塞进 prompt。

import re
FORBIDDEN = re.compile(r"\b(drop|delete|update|insert|alter|attach|copy)\b", re.I)

def safe_execute(con, sql, max_rows=2000):
    s = sql.strip().lower()
    assert s.startswith("select") or s.startswith("with"), "只允许查询"
    assert not FORBIDDEN.search(s), "含危险关键字,已拦截"
    con.execute("PRAGMA query_timeout=10000")
    df = con.execute(sql).fetch_df()
    return df.head(max_rows)

图表输出是下一个高价值功能:解读函数之后,让模型再生成一段 matplotlib 代码,画销售额趋势图存 PNG。注意画图代码同样要过安全检查(只允许 import matplotlib 和 pandas)。

模型选型对照:4B 模型简单单表聚合可用,涉及 JOIN 和嵌套子查询时准确率掉得厉害;8B 是甜点,单表加日期过滤基本稳;14B 以上复杂多表关联才有质变。建议从 8B 起步,评测集(20 条业务问题,看一次执行成功率)说了算,不够再升档。

九、端到端完整脚本与业务问题清单

把前面各步拼成一个文件就能跑。建议按这个顺序组织:建库、schema 函数、模型连接、生成 SQL、执行自修、解读、问答循环,全文件不到 150 行。第一次跑通后,拿下面这 10 个业务问题逐个试:1 月总销售额是多少;各地区销售额排名;华东卖得最好的产品;客单价最高的是哪天;键盘和鼠标哪个卖得多;周末和工作日销售额对比;销售额前 10 的订单明细;哪个产品退货率最高(需退货列);2 月第一周环比增长多少;各产品月均销量。这 10 题覆盖聚合、排名、过滤、对比、环比五种范式,全过则系统可用,有挂则看挂的是哪一类,针对性补 few-shot 示例。

错误案例最有营养,记三个典型的。案例一:问华东最好的产品,模型写成 WHERE region = ‘east’,因为 schema 样例没覆盖地区写法。修法:schema 样例里显式列出全部地区值。案例二:问 1 月数据,模型写 order_date LIKE ‘2025-01%’,字符串匹配在 DATE 列上慢且可能漏。修法:system prompt 规定日期比较必须用 BETWEEN DATE,这是 DuckDB 方言铁律。案例三:问环比,模型一次写出嵌套子查询,括号不配对报错。修法:重试机制兜住,第二次模型通常会拆成两次简单查询。把这三个案例记进你们的运维手册,新人接手少踩一半坑。

长期维护上,数据每月追加时用 ATTACH 加 INSERT 增量入库,不要重建全库;schema 变了(加列)要同步更新 prompt 模板和 few-shot;模型换版本后把 10 题重跑一遍,成功率掉了就回滚。本地 BI 助手的最大敌人不是模型,是没人维护的脏数据和过期的 prompt,每月花半小时体检一次,系统能稳跑一年。

十、常见 SQL 写法红黑榜

本地小模型写 SQL 有固定偏好,红黑榜记熟能省一半调试。红榜三条:SELECT 显式列名而不是星号,减少 schema 幻觉;日期用 BETWEEN DATE 起止;聚合必带 GROUP BY 且别名用英文。黑榜五条:LIKE 匹配日期;SELECT 星号加 LIMIT 充数;JOIN 不写 ON 条件;用 MySQL 的 DATE_FORMAT 和 LIMIT 逗号写法;中文别名(DuckDB 支持但下游解读容易乱码)。把红黑榜直接贴进 system prompt,模型的方言错误率掉一半。每次升级模型版本,拿 10 题重跑,红榜守住、黑榜没新增,才算升级成功。

十一、从只读问答到写入辅助的边界

本地数据助手迟早被问能不能顺手写点什么,比如自动生成周报、把结论写入表格。红线划清楚:只读查询随便玩,任何写入动作必须人确认。推荐模式是助手生成待执行的 SQL 和变更预览,人点确认后再跑,日志记下谁确认的。自动写库、自动发邮件、自动调接口三件事,默认全部关闭,开任何一个都要单独评审。数据安全上,CSV 里若有手机号身份证,解读时让模型脱敏(只展示后四位),prompt 加一条敏感字段展示规范,避免截屏外传翻车。边界守住了,助手才能从个人玩具变成团队设施。

多表关联是本地小模型的分水岭:两表 JOIN 带别名还能应付,三表以上加子查询就频繁翻车。务实做法是预建宽表(VIEW 把常用关联固化),模型只查单表,复杂度降维。VIEW 随 ETL 更新,prompt 里只暴露 VIEW,基表隐藏,既准又安全。记住让数据工程吃掉复杂度,而不是考验模型的 SQL 杂技,这是生产 Text-to-SQL 最重要的一课。

附:首周行动清单。周一装好 LM Studio 并跑通一条查询;周二把 schema 函数换成你们真实表;周三写 10 道业务题验收;周四加上只读连接和关键字拦截;周五给同事演示并收反馈。五天落地,第二周开始它就是你的日常分析入口。过程中所有报错和修正记进文档,攒够 20 条就是团队版的 Text-to-SQL playbook,新人照着跑一遍即上手。

一句话收束:DuckDB 管查数、Qwen 管翻译、LM Studio 管落地,三者各做各最擅长的事。别在 SQL 里考验模型杂技,别把脏数据丢给 AI,先建宽表、定口径、写拦截,再谈智能。五天落地,每月体检,这个本地助手会成为你分析工作的默认入口。数据不出内网、花钱几乎为零、换模型半小时验收,这就是本地路线的底气。 点击阅读原文

写在最后:先别贪多,今晚只干一件事——把一条真实业务问题跑通端到端,看到中文答案从本地模型嘴里出来。跑通一次,信心和手感全有了,剩下的优化都是顺水推舟。工具链已经齐了,缺的只是第一次运行。 点击阅读原文

参考资料:MotherDuck 官方博客 Agentic SQL with Qwen + DuckDB 一文的本地化思路。 点击阅读原文

© 版权声明

相关文章

暂无评论

暂无评论...