深浅色
pivot-workbench 追问
面试官可能追问的 20 个问题 + 我的答案。
Agent 设计
Q:为什么说这是「前端编排型 Agent」?和常见的后端 Agent 有什么不同? A:常规做法是后端持有工具和数据集,LLM 决定调用后由后端执行。我反过来:后端只做「目标理解 + 计划生成」,返回结构化的 JSON actions;真正的工具执行在浏览器里,由 agentToolExecutor 完成。好处是数据不用出浏览器(隐私和流量都省),聚合延迟是本地毫秒级;代价是 Agent 能力被前端暴露的工具集限制,且需要一整层校验来兜住 LLM 的非法输出。
Q:为什么不让 LLM 直接返回透视配置? A:我第一版就是这么做的,结果是大量非法配置:引用不存在的字段、聚合方式不在支持列表里、values 为空。所以我改成两层:LLM 只返回白名单 action(inspectDataset / suggestPivotConfig / runPivot / readPivotResult / generateReport),配置作为 action 的参数,再由 validateAgentConfig 做字段存在性、聚合器白名单、filter operator 白名单、sort 归一化校验。LLM 的输出永远是不可信输入。
Q:LLM 返回的配置非法时,你是报错还是自动修? A:分两类。结构性错误(字段不存在、操作符不支持)直接判非法,返回失败 observation 让 Agent 重规划。可保守修复的问题(values 为空、sort 目标非法)则归一化 + 补默认值 + 记 warning 回传给模型。原则是「只做保守修复,绝不臆造字段」——猜一个相近字段比报错更危险,用户会拿到看起来正确其实是错的数据。
Q:失败恢复为什么要限制重试次数? A:因为每次重规划都是一次 LLM 调用。如果不限次数,模型可能反复选同一个不存在的字段,陷入死循环烧 token 且用户一直等。所以我限制重规划最多一次,并且从失败 observation 里提取 avoidFieldKeys 塞进下一轮上下文,明确告诉模型「别再选这几个字段」。
Q:action 执行失败了,会不会把界面搞坏? A:不会,这是我特意设计的。执行顺序是「先校验 -> 通过才 apply」,非法配置根本进不到 pivotConfig 状态里。所以失败时用户的当前透视视图完全不变,只是 Agent 面板上多一条失败步骤。这比「应用了一半再回滚」干净得多。
Q:Agent 上下文里塞了哪些东西?会不会超 token? A:四块:字段摘要(类型、空值数、distinct 数、数值的 min/max/avg、Top 值)、前 12 行样例数据、当前透视配置、透视结果摘要(只带 8 行)。所有维度都有硬上限,且字段统计只扫前 10000 行。所以 prompt 体积是可预测的,不会随数据集增长而爆炸。
Q:为什么字段统计只扫前 10000 行?采样会不会导致判断错误? A:因为全量扫描 10 万行做 distinct 和 top 统计会阻塞主线程几百毫秒,用户能明显感到卡顿。对 LLM 决策而言,「这个字段大概是什么类型、有哪些取值」用采样足够。要注意我保留了 rowCount 的全量值,所以给模型的规模信息是准的,只是分布信息是采样的。这是明确写进注释的取舍。
Q:怎么支持「继续分析利润」这种多轮追问? A:把当前数据集上下文、当前透视配置、上一轮的执行结果都带进下一轮请求,所以模型知道「现在看的是什么」。加上步骤时间线(AgentRun.steps),用户可以回看每一轮调了什么工具、拿到什么结果。
Q:用户点「终止」时会发生什么? A:前端通过 AbortSignal 打断在途的 fetch 和工具执行,工具执行器在关键点检查 signal.aborted,已中止就返回统一的「分析已终止」observation 而不是抛异常。另外有数据集切换守卫,防止旧 run 的结果写回到新数据集上。
Q:local-fallback 是什么?为什么要有? A:没有配 OPENAI_API_KEY 时的本地降级实现。它不调 LLM,而是用规则挑维度字段和度量字段、拼出 action 列表,并生成模板化报告。三个作用:开发环境零成本跑通闭环;E2E 测试可以不依赖外网和费用;LLM 服务不可用时功能降级而不是白屏。
前端工程
Q:为什么把计算放 Web Worker? A:透视聚合在数据量大时是 CPU 密集操作,跑在主线程会阻塞渲染,用户滚动和点击都会卡。放 Worker 后主线程只负责配置和渲染,聚合在后台线程跑完再 postMessage 回来。同样地,CSV/Excel 解析和字段推断也下沉到了 Worker。
Q:Worker 通信有什么代价? A:结构化克隆是有成本的,大数组来回传会复制内存。目前数据量下可以接受;如果数据继续变大,应该用 Transferable 或 SharedArrayBuffer 来避免拷贝。这是我知道但还没做的优化点。
Q:透视引擎为什么要保持纯函数? A:纯函数意味着「相同输入必然相同输出、没有副作用」,可以直接用 Vitest 断言输入输出,不用 mock DOM 或状态。UI 层只做配置和展示,这样引擎的正确性可以独立验证,也方便以后搬到 Worker 或服务端复用。
Q:结果表数据量很大怎么办? A:用行虚拟滚动,只渲染视口内的行,DOM 节点数是常数级而不是随结果行数线性增长。另外透视结果本身是聚合过的矩阵,行数通常远小于原始数据。
Q:本地持久化为什么用 IndexedDB 还要 localStorage 兜底? A:IndexedDB 能存大数据集和结构化对象,但存在隐私模式下不可用、或者被浏览器清理的情况。localStorage 容量小(几 MB)但几乎总是可用,所以用它做小数据的降级兜底,保证基本功能不丢。
Q:为什么选 Element Plus + ECharts + SheetJS 这套? A:Element Plus 提供中后台基础组件,ECharts 覆盖柱/折线/饼三种图表需求,SheetJS 负责 xlsx 读写。这三个都是成熟库,我不重复造轮子,把精力放在 Agent 编排和透视引擎这些真正的项目价值点上。
Q:前端怎么保证质量? A:三层。类型层用 vue-tsc 做 typecheck;单测用 Vitest 覆盖 agentToolExecutor、buildAgentContext、validateAgentConfig、pivotEngine、pivot.worker、csv、cells、usePivotWorker;端到端用 Playwright 跑两条真实用户链路(agent-workflow 和 pivot-workflow)。再加上 ESLint 和 GitHub Actions,
npm run build本身就包含 typecheck。Q:E2E 测试怎么做到不依赖真实 LLM? A:靠 local-fallback 模式。测试环境不配 OPENAI_API_KEY,Agent 服务走规则化实现,返回结构稳定、可断言。这样 E2E 既快又不会因为模型输出波动而 flaky。
Q:你提到修过「懒加载引入的 e2e 竞态」,具体是什么? A:页面改成懒加载后,E2E 里点完按钮立刻断言元素,但组件 chunk 还没加载完,导致偶发失败。修法是让断言等待真正就绪的信号而不是固定延时。这也是为什么我坚持 E2E 要跑在 CI 上——本地手点不会发现这种时序问题。
Q:这个项目最大的不足是什么? A:三块。第一,Agent 的规划质量完全依赖 prompt,我没有做评测集,说不清「换了模型之后效果差多少」,这是最该补的。第二,透视引擎的聚合函数是硬编码的 5 个,加同环比要改引擎核心,应该抽成可注册的聚合器。第三,Worker 是一次性返回结果,大结果集时缺少进度反馈。