面试表达
面试时怎么讲
一句话:我围绕「Text-to-SQL 与数据分析 Agent(语义层 / 指标口径 / SQL 安全)」做过从问题拆解、方案设计、工程落地到效果验证的闭环。
追问重点:为什么这么设计、指标怎么验证、失败案例如何定位、上线后怎么观测和回滚。
证据建议:准备 README、架构图、关键代码片段、评测表和 2 个坏例复盘。
把"会画图"升级为"能回答业务问题":自然语言 → SQL / pandas / Polars,覆盖 schema linking、指标口径、SQL 安全、错误恢复、图表推荐、HITL 审核。
本地学习进度
只保存在当前浏览器,不上传服务器。
学习产出
进阶能力,P0。阅读时尽量把知识点转成可演示项目、面试回答和简历证据。
能回答
课程目标(14 天,50K+ 岗位作品集重器) / 为什么 Text-to-SQL 是"可作品集化"的最佳选题 / 核心架构(面试可直接口述)
能交付
完成练习与自测,留下可复盘的代码、截图或 README
下一步
先读手把手版,再回到正文补架构和取舍
面试表达
一句话:我围绕「Text-to-SQL 与数据分析 Agent(语义层 / 指标口径 / SQL 安全)」做过从问题拆解、方案设计、工程落地到效果验证的闭环。
追问重点:为什么这么设计、指标怎么验证、失败案例如何定位、上线后怎么观测和回滚。
证据建议:准备 README、架构图、关键代码片段、评测表和 2 个坏例复盘。
简历表达
围绕 Text-to-SQL 与数据分析 Agent(语义层 / 指标口径 / SQL 安全) 场景,完成「把"会画图"升级为"能回答业务问题":自然语言 → SQL / pandas / Polars,覆盖 schema linking、指标口径、SQL 安全、错误恢复、图表推荐、HITL 审核。」相关能力建设,沉淀可复用工程方案、测试样本和面试表达材料。
读完本课你要能做到:
| 对比维度 | 只读 CSV 画图 | 真·数据分析 Agent |
|---|---|---|
| 数据源 | 单表单文件 | 多库多表、join、权限、跨源 |
| 问题类型 | "画个柱状图" | "上周华东区 30 天留存比上月降了多少" |
| 语义 | 无 | schema linking + 指标口径 + 业务词典 |
| 安全 | 无 | 只读账号 + SQL 白名单 + 超时 + 敏感字段脱敏 |
| 可解释 | "这是图" | "我查了 orders + users,因为 XX 字段表示 YY" |
| 产品形态 | 玩具 | B 端 SaaS 可卖 |
招聘方真正愿意付 50K+ 的点在右列。
自然语言问题
├── 意图识别(query / compare / drill-down / forecast)
├── 实体抽取(时间范围 / 维度 / 指标名)
▼
语义层
├── Schema Linking:表/字段候选 + 打分 + 消歧
├── 指标口径库:GMV = sum(paid_amount) where status='paid'
├── 业务词典:别名 / 同义词 / 黑话
▼
生成层
├── SQL 生成(few-shot + schema 裁剪 + 结构化输出)
├── 可选:pandas / Polars 后置计算
▼
信任层
├── SQL AST 校验(只读、表白名单、行数限制)
├── Dry-run + 超时 + cost 估算
├── 错误恢复(自修复 / 追问)
├── 高风险 HITL 审批(大表扫描 / 跨库 / 敏感字段)
▼
执行层(只读账号)
▼
解释与可视化
├── 查询解释(为什么查这些表、这样 join)
├── 图表推荐(折线 / 柱状 / 漏斗 / 表格 / KPI 卡)
├── 洞察摘要(环比、同比、异常点)
▼
前端流式渲染(与 ai-frontend-streaming-ux 联动)schema.json:每表每列带 description、data_type、sample_values、is_pii、tags。schemas/company_db.json + libs/schema_store.py。见下文「Schema Linking 深讲」独立章。今天做消歧实验:同一问题在 2 个同义字段(user_id vs customer_id)上如何打分。
把口径从 LLM 搬出来,落到代码:
# metrics/gmv.yaml
name: GMV
aliases: [销售额, 成交金额, 交易额]
definition: |
支付成功的订单金额总和(不含退款)
sql: |
SUM(CASE WHEN status='paid' THEN paid_amount ELSE 0 END)
filters:
- status: paid
dimensions_allowed: [date, region, channel, category]
owner: data-team
updated_at: 2026-03-01render_metric("GMV", dims=["region","month"]) 直出 SQL 片段。def nl2sql(question: str, ctx: DataCtx) -> SQLResult:
intent = classify_intent(question) # query/compare/trend
tables = schema_link(question, ctx, top_k=8)
metrics = metric_lookup(question) # 命中口径直出模板
prompt = build_prompt(question, tables, metrics, few_shots=ctx.few_shots)
sql = llm_structured(prompt, schema=SQLPlan) # JSON 输出
return sql{"sql": "...", "tables_used": [...], "assumptions": "...", "confidence": 0.86}。详见下文「SQL 安全专题」。今天至少做到:只读账号、AST 校验、超时、LIMIT、敏感字段屏蔽。
模型生成的 SQL 三类错误 + 对应自修复:
| 错误 | 检测 | 修复 |
|---|---|---|
| 语法错误 | 执行报 SyntaxError | 把错误信息 + SQL 回灌模型,最多 2 次 |
| 表/列不存在 | 执行报 UnknownTable | 回退 schema linking,给 top-5 备选,让模型重选 |
| 语义错误(结果为空/异常大) | 规则检测(空集、行数爆炸、指标量纲异常) | 追问用户 or 换口径 |
生成 SQL 的同时产出可读解释:
"为回答'上周华东 GMV',我查询了 orders 表与 regions 表,
通过 orders.region_id = regions.id 关联,
过滤 status='paid'(口径:GMV)
过滤 created_at in 2026-04-29 ~ 2026-05-05,
按 region='华东' 汇总。"assumptions + tables_used + join_keys + filters 模板化,不要让模型自由发挥。决策树(不必 LLM,就用代码):
def recommend_chart(cols, rows):
if len(rows) == 1 and len(cols) == 1: return "kpi"
if has_time(cols) and len(numeric(cols)) >= 1: return "line"
if len(categorical(cols)) == 1 and len(numeric(cols)) == 1:
return "bar" if len(rows) <= 20 else "table"
if is_funnel_pattern(cols, rows): return "funnel"
return "table"to_echarts_option(chart_type, data)。executors/cross_source.py。三层评测集:
| 层 | 规模 | 衡量 |
|---|---|---|
| Spider-like(公开) | 100 | 执行结果等价 |
| 业务黄金集 | 50 | 结果等价 + 口径一致 |
| 对抗集 | 30 | 模糊问法、别名、跨域词 |
normalize(question) + schema_version + metric_version 做 key。对接流程:
type DataAgentEvent =
| { type: "plan"; intent: string; tables: string[] }
| { type: "sql"; sql: string; explain: string; warning?: string }
| { type: "approval_needed"; reason: string; est_rows: number }
| { type: "rows"; preview: any[]; total: number }
| { type: "chart"; spec: EChartsOption; insight: string }录 3 分钟视频:一句"上周华东 top 10 品类销售同比"→ 5 秒内出 SQL + 解释 + 图 + 洞察 + 下钻按钮。
CREATE ROLE ai_reader LOGIN PASSWORD '...' CONNECTION LIMIT 20;
GRANT USAGE ON SCHEMA public TO ai_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO ai_reader;
REVOKE SELECT ON sensitive_users FROM ai_reader; -- 黑名单
ALTER ROLE ai_reader SET statement_timeout = '5s';
ALTER ROLE ai_reader SET default_transaction_read_only = on;用 sqlglot 解析,拒绝一切非 SELECT:
import sqlglot
def validate_sql(sql: str, allowed_tables: set[str]) -> SQL:
tree = sqlglot.parse_one(sql, dialect="postgres")
if not isinstance(tree, sqlglot.exp.Select):
raise Unsafe("只允许 SELECT")
# 表白名单
for t in tree.find_all(sqlglot.exp.Table):
if t.name not in allowed_tables:
raise Unsafe(f"表 {t.name} 不在白名单")
# 拒绝危险函数
for fn in tree.find_all(sqlglot.exp.Anonymous):
if fn.this.lower() in DANGEROUS_FUNCS:
raise Unsafe(f"禁用函数 {fn.this}")
# 强制 LIMIT
if not tree.args.get("limit"):
tree = tree.limit(10000)
return tree.sql(dialect="postgres")statement_timeout 5s;超时强杀。EXPLAIN 估 rows 与 cost,超阈值进入 HITL。is_pii: true。mask(col) 或直接拒答。# 召回
bm25_hits = bm25(question, over="table+col names+descriptions")
vec_hits = vec_search(question, top_k=30)
candidates = rrf_merge(bm25_hits, vec_hits, k=20)
# 精排(可选 cross-encoder)
ranked = cross_encoder_rerank(question, candidates, top_k=10)
# 消歧
if has_ambiguity(ranked):
clarify = ask_clarifying_question(ranked) # "你说的用户是 customer 还是 employee?"给每张表/列打分的特征:
线性组合出 score,top-K 进 prompt。
不要塞整张 schema,只塞候选:
Table orders (电商订单表):
id BIGINT PK
user_id BIGINT FK -> users.id
status TEXT -- 'paid'/'unpaid'/'refunded'
paid_amount NUMERIC -- 实付金额(含税,不含运费)
created_at TIMESTAMP
Relation: orders.user_id = users.iddeprecated: true,检索过滤。name: GMV
def: 支付成功订单金额(不含退款、不含运费)
sql_expr: SUM(CASE WHEN status='paid' THEN paid_amount END)
cautions:
- 不要用 total_amount(含未支付)
- 退款走单独表 refunds,月末对齐时要单独扣name: D1_Retention
def: |
Day0 活跃用户在 Day1 再次活跃的比例
sql_template: |
WITH d0 AS (
SELECT DISTINCT user_id FROM events
WHERE date = :date AND event_name = 'login'
),
d1 AS (
SELECT DISTINCT user_id FROM events
WHERE date = :date + 1 AND event_name = 'login'
)
SELECT COUNT(d1.user_id)::float / NULLIF(COUNT(d0.user_id), 0) AS retention
FROM d0 LEFT JOIN d1 USING(user_id)
cautions:
- "活跃"定义必须和业务对齐:login / any_event / pay 三种口径差异很大name: Checkout_Conversion
def: "加购 → 支付"转化
steps: [add_to_cart, start_checkout, paid]
window: 7d
cautions:
- 窗口外的成交不算
- 同一用户多次 funnel 取最后一次name: LTV_90d
def: 用户首次付费后 90 天内累计付费金额
cautions:
- 只算 paid 状态
- 退款从 LTV 扣除
- 新老用户要分开看面试杀招:能说清"每个指标至少 3 种算法,选哪个取决于业务问的是什么"。
EXPLAIN 估 rows/cost → 阈值以上 HITL 或 sample 预览。sqlglot 能做跨方言转译;Prompt 明确方言;生成器输出也标 dialect。schemas/company_db.json + libs/schema_store.pymetrics/*.yaml + libs/metric_resolver.pylibs/sql_guard.py(AST 校验)libs/nl2sql.py(生成链路)libs/chart_recommender.pyexecutors/readonly.pyeval/text2sql_suite/(三层评测集 + grader)docs/data-agent-architecture.mddemos/data-agent/ 可运行 demo| 级别 | 资源 |
|---|---|
| A | Spider Dataset(Text-to-SQL 学术基准) |
| A | BIRD-SQL(大规模真实数据 benchmark) |
| A | sqlglot 文档 |
| A | Polars 文档 |
| A | LangChain SQL Agent Tutorial |
| A | dbt Metrics / Semantic Layer |
| A | Cube Semantic Layer |
| B | Vanna.AI / Dataherald 开源实现参考 |
| B | Uber QueryGPT / Pinterest Text-to-SQL 工程博客 |
与 ai-data-analysis-system、ai-frontend-streaming-ux、ai-application-security、agent-state-persistence-hitl、llmops-observability-evaluation 全面联动。