Text-to-SQL 评测体系建设

Text-to-SQL 评测体系建设
AI Agent 工程实践教程 · 第 11 章
围绕 SQL 正确性、执行结果、用例集和指标,建立 Text-to-SQL 的评测闭环。
第十一天:Text-to-SQL 评测体系建设
前面几天我们已经把 Text-to-SQL 主流程跑起来了。
用户输入一个自然语言问题,系统会经过:
问题路由
↓
业务识别和业务 Skill 加载
↓
指标检索和表结构检索
↓
SQL 生成
↓
SQL 安全校验和执行
↓
结果分析
这条链路跑通以后,系统已经从 Demo 进入了“可用”的阶段。
但真实项目里,“可用”还不够。我们还要继续回答几个更难的问题:
这次改 Prompt 以后,效果有没有变好?
这次改业务 Skill 以后,原来正确的问题有没有被改错?
这次新增表结构以后,模型会不会误选表?
这次澄清逻辑调整以后,模型是更谨慎了,还是更容易过度追问?
这次最终回答看起来差不多,语义上到底算不算正确?
这些问题不能靠感觉回答,也不能靠开发同学手动问两三个问题回答。
我们需要一套可以长期沉淀、重复执行、自动对比的机制。
这就是今天要建设的内容:Text-to-SQL 评测体系。
1. 为什么 Text-to-SQL 必须做评测
普通后台接口大多是确定性的。
固定输入
↓
固定代码逻辑
↓
固定输出
但 Text-to-SQL 不是这样。
Text-to-SQL 中间有大模型参与,模型会做很多判断:
| 判断环节 | 可能出错的地方 |
|---|---|
| 问题路由 | 没识别成数据查询,或者把普通问答误判成查询 |
| 业务识别 | 订单问题识别成学生问题 |
| Skill 使用 | 没遵守业务里的统计口径 |
| 表结构检索 | 找到了 prepay_order,但实际应该用 order_payment |
| SQL 生成 | 字段、条件、聚合、时间范围写错 |
| SQL 执行 | SQL 语法错误,或者查询慢、超时 |
| 澄清判断 | 信息不足时没有追问,或者不该追问时过度追问 |
| 结果分析 | SQL 查对了,但最终解释说错 |
例如用户问:
统计上个月订单状态的分类占比
这句话看起来很简单,但系统需要判断:
订单指的是支付订单还是预支付订单?
上个月用 created_at 还是 pay_success_time?
分类占比按 order_status 还是 status?
是否需要先澄清?
如果不澄清,应该选择哪张表?
所以评测不能只看:
SQL 能不能执行?
更应该看:
整条 Text-to-SQL 链路中的关键判断是否符合预期?
2. 当前项目的评测总览
当前项目的评测体系已经不再是“写一个 JSON 去比对 State”的简单版本,而是拆成了几层:
| 模块 | 作用 |
|---|---|
| 运行记录 | 保存真实运行、评测运行、最终 State、SQL、回答、步骤记录 |
| 人工反馈 | 把真实问题中的错误沉淀成候选样本 |
| 评测样本 | 保存可复用的问题、业务、目标、场景和判断标准 |
| 判断标准 | 每条样本可以配置多条断言规则 |
| 评测任务 | 选择正式样本,异步批量运行完整 Text-to-SQL graph |
| 评测结果 | 保存每条样本的实际 State、SQL、回答和通过情况 |
| 断言结果 | 保存每条判断标准的实际值、得分、原因和是否通过 |
| 评测报告 | 展示通过率、失败样本、失败原因分布 |
整体架构是:
现在的核心思想是:
样本只负责描述“要测什么”
断言负责描述“怎么判定”
评测任务负责“真实跑一遍”
评测结果负责“留下证据”
3. 从真实运行到评测样本
评测样本最好不要完全靠拍脑袋录入。
真实项目里,最有价值的样本通常来自这几类场景:
| 来源 | 说明 |
|---|---|
MANUAL |
人工设计的典型问题 |
RUN_HISTORY |
从真实运行记录中加入样本 |
FEEDBACK |
从用户反馈或测试反馈中沉淀样本 |
一条真实运行记录里有:
原始问题
最终 State
生成 SQL
执行 SQL
最终回答
每一步 graph 状态
如果人工发现这次结果有问题,比如:
应该查 order_payment,不应该查 prepay_order。
就可以把它加入评测样本,变成以后每次都能复跑的回归用例。
样本状态分为:
| 状态 | 说明 |
|---|---|
DRAFT |
草稿样本,还在整理,不参与评测 |
ACTIVE |
正式样本,可以被评测任务选择 |
DISABLED |
暂停使用 |
只有 ACTIVE 样本会进入评测任务。
4. 评测样本应该保存什么
一条评测样本不是简单保存一个问题。
它至少要回答四件事:
测什么问题?
属于哪个业务?
主要测哪段能力?
怎么判断通过?
当前样本主表是 text_to_sql_eval_question,重点字段如下:
| 字段 | 说明 |
|---|---|
question |
评测问题 |
business_id |
业务标识,例如 order |
business_name |
业务名称 |
eval_target |
评测目标 |
sample_category |
样本场景 |
source_type |
样本来源 |
source_run_id |
来源运行记录 ID |
source_conversation_id |
来源会话 ID |
generated_sql |
来源运行生成 SQL |
executed_sql |
来源运行执行 SQL |
final_answer |
来源运行最终回答 |
feedback_result |
来源反馈结果 |
feedback_error_type |
来源反馈错误类型 |
feedback_comment |
来源反馈说明 |
judge_note |
人工判断说明 |
remark |
备注 |
status |
样本状态 |
这里要注意一个变化:
新版已经不再使用 expected_state_json 作为主要判断方式。
原因是 expected_state_json 虽然灵活,但对页面不友好,用户要手写 JSON,不适合长期维护。
新版改成了“表格化判断标准”:
期望 Key
判断方式
期望 Value
失败原因
是否必过
语义判断参考答案
关键要点
禁止内容
最低分
这样业务人员和测试人员不需要写 JSON,也能维护评测标准。
5. 评测目标和样本场景
不是所有样本都测端到端。
同一个问题可以用于验证不同能力:
| 评测目标 | 重点检查 |
|---|---|
END_TO_END |
整条链路最终是否可用 |
ROUTING |
是否识别为正确的问题类型 |
BUSINESS_SKILL |
是否命中正确业务和业务规则 |
METRIC_RETRIEVAL |
是否找到正确指标口径 |
TABLE_SCHEMA_RETRIEVAL |
是否选到正确表结构 |
SQL_GENERATION |
是否生成正确 SQL |
CLARIFICATION |
是否在信息不足时正确澄清 |
RESULT_ANALYSIS |
是否正确解释查询结果 |
样本场景用于标记这条样本属于哪类测试:
| 样本场景 | 说明 |
|---|---|
NORMAL |
正常样本 |
BOUNDARY |
边界样本 |
REGRESSION |
回归问题 |
AMBIGUOUS |
歧义澄清 |
NEGATIVE |
负向样本 |
这些选项不建议写死在前端。
当前项目把它们放在动态配置里:
text.to.sql.eval.metadata
前端通过接口读取配置,用于渲染:
评测目标
样本场景
判断方式
失败类型
这样后续新增一个评测目标或失败类型,不需要改前端代码。
6. 判断标准:从 JSON 改成断言规则
新版评测判断标准使用独立表:
text_to_sql_eval_assertion
一条样本可以有多条判断标准。
例如问题:
统计上个月每天支付成功的订单数量
可以配置两条标准:
| 期望 Key | 判断方式 | 期望 Value | 失败原因 | 必过 |
|---|---|---|---|---|
used_tables |
包含 | order_payment |
表选择错误 | 是 |
final_answer |
语义判断 | - | 回答质量错误 | 是 |
其中第一条是客观判断:
实际 state.used_tables 必须包含 order_payment。
第二条是主观判断:
最终回答不要求逐字一致,但要语义上覆盖参考答案和关键要点。
断言表关键字段:
| 字段 | 说明 |
|---|---|
eval_question_id |
属于哪条评测样本 |
actual_key |
从实际 State 里取哪个字段 |
operator |
判断方式 |
expected_value |
客观判断的期望值 |
required |
是否必过 |
failure_type |
失败后归类到哪种错误 |
reference_answer |
语义判断参考答案 |
key_points |
语义判断必须覆盖的要点,按行分隔 |
forbidden_points |
语义判断禁止出现的内容,按行分隔 |
min_score |
语义判断最低通过分 |
remark |
备注 |
支持的判断方式:
| 判断方式 | 用途 |
|---|---|
EQ |
实际值等于期望值 |
CONTAINS |
实际数组或文本包含期望值 |
NOT_CONTAINS |
实际数组或文本不包含期望值 |
EXISTS |
字段存在 |
NOT_EMPTY |
字段不为空 |
REGEX |
正则匹配 |
SEMANTIC |
调用大模型做语义评分 |
7. 客观判断:每条规则都要有结果
早期实现容易犯一个错误:
遇到第一条失败规则就直接返回。
这样虽然能判断这条样本失败,但排查体验很差。
因为用户看不到:
其他规则到底有没有过?
是只错了表?
还是业务、动作、SQL、回答都错了?
所以当前实现是:
所有判断标准都会执行
所有判断结果都会保存
最终样本是否通过,由必过规则决定
判定逻辑可以理解为:
for 每条断言规则:
从 actual_state_json 里按 actual_key 取值
按 operator 判断
保存一条 assertion_result
如果存在 required = true 且未通过的规则:
样本失败
失败类型取第一条必过失败规则
否则:
样本通过
这样做的好处是:
| 好处 | 说明 |
|---|---|
| 排查完整 | 每条规则都有实际值和结果 |
| 报告更准 | 可以统计哪类规则最容易失败 |
| 适合调参 | 改 Prompt 后能看每个环节的变化 |
| 支持非必过规则 | 可以观察但不阻断通过 |
8. 主观判断:用大模型评估回答质量
有些结果不能用“完全相等”判断。
尤其是最终回答:
数据覆盖 9 个有订单的日期,累计支付成功订单 21 单。
6 月 25 日达到峰值 8 单,其余交易日集中在 1 至 3 单,整体呈零星分布态势。
这类回答只要意思对,就不应该要求 100% 字符串一致。
所以新增了 SEMANTIC 判断方式。
语义判断会把下面这些内容发给 Python 服务:
| 输入 | 说明 |
|---|---|
question |
原始问题 |
actual_answer |
本次实际回答 |
expected_value |
可选的额外期望 |
reference_answer |
参考答案 |
key_points |
必须覆盖的关键要点 |
forbidden_points |
禁止出现的内容 |
min_score |
最低通过分 |
Python 服务调用大模型,返回:
{
"passed": true,
"score": 95,
"reason": "实际回答准确涵盖了所有关键统计数据,与参考答案语义一致。",
"coveredPoints": ["主要的数据要正确"],
"missedPoints": [],
"forbiddenHits": []
}
Java 后端会保存:
| 字段 | 说明 |
|---|---|
score |
本次语义评分 |
failure_reason |
判断原因。通过时也保存原因,失败时保存失败原因 |
actual_value_json |
本次实际回答 |
passed |
该条语义规则是否通过 |
这里要注意:
SEMANTIC 不需要展示期望 Value。
因为它的标准不是一个简单值,而是:
参考答案 + 关键要点 + 禁止内容 + 最低分
9. 评测任务:异步批量运行正式样本
有了正式样本以后,就可以创建评测任务。
评测任务页面负责:
选择一批 ACTIVE 样本
↓
点击开始评测
↓
后端创建任务
↓
接口立即返回
↓
后台异步逐条运行完整 graph
↓
保存每条结果和每条断言结果
↓
汇总通过率和失败原因
为什么必须异步?
因为每条样本都是真实跑一遍 Text-to-SQL graph:
模型路由
业务 Skill
检索
SQL 生成
SQL 执行
结果分析
自动判定
如果评测任务有几十条样本,同步接口会等待很久,页面体验很差,也容易超时。
现在的设计是:
- 点击“开始评测”。
- 后端创建
text_to_sql_eval_run,状态为RUNNING。 - 接口立即返回任务 ID。
- 后台线程逐条执行样本。
- 前端轮询任务状态和结果列表。
- 所有样本结束后刷新通过率、失败数和失败原因分布。
任务状态:
| 状态 | 说明 |
|---|---|
RUNNING |
正在后台运行 |
COMPLETED |
已完成,没有待澄清样本 |
WAITING_CLARIFICATION |
有样本进入澄清态,需要人工补充 |
FAILED |
任务级异常 |
10. 评测运行记录为什么要标记 EVAL
评测不是模拟判断。
每条评测样本都会真实调用当前 Text-to-SQL 流程。
所以评测也会产生运行记录:
text_to_sql_run.source_type = EVAL
真实用户提问则是:
text_to_sql_run.source_type = USER
为什么要区分?
如果不区分,评测任务跑几十条样本,就会把运行记录页面刷满,影响日常排查真实用户问题。
当前设计是:
| 来源 | 页面行为 |
|---|---|
USER |
运行记录页面默认展示 |
EVAL |
默认过滤掉,需要排查评测失败时再筛选 |
评测结果里保存 conversation_id。
如果某条样本失败,可以这样排查:
评测结果
↓ conversation_id
EVAL 运行记录
↓ run_id
运行步骤 State History
11. 评测结果:保存样本级结果
每条样本运行后,会保存一条:
text_to_sql_eval_run_result
它保存的是“这条样本这次整体跑得怎么样”。
关键字段:
| 字段 | 说明 |
|---|---|
eval_run_id |
属于哪次评测任务 |
eval_question_id |
对应哪条评测样本 |
question |
本次评测问题 |
business_id |
业务标识 |
eval_target |
评测目标 |
sample_category |
样本场景 |
conversation_id |
本次 graph 会话 ID |
passed |
样本是否通过 |
failure_type |
样本失败类型 |
failure_reason |
样本失败原因 |
actual_state_json |
本次实际 State |
actual_sql |
本次实际 SQL |
actual_answer |
本次实际回答 |
execution_error |
执行错误 |
duration_ms |
耗时 |
这里的 actual_state_json 是统一后的实际 State。
也就是说,评测会尽量把 Python 服务返回值和 Java RouteRes 中常用字段整理成统一 Key,例如:
question_type
business_id
business_name
action
sql
executed_sql
used_tables
used_fields
missing_info
interrupted
final_answer
这样前端填写断言时就可以直接写:
business_id
used_tables
action
final_answer
不需要用户关心 Python 内部字段到底叫 sql_action 还是 Java 里映射成 action。
12. 断言结果:保存每条规则的证据
样本级结果只能告诉我们:
这条样本通过还是失败。
但排查时我们更想知道:
哪条规则过了?
哪条规则没过?
实际值是什么?
语义判断得了多少分?
大模型为什么给这个分?
所以新增:
text_to_sql_eval_run_assertion_result
关键字段:
| 字段 | 说明 |
|---|---|
eval_run_id |
评测任务 ID |
eval_run_result_id |
样本级结果 ID |
eval_question_id |
样本 ID |
eval_assertion_id |
判断标准 ID |
actual_key |
判断字段 |
operator |
判断方式 |
expected_value |
期望值 |
actual_value_json |
实际值 |
required |
是否必过 |
passed |
该规则是否通过 |
failure_type |
失败类型 |
failure_reason |
判断原因,通过和失败都会保存 |
score |
语义判断得分 |
页面上“规则”弹框会把两部分放在一起:
样本定义的规则
+
本次运行的实际回答、实际值、评分、判断原因
这样看主观题时,不需要在“规则”和“详情”之间来回切。
13. 澄清样本怎么评测
Text-to-SQL 里有一类很重要的场景:需要澄清。
例如:
统计上个月订单状态占比
如果业务里同时有预支付订单和支付订单,这句话可能信息不足。
系统应该先追问:
请问要统计预支付订单,还是支付订单?
这种样本不能简单判定为失败。
它应该进入:
WAITING_CLARIFICATION
当前设计是:
- 第一轮评测运行问题。
- 如果 graph 返回需要澄清。
- 当前结果标记为
CLARIFICATION_REQUIRED。 - 任务状态变成
WAITING_CLARIFICATION。 - 页面上对这条结果显示“补充澄清”。
- 人工输入这条样本的补充回答。
- 后端立即把该结果标记为
CLARIFICATION_RUNNING并返回。 - 后台异步用这条结果自己的
conversation_id调用 continue。 - 更新这条结果的实际 State、SQL、回答和断言结果。
- 所有待澄清结果处理完后,任务变成
COMPLETED。
为什么要一条一条澄清?
因为每条样本都有自己的 graph thread:
样本 A -> conversationId A -> 补充回答 A -> continue A
样本 B -> conversationId B -> 补充回答 B -> continue B
样本 C -> conversationId C -> 补充回答 C -> continue C
不同样本缺的信息不同,会话上下文也不同。
所以不能把所有澄清答案一起发送。
14. 页面使用流程
当前后台主要有三个相关页面。
14.1 评测样本页面
评测样本页面负责维护样本。
常用操作:
| 操作 | 说明 |
|---|---|
| 录入样本 | 手动新增一个问题 |
| 从运行记录加入 | 把真实运行记录转成候选样本 |
| 整理样本 | 修改问题、业务、目标、场景 |
| 维护判断标准 | 添加客观规则和语义规则 |
| 查看来源 | 查看来源 SQL、回答、反馈说明 |
| 启用样本 | 把 DRAFT 改成 ACTIVE |
这个页面回答的是:
哪些问题可以用来评测?
每个问题应该怎么判断?
14.2 评测任务页面
评测任务页面负责真正跑评测。
常用操作:
| 操作 | 说明 |
|---|---|
| 新建评测任务 | 选择一批正式样本 |
| 开始评测 | 后端异步运行 |
| 查看通过率 | 看样本数、通过数、失败数 |
| 查看失败分布 | 看哪类错误最多 |
| 查看结果详情 | 看实际 State、SQL、回答 |
| 查看规则 | 看样本规则和本次断言结果 |
| 补充澄清 | 对 CLARIFICATION_REQUIRED 结果继续评测 |
| 跳转样本 | 从结果回到样本定义 |
这个页面回答的是:
这次系统整体表现怎么样?
具体失败在哪里?
14.3 运行记录页面
运行记录页面默认看真实用户提问,也就是:
source_type = USER
如果要排查评测失败,可以筛选:
source_type = EVAL
然后根据评测结果里的 conversation_id 找到完整运行步骤。
15. 一个完整例子
假设我们有一条样本:
统计上个月每天支付成功的订单数量
这条样本期望使用支付订单表,并且最终回答要说清楚主要统计结果。
可以配置两条判断标准:
| 期望 Key | 判断方式 | 期望 Value | 必过 | 失败类型 |
|---|---|---|---|---|
used_tables |
包含 | order_payment |
是 | 表选择错误 |
final_answer |
语义判断 | - | 是 | 回答质量错误 |
语义判断里再配置:
| 字段 | 示例 |
|---|---|
| 参考答案 | 数据覆盖 9 个有订单的日期,累计支付成功订单 21 单,其中 6 月 25 日达到峰值 8 单 |
| 关键要点 | 主要的数据要正确 |
| 禁止内容 | 不能编造不存在的状态 |
| 最低分 | 80 |
评测运行后,可能得到:
used_tables = ["order_payment"]
final_answer = "数据共覆盖 9 个有订单的日期,累计支付成功订单 21 单..."
断言结果:
| 规则 | 结果 | 说明 |
|---|---|---|
used_tables contains order_payment |
通过 | 实际 used_tables 包含 order_payment |
final_answer semantic |
通过 | 得分 95,覆盖关键要点,语义与参考答案一致 |
样本通过。
如果实际用了 prepay_order:
used_tables = ["prepay_order"]
第一条规则失败:
failure_type = TABLE_SELECTION_ERROR
failure_reason = used_tables CONTAINS 期望 order_payment,实际 ["prepay_order"]
样本失败。
16. 课程练习
今天的练习建议按这个顺序做:
- 在“数据分析”页面提出一个订单统计问题。
- 查看“运行记录”,观察最终 State 和每步 State History。
- 把这条运行记录加入“评测样本”。
- 在“评测样本”页面整理: - 评测目标 - 样本场景 - 判断标准 - 判断说明
- 添加一条客观规则,例如:
-
used_tables- 包含 -order_payment - 添加一条语义规则,例如:
-
final_answer- 语义判断 - 参考答案 - 关键要点 - 最低分 - 把样本状态改成
ACTIVE。 - 在“评测任务”页面新建任务,选择这条正式样本。
- 开始评测,查看结果。
- 打开“规则”弹框,查看每条规则的实际值、得分和原因。
- 如果进入待澄清,补充澄清答案继续评测。
- 到“运行记录”页面筛选
EVAL,查看评测产生的完整 graph 记录。
17. 小结
今天这节课的重点不是某张表,也不是某个按钮。
重点是建立一套 AI 应用的工程闭环:
真实问题
↓
运行记录
↓
人工反馈
↓
评测样本
↓
判断标准
↓
异步评测任务
↓
客观判断 + 语义判断
↓
断言结果明细
↓
澄清继续评测
↓
失败分析和回归优化
有了这套机制以后,每次改 Prompt、改 Skill、改表结构、改澄清策略,都可以通过评测任务验证。
Text-to-SQL 也就从:
能跑通
升级为:
能验证、能回归、能持续优化。







