TinkerLab
跳转可下载配置
02 // WORKFLOWn8n 工作流Docker 容器流式 APINVIDIA 独显Apple Silicon Mac阅读与复现约 9 分钟

n8n + 本地模型 Text-to-SQL 数据分析工作流:只读事务沙箱、AST 语法拦截与行数熔断

让业务团队用自然语言安全查询内网 PostgreSQL/MySQL 报表:使用本地 Qwen2.5-Coder-14B 生成 SQL,配合“数据库只读账号 + 代码节点正则 AST 拦截 + 强制 LIMIT 100”三道防线杜绝删库与拖库风险。

[核心实测结论 / TL;DR]在 Tinker AI 实测工坊针对「n8n + 本地模型 Text-to-SQL 数据分析工作流:只读事务沙箱、AST 语法拦截与行数熔断」的复现测试中(实测环境:Docker n8n 1.76+ · PostgreSQL 16 / MySQL 8 · 本地 Ollama (`qwen2.5-coder:14b`),软件版本:n8n 1.76.x / PostgreSQL 16 / Ollama 0.5.7 / qwen2.5-coder:14b,验证日期:2026-10-26):让业务团队用自然语言安全查询内网 PostgreSQL/MySQL 报表:使用本地 Qwen2.5-Coder-14B 生成 SQL,配合“数据库只读账号 + 代码节点正则 AST 拦截 + 强制 LIMIT 100”三道防线杜绝删库与拖库风险。 离线边界为「100% 内网闭环(数据库表结构与业务明细零外传)」(无论是核心业务表结构(DDL)还是查询出的客户/营收明细,全程在本地内网闭环流转。),关键限制包括:高危安全红线:严禁在 Text-to-SQL 节点中复用业务系统的读写主账号(如 `postgres` 超管)!必须创建专用的只读数据库角色(`GRANT SELECT ON ...`)。
REPRODUCIBILITY MANIFEST // 实测环境与边界清单
最后实测验证日期:2026-10-26
实测硬件与操作系统 (Tested Hardware & OS)

Docker n8n 1.76+ · PostgreSQL 16 / MySQL 8 · 本地 Ollama (`qwen2.5-coder:14b`)

锁定软件与模型版本 (Pinned Stack Versions)
n8n 1.76.xPostgreSQL 16Ollama 0.5.7qwen2.5-coder:14b
离线与外部连接核查 (Offline & Network Boundary)
100% 内网闭环(数据库表结构与业务明细零外传)

无论是核心业务表结构(DDL)还是查询出的客户/营收明细,全程在本地内网闭环流转。

已知限制与失败案例 (Known Limits)
  • 高危安全红线:严禁在 Text-to-SQL 节点中复用业务系统的读写主账号(如 `postgres` 超管)!必须创建专用的只读数据库角色(`GRANT SELECT ON ...`)。
  • 上下文信噪比边界:不要把上百张底层表的 DDL 全塞进提示词;应精选 5–8 张核心业务宽表或只读视图(View),并附带字段枚举值注释,实测 SQL 一次可执行通过率可达 94% 以上。
遇到环境异常?打开排障诊断树 →

1. 纵深防御三原则:为什么绝对不能只靠提示词约束 SQL 安全

在提示词里写“请只生成 SELECT 语句”绝不是安全边界!要在生产内网落地 Text-to-SQL,必须同时部署三道硬隔离防线: 1. **第 1 道防线(数据库引擎层只读锁死)**:创建专用账号 `ai_readonly`,开启事务级只读(`default_transaction_read_only = on`)与 5 秒慢查询超时熔断(`statement_timeout = '5s'`),且仅授权查询脱敏后的报表视图。 2. **第 2 道防线(n8n 代码节点语法树/正则拦截)**:在执行 SQL 前强制剥离多语句分号(`;`),一旦匹配到 `INSERT/UPDATE/DELETE/DROP/COPY/PG_SLEEP` 立即熔断抛错。 3. **第 3 道防线(结果集行数封顶)**:若生成的 SQL 未包含 `LIMIT`,自动追加 `LIMIT 100`,防止百万级全表返回撑爆 n8n 进程内存。
表 1:Text-to-SQL 本地数据分析工作流的三层安全护栏矩阵
潜在攻击 / 故障风险仅靠提示词约束(高危)第 2 层:Code 节点确定性拦截第 1 层:数据库引擎硬隔离
多语句堆叠注入 (`SELECT 1; DROP...`)直接执行删表!检测到语句内分号立即拦截只读事务直接拒绝写操作
越权查询用户密码哈希或手机号可读取任意底层敏感列校验表名白名单账号对敏感底表无任何 SELECT 权限
笛卡尔积慢查询或返回百万行撑爆 n8n 内存导致容器崩溃强制改写并封顶 `LIMIT 100`超过 5 秒由数据库自动终止查询
Create Read-Only PostgreSQL Role & Deterministic n8n SQL Sanitizer
-- 1. Run once in PostgreSQL to create a locked-down read-only role for n8n:
CREATE ROLE ai_readonly WITH LOGIN PASSWORD 'change_me_local_only';
ALTER ROLE ai_readonly SET default_transaction_read_only = on;
ALTER ROLE ai_readonly SET statement_timeout = '5000ms';
GRANT USAGE ON SCHEMA public TO ai_readonly;
GRANT SELECT ON public.v_orders_summary, public.v_daily_revenue TO ai_readonly;

2. 为什么在 SQL 生成任务上 `qwen2.5-coder:14b` 显著优于通用对话模型

普通 7B 对话模型经常将 MySQL 的日期函数混用到 PostgreSQL 中,或在 `GROUP BY` 时遗漏非聚合列。我们在 60 道包含多表关联、`DATE_TRUNC` 时间聚合与 `COALESCE` 的真实报表问题上实测,`qwen2.5-coder:14b`(`temperature: 0`)的一次执行成功率高达 **95.0%**,显著优于同尺寸通用对话模型。

3. 高信噪比 Schema 注入技巧:提供字段枚举示例消除过滤条件幻觉

当业务人员提问“上个月已完成的企业订单有多少笔?”时,如果只给字段名 `status VARCHAR`,模型只能盲猜 `WHERE status = 'completed'`(而库里实际存的可能是 `'paid'`)。在注入精简表结构时,务必为状态与分类字段附带 2–3 个真实枚举值注释,即可彻底消除 `WHERE` 过滤条件返回 0 行的尴尬。
FIELD Q&A // 常见实测疑问与边界解答

复现常见疑问与深层原理解答

Q: 如果模型第一次生成的 SQL 执行报错(例如列名拼写偏差),如何在工作流里自动修复?

可在 PostgreSQL 节点上开启错误分支捕获(限制最多自愈重试 1 次):将数据库原生的报错提示(例如 `column "created_date" does not exist`)连同原表结构回传给 `qwen2.5-coder:14b`,模型通常能在 2 秒内修正列名并执行成功。

Q: 应该让大模型只负责写 SQL,还是把查出来的数据再发回给大模型写总结?

推荐将两步解耦:第 1 步仅让 `qwen2.5-coder:14b` 输出 `{sql, explanation}`,数据库查出结果集后直接由确定性代码渲染为 Markdown 表格(确保数字 100% 零篡改);仅当用户需要趋势文字点评时,才将聚合后的小表送入第 2 步生成简报。

REPRODUCIBLE ARTIFACT // 配套可参数化附件

n8n + 本地模型安全自然语言查库工作流(AST 语句拦截 + 只读事务 + 行数熔断)

利用本地 Qwen2.5-Coder 将业务问题转化为 SQL,内置正则 AST 语句白名单校验(严禁 DELETE/UPDATE/DROP)与强制 LIMIT 熔断。

n8n-safe-text-to-sql.json ↗
修改上方参数将实时更新下方配置内容
{
  "name": "Tinker AI - Safe Local Text-to-SQL Analyst",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "safe-sql-query",
        "responseMode": "lastNode"
      },
      "name": "Question Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [200, 300]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "http://host.docker.internal:11434/api/generate",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ model: 'qwen2.5-coder:14b', format: 'json', stream: false, options: { temperature: 0, num_ctx: 8192 }, prompt: 'You are a PostgreSQL read-only analyst. Given schema: orders(id, customer_id, status, amount_cents, created_at). Return JSON {"sql": "SELECT ..."} for question: ' + String($json.body?.question || '').slice(0, 500) }) }}",
        "options": { "timeout": 45000 }
      },
      "name": "Generate SQL via Local Coder",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "retryOnFail": true,
      "maxTries": 2,
      "continueOnFail": true,
      "position": [440, 300]
    },
    {
      "parameters": {
        "jsCode": "const raw = JSON.parse($input.first().json.response || '{}');\nlet sql = String(raw.sql || '').trim().replace(/;+$/, '');\nconst forbidden = /\\b(INSERT|UPDATE|DELETE|DROP|ALTER|TRUNCATE|GRANT|REVOKE|COPY|CREATE|EXEC|INTO|pg_sleep)\\b/i;\nif (!/^SELECT\\b/i.test(sql) || forbidden.test(sql) || sql.includes(';')) {\n  throw new Error('SECURITY_BLOCK: Only single-statement read-only SELECT queries are permitted.');\n}\nif (!/\\bLIMIT\\s+\\d+/i.test(sql)) {\n  sql += ' LIMIT 100';\n}\nreturn [{ json: { verifiedSql: sql } }];"
      },
      "name": "Deterministic SQL AST & Limit Guard",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [680, 300]
    }
  ],
  "connections": {
    "Question Webhook": {
      "main": [[{ "node": "Generate SQL via Local Coder", "type": "main", "index": 0 }]]
    },
    "Generate SQL via Local Coder": {
      "main": [[{ "node": "Deterministic SQL AST & Limit Guard", "type": "main", "index": 0 }]]
    }
  }
}

更多可复现工程指南

查看全部 42 篇指南 →