ソースを参照

feat: 问数金标准评测集 + 迭代路线文档

- scripts/eval-ask.js(npm run eval):隔离湖直跑工具链,15 例口径断言
  (加权比率 0.0709 vs 算术平均 0.0567 可区分、环比可比集剔除新开店、
  缺数按实算、层间核对、同行对标、跨主体、报告钳制、参数防呆),
  末尾附真机金标准问句清单;上线即抓到 query_report 参数键不一致真 bug
- 修复:query_report 门店过滤参数双键兼容(文档键 shops / 历史键 shopNames),
  此前按文档传参会被静默忽略导致门店过滤失效
- docs/问数迭代路线.md:行业实践对照的 P0/P1/P2 迭代路线
- package.json:补 smoke / eval 脚本入口
KenZ1117 2 週間 前
親
コミット
3ccd6c9ff7
4 ファイル変更、323 行追加、2 行削除
  1. 40 0
      docs/问数迭代路线.md
  2. 3 1
      nl-query.js
  3. 3 1
      package.json
  4. 277 0
      scripts/eval-ask.js

+ 40 - 0
docs/问数迭代路线.md

@@ -0,0 +1,40 @@
+# 智能问数 · 迭代路线(行业最佳实践对照)
+
+> 2026-10 定稿。对照 2026 年 Data Agent/ChatBI 业界共识(语义层+指标层为可信问数基础设施、
+> 评测金标准是从「能演示」到「可规模化」的分水岭、动态 few-shot 与反馈飞轮提准确率),
+> 结合本产品定位(单机部署 / flash 模型 / 零依赖 / 餐饮多主体运营)排定优先级。
+
+## 当前成熟度定位
+
+已具备(不需要推翻):
+- 受控工具调用而非裸 Text2SQL(业界生产共识路线)+ 语义层雏形(REPORT_COLUMNS 列注册表)
+- 验证层:verdict 卡(体检/恒等式/层间一致性/完整性/云端一致性)+ 数字溯源 post-check
+- 可审计:运行留痕与回放(编排时间线);评测基建雏形(NAPOS_AGENT_MOCK 确定性假模型 + 隔离湖)
+
+## P0(下一迭代,高价值 × 低成本)
+
+| 项 | 内容 | 落点 | 状态 |
+| --- | --- | --- | --- |
+| 评测金标准集 | 真实业务问句 → 期望工具/参数/关键数字的口径断言;改 prompt/工具/模型后跑一遍即知退化 | `npm run eval`(= scripts/eval-ask.js,隔离湖直跑工具链,零网络)。已覆盖 15 例:TopN/按日趋势/加权比率/门店部分匹配/环比可比集/显式区间与缺数实算/体检/层间核对/同行对标/源侧字段/跨主体/报告钳制/参数防呆 + 真机问句清单 | ✅ 2026-10-17(上线即抓到 query_report shops/shopNames 参数键不一致的真 bug) |
+| 主动式分析闭环 | autoSync 后自动 体检+层间核对+环比 → 钉钉群机器人晨报卡片(verdict 天然带可信度标签) | 复用 autoSync 调度 + agent 工具 + aitable 凭据 | ⬜ |
+| 动态 few-shot + 问答复用 | 人工追问收敛的问答对沉淀为「已验证语料」;相似问命中直接复用查询参数(秒回零成本),未命中注入 few-shot | sessions.json 扩展 + 相似度匹配(先编辑距离/关键词,够用) | ⬜ |
+
+## P1(答题质量与成本)
+
+| 项 | 内容 | 落点 |
+| --- | --- | --- |
+| 指标层升级 | 列注册表 → 指标对象(维度×聚合×时间粒度×口径),支持基于基础指标动态派生(同店环比/7日移动平均/周内节律归一),模型引用指标 key 而非自组口径 | REPORT_COLUMNS 旁新建 metrics 注册表,query/compare 工具参数引用 |
+| Schema linking | 门店清单不再 300 行常驻 prompt:按问题检索 top-k 门店 + 别名表(俗称/简称);指标说明按相关性裁剪 | prompt.js + 新工具 resolve_shops |
+| 模型路由与结构化解码 | 快问 flash / 深度分析可配更强模型;网关支持 JSON mode 时动作协议走结构化输出;流式打字机 | llm.js + context.js 模型配置 |
+| 反馈闭环 | 答案卡「采纳/纠错」,纠错进入评测集与 few-shot 池(数据飞轮) | ask.js + sessions.json |
+
+## P2(平台化,按需)
+
+- 统计异常检测:移动平均 ±kσ、季节修正寒暑假基线,verdict 新增「统计异常」类;LMDI 归因从趋势模板抽成 agent 工具
+- 深度报告 agent:周报自动生成(plan → 并行子查询 → 汇总 HTML/钉钉文档)
+- 物化聚合:按月/店/日预聚合物;换查询引擎沿用既定触发条件(单主体 >50MB / 卡顿 / 跨主体 SQL)
+- 治理:多用户按主体授权、run 成本展示与预算联动(tokens 已捕获)
+
+## 不追的方向
+
+向量库/嵌入式检索(同行均值已在 raw 里)、裸 Text2SQL 兜底(破坏口径可控)、大而全 BI 平台化(单机工具定位)。

+ 3 - 1
nl-query.js

@@ -103,11 +103,13 @@ function sanitizeQuery(raw, shops, dateRange) {
   const mode = ['detail', 'shop', 'date'].includes(q.mode) ? q.mode : 'shop';
   const from = normDay(q.from) || daysAgoYmd(30);
   const to = normDay(q.to) || daysAgoYmd(1);
+  // 门店名参数双键兼容:shopNames(两段式 prompt 的原始键)/ shops(工具清单文档键)
+  const shopNames = Array.isArray(q.shopNames) ? q.shopNames : (Array.isArray(q.shops) ? q.shops : undefined);
   const clamped = {
     mode,
     from: from < dateRange.from ? dateRange.from : from,
     to: to > dateRange.to ? dateRange.to : (to > from ? to : dateRange.to),
-    shopIds: matchShopsByName(shops, q.shopNames),
+    shopIds: matchShopsByName(shops, shopNames),
     metrics: (Array.isArray(q.metrics) ? q.metrics : ['grossAmt'])
       .map(String).filter((k) => COLUMN_KEYS.has(k)).slice(0, 4),
     sort: COLUMN_KEYS.has(q.sort) ? q.sort : 'grossAmt',

+ 3 - 1
package.json

@@ -3,7 +3,9 @@
   "version": "1.0.0",
   "description": "智慧运营中台(淘宝闪购专业版)",
   "scripts": {
-    "start": "node server.js"
+    "start": "node server.js",
+    "smoke": "node scripts/smoke.js",
+    "eval": "node scripts/eval-ask.js"
   },
   "dependencies": {
     "jpeg-js": "^0.4.4",

+ 277 - 0
scripts/eval-ask.js

@@ -0,0 +1,277 @@
+#!/usr/bin/env node
+'use strict';
+
+/**
+ * 智能问数 · 金标准评测(口径回归)
+ * ----------------------------------------------------------------------------
+ * 在隔离数据湖上直跑工具链(sanitize → handler),对真实业务问句断言
+ * 「期望工具 + 期望关键数字」。专测口径:加权比率、可比门店集、缺失日、
+ * 层间一致性、同行对标、跨主体对比与参数防呆。零网络、零真实模型。
+ *
+ * 与 smoke 的分工:smoke 管「端点行为没坏」,本脚本管「数字算得对不对」——
+ * 改 prompt / 工具 / 口径 / 换模型前后都应跑一遍:node scripts/eval-ask.js
+ *
+ * 末尾附「真机金标准问句清单」:接真实模型后人工抽查的多轮与表述变体。
+ */
+
+const fs = require('fs');
+const os = require('os');
+const path = require('path');
+
+const ROOT = path.resolve(__dirname, '..');
+
+// ---------- 隔离湖夹具(数字为断言而设计) ----------
+// 主体 eval「金标准主体」:一食堂(101)/二食堂(102)@第一餐厅 + 三食堂(103)@第二餐厅
+// 本期 20261001-1007(7 天全齐);上期 0924-0930 中 0925 全体缺数、二食堂 0927 缺数;
+// 三食堂仅本期 1004-1007 有数(模拟新开店,测可比集剔除)。
+// 关键期望值:compare 可比集(一+二) 10500 vs 7900(+32.9%);加权进店率 0.0709
+// (算术平均会是 0.0567,故意设计两套算法可区分)。
+
+function mkRow(date, scope, ids, m) {
+  return {
+    date, scope,
+    shopId: ids.shopId || '', shopName: ids.shopName || '',
+    chainId: ids.chainId || '', chainName: ids.chainName || '',
+    bizHours: null, abnormalCloseHours: null, goodsSaleAmt: null,
+    syncedAt: new Date().toISOString(),
+    validOrderCnt: m.orders || 0, incomeAmt: m.income || 0, grossAmt: m.gross || 0,
+    expenseAmt: m.expense || 0, expUv: m.uv || 0, orderUserCnt: m.buyers || 0,
+    clkExpRate: m.clk || 0, ordClkRate: 0, perActualPay: 0,
+    actOrdCnt: 0, actSubsidyAmt: 0, shopRoi: 0, rebuyRate30d: 0,
+    raw: {
+      gross_amt: m.gross || 0, valid_order_cnt: m.orders || 0,
+      gross_amt_mall_avg: 800,
+      gross_amt_mall_avg_rank: m.rank != null ? m.rank : 9,
+      valid_order_cnt_diff: m.orders || 0,
+    },
+  };
+}
+
+function buildFixture(lakeDir) {
+  const now = new Date().toISOString();
+  const rows = [];
+  const put = (date, scope, ids, m) => rows.push(mkRow(date, scope, ids, m));
+  const A = { shopId: '101', shopName: '一食堂', chainId: '9', chainName: '第一餐厅' };
+  const B = { shopId: '102', shopName: '二食堂', chainId: '9', chainName: '第一餐厅' };
+  const C = { shopId: '103', shopName: '三食堂', chainId: '8', chainName: '第二餐厅' };
+  const curDays = ['20261001', '20261002', '20261003', '20261004', '20261005', '20261006', '20261007'];
+  const prevDays = ['20260924', '20260926', '20260927', '20260928', '20260929', '20260930'];
+  const Acur = { gross: 1000, income: 800, expense: 200, orders: 100, uv: 2000, clk: 0.10, rank: 1 };
+  const Aprev = { gross: 900, income: 720, expense: 180, orders: 90, uv: 1800, clk: 0.10, rank: 1 };
+  const Bcur = { gross: 500, income: 400, expense: 100, orders: 40, uv: 1000, clk: 0.02, rank: 4 };
+  const Bprev = { gross: 500, income: 400, expense: 100, orders: 40, uv: 1000, clk: 0.02, rank: 4 };
+  const Cm = { gross: 300, income: 240, expense: 60, orders: 30, uv: 600, clk: 0.05, rank: 6 };
+  for (const d of curDays) {
+    put(d, 'shop', A, Acur);
+    put(d, 'shop', B, Bcur);
+    put(d, 'chain', { chainId: '9', chainName: '第一餐厅' }, { gross: 1500, income: 1200, expense: 300, orders: 140, uv: 3000, clk: 0.08 });
+    if (d >= '20261004') {
+      put(d, 'shop', C, Cm);
+      put(d, 'chain', { chainId: '8', chainName: '第二餐厅' }, { gross: 300, income: 240, expense: 60, orders: 30, uv: 600, buyers: 30, clk: 0.05 });
+    }
+    const c = { gross: d >= '20261004' ? 1800 : 1500, income: d >= '20261004' ? 1440 : 1200, expense: d >= '20261004' ? 360 : 300, orders: d >= '20261004' ? 170 : 140, uv: d >= '20261004' ? 3600 : 3000, clk: 0.08 };
+    put(d, 'company', { chainId: '-1' }, c);
+  }
+  for (const d of prevDays) {
+    put(d, 'shop', A, Aprev);
+    if (d !== '20260927') {
+      put(d, 'shop', B, Bprev);
+      // 连锁/公司行 = 门店合计(一 900 + 二 500)
+      put(d, 'chain', { chainId: '9', chainName: '第一餐厅' }, { gross: 1400, income: 1120, expense: 280, orders: 130, uv: 2800, clk: 0.08 });
+      put(d, 'company', { chainId: '-1' }, { gross: 1400, income: 1120, expense: 280, orders: 130, uv: 2800, clk: 0.08 });
+    } else {
+      // 二食堂缺数日:连锁/公司行 = 仅一食堂
+      put(d, 'chain', { chainId: '9', chainName: '第一餐厅' }, { gross: 900, income: 720, expense: 180, orders: 90, uv: 1800, clk: 0.10 });
+      put(d, 'company', { chainId: '-1' }, { gross: 900, income: 720, expense: 180, orders: 90, uv: 1800, clk: 0.10 });
+    }
+  }
+  fs.writeFileSync(path.join(lakeDir, 'entities.json'), JSON.stringify({ entities: [
+    { key: 'eval', firstSeen: now, lastSeen: now, merchantId: 'eval', name: '金标准主体', chainCount: 2, shopCount: 3,
+      chains: [{ chainId: '9', chainName: '第一餐厅' }, { chainId: '8', chainName: '第二餐厅' }],
+      shops: [
+        { shopId: '101', shopName: '一食堂', chainId: '9', chainName: '第一餐厅' },
+        { shopId: '102', shopName: '二食堂', chainId: '9', chainName: '第一餐厅' },
+        { shopId: '103', shopName: '三食堂', chainId: '8', chainName: '第二餐厅' },
+      ] },
+    { key: 'evalb', firstSeen: now, lastSeen: now, merchantId: 'evalb', name: '对照主体', chainCount: 1, shopCount: 1,
+      chains: [{ chainId: '8', chainName: '第二餐厅' }],
+      shops: [{ shopId: '201', shopName: '样本店', chainId: '8', chainName: '第二餐厅' }] },
+  ] }));
+  fs.mkdirSync(path.join(lakeDir, 'eval'), { recursive: true });
+  const byMonth = (list) => JSON.stringify({ version: 2, updated: now, rows: list });
+  fs.writeFileSync(path.join(lakeDir, 'eval', 'daily-202609.json'), byMonth(rows.filter((r) => r.date < '20261000')));
+  fs.writeFileSync(path.join(lakeDir, 'eval', 'daily-202610.json'), byMonth(rows.filter((r) => r.date >= '20261000')));
+  // 对照主体:本期每天 100 元(跨主体对比第二名)
+  fs.mkdirSync(path.join(lakeDir, 'evalb'), { recursive: true });
+  const bRows = curDays.map((d) => mkRow(d, 'shop', { shopId: '201', shopName: '样本店', chainId: '8', chainName: '第二餐厅' }, { gross: 100, income: 80, expense: 20, orders: 10, uv: 300, clk: 0.03 }));
+  fs.writeFileSync(path.join(lakeDir, 'evalb', 'daily-202610.json'), byMonth(bRows));
+}
+
+// ---------- 评测执行 ----------
+
+const results = [];
+function check(name, fn) {
+  try { fn(); results.push({ name, ok: true }); }
+  catch (e) { results.push({ name, ok: false, error: e.message }); }
+}
+function expect(cond, msg) { if (!cond) throw new Error(msg); }
+function near(a, b, tol) { return Math.abs(a - b) <= (tol || 1e-6); }
+
+function main() {
+  const lakeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'genpic-eval-'));
+  process.env.GENPIC_LAKE_DIR = lakeDir;
+  buildFixture(lakeDir);
+
+  // env 先行,require 在后:确保湖指向隔离目录
+  const svc = require(path.join(ROOT, 'napos-service.js'));
+  const nlQuery = require(path.join(ROOT, 'nl-query.js'));
+  const registry = require(path.join(ROOT, 'server/agent/tools/registry.js'));
+  const meta = svc.store.getEntity('eval');
+  const ctx = { svc, entityKey: 'eval', meta, shops: meta.shops || [], dateRange: nlQuery.lakeDateRange('eval', svc) };
+
+  const run = (toolName, args) => {
+    const tool = registry.get(toolName);
+    expect(tool, `工具未注册:${toolName}`);
+    return tool.handler(ctx, tool.sanitize(args || {}, ctx));
+  };
+  const runRaw = (toolName, args) => {
+    const tool = registry.get(toolName);
+    return tool.sanitize(args || {}, ctx);
+  };
+
+  // ---- 查询口径 ----
+  check('TopN 排行:一食堂 7000 > 二食堂 3500(二食堂上期缺数不影响本期)', () => {
+    const out = run('query_report', { mode: 'shop', from: '20261001', to: '20261007', sort: 'grossAmt', limit: 2 });
+    expect(out.data.rows.length === 2, `行数 ${out.data.rows.length}`);
+    expect(out.data.rows[0].shopName === '一食堂' && out.data.rows[0].grossAmt === 7000, JSON.stringify(out.data.rows[0]));
+    expect(out.data.rows[1].shopName === '二食堂' && out.data.rows[1].grossAmt === 3500, JSON.stringify(out.data.rows[1]));
+  });
+  check('按日趋势:7 天、缺数日不含三食堂时合计正确', () => {
+    const out = run('query_report', { mode: 'date', from: '20261001', to: '20261007', metrics: ['grossAmt', 'validOrderCnt'] });
+    expect(out.data.rows.length === 7, `行数 ${out.data.rows.length}`);
+    const d3 = out.data.rows.find((r) => r.date === '20261003');
+    const d5 = out.data.rows.find((r) => r.date === '20261005');
+    expect(d3.grossAmt === 1500 && d5.grossAmt === 1800, `1003=${d3.grossAmt} 1005=${d5.grossAmt}`);
+    expect(out.data.summary.grossAmt === 11700 && out.data.summary.validOrderCnt === 1100, JSON.stringify(out.data.summary));
+  });
+  check('加权比率:进店率按曝光加权 0.0709(算术平均 0.0567 是错的)', () => {
+    const out = run('query_report', { mode: 'shop', from: '20261001', to: '20261007', metrics: ['clkExpRate'] });
+    const w = out.data.summary.clkExpRate;
+    expect(near(w, 0.0709, 1e-6), `加权进店率 ${w}(期望 0.0709)`);
+    expect(Math.abs(w - 0.0567) > 0.001, '与算术平均不可区分,加权实现可疑');
+  });
+  check('门店部分匹配:问「二」只出二食堂', () => {
+    const out = run('query_report', { mode: 'shop', from: '20261001', to: '20261007', shops: ['二'] });
+    expect(out.data.rows.length === 1 && out.data.rows[0].shopName === '二食堂' && out.data.rows[0].grossAmt === 3500,
+      JSON.stringify(out.data.rows));
+  });
+
+  // ---- 同比环比口径 ----
+  check('环比:可比集剔除新开店,10500 vs 7900(+32.9%)', () => {
+    const out = run('compare_periods', {});
+    const gross = out.data.deltas.find((d) => d.key === 'grossAmt');
+    expect(gross.cur === 10500 && gross.prev === 7900, `环比数字 ${JSON.stringify(gross)}`);
+    expect(near(gross.deltaPct, 0.3291, 0.0005), `变化率 ${gross.deltaPct}`);
+    expect(out.data.comparableShopCount === 2, `可比门店 ${out.data.comparableShopCount}`);
+    expect(out.data.opened.length === 1 && out.data.opened[0].shopName === '三食堂', '新开店未单列');
+    expect(out.data.missingDays.length === 0, '本期不应缺日');
+  });
+  check('环比 verdict:恒等式通过 + 可比集警告(有新开店)', () => {
+    const out = run('compare_periods', {});
+    const vs = out.data.verdicts || [];
+    expect(vs.filter((v) => v.kind === 'identity' && v.status === 'ok').length === 2, '恒等式 verdict 异常');
+    const cov = vs.find((v) => v.kind === 'coverage');
+    expect(cov && cov.status === 'warn' && /新出现/.test(cov.detail), `coverage verdict 异常:${JSON.stringify(cov)}`);
+  });
+  check('显式区间环比:等长平移取上期,缺数按实算', () => {
+    const out = run('compare_periods', { from: '20261001', to: '20261003' });
+    const gross = out.data.deltas.find((d) => d.key === 'grossAmt');
+    // 基期 = 0928-0930(三天全齐):一 2700 + 二 1500 = 4200;本期 = 一 3000 + 二 1500 = 4500
+    expect(gross.cur === 4500 && gross.prev === 4200, `环比数字 ${JSON.stringify(gross)}`);
+    expect(near(gross.deltaPct, 0.0714, 0.0005), `变化率 ${gross.deltaPct}`);
+  });
+  check('显式区间环比:上期缺数日按实算不虚构(0925 全体缺数、0927 二食堂缺数)', () => {
+    const out = run('compare_periods', { from: '20260926', to: '20260928' });
+    const gross = out.data.deltas.find((d) => d.key === 'grossAmt');
+    // 本期窗口 0926-0928:1400 + 900 + 1400 = 3700;基期 0923(无)/0924(1400)/0925(无) = 1400
+    expect(gross.cur === 3700 && gross.prev === 1400, `环比数字 ${JSON.stringify(gross)}`);
+  });
+
+  // ---- 质量与校验 ----
+  check('数据体检:缺 1/14 天为 info、无全零误报', () => {
+    const out = run('data_quality', {});
+    expect(out.summary.includes('缺 1/14'), `缺日未识别:${out.summary}`);
+    expect(out.summary.includes('未发现全零行'), `全零误报:${out.summary}`);
+    expect(!out.summary.includes('[fail]'), `健康湖误报 fail:${out.summary}`);
+    expect((out.data.verdicts || []).length >= 5, 'verdicts 未产出');
+  });
+  check('层间核对:13 天 × 双指标全一致', () => {
+    const out = run('cross_check', {});
+    expect(out.data.checked === 52 && out.data.mismatchCount === 0 && out.data.severe === false,
+      `checked=${out.data.checked} mismatch=${out.data.mismatchCount}`);
+  });
+
+  // ---- 同行对标与源侧 ----
+  check('同行对标:排名升序、自身日均与同行均值正确', () => {
+    const out = run('peer_benchmark', { field: 'gross_amt' });
+    const shops = out.data.shops;
+    expect(shops.length === 3, `门店数 ${shops.length}`);
+    expect(shops[0].shopName === '一食堂' && shops[0].rankAvg === 1 && shops[0].peerAvg === 800, JSON.stringify(shops[0]));
+    expect(near(shops[0].own, 953.85, 0.01), `自身日均 ${shops[0].own}(期望 (6×900+7×1000)/13=953.85)`);
+    expect(shops[2].rankAvg === 6, `末位排名 ${shops[2].rankAvg}`);
+  });
+  check('源侧字段:raw_inspect 按包含匹配取到 mall 均值与排名', () => {
+    const out = run('raw_inspect', { date: '20261001', shops: ['一食堂'], keys: ['gross_amt'] });
+    expect(out.data.found && out.data.hasRaw && out.data.matched.length >= 3, JSON.stringify(out.data));
+    expect(out.data.values.gross_amt_mall_avg === 800, `均值 ${JSON.stringify(out.data.values)}`);
+  });
+
+  // ---- 跨主体与报告 ----
+  check('跨主体对比:两主体按本期营业额降序', () => {
+    const out = run('cross_entity', {});
+    expect(out.data.entities.length === 2, `主体数 ${out.data.entities.length}`);
+    expect(out.data.entities[0].name === '金标准主体' && out.data.entities[0].cur.gross === 11700, JSON.stringify(out.data.entities[0]));
+    expect(near(out.data.entities[0].grossDeltaPct, 48.1, 0.05), `环比 ${out.data.entities[0].grossDeltaPct}`);
+  });
+  check('分析报告:URL 组装且区间被钳制', () => {
+    const out = run('build_report', { from: '20200101', to: '20991231' });
+    expect(out.data.url.includes('entity=eval') && out.data.url.includes('from=20260924') && out.data.url.includes('to=20261007'),
+      out.data.url);
+  });
+
+  // ---- 参数防呆 ----
+  check('防呆:不存在的门店/非法字段/缺日期必须拒绝', () => {
+    for (const [tool, args] of [
+      ['compare_periods', { shops: ['不存在的店'] }],
+      ['peer_benchmark', { field: 'no_such_field' }],
+      ['raw_inspect', {}],
+    ]) {
+      let rejected = false;
+      try { runRaw(tool, args); } catch { rejected = true; }
+      expect(rejected, `${tool} 未拒绝非法参数:${JSON.stringify(args)}`);
+    }
+  });
+
+  // ---- 报告 ----
+  const failed = results.filter((r) => !r.ok);
+  console.log('\n========== 问数金标准评测 ==========');
+  for (const r of results) console.log(`${r.ok ? '✓' : '✗'} ${r.name}${r.ok ? '' : '  ← ' + r.error}`);
+  console.log(`\n通过 ${results.length - failed.length}/${results.length}`);
+
+  console.log('\n—— 真机金标准问句清单(接真实模型后人工抽查,覆盖表述变体与多轮) ——');
+  console.log('1. 上月营业额Top5门店 → 查询排行,数字与湖一致');
+  console.log('2. 最近7天营业额和单量趋势 → 按日折线,7 个点');
+  console.log('3. 本期比上期营业额变化多少?主要受哪些门店影响 → 可比集口径 + 主因店');
+  console.log('4. 二食堂最近进店转化率怎么样 → 加权比率(对照表格页数字)');
+  console.log('5. 那一食堂呢(追问) → 上下文门店切换');
+  console.log('6. 做一次数据体检,看看有没有问题 → 分级问题清单');
+  console.log('7. 哪家店同行对标掉队了 → peer_benchmark + 排名变化');
+  console.log('8. 几个食堂里哪家最近表现更好 → cross_entity');
+  console.log('9. 云端和本地的数据一致吗 → aitable_verify');
+  console.log('10. 给我出一份上周的经营报告 → build_report 链接可打开');
+
+  try { fs.rmSync(lakeDir, { recursive: true, force: true }); } catch { /* ignore */ }
+  if (failed.length) process.exit(1);
+}
+
+main();