Featured image of post 用 DuckDB 搭建 AI 数据问答机器人:让业务人员自己查数据

用 DuckDB 搭建 AI 数据问答机器人:让业务人员自己查数据

用 DuckDB + LLM 搭建 Text-to-SQL 数据问答机器人,让不会 SQL 的业务人员通过自然语言获取数据洞察。附完整可运行代码、安全执行引擎和三种变现场景。

用 DuckDB 搭建 AI 数据问答机器人:让业务人员自己查数据

💰 变现路径:为企业搭建内部数据问答系统,单次交付 5000-20000 元


一、业务人员的痛点,就是你的商机

你有没有遇到过这种场景:去找一家电商公司的运营总监聊合作,他说"我们每天要看 5 个维度的销售数据,但每次都要找数据分析师排期"。

你回答:“我帮你搭一个系统,以后你们直接在聊天框里问’上周华东区哪三款商品退货率最高’,系统自动给你答案。”

这就是 AI 数据问答机器人的核心价值——让不会 SQL 的业务人员,能用自然语言获取数据洞察。

传统方案怎么做?前端写 React → 后端写 FastAPI → 对接 LLM API → 把自然语言转 SQL → 执行查询 → 返回结果。这套流程至少需要 2000 行代码。

用 DuckDB,整个核心逻辑不到 150 行。原因很简单:DuckDB 是进程内数据库,不需要单独部署,Python 里 import duckdb 就能用,天然适合嵌入 AI Agent 的推理链路中。


二、系统架构:四步完成自然语言到数据结果

整个系统的核心链路非常清晰:

用户提问(自然语言)→ LLM 生成 SQL → DuckDB 执行查询 → 格式化结果
       ↑                                              ↓
       └────────── 结果反馈给 LLM 生成自然语言回答 ←┘

四个关键模块:

  1. Schema 感知:让 LLM 了解数据库表结构
  2. SQL 生成器:将自然语言转为 DuckDB SQL
  3. 安全执行引擎:防止恶意查询,保护数据安全
  4. 自然语言回答器:将查询结果转化为业务可读的分析

三、第一步:准备数据

DuckDB 的强大之处在于它能直接读取各种格式的文件,无需导入过程。

import duckdb
import json

# 创建 DuckDB 内存数据库(零配置)
con = duckdb.connect(':memory:')

# 直接读取 CSV(自动推断 schema)
con.execute("""
CREATE TABLE orders AS
SELECT * FROM read_csv_auto('https://github.com/duckdb/duckdb-data/raw/main/data/orders.csv');
""")

# 查看表结构和数据量
print(con.execute("DESCRIBE orders").fetchall())
print(con.execute("SELECT COUNT(*) FROM orders").fetchone())

# 也支持 Parquet、JSON、Excel 等多种格式
# con.read_parquet('/path/to/data/*.parquet')
# con.execute("CREATE TABLE events AS SELECT * FROM read_json_auto('data.json')")

关键优势

  • read_csv_auto 自动推断列类型,无需手动指定
  • 支持 Glob 模式匹配,一次读取多个文件
  • 内存数据库,关闭连接即释放资源,适合多租户 SaaS 场景

四、第二步:构建 SQL 生成器

这一步是整个系统的核心——让 LLM 理解数据库结构并生成正确的 SQL。

from openai import OpenAI
import os

client = OpenAI(api_key=os.environ.get('OPENAI_API_KEY'))

def get_table_schema(connection):
    """获取数据库表结构的函数"""
    tables = connection.execute(
        "SELECT table_name FROM information_schema.tables WHERE table_schema = 'main'"
    ).fetchall()
    
    schema_info = {}
    for (table_name,) in tables:
        columns = connection.execute(
            f"DESCRIBE {table_name}"
        ).fetchall()
        schema_info[table_name] = [
            {"column": col[0], "type": col[1]} for col in columns
        ]
    return schema_info

def generate_sql(user_query, schema_info):
    schema_str = json.dumps(schema_info, indent=2, ensure_ascii=False)
    
    messages = [
        {
            "role": "system",
            "content": """你是一个 SQL 专家。用户会用自然语言提问,你需要将其转换为 DuckDB SQL 查询。
重要规则:
1. 只允许使用提供的表,不要创建新表
2. 查询结果控制在 100 行以内
3. 如果涉及数值计算,使用 ROUND 保留 2 位小数
4. 只输出 SQL,不要输出解释文字"""
        },
        {
            "role": "user",
            "content": f"""数据库表结构:
{schema_str}

用户问题:{user_query}

请生成 SQL 查询:"""
        }
    ]
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        temperature=0.1,  # 低温度保证稳定性
        max_tokens=500
    )
    
    sql = response.choices[0].message.content.strip()
    # 清理 markdown 代码块标记
    if sql.startswith("```"):
        sql = sql.split("\n", 1)[1].rsplit("```", 1)[0].strip()
    return sql

为什么用 gpt-4o-mini?

  • 成本低:每百万 token 约 $0.6,一个问答请求成本不到 1 分钱
  • 速度快:响应时间通常在 500ms 以内
  • 足够聪明:Text-to-SQL 任务不需要最强的模型

进阶技巧:如果数据表很多,可以用 RAG 只检索相关的表结构给 LLM,减少 prompt 长度和成本。


五、第三步:安全执行引擎

这是最容易忽略但最重要的环节。LLM 生成的 SQL 可能包含恶意内容,必须做严格的安全校验。

class SafeQueryEngine:
    """安全的 DuckDB 查询执行器"""
    
    def __init__(self, connection):
        self.con = connection
    
    def execute_query(self, sql, max_rows=100):
        # 安全检查:只允许 SELECT
        sql_upper = sql.strip().upper()
        if not sql_upper.startswith("SELECT"):
            raise PermissionError("只允许执行 SELECT 查询")
        
        dangerous_keywords = ['DROP', 'DELETE', 'INSERT', 'UPDATE', 'ALTER', 'CREATE', 'EXEC']
        for keyword in dangerous_keywords:
            if keyword in sql_upper:
                raise PermissionError(f"不允许执行包含 {keyword} 的查询")
        
        try:
            result = self.con.execute(sql)
            df = result.fetchdf()
            if len(df) > max_rows:
                df = df.head(max_rows)
            return {
                'success': True,
                'data': df.to_dict(orient='records'),
                'columns': list(df.columns),
                'row_count': len(df)
            }
        except Exception as e:
            return {'success': False, 'error': str(e)}

安全策略总结

风险类型防护手段说明
写入操作只允许 SELECT 开头拦截 INSERT/UPDATE/DELETE
结构修改黑名单关键字过滤拦截 DROP/ALTER/CREATE
资源耗尽限制返回行数防止大查询拖垮系统
SQL 注入参数化查询 + 白名单DuckDB 本身不提供参数化,需额外处理

六、第四步:生成自然语言回答

查询结果返回后,还需要让 LLM 将其转化为业务人员能看懂的文字分析。

def generate_answer(user_query, query_result, schema_info):
    if not query_result['success']:
        return f"抱歉,查询执行失败:{query_result['error']}。请换一种方式提问。"
    
    if query_result['row_count'] == 0:
        return "根据当前数据,没有找到匹配的结果。"
    
    # 只取前 5 条数据作为上下文,节省 token
    data_summary = json.dumps(query_result['data'][:5], indent=2, ensure_ascii=False)
    
    messages = [
        {
            "role": "system",
            "content": """你是一个数据分析师。请将查询结果转化为用户容易理解的中文回答。
1. 先总结核心发现(1-2句话)
2. 列出关键数据点
3. 给出简短的业务建议"""
        },
        {
            "role": "user",
            "content": f"""用户问题:{user_query}

查询结果(前5条):
{data_summary}

{query_result['row_count']} 条记录

请用自然语言回答用户的问题:"""
        }
    ]
    
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=messages,
        temperature=0.3,  # 稍高温度让回答更自然
        max_tokens=1000
    )
    
    return response.choices[0].message.content

七、完整的交互循环

把所有模块组装起来,就是一个可用的数据问答系统:

def chat_loop():
    print("=" * 60)
    print("🤖 DuckDB 智能数据问答助手")
    print("=" * 60)
    
    schema_info = get_table_schema(con)
    engine = SafeQueryEngine(con)
    
    while True:
        user_query = input("💬 你: ").strip()
        if user_query.lower() in ('quit', 'exit', 'q'):
            break
        
        # 1. 生成 SQL
        sql = generate_sql(user_query, schema_info)
        print(f"📝 生成 SQL: {sql}")
        
        # 2. 执行查询
        query_result = engine.execute_query(sql)
        
        # 3. 生成回答
        answer = generate_answer(user_query, query_result, schema_info)
        print(f"\n🤖 助手: {answer}")
        print("-" * 60)

# 启动
chat_loop()

实际对话示例

💬 你: 上个月哪个国家的订单最多?
📝 生成 SQL: SELECT ship_country, COUNT(*) as order_count 
             FROM orders 
             WHERE order_date >= DATE '2024-05-01' 
               AND order_date < DATE '2024-06-01' 
             GROUP BY ship_country 
             ORDER BY order_count DESC LIMIT 10;
🤖 助手: 上个月订单量最多的国家是中国,共有 127 笔订单。其次是美国(98 笔)和日本(76 笔)。建议针对中国市场加大营销投入。

八、进阶:封装成 Web 服务

将上述系统封装为 FastAPI 服务,就可以让任何前端接入:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI(title="DuckDB 数据问答 API")

class QueryRequest(BaseModel):
    question: str

@app.post("/ask")
def ask_question(req: QueryRequest):
    sql = generate_sql(req.question, schema_info)
    result = engine.execute_query(sql)
    answer = generate_answer(req.question, result, schema_info)
    return {"answer": answer}

启动后,任何前端(网页、微信小程序、飞书机器人、Slack Bot)都能调用这个 API。这意味着你只需要开发一次后端,就能覆盖多种触达渠道。


九、关键优化技巧

1. 开启并行执行

con.execute("SET threads TO 4")  # 充分利用多核 CPU

2. 缓存高频查询

from functools import lru_cache

@lru_cache(maxsize=128)
def cached_ask(question_hash: str, question: str, schema_hash: str):
    """缓存相同问题的回答,避免重复调用 LLM"""
    sql = generate_sql(question, schema_info)
    result = engine.execute_query(sql)
    return generate_answer(question, result, schema_info)

3. 复杂查询分步执行

多表 JOIN 时,LLM 容易出错。可以将复杂查询拆分为多个简单查询:

  • 先用简单查询获取基础数据
  • 再用 SQL 聚合计算
  • 最后让 LLM 分析结果

4. 添加错误重试机制

def robust_generate_sql(user_query, schema_info, max_retries=3):
    for attempt in range(max_retries):
        try:
            sql = generate_sql(user_query, schema_info)
            # 尝试执行看看是否有效
            test_result = engine.execute_query(sql)
            if test_result['success']:
                return sql
        except:
            pass
    return None

十、与传统方案对比

维度传统方案(React + FastAPI + PostgreSQL)DuckDB 方案
部署复杂度需要 3 个服务(前端、后端、数据库)一个 Python 进程即可
代码量2000+ 行150 行核心逻辑
数据加载需要先 ETL 导入数据库直接读取 CSV/Parquet
启动时间分钟级毫秒级
适用场景大规模生产环境中小规模、嵌入式、原型验证

DuckDB 的核心优势:嵌入式、零配置、直接读取文件。对于数据问答机器人这类应用场景,不需要独立数据库服务器,大大降低了部署和维护成本。


十一、变现指南:三种场景,三种定价

场景 1:中小企业内部数据看板

  • 目标客户:日订单 100-1000 单的电商卖家、连锁餐饮
  • 卖点:“让店长不用等分析师,自己就能查数据。”
  • 定价:一次性交付 8000-15000 元 + 每月 500-1000 元维护费
  • 交付周期:1-2 天(数据量不大时)

场景 2:金融研报自动化

  • 目标客户:券商研究所、基金公司初级分析师
  • 卖点:“把重复性的数据整理工作交给 AI,分析师专注于逻辑判断。”
  • 定价:企业级授权 30000-80000 元/年
  • 交付周期:1-2 周(需要对接多个数据源)

场景 3:SaaS 数据问答产品

  • 卖点:面向没有数据团队的中小企业,按席位收费
  • 定价:99-299 元/月/用户
  • 交付周期:持续迭代,MVP 一周上线

十二、总结

这套系统的 MVP 你可以在一个下午搭出来,然后拿去见客户谈单。数据产品化,从来不是技术问题,而是"你能不能把数据变成别人看得懂的语言"。

DuckDB 在这个场景中扮演的角色非常关键:它让你不需要关心数据库的部署和维护,把精力集中在"如何让业务人员更好地获取数据洞察"这个真正的商业问题上。

当你的客户问"为什么选 DuckDB 而不是 MySQL"时,你可以自信地回答:因为你的系统应该像手机 App 一样开箱即用,而不是像安装打印机驱动一样折腾半天。


📖 本文的完整版已发布在 duckdblab.org,包含更详细的步骤、错误排查指南和部署模板。想系统学习 DuckDB 实战?duckdblab.org 上有完整的教程系列,从入门到企业级部署全覆盖。

📺 Watch video tutorials → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials

使用 Hugo 构建
主题 StackJimmy 设计