Skip to content

V1|可测量的问数基线

实现状态:已完成。V1-S01~S05 全部 16 个任务通过(2026-09-09)。

这一期学会什么:基线的价值是发现失败分布,而不是在小数据集上追求漂亮演示。

打开本期 5 个故事 / 16 个任务 · 查看总进度

为什么有这一期

限定 Sales 的 10~15 张表,跑通最小安全链路并发现真实失败。进入条件:V0 验收通过;选择并冻结 Sales 子集。

本期链路

text
Web 提问 → API → 固定 Sales Schema → Prompt/LLM → SQLGlot AST 校验 → 只读执行 → 表格 → Trace

跟读顺序

  1. 从前端 QueryRequest 跟到 API 校验,确认用户身份与 trace 如何传递。
  2. 阅读 Prompt Builder,明确固定 15 张表的上下文范围。
  3. 先用 fake Provider 理解流程,再用真实模型记录质量。
  4. 找到 AST 拒绝点、只读事务、timeout 和结果上限。
  5. 读逐题评测报告,选择一条选表错与一条指标错的样例。

必做学习实验

用一个未知列问题、一个写 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.pyQueryOrchestrator.submitSafeQueryPipeline.runSQLValidator.validateAsyncpgReadOnlyExecutor.executeResultSummaryBuilder.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 跟读:请求怎样进入固定范围

  1. server/api/query.py 校验请求并从 x-atlas-identity 取得 V1 临时身份。
  2. server/domain/query.py 定义所有状态共用的请求、响应、列、错误和 Trace 契约。
  3. server/domain/sales_scope.py 先拒绝库存等明确跨域请求;"收入"要求澄清;"销售额"绑定到 V0 指标草案 net_sales
  4. server/orchestrator/query.py 把身份、数据/schema/模型版本和超时预算组成不可变上下文。
  5. server/query/repository.py 保存请求和状态;控制库表由迁移 9cb21c73e4a1 创建。

关键失败场景是 pipeline 抛出包含连接信息的异常:编排器只返回 internal_error 和 trace_id,不会把异常原文交给用户。这里选择显式 QueryPipeline 端口,是为了让 S02 替换 Provider 时不修改编排逻辑。

V1-S02 跟读:候选 SQL 怎样生成

  1. server/generation/prompt.py 从固定 15 表白名单、指标草案和 train 示例构造版本化 Prompt。
  2. server/llm/gateway.py 隔离 DeepSeek Chat Completions,并提供可重放、可延迟的 Fake。
  3. server/generation/sql.py 在总超时预算内有限重试,再用 Pydantic 强校验 JSON 输出。
  4. server/query/repository.py 保存候选 SQL、Prompt hash、模型参数和 token 用量。
  5. 候选 SQL 到 S03 才经过 AST 校验与只读执行。

真实 DeepSeek 冒烟使用 deepseek-v4-flash,共消耗 4,517 tokens。Fake API 冒烟将相同追踪字段写入了控制库。格式错误、空 SQL、输出过长、限流和超时均有离线失败测试。

V1-S03 跟读:SQL 怎样被验证和执行

text
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 跟读:用户端怎样展示结果

  1. apps/web/app/page.tsx"use client" 单页,覆盖 7 种状态(提问 / 处理中 / 成功表格 / 空结果 / 澄清 / 拒绝 / 失败)。
  2. formatCell(value, type) 处理类型映射:Decimal 不转 JS Number(避免精度损失);时间戳用 Asia/Shanghai 时区;NULL 显式标注。
  3. ResultSummaryBuilder.buildserver/generation/answer.py)只读 columns/rows,不调模型,不可能输出与表格不一致的数字。
  4. 防重复提交:requestSequence 计数器,旧回调序列号不匹配时静默丢弃。
  5. XSS 防护:所有动态内容经 React JSX 渲染(无 dangerouslySetInnerHTML);SQL 在 <pre><code> 中展示。
  6. 空结果固定显示"没有符合当前条件的记录。这不等同于指标数值为零。"

V1-S05 跟读:Trace 与失败归因怎样工作

  1. SafeQueryPipeline.runquery_record 写入 stagesexecution_msreferenced_tablesreferenced_columns
  2. server/api/trace.py 提供三个端点:列表(支持 status / failure_category 过滤)、详情、PATCH review(写 failure_category / review_note / reviewed_at)。
  3. server/observability/failure.pysuggest_failure_category 给出自动初始标签,管理员在 apps/admin/app/traces/page.tsx 可以覆盖。
  4. server/evaluation/baseline.pyselect_v1_cases 用 SQLValidator 过滤分母,保证分母与白名单能力匹配;BaselineReport 同时记录样本数、分母和环境,不能省略分母宣称成绩。

实测指标与环境

指标环境
全仓测试183 passed,1 warningmacOS arm64,Python 3.12.12,pytest 8.4.2
ruff checkAll checks passed
mypy65 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 currentc814be6d3a92(head)PostgreSQL 17,Docker
人工复核标签(数据库)10 条写入PATCH /api/v1/admin/traces/{id}/review
syntax_valid_rate61.54%(8 / 13)deepseek-v4-flash,v1-sql-generation-001,development
execution_correct_rate30.77%(4 / 13)同上
first_success_rate47.37%(9 / 19)同上,19 题(30 题中 11 题超出白名单被过滤)
latency p50 / p951189 ms / 2927 ms同上
total_tokens85,634(均 4,507 / 题)同上

基线报告全文:artifacts/evaluation/v1-baseline.json(2026-09-09)

失败分布:自动标注 vs 人工复核

评测器自动标注与人工复核存在 3 处差异,体现了自动规则的局限:

题号自动标注人工复核说明
test-009wrong_aggregationwrong_join根因是 Join 扇出,不是聚合逻辑
test-010wrong_aggregationwrong_value模型触发澄清,根因是值映射缺失
test-012wrong_aggregationundetermined模型对明确题要求澄清,无 SQL 可定位

人工复核后最终分布(10 条失败,分母 19 题):

分类数量V2 行动
wrong_metric2注入语义模型注册表
wrong_value2补充字段值说明(refund_status、gross_amount)
undetermined2test-012 待复测;test-030 属 V4 行级权限
wrong_aggregation1few-shot 示例补充
wrong_join1few-shot 多表 Join 示例
wrong_time1注入业务时钟约束(business_clock=2026-06-30)
wrong_column1Prompt 示例强化字段存在性

延伸阅读

需求 → 代码 → 验证 → 复盘