用 AI 智能体把舞台工艺审图跨源比对做成真验证:列名+数值双对,杜绝伪一致

一、问题背景

舞台工艺工程是剧场、演艺场馆里舞台机械、舞台灯光、舞台电气三大专业的统称;审图则是把这些专业的设计成果——计算书、系统图、设备布置图——与现行规范、与现场土建与机电条件逐条核对的环节。在舞台工艺审图里,一份项目的电气计算成果常常不是单一来源:设计单位用专业软件出一份计算书,施工方或第三方用另一套表格做独立复核,业主方(项目的出资与最终验收主体,下文统称业主方)也可能保留一份归档版本。我们要做交叉验证时,本质是把这几份"长得像"的表摆在一起,逐行逐列对一遍。

跨源比对做交叉验证,不能只对数值,必须列名+数值双对;只比对数值就下"一致"结论,是伪验证,会把列名错位当成真一致、漏掉真实矛盾。

这句话是我这几年在审图现场反复撞墙后,最想钉进每个工程师脑子里的。很多团队的交叉验证流程,到头来只干了一件事:把两张表按行对齐,看 A 表第 3 行是不是等于 B 表第 3 行,数值对得上就盖章"一致"。问题恰恰是——"数值对得上"和"结论一致"之间,还隔着一层列名语义。一旦列名错位,纯数值比较会极其自信地输出一个完全错误的"一致"判定,而且从头到尾不报任何错。

为什么会出现多份来源?舞台工艺电气涉及照明、动力、舞台机械驱动等多个子系统,不同参与方用的工具链不同:设计单位可能用某种电气计算软件直接出成果,施工方习惯用电子表格二次整理,第三方审图机构又可能用独立的分析工具重算。每家导出的表头写法、列序、单位都不完全一样。一份"计算电流"在 A 家叫"计算电流 Ijs"、B 家叫"Ijs"、C 家叫"计算电流";“整定电流"也可能被写成"整定电流 Ir”“Ir”“脱扣器整定”。这些差异单独看都不大,但叠在一起,就成了纯数值比对的陷阱。

更底层的原因是:这些计算表最初大多脱胎于设计软件导出的 Excel,更源头是 CAD 图纸里的 DWG 文件。有些表格是直接从图纸的文字层(CAD 图里专门承载文字标注的图层,与图形层相对)提取出来的 ACAD_TABLE(AutoCAD 内部的表格实体对象),也有些是被栅格化(把矢量图转成像素图的过程)后以图片形式流传,再被人手工誊抄回来。来源越杂,列名写法越容易漂移,列序错位的概率越高。

这也是为什么我后来把这件事交给一个 AI 智能体(具备自主调用工具、按既定规则执行校验流程的软件智能体)来跑:不是因为它比人聪明,而是因为它不会"嫌麻烦就只比数值",每一次比对都强制走列名+数值双对这条链,且每一步都留下日志。人会在疲劳时抄近路,智能体不会;人会对"看着差不多"放松警惕,智能体不会。

二、伪验证长什么样

伪验证最常见的形态,是一段看起来"很严谨"的 pandas 代码:

import pandas as pd

dfA = pd.read_excel("设计方计算书.xlsx")
dfB = pd.read_excel("施工方复核表.xlsx")

# 只比数值:把两表按 index 对齐后求全局最大差
diff = (dfA - dfB).abs().max().max()
assert diff < 1e-6, "存在不一致"

这段代码的问题不在语法,而在它的判定前提:它默认 dfA 和 dfB 的列是一一对应、语义等价的。只要这个前提被悄悄打破,断言就会给出错误的安全感,而且因为 diff 很小,它甚至会反过来"证明"两份表高度一致。

更隐蔽的写法是先用 dfB = dfB[dfA.columns] 偷偷按 A 的列名去切 B。如果 B 里恰好有同名列,切片成功,数值对齐;可一旦 B 的"整定电流 Ir"被错放到了"计算电流 Ijs"该在的位置,切片后两列同名、数值又碰巧接近,伪一致就达成了。列名漂移 + 列序错位,是伪验证的两大温床。

还有两类容易被忽略的伪验证变种。第一类是类型错位:A 表的电流列是浮点 256.0,B 表因为混进了单位字符串被读成 object 类型的 "256A",直接相减会抛异常或静默得到 NaN,继而被人用 fillna(0) 抹平,错误地"通过"。第二类是索引错位:两表行序不同,却没用关键列(如回路编号)做 merge 对齐,而是按默认行号硬比,于是第 3 行对第 3 行,其实是把甲回路和乙回路比在了一起。这两个变种同样只在"只比数值"的框架里才会发生。

举个舞台灯光电气里的真实情形:两张表都有一列数值是 256,但 A 表那一列叫"整定电流 Ir"(断路器脱扣器整定值),B 表那一列叫"计算电流 Ijs"(回路实际计算电流)。在低压配电里,保护配合要求整定电流必须小于计算电流(Ir < Ijs 才满足选择性)。如果这两列被错位摆在一起、只比数值,256 等于 256,代码报"一致",可真正的矛盾——整定没有小于计算——反而被完美掩盖。

我把这种"看起来一致、实则错位"的情况归为审图里最隐蔽的一类。它不报错、不报警,安静地混过流程,直到现场调试阶段调光柜(舞台照明配电里控制灯具亮度的柜体,内部是晶闸管调光模块)带载、DMX512(舞台灯光设备之间传输控制信号的串行协议,用一根线串起所有灯具)信号下发后才暴露过载。到时候再回头查,源头早已埋下。

这里还牵涉齐套性(一套图纸与文件是否完整、有无缺页缺表的状态)问题:当多份表齐套性不足、互为补充而非互为副本时,单纯"求差比对"的假设从根本上就不成立。我们做审图,用的是 JGJ 57(剧场建筑设计规范)和一系列 WH/T(文化行业标准的代号,由文化和旅游部发布)来框定对错;比对方法如果本身就是伪的,再权威的规范也救不回那份错误的"一致"结论。

三、列名+数值双对的判定链

真正的交叉验证,必须满足两个条件同时成立:

  • 列名对齐:A、B 两表的列集合、列序、列语义一一对应;
  • 数值比对:在列名对齐的前提下,逐列逐行数值在容差内相等。

形式化写就是:交叉验证 = 列名对齐 ∧ 数值比对。两条件之间是"与"关系,缺一不可。只满足数值比对、跳过列名对齐,就是开篇核心结论所定义的伪验证。

判定链必须按固定顺序走,顺序本身也是防错的一部分:

第一步,锁列名。 在碰任何数值之前,先把两表的列名拉出来做全等比较。列名不一致立即中止,不允许进入数值比对。这一道把"列名漂移""列序错位"在源头拦截。注意这里用的是 list(df.columns) 的顺序全等,而不是 set 比较——集合比较会放过列序错位,而列序错位恰恰是伪验证的伪装之一。

还要处理"列部分重叠"的情形:A 表有"计算电流、整定电流、备注明细"三列,B 表只有"计算电流、整定电流"两列。此时列集合不等,锁列名应当直接失败,而不是自动"按交集裁剪后硬比"——自动裁剪会悄悄丢掉"备注明细"这一列,属于另一种静默吞行。凡是涉及裁剪,都必须显式记录被裁剪的列,并纳入审计。只有在列集合、列序、列语义三者都确认对齐之后,才允许进入数值比对这一关。

第二步,归一别名(可选但强烈推荐)。 当两表列名写法不同但语义相同(如"计算电流Ijs"与"Ijs"),引入一个别名族映射 colmap,把同义列名归一到规范名再比。注意:归一必须可审计,未命中的列要单独记下来,绝不能静默吞行(见第六节)。

第三步,逐列数值比对。 在列名已对齐(或已可审计地归一)之后,对每一列调用 np.isclose 做带容差的浮点比较。电流、功率这类量是浮点,不能用等号,必须用相对/绝对容差。同时要先处理 NaN:两边都是空,应判为"一致"而非"不一致";只有一边为空一边有值,才算真不一致。

第四步,归类定级。 把每一条违例按性质打标:是"列名-数值"型(列名错位导致假一致被识破),还是"纯数值"型(列名对、数值真不一样)。后者再按 A-B-C-D 等级(按缺陷严重程度划分的 ABCD 四级分类,A 最致命、D 最轻微)落到对应处置通道。

只有四步全过,才允许闸门放行(CI/CD 语境里把检查通过称为放行);任何一步失败,流程拦截,把违例清单吐给人工。

四、代码实现

下面是一段可直接跑的最小实现。环境:Python 3.10、pandas 2.1、numpy 1.26。

import pandas as pd
import numpy as np

# ---------- 基础双对校验 ----------
def strict_compare(dfA: pd.DataFrame, dfB: pd.DataFrame, rtol: float = 1e-3):
    # 第一步:先锁列名,列名不等直接抛错,禁止进入数值比对
    assert list(dfA.columns) == list(dfB.columns), (
        f"列名不一致,禁止直接比对。A={list(dfA.columns)} B={list(dfB.columns)}"
    )
    # 第三步:逐列数值比对,浮点用 np.isclose 带容差,NaN 双方为空视为一致
    for col in dfA.columns:
        a = dfA[col].astype(float).to_numpy()
        b = dfB[col].astype(float).to_numpy()
        mask = ~(np.isnan(a) & np.isnan(b))   # 两边皆空不计入差异
        if not np.isclose(a[mask], b[mask], rtol=rtol, atol=1e-6).all():
            raise AssertionError(f"列 [{col}] 数值不一致")
    return True

# 等价写法:用 pandas 内置断言,check_like=False 防止"列名错位却因位置巧合通过"
# pd.testing.assert_frame_equal(dfA, dfB.rename(columns={...}), check_like=False)

当列名只是写法不同、语义相同,需要先用 colmap 归一:

# ---------- 别名族归一(可审计版) ----------
COLMAP = {
    "计算电流": ["计算电流", "计算电流Ijs", "Ijs", "计算电流Ij", "Ijs_A"],
    "整定电流": ["整定电流", "整定电流Ir", "Ir", "整定电流Irset"],
}

def normalize_columns(df: pd.DataFrame):
    rev = {}
    for canonical, aliases in COLMAP.items():
        for alias in aliases:
            rev[alias] = canonical
    renamed, unmapped = {}, []
    for c in df.columns:
        if c in rev:
            renamed[c] = rev[c]
        else:
            # 审计点:未命中别名的列原样保留,并记入 unmapped,绝不静默丢弃
            renamed[c] = c
            unmapped.append(c)
    return df.rename(columns=renamed), unmapped

dfA2, unA = normalize_columns(dfA)
dfB2, unB = normalize_columns(dfB)
if unA or unB:
    print("未归一列(需人工确认):", unA, unB)
strict_compare(dfA2, dfB2)

关键点:归一后若 unmapped 非空,说明出现了 colmap 没覆盖的新列名,必须人工确认,不能假装它不存在。这正是"归一必须可审计、不能静默吞行"的代码落地。

把上面的函数串成一个带审计日志的流水线,便于在闸门里复用:

def run_pipeline(dfA: pd.DataFrame, dfB: pd.DataFrame, colmap: dict | None = None):
    report = {"column_lock": None, "unmapped": [], "value_ok": None, "violations": []}
    if colmap:
        dfA, unA = normalize_columns(dfA)
        dfB, unB = normalize_columns(dfB)
        report["unmapped"] = unA + unB
    try:
        strict_compare(dfA, dfB)
        report["column_lock"] = "pass"
        report["value_ok"] = True
    except AssertionError as e:
        report["column_lock"] = "fail"
        report["value_ok"] = False
        report["violations"].append(str(e))
    return report   # 返回结构化报告,供闸门写审计日志

下面给一个最小可运行演示,验证这套链确实能拦住违例。构造两份小样本:A 是设计方、B 是施工方,整定电流应当小于计算电流。

# 最小可运行演示:构造一个会触发数值型违例的小样本
import pandas as pd

dfA = pd.DataFrame({"回路": ["L1"], "计算电流Ijs": [256.0], "整定电流Ir": [200.0]})
dfB = pd.DataFrame({"回路": ["L1"], "计算电流Ijs": [256.0], "整定电流Ir": [256.0]})
print(run_pipeline(dfA, dfB, COLMAP))
# 输出:column_lock=pass,value_ok=False
# 违例来自"整定电流Ir"列:200(设计)vs 256(施工),闸门拦截

若把 B 的列名误写成与设计方不同的名字、却把 256 这个值摆到了整定该在的位置,那么第一步锁列名就会先失败,根本走不到数值比对——这正是"列名-数值"型违例被提前拆穿的地方。可运行演示的意义,是让这条判定链从"我看懂了"变成"我跑通了",也是交给智能体去执行时最省心的那一层保障。

五、真实案例复盘

回到第二节那 256A 的情形,把它放到一个具体项目里复盘。某项目的舞台灯光配电,设计方出了一份回路计算书,施工方另出了一份整定清单,两份都要进审图交叉验证。

设计方表里有一列"计算电流 Ijs = 256A",施工方表里有一列"整定电流 Ir = 256A"。如果按伪验证做法,两表按行对齐、只比数值:256 == 256,判定"一致",流程放行。

但 GB 50054《低压配电设计规范》对过负荷保护与短路保护的选择性有明确要求:断路器的整定电流 Ir 必须小于被保护回路的计算电流 Ijs,否则保护配合不满足(即发生故障时断路器可能不按预期动作,或上级抢先跳闸导致大面积失电)。这里 Ir = 256、Ijs = 256,整定并未小于计算,配合不满足,本应判为违例。进一步说,如果整定值不从计算电流往下取,电缆在过负荷时可能先于断路器达到危险温度,而断路器却迟迟不跳闸,结果是电缆绝缘老化甚至起火,保护彻底失效。

伪验证把"整定电流"和"计算电流"当成同一列来对齐,于是数值相等被误读成"两份表一致",真正的保护配合缺陷被漏掉。等现场调光柜带载、灯具按 DMX512 信号全功率点亮,回路长期运行在临界状态,过载保护却因为整定不当而失效——这时再查,源头那份错误的"一致"早已归档。

用第三节的判定链重跑:第一步锁列名就发现 A 表有"计算电流 Ijs"、B 表对应位置是"整定电流 Ir",列名不等,立即中止数值比对并报警。即便 A、B 两表各自内部列序不同,只要经过 colmap 归一到"计算电流""整定电流"两个规范名,第二步归一后第三步逐列比,也会发现这是两个不同语义的列、不能合并判等。伪一致从根上被拆穿。

这个案例的教训很直白:256 这个数值本身没错,错的是把它从"整定电流"那列挪去和"计算电流"对齐。只比对数值的验证,永远分不清"谁和谁在比";而分不清比对的两边到底是什么,结论就毫无意义。

六、把机制做成可审计闸门

把上面的判定链固化成一个"闸门"(CI/CD 里把检查不通过就拦截发布的关卡),比写一段一次性脚本更有长期价值。闸门的核心不是"比完就完",而是"每一次比对都可追溯、可审计"。

我在一个项目的审图辅助流程里,基于一个大模型驱动的 AI 智能体(具备自主调用工具、按既定规则执行校验流程的软件智能体),并通过一个审图校验 Skill(把校验流程封装成可复用的能力单元)来触发它,让智能体担任闸门 runner:它读取多份待比对的表,强制走列名锁定的第一步,再做归一与数值比对,最后把违例清单连同原始列名、归一映射、未命中别名一并写进审计日志。

有一个数字锚点值得记下来:某项目累计回填了 34 份读图步骤记录(审图时逐步读图、逐条核对的步骤流水),闸门跑下来报出 11 处"列名-数值"型违例——也就是列名错位导致、如果只比数值永远发现不了的那种错。11 处是那一轮的实测值,但重点是这一类违例在纯数值比对下是零可见的;没有列名+数值双对,它们会永远躺在"一致"的假结论里。34 份记录里揪出的每一处"列名-数值"型违例,都是纯数值比对永远给不出的一分安全感。

为了让归一可审计,闸门对 colmap 做了两条硬约束:

  • 别名映射必须显式登记:任何新出现的列名写法,要先加进 COLMAP 再跑,禁止运行时临时猜测同义。猜测会悄悄把"整定电流"和"计算电流"归成一族,那是另一种伪验证。
  • 未命中必须留痕:normalize_columns 返回的 unmapped 一旦非空,闸门标记为"需人工确认"并阻断自动放行,绝不静默吞行。

审计日志建议至少记录四条:原始列名列表、归一后的规范名列、未命中别名的列、每一条违例的"列名-数值"定位。这四条合起来,让任何一次"放行"都能被事后还原——这才是闸门区别于一次性脚本的地方。

一条具体的审计记录长这样(示意):原始列 A=[“计算电流Ijs”,“整定电流Ir”]、原始列 B=[“Ijs”,“整定电流”]、归一后=[“计算电流”,“整定电流”]、未命中=[]、违例=[“列[整定电流] 数值不一致:256 vs 200”]。有了这条记录,任何人都能在事后十秒内说清"为什么这次比对没放行",而不是翻聊天记录去猜。

容差的选择也要写进闸门配置而非散落在代码里:电流类建议相对容差 1e-3、绝对容差 1e-6,功率类可按工程允许误差放宽到 1e-2;容差一旦变更,审计日志要记一笔"容差由 X 改为 Y",避免"今天能过、昨天不能过"却查不出原因。容差本身也是可审计对象,不是藏在脚本角落的魔法数。

落到日常审图里,闸门不是要取代审图人,而是把审图人从重复劳动里解放出来。绝大多数比对都是真一致,闸门自动放行、留痕即可;只有当它报出"列名-数值"型违例或"未归一列"时,才把问题抛给审图人做判断。这种"默认信任、异常升级"的模式,既保住了效率,又守住了底线。需要强调的是,升级上来的判断必须由人拍板,闸门只负责把问题定位到"哪一列、什么值、为什么不一致",不代替人下结论。这也正是把机制做成可审计闸门、而非一次性脚本的终极意义:脚本只解决"这一次",闸门解决"每一次都可解释"。当某项目回填那 34 份读图步骤记录、闸门又揪出 11 处纯数值比对永远看不见的"列名-数值"型违例时,你会真切体会到——所谓真验证,从来不是数值看着对,而是列名与数值双双对上、且每一步都留得下痕迹。

最后补一句机制层面的提醒:闸门的"放行"是结论,不是起点。它只对"已经过列名对齐 ∧ 数值比对"的输入放行;任何绕开列名锁定直接比数值的捷径,都是把伪验证重新请回流程。审图的安全感,必须建立在列名+数值双对之上,而不是建立在"数值看着差不多"的侥幸上。把这件事交给 AI 智能体检查(由智能体执行而非人工目检的自动化核查)去跑,不是为了替代工程师的判断,而是把工程师从"不得不逐行肉眼对账"里解放出来,让他们把精力放在真正的保护配合、规范符合性这些只有人能拍板的事上。

Logo

更多推荐