# 问数结果 · 分析助手(文字 + 可选图表)设计 **日期:** 2026-09-12 **状态:** 已实现(2026-09-12) **关联需求:** D-12 子集(问数页可视化)· 不实现 D-09 多轮 · NL2SQL 路径不变 --- ## 1. 目标与边界 ### 1.1 用户故事 分析员在问数工作台先 NL 问数得到表格;再打开**分析助手对话框**,用自然语言说明要什么——可能只要文字解读、只要可视化、或两者都要。大模型根据**当前表格结构 + 用户分析提示词**决定输出形态与图表类型(如时间序列 → 折线图),前端渲染可简单互动的图表。 ### 1.2 必须覆盖的三类意图(分析阶段) | 用户意图 | 问数阶段 | 分析对话框提交后 | | --- | --- | --- | | **只要表** | 点「问数」即可 | **不打开**分析助手;无额外 LLM | | **只要分析(文字)** | 先有表 | `answer` 有内容,`chart=null` | | **只要图** | 先有表 | `chart` 有 spec,`answer` 可为空或一句说明 | | **要表 + 分析(文+图)** | 先有表 | `answer` + `chart` 同时存在 | 「表」指问数 SQL 执行结果表格,始终在问数成功后展示;分析助手**不再执行 SQL**。 ### 1.3 明确不做(本期) - D-09 多轮追问 / Redis 会话上下文 - 在主问数输入框里混合「画图」语句(避免误走 NL2SQL) - 把 chart spec 沉淀为 D-11 资产 - D-12 看板卡片钻取进对话 - 替换行情详情页 `ProductNavChartPanel`(Chart.js) --- ## 2. 交互设计 ### 2.1 问数工作台(`AnalystQueryPage.tsx`) 1. **主输入 +「问数」**:行为不变,`POST /api/analyst/chat`,`interpret=false`。 2. 成功/降级且有行时展示结果表格、SQL、溯源/转人工等现有能力。 3. 原「分析该数据」单按钮 → **「分析助手…」**,打开 **Modal**(或小 Drawer): - 说明:基于**本轮问数问题 + 下方表格**,不会重新查库。 - **分析提示词**(必填):多行输入。 - Placeholder 示例轮换: - 「用三句话总结这张表的关键结论」 - 「把各产品按日期的净值趋势画成可悬停折线图」 - 「总结要点并画一张适合这张表的图」 - 主按钮:**「生成分析」**(loading 态)。 4. 提交后在表格下方新增区域(可与原「数据解读」Card 合并): - 有 `answer` → 文字 Card + disclaimer - 有 `chart` → `ChartCard` + `AnalystResultChartPanel` 5. **新开一轮问数**时清空分析结果(与现清空 interpret 一致)。 6. **门禁**:`status ∈ {success, degrade}` 且 `table.rows.length > 0` 才可打开对话框;否则 Tooltip 提示先问数。 ### 2.2 与「只要表」的关系 用户若仅需表格:**问数后不点分析助手**即可。无需新开关;顶栏 Alert 可补一句「需要解读或图表时点分析助手」。 --- ## 3. API 契约 ### 3.1 新增:`POST /api/analyst/analyze` | 项 | 说明 | | --- | --- | | 鉴权 | 与 `/api/analyst/chat` 相同(`deps` + analyst RBAC) | | 请求 | 扩展问数快照 + 分析提示词 | ```json { "question": "上轮 NL 问数原问题", "analysis_prompt": "用户在对画框里输入的内容", "status": "success", "answer": "", "table": { "columns": [], "rows": [] }, "sql": "...", "trace_id": "trace-...", "meta": { "row_count": 0, "...": "..." } } ``` | 字段 | 约束 | | --- | --- | | `question` | 上轮问数问题(审计关联) | | `analysis_prompt` | 1~2000 字,trim 后非空 | | 其余 | 与现有 `InterpretRequest` 一致,客户端提交当前页快照 | ### 3.2 响应:扩展 `AnalystResponse` 新增字段: ```typescript type ChartType = 'line' | 'bar' | 'column' | 'pie' | 'none' type ChartSpec = { chart_type: ChartType title: string reason?: string // 模型简述为何选此图型(展示在 subtitle) x_field: string | null // pie 可为 null y_fields: string[] // 一个或多个度量列 series_field?: string | null // 多系列折线/柱:系列维度列 } // AnalystResponse 增: analysis_kind: 'text' | 'chart' | 'both' | 'none' chart: ChartSpec | null ``` | `analysis_kind` | 条件 | | --- | --- | | `text` | 有 `answer`,无有效 chart | | `chart` | 有 chart,`answer` 可选短说明 | | `both` | 同时有 | | `none` | 失败/空(应极少;降级时用 `degrade` + message) | `table` / `sql` / `meta`:原样回传客户端快照(便于前端不丢上下文);**不**因分析而改表数据。 ### 3.3 保留 `/api/analyst/interpret` - 一期实现 **`/analyze` 后前端改走 analyze**;`/interpret` 保留兼容旧客户端,内部可委托 analyze(`analysis_prompt` 默认「请解读下表」)或标记 deprecated(文档说明即可)。 --- ## 4. 后端逻辑(`analyst_agent.py`) ### 4.1 新入口 `analyze(req, auth) -> AnalystResponse` 1. 权限:`assert_analyst_query_access`(与 interpret 相同)。 2. 终态 `clarify/deny/error/escalate`:echo 快照,不调用 LLM。 3. 空表:返回 `degrade`,`answer` 说明无数据可分析。 4. 行数上限:**500 行**(可配置 `ANALYST_CHART_MAX_ROWS`);超出 → `degrade`,提示缩小问数范围或加条件。 5. 构造 LLM 消息: - System:角色 + JSON schema + 图表类型选择规则 + 禁止编造未出现在表中的数值。 - User:`question`、`analysis_prompt`、列名、行数、`data_as_of`、SQL 一行、表格 **前 30 行 TSV**(超 30 行注明「仅展示样本」)。 6. LLM 输出 **单 JSON 对象**(temperature=0);解析失败 → 重试 1 次 → 仍失败 `escalate` / `INTERPRET_ERROR`。 7. **`validate_chart_spec(spec, columns, row_count)`**(新模块或 `analyst_chart.py`): - `chart_type` 枚举内;`none` 表示不要图。 - 所有 field 名 ∈ `columns`(大小写按列名精确匹配)。 - `y_fields` 非空(当 chart 非 none);pie 时 `y_fields.length === 1` 且需 `series_field` 或 x 为分类。 - 可选:检测 x 列是否日期型(≥80% 可 parse)→ 建议 line + time,但 **最终 chart_type 以模型为准**,校验只拦非法映射。 8. 文字解读:若用户 prompt 明显要文字或 `both`,生成/保留 `answer`;若仅要图,`answer` 可为 `reason` 一句。 9. **数字护栏**:若 `answer` 含数字,复用现有 `_generate_verified_answer` / guard 逻辑(与 interpret 同路径);纯 chart 无 prose 数字可跳过 guard。 10. 审计:`log_audit(event_type=analyst_analyze, input_summary={question, analysis_prompt, analysis_kind, chart_type}, trace_id)`。 ### 4.2 LLM 图表选择规则(写入 system prompt 摘要) - 日期/时间 + 数值 → 优先 **line** - 分类 + 单指标比较 → **bar** / **column** - 占比、少量分类 → **pie**(分类 ≤ 12) - 无法安全绑列 → `chart_type: none` + `answer` 解释需要什么列 - 用户明确说「不要图」→ `none` - 用户明确图型时优先遵从,若与数据结构严重不符则在 `reason` 说明并选最接近合法类型 ### 4.3 NL2SQL **不修改** `run()` 的模板/LLM SQL 生成、sql_guard、execute 路径。 --- ## 5. 前端实现 ### 5.1 API 层(`web/src/api/analyst.ts`) - `postAnalystAnalyze(token, payload)` → `AnalystChatResponse & { analysis_kind, chart }` - 类型 `ChartSpec` 与后端一致 ### 5.2 组件 | 中文职责 | 路径 | | --- | --- | | 分析助手 Modal + 结果区编排 | `AnalystQueryPage.tsx` | | ChartSpec + table → @ant-design/charts | `AnalystResultChartPanel.tsx` | | 列类型推断、长表 series pivot | `web/src/utils/analystChartSpec.ts` | ### 5.3 图表库 - 使用 **`@ant-design/charts`**(与 Dashboard 一致)。 - 互动:**tooltip**、**legend** 切换;折线支持 smooth optional。 - x 为日期字符串时:parse 为 Date,axis formatter `MM-DD` / 自动。 ### 5.4 布局规则 | `analysis_kind` | UI | | --- | --- | | `text` | 仅文字 Card | | `chart` | 仅 ChartCard(title + reason) | | `both` | 文字在上或左,图在下(移动端纵向堆叠) | --- ## 6. 错误与降级 | 场景 | status | 用户可见 | | --- | --- | --- | | chart spec 校验失败 | `degrade` | 文字说明 + 仍展示原表 | | LLM JSON 无效 | `escalate` | 与现 interpret 失败一致 | | 行数 > 500 | `degrade` | 提示缩小问数 | | 客户角色 self 域 | 同 interpret | disclaimer + AI 风险提示 | --- ## 7. 测试计划 ### 7.1 pytest - `validate_chart_spec`:合法 line / 非法列名 / pie 多 y / 空 y - `analyze()` mock LLM:返回 text-only、chart-only、both;guard 误杀 degrade - API route 401/403 与 chat 一致 ### 7.2 Vitest - `analystChartSpec.ts`:宽表 pivot、日期 x、多 series - 可选:Panel smoke render with fixture table ### 7.3 手工 Demo 1. 问数:「各产品最近 30 天每日净值」→ 表有 `nav_date` + `nav` + `product_name` 2. 分析助手:「画趋势折线,按产品分系列」→ 多系列折线 + tooltip 3. 「三句话总结」→ 仅文字 4. 「总结并画图」→ both 5. 问数后不点助手 → 仅表 --- ## 8. 文档与 memory(实现后) - `MEMORY.md` / `FRAMEWORK.md`:问数页增 analyze + chart - `docs/答辩/DEMO-SOP-问数.md`:加 30 秒可视化演示步骤 - `REQUIREMENTS`:可挂 D-12 子能力脚注(非完整看板钻取) --- ## 9. Spec 自检(2026-09-12) - [x] 无 TBD 占位 - [x] 「只要表」= 不问 analyze,与「只要图/文/both」不矛盾 - [x] NL2SQL 与 analyze 边界清晰 - [x] 单迭代可交付(API + agent + 页 + 校验 + 测试) --- **请评审本文件。** 确认或修改后回复,将进入 **writing-plans** 生成实现任务清单(仍不自动 commit,除非你要求)。