简体中文 · English
上传、画像、清洗并用自然语言分析数据 —— Agent 在只读安全边界内生成和执行 PostgreSQL,并把结果解释和可视化。
一个面向企业数据分析场景的 AI Agent:业务人员用中文/英文提问("上季度营收最高的 5 个产品?"),Agent 生成 SQL、安全执行、画成图表,并给出自然语言结论。
核心看点是工程安全性:LLM 生成的 SQL 默认不可信;应用用三层纵深防御把执行约束为只读,并限制慢查询与跨数据源访问。
打开在线 Demo 后,无需注册即可使用隔离的匿名会话。可以直接尝试:
营收最高的 5 个产品是哪些?2025 年每月的销售趋势如何?各个地区的销售额占比是多少?
Agent 会展示实际执行的 SQL、查询结果和图表;每个匿名访客、会话和上传数据源都经过 ownership 校验与租户隔离。
要复现完整的上传 → 画像 → 清洗 → 分析流程,可使用仓库内的固定合成数据和可执行预期结果,再按 90 秒演示脚本操作。
- 🗣️ 自然语言 → SQL:中英双语提问,自动生成 PostgreSQL 查询
- 🛡️ 三层 SQL 安全防御:只读角色 + AST 校验 + 语句超时(详见下)
- 🔁 自我纠错:SQL 出错时,Agent 读取结构化错误码、查 schema、自动重写重试
- 📊 可追溯可视化:Agent 选择图表和结果列,服务端只从对应 SQL 结果构造柱状图 / 折线图 / 饼图(Recharts)
- 🔀 多模型路由:DeepSeek / OpenAI / Anthropic / Gemini / OpenRouter 运行时切换
- 📈 Eval 评测体系:分别衡量 SQL 结果正确性、应用内工具成功、最终回答完整性与端到端任务成功,而非关键词匹配
- 🧹 确定性数据清洗:结构化 Recipe、三档透明预设、Dry Run Diff、显式确认、并发 revision 防护、清洗后验证与历史记录
CSV / XLSX 上传
↓
Schema 推断 → 质量画像 → 预览 / 编辑
↓
Cleaning Recipe → Dry Run Diff → 显式 Apply → Validation
↓
用户提问(中/英)
↓
Next.js Chat UI (streaming + 工具卡片)
↓
/api/chat ── Auth (Better Auth + Guest) · Rate Limit · Provider 路由
↓
streamText (maxSteps: 5) + 3 Tools
├── runSql → 校验 + 只读执行 SELECT,返回结构化结果/错误
├── getSchema → 返回库结构(供 LLM 自查/纠错)
└── renderChart → 按 resultId 绑定真实 SQL rows,服务端生成图表 spec
↓
已授权数据源
├── 零售 Demo (Neon Postgres · retail_readonly)
└── 用户上传表 (userdata schema · userdata_readonly + table allowlist)
LLM 生成的 SQL 默认不可信。本项目用三层纵深防御约束其只读执行,并让数据库权限成为最终防线:
| 层 | 机制 | 防住什么 |
|---|---|---|
| 1. 数据库角色 | retail_readonly / userdata_readonly 仅有业务表 SELECT,并撤销写入、高权限与 schema 创建权限;共享 userdata_readonly 不承担租户读隔离 |
即使应用校验失效,数据库仍拒绝写操作;跨租户读隔离由第 2 层物理表 allowlist 承担 |
| 2. AST 校验 | node-sql-parser 解析成 AST:必须是单条 SELECT,且逐操作校验 tableList(防数据修改 CTE 绕过),拒绝多语句/注释/SELECT INTO/系统表/危险函数,并强制注入 LIMIT 1000 |
注入、写操作(含 CTE 内写)、数据泄露、拖库 |
| 3. 语句超时 | 角色级 statement_timeout = 5s + JS 侧超时兜底 |
笛卡尔积、慢查询拖垮数据库 |
校验逻辑在 src/lib/sql-validator.ts,执行在 src/lib/sql-executor.ts。仓库当前有 300+ 个单元/回归测试,其中 75 个聚焦 SQL validator(写操作/DDL/注入/多语句/数据修改 CTE、XML 映射函数绕过与 LIMIT 边界等)。另有 3 个连接一次性 Postgres 的安全集成测试,验证 Guest/ownership transfer、物理表 allowlist 与清洗事务。
真实的对抗性发现:最初的校验只判断
stmt.type === 'select',但 PostgreSQL 的数据修改 CTE(WITH t AS (UPDATE ... RETURNING *) SELECT * FROM t)顶层仍报告为select,可绕过该检查;修复方式是逐一校验tableList中每个操作。后续审查又发现query_to_xml_and_xmlschema等 XML 映射函数可把查询藏入字符串参数,使 AST 看不到被访问表;当前已拒绝完整 XML 映射函数族并加入嵌套/限定名回归。数据库只读角色仍是写操作的最终防线,但共享角色不能替代租户物理表 allowlist。
runSql 失败时返回结构化错误码,Agent 在同一 maxSteps 预算内自动恢复:
| 错误码 | Agent 的恢复动作 |
|---|---|
UNKNOWN_COLUMN |
调 getSchema 查正确列名 → 重写 |
SYNTAX_ERROR |
简化查询 → 重试 |
TIMEOUT |
加 WHERE / LIMIT 缩小范围 → 重试 |
VALIDATION_ERROR |
改写为单条 SELECT |
重试上限 3 次,避免无限循环。
零售销售场景,6 张表约 2.5 万行,覆盖 join / 聚合 / 时间序列 / 过滤四类分析查询:
customers · categories · regions · products · orders · order_items
由 scripts/seed-retail-db.ts 用 faker 生成(确定性种子),并自动创建只读角色。
上传数据源带有独立的 dataRevision 和画像状态。单元格编辑、增行和删行会在同一数据库事务中修改物理表与 metadata,同时把画像标记为 stale;重新画像通过 revision 乐观并发校验,避免把并发修改前算出的统计发布成最新结果。过期画像不会注入 Agent 提示词。
清洗执行边界为:
Preset / Structured Recipe
→ 确定性 TypeScript Executor
→ 全表 Preview Diff
→ 用户显式确认
→ 事务 Apply
→ Before / After Validation + History
当前支持空白/全角字符规范化、可配置 NULL 标记、金额/千分位/百分比规范化、无歧义日期规范化、缺失值处理、精确或基于 Key 的去重。上传、清洗和导出统一限制为每个数据源 50,000 行,以控制当前 Serverless 实现的时间和内存边界。Cleaning Dashboard 汇总最近 20 次运行,并可展开检查 recipe、影响范围、数据 revision 与清洗前后验证;尚未实现一键 Undo。旧 .xls 因原解析依赖存在未修复安全公告而不再接受,请先另存为 .xlsx 或 CSV。
# 1. 安装依赖
npm install
# 2. 配置环境变量
cp .env.example .env.local
# 填入 DEEPSEEK_API_KEY、DATABASE_URL(app 库)、
# RETAIL_ADMIN_DATABASE_URL(种子用)、BETTER_AUTH_SECRET
# 3. 初始化或迁移 app 库(auth/chat 表)
npx drizzle-kit migrate
# 4. 种子零售演示库(建表 + 数据 + 只读角色)
RETAIL_ADMIN_DATABASE_URL=postgresql://... npm run seed
# 完成后把输出的 retail_readonly 连接串填入 RETAIL_DATABASE_URL
# 5. 启动
npm run dev # → http://localhost:3000npm test # 单元/回归测试(不连接外部数据库)
npm run eval -- --validate-only # 仅验证 Eval CLI 与 50 题契约,不连接数据库/模型
npm run typecheck # tsc --noEmit
npm run eval # 端到端:50 个 NL→SQL 用例,输出可追溯执行准确率报告
npm run eval -- --rescore eval/results-....json # 不调用模型;按当前契约重放已保存 SQL
npm run test:e2e # Playwright:Guest 与画像/清洗 UI 闭环Eval 扩展为 50 个中英双语用例,覆盖 simple / aggregation / join / time series / multi-step / NULL / edge case。生成 SQL 先经过生产 SQL validator,再与 reference SQL 在同一只读数据库执行。比较器默认要求数值精确相等、保留列位置;只有逐题契约明确声明时,才允许绝对数值容差、额外解释列、文本组合或月/季度等价表示。无法唯一确定业务口径的题目保留为 diagnostic case,但不进入主分数。报告分别记录 SQL 重放结果、应用内 runSql 结果、正常流结束后的最终解释及端到端任务成功,并保留 provider/model、prompt、commit SHA、源码快照 hash、dataset hash、延迟、tool steps、真实失败后重试数、token usage 与 failure category。
2026-09-09 的一次 DeepSeek deepseek-v4-flash、prompt v4、50-case 真实运行,在修订评测契约前得到 74.0% raw strict result match / 74.0% task success,SQL validity、replay execution、应用工具成功和最终回答完整性均为 100%。13 个 raw mismatch 的后续审查识别出 9 个表示层差异、1 个 reference SQL 的实体粒度错误、2 个指标歧义和 1 个明确模型错误。基于该审查形成的 v2 契约离线重评分为 47/48(97.9%),另有 2 个 diagnostic case;因为契约是在查看本次输出后形成,该数字明确标记为 post-run adjudication,不能冒充预先冻结的独立基线:
一次 20-case、同模型的 prompt A/B snapshot(2026-08-15)中,旧版比较器记录的结果集执行准确率从默认 v2 的 40% 提升到 v4 的 60%,Validity 和当时的 Schema-adherence proxy 都为 100%。旧版比较器使用约 1% 的全局相对数值容差,因此这组结果只作为 prompt 迭代的历史方向性证据;原始结果与失败用例均保留:
40% / 60% 是历史 20-case、旧比较器口径的 snapshot;74% 是 50-case 原始严格结构分数;97.9% 是看过失败后形成契约的离线重评分。下一次冻结契约后的独立运行,才适合作为新的 prospective baseline。
| 验证项 | 当前状态 | 说明 |
|---|---|---|
| Unit / regression tests | 300+ passed | 不连接外部数据库;包含固定 Demo fixture 的清洗与分析 oracle |
| Browser E2E | 3 passed | Guest 入口、匿名升级入口与画像 → 清洗预览 → 显式 Apply |
| TypeScript | passed | tsc --noEmit |
| ESLint | passed | eslint . --max-warnings=0,可在 CI 非交互运行 |
| Production build | passed | Next.js production build |
| Security integration | 3/3 passed(GitHub Actions) | 真实 Guest/ownership transfer、物理表 allowlist 与清洗事务 |
| Deployment readiness | passed(staging) | 检查 Guest migration、只读角色属性及 userdata schema/table 权限 |
eval/中保留的 2026-06 报告是早期基线,不代表当前 hardened 版本。运行npm run eval会生成带时间戳的 JSON 与 Markdown 报告,避免用旧指标包装新实现。可用--prompt-variant v2或--prompt-variant v4做显式 A/B。
迁移后先配置/加固 userdata 只读角色,再执行 readiness gate。这两个命令都要求显式目标,不会回退读取 DATABASE_URL:
HARDEN_DATABASE_URL=postgresql://... \
USERDATA_READONLY_PASSWORD=replace-with-a-strong-random-password \
npm run db:harden-userdata
CHECK_DATABASE_URL=postgresql://... npm run db:check真实认证、Guest 隔离、ownership transfer 和 SQL allowlist 集成测试只能指向一次性测试数据库:
INTEGRATION_DATABASE_URL=postgresql://... \
ALLOW_DESTRUCTIVE_INTEGRATION=true \
npm run test:integration匿名数据清理默认为 dry-run,并要求显式数据库目标。实际删除还需要 --execute 和确认口令;旧共享 Guest 只有加 --include-legacy 才会进入候选:
CLEANUP_DATABASE_URL=postgresql://... npm run guest:cleanup
CLEANUP_DATABASE_URL=postgresql://... \
CONFIRM_GUEST_CLEANUP=DELETE_EXPIRED_GUEST_DATA \
npm run guest:cleanup -- --execute前端:Next.js 15 (App Router) · React 19 · Tailwind v4 · Recharts
Agent:Vercel AI SDK v4 (streamText + maxSteps 多轮工具调用)
安全:node-sql-parser (AST 校验) · Postgres 只读角色 · 语句超时
数据:Neon Serverless Postgres · Drizzle ORM
认证:Better Auth (email/password + Dev/Guest 模式)
质量:Vitest · 多供应商 Eval 框架
作者:Leo · AI 应用工程师
