首页
/ LobeHub ux-audit 实战:以 Eval 评测模块为例的三层 UX 审计方法与证据回灌闭环

LobeHub ux-audit 实战:以 Eval 评测模块为例的三层 UX 审计方法与证据回灌闭环

2026-09-06 16:00:38作者:温玫谨Lighthearted

本文以 LobeHub 仓库中 ux-audit skill 的 Eval 模块审计案例 为主体,完整还原一次针对「评测(Eval)模块」五个界面的标准化 UX 审计:从模式盘点、亮点与缺口分级,到源码级证据印证(SWR onSuccess-only 初始化、return null 空窗、导入向导等)。读完本文,你能掌握一套可重复执行的「静态读码 → 视觉截图 → 动态驱测」三层审计流程,以及审计结论如何回灌(回灌)为团队 ux 检查清单的闭环机制,可直接套用到自己的前端模块审查上。

1. 审计框架:ux-audit skill 是什么

该案例出自 ux-audit skill 定义。这个 skill 的目标是对单个界面(surface)做一次可重复、有标准依据的 UX 审查,其评判基准是两样东西的组合:

  1. 《Designing Interfaces》(Jenifer Tidwell)模式语言——回答「一个好界面由哪些模式构成」,即 pattern-catalog.md 中的模式清单(导航、布局、输入、命令与动作、复杂数据呈现、反馈、入门引导、视觉风格八大族);
  2. LobeHub 的 ux skill 检查清单——回答「一条流程应当如何表现」(loading 可失败、空态与失败态区分、草稿安全、动作状态机、轮询流等)。

审计要回答两个问题:这个界面用了哪些模式(以及用得好不好)体验在哪里薄弱(每个缺口必须挂钩到某条检查清单)。skill 明确要求「一次审计只做一个界面」,随产品增长逐页复跑,这就是其「持续(continuous)」的含义。

1.1 三层执行模型

一次审计不是一项活动,而是三层,核心规则是**「结论必须来自能真正看到它的层」**——不能从代码勾选视觉结论,也不能从代码勾选运行时结论:

程序文件 做什么 能抓住什么 成本
L1 静态 layer-1-static.md 读代码 缺失的状态/分支(empty/error/retry)、草稿未持久化、模式缺席、结构问题 低、离线,每次必跑
L2 视觉 layer-2-visual.md 对渲染后的界面截图 真实视觉层级与主导控件、间距/对比/对齐、截断溢出、empty/loading/error 实际长什么样、响应式断点、深浅色 中;需要渲染
L3 动态 layer-3-dynamic.md 用 acceptance 框架 + 探针驱测真实用户旅程 进行中/锁定态、强制 error/empty、第 N 步是否导向 N+1、焦点/键盘、量化的 CLS/LCP/INP/long-task 高;需要运行环境与登录态

分档原则:L1 永远跑(快速全覆盖基线);发现与布局/层级/渲染态/响应式相关时加 L2;需要走旅程、强制 L1/L2 到不了的状态、或测性能时加 L3。--l1 / --l2 / --l3 参数可把一次运行限定在单层,默认 L1(提供了截图则加 L2)。

L1 层还有一条关键认知边界(见 L1 程序文件):代码阅读对「整块能力从未实现」是结构性盲的——完全缺席的功能没有 file:line 可 grep。因此 skill 要求先给界面命名「类」(class)并列出该类成熟产品的领域惯例(例如 OAuth 同意页应有切换账号路径),把「期望能力清单」带入 L1 逐项核对。Eval 案例正是这样做的:先把界面类定为「eval / experiment 平台」,对标 OpenAI Evals UI / LangSmith / Braintrust。

2. 审计对象:Eval 模块的五个界面与代码结构

案例开篇给出了审计对象地图(2026-07 的一次真实运行):Eval 模块是一个 benchmark / 评测平台,信息流为 benchmark 列表(overview)→ benchmark 详情(datasets + runs 双 Tab)→ run 详情(长时执行)→ 单个测试用例详情,外加 dataset 详情,共五个界面,采用 feature-in-route 布局置于 src/routes/(main)/eval/** 之下,数据层由 src/store/eval/slices/ 下的 {benchmark,dataset,run,testCase} 四个 slice 支撑。

对照仓库当前源码,这一结构完全属实:

  • overview 页/eval/index.tsx):benchmark / experiment / dataset 三个独立分区;
  • benchmark 详情页/eval/bench/[benchmarkId]/index.tsx) 与其 DatasetsTab/eval/bench/[benchmarkId]/features/DatasetsTab/index.tsx)、RunsTab/eval/bench/[benchmarkId]/features/RunsTab/);
  • run 详情页/eval/bench/[benchmarkId]/runs/[runId]/index.tsx)、case 详情页/eval/cases/[caseId]/index.tsx)、dataset 详情页/eval/datasets/[datasetId]/index.tsx);
  • store 侧实际还有第五个 experiment slice(案例快照时 overview 已引入 Experiments 分区)。

案例文档在开头特别声明了引用边界:「把它当作输出形状的模板,而非当前状态的真相(引用前先复验)」。这一点在本文第 5 节会得到印证——当前代码中部分结论已被修复。

3. 案例第一块:在用的模式盘点(Patterns in use)

L1 程序的第 2 步是「逐族走模式目录、给每个区块打模式标签并评级:✅ solid / ⚠️ partial-or-misused / — absent-but-expected」。Eval 案例产出的完整盘点表如下(原表完整继承):

模式(族) 位置 评级 备注
Overview + Detail(导航) overview → bench → run → case;dataset 详情 干净的多级钻取
Empty-as-onboarding(成长) overview 空态(eval/index.tsx:83-95)、Datasets/Runs Tab 空态 真实页面 + CTA
Loading Skeleton(反馈) overview SkeletonGrid 复用卡片外壳 教科书 §4.1
Failure + Retry(反馈) 每个 fetch — abs. 系统性根因(缺口 A)
详情页 Loading 态(反馈) run / case / dataset 详情 — abs. return null → 白屏(缺口 B)
Run 状态机(动作/反馈) idle / pending / external / running / finished 成熟——亮点
进度 + 实时轮询(数据) run 详情活跃期 3s 轮询 + 进度条 §1.7
实体生命周期——删除(动作) benchmark / dataset / testCase / run 删除 confirm + 成功/错误 toast
实体生命周期——创建/编辑(动作) RunCreate / RunEdit / BenchmarkEdit / BatchResume 弹窗 ⚠️ 多个缺错误 toast(缺口 F)
向导(输入) DatasetImportModal(Upload → Mapping) 进度、校验、toast——亮点
草稿安全(编辑) 全部表单弹窗 ⚠️ 仅内存态,无未保存警告(缺口 H)

判读:写侧(run 执行、删除、导入)扎实;薄弱点完全聚集在读侧——列表/详情的 error + loading——外加一条创建/编辑变更反馈不一致的带状问题。这个「模式表 + 一句话判读」正是 skill 输出格式的第 1 节要求:每个 ✅ 行必须有 file:line 证据支撑,不能是一词勾选。

4. 案例第二、三块:亮点清单与分级体验缺口

skill 明确要求「报告好的,而不仅是缺口」——亮点是发现的一等公民,因为它是「回灌循环」的 ✅ 半边,也是下一次重构的「不要回退」清单。

4.1 亮点 / 好案例(不要回退)

  • ✅ 亮点 — Run 执行状态机:idle / pending / external / running / finished,活跃期 3s 轮询 + 进度条,另有 retry-errors / batch-resume / per-case resume——每条路径都有 confirm + loading + toast。成熟到可以充当模块旗舰 ✅(act/feedback,§1.7)。
  • ✅ 亮点 — 完整删除生命周期:benchmark / dataset / testCase / run 的删除全部 confirm + 成功/错误 toast 配对——这正是不均的创建/编辑写侧(缺口 F)没能达到的基线。
  • ✅ 亮点 — DatasetImport 两步向导:DatasetImportModal/eval/features/DatasetImportModal/index.tsx)(Upload → Mapping,见 UploadStep/eval/features/DatasetImportModal/UploadStep.tsx) 与 MappingStep/eval/features/DatasetImportModal/MappingStep.tsx))具备上传进度、解析错误 toast、映射校验门控按钮、导入锁 + 成功/错误 toast——教科书式分阶段输入。
  • ✅ 亮点 — overview 空态即 CTA + 复用外壳的骨架屏:overview 空态是真实页面 + 「创建你的第一个 benchmark」CTA,其 SkeletonGrid 复用卡片外壳,实现原位 load→content 切换、无重排(教科书 §4.1)。
  • 全模块没有一处 antd Spin:loading 永远是外壳匹配的骨架而非裸 spinner——一个安静的正确性胜利,值得保留。

4.2 体验缺口(按严重度排序)

严重度共用 skill 的判级标尺:🔴 破坏信任(数据/输入丢失、卡死态、掩盖失败的假空态);🟠 死路或误导;🟡 摩擦/不一致。

  • 🔴 A — 全模块没有 error/retry;每个 fetch 只在成功时 resolve → 永久骨架 / 白屏 / 假空态。 系统性根因:每个 slice 初始化 isLoadingX: true / xInit: false,且只在 onSuccess 翻转,没有 onError,覆盖全部 9 个 fetch(benchmark/action.ts:106-130dataset/action.ts:35-67run/action.ts:151-209testCase/action.ts:42-72,均为 2026-07 快照行号)。各消费方挂死形态不同:
    • overview 假空态——读 SWR isLoading;失败 → 空列表 → 展示「创建你的第一个 benchmark」入门引导(eval/index.tsx:81-95);
    • 侧边栏永久骨架——门控在 benchmarkListInit(仅成功置位)(_layout/Sidebar/Body/BenchmarkList.tsxbenchmark/initialState.ts:17);
    • bench 详情永久骨架——if (!benchmark) return <Skeleton>bench/[benchmarkId]/index.tsx:114);
    • run / case / dataset 详情永久白屏——if (!record) return nullruns/[runId]/index.tsx:85cases/[caseId]/index.tsx:79datasets/[datasetId]/index.tsx:202)→ 违反 Feedback §4.2 + Read §1.1(error 先于 empty)。模块旗舰 ❌ 案例
  • 🟠 B — run / case / dataset 详情无 loading 态(return null → 白屏闪烁)。 即便走 happy path,首帧是白屏而非骨架。与 A 的区别:A 是没 error,B 是没 loading。→ Read §1.1(「loading 是骨架,绝不白闪」)。
  • 🟠 C — case 详情深链到不存在的 case → 永久白屏,无 not-found。 caseResult 来自 results.find(testCaseId === caseId);查不到保持 null → 白屏,没有「case not found」(cases/[caseId]/index.tsx:41-46,79)。
  • 🟠 F — 多个异步写操作缺错误 toast,与删除路径不一致。 删除路径全部 confirm + 成功/错误 toast;但 RunCreateModal、RunEditModal、BatchResumeModal、BenchmarkEditModal 失败时什么都不提示(依赖静默的 store 处理);BenchmarkEdit 还缺按钮 loading 态。同样是「提交」意图,反馈却不均。→ Act §3.1(done/error)、Feedback §4.4。
  • 🟡 D — i18n:硬编码英文字符串。 'Failed to start run' fallback ×3(RunsTab/RunCard.tsx:220runs/[runId]/features/RunHeader/index.tsx:231runs/[runId]/features/IdleState/index.tsx:122);'Awaiting for external evaluation'CaseResultsTable/index.tsx:211,且应写作 "Awaiting external evaluation");两处 DatasetEditModal placeholder(features/DatasetEditModal/Content.tsx:204,207)。
  • 🟡 E — 同一数据、两个 loading 源。 benchmark 列表在 overview 经 SWR isLoading 加载,在侧边栏却经 store benchmarkListInit → 失败时行为分叉(假空态 vs 永久骨架)。应合并为单一 loading/error/empty 源。
  • 🟡 G — 异步动作只靠轮询传达成功,无显式前向反馈。 start run / resume case / batch resume 不给成功 toast,用户要等 3s 轮询看到状态变化(runs/[runId]/index.tsx)。→ Act §3.1。
  • 🟡 H — 表单弹窗仅内存草稿;关闭时无未保存警告。 所有创建/编辑/导入弹窗误关即丢输入(features/**Modal/Content.tsx)。→ Edit §2.1(弹窗可轻量执行)。

5. 源码印证:把审计结论钉在代码证据上

skill 的第一条地面规则是「证据,而不是感觉」:每条发现必须引用证据——L1 是 file:line,L2 是「用 Read 工具核实过的截图」,L3 是捕获值/快照。本节对案例的核心结论做仓库级复验。

5.1 缺口 A 的机制:onSuccess-only 初始化

benchmark slice 的 action 当前实现清晰展示了案例描述的根因形态——列表 fetch 的初始化标志只在 onSuccess 回调中置位

useFetchBenchmarks = (): SWRResponse =>
  useClientDataSWR(evalKeys.benchmarks(), () => agentEvalService.listBenchmarks(), {
    onSuccess: (data: any) => {
      this.#set(
        { benchmarkList: data, benchmarkListInit: true, isLoadingBenchmarkList: false },
        false,
        'useFetchBenchmarks/success',
      );
    },
  });

没有任何 onError 分支。于是请求失败时 benchmarkListInit 永远是 falseisLoadingBenchmarkList 永远是 true——所有以 benchmarkListInit 为门控的消费方(如侧边栏 BenchmarkList/eval/_layout/Sidebar/Body/BenchmarkList.tsx))就停在骨架,所有读 isLoading 的消费方则在 data 为空时落入「假空态」。这就是一个「file:line 级」的系统性结论:不改 9 个 fetch 的回调契约,任何单点的 UI 修补都只是换挂死形态。

5.2 亮点的机制:复用外壳的骨架与空态门控

overview 页源码/eval/index.tsx#L73-L90) 中的 SkeletonGrid 与其注释,正是案例亮点第 4 条的活证据:

// Loading placeholder that reuses the benchmark-card chrome so loading → loaded
// is a content swap, not a relayout (ux §4.1).
const SkeletonGrid = memo(() => (
  <div className={styles.grid}>
    {[0, 1, 2, 3].map((i) => (
      <Flexbox className={styles.skeletonCard} gap={16} key={i}>
        ...

骨架卡片直接复用 styles.grid / styles.skeletonCard 与真实 BenchmarkCard 相同的栅格、边框、圆角变量,因此 load→content 是原位内容替换而非重排——这正是 Feedback §4.1「骨架要匹配文本宽度比例与卡片外壳」所要求的。

同时,空态分支的注释/eval/index.tsx#L109-L112) 写明了「error-as-empty trap」的门控纪律:

// Purpose-built onboarding empty — only reached when the fetch succeeded with
// zero benchmarks. A *failed* fetch is gated ahead of this by AsyncBoundary so
// we never invite the user to re-create benchmarks they already own (ux Read
// §1.1 error-as-empty trap).

这里正体现了案例的「引用前先复验」警告:当前 overview 页/eval/index.tsx#L172-L224) 的 benchmark 与 experiments 分区已经包裹在 AsyncBoundary 中,携带 error / onRetrymutate 重取)——即快照时点的缺口 A 在 overview 这一层已被部分修复,且页面通过 SWRConfig value={{ suspense: false }}/eval/index.tsx#L272-L283) 显式退出路由级 suspense,使三个分区可以独立报错、独立重试,而不被首个失败请求替换成整页错误。这说明该案例作为「输出形状模板 + 历史快照」的定位是准确的:复验时应逐条重新对线,而不是照抄 2026-07 的行号。

5.3 导入向导与 Run 状态机

案例的两个写侧亮点同样可以在代码中找到对应物:DatasetImportModal 目录/eval/features/DatasetImportModal/) 内 UploadStep.tsxMappingStep.tsx 的两步结构,与「上传进度、解析错误 toast、映射校验门控、导入锁」的描述一致;run 执行状态机则由 run slice 驱动,cases/[caseId]runs/[runId] 下的 feature 组件(RunHeaderIdleStateCaseResultsTable)分别承载 external-evaluation 等待态与用例结果表——这些也是缺口 D 硬编码英文字符串的出处。

6. 案例第四、五块:Skill 反馈与待办(L2/L3)

skill 输出格式的第 4 节要求区分「现有规则的真实实例」与「值得新增的可泛化规则」。Eval 案例的结论是不新增规则——Eval 是现有规则的教科书式实例:

  • 落为现有规则的 ❌ 案例(无新规则):
    • Feedback §4.2 —— 全模块 onSuccess-only / 无 onError 模式(缺口 A);
    • Read §1.1 —— Eval overview 被加入「error 先于 empty」示例(缺口 A);新增一条 ❌:详情 return null 不是 loading 态(缺口 B),外加一条 checklist 条款。
  • 验证了既有规则:§4.2 永久骨架、Read §1.1 空态 vs 失败态、Act §3.1 done/error 反馈(缺口 F/G)。

第 5 节列出未跑的层及其待验证项(L2:永久骨架/白屏实际长什么样、run 详情报告布局、benchmark 卡片栅格在窄宽度下的表现;L3:强制每个 fetch 失败以现场确认 A/B/C、跑一次真实 eval 观察 running→finished 与运行中轮询失败、强制创建/resume 失败以确认缺口 F 无 toast)。L3 的具体手段可参考 L3 程序文件:通过 CDP Network.emulateNetworkConditions 断网或按路由拦截请求来「活捉」永久骨架,用 agent-browser --cdp 9222 eval 注入 PerformanceObserver 量化 CLS/LCP/INP(阈值:CLS ≤ 0.1 好、≤ 0.25 待改进;LCP ≤ 2.5s;INP ≤ 200ms)。

7. 闭环:发现如何落地(Land the findings)

skill 强调**「审计在发现落地时才完成」**,三步缺一不可:

  1. 具体 bug → 修复最高 🔴,或挂在「UX Audit」父问题下按页建子问题;
  2. 可泛化缺口 → 回灌 ux(强制):每条超出本界面的发现都要加强一条 ux 检查清单条目(规则 + 对应模块的 ✅/❌ 示例,并镜像一行到 ux Quick review),引用被审计界面作为 ❌ 案例。「回灌」是审计「持续」的部分——每次运行都让检查清单比使用前更锋利;若某次确实没有可泛化缺口,也要在报告里显式说明(如本案例);
  3. 示范级好案例 → 回灌 ux 规则本身:好案例只有在「教会规则新东西」时才值得落地——提炼出当前规则未言明的子规则(例如 Fleet 案例把「重排滚动触发有两种形态」「滚动轴跟随列表方向」提炼进 Read §1.3)。
  4. 审计本身 → 存为 references/example/<page>.md,供下一次运行作模板——本案例 eval.md 与同目录的 home.mdfleet.md 等 20+ 份案例共同构成模板库。

审计与 ux skill 因此是闭环ux 是审计的度量基准,审计是让 ux 保持诚实的机制;跳过回灌会把审计降格为一次性 review。

8. 把这套方法套用到自己的模块:操作清单

从本案例可提炼一份可直接执行的复跑清单(严格对应 L1 程序 的 4 步):

  1. 定类:给界面命名「类」(列表/详情/向导/同意页……),列出该类成熟产品的期望能力清单,带着它进 L1;
  2. 圈范围:钉住路由(src/routes/**)与它委托的 feature 组件(src/features/**),枚举用户所见区块,记录每个区块的数据获取机制与 empty/loading/error/retry 四态的存在性(带 file:line);
  3. 盘点模式:逐族走 pattern-catalog,产出 模式 | 位置 | 评级 | 备注 表;本案例提示优先查 Feedback(失败/重试缺失)与 Input(草稿安全) 两族——这是该代码库反复偏弱的族;
  4. 对清单审计状态:重点跑五条高收益检查——「loading 可失败」(盯 init 标志是否仅在成功时置位,即本案例的 benchmarkListInit 模式)、「空态 vs 失败 vs 未加载」是否混淆、「草稿是否跨重载持久化」、「confirm→in-progress→done/error 是否齐备」、「实时流是否有新项信号且不乱序」;
  5. 分级记录:按 🔴/🟠/🟡 标尺排序;无法从代码下结论的判词标「pending L2/L3」,交下一层确认——永远不要从 variant prop 勾选「主按钮唯一」这类视觉判词
  6. 报好消息:每个 ✅ 亮点给出 file:line 与「为何承重」,作为下次重构的回退清单与严重度校准基线;
  7. 落地:修 🔴、回灌 ux、存 references/example/<page>.md

9. 关键路径速查

资源 仓库相对路径
本文主体案例(Eval 审计,2026-07 快照) .agents/skills/ux-audit/references/example/eval.md
ux-audit skill 总纲(三层模型、地面规则、严重度标尺、回灌闭环) .agents/skills/ux-audit/SKILL.md
L1 / L2 / L3 程序文件 .agents/skills/ux-audit/references/layer-1-static.md · .agents/skills/ux-audit/references/layer-2-visual.md · .agents/skills/ux-audit/references/layer-3-dynamic.md
Tidwell 模式目录(审计度量基准) .agents/skills/ux-audit/references/pattern-catalog.md
Eval overview(AsyncBoundary + 骨架/空态实现) src/routes/(main)/eval/index.tsx/eval/index.tsx)
缺口 A 证据源:onSuccess-only fetch src/store/eval/slices/benchmark/action.ts
写侧亮点:两步导入向导 src/routes/(main)/eval/features/DatasetImportModal//eval/features/DatasetImportModal/)
五个被审计界面 src/routes/(main)/eval//eval/) 下 overview / bench / runs / cases / datasets
案例模板库(20+ 份已审计界面) .agents/skills/ux-audit/references/example/

适用前提与限制:文中缺口 A~H 的行号均出自 2026-07 审计快照,案例文档自身声明「引用前先复验」;经本次对仓库当前代码的抽查,overview 层已引入 AsyncBoundary 错误/重试门控,其余条目的现状应以最新源码为准。审计方法论(三层模型、覆盖矩阵、回灌闭环)不随界面变化而失效,可直接迁移到其他 feature-in-route 模块。

登录后查看全文
热门项目推荐
相关项目推荐