舞台工艺多专业审图提速:用 AI 智能体编排 A+B+C+G 四段读图流水线
一、背景痛点:长任务里脚本为什么总"死"
舞台工艺审图覆盖机械、灯光、音响三大专业。机械专业关注吊杆布置与荷载、调光柜供电、DMX512 链路;灯光与音响又各自有一套图号与提资,这些图最终要落到 JGJ57 与 WH/T 的强制性条文上。一次完整重审,文档量轻松破两百页:机械的吊杆平面与荷载图、灯光的调光柜与 DMX512 布线图、音响的声场与提资图混在一个批次里,靠人肉翻页加手工记录,效率低且极易漏判。
后来我们把读图工具链自动化,本以为能一劳永逸,结果在长任务里踩到一个更隐蔽的坑:脚本"死"在中间。
典型死法有三种。
第一种,单页崩。一张图解析异常------比如矢量图层引用缺失、字体丢失、外部参照断链------未捕获的异常一路冒泡,整个批处理进程退出,前面跑完的几十张图全部作废,下次只能从头来。以某次灯光专业第 31 页(图号片段 ED2120K)为例,它因为矢量图层引用缺失抛异常,如果没有隔离,整批 66 张灯光图全得重跑。
第二种,假阳性洪流。自动核查把 200dpi 渲染默认值当成真实尺寸报出来,AI 智能体检查产出的信号清单里八成是噪声,人工筛一遍比自己读图还累。核查的价值不在于"报得多",而在于"报得准"。
第三种,齐套盲区。三专业图号混在一起,到底该有多少张、缺了哪张,没有人能秒答;经常是意见都写完了,才发现某个子项整页缺失,前面写的几百条意见基于一套不完整的图集。
这三种死法,本质上都不是"读图算法不准",而是"编排层没设计"------谁决定先跑谁、谁崩了怎么办、跑到一半怎么续,这些事没人管。算法再准,长任务里没有编排,脚本就活不下去。
于是我们换了一个思路------把读图工具链拆成 A(编排)→B(核查)→G(齐套)→C(成稿) 四段,用 manifest 派单+失败隔离+--resume 断点续跑,让脚本在长任务里自己活下去。
这句话是本文的灵魂,后面所有代码与实证都围绕它展开。它回答的不是"怎么读准一张图",而是"怎么让两百张图的读图任务在崩过、漏过、假阳性过之后,依然能自己跑到终点"。
需要明确边界:本文讲的是多段工具链的编排与断点续跑,属于编排层方法论,与"审图交付机器闸门"(单道闸门的逐条断言)和"多智能体审图实战"(单 agent 的对抗式审图)是明确不同的视角,请勿混读。闸门讲的是"单点判不判得过",本文讲的是"一长串任务怎么不死"。
二、四段机制:A→B→G→C 各自负责什么
四段不是四个独立脚本随意拼凑,而是一条用文件解耦、逐段落盘、彼此幂等的生产线。
A 段|编排(manifest 派单)
A 段不读图,只负责把任务摊开成可并行、可追踪的清单。manifest 是一张派工单,列出每张图要做的任务(读哪张、出什么、属于哪个专业)。A 段同时决定两个关键策略:失败隔离(单图失败不中断整批)与断点续跑(从已完成图继续不重跑)。
B 段|核查(AI 智能体检查)
B 段对每张已读图做规范核查:先过 R49 基线,再过 R28 三关------清单关(本页提资是否满足清单)、他图承载关(尺寸/荷载是否被其他图承载)、交叉矛盾关(与他图同一构件是否冲突),再叠加跨图比对。--strict 严模式用于压制假阳性,典型就是剔除 200dpi 渲染默认值带来的虚假尺寸信号。
G 段|齐套(齐全度自动比对)
G 段扫一遍所有 read.json,汇总出"实际读到的图号集合",再与"期望目录集合"比对,自动算出 EXPECTED 矩阵的齐全度百分比。缺哪张、多哪张,一眼可见。这一步必须排在成稿之前,否则写出的意见可能基于不完整的图集。
C 段|成稿(六列意见初稿)
C 段消费 B 段产出的信号清单,生成六列审查意见初稿 md:编号/位置/问题/建议/等级/复核。其中等级给建议(A-B-C-D 等级,即缺陷严重程度分级,A 最重、D 最轻),复核这一栏留空交人工终审。注意:C 段只产出 md 初稿,不展开文档排版细节。
四段之间用文件而非内存耦合:A 写 manifest 与 done.json,B 写每张图的信号 json,G 读这些 json 做集合比对,C 再读 B 的信号 json 出初稿。正因为每段都落盘、都幂等,--resume 才能成立------任何一段崩了,修好那一段重跑即可,已完成的段不动。
已落地的入口是一键串联:
bash
python run_ab_pipeline.py --with-g --with-c --resume
这条命令按顺序跑完 A→B→G→C,并在任意中断后用 --resume 从断点续跑。
三、每段代码或伪代码
下面给出四段的核心片段,均可在工程中直接套用。
A 段:manifest 派单 + 失败隔离 + --resume
python
# run_ab_pipeline.py 中的 A 段:编排层
import json, argparse
from pathlib import Path
MANIFEST = "manifest.json" # manifest:派工单,列出每张图的任务
def load_manifest(p):
return json.loads(Path(p).read_text(encoding="utf-8"))
def done_set(done_file):
# --resume:读回已完成图集合,断点续跑不重跑
if not Path(done_file).exists():
return set()
return set(json.loads(Path(done_file).read_text(encoding="utf-8")))
def read_one(img, task):
try:
return do_read(img, task) # 单图失败隔离
except Exception as e:
print(f"[WARN] {img} 读图失败已隔离: {e}")
return None # 失败隔离:返回 None,不中断整批
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--resume", action="store_true") # --resume 断点续跑
ap.add_argument("--with-g", action="store_true")
ap.add_argument("--with-c", action="store_true")
args = ap.parse_args()
manifest = load_manifest(MANIFEST)
done = done_set("done.json") if args.resume else set()
finished = set()
for item in manifest["items"]:
img = item["img"]
if img in done:
print(f"[SKIP] {img} 已完成,跳过")
continue
r = read_one(img, item["task"])
if r is not None:
finished.add(img)
# 每完成一张就落盘,保证任意时刻中断都可续
Path("done.json").write_text(
json.dumps(sorted(done | finished), ensure_ascii=False, indent=2))
if args.with_g:
run_G()
if args.with_c:
run_C()
配套的 manifest 派工单长这样:
json
{
"items": [
{"img": "J23171.dwg", "task": "read_mechanical", "discipline": "机械"},
{"img": "ED2120K.dwg", "task": "read_lighting", "discipline": "灯光"}
]
}
B 段:R49 基线 + R28 三关 + 跨图比对 + --strict
python
# B 段:AI 智能体检查的前置核查
def check_R28(page, neighbors):
sigs = []
sigs += gate_checklist(page) # 清单关:本页提资满足清单
sigs += gate_crossload(page, neighbors) # 他图承载关:尺寸/荷载被他图承载
sigs += gate_contradiction(page, neighbors) # 交叉矛盾关:同构件跨图冲突
return sigs
def run_B(page, neighbors, strict=False):
base = check_R49_baseline(page) # R49 基线:规范底线
sigs = check_R28(page, neighbors) # R28 三关
if strict:
sigs = drop_false_positive(sigs) # --strict:剔除 200dpi 渲染默认值假阳性
return base + sigs
G 段:齐套性自动比对
python
# G 段:扫 read.json 汇总实际图号,与期望集合比对
def normalize(did):
return did.strip().lower().lstrip("dwg-") # 归一化:小写、去 dwg- 前缀
def check_completeness(read_dir, expected):
actual = set()
for f in Path(read_dir).glob("**/read.json"):
data = json.loads(f.read_text(encoding="utf-8"))
actual.update(normalize(d) for d in data.get("drawing_ids", []))
exp = set(normalize(d) for d in expected)
missing = exp - actual
ratio = len(actual & exp) / max(len(exp), 1) * 100 # 齐全度%
return {"actual": actual, "missing": missing, "ratio": round(ratio, 1)}
expected = set(json.loads(Path("expected.json").read_text())["ids"])
report = check_completeness("out/", expected)
print(f"齐全度 {report['ratio']}%,缺失:{sorted(report['missing'])}")
C 段:消费 B 信号清单产出六列初稿
python
# C 段:B 的信号清单 -> 六列审查意见初稿 md
def draft_opinion(signals):
lines = ["| 编号 | 位置 | 问题 | 建议 | 等级 | 复核 |",
"| --- | --- | --- | --- | --- | --- |"]
for i, s in enumerate(signals, 1):
level = suggest_level(s) # 等级建议:A-B-C-D 等级
lines.append(f"| {i} | {s['loc']} | {s['issue']} | {s['fix']} | {level} | |")
# 复核这一栏留空,由人工终审填写
return "\n".join(lines)
def suggest_level(s):
# A-B-C-D 等级:结构/安全类给 A,一般违规给 B,提示类给 C/D
return {"critical": "A", "major": "B", "minor": "C", "tip": "D"}.get(s["sev"], "C")
C 段只到 md 初稿为止;六列表格后续的文档化排版不属于本篇范围,本文不展开。
四、实证数据:某城市交响音乐厅 218 页三专业重审
最近一次实战落地的场景是一家城市交响音乐厅(项目名已泛化,不出现真实地名与编制单位)的三专业重审。读图工具链被工程化编排后,单批处理规模达到 218 页:机械专业 99 页、灯光专业 66 页、音响专业 53 页(图号仅保留技术性片段如 J23171、ED2120K 作教学样本,不具身份识别性)。
这次重审有几个可被验证的结果。
第一,断点续跑真正救了场。重审当日,灯光专业第 31 页(图号片段 ED2120K)因矢量图层引用缺失抛异常。由于 A 段做了失败隔离,该页被标记为隔离、其余 217 页正常跑完;修复该页字体引用后用 --resume 续跑,仅重跑了 1 页,没有让整批 218 页推倒重来。
第二,齐套性比对提前暴露缺口。G 段跑完,齐全度显示为 96.3%,缺失 8 个图号;人工核对后发现其中 3 个属于本轮暂不交付、5 个确实漏提。意见成稿前就把缺口摆到桌面上,避免了"写完两百多页意见才发现少了 8 张"的尴尬。
第三,精力从"救火"转向"编排"。重审团队当日的轮次权重相对估算(注意:这是相对权重,非绝对工时;AI 智能体的处理时长本身不可证伪,本段数字仅作方法演示,不代表真实人日)显示,用于"修脚本/救崩批"的时间占比从以往的约 40% 降到约 12%,腾出的权重转向 manifest 设计与信号清单的精读------也就是从"让脚本别死"升级到"让脚本跑得对"。
需要强调的是:上述"轮次权重"是相对估算,不是可审计的工时;AI 智能体的处理时长不可证伪,任何号称"省了 N 人日"的绝对表述都应打问号。本文只承诺方法可被复现,不承诺具体节省数字。
五、踩坑:四个最值得记的坑
下面按"现象 / 成因 / 解法"记录实战里踩过的坑。
坑一:失败隔离不做落盘,--resume 就成摆设
- 现象:加了 try/except 单图失败不崩了,但崩在中间断电或手动杀进程后,done.json 没更新,--resume 从头跑。
- 成因:done 集合只在进程末尾写盘,中途退出全部丢失。
- 解法:每完成一张就追加写盘(见 A 段代码
Path("done.json").write_text(...)在循环内),保证任意时刻中断都可续。
坑二:--strict 不开,信号清单被 200dpi 假阳性淹没
- 现象:B 段不开启严模式时,把 200dpi 默认渲染分辨率当成真实像素尺寸,报出大量"尺寸不符",人工筛信号比自己读图还累。
- 成因:栅格化渲染默认值被误当作矢量真实尺寸。
- 解法:重审场景默认开 --strict,先压假阳性;确需保留疑似项的,单独建宽松通道复核,不混进主信号清单。
坑三:G 段图号大小写/前缀不一致,齐全度算错
- 现象:期望目录写的是 J23171,read.json 里是 j23171 或带前缀 DWG-J23171,集合比对直接判缺。
- 成因:图号字符串未归一化。
- 解法:比对前统一小写、剥离 dwg- 前缀再入集合(见 G 段
normalize);EXPECTED 矩阵与 read.json 用同一归一化函数,避免"同图不同名"。
坑四:C 段等级建议过严,A 级泛滥
- 现象:suggest_level 初版把"提示类"也标 A,意见初稿满屏 A 级,失去了分级意义。
- 成因:严重度映射阈值没校准。
- 解法:A 级只留给结构/安全类硬冲突,提示类落到 C/D;等级是建议,复核这一栏始终留空交人工终审。
六、可迁移结论:让脚本"活下去"的四条通则
第一条,长任务先设计编排层,再谈算法精度。任何会跑几百次的批处理,第一件事不是"读准",而是"崩了怎么办、断了怎么续"。manifest 派单+失败隔离+--resume 是三件套,缺一不可。
第二条,段间用文件解耦,幂等落盘。A/B/G/C 每段都落盘、都幂等,--resume 才能成立。内存里传对象看似快,崩一次全没。
第三条,严模式默认开,假阳性是长任务最大的隐性成本。AI 智能体检查的价值不在"报得多",而在"报得准"。--strict 压住 200dpi 这类默认值假阳性,信号清单才有人愿意读。
第四条,齐套性要前置到成稿前。G 段不跑,C 段写得再漂亮也可能基于不完整的图集。齐全度百分比是最便宜的"你漏了没"提醒。
把这四条放回舞台工艺审图:机械的吊杆荷载、灯光的调光柜与 DMX512 链路、音响的声场提资,三专业图号混在一起时,编排层比任何单点算法都更能决定一次重审能不能在当天收口。脚本自己活下去,审图团队才能把精力从"救火"挪到"编排"------这正是 A+B+C+G 四段流水线想交付的东西。