LobeHub ux-audit 实战:以 Eval 评测模块为例的三层 UX 审计方法与证据回灌闭环
本文以 LobeHub 仓库中 ux-audit skill 的 Eval 模块审计案例 为主体,完整还原一次针对「评测(Eval)模块」五个界面的标准化 UX 审计:从模式盘点、亮点与缺口分级,到源码级证据印证(SWR onSuccess-only 初始化、return null 空窗、导入向导等)。读完本文,你能掌握一套可重复执行的「静态读码 → 视觉截图 → 动态驱测」三层审计流程,以及审计结论如何回灌(回灌)为团队 ux 检查清单的闭环机制,可直接套用到自己的前端模块审查上。
1. 审计框架:ux-audit skill 是什么
该案例出自 ux-audit skill 定义。这个 skill 的目标是对单个界面(surface)做一次可重复、有标准依据的 UX 审查,其评判基准是两样东西的组合:
- 《Designing Interfaces》(Jenifer Tidwell)模式语言——回答「一个好界面由哪些模式构成」,即 pattern-catalog.md 中的模式清单(导航、布局、输入、命令与动作、复杂数据呈现、反馈、入门引导、视觉风格八大族);
- LobeHub 的
uxskill 检查清单——回答「一条流程应当如何表现」(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-130、dataset/action.ts:35-67、run/action.ts:151-209、testCase/action.ts:42-72,均为 2026-07 快照行号)。各消费方挂死形态不同:- overview 假空态——读 SWR
isLoading;失败 → 空列表 → 展示「创建你的第一个 benchmark」入门引导(eval/index.tsx:81-95); - 侧边栏永久骨架——门控在
benchmarkListInit(仅成功置位)(_layout/Sidebar/Body/BenchmarkList.tsx、benchmark/initialState.ts:17); - bench 详情永久骨架——
if (!benchmark) return <Skeleton>(bench/[benchmarkId]/index.tsx:114); - run / case / dataset 详情永久白屏——
if (!record) return null(runs/[runId]/index.tsx:85、cases/[caseId]/index.tsx:79、datasets/[datasetId]/index.tsx:202)→ 违反 Feedback §4.2 + Read §1.1(error 先于 empty)。模块旗舰 ❌ 案例。
- overview 假空态——读 SWR
- 🟠 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:220、runs/[runId]/features/RunHeader/index.tsx:231、runs/[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加载,在侧边栏却经 storebenchmarkListInit→ 失败时行为分叉(假空态 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 永远是 false、isLoadingBenchmarkList 永远是 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 / onRetry(mutate 重取)——即快照时点的缺口 A 在 overview 这一层已被部分修复,且页面通过 SWRConfig value={{ suspense: false }}/eval/index.tsx#L272-L283) 显式退出路由级 suspense,使三个分区可以独立报错、独立重试,而不被首个失败请求替换成整页错误。这说明该案例作为「输出形状模板 + 历史快照」的定位是准确的:复验时应逐条重新对线,而不是照抄 2026-07 的行号。
5.3 导入向导与 Run 状态机
案例的两个写侧亮点同样可以在代码中找到对应物:DatasetImportModal 目录/eval/features/DatasetImportModal/) 内 UploadStep.tsx → MappingStep.tsx 的两步结构,与「上传进度、解析错误 toast、映射校验门控、导入锁」的描述一致;run 执行状态机则由 run slice 驱动,cases/[caseId] 与 runs/[runId] 下的 feature 组件(RunHeader、IdleState、CaseResultsTable)分别承载 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 条款。
- Feedback §4.2 —— 全模块
- 验证了既有规则:§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 强调**「审计在发现落地时才完成」**,三步缺一不可:
- 具体 bug → 修复最高 🔴,或挂在「UX Audit」父问题下按页建子问题;
- 可泛化缺口 → 回灌
ux(强制):每条超出本界面的发现都要加强一条ux检查清单条目(规则 + 对应模块的 ✅/❌ 示例,并镜像一行到 ux Quick review),引用被审计界面作为 ❌ 案例。「回灌」是审计「持续」的部分——每次运行都让检查清单比使用前更锋利;若某次确实没有可泛化缺口,也要在报告里显式说明(如本案例); - 示范级好案例 → 回灌
ux规则本身:好案例只有在「教会规则新东西」时才值得落地——提炼出当前规则未言明的子规则(例如 Fleet 案例把「重排滚动触发有两种形态」「滚动轴跟随列表方向」提炼进 Read §1.3)。 - 审计本身 → 存为
references/example/<page>.md,供下一次运行作模板——本案例 eval.md 与同目录的 home.md、fleet.md 等 20+ 份案例共同构成模板库。
审计与 ux skill 因此是闭环:ux 是审计的度量基准,审计是让 ux 保持诚实的机制;跳过回灌会把审计降格为一次性 review。
8. 把这套方法套用到自己的模块:操作清单
从本案例可提炼一份可直接执行的复跑清单(严格对应 L1 程序 的 4 步):
- 定类:给界面命名「类」(列表/详情/向导/同意页……),列出该类成熟产品的期望能力清单,带着它进 L1;
- 圈范围:钉住路由(
src/routes/**)与它委托的 feature 组件(src/features/**),枚举用户所见区块,记录每个区块的数据获取机制与 empty/loading/error/retry 四态的存在性(带file:line); - 盘点模式:逐族走 pattern-catalog,产出
模式 | 位置 | 评级 | 备注表;本案例提示优先查 Feedback(失败/重试缺失)与 Input(草稿安全) 两族——这是该代码库反复偏弱的族; - 对清单审计状态:重点跑五条高收益检查——「loading 可失败」(盯 init 标志是否仅在成功时置位,即本案例的
benchmarkListInit模式)、「空态 vs 失败 vs 未加载」是否混淆、「草稿是否跨重载持久化」、「confirm→in-progress→done/error 是否齐备」、「实时流是否有新项信号且不乱序」; - 分级记录:按 🔴/🟠/🟡 标尺排序;无法从代码下结论的判词标「pending L2/L3」,交下一层确认——永远不要从
variantprop 勾选「主按钮唯一」这类视觉判词; - 报好消息:每个 ✅ 亮点给出
file:line与「为何承重」,作为下次重构的回退清单与严重度校准基线; - 落地:修 🔴、回灌
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 模块。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00