目标:移除经营分析页右下角问数机器人弹窗,新增独立「智能问数」页面;问数能力从 「单发 NL2Query」升级为类 Codex / WorkBuddy 的数据智能体 —— 带 harness(编排契约)
- loop(工具循环)架构,专注数据处理与分析:交叉比对、同比环比验证、数据体检、 同行对标。基于现有「本地数据湖 + 钉钉 AI 表格」双层存储寻最优解,参考 ai-fde 的副驾(copilot-delegation)与 agent runtime 模式。
| 层 | 位置 | 现状 |
|---|---|---|
| 前端 | public/reports.js:1292-1470 + public/index.html:768+ |
右下角 FAB 弹窗,仅 reports/analysis 视图显示;localStorage 按主体存 20 条;ECharts 渲染 +「应用到报表」跳转数据中台 |
| 路由 | server/routes/reports.js:22 |
POST /api/reports/ask 单发同步接口 |
| 问数 | nl-query.js(334 行) |
两段式:①语义层 prompt → 模型输出查询 JSON → sanitizeQuery 钳制 → 本地湖执行;②Top≤10 行 digest 回传模型写一句结论,失败降级模板句 |
| 语义层 | napos-service.js REPORT_COLUMNS |
19 列注册表:key/label/同义词/口径说明/预留列标记 |
| 数据湖 | reports-store.js |
JSON 按月分片,行键 date\|scope\|chainId\|shopId;scope=company/chain/shop/summary;全字段落库(每行 raw 含环比差值 _diff、同行均值/排名 _mall_avg(_rank)、上期对比 _last_period,约 60+ 字段) |
| 云端 | server/services/aitable.js |
钉钉 AI 表格三表(门店明细/连锁汇总/公司汇总),pushLake/pullLake 水位同步 |
| 模型 | server/context.js makeChatCfg |
OpenAI 兼容 /v1/chat/completions,deepseek-v4.1-flash(默认)/ glm-5.3-flash,stream:false,文本内嵌 JSON(extractJson),未用原生 function calling |
_diff/_mall_avg/_last_period、syncs 同步日志、bench 同行对标、跨主体 crossData)全都触达不到。summarizeRows 的加权口径:进店率按曝光加权、下单率按进店加权 —— 这是防「算术平均比率」错误的关键,agent 一切聚合必须走它,不允许模型自己算。sanitizeQuery 的钳制哲学:日期钳在湖内区间(T-1 结算)、指标白名单、limit 上限。REPORT_COLUMNS + 同义词表。/api/reports/bench(同行对标)、ensureCrossData(跨主体对比)、report-html.js(HTML 周报)、aggregateReport/queryReport。syncState/syncSnapshot 的任务状态机模式、syncs.json 的留痕模式 —— agent 运行态照此办理。package.json 仅图像编解码两个依赖,LLM 调用手写 HTTP。智能体全程延续零依赖。| ai-fde | 本方案 |
|---|---|
copilot-delegation.ts 双模式:direct(点名工具直调,成本 ~1/20)vs agent(完整 harness) |
「快问」保留 nl-query 单发;「深度分析」走 loop。双模式并存,同一页面切换 |
runtime.ts 主循环 + 循环侦测(回注重复提醒而非硬杀) |
loop.js 同款:相同 tool+args 重复 → 注入提醒 → 再犯才强制收敛 |
harness-modes(auto/plan/research/…,模式定温度/预算/确认闸门) |
收敛为两模式:快问(低成本)/ 深度分析(loop),温度 0.2 |
tool-registry 清单求交防幻觉工具名 + tool-guard 风险级 |
tools/registry.js 同款:模型输出工具名先与清单求交,再过参数 sanitize |
harness-trace 审计事件(tool.completed outcome / agent.error,成败都留痕)+ turn-assessment 零 token 规则打分 |
trace.js 事件流 + 落盘;「数字溯源 post-check」沿用零 token 规则哲学 |
| 委派永不抛出、失败折叠进结构化结果 + EventLog | run 状态机失败折叠为 {status:'failed', error},前端可读 |
目标
非目标
┌─ 前端 public/ask.js + index.html(view=ask) ─────────────────────────┐
│ 会话列表 │ 对话流:用户消息 / 工具步骤折叠条 / verdict 卡 / 图表表格卡 │
│ │ 底部 composer:模式(快问/深度) · 发送 · 停止 │
└──────────────┬──────────────────────────────────────────────────────┘
│ POST /api/agent/runs → {runId};GET /api/agent/runs/:id 轮询
┌─ server/agent ─▼────────────────────────────────────────────────────┐
│ index.js 运行注册表(syncState 模式:running/steps/cancel) │
│ loop.js while 循环:prompt→解析→护栏→执行→观察→(提醒/收敛) │
│ llm.js chatCompletion(自 nl-query 抽出共用)+ JSON 解析与修复 │
│ guard.js 工具清单求交、参数 sanitize、预算(轮次/调用/时长)、循环侦测│
│ trace.js 事件流 + runs 落盘 + 数字溯源 post-check(零 token 规则) │
│ tools/ registry + lake_overview/query_report/compare_periods/ │
│ make_chart/cross_check/data_quality/peer_benchmark/… │
│ skills/ 周期对比 · 数据体检 · 同行对标 · 下钻归因(prompt playbook)│
└──────────────┬──────────────────────────────────────────────────────┘
│
┌──────────▼──────────┐ ┌──────────────────────────────┐
│ 本地数据湖 reports- │ │ 钉钉 AI 表格 aitable.js │
│ store(计算缓存层, │ │ (在线主档案/共享层,三表) │
│ raw 全字段落库) │ │ P3: aitable_verify 湖↔云比对 │
└─────────────────────┘ └──────────────────────────────┘
沿用 extractJson 已被验证的路子(两个 flash 模型、temperature 0 下稳定),结构化为单一动作对象:
{"thought": "先看整体趋势,再定位异常门店",
"action": {"tool": "query_report", "args": {"mode": "date", "from": "20261001", "to": "20261007", "metrics": ["grossAmt","validOrderCnt"]}}}
结束回合:
{"thought": "数据齐了,二店环比下滑主因是曝光下降",
"final": {"answer": "markdown 文本",
"chart": {"type": "line", "x": "date", "y": ["grossAmt"]},
"applied": {…同现有 applied 形状,供「应用到报表」…},
"verdicts": ["v1", "v3"], "sources": ["t1", "t2", "t4"]}}
准备:系统提示(角色 + 语义层 + 工具清单 + skills 打法 + 输出契约 + 今天/湖区间)
+ 会话历史(压缩后)+ skills 注入
while (true):
1 调 LLM(快问直连路径除外)
2 解析动作 → guard 校验(清单求交 / sanitize / 预算)
3 执行工具 → 观察(rows 裁剪 Top≤20 + summary;全量留在 artifacts)
4 观察 appended;循环侦测:同 tool+规范化args 连续重复 → 注入提醒;第三次强制 finish
5 final 或触发预算上限(maxTurns/maxToolCalls/超时)→ 强制 finish 或折叠失败
收尾:trace 落盘、数字溯源 post-check、状态机置 done/failed/cancelled
cancelRequested,工具间隙检查(照抄 requestSyncCancel)。nl-query.ask(2 次 LLM 调用),回答快、成本 1/3。适合「昨天营业额多少」「上周 Top5」。server/agent/index.js 持有 Map<runId, state>,state 形状仿 syncSnapshot(status/steps/error/startedAt/finishedAt)。POST /api/agent/runs(body: entity, question, mode, sessionId)→ {runId},立即返回GET /api/agent/runs?entity= 会话与历史 run 列表GET /api/agent/runs/:id → 进度 + 事件数组(前端 1s 轮询,模式与任务中心一致)POST /api/agent/runs/:id/cancelGET /api/agent/sessions?entity=data/agent/<entityKey>/runs/<runId>.json(事件数组:llm.call/tool.completed{outcome,durationMs}/verdict/final),sessions.json 索引 cap 30。成败都留痕(ai-fde EventLog 哲学:失败无留痕时问题不会有任何信号)。| 工具 | 职责 | 复用/说明 |
|---|---|---|
lake_overview |
主体/门店清单、湖内日期范围、各月行数、最近同步时间与成败 | 入口必调:先知道有什么数据再问;多机场景提示「先云端拉取」 |
query_report |
明细/按店/按日查询 + 排序 + TopN + 区间合计 | 内核 = queryReport/aggregateReport/summarizeRows(加权口径);参数走 sanitizeQuery 同源钳制 |
compare_periods |
环比/同比:本期 vs 基期(等长平移,支持任意偏移) | 门店集合对齐(开/闭店列出)、缺失日对齐说明、比率加权 delta |
cross_check |
层间一致性:每日 sum(shop)≈chain 行、sum(chain)≈company 行;恒等式:收入+支出=营业额、营业额/订单≈客单价;日期连续性 | 容差可配;直接命中「shop 全零」这类已踩的坑(见 §8) |
data_quality |
体检:全零行、缺失日、预留列 null、syncs 最近失败/告警 | 同步步骤里已有 nonZero===0 → warn 的先例,升级为工具 |
raw_inspect |
取某行 raw 的指定字段(_diff/_last_period/_mall_avg/_rank) |
源侧真值交叉验证:本地算的环比 vs 取数接口自带的环比差值 |
peer_benchmark |
同行对标:门店×天 自身值/同行均值/排名,跨期排名变化 | 内聚 /api/reports/bench 逻辑为服务函数供两端复用 |
make_chart |
guard.clampChart 按语义层白名单钳制;图表数据取自本运行最后一次查询产物(rows/curByDate/curByShop),模型不传数据只传规格 |
复用 sanitizeQuery 的 chart 钳制思路 |
aitable_verify(P3) |
湖↔云:行数比对 + 按行键抽样值 diff | 只读拉取(记录接口无限流),复用 aitable.js pullLake 的读路径 |
cross_entity(P3) |
跨主体(跨校)本期/基期对比 | 内聚 ensureCrossData 逻辑 |
工具出参统一带 digest(给模型的压缩观察)与 data(给前端的完整产物)两路。
| skill | 打法(注入的步骤约束) | 阶段 |
|---|---|---|
| 周期对比分析 | overview → 本期+基期 query → compare_periods → cross_check 抽查 → 图表 → 结论必答「变化量/变化率/主因店」 | P2 |
| 数据体检 | data_quality 全量 + cross_check 全量 → 出体检报告(分级问题清单 + 指引:如全零行建议跑 scripts/diagnose-napos.js) | P2 |
| 同行对标 | peer_benchmark → 排名恶化店下钻逐日 → raw_inspect 看同行均值是否整体下移 | P3 |
| 下钻归因 | TopN → 锁定异常店逐日 → 定位拐点日 → 区分自身问题 vs 大盘问题(_diff/_mall_avg) |
P3 |
skills 触发:用户点页面快捷 chip 或模型在规划回合自选(最终回答的 final.verdicts/sources 契约不变)。
每个校验产出结构化判定:
{"id":"v1","kind":"identity|cross-layer|freshness|period|peer",
"target":"2026-10 全部门店","expect":"sum(shop)=chain 行","actual":"差 0.4%",
"status":"pass|warn|fail","note":"…"}
前端渲染为 verdict 卡(绿/黄/红),结论文本必须引用 verdict id —— 「数据可不可信」和「结论是什么」并排呈现。
final 返回后,本地规则校验:从 answer 抽取数值(金额/单量/百分比),逐个在 sources 对应工具观察的数值集合中做容差匹配;匹配不到的标注 ⚠ 未溯源 展示(不阻断,但可见)。防编造的底线,纯字符串/数值匹配,零额外模型成本。
summarizeRows 同源实现。dataQualityNote 的空缺标注逻辑)。raw._diff(源侧环比差值)比对,超容差 → warn。「shop 行全零、chain 行正常」的挂起 bug 是本方案的活教材:cross_check 的层间一致性会把这类问题在每次问数时自动暴露为 fail verdict,而不是等人工发现数字不对。配套两点:
scripts/diagnose-napos.js)。data-view="ask"),页面全高(100vh)。window.shanReportsApplyQuery 跳数据中台明细 tab)。.chatbot-*。旧 localStorage 对话不迁移(价值低,新页服务端留痕)。P1 骨架(先移除、立骨架、跑通闭环)—— 2026-10-17 已完成,smoke 86/86
/api/reports/ask 与 nl-query.js 保留(快问路径继续用);reports.js 以 window.shanReportsApplyQuery 承接「应用到报表」。server/agent/:llm.js(chatCompletion 自 nl-query 抽出共用)、loop.js、guard.js、trace.js、index.js + 4 个核心工具(lake_overview/query_report/compare_periods/data_quality);测试隔离环境变量 GENPIC_LAKE_DIR(reports-store)与 GENPIC_AGENT_DIR(trace)。server/routes/agent.js);前端 public/ask.js + viewAsk 页面 + nav「智能问数」。NAPOS_AGENT_MOCK=1 确定性假模型在隔离湖上端到端跑通 loop(四工具序列/环比数字/体检判定/用量/会话索引),加解析/钳制/循环侦测纯函数断言。P2 验证层 —— 2026-10-17 全部完成
P3 —— 2026-10-17 完成
自检记录(2026-10-17,用户要求)
预估:P1 约 6~8 个新文件 + 3 处移除;P2/P3 均为增量工具/技能注册,不动骨架。
| 风险 | 对策 |
|---|---|
| flash 模型多轮 JSON 输出不稳 | temperature 0.2、单一 JSON 动作契约、失败回注修复一次、maxTurns 兜底、mock 锁行为进 smoke |
| loop 成本高于单发(3~10 次调用) | 快问双模式并存 + 预算护栏 + 观察 digest 压缩(控制输入 token) |
| 钉钉侧限流 | aitable_verify 只读、抽样比对,不批量拉全表 |
| 多机湖不同步导致答旧数据 | lake_overview 展示最近同步时间,提示先「云端拉取」 |
| 每次问数都暴露既有全零 bug 引发困惑 | 体检报告给处置指引 + 结论加数据质量角标(§8) |
| 开放:是否要流式(SSE)体验 | P1 用轮询(与任务中心一致、smoke 友好);SSE 可后置为纯增强,接口形状不变 |
| 开放:会话是否上云(钉钉)共享 | 本期仅本地落盘;跨机共享待 aitable 场景成熟后再议 |