舞台工艺 AI 交付物措辞闸门:同一套词表为什么必须拆成 opinion / system 双模式
一、交付物的措辞不是文风问题,是责任边界问题
舞台工艺审图的交付物分两大类:一类是审核意见——面向设计单位和业主方,回答"这张图哪些地方不符合、依据是什么、怎么改";另一类是体系/方法论文件——面向内部团队,回答"这套审查是怎么组织的、依据哪些标准、每一步怎么走"。两类文档都由 AI 参与起草,再由人工复核定稿,流程上共用同一条出包链路。
问题出在措辞上。AI 在起草时会自然地带出两类不该出现在交付物里的话术:
第一类是读图过程词。 比如"文字层"“文本层”“提取内容”“自检表”“坐实”“疑为”。这些词描述的是"我是怎么把这张图读出来的",而外发意见的读者只关心"这张图有什么问题"。把过程词写进交付,等于把内部作业痕迹塞给了对方;更麻烦的是"疑为"“疑是”"疑为笔误"这类不定性表述——审核结论必须给确定判断,"疑为笔误"把核实成本原样推回给读者,等于这条意见没写完。
第二类是自证语。 比如"经复核"“经核对”“已确认达标”“未发现”“无问题”。审核意见的效力来自"问题—依据—建议"三要素,不来自审核人替自己的过程背书。"经复核"三个字不改变任何一条意见的成立与否,却让整份文档带上自我担保的语气;一旦后续发现漏项,这句话反而成为追责时的把柄。
这两类词不是人工写错,而是 AI 起草时的系统性倾向。读图过程词来自链路本身:图纸要先转成可读文本才能被理解,中间产物里到处是"文字层"“提取内容”“覆盖率自检"这类说法,模型起草时很自然地把上游语境带进正文。自证语则来自语料风格:公开的审查报告、验收纪要里满是"经复核”"未发现问题"这类套话,模型学到的就是这套腔调。既然是系统性倾向,靠提示词"请避免使用……"是压不住的——提示词只能降低频率,不能给出确定性;要确定性只能上闸门。
所以必须在出包前加一道机器闸门:对交付 docx 全文做禁词扫描,命中即阻断,退出码非 0 就不许打包。
但真正难的不是"加闸门",而是词表该长什么样。第一版我们把所有禁词做成一张平表,一把梭扫全文,结果两头不讨好:意见稿里的自证语确实被拦住了,可体系文件里"3.1 审查依据""4.2 审查方法"这些合法章节名也被判违例,导致体系文件根本过不了门。修法不是删词,而是把词表按文档类型分区、按类型路由。这就是 –type opinion|system 双模式存在的唯一理由。
本文讲的门禁脚本 audit_delivery_gate.py 实测 384 行(wc -l,16537 字节),禁词表拆成 4 组,退出码三值。下面按"为什么拆—怎么拆—怎么实现—怎么接入"的顺序讲透,所有数字均取自脚本真源或本机实测。
二、同一个词,两套合法性:一把梭的词表为什么必然两头不讨好
先看一组实测。构造一份同时满足两类文档形态的样本:抬头齐备、六列意见表干净、正文里写"审查依据为 WH/T 系列与 GB/T 相关标准,审查方法为逐项比对"。同一份文件,只切换 –type:
$ python audit_delivery_gate.py sample_A_dual.docx –type opinion
文档类型: opinion(审核意见类)
[FAIL] sample_A_dual.docx (意见行数=2;复核列已填=0)
– 禁词命中: 审查依据、审查方法
==== 门禁结果 ====
通过 0/1
存在违例,交付被阻断。请修正后重跑。
rc=1
$ python audit_delivery_gate.py sample_A_dual.docx –type system
文档类型: system(体系/方法论类)
[OK] sample_A_dual.docx (表格列数分布=6列×1;已标注空白模板=0)
==== 门禁结果 ====
通过 1/1
全部通过,可出文/出包。
rc=0
同一份文件、同一段文字,opinion 模式判违例,system 模式判通过。这不是门禁的双标,是这两类文档对同一个词的合法性要求本来就相反。
拆开看"审查依据"这个词在两种语境里的作用:
- 在审核意见里,它出现在哪都没用。意见的读者要的是"哪一条不符、违反了哪条规范、改成什么"。写"审查依据为 WH/T 系列"既没有定位到具体条款,也没有指向具体不符项,是一句无法执行的空话;更糟的是它会让整份意见看起来像在声明"我是按规矩查的",这正是自证语的典型形态。
- 在体系文件里,它是章节名。体系文件的作用就是交代这套审查怎么组织,第一章不写"审查依据"、不写"审查方法",这份文件的骨架就塌了。把它禁掉,等于逼着体系文件改名换词,最后写出"审查所依照的东西"这种更糟的表述。
同样的分裂也发生在"经复核"“未发现”"无问题"上:意见稿里是自证语,体系文件里是合法的流程描述与结论句。
于是得到一个判据:一个词是不是禁词,不取决于词本身,而取决于它所在的文档在交付链里扮演什么角色。 词表的第一行注释应该写清楚"这个词在哪一类文档里合法",而不是只写"这个词不许出现"。
再往深一层,被禁的这些词实际上分三种性质,混在一张平表里会让后续维护失去依据:
只有第 2 类的合法性会随文档类型翻转,第 1、3 类恒定。这就是分区的依据:会翻转的进专用组,不翻转的进通用组。
把三类拆开还有第二个好处:出现新的污染词时,归档位置是确定的。有一次模型在意见稿里写出"端口容量无溢出"——这既不是读图过程词也不是自证语,而是把齐套性核对的内部结论写进了单条意见(齐套性是整册图纸层面的结论,不该出现在逐条意见里)。这类词该进哪一组?按上面的判据一问即有答案:体系文件里当然可以写"端口容量核对"“无溢出”,所以它们进 OPINION_ONLY。如果没有这套分区,新词只能随手追加到平表末尾,不出三个月就没有人记得某条词当初为什么进表、能不能删。
三、四组词表的分区依据与合并规则
脚本把词表拆成 4 组,每组承担一种独立语义。以下条数均为本机实测 len() 值。
OPINION_ONLY_PATTERNS(意见类专用,实测 13 条)
审查依据、审查方法、审查方式、审查标准、经核对、经复核、经…复核、经核对其、经对其、齐套性核对、齐套性结论、端口容量、无溢出。
判定口径:这些词在体系文件里全部是合法表述(章节名或流程描述),只在意见稿里越权。
COMMON_PATTERNS(两类通用,实测 17 条)
文字层、文本层、可读文字层、提取内容、覆盖率自检、自检表、坐实、无误判、无漏检、漏检、已确认达标、疑为、疑是、疑为笔误、疑是笔误、疑为误写、疑是误写。
判定口径:读图过程词 + 不定性表述 + 通用自证语,任何一类交付物出现都应拦截。
OPINION_EXTRA_PATTERNS(意见类附加自证语,实测 2 条)
未发现、无问题。
判定口径:这两个词在体系文件里是正常结论句(“现场核查未发现问题”),但在意见稿里属于替自己下结论,故单独成组、只进意见类全量。
PLACEHOLDER_PATTERNS(体系文件占位符残留,实测 6 条)
####、TODO、TBD、XXX占位、待补充、待补齐。
判定口径:这不是措辞问题,是"没写完"问题。它走独立通道(体系类专用),不参与意见类的措辞判定。
合并规则只有两条:
- 意见类全量 R17_PATTERNS = OPINION_ONLY + COMMON + OPINION_EXTRA,实测 32 条;
- 体系类只取 COMMON_PATTERNS,实测 17 条。
判定一个词该进哪一组,只需问一句:“它有没有可能是体系文件里的合法表述?” 答案是"是",进 OPINION_ONLY 或 OPINION_EXTRA;答案是"绝不可能是",进 COMMON。这条判据看起来简单,却是后面几次词表修订里唯一没被推翻过的东西——2026-10-13 那次把"待补"改成"待补充/待补齐"的修订,本质上就是因为"待补"这个词有可能是合法状态词的一部分(待补证=待补证据),违反了这条判据。
顺带说一个容易被忽略的配套项:位置列禁读图坐标。六列意见表的"位置"列如果写成 line 12、p003、第 3 页,那是把读取过程的坐标和页面锚点带进了交付——对改图毫无意义,图纸一重排、一换版,这些数字立刻失效。脚本用一条正则拦三类形态:
POSITION_ANCHOR_RE = re.compile(r"(?i)\\bline\\s*\\d+\\b|\\bp\\s*\\d{3,}\\b|第\\s*\\d+\\s*[页行]")
本机实测 7 例:
- line 12 → 命中 line 12(\\s* 兼容无空格写法 line12)
- p003 → 命中(三位以上数字)
- p12 → 不命中(两位数字,避免误伤轴网编号之类的短码)
- 第 3 页 / 第 5 行 → 命中
- 轴网 3-4 交 A → 不命中(真正该写进位置列的内容)
这条正则的写法体现了同一条原则:锚点形态要写得足够"窄",宁可放过,不可误伤。 位置列真正该写的是"面光桥 A 区"“耳光室”“栅顶 3 轴至 5 轴"这类图纸自身的定位语言——它在版次变更后依然成立,改图的人拿着它就能找到位置。锚点坐标和页码则相反,它们定位的是"AI 当时读到的是哪一页”,换个软件、换个版本立刻失效。
四组之外还有一点值得记下来:PLACEHOLDER 组是独立于措辞闸门的另一条通道。措辞闸门管"说了不该说的话",占位符检查管"该说的话还没写"。两者混在一张表里会造成一个尴尬后果——命中报告里"待补充"和"经复核"并列出现,前者要补内容,后者要删词,处置动作完全相反,却共用同一条违例列表。分成两组之后,输出的处置指令才能一句话说清。
四、Python 实现:路由、取文、编译、定位
4.1 词表定义与双模式路由
# audit_delivery_gate.py —— 禁词表四组分区
OPINION_ONLY_PATTERNS = [
"审查依据", "审查方法", "审查方式", "审查标准",
"经核对", "经复核", "经…复核", "经核对其", "经对其",
"齐套性核对", "齐套性结论",
"端口容量", "无溢出",
] # 实测 len = 13
COMMON_PATTERNS = [
"文字层", "文本层", "可读文字层", "提取内容",
"覆盖率自检", "自检表",
"坐实", "无误判", "无漏检", "漏检", "已确认达标",
"疑为", "疑是", "疑为笔误", "疑是笔误", "疑为误写", "疑是误写",
] # 实测 len = 17
OPINION_EXTRA_PATTERNS = ["未发现", "无问题"] # 实测 len = 2
PLACEHOLDER_PATTERNS = ["####", "TODO", "TBD", "XXX占位", "待补充", "待补齐"] # len = 6
R17_PATTERNS = OPINION_ONLY_PATTERNS + COMMON_PATTERNS + OPINION_EXTRA_PATTERNS # 实测 len = 32
def route_patterns(doc_type: str) –> list[str]:
"""双模式词表路由:system 只取通用组,opinion 取全量。
路由前先校验类型值——未知类型必须报错退出,不能静默降级为 opinion,
否则体系文件会被按意见类标准误拦。
"""
if doc_type not in ("opinion", "system"):
raise ValueError(f"未知文档类型: {doc_type}(应为 opinion 或 system)")
return COMMON_PATTERNS if doc_type == "system" else R17_PATTERNS
路由函数里的类型校验不能省。–type 拼错成 opinon 时,如果代码走 if doc_type == "system" 的否定分支默认取全量表,体系文件就会被意见类标准拦下一大片,而且报错信息完全指不到根因。真源把这段放在 main() 里,用 sys.exit(2) 退出。
4.2 取文:表格单元格必须进扫描范围
def collect_text(doc) –> str:
"""汇总 docx 全文字:段落 + 所有表格单元格。
★ 只扫 doc.paragraphs 会让六列意见表整张漏检——
禁词最常藏在"问题"列里,而表格文字不在 doc.paragraphs 中。
"""
out = [p.text for p in doc.paragraphs]
for t in doc.tables:
for r in t.rows:
for c in r.cells:
out.append(c.text)
return "\\n".join(out)
这段看着平庸,实际是整道闸门里最容易踩空的一环。实测对比:构造一份文档,段落只有一句"本页无禁词,段落干净。“,六列意见表的"问题"列里写"标注数值疑为笔误”——
- 只扫 doc.paragraphs:命中 0 条,闸门放行;
- 走 collect_text():命中 疑为、疑为笔误、文字层(实测 3 条),闸门阻断。
漏掉表格的扫描等于闸门形同虚设,因为审核意见的实质内容 100% 在表里。
4.3 编译:长度降序,避免短词吞长词
真源当前的判定是 collect-all 子串法:
hits = sorted([kw for kw in patterns if kw in full])
这种写法下短词不会"吞掉"长词——实测对"标注文字疑为笔误"一句,疑为 和 疑为笔误 都会被报出来(命中列表长度 2)。它的问题是只报词、不报位置,回改时还得自己全文搜索。
一旦要把闸门升级成"定位"或"自动改写",就必须引入正则,此时词表必须按长度降序编译:
import re
def compile_banned(words) –> re.Pattern:
"""按长度降序编译。
交替分支 `|` 在 Python re 里是最左优先匹配:短词排在前面会先命中,
把长词吞掉("疑为" 吃掉 "疑为笔误"),导致定位结果把长词截断成短词。
"""
return re.compile("|".join(re.escape(w) for w in sorted(words, key=len, reverse=True)))
def scan_banned(full_text: str, words) –> list[tuple[str, int, int]]:
"""返回 (命中词, 起始下标, 结束下标),供回改定位。"""
rx = compile_banned(words)
return [(m.group(0), m.start(), m.end()) for m in rx.finditer(full_text)]
实测同一句"该标注疑为笔误,另一处疑为尺寸标注缺失。":
- 不排序直接 | 拼接:首个命中是 疑为(长度 2),位置 3;
- 按长度降序编译:首个命中是 疑为笔误(长度 4),位置 3;
- 降序后 finditer 不重叠枚举:['疑为笔误', '疑为']。
两条路径的起始位置相同,但匹配到的词长度不同。对"只报有没有"的门禁没差别;对"改成什么"的回改脚本差别巨大——按短词定位去替换,会把"疑为笔误"的"笔误"两个字留在原地,产出一句更不通的话。
4.4 回改而不是删词
命中之后该做什么,是这道闸门最容易被做错的地方。两个极端都要避开:
- 删词:把"疑为笔误"四个字删掉,问题列变成"标注数值",语义残缺,这条意见等于废了。
- 整条丢弃:把含禁词的那一行意见整行删掉,会连真正属于本职工作的判断一起丢掉——"疑为笔误"底下往往藏着一个真实的不符项。
正确的动作是拦截 + 定位 + 回改:闸门只负责说"第几表第几行第几列、命中哪个词、在全文第几个字符",由人或上游脚本把"疑为笔误"改成"与图例不一致"、"经复核"改成具体条款引用。实测:把样本里的问题列从"标注数值疑为笔误"改成"标注数值与图例不一致"后,同一份文档 –type opinion 从 rc=1 变 rc=0:
$ python audit_delivery_gate.py sample_C_fixed.docx –type opinion
文档类型: opinion(审核意见类)
[OK] sample_C_fixed.docx (意见行数=2;复核列已填=0)
==== 门禁结果 ====
通过 1/1
全部通过,可出文/出包。
rc=0
所以闸门的输出格式要设计成可直接回改的:报表名/行号/列号比只报词有用得多,报字符下标比只报行数有用得多。
4.5 主入口与退出码
import sys
from docx import Document
def gate_one(path: str, doc_type: str = "opinion") –> int:
"""单文档门禁。返回 0=通过;1=存在违例;2=用法/路径错误。"""
try:
doc = Document(path)
except Exception as exc:
print(f"打开失败(非 docx 或文件损坏): {path} -> {exc}")
return 2
words = route_patterns(doc_type)
hits = scan_banned(collect_text(doc), words)
if hits:
for w, s, e in hits:
print(f"[禁词] {w} @字符 {s}–{e}")
return 1
return 0
if __name__ == "__main__":
argv = sys.argv[1:]
dtype = "opinion"
if "–type" in argv:
i = argv.index("–type")
try:
dtype = argv[i + 1]
except IndexError:
print("–type 缺少取值(opinion|system)")
sys.exit(2)
del argv[i:i + 2]
if dtype not in ("opinion", "system"):
print(f"未知文档类型: {dtype}(应为 opinion 或 system)")
sys.exit(2)
if not argv:
print("用法: python audit_delivery_gate.py <根目录 | 单个.docx> [–type opinion|system]")
sys.exit(2)
sys.exit(gate_one(argv[0], dtype))
三个退出码各司其职:0 全部通过可出包;1 存在违例、交付被阻断;2 用法或路径错误(区别于 1 的意义在于——2 是"门没跑起来",不该被当成"内容有问题"去改文档)。
五、闸门接入:退出码是唯一判据
门禁脚本单独能跑只是第一步,真正让它生效的是接入打包链路并把退出码当成唯一判据。三条接入纪律:
第一,不接受"口头通过"。 上游打包脚本必须检查子进程返回码,不允许靠人看一眼终端输出就说"过了"。subprocess.run(…).returncode != 0 就中止出包,不打印"警告但继续"。任何形式的"警告级违例"都会在三周内退化成"默认忽略"。
第二,区分 1 和 2 的处置路径。 返回 1 是内容问题,去改文档;返回 2 是调用问题,去改命令行。把两者混成一个非零分支,最常见的后果是路径写错时反复去改文档,改了半天毫无进展。
第三,文档类型由目录位置决定,不由人记。 不要依赖执行者每次记得敲 –type。可行的做法是让打包脚本按输出目录自动判定:_审核输出/05_正式审核意见 下的走 opinion,体系文件目录走 system,并把判定结果打印在门禁输出的第一行(真源就打印了"文档类型: opinion(审核意见类)"这一行),出问题时一眼能看出是不是类型选错。
接入之后还有一件事:门禁自身也要能被回归测试。每次改词表,都应该拿一批已知结论的样本跑一遍——哪些该红、哪些必须不红。词表越改越大时,真正会出问题的不是"漏了新禁词",而是"新加的词把合法表述误伤了"。
漏词的后果是显性的:它没被拦住,人一眼能看见,改一次词表就补上了。误伤的后果是隐性的:体系文件过不了门,执行者最直接的反应是"这闸门太严了",于是加白名单、加豁免、甚至干脆绕开——闸门在一次次妥协里被架空。所以回归样本里"必须放行"的那些,比"必须拦下"的那些更值得花力气维护。
还有一条经验:门禁的报错文案要写清"怎么改",而不是只写"哪里错了"。禁词命中: 疑为、疑为笔误 这条信息对第一次遇到它的人毫无帮助,他不知道该删、该改、还是该整条重写。真源在体系类的空表违例里就写得更好——“若为空白表单模板,须在上文标注『空白/模板/示例』”,一句话把处置动作说完了。措辞违例同样应该带一句处置提示,比如"疑为/疑是类:改为确定性表述(与图例不一致 / 数值应为 X)"。
最后一点是门禁的执行时机。它必须是出包前的最后一道,而不是起草后的第一道。放在起草后立刻跑,会拦截大量还在编辑中的半成品,噪音淹没信号;放在出包前跑,被拦的都是真的要发给对方的东西,每一条违例都值得处理。真源的定位写得很明确——“出包前必跑、零违例方可出文”。
六、上线前验收清单
按以下七条逐项打勾,全部满足才算这道闸门可用:
第 7 条值得单独强调一次。前六条保证闸门"拦得准",第 7 条保证闸门"活得下去"。一道每次改词表都会误伤一批合法文档的闸门,最终结局一定是被人绕过;而一套稳定的回归样本,能让每一次词表修订都变成"改完跑一遍、全绿就合并"的确定性动作,维护者不再需要凭记忆判断"这次改的词会不会碰坏什么"。
最后回到开头那句话:交付物的措辞不是文风问题,是责任边界问题。禁词闸门的价值不在于它拦下了多少词,而在于它把"这份文档到底是给谁看的、该说哪些话、不该说哪些话"这件原本靠人凭经验把握的事,变成了一条可复跑、可回归、可追责的机器判据。词表要分区、路由要显式、命中要能定位——这三件事做到了,这道闸门才真正立得住。

