深浅色
pivot-workbench
基于 Vue 3 + TypeScript 的「AI 数据分析 Agent 工作台」,核心是前端编排型 Agent。
一句话
面向本地表格数据的 AI 数据分析工具:用户导入 CSV/Excel,用自然语言说「按渠道看利润」,Agent 自动生成透视配置、调用浏览器侧工具执行真实聚合、读取结果摘要,最后输出带数据的 Markdown 分析报告。我独立完成了从透视引擎、Worker 计算、状态编排到 Agent 闭环的全部实现。
项目定位
这不是一个电子表格产品,而是「AI 数据分析 Agent 工作台」。最值得讲的设计是前端编排型 Agent:后端(server/index.mjs)只负责「理解目标 + 返回 JSON actions」,真实工具调用全部发生在浏览器侧。
明确不做的边界(避免面试被抬预期):单元格自由编辑、公式系统、多 Sheet、撤销重做、多人协作、服务端数据集管理。
架构
数据流:
CSV / Excel
-> parseCsv / parseSpreadsheetFile(已下沉 Worker)
-> normalizeHeaders / normalizeCellValue / inferFields
-> DataRow[] + FieldMeta[]
-> PivotConfig
-> Web Worker computePivot
-> PivotMatrixResult
-> Table / Chart / Export / PerformancePanel
Agent 闭环:
用户自然语言目标
-> buildAgentContext(字段摘要 + 样例数据 + 当前配置 + 透视结果摘要)
-> POST /api/agent/chat(phase=plan)
-> 返回 plan + actions
-> agentToolExecutor 校验并执行 action
-> runPivot:validateAgentConfig -> applyConfig -> 等 Worker 结果
-> observation 回传(phase=finalize)
-> Markdown 报告
分层理由:
- 透视引擎(utils/pivotEngine.ts)保持纯函数,UI 只负责配置和展示,便于单测
- 重计算(分组/筛选/聚合/排序)与文件解析都在 Worker,主线程只做渲染
- Agent 的工具执行器与 UI 解耦:它只依赖一组 Ref 运行时(AgentToolRuntime),方便测试和替换
我的模块
全部,文件级说明:
- src/agent/buildAgentContext.ts:构造 Agent 上下文。字段统计只扫前 10000 行(MAX_SCAN_ROWS),样例 12 行(MAX_SAMPLE_ROWS)、每字段 6 个取值(MAX_SAMPLE_VALUES)、5 个 Top 值(MAX_TOP_VALUES)、透视摘要只带 8 行(MAX_PIVOT_ROWS)。原因是全量扫描 10 万行会阻塞主线程几百 ms,而对 LLM 决策来说采样统计已经足够
- src/agent/validateAgentConfig.ts:把 LLM 返回值当作不可信输入。做字段存在性校验、聚合器白名单(sum/count/avg/min/max)、filter operator 白名单(equals/contains/between/gt/lt/notEmpty)、sort 归一化;values 为空时自动补一个默认指标并记 warning,而不是直接报错
- src/agent/agentToolExecutor.ts:5 个工具 inspectDataset / suggestPivotConfig / runPivot / readPivotResult / generateReport;单工具超时 8s;支持 AbortSignal 终止,终止时返回统一文案「分析已终止」
- src/agent/agentApi.ts:POST /api/agent/chat 封装,统一归一化后端返回(含 finalReport 兼容)
- src/composables/useAnalysisAgent.ts:多轮上下文、run 状态机(idle/planning/executing/waiting/completed/failed)、步骤时间线、失败恢复
- src/workers/pivot.worker.ts + src/utils/pivotEngine.ts:分组/筛选/聚合/排序在 Worker 执行
- server/index.mjs:/api/agent/chat,phase=plan|finalize 两段式;response_format 强制 json_object,temperature 0.2,action 白名单校验;无 OPENAI_API_KEY 时走 local-fallback(E2E 测试就靠它)
- src/db/pivotDb.ts:Dexie/IndexedDB 持久化数据集、配置、模板,localStorage 兜底
- src/composables/useBenchmarkMetrics.ts + src/utils/performanceData.ts:本地性能面板
- 质量门禁:Vitest 单测 8 个文件(agentToolExecutor / buildAgentContext / validateAgentConfig / usePivotWorker / pivotEngine / pivot.worker / cells / csv)+ Playwright E2E 2 条链路(agent-workflow、pivot-workflow)+ ESLint + GitHub Actions
难点与取舍
1. 工具调用放前端还是后端
- 问题:Agent 要执行真实聚合,谁来执行?
- 方案 A:后端执行。需要把整个数据集上传,隐私、流量、延迟三输
- 方案 B:前端执行。但 LLM 返回的配置完全不可信,可能引用不存在的字段
- 选 B,并配套一整层校验(validateAgentConfig)。代价:Agent 能力被前端暴露的工具集限制,想加能力必须先加工具
2. LLM 返回非法配置怎么办
- 方案 A:校验不过就丢弃,让用户重说 -> 体验差,LLM 经常只是漏了 values
- 方案 B:校验 + 保守归一化 + 自动补默认值 + warning 回传
- 选 B。关键是「保守」:只做白名单过滤和兜底补全,绝不臆造字段或猜聚合方式。字段不存在就报错让 Agent 重规划,而不是猜一个相近的字段
3. 失败恢复的重试预算
- action 失败后把 observation 回传,让 LLM 重新规划,但只重试一次,避免无限循环烧 token
- 失败不污染当前 pivotConfig:先校验、后 apply,非法配置根本进不到状态里
- 从失败 observation 里提取 avoidFieldKeys,提示 LLM 别再选同一个不存在的字段
4. runPivot 对同配置请求确定性超时
- 现象:相同配置重复请求时,runPivot 永远等不到结果,8s 后超时
- 原因:isResultFor 判断没覆盖「结果已经算好且配置相同」的情况,导致一直等一个不会到来的 Worker 回调
- 修法:进入等待前先比对配置是否已有结果,命中则直接复用,未命中才 applyConfig + 等 Worker
5. 终止与数据集切换守卫
- 用户点终止、或中途切数据集时,用 AbortSignal 打断在途请求
- 加守卫确保旧 run 的结果不会写回新数据集(否则会出现「分析 A 数据集的报告出现在 B 数据集上」)
6. 字段统计的采样扫描
- 全量扫描 10 万行做 distinct/top 统计会阻塞主线程几百 ms
- 改成只扫前 10000 行;rowCount 仍取全量,保证给 LLM 的规模信息不失真
量化结果
仓库里可验证的硬指标:
- 字段统计扫描上限 10000 行;上下文只带 12 行样例 / 每字段 6 个取值 / 5 个 Top 值;透视摘要只带 8 行,控制 prompt 体积
- 单工具超时 8000ms;失败重规划最多 1 次
- 全部重计算在 Web Worker,主线程只做配置与渲染;结果表用行虚拟滚动控制 DOM 数量
- 质量门禁:8 个 Vitest 单测文件 + 2 条 Playwright E2E 链路
实测(Intel i9-14900HX / 32GB / Windows,Edge headless,Vite dev server,3 次中位数):
| 数据规模 | 主线程直接算(阻塞) | 主线程最长长任务 | Worker 内聚合 | Worker 克隆传输 | Worker 计算往返 |
|---|---|---|---|---|---|
| 1 万行 | 87ms | 110ms | 88ms | - | 136ms |
| 5 万行 | 462ms | 476ms | 456ms | - | 560ms |
| 10 万行 | 870ms | 901ms | 903ms | 118ms | 950ms |
结论:
- Worker 不让聚合变快(903ms vs 870ms,同一份计算量),它消除的是阻塞:主线程最长长任务从 901ms 降到 0ms(3 次中 2 次为 0,1 次 58ms 来自模块初始化)。
- 代价是把 10 万行交给 Worker 的结构化克隆约 118ms,一次性(每个数据集一次)。
- Agent 闭环端到端:中位数 365ms(3 个目标各 3 步全部成功),服务端 plan 5-12ms、finalize 4-5ms,其余为浏览器侧工具执行与渲染;local-fallback 模式,不含 LLM 网络耗时。
- 质量门禁:8 个 Vitest 单测文件 + 2 条 Playwright E2E 链路。
如果重做
- Agent 工具协议应该更早定稿。第一版我让 LLM 直接返回 PivotConfig,结果大量非法配置;后来才改成「白名单 action + 参数校验」。如果一开始就把 action 协议设计好,validateAgentConfig 会简单很多。
- local-fallback 不该和 LLM 分支挤在一个文件。现在 server/index.mjs 里两套逻辑混着,测试和调试都别扭,应该拆成 provider 接口。
- Worker 应该支持流式进度。现在是「算完一次性返回」,大结果集时用户只能干等,可以用 postMessage 分块回传做进度条。
- 透视引擎缺一层可注册的聚合器。现在聚合函数硬编码 5 个,加「同环比」「占比」要改引擎核心,应该抽成插件式注册。