14.3 工业化与质量的一致性
14.2 排期与美术依赖 算出了「什么时候做什么」,但排期成立还有一个隐含前提:做出来的东西是同一套规格。如果每个美术师按自己的习惯出图,那么「5 天画一批头像」这个估计就不可信——因为下一批的 5 天可能是另一种质量。这一节把「工业化」翻译成一个可算的量:一致性。
先定义清楚:工业化不是「做得好看」,而是「可重复、可预测」。 一次做到合格是手艺,每一批都合格才是工业化。判据不靠审美(审美需真实环境(真实画师与评审),命令行不可复现),而靠规格可测的维度——命名、网格、线宽、色板上限。本节的资产记录是示例参数,规格与阈值都来自可测的整数。
14.3.1 一致性:跨批稳定,而非单批合格
一个常见的误判,是拿「本批通过率」当质量指标。本批 100% 通过,不代表下一批也是——如果两批由不同的人、用不同的模板做,通过率会漂。所以工业化要看两个量:
- 批内一致性指数:本批有多少资产在全部维度上合规;
- 跨批漂移:两批(或两个小组)之间一致性指数的差。
漂移比绝对水平更能暴露问题:一个小组 843‰、另一个 937‰,差 94‰ 说明规格没有被同一套模板约束住——这正是「工业化」要消灭的方差。下面用《星屑旅店》一批 16 个资产的实测记录来算这两个量。
一致性指数为什么取四维合规率的整数平均?因为它要的是「可比较」而非「精确」:四个维度的量纲不同(正则匹配、集合隶属、等值、大小比较),唯一的共同语言是「合规比例」。平均的代价是维度等权——命名错 1 件和色板错 1 件对指数的影响一样大。真实项目可以给维度加权,但加权之后跨批比较的前提就变了,必须同步改;本节用等权,是为了让「同一套算法、两批数据」这件事最干净。
14.3.2 配置:规格与一批资产实测记录
下面是 D 层的规格与一批资产记录。规格有四个维度:命名正则、允许的网格尺寸集合、线宽、色板上限。16 个资产分两个小组(A = 场景与角色,B = UI 与特效),每行记下它的实测属性,squad 用来算跨批漂移。
{
"project": "stardust-inn",
"chapter": 14,
"note": "示例参数:本套构造的一批资产实测记录,非任何真实项目的交付数据。",
"spec": {
"namePattern": "^[a-z][a-z0-9]*(_[a-z0-9]+)*$",
"gridAllowed": [32, 64, 128, 256],
"lineWidthPx": 2,
"paletteMax": 16
},
"reworkCost100": 80,
"capacityRef100": 3570,
"assets": [
{ "id": "furn_lamp_01", "squad": "A", "role": "env", "name": "furn_lamp_01", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 12 },
{ "id": "furn_lamp_01_v2", "squad": "A", "role": "env", "name": "furn_lamp_01_v2", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 14 },
{ "id": "furn_counter_01", "squad": "A", "role": "env", "name": "furn_counter_01", "gridPx": 64, "lineWidthPx": 3, "paletteCount": 10 },
{ "id": "furn_chair_01", "squad": "A", "role": "env", "name": "furn_chair_01", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 18 },
{ "id": "furn_rug_01", "squad": "A", "role": "env", "name": "furn_rug_01", "gridPx": 48, "lineWidthPx": 2, "paletteCount": 9 },
{ "id": "portrait_innkeeper", "squad": "A", "role": "char", "name": "portrait_innkeeper", "gridPx": 128, "lineWidthPx": 2, "paletteCount": 20 },
{ "id": "portrait_guest_a", "squad": "A", "role": "char", "name": "portrait_guest_a", "gridPx": 128, "lineWidthPx": 2, "paletteCount": 16 },
{ "id": "portrait_guest_b", "squad": "A", "role": "char", "name": "portrait_guest_b", "gridPx": 128, "lineWidthPx": 3, "paletteCount": 15 },
{ "id": "ui_icon_coin", "squad": "B", "role": "ui", "name": "ui_icon_coin", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 6 },
{ "id": "ui_icon_star", "squad": "B", "role": "ui", "name": "ui_icon_star", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 5 },
{ "id": "ui_icon_heart", "squad": "B", "role": "ui", "name": "UI_Icon_Heart", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 7 },
{ "id": "ui_frame", "squad": "B", "role": "ui", "name": "ui_frame", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 4 },
{ "id": "ui_panel", "squad": "B", "role": "ui", "name": "ui_panel", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 8 },
{ "id": "vfx_burst", "squad": "B", "role": "vfx", "name": "vfx_burst", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 22 },
{ "id": "vfx_sparkle", "squad": "B", "role": "vfx", "name": "vfx_sparkle", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 11 },
{ "id": "vfx_smoke", "squad": "B", "role": "vfx", "name": "vfx_smoke", "gridPx": 64, "lineWidthPx": 2, "paletteCount": 13 }
]
}
capacityRef100: 3570 引用的是 14.1 算出的有效人天×100,用来把返工折算成产能占比。reworkCost100: 80 是「返工一件资产」的示例成本(0.80 人天)。
14.3.3 S 层:四维合规率与一致性指数
下面这段是 S 层脚本,逐资产判四个维度是否合规,汇总成四个千分比,取整数平均作一致性指数;再按 squad 分组算跨批漂移,并折算返工账:
#!/usr/bin/env python3
# file: sim/ta14_consistency.py
# tier: S
# verify: /opt/homebrew/bin/python3 sim/ta14_consistency.py
"""14.3 工业化与质量的一致性(S 层,卷二第 14 章)。
读 ``config/ta14_consistency.json`` 的规格与一批资产实测记录,算四个维度的合规率(千分比):
* 命名:匹配 ``namePattern``;
* 网格:``gridPx`` 属于 ``gridAllowed``;
* 线宽:``lineWidthPx`` 等于规格值;
* 色板:``paletteCount <= paletteMax``。
``一致性指数 = (四维合规率之和) // 4``(整数地板除)。再把同批按 ``squad`` 分组,算两组指数的
**漂移** ``|A - B|``——工业化要的不是一批内合规,而是**跨批稳定**。返工账:任一维度不合规即
``rework``,``rework100 = rework × reworkCost100``,并对比 14.1 的 ``capacityRef100``。
全整数、无浮点、无随机。落 ``out/ta14_consistency.json``。
"""
import json
import os
import re
import sys
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
CFG = os.path.join(ROOT, "config/ta14_consistency.json")
OUT = os.path.join(ROOT, "out")
DIMS = ("name", "grid", "line", "palette")
def check_asset(spec, a):
"""四个维度的合规布尔值。"""
return {
"name": bool(re.match(spec["namePattern"], a["name"])),
"grid": a["gridPx"] in spec["gridAllowed"],
"line": a["lineWidthPx"] == spec["lineWidthPx"],
"palette": a["paletteCount"] <= spec["paletteMax"],
}
def index_of(spec, assets):
"""返回 (合规率 dict, 一致性指数, 返工 id 列表)。"""
if not assets:
return {d: 0 for d in DIMS}, 0, []
hits = {d: 0 for d in DIMS}
rework = []
for a in assets:
ok = check_asset(spec, a)
for d in DIMS:
hits[d] += 1 if ok[d] else 0
if not all(ok.values()):
rework.append(a["id"])
rates = {d: hits[d] * 1000 // len(assets) for d in DIMS}
idx = sum(rates[d] for d in DIMS) // len(DIMS)
return rates, idx, sorted(rework)
def main() -> int:
cfg = json.load(open(CFG, encoding="utf-8"))
spec, assets = cfg["spec"], cfg["assets"]
rates, idx, rework = index_of(spec, assets)
squads = sorted({a["squad"] for a in assets})
per = {}
for s in squads:
r, i, rw = index_of(spec, [a for a in assets if a["squad"] == s])
per[s] = {"rates": r, "index": i, "rework": rw, "count": sum(1 for a in assets if a["squad"] == s)}
drift = abs(per[squads[0]]["index"] - per[squads[-1]]["index"])
rework100 = len(rework) * cfg["reworkCost100"]
cap = cfg["capacityRef100"]
report = {
"project": cfg["project"], "assetCount": len(assets),
"rates": rates, "consistencyIndex": idx, "reworkIds": rework,
"reworkCount": len(rework), "reworkCost100": cfg["reworkCost100"],
"rework100": rework100,
"reworkSharePermille": rework100 * 1000 // cap if cap else 0,
"capacityRef100": cap,
"squads": per, "driftPermille": drift,
"lineWidths": sorted({a["lineWidthPx"] for a in assets}),
"paletteRange": [min(a["paletteCount"] for a in assets),
max(a["paletteCount"] for a in assets)],
}
os.makedirs(OUT, exist_ok=True)
with open(os.path.join(OUT, "ta14_consistency.json"), "w", encoding="utf-8") as f:
json.dump(report, f, ensure_ascii=False, indent=2, sort_keys=True)
f.write("\n")
print(f"assets={len(assets)} index={idx}permille rework={len(rework)} rework100={rework100}")
print("rates " + " ".join(f"{d}={rates[d]}" for d in DIMS))
for s in squads:
print(f" squad {s}: n={per[s]['count']} index={per[s]['index']}permille "
f"rework={len(per[s]['rework'])}")
print(f"drift={drift}permille reworkShare={report['reworkSharePermille']}permille "
f"lineWidths={report['lineWidths']} paletteRange={report['paletteRange']}")
print(f"reworkIds={rework}")
print("wrote out/ta14_consistency.json")
return 0
if __name__ == "__main__":
sys.exit(main())
实跑 /opt/homebrew/bin/python3 sim/ta14_consistency.py:
assets=16 index=890permille rework=7 rework100=560
rates name=937 grid=937 line=875 palette=812
squad A: n=8 index=843permille rework=5
squad B: n=8 index=937permille rework=2
drift=94permille reworkShare=156permille lineWidths=[2, 3] paletteRange=[4, 22]
reworkIds=['furn_chair_01', 'furn_counter_01', 'furn_rug_01', 'portrait_guest_b', 'portrait_innkeeper', 'ui_icon_heart', 'vfx_burst']
wrote out/ta14_consistency.json
14.3.4 读账:指数、漂移与返工
- 批内一致性指数 = 890‰。四个维度里色板最松(812‰,3 件超上限:
furn_chair_0118 色、portrait_innkeeper20 色、vfx_burst22 色),线宽次之(875‰,furn_counter_01与portrait_guest_b用了 3px),命名与网格各 937‰(各 1 件:UI_Icon_Heart大小写错、furn_rug_01网格 48 不在允许集里)。 - 跨批漂移 = 94‰。A 组(场景与角色)843‰,B 组(UI 与特效)937‰。差 94‰ 说明两组用的不是同一套模板——色板与线宽是 A 组的重灾区,命名是 B 组的问题。工业化要压的就是这 94‰:把规格写进工具(12.1 的规则引擎)让两组共用同一套闸门,而不是靠人自觉。
- 返工 7 件(任一维度不合规即计入),折算
7 × 0.80 = 5.60 人天(rework100=560)。lineWidths=[2, 3]与paletteRange=[4, 22]这两个分布,直观地说明「同一批资产」在规格上的实际跨度有多大。
把 7 件不合规按维度拆开,看得更清楚:
| 维度 | 不合规件数 | 合规率 | 不合规资产(实测值) |
|---|---|---|---|
| 命名 | 1 | 937‰ | ui_icon_heart(写成 UI_Icon_Heart) |
| 网格 | 1 | 937‰ | furn_rug_01(48px 不在允许集) |
| 线宽 | 2 | 875‰ | furn_counter_01、portrait_guest_b(3px) |
| 色板 | 3 | 812‰ | furn_chair_01(18)、portrait_innkeeper(20)、vfx_burst(22) |
| 合计 | 7 | — | 7 件资产,各错一个维度 |
这张表还说明一件事:7 件资产恰好各错一个维度(没有任何一件同时错两项),所以「不合规件数 7」与「不合规维度命中 7」数值相同——若换成「一错就错一片」的数据,两者就会分叉,返工率也会随之变化。返工成本按「件」计,所以真正决定返工账的是不合规资产数,而不是维度命中数。
14.3.5 一致性如何吃掉产能:回到 14.1
把返工折算回 14.1 的产能账:5.60 人天 ÷ 35.70 有效人天 = 156‰(reworkSharePermille)。也就是说,这一批的返工吃掉了整支团队有效产能的 15.6%。如果这 7 件在第一次就合规,那 5.60 人天本可以投给 14.2 关键路径上最紧的 furniture。
这条链把本章三节串起来了:14.1 算产能 → 14.2 排依赖 → 14.3 把「不合规造成的返工」折回产能。一致性差不是审美问题,是产能泄漏:reworkSharePermille 越高,14.1 里那份利用率就越乐观。所以工业化真正的收益,是把返工率压下去,让 14.1 的有效人天尽可能落在有效产出上。
换个角度看这笔账:14.1 的利用率 1058‰ 已经说明计划排不下,返工又额外吃掉 156‰ 的产能——两者叠加,才是这支团队真实的工作强度。一致性差的项目,产能账永远比纸面更紧:你以为有效人天有 35.70,实际能落在「第一次就对的产出」上的只有 35.70 × (1 − 0.156) ≈ 30.13 人天。把返工率从 156‰ 压到 60‰,等于凭空多出 35.70 × 0.096 ≈ 3.43 人天——这是工业化最容易被低估的收益。
所以三节的关系也可以倒过来读:14.3 的返工率是 14.1 产能账的隐藏折扣,也是 14.2 关键路径的隐藏风险——返工一旦落在关键路径上(比如 furniture),整个工期会直接顺延。工业化要做的,就是把这个折扣和这个风险一起压小。
14.3.6 质量门:把一致性变成闸门
一致性指数是事后的度量。要把 94‰ 的漂移压下去,得把它变成事前的闸门:在资产进管线的路上设一道自动检查,不合规就拦住——把 spec 的四条规则写成管线里的一步,让 A、B 两组共用同一套判据,而不是各自凭习惯。
闸门放在哪里,决定它拦住什么:
| 闸门位置 | 拦住什么 | 对应维度 |
|---|---|---|
| 提交前(本地) | 命名、网格 | namePattern、gridAllowed |
| 导入时(管线) | 线宽、色板 | lineWidthPx、paletteMax |
| 出图前(评审) | 审美与构图 | 需真实环境,命令行不可复现 |
前两道能自动跑(就是本节脚本判的四个维度),第三道必须人看。闸门越靠前,返工越便宜:提交前拦住,改一个字段;进了管线才发现,要重导整条链路。把可自动的部分尽量前置,才能把 14.3.5 那 156‰ 的产能泄漏压下来。注意闸门本身也要维护——规格变了闸门要跟着变,否则它会从「保护」变成「误拦」,这正是 13.3 讨论的采纳率与维护成本问题。
14.3.7 边界
- 判据只覆盖可测维度。命名、网格、线宽、色板上限都能被脚本判定,但「这张图好不好看」不能——审美需真实环境(真实画师与评审),命令行不可复现。本节的一致性指数是必要条件,不是充分条件:全合规不等于好看。
- 阈值是示例参数。
paletteMax=16、reworkCost100=80都是本套构造的,换成别的值结论会变。真实项目要把阈值写进规范(卷一第 6 章的清单 schema),而不是散落在脚本里。 - X 层:真实交付验收要人看、要评审、要打回重做,这一步需真实环境,命令行不可复现;本节只做规格维度的整数合规率,不替人做审美判断。
阅读导航:上一节:14.2 排期与美术依赖 · 下一节:15.1 外包切分与计价模型 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。