# 智能问数 · 智能体化方案(harness + loop) > 目标:移除经营分析页右下角问数机器人弹窗,新增独立「智能问数」页面;问数能力从 > 「单发 NL2Query」升级为类 Codex / WorkBuddy 的数据智能体 —— 带 harness(编排契约) > + loop(工具循环)架构,专注数据处理与分析:交叉比对、同比环比验证、数据体检、 > 同行对标。基于现有「本地数据湖 + 钉钉 AI 表格」双层存储寻最优解,参考 ai-fde > 的副驾(copilot-delegation)与 agent runtime 模式。 --- ## 一、审计结论 ### 1.1 现状链路 | 层 | 位置 | 现状 | | --- | --- | --- | | 前端 | `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** | ### 1.2 现状局限(升级动因) 1. **单发无循环**:一次规划定死查询,答不了「先看整体再下钻到异常店再看异常日」的复合分析;追问只靠 history 里拼接的文字摘要。 2. **无验证层**:结论句直接采信一次查询结果。shop 行全零而 chain 行正常的已知 bug(挂起中)就是例证 —— 没有任何环节做层间一致性核对;比率指标若被简单算术平均(而非加权)也不会被发现。 3. **无工具抽象**:取数逻辑写死在 nl-query 里,湖里最值钱的数据(raw 的 `_diff/_mall_avg/_last_period`、syncs 同步日志、bench 同行对标、跨主体 crossData)全都触达不到。 4. **无过程可见性**:用户只能看到最终一句话 + 图,模型选了什么区间/门店/指标,只有「应用到报表」后反向可见;出了错无法定位。 5. **无护栏**:无轮次/调用/时长预算,无循环侦测,无取消。 6. **无审计**:对话只在浏览器 localStorage,服务端零留痕,换电脑即失。 ### 1.3 必须保住的既有资产(方案全部复用) - `summarizeRows` 的**加权口径**:进店率按曝光加权、下单率按进店加权 —— 这是防「算术平均比率」错误的关键,agent 一切聚合必须走它,不允许模型自己算。 - `sanitizeQuery` 的钳制哲学:日期钳在湖内区间(T-1 结算)、指标白名单、limit 上限。 - 语义层注册表 `REPORT_COLUMNS` + 同义词表。 - 全字段落库的 raw 字段:环比/上期/同行排名的**源侧真值**,验证层直接比对,不用重算。 - `/api/reports/bench`(同行对标)、`ensureCrossData`(跨主体对比)、`report-html.js`(HTML 周报)、`aggregateReport/queryReport`。 - `syncState/syncSnapshot` 的任务状态机模式、`syncs.json` 的留痕模式 —— agent 运行态照此办理。 - 零依赖纪律:`package.json` 仅图像编解码两个依赖,LLM 调用手写 HTTP。**智能体全程延续零依赖**。 ### 1.4 从 ai-fde 借的模式(映射) | 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}`,前端可读 | --- ## 二、目标与非目标 **目标** 1. 独立「智能问数」页(经营分析分组第 4 项),会话式、过程可见、可取消、可回放。 2. harness + loop 智能体:规划 → 调工具取数 → 校验 → 结论,多步复合分析。 3. 验证层为一等公民:同比环比验证、层间交叉比对、恒等式校验、数据体检、同行对标,产出结构化 verdict 卡。 4. 内置 tools + skills 双层:tools 是可执行取数/校验动作;skills 是注入 prompt 的分析打法(playbook)。 5. 双层存储用满:本地湖为计算层(快、全字段),钉钉 AI 表格为档案层(跨机共享)—— 增加湖↔云比对工具。 **非目标** - 不引入任何 npm 依赖、不换框架、不上向量库/SQL。 - 不做通用代码执行沙箱(模型永不直接算数,聚合只走本地确定性函数)。 - 不动生成/采集/任务中心等既有模块。 - 本期不做流式打字机(沿用轮询;见 §9 开放问题)。 --- ## 三、总体架构 ```text ┌─ 前端 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 湖↔云比对 │ └─────────────────────┘ └──────────────────────────────┘ ``` --- ## 四、Harness + Loop 设计 ### 4.1 回合协议(文本 JSON,非原生 function calling) 沿用 `extractJson` 已被验证的路子(两个 flash 模型、temperature 0 下稳定),结构化为单一动作对象: ```json {"thought": "先看整体趋势,再定位异常门店", "action": {"tool": "query_report", "args": {"mode": "date", "from": "20261001", "to": "20261007", "metrics": ["grossAmt","validOrderCnt"]}}} ``` 结束回合: ```json {"thought": "数据齐了,二店环比下滑主因是曝光下降", "final": {"answer": "markdown 文本", "chart": {"type": "line", "x": "date", "y": ["grossAmt"]}, "applied": {…同现有 applied 形状,供「应用到报表」…}, "verdicts": ["v1", "v3"], "sources": ["t1", "t2", "t4"]}} ``` - 解析失败:带错误信息回注重模型修复一次,再失败即终止该轮(折叠为可读失败)。 - 工具名与注册表清单求交,未注册名 → 回注「工具不存在,可用清单:…」修正一次(ai-fde 防幻觉同款)。 ### 4.2 主循环(loop.js) ```text 准备:系统提示(角色 + 语义层 + 工具清单 + 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 ``` ### 4.3 护栏与预算(guard.js,常量起步零配置) - maxTurns=10、maxToolCalls=16、单次 LLM 45s、单 run 墙钟 120s(照抄 nl-query 超时量级)。 - 循环侦测用「提醒注入」,不硬杀(ai-fde runtime 注释里的 Codex/Claude Code 同款思路)。 - 取消:run 状态机带 `cancelRequested`,工具间隙检查(照抄 `requestSyncCancel`)。 - 工具只读:本期全部工具均为只读(湖查询/校验/图表规格),无写操作 —— 不需要 ai-fde 的风险级审批链,guard 简化为参数钳制。 ### 4.4 双模式(ai-fde direct/agent 同构) - **快问**:直接走现有 `nl-query.ask`(2 次 LLM 调用),回答快、成本 1/3。适合「昨天营业额多少」「上周 Top5」。 - **深度分析**:走 loop(3~10 次调用)。适合「环比为什么跌」「哪些店数据有问题」「对标同行谁掉队了」。 - P1 前端显式 toggle,默认深度分析;P3 加零成本规则启发自动分流(问题长度 + 指标词命中 + 是否含「为什么/对比/变化」类词)。 ### 4.5 运行态与留痕(trace.js) - 运行注册表:`server/agent/index.js` 持有 `Map`,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/cancel` - `GET /api/agent/sessions?entity=` - 落盘:`data/agent//runs/.json`(事件数组:llm.call/tool.completed{outcome,durationMs}/verdict/final),`sessions.json` 索引 cap 30。**成败都留痕**(ai-fde EventLog 哲学:失败无留痕时问题不会有任何信号)。 - 历史压缩(compaction):新回合只携带旧回合的「final 结论 + 关键数字 + 用过的区间/门店」,旧观察全部丢弃 —— 防上下文随轮次膨胀(ai-fde compaction 的简化版)。 --- ## 五、工具集设计(tools/) | 工具 | 职责 | 复用/说明 | | --- | --- | --- | | `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` | ~~独立工具~~ P1 实施调整:图表规格随 final 输出、由 `guard.clampChart` 按语义层白名单钳制;图表数据取自本运行最后一次查询产物(rows/curByDate/curByShop),模型不传数据只传规格 | 复用 sanitizeQuery 的 chart 钳制思路 | | `aitable_verify`(P3) | 湖↔云:行数比对 + 按行键抽样值 diff | 只读拉取(记录接口无限流),复用 aitable.js pullLake 的读路径 | | `cross_entity`(P3) | 跨主体(跨校)本期/基期对比 | 内聚 ensureCrossData 逻辑 | 工具出参统一带 `digest`(给模型的压缩观察)与 `data`(给前端的完整产物)两路。 ## 六、技能集设计(skills/,prompt playbook,非工具) | 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` 契约不变)。 --- ## 七、验证层(本方案的差异化核心) ### 7.1 verdict 体系 每个校验产出结构化判定: ```json {"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 —— 「数据可不可信」和「结论是什么」并排呈现。 ### 7.2 数字溯源 post-check(零 token,仿 turn-assessment 哲学) final 返回后,本地规则校验:从 answer 抽取数值(金额/单量/百分比),逐个在 `sources` 对应工具观察的数值集合中做容差匹配;匹配不到的标注 `⚠ 未溯源` 展示(不阻断,但可见)。防编造的底线,纯字符串/数值匹配,零额外模型成本。 ### 7.3 同比环比验证方法论(compare_periods 内置,模型无权偏离) 1. 基期 = 等长平移(环比)或上年同期(同比,湖内无数据时如实说明)。 2. 门店集合对齐:两期都在线的店才算可比集,开/闭店单列。 3. 比率指标一律加权(曝光/进店为权),禁止算术平均 —— 与 `summarizeRows` 同源实现。 4. 缺失日显式列出(复用趋势图表 `dataQualityNote` 的空缺标注逻辑)。 5. 交叉验证:本地计算的 delta 与该行 `raw._diff`(源侧环比差值)比对,超容差 → warn。 --- ## 八、与已知数据问题的关系 「shop 行全零、chain 行正常」的挂起 bug 是本方案的活教材:`cross_check` 的层间一致性会把这类问题在**每次问数时自动暴露**为 fail verdict,而不是等人工发现数字不对。配套两点: - 数据体检 skill 的报告对已知问题给出处置指引(诊断脚本 `scripts/diagnose-napos.js`)。 - 体检 fail 时,涉及该数据的结论自动加「数据质量受影响」角标,避免用户把平台 bug 误读为经营事实。 --- ## 九、前端页面(public/ask.js + index.html view=ask)—— 已按 FDE 副驾式布局实施 - 导航:经营分析分组第 4 项「智能问数」(`data-view="ask"`),页面全高(100vh)。 - 布局(2026-10-17 用户以 ai-fde「FDE 副驾」截图定版): - **左会话栏**:品牌头(图标+标题+「+」新建)、会话搜索框(按标题过滤)、会话列表(标题 + 最近一问预览 + 悬停删除); - **右舞台**:顶部「新对话/会话标题」+ 主体徽章 + 一行能力说明;空态为居中大标题「给问数一个目标」+ 说明 + 推荐 chips; - **底部大输入卡**:textarea(回车发送 / Shift+Enter 换行 / 自动伸缩),控制行含主体下拉、深度分析/快问 seg、停止、圆形渐变发送钮。 - 对话流元素:用户消息右气泡;工具步骤折叠条(工具名/参数摘要/耗时/结果行数,可展开看观察与原始参数);verdict 卡(P2);图表卡(ECharts,复用现有配色与格式化);「应用到报表」按钮(复用 `window.shanReportsApplyQuery` 跳数据中台明细 tab)。 - 样式遵循既有约定:配色/字体沿用应用设计令牌(暖色 + 橙 accent),组件复用 seg-control/btn/panel 体系;help 小字克制(输入卡 placeholder 一行 + 空态说明一句)。 - 移除:index.html 机器人 markup、reports.js 的 chat 区块(state/render/bindings 约 170 行)、app.js 视图可见性里的机器人逻辑、style.css 的 `.chatbot-*`。旧 localStorage 对话不迁移(价值低,新页服务端留痕)。 --- ## 十、分阶段落地 **P1 骨架(先移除、立骨架、跑通闭环)—— 2026-10-17 已完成,smoke 86/86** 1. 移除机器人(§9 清单),`/api/reports/ask` 与 nl-query.js 保留(快问路径继续用);reports.js 以 `window.shanReportsApplyQuery` 承接「应用到报表」。 2. `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)。 3. 路由(§4.5 五条,挂 `server/routes/agent.js`);前端 `public/ask.js` + viewAsk 页面 + nav「智能问数」。 4. smoke:`NAPOS_AGENT_MOCK=1` 确定性假模型在隔离湖上端到端跑通 loop(四工具序列/环比数字/体检判定/用量/会话索引),加解析/钳制/循环侦测纯函数断言。 **P2 验证层 —— 2026-10-17 全部完成** - ✅ 工具齐装(9 个):lake_overview / query_report / compare_periods / data_quality / cross_check / raw_inspect / peer_benchmark / cross_entity(跨主体对比)/ aitable_verify(湖↔云只读比对,aitable 服务新增 snapshotForVerify,未启用时优雅降级为 warn verdict); - ✅ 内置技能:skills.js 四大技能(周期对比分析/数据体检/同行对标/下钻归因),打法常驻 prompt,composer「技能」下拉点名(run.skill),GET /api/agent/skills; - ✅ 编排时间线 UI:运行头「时间 + 状态·总耗时·N 个工具·自动分流·技能」+ 逐步骤(思考·Turn N 带耗时与 in/out tokens;工具带耗时/载荷字节/行数); - ✅ verdict 体系:校验类工具产出结构化判定(quality/identity/cross-layer/freshness/coverage/cloud),loop 归档到 run.verdicts(v1..vn),前端 verdict 卡(绿/黄/红左边条); - ✅ 数字溯源 post-check:provenance.js 零 token 规则(数值抽取剔除日期/TopN 噪声,容差匹配含 %/万/亿换算),未溯源数字前端黄条警示不阻断。 **P3 —— 2026-10-17 完成** - ✅ 快问自动分流:mode=auto(前端默认档),服务端规则(>18 字或含分析类词 → 深度分析,否则快问),run.autoPicked 标记并在时间线头展示「自动→快问/深度分析」; - ✅ aitable_verify、cross_entity(见上); - ✅ HTML 分析报告:build_report 工具(复用 report-html.js 即时生成,零重复实现),final 携带 reportUrl,前端「查看分析报告」新窗口打开可打印导出 PDF;快问模式同样附带当期报告链接; - ✅ 会话管理:新建/搜索/删除/轮询续显(切页运行不中断,回来自动续接轮询)。 **自检记录(2026-10-17,用户要求)** - ✅ 修复:问数页运行中切换会话/新建会话会让 composer 卡在「停止」态(busy 未复位); - ✅ 修复:切页回来运行中的运行不会恢复轮询(现 openSession 检测 running 运行并续接); - ✅ 修复:agent 路由重复实例化 aitable 服务(server.js 已有 ctx.aitable,改为复用); - ✅ 修复:reports.js 残留死变量 lastAskApply; - ✅ 确认:cloud 行键映射 scope 已转回本地枚举,湖↔云比对逻辑成立;无 console.log/debugger 残留;无 ask-thought 等死样式引用。 预估:P1 约 6~8 个新文件 + 3 处移除;P2/P3 均为增量工具/技能注册,不动骨架。 --- ## 十一、测试与回归 - **纯函数单测进 smoke**:动作解析(含围栏/脏输出)、guard 钳制、compare_periods(fixture 行构造:比率加权、闭店对齐)、cross_check(合成全零/恒等式破坏 fixture 必须出 fail)、溯源 post-check。 - **mock 模型端到端**:NAPOS_AGENT_MOCK 固化工具调用序列 → 断言 run 状态机、事件留痕、取消、预算强制收敛。 - **红线不变**:smoke 绝不发真实 LLM 请求、不触碰 DELETE/启动类端点(沿用现有约定)。 ## 十二、风险与开放问题 | 风险 | 对策 | | --- | --- | | 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 场景成熟后再议 |