工业可靠性:换掉分母
2026年7月15日
我当时在做一台力控工业机器人的操作员界面——一条机械臂,以设定的力把一个带动力的工具压在工件上,沿路径移动。客户问了我这些年被问过的最好的问题:"车间里真正打到我们的问题,一定是我们没想到的那些——'没想到'就是这个意思。可我们每一个可靠性估算,用的分母都是'我们想到的东西'。那你到底怎么保证工业可靠性?"
为什么"再审仔细点"永远收敛不了
审查能找出你想象得到的 bug。而伤到你的 bug,按定义,正是你想象不到的那些。于是你再审一遍——第二遍又找出几个想象得到的,第三遍更少。这个数列不收敛到零;它收敛到"我能想象的一切",而那根本是另一回事。那个项目上,我已经跑了五轮审查,自己的加上几轮独立的冷眼复审,每一轮都还在找出真实缺陷。这不说明代码格外烂——它说明"再审仔细点"是一台跑步机。如果第五轮还在找出 bug,第六轮也会,而你永远没法站在一台正在动的机器前说"没有了"。
数 bug 是一场必输的游戏:分母无限且不可知。出路不是数得更准——而是换掉你数的东西。
换掉分母
整个想法一句话:别再试图保证"我的软件没有 bug";改为保证"即使我的软件彻底地、任意地错了,后果也是有界的"。前者不可能。后者可做到——因为它把一个无限的分母换成了一个有限的。你没法枚举你的 bug,但你能枚举一个 bug 能造成的伤害种类。对这样一台机器,所有坏结果的空间装进五个桶里:
- 有人受伤。 · 机器或工件被损坏。 · 已存数据丢失。 · 系统停机或界面卡死。 · 活儿做错了。
你有多少 bug、多离奇,都无所谓——它们造成的伤害必然落在这五个之一。没有第六种坏日子。于是问题从"我想到了多少 bug?"(分母 ∞,永远做不完)变成"对五个后果类中的每一个,我有没有一个不依赖我自己代码正确的兜底?"(分母 5,有限、可核验、做得完)。最后那句是承重的:一个和 bug 写在同一套代码库里的兜底,是同一个作者给自己的考卷打分。保证必须落到一个有 bug 的软件无法编写也无法削弱的层里。
向我证明这一点的那个发现
在后面一个阶段,界面会把操作员选定的参数——力、速度、路径次数——下发给机器人的运动控制器。承载这个的是厂商随平台模板附带的样板代码。它从第一天就躺在代码树里,没被用过。五轮审查从它上面滑过去了——而且是对的,因为它还没在执行;逐行看,没有哪里"错"。然后我停止审查,改问后果问题:假设这段传输被攻陷——它最坏能干什么,什么拦得住它?
这段样板代码构造发给控制器的消息的方式,是把参数当作文本,粘进一段控制器随后会执行的程序中间——指望一个标准序列化器把它们兜住。可那个序列化器转义了某些引号字符、却漏了另一些。这意味着一个含有特定字符的参数值,可以提前闭合那个带引号的字符串,把它自身剩下的部分变成给机器人的实时指令。顺着路径走:那些参数里有一个是名称——一个操作员敲进去的标签,更糟的是,一个搭着从 U 盘导入的备份文件进来的标签。一个 U 盘上的不可信文件 → 一条末端转着动力工具的工业机械臂上的任意运动。
那个缺陷对五轮逐行审查是隐形的——不是因为审查者粗心,而是因为按"这一行对吗?"的标准,这一行确实对,而且它压根没在运行。而当我从"找 bug"切换到"假设它被攻陷;兜底在哪?"的那一刻,它立刻现形了。没有兜底。一个不可信文件和一台运动机器之间,唯一挡着的,是我自己的代码完美无缺——而那恰是我刚花五轮证明了我无法承诺的那件事。
兜底具体长什么样
- 在你不编写的层里,于远端重新核验限值。我的界面把力和速度钳到安全限值——但那个钳位活在可能发出坏值的同一套软件里。所以控制器再独立钳一次,在它自己那段我不去写的程序里。于是界面就算发 999,机器仍然用 40。远端钳位是权威;近端钳位是礼貌。这一行,就是"我代码对才安全"和"安全"之间的区别。
- 一个软件够不到的物理包络。多数工业控制器能强制一个几何盒子——在安全密码后面配置一次,运动系统本身拒绝越出。把它紧紧设在工作单元四周,那我的软件算出什么坐标就都无所谓了——无论算得对还是发疯——因为那个安全层(我的应用无法编写、无法削弱、甚至看都看不见)会先在墙上把机械臂拦住。对一整类"坏坐标"的 bug,这是唯一真正的兜底,因为对这类 bug 钳位毫无意义:空间里的一个位置,不是一个你能 min/max 的数。
- 分级解锁——把"没想到"变成"安全地看它发生了"。你不会一步从"软件控制机器"跳到"在真件上全力施压"。你穿过一道道闸,每道都把你尚未发现的一切的后果兜住:干跑——机器不动;软件真实运行,并把它本该下发的每一条指令记进日志(一个错参数的代价,是日志里的一行)。空切——真实运动,工具关闭,路径抬到工件上方;一条错路径,在空气里画个形状。废料——在一块你乐意扔掉的料上施真实的力。然后才是生产。你没想到的那个 bug 在每一级都还在里面——这正是重点,你从没移除它——但现实在它后果有界的那一级,把它揭示给你。
这才是工业可靠性真正的样子。不是"机器不会出问题"。而是:当机器出了问题——它一定会——那问题浮现在半空、在工件上方、工具已关闭,没有人受伤。
没人明说、但该说的那部分
这是我认为每一个卖"会动的东西"的诚实供应商都该愿意说的一句话:我们不是朝着"没有 bug"去建。我们知道会有 bug——包括我们永远找不到的。我们建的,是让一个 bug 触发时,它被接住、被兜住、或在物理上根本不可能变成伤害。这听起来像承认软弱;恰恰相反。承诺无 bug 软件的人,要么天真,要么在推销。而给你看那些闸、两道独立钳位、和机械臂出不去的那个盒子的人,已经想过它出错的那一天——那是唯一要紧的一天。"无 bug"是个营销词;"后果有界"是一种设计。
写自一次真实的力控机器人集成;所有客户、厂商、产品细节均已抹去。方法是通用的——它适用于任何"软件失败会带来物理或不可逆后果"的场景,从运动控制到支付,到部署。
这套东西的出处
这些都不是我的发明——它是安全工程里成熟的学科,只是换成了大白话:
- "测试只能证明 bug 的存在,不能证明它的不存在。"——Edsger Dijkstra,1969。"你没法测你没预见的东西"的源头。
- Nancy Leveson,《Engineering a Safer World》(MIT Press)——为什么安全不等于可靠,以及为什么你去约束系统,而不是试图让每个零件都完美。
- IEC 61511 — 保护层分析(LOPA):独立兜底背后的工业标准(即使控制系统失效,安全层也必须继续工作)。
- "瑞士奶酪"模型(James Reason)——为什么多个独立层能接住任何单独一层漏掉的东西。
这套论证有一个可运行的公开实现。后果分类、每类的硬前置条件、运动过程中持续复检的看门狗,以及一个八场景 harness——每次都试图让机器出错,每次都失败。可以用 MQTT 指挥它,也可以点浏览器里的 HMI。 代码 →
如果你造的机器需要一套界面、一个设备连接,或者数据必须落到别处去——告诉我它现在正让你付出什么代价。你会得到一个诚实的判断:能不能解;通常还会收到一个能跑的东西。从这里开始 →