WRITING
我编了六个数,所有的门都是绿的

把别人的表格变成我的配置,最危险的动作不是算错。是在原文没给数的地方,填一个看着合理的数。
算错会被抓到。除法写反了,余额对不上,测试立刻红。可编出来的数不会——它和忠实转录的数在产物里长得一模一样。同样的字段,同样的量级,同样的排版。系统照常运转,账照常对得上,页面照常好看。
这周我给孩子做了个积分系统,八十三条规则,一张表结构化成一份配置。所有的门都是绿的。里面有六处是我编的。
一、先说这东西是干什么的
我给孩子做了个练习平台,语文数学的错题从他自己的试卷里长出来。练习做多了得给个回报,于是加了积分:做对题涨分,考试优秀涨分,跳绳跳够了涨分,攒够了自己去兑换。做成股票盘面的样子,看着数字一直往上走,那种爽。
积分的规矩不是我拍脑袋定的。家里先有一张《积分条例》,Excel 表,按学校科目分区,语文数学英语科学体育道法劳动一直到「爸爸特别奖励」,奖分扣分并列。这张表是权威,我的活是把它变成机器能执行的东西。
八十三条规则落成 rules.json。五种形态:固定分、按个数算、家长在区间里填、查档位表、换电视时间。分值一律服务端算,前端报什么都不算数。档位表达式用 ast 白名单求值,绝不用 eval——配置文件里的表达式是能被改的,eval 等于把任意代码执行权交出去。
然后是门。服务端接口门八十七项:匿名调接口拦不拦得住、家长密码错了加不加得了分、区间填超了报不报错、刷题积分连刷十二次会不会穿过每日上限、同一笔撤销两次认不认、表达式求值器喂 __import__ 和 __subclasses__ 会不会被打穿。真浏览器冒烟四十六项:没输密码点加分市值动不动、一道题不做直接交卷给不给分、特效层吃不吃点击。
全绿。
二、六个数
那天我把这套东西交出去之后,回头做了一件本来该在交出去之前做的事:找几路人手,分头对着原表逐个科目核一遍。
核出来六处。
| 原表写的 | 我写进配置的 |
|---|---|
| 「没有完成基本卷面的扣 10 以上」——只有下限 | 补了 −30 的上限 |
| 「其他酌情加分」——没给数 | 补了 1~10 |
| 「爸爸特别奖励」——就这六个字 | 补了 1~500 |
| 「仰卧起坐……约定时间内每增加 5 个得 1 分」 | 读成「约定个数」,还给它造了个输入框 |
| 「作文 300 字之内奖 2-10 分」——家长自己定 | 做成了按字数的公式 |
| 「或者相应奖励看影视时间」 | 整条漏掉 |
这六处里,没有一处是算错。
第一处,原表说扣十分以上,我心想总得有个底吧,不能一次扣光,−30 听着挺合理。第二处,「酌情」这两个字机器执行不了,总得给个区间,110 听着挺合理。第三处,「爸爸特别奖励」孤零零六个字,我给了 1500,因为这是特别奖励嘛,得有点分量。
每一步我都在做一件看起来很负责的事:把一个没法执行的描述,补成一个能执行的规则。
问题是我补完之后什么都没说。那六个数就那样躺在配置里,和旁边那些真转录的数排在一起,一模一样的格式。
三、为什么八十七项门一项都没红
因为它们问的全是同一类问题。
字段齐不齐、权限拿不拿得住、余额对不对得上、上限刷不刷得穿、注入打不打得进来——每一道门都在问「这个系统运转得对不对」。
没有一道门在问:这个数是从哪儿来的。
这是转录类工作特有的失效模式。它不制造异常。它制造一个自洽的假:跳绳三组一百九十个,页面弹出 120 分,账本记一笔,余额涨上去,孩子高高兴兴。整条链路上没有任何一个环节有理由怀疑那个 120 是不是表里写的。
我后来想明白一件事:凡是「把 A 转成 B」的活——法条转成检查项、规范转成参数、合同转成报价单、别人的表转成我的配置——机器门天然只能验 B 自己自洽,验不了 B 忠不忠于 A。要验忠诚,得把 A 也搬进来。
四、七个档位,我核过,我以为没问题
跳绳那条规则有七个档位:三组一百九十个以上一百二十分,两组一百九十个一百分,一组一百九十个八十分,三组一百八十个五十分……原表末尾还加了一句「不重复累计」。
「不重复累计」这五个字的实现方式,是档位表有序,从上往下第一个命中的就返回。三组一百九十个必须先于三组一百八十个匹配——顺序错了,孩子跳得更好反而拿得更少。
这七个档位我自己核过。逐条对着表读了一遍,觉得没问题。
后来那几路校对把每个档位的边界逐个试过去,才敢说全对。
自己核自己不算。 这不是谦虚,是机制问题:我核的时候,脑子里装的是我当时怎么理解那句话的;我要找的错,恰恰是「我当时理解错了」。用同一个理解去验同一个理解,两边一起错,永远绿。
这和验算题目是一回事——出题时用公式算出答案,验算就不能再用同一个公式,得穷举一遍真做除法。复用同一条路径去「验证」,等于测了个替身。
五、于是我给它建了道门
思路很简单:每个数值回原表搜,搜不到的,必须在那条规则上写明「这是我们定的」。
想编个数进去而不留痕,结构上做不到。
第一版三十行就写完了:把原表导成纯文本,把配置里每条规则的 pts、min、max 拿出来,在文本里 grep 一下。搜得到放行,搜不到又没写说明就红。
然后我跑反向验证——把编造的数原样放回去,看它红不红。
六处里有四处是带数字的:−30、110、1500,还有后来补「影视时间」那条时我自己定的 60 分钟。剩下两处(读成「约定个数」、把区间做成公式)数字本身是忠实的,错在别的地方,这一版先不算。
四处只抓到两处。

漏掉的那两处很有意思。−30 放行了,因为原表里确实有 30——在「400 字以上奖 30 分」里,在「扣 2 分奖 30」里,在「2 组 180 个 30 分」里。三处都不是它。跟卷面分一点关系都没有。
1~10 更惨,原表里遍地都是 10。
那一刻我看清了第一版在验什么:它证明的是这张表里有 30,不是这条规则有 30。这两句话看着差不多,一个是真的门,一个是发绿灯的机器。
如果我没跑反向验证,我会以为我建了道门。它会一直绿,一年后我还会指着它说「出处都查过了」。
第二版加了定位:先按规则的中文名,在原表里找到它对应的那个格子——中文没有词边界,就切二元组算重合度——然后只在那一格里搜数字。
三处。
还剩 1~10。原表那一格里写的是「主动帮助洗菜、烧菜等加 2-10 分,其他酌情加分」。两条规则挤在一个格子里。我要查的是后半句,可前半句的那个 10 把它蒙混过去了。
第三版把格子再切成子句,匹配度相同时取最短的那段——本意是让「其他酌情加分」这六个字自己去当定位目标,别挂靠在邻居的分值上。
四处,全抓到。

可这一处是撞对的。
这是写这篇文章、回头去核每一句的时候才发现的:我的切分只切分号和句号,不切逗号。而「其他酌情加分」前面正好是个逗号——那六个字从来没成为过候选段。它真正定位到的是体育那一行的「其他运动酌情奖 5-20 分」,那一格恰好没有 10,于是红了。
结论对,理由不对。
我试着把逗号也切进去。定位确实精确落到那六个字了,可同一批逗号在别处只是隔开主语和分值——「作业未完成、上课不认真被老师点名批评的,每一次扣 10 分」——一切开,分值就留在了另一半,当场冒出五条假红。这五条全是忠实转录的规则。机器分不出这两种逗号。
所以逗号那一版我撤了,把这个已知的不精确写进了代码注释和反向验证清单。一道会哭狼的门迟早被人关掉,宁可留着看得见的不精确,也不能制造假红。
更值得记的是另一层:门抓对了,可我说不清它为什么抓对,直到有人拿着原表来核它。门本身也需要被核。
六、门第一次跑,抓了我一条我自己都不知道的
上线跑第一遍,它红了。红在语文考试那条。
原表写的是「扣 1 分奖 50 分,扣 2 分奖 30,扣 3 分奖 10 分」。我做成了按分数查档:99 分给 50,98 分给 30,97 分给 10。
换算没错——满分一百,扣一分就是九十九。可原表里确实没有 97、98、99 这三个数。它们是我推导出来的。推导得再对,也是我推的,不是表上写的。
门逼我把这个推导写进了 note。现在那条规则旁边挂着一行字:「原表写的是扣 1 分奖 50、扣 2 分奖 30、扣 3 分奖 10,按满分 100 换算成绝对分数 99/98/97」。
半年后我再看这份配置,不用重新推一遍,也不用去猜当时是不是抄错了。
还有一条更能说明问题:「爸爸特别奖励」那个 1~500,我最后没改。
值一模一样。变的是它旁边多了一行「原表只有这六个字,没给分值,1~500 是我们定的操作范围」。
规矩从来不是禁止自己定值。 那不可能——「酌情」两个字机器执行不了,配置总得能跑,总得有人把它变成一个数。规矩是禁止不留痕地定值。
差别就在一行字。可就是这一行字,决定了这份配置半年后还是不是可信的。
七、这条规矩最后落在哪儿
想让一条规矩真的生效,它得落在能拦住人的地方。写进文档不算——文档只承载判断,拦不住手。
所以落了三层,从硬到软:
最硬的一层是那道门,接进了项目的一键检查里。改配置、加规则、上线前,它都会跑。它不讲道理,也不听解释:数指不回原文,又没写说明,就是红。
中间一层是我给自己所有项目定的通则:凡是转录别人的文档——表格、法条、规范、合同、报价——产物里每个数值都必须能指出原文出处。指不出的,要么当场标注「这个值是我们定的」,要么去问对方。禁止填一个看着合理的值,禁止造原文没有的字段,禁止把区间读成公式。
最软的一层是一条笔记,记着这次到底是怎么错的,给以后的自己看。
还有一件顺手做的事:那张原始 Excel 也进了版本库。
仓库本来是排除 .xlsx 的,我给它开了个例外。理由很简单——它不是生成物,它是这份配置的一手源。丢了它,那道门就成了瞎子:没有原表,它连「这个数在不在原文里」都判不了。所以我让它在原表文件不存在的时候直接退出报错,拒绝在空集上报绿。
一道在没有数据时会假装通过的门,比没有门更危险。
八、有一个数我也改了,但那不叫编
原表第一行写着:「1 分可兑换 1 毛,10 分兑换 1 元。」
我没按这个做。我按 100 分兑换 1 元。
按表里的分值估一下:正常表现一个月大概能挣一千七百分。十比一的话就是一百七十块一个月,一个三年级小孩的零花钱,这个数我判定偏高。所以我把汇率压了十倍。
这也是一处「产物和原表不一致」。但它和那六处的区别不在于对错——在于我知道原表怎么写的,并且明确否掉了它。这个决定留了痕,能被翻出来,能被推翻。
那六处不行。那六处的问题不是数值本身不合理——−30 和 1~10 单看都挺合理的,真让人定说不定就定这个数。问题是没人做过那个决定。它们不是被判断出来的,是被填出来的。
一份配置里最危险的,从来不是那些明显错的数。是那些没有人对它负过责、却长得和别的数一模一样的数。
九、这道门管不住什么
得说清楚它的边界,不然它就成了另一个假绿。
它只查数值有没有出处。它查不到「文字描述读没读对」。
回头看那六处:把「约定时间」读成「约定个数」,它抓不到——因为那处压根没有数值错,错的是我给它造了个输入框。把「300 字之内 2-10 分」读成按字数的公式,它也抓不到——2 和 10 都在原文里,忠实得很,错的是那两个数的身份:原文里它们是一个让家长自己拿主意的区间,被我读成了公式的两端。
六处里,这道门只防得住其中一类。
剩下那类是语义,机器分不出来。只能自己对着原表一句一句读,或者找个不知道我当时怎么想的人去核。
所以最后沉下来的不是一道门,是一条顺序:
先转录,再自定;自定当场留痕;上线前找个外人对着原文核一遍。
前两步机器能帮你兜住。第三步兜不住——因为要核的恰恰是「我以为我读懂了」。
而这一步,正是我这次省掉的那一步。
相关文章
订阅
新文章都先发在这里。用 RSS 订阅: /feed.xml