让模型「查到数」很容易;让它查对、算对、说清楚,是一整套工程。
「用自然语言问数据库」是 demo 里最惊艳、生产里最容易翻车的场景。原因是它把三种难题叠在一起:把模糊问题落到精确 schema(NL2SQL)、把计算交给可执行代码而非模型幻觉、把结果讲成可信的结论而不踩统计陷阱。这一期不讲「什么是 text-to-SQL」,假设你已经让 Claude 写过 SQL、用过 Code Interpreter。我们讲的是工程实战:为什么大 schema 一定要先检索再生成、为什么「让模型写代码跑」远比「让模型直接给数」可靠、数据 agent 怎么对账自己的数字才不会自信地算错。BIRD/Spider 2.0 的残酷数字会贯穿全文——它们告诉你:从 demo 到生产,差的不是模型聪明度,是这套围绕模型的验证脚手架。
学术 Spider 上 SOTA 早已超 90% 执行准确率,但换到 BIRD(贴近真实业务的大库,arXiv 2305.03111),当年最强模型执行准确率只有 40% 出头、人类 93%;换到 Spider 2.0(企业级,单库常 >1000 列、多方言,arXiv 2411.07763、ICLR 2025 Oral),连 o1-preview 也只有约 21%。差距几乎全来自 schema linking:把问题里的「活跃用户」「上月营收」对应到具体表、列、join 路径和过滤条件。真实库里列名是 usr_st、状态码是魔法数字、同义列散在多表——这不是语法问题,是语义对齐问题。
工程上正确的姿势是别把整个 schema 塞进 context。大库的 DDL 几万 token,既爆预算又稀释注意力。正解是把 schema linking 当成一个检索问题:用问题去召回最相关的表/列子集,再带真实列值样例(让模型知道 status 取值是 'A'/'C' 而不是猜 'active')喂给生成。
# 不要:把整库 DDL 全塞进 prompt(爆 token + 注意力稀释)
# 要:检索相关表 → 带样例值 → 再生成
def build_schema_context(question, db):
tables = retrieve_tables(question, db, top_k=8) # 向量召回相关表
ctx = []
for t in tables:
cols = t.columns
samples = db.query(f"SELECT * FROM {t} LIMIT 3") # 真实值锚定语义
ctx.append(f"{t.ddl}\n-- 样例: {samples}")
return "\n\n".join(ctx)
# prompt 里再加一行业务词典,治标列名/魔法数字歧义
GLOSSARY = "活跃用户 = users.status='A' 且 last_login > now()-30d"
那一行业务词典(metric definition)往往比换更强的模型收益更大——它把组织内「活跃」「营收」的口径固化,模型不再自由发挥。
LLM 是概率文本机,不是算术机。让它「心算」聚合、相关系数、同比,结果不可信也不可复现。Code Interpreter 范式把分析重写成:模型生成 Python/SQL → 在沙箱执行 → 拿真实输出(数字 + 图)回模型。好处有三:计算交给确定性运行时、错误可被执行反馈捕获并自我纠错、整段代码就是可审计的分析过程。Anthropic 的 code execution tool 与 OpenAI Code Interpreter 都是服务端沙箱:脚本结果由代码消费,模型只看最终产物——这同时省 token。Anthropic 的 advanced tool use 工程博客提到,把工具调用编排进代码执行(programmatic tool calling),在一个多工具基准上把计费输入 token 砍掉约 38% 且准确率不变。
关键工程点:图表也别让模型「描述」,让它画。模型直接输出图表 JSON/SVG 极易幻觉坐标和数值;正确做法是生成 matplotlib/plotly 代码执行出真图。
import anthropic
client = anthropic.Anthropic()
resp = client.beta.messages.create(
model="claude-sonnet-4-6",
betas=["code-execution-2025-05-22"],
max_tokens=4096,
tools=[{"type": "code_execution_20250522", "name": "code_execution"}],
messages=[{"role": "user", "content":
"用 sales.csv 算各区月环比,并画折线图。"
"用 pandas 读,断言无空值再算,把结果数字也打印出来。"}],
)
# 模型写代码 → 沙箱执行 → 数字与图来自真实运行,可复现
# 出错时执行反馈回流,模型自我修正下一轮代码
prompt 里那句「断言无空值再算」「把结果数字打印出来」是工程纪律:强制模型把假设写进代码、把中间值暴露出来,便于你复核。
read_csv 进内存 OOM;对大表应推到数据库做聚合,只把结果拉回沙箱。(3)盲信图——模型可能因 dropna 静默丢了半数行还画出「漂亮」趋势。永远要求它同时打印 len(df) 和处理前后行数差。
「数据故事」是这类工具的卖点,也是最大的陷阱:模型天然擅长把任意数字编成因果叙事。常见翻车点:Simpson 悖论(分组后趋势反转)、幸存者偏差(只看留存用户算满意度)、类型/NULL 静默错误(字符串 '100' 求和、NULL 被当 0 拉低均值)、p-hacking(agent 自动跑几十个切片,挑出「显著」的那个讲故事)。模型不会主动质疑数据生成过程——它只会让你的数听起来更可信。
工程防御不是「让模型更小心」(没用),而是把怀疑固化成强制步骤:先 EDA(缺失率、分布、异常值、基数)再下结论;让模型把每个论断的假设和口径显式写出来;对自动切片做多重比较校正或干脆限制切片数。最有效的一招:要求叙述里每个数字都附上产生它的查询/代码,可一键回溯。
# 系统提示里把统计纪律写成硬约束(节选)
你是数据分析师,输出前必须:
1. 先做 EDA:报告行数、各列缺失率、关键列的分布与异常值
2. 任何聚合前,显式说明:如何处理 NULL、用什么粒度、排除了谁
3. 给结论时区分【相关】与【因果】,不得用未验证的因果措辞
4. 若按维度下钻,检查整体趋势与分组趋势是否一致(Simpson 检查)
5. 每个关键数字后附 [来源: 第N段代码],便于回溯
# 反例:「华东转化率最高,应加大投放」
# 正确:「华东转化率最高(12%),但其样本量仅占3%且新客占比异常,
# 建议先确认样本代表性再决策」
把单次查询升级成分析 agent(plan → query → inspect → refine 多轮),收益是能处理「先看分布再决定怎么切」这种依赖中间结果的开放任务;代价是它会跑飞而不自知。区别一个玩具和一个可信 agent 的,是有没有对账层(reconciliation):把 agent 算出的数字与独立的 ground truth 交叉验证。便宜且有效的对账手段——总量校验(各分组之和 = 总表行数/总额)、跨路径验证(同一指标用两种 SQL 算应相等)、量级 sanity(结果是否在历史区间内)。这些不交给模型判断,写死在 harness 里。
# 对账作为硬门:agent 给的数过不了校验就打回重算
def reconcile(result, db):
checks = []
# 1) 分组之和 == 全表总量
grp_sum = sum(r.value for r in result.rows)
total = db.query("SELECT SUM(amount) FROM orders")[0]
checks.append(("分组和=总量", abs(grp_sum-total) < 1e-6))
# 2) 量级 sanity:在历史 ±3σ 内
checks.append(("量级合理", in_historical_range(result)))
failed = [name for name, ok in checks if not ok]
if failed:
return {"pass": False, "retry_hint": f"对账失败:{failed},检查join是否重复计数"}
return {"pass": True}
对账失败最常见的根因是 join 导致的重复计数(一对多 join 让金额翻倍)——「分组和 ≠ 总量」能一眼抓出它,而模型自己几乎永远发现不了。
把四点串成一个周末项目:对一个你熟的真实数据集(订单 / 健身 / 记账皆可),造一个能问答 + 画图 + 自我对账的分析 agent。
LIMIT 3 样例值。先验证:换更弱模型 + 好词典 vs 强模型无词典,哪个准。reconcile(),至少含「分组和=总量」「量级 sanity」两条,过不了就回流重算。做完这套,你以后看任何「AI 数据分析」产品都会先找它的对账层在哪——没有对账层的,本质是个会编故事的随机数生成器。