来源:https://github.com/mehrabr/agentic-duckdb-analyst

Agentic DuckDB Analyst

一个面向 DuckDB 的“自然语言转 SQL”代理,它不信任自己的第一个答案。它会编写查询、运行它、检查结果,通过从便宜到昂贵的验证阶梯逐步升级,然后进行修订——或者诚实地拒绝——而不是返回一个自信的猜测。

Text-to-SQL 只是一个已解决的演示。这里的实质是验证阶梯、自洽层,以及一个作为一等组件的评测工具——这些才是一个代理工程面试真正会考察的东西。

结果(离线模拟评测工具)

verify_offline.py 针对一个确定性模拟对象生成,用于验证评测工具本身。请替换为真实模型在完整标准答案集上的数字——并保持失败类别的可见性。

questions:                64         execution accuracy:       98.4%
decline recall:           100.0%     decline precision:        88.9%
confidence precision:     100.0%     confidence recall:        100.0%
accuracy by difficulty:   answerable buckets 100%, window 83.3%,
                          unanswerable 100%, ambiguous 100%

(模拟对象返回正确的 SQL,因此这些数字验证的是评测工具,而不是模型。唯一的一次失败是故意设置的一次放弃——这也表现为一次不恰当的拒绝,将拒绝精确率降至 8/9。替换为真实模型后,这些数字才变得有意义。)

准确率是根据结果集等价性与人工验证的标准答案行进行比较,而不是 SQL 字符串匹配——两个不同的查询可能同样正确。

验证阶梯(成本阶梯)

我们只在信心需要时才会向上攀登;策略在 Settings 中是明确的,因此一个清晰的问题只花费一次模型调用,而昂贵的验证只在必要时才进行。

层级 成本 捕获内容 触发条件
0 — 执行保护 免费 解析/错误列/类型错误;DuckDB 的错误文本会被反馈用于自我修正 始终执行
1 — 结果合理性 便宜 空结果 / 全 NULL / 退化结果 始终执行
2 — 意图对齐 1 次调用 查询运行但回答了错误的问题(将 SQL 反向翻译成英语,与意图进行比较) verify_mode(默认:在层级 1 发现异常时触发)
3 — 自洽性 N 次调用 抽取独立样本,按结果集等价性进行聚类,衡量一致性,揭示疑虑 consistency_mode(默认:仅在路径不稳时触发)

如果所有层级都耗尽,代理会返回 gave_up=True。一个经过校准的 “我无法验证这一点” 胜过看似合理的错误数字——并且评测工具会衡量这个不确定性信号是否可信(根据实际错误情况的精确率/召回率)。

为什么标准答案集不是规范的 TPC-H 22 个查询

标准的 22 个查询会逐字出现在模型的训练数据中,因此根据它们打分衡量的是模型的记忆能力。questions.yaml 中包含了针对同一模式的 64 个原始提示,分为三种类型

  • 可回答(50 个) —— 一个正确的结果。按类别划分:单表 12 个,连接 12 个,聚合 14 个,窗口函数 6 个,嵌套查询 6 个,因此低分可以按类别诊断。
  • 不可回答(8 个) —— 模式中没有这样的列/表/概念(例如“客户 1 的电子邮件地址”)。正确的行为是拒绝,而不是编造 SQL。这是“放弃”真正发挥作用的地方。
  • 有歧义(6 个) —— 存在多种站得住脚的解读(例如“前五名客户”——按消费额?余额?订单数量?)。正确 = 答案匹配任何列出的合理解释,认可一种站得住脚的解读,而不是因为措辞而扣分。

因为对于不可回答的问题,拒绝是正确答案,所以评测工具会单独报告弃权情况:拒绝召回率(在不可回答的问题中,它拒绝了多少)和拒绝精确率(在所有拒绝中,有多少是恰当的——低值意味着它在可回答的问题上也退缩了)。信心校准仅针对已承诺的答案进行衡量。可回答/有歧义问题的标准答案集可以被快照analyst snapshot),这样对参考查询的不小心修改就不会悄悄地移动目标。

局限性(请阅读)

  • 列比较是按位置,而不是按排列顺序。 正确列但顺序不同将被判错。Spider 测试套件的评估器会搜索排列;这是一个 v1 项目,诚实地列出而不是隐藏。
  • 模式检索是基于词法且较弱的。 对于模糊的标识符(“balance” ≠ c_acctbal),它会回退到完整模式。这个接口是显式的——用嵌入相似度替换 score_table,其他一切都不需要改动。
  • 层级 1 是启发式的。 一个合法的空结果会触发不必要的层级 2 检查(花费一次调用;但不会导致错误答案)。
  • 自洽性需要真实的 temperature > 0 才能获得独立的样本;离线模拟只能近似模拟分歧。

运行

pip install -e ".[llm,dev]"           # 或者: pip install -r requirements.txt
make verify                            # 离线:合成模式 + 模拟,无需密钥
make test                              # 19 个单元测试

python -m analyst --offline eval                       # 模拟端到端
python -m analyst --sf 1.0 eval --snapshot gold_snapshot.json   # 真实运行(需要密钥)
python -m analyst --md md:my_db eval                   # 针对 MotherDuck
python -m analyst --offline ask "how many ... ?"
python -m analyst --offline --trace traces/run.jsonl eval       # 结构化追踪

真实运行使用 AnthropicClient(需要 ANTHROPIC_API_KEY);--offline 使用模拟对象。在 Settings 中设置 price_in_per_mtok / price_out_per_mtok 以查看成本数据。

真实运行

评测工具通过离线方式针对模拟对象进行自我验证。要获得有意义的数字,请将其指向一个真实的模型。以下是确切的步骤。

1. 密钥

export ANTHROPIC_API_KEY=sk-...

2. 首先冻结标准答案

对标准答案集进行快照,这样目标就不会在多次运行之间漂移:

python -m analyst snapshot --sf 1.0 --out gold_snapshot.json

sf=1.0 大约是 1GB 的 TPC-H 数据,生成需要几秒钟;在你调试时,可以降低到 --sf 0.1 以获得更快、更便宜的测试。)

3. 运行,持久化,包含成本和重复次数

python -m analyst \
  --model claude-sonnet-4-6 \
  --price-in 3 --price-out 15 \
  eval --sf 1.0 --snapshot gold_snapshot.json \
       --repeat 3 --out runs/sonnet
  • --price-in / --price-out 是每百万 token 的美元价格。请将它们设置为你的提供商当前公布的费率——它们默认值为 0(不计算成本),正是为了避免硬编码可能过时的数据。
  • --repeat 3 会完整运行整个测试套件三次。模型不是确定性的;一个数字会掩盖这一点。你会得到主要指标的平均值/标准差/最小值/最大值。
  • --out runs/sonnet 会将运行结果写入磁盘。

使用 MotherDuck 替代本地 TPC-H:

export motherduck_token=...
python -m analyst --model claude-sonnet-4-6 --price-in 3 --price-out 15 \
  eval --md md:my_db --snapshot gold_snapshot.json --out runs/sonnet-md

4. 写入磁盘的内容

runs/sonnet/ 目录下:

  • misses.csv —— 仅包含它答错的题目,以及它最终确定的 SQL。这是你撰写分析报告时需要的文件。
  • outcomes.csv —— 每个题目,每次运行的结果:结论、置信度、尝试次数、一致性、成本、延迟、最终 SQL。
  • report.json —— 摘要面板(准确率、拒绝召回率/精确率、信心校准、按难度分类的准确率、平均成本/延迟)。
  • aggregate.json —— 多次重复运行的平均值/标准差/最小值/最大值。
  • meta.json —— 模型、价格、规模因子、设置、时间戳。

5. 按此顺序阅读

  1. aggregate.json → 标题准确率以及它在多次运行间的波动情况。
  2. report.json → 哪些难度类别失败了,以及拒绝召回率是否保持(它是否仍然拒绝不可回答的问题,还是开始编造列了?)。
  3. misses.csv → 实际的失败案例及其背后的 SQL。分析文章就在这里。

比较模型

使用不同的 --model--out 再次运行步骤 3,然后比较两个 report.json 文件。相同的评测工具,相同的冻结标准答案,两个模型——这是一个公平的比较,也是第二篇分析文章的内容。

项目结构

analyst/
  config.py        Settings: 所有配置项 + 升级策略集中在一处
  db.py            DuckDB + TPC-H 设置, MotherDuck 路径, 只读执行
  schema.py        模式描述 + 词法检索(已预留嵌入接口)
  comparator.py    结果集等价性(多重集合、浮点数容忍、顺序无关)
  consistency.py   层级 3: 聚类独立样本, 衡量一致性
  agent.py         四层阶梯 + 成本感知升级 + 置信度
  gold.py          标准答案题目 + 快照
  metrics.py       按难度分类的准确率, 成本/延迟, 不确定性校准
  harness.py       在标准答案集上运行代理, 评分, 报告
  llm.py           提供者接口 + Anthropic 客户端 + 离线模拟 + 用量
  trace.py         JSONL 运行跟踪器(可观测性)
  cli.py           eval / snapshot / ask
tests/             19 个单元测试: comparator, consistency, agent, harness, schema, gold
questions.yaml     64 题标准答案集: 可回答 + 不可回答 + 有歧义
demo.py            离线夹具(完整合成 TPC-H + 模拟构建器)
run.py             便捷入口点
verify_offline.py  无需网络的冒烟测试

路线图

v0 版本包含所有四个层级、成本感知升级、检索、快照、跟踪、弃权 + 校准指标,以及一个包含 64 道覆盖可回答、不可回答和有歧义三种类型题目的标准答案集。下一步:列排列不变性评分、基于嵌入的检索,以及一项真实的分歧研究(层级 3 是否真的能捕获单次传递遗漏的错误,以及它是否能在对抗性集合上提高拒绝精确率?)。这项研究将是让项目落地的关键报告。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐