蛋炒饭材料学模型,三年之后
Published:
2023 年 10 月,我问了 ChatGPT 一个过度工程化的问题:怎么炒蛋炒饭,才能让每一粒米都裹上蛋,既没有裸露的白米粒,也没有单独抱团、没沾到米的蛋碎?那次对话我存成了一个 HTML 文件,扔进 Downloads 文件夹,然后差不多三年没再打开过。上周我终于把它整理成了一个真正的开源仓库,回头重读那段 2023 年的对话、把它做成代码的过程,比我预想的有意思得多。
这个问题本来就是故意问得很傻,但正因为这样,它才逼出了真正的建模。
蛋炒饭的最高境界包含两个紧密相连的要求:饭粒要粒粒分明,蛋和米也要紧紧裹在一起,让每一粒米都呈现金黄色,既没有裸米,也没有孤立的蛋。这正是中国厨师评判一盘蛋炒饭是否炒得好的综合标准。把它当成一个硬约束提出来,恰恰能把对话从”大火快炒”“不停翻炒”这种模糊的厨房经验,逼到有实际变量可谈的地步。一旦你要求”全覆盖”是一个字面意义上的条件,你就得有一个覆盖率的概念,得有它随时间变化的方式,还得有一个和它对抗的东西的描述。不管你是不是故意的,这就是一个材料学问题。
ChatGPT 中途解错了一道题,但这件事本身很重要。
对话进行到一半的时候,话题一度飘到了”蛋包饭”上——这是一道真实存在但完全不同的菜,一整张蛋皮把一整块米饭包起来,而不是蛋液碎裂成一层涂层,分布在无数颗独立的米粒表面。这种混淆很容易发生:两道菜都追求蛋和饭之间”接触最大化”,但一个是连续薄膜问题,另一个是分散涂层问题。把这两道菜到底在建模哪一道理清楚,之所以重要,是因为它们需要完全不同的物理框架。一整张膜包住一整块米饭,是个几何问题。而蛋液薄薄地铺在成千上万颗互相独立的米粒上,每一粒都被锅铲不断撞向蛋液、撞向彼此,这是一个随机动力学问题。一开始被带偏到错的那道菜,然后又明确地纠正回来,正是后来那个初版模型之所以还能用的原因之一。
2023 年那版模型的核心思路是对的:把蛋液凝固当成一个相变过程,而不是一个温度开关。
蛋白和蛋黄不是”生的”然后一过某个阈值温度就瞬间”熟了”。蛋白质是逐渐变性、交联的,所以凝固的比例,记作 φ(t),是一个连续变量,不是阶跃函数。原始对话里把粘附能设成正比于这个凝固比例,逻辑是:生蛋液几乎不粘任何东西,但完全凝固的蛋会形成真正能粘住东西的膜,所以部分凝固应该对应部分的粘性。然后它又把这个粘附项乘上一个依赖翻炒的”接触概率”,理由是蛋和米得先物理碰上,才有可能粘在一起。两个效应相乘,合成一个”结合概率”。这个骨架是合理的,但所有真正的漏洞,也恰好都藏在这两项里。
原来的凝固动力学是拍脑袋猜的,不是一个真正的机制,所以我把它换成了晶体学里描述结晶过程的那套数学。
2023 年的模型把 φ(t) 处理成一个最简单的一阶弛豫过程,从加热那一刻起就平滑、指数式地逼近完成。真实的蛋白质凝固其实是变性、聚集、凝胶化的叠加,没有哪个闭式表达式是精确正确的,但 Avrami 形式比一阶衰减是更好的一阶近似:当指数 n > 1 时,它能刻画出一阶定律给不出的”先成核、再生长”的特征:φ(t) = 1 − exp[−(k(T)t)ⁿ]。我这里取 n > 1,正是为了让凝固一开始慢、然后加速,而不是一上来就是最大速率;当 n = 1 时,它就退回到原来那个一阶形式。温度依赖性来自一个 Arrhenius 速率 k(T),但在 n 固定的前提下,它主要只是把时间轴拉伸缩放:锅凉一点,跑的是同一条曲线被拉长的版本,而不是一条本质不同的曲线。即便如此,换成 Avrami 也不是装饰性的改动,它改变的是覆盖率随烹饪时间变化的形状,而这个形状会一路影响后面所有依赖时机的东西。
原来的翻炒项,符号是反的,改回来之后,才第一次出现了一个真正的最优解。
这是 2023 年那版对话里我最不满意、也最庆幸自己回头重做的一部分。原来的模型用 P = P₀e^(−I/I₀) 描述接触概率随翻炒强度 I 的变化,意思是翻炒越多,蛋和米之间的接触概率就单调下降,而且完全没给出为什么会这样的机制。这也和厨房里的真实经验直接矛盾:翻炒太少的蛋炒饭并不会均匀结合,只会炒成一块一块互相分离的斑块。重做的版本改成两个互相竞争、都合理地随翻炒强度变化的效应。米粒和蛋碎之间的碰撞频率,大致随 I 线性增长——翻炒越多,接触的机会自然越多。但翻炒不是纯粹正向的:它同时也会把已经粘在米粒上的蛋膜重新撕扯下来,这种剪切导致的破碎效应大致随 I 的平方增长,因为它既取决于碰撞的频率,也取决于碰撞的力度。这两个指数(碰撞取 1 次方、破碎取 2 次方)是一个建模假设,不是推导出来的定律;真正关键的只是破碎随 I 增长得比碰撞更快。在这个假设下,线性的收益、平方的损耗,意味着翻炒强度足够高之后,破碎效应必然反超。这就产生了一个真正的非单调最优解:对于给定的烹饪时间,存在一个具体的最优翻炒强度 I*,能让最终覆盖率最大化,而不是像原来那样,”应该多翻炒还是少翻炒”这个问题的答案永远是”越少越好”或者”越多越好”两个极端之一。翻炒太少和翻炒太多,会因为完全不同的物理原因把一盘蛋炒饭毁掉,这是再普通不过的做饭经验。原来的模型给不出这个结论,重做之后的模型可以。
流程再完美,也补不了蛋本来就不够的坑。
原来的对话完全没有给蛋液的量设预算——按照那个逻辑,只要时间够长、翻炒够到位,覆盖率理论上可以逼近 100%,不管锅里到底放了多少蛋。我加了一个化学计量学意义上的覆盖率上限,θ_max(r) = min(1, r/r_c),其中 r 是蛋和米的质量比。低于某个临界比例 r_c,锅里凝固的蛋白质总量本来就不够覆盖每一粒米,不管火候和翻炒节奏控制得多精准都没用。这是回头看最明显、原始对话也最明显漏掉的一个修正,因为它当时完全是在速率和概率的框架里推理,从来没有回头检查过最基本的质量守恒。
“全部结合”说的是方差,不只是平均值。
最初的问题问的是每一粒米都要结合上蛋,但原来的模型自始至终只追踪平均覆盖率 ⟨θ⟩。可一群米粒完全可能整体平均覆盖率很高,却依然不满足题目本身的要求:一部分米粒完全裸露,另一部分裹了厚厚一层蛋,平均下来是个好看的数字,却不对应锅里任何一粒真实的米。重做的版本给”没有裸露的米粒”加了一个工作判据:既要 ⟨θ⟩ ≥ θc,也要米粒之间的方差足够低,还要对”裸露米粒所占比例”有一个明确的上界。这个裸露比例上界,才是真正把”每一粒”落实下来的那一项,因为它是唯一一个没法被一大群裹得好的米粒平均掉的条件。这是我强加的一个启发式验收判据,不是一个充分必要的物理定义,所以我没有把它写成”当且仅当”。而且它诚实地说是单边的:θ 追踪的是每一颗米粒裹得好不好,所以它管得住”没有裸米”,却管不住对称的那一半”没有孤立的蛋”——后者需要另一个描述游离蛋液质量的变量。即便如此,平均值高但方差也高,正是任何人手忙脚乱炒过一次饭都见过的失败状态——秃斑挨着蛋疙瘩。
仓库里特意留着当年那个错误,没有悄悄抹掉。
original-2023/蛋炒饭的材料学模型.html 是 2023 年那次对话原封不动的浏览器存档,连框架的 JS 打包文件都保留着,逐字节原样保存,没有重新整理或导出。我本可以只把改进后的模型写出来,把原始文件锁进一个私人文件夹里。我没有这么做,因为上面这些漏洞只有和 2023 年当时真实说过的话对照起来看,才有意思;一份符号写反、没有质量守恒的三年前 ChatGPT 对话,比一份被我事后美化过的总结,是更诚实的原始材料。
互动演示上线之后,自己又抓出了一个 bug。
demo.html 是一个不需要任何构建步骤、完全自包含的重做模型可视化:可以调节锅温、翻炒强度、蛋米质量比、烹饪时间这几个滑块,一锅动画里的米粒会随着各自模拟出的覆盖率从白色慢慢变金黄,还有一个实时的数值扫描图表,会针对当前参数找出能让覆盖率最大化的最优翻炒强度 I*。发布之后我才注意到,蛋米比例这个滑块只要拖过大约三分之一的行程,剩下的部分就完全没有任何可见效果了。原因是化学计量上限里的临界比例 r_c 设得太小,覆盖率上限早早就封顶在 1.0 了,滑块剩下那一大截行程其实什么都没做,只是悄悄地失效了。修复方式是把 r_c 放宽,让这个上限的变化能覆盖滑块的整个行程,而不是提前封顶。这是个不大的 bug,但也正因为它不大才容易被忽略:演示看起来完全正常,动画照样在跑,它只是悄悄地对你的一个输入不再有反应而已。
这些其实都没必要做到这个程度。这不过是蛋炒饭。但我之所以愿意回去翻一个三年前的浏览器存档,认真重做整个模型,而不是随手写一篇怀旧小文应付过去,是因为原来那次对话有几个具体、可以指认出来的漏洞——翻炒项的符号反了、没有质量守恒、没有方差判据——而这些漏洞恰好都能用相邻领域里现成的工具修好。这比我预期中能从 Downloads 文件夹里挖出来的东西,要令人满意得多。
想看代码、理论推导,或者原始对话存档,仓库在这里:github.com/ktwu01/egg-fried-rice-materials-model
相关文章:
