V1|可测量的问数基线
实现状态:已完成。V1-S01~S05 全部 16 个任务通过(2026-09-09)。
这一期学会什么:基线的价值是发现失败分布,而不是在小数据集上追求漂亮演示。
为什么有这一期
限定 Sales 的 10~15 张表,跑通最小安全链路并发现真实失败。进入条件:V0 验收通过;选择并冻结 Sales 子集。
本期链路
Web 提问 → API → 固定 Sales Schema → Prompt/LLM → SQLGlot AST 校验 → 只读执行 → 表格 → Trace跟读顺序
- 从前端 QueryRequest 跟到 API 校验,确认用户身份与 trace 如何传递。
- 阅读 Prompt Builder,明确固定 15 张表的上下文范围。
- 先用 fake Provider 理解流程,再用真实模型记录质量。
- 找到 AST 拒绝点、只读事务、timeout 和结果上限。
- 读逐题评测报告,选择一条选表错与一条指标错的样例。
必做学习实验
用一个未知列问题、一个写 CTE 和一个超时 SQL 观察三种失败;把取消单加入销售数据,检查"能执行"与"业务正确"的差别。
记录输入、预测、实际输出、代码入口和失败原因;实验通过后再勾选相应任务。只有文档或 fake 运行不能替代真实集成与质量验收。
规模与验收门槛
形成可复现 Baseline 与失败分类;不预设准确率,不将示意百分比当实测。
完整功能与错误场景以本期计划为准。基线指标已通过真实评测记录(见下方"实测指标"节)。
设计取舍
限制范围让后续质量提升可归因。V1 不使用混合召回、语义注册表或 LangGraph,但安全基线不能为了演示而省略。
两分钟复述骨架
"我的 V1 是固定 Sales 子集的基线,记录每道题的 SQL、版本与错误类型。通过 SQLGlot AST 白名单校验和 asyncpg 只读事务保证安全;管理端可人工标注 Wrong Table / Column / Join / Value / Metric / Time / Aggregation 等分类。V2 的优先级来自真实 wrong_metric + wrong_value(4/10)的高频分布,而不是预先堆功能。"
真实实现代码导读
| 内容 | 详情 |
|---|---|
| 已验收任务及证据 | V1-S01-T01~T03、V1-S02-T01~T03、V1-S03-T01~T03、V1-S04-T01~T03、V1-S05-T01~T04;plan/evidence/ 下 5 个验收记录文件 |
| 真实入口与关键函数 | server/api/query.py;QueryOrchestrator.submit;SafeQueryPipeline.run;SQLValidator.validate;AsyncpgReadOnlyExecutor.execute;ResultSummaryBuilder.build |
| 上游/下游与数据契约 | HTTP → QueryRequest → Orchestrator → Prompt → LLMGateway → SQLValidator → ReadOnlyExecutor → QueryRepository;响应统一为 QueryResponse / QueryTrace |
| 正常与失败场景的命令 | uv run pytest tests/test_sql_safety.py tests/test_result_and_evaluation.py tests/test_llm_generation.py tests/test_query.py -q |
| 实测指标 | 183 passed,1 warning;syntax_valid 61.54%,execution_correct 30.77%,first_success 47.37%(19 题,deepseek-v4-flash);10 条人工复核标签写入数据库 |
| 本期新决策与限制 | SQLGlot 默认拒绝未知函数;reader DSN + 只读事务双重隔离;Decimal 以字符串返回;摘要不调模型;V1 身份头仍是可信上游占位 |
V1-S01 跟读:请求怎样进入固定范围
server/api/query.py校验请求并从x-atlas-identity取得 V1 临时身份。server/domain/query.py定义所有状态共用的请求、响应、列、错误和 Trace 契约。server/domain/sales_scope.py先拒绝库存等明确跨域请求;"收入"要求澄清;"销售额"绑定到 V0 指标草案net_sales。server/orchestrator/query.py把身份、数据/schema/模型版本和超时预算组成不可变上下文。server/query/repository.py保存请求和状态;控制库表由迁移9cb21c73e4a1创建。
关键失败场景是 pipeline 抛出包含连接信息的异常:编排器只返回 internal_error 和 trace_id,不会把异常原文交给用户。这里选择显式 QueryPipeline 端口,是为了让 S02 替换 Provider 时不修改编排逻辑。
V1-S02 跟读:候选 SQL 怎样生成
server/generation/prompt.py从固定 15 表白名单、指标草案和 train 示例构造版本化 Prompt。server/llm/gateway.py隔离 DeepSeek Chat Completions,并提供可重放、可延迟的 Fake。server/generation/sql.py在总超时预算内有限重试,再用 Pydantic 强校验 JSON 输出。server/query/repository.py保存候选 SQL、Prompt hash、模型参数和 token 用量。- 候选 SQL 到 S03 才经过 AST 校验与只读执行。
真实 DeepSeek 冒烟使用 deepseek-v4-flash,共消耗 4,517 tokens。Fake API 冒烟将相同追踪字段写入了控制库。格式错误、空 SQL、输出过长、限流和超时均有离线失败测试。
V1-S03 跟读:SQL 怎样被验证和执行
SafeQueryPipeline.run
├── generation → QueryOutcome(PROCESSING, candidate_sql)
├── validation → SQLValidator.validate(candidate_sql)
│ ├── parse(dialect="postgres") 拒绝非 PostgreSQL 语法
│ ├── _reject_write_nodes 拒绝 DDL/DML/COPY/Transaction
│ ├── _validate_tables 拒绝跨 schema / 超范围表
│ ├── _validate_columns 拒绝超范围字段
│ └── _validate_functions (23 个白名单) 拒绝 pg_sleep 等危险函数
└── execution → AsyncpgReadOnlyExecutor.execute(validated_sql)
├── asyncio.timeout(effective_ms) asyncio 层超时
├── asyncio.Semaphore 并发门槛
├── transaction(readonly=True) 只读事务
├── SET LOCAL statement_timeout 数据库层超时
└── LIMIT max_rows+1 截断检测核心设计点:ValidatedSQL 是值类型(@dataclass(frozen=True)),执行器端口只接受它, 未经校验的字符串在接口层被物理隔离,不靠运行时判断。
实际通过的正反例(tests/test_sql_safety.py,18 passed):
- 正例:
SELECT COUNT(*) FROM fact_order、CTE 只读、EXISTS 子查询、多表 JOIN - 反例:DELETE、注释包裹的 UPDATE、多语句 + DROP、SELECT INTO、写 CTE、COPY、跨 schema 表、不存在字段、
pg_sleep - 管道集成:
test_invalid_sql_never_reaches_executor用 Mock 断言校验失败时 executor.execute 调用次数为 0
V1-S04 跟读:用户端怎样展示结果
apps/web/app/page.tsx是"use client"单页,覆盖 7 种状态(提问 / 处理中 / 成功表格 / 空结果 / 澄清 / 拒绝 / 失败)。formatCell(value, type)处理类型映射:Decimal 不转 JS Number(避免精度损失);时间戳用Asia/Shanghai时区;NULL 显式标注。ResultSummaryBuilder.build(server/generation/answer.py)只读 columns/rows,不调模型,不可能输出与表格不一致的数字。- 防重复提交:
requestSequence计数器,旧回调序列号不匹配时静默丢弃。 - XSS 防护:所有动态内容经 React JSX 渲染(无
dangerouslySetInnerHTML);SQL 在<pre><code>中展示。 - 空结果固定显示"没有符合当前条件的记录。这不等同于指标数值为零。"
V1-S05 跟读:Trace 与失败归因怎样工作
SafeQueryPipeline.run向query_record写入stages、execution_ms、referenced_tables、referenced_columns。server/api/trace.py提供三个端点:列表(支持 status / failure_category 过滤)、详情、PATCH review(写 failure_category / review_note / reviewed_at)。server/observability/failure.py的suggest_failure_category给出自动初始标签,管理员在apps/admin/app/traces/page.tsx可以覆盖。server/evaluation/baseline.py的select_v1_cases用 SQLValidator 过滤分母,保证分母与白名单能力匹配;BaselineReport同时记录样本数、分母和环境,不能省略分母宣称成绩。
实测指标与环境
| 指标 | 值 | 环境 |
|---|---|---|
| 全仓测试 | 183 passed,1 warning | macOS arm64,Python 3.12.12,pytest 8.4.2 |
| ruff check | All checks passed | — |
| mypy | 65 source files,no issues | — |
| apps/web TypeScript 构建 | ✓(512 ms) | Next.js 16.3.4,Turbopack |
| apps/admin TypeScript 构建 | ✓(463 ms) | Next.js 16.3.4,Turbopack |
| alembic current | c814be6d3a92(head) | PostgreSQL 17,Docker |
| 人工复核标签(数据库) | 10 条写入 | PATCH /api/v1/admin/traces/{id}/review |
| syntax_valid_rate | 61.54%(8 / 13) | deepseek-v4-flash,v1-sql-generation-001,development |
| execution_correct_rate | 30.77%(4 / 13) | 同上 |
| first_success_rate | 47.37%(9 / 19) | 同上,19 题(30 题中 11 题超出白名单被过滤) |
| latency p50 / p95 | 1189 ms / 2927 ms | 同上 |
| total_tokens | 85,634(均 4,507 / 题) | 同上 |
基线报告全文:
artifacts/evaluation/v1-baseline.json(2026-09-09)
失败分布:自动标注 vs 人工复核
评测器自动标注与人工复核存在 3 处差异,体现了自动规则的局限:
| 题号 | 自动标注 | 人工复核 | 说明 |
|---|---|---|---|
| test-009 | wrong_aggregation | wrong_join | 根因是 Join 扇出,不是聚合逻辑 |
| test-010 | wrong_aggregation | wrong_value | 模型触发澄清,根因是值映射缺失 |
| test-012 | wrong_aggregation | undetermined | 模型对明确题要求澄清,无 SQL 可定位 |
人工复核后最终分布(10 条失败,分母 19 题):
| 分类 | 数量 | V2 行动 |
|---|---|---|
| wrong_metric | 2 | 注入语义模型注册表 |
| wrong_value | 2 | 补充字段值说明(refund_status、gross_amount) |
| undetermined | 2 | test-012 待复测;test-030 属 V4 行级权限 |
| wrong_aggregation | 1 | few-shot 示例补充 |
| wrong_join | 1 | few-shot 多表 Join 示例 |
| wrong_time | 1 | 注入业务时钟约束(business_clock=2026-06-30) |
| wrong_column | 1 | Prompt 示例强化字段存在性 |