跳转至

我给模拟器写了一条「我的政策会输」的测试

「给个人账本引擎挑刺」系列 · 第 5/5 篇。模拟器最大的风险不是不准,是讨好。我用一条性质测试钉住"政策会输",结果发现报告层还在嘴硬——负数照常庆祝"纪律的复利"。

TL;DR 三条性质测试(恒等、支配、可输)把"沙盘不是预测"从口号变成可断言的事实;但扫描实验显示结论行是硬编码的庆祝语,负数也照念。诚实性是一条链:模型 → 测试 → 渲染 → 文案,断在最后一环,前面全白守。

模拟器最大的风险不是不准,是讨好。

现象

我的模拟器测试里,有一条专门证明"节流+开源"(C 场景)会输给"只节流"(B 场景):

#[test]
fn skill_investment_can_lose() {
    // 理论性质:技能投资回报率太低时,C 不如 B —— 沙盘必须诚实地把它暴露出来
    ...
    assert!(last.b > last.c, "低回报技能投资应输给纯节流: B={} C={}", last.b, last.c);
}

(src/sim.rs:141)

另一边,月报的结论行是硬编码的庆祝语:结论:N 个月后,C 比 A 多 ¥X —— 这就是「纪律的复利」(src/report.rs:160)。两条线在参数扫描里撞车了——见证据三。

我一开始的错误判断:把免责声明当成了诚实

物证是同一份输出里的两行字:src/report.rs:164 一直打印「提示:模拟基于你的滚动历史,是沙盘不是承诺」,而它上面几行就是硬编码的庆祝语。声明一直都在,性质却没落到测试与渲染——免责声明是写给读者的,性质测试才是写给未来的自己的;只有前者的时候,它只是文字。

排查与关键证据

证据一:三条性质测试,各管一种"不许讨好"。(src/sim.rs:131-157)

  • 恒等性(:150):拦截率归零时,B 必须与 A 完全一致——政策参数是摆设时,模型必须退化为现状,一条等式钉死;
  • 支配性(:131):拦截率大于零时,B 必须不输 A——节流不可能更穷;
  • 可输性(:141):收入增速足够低时,C 必须输给 B——沙盘要主动暴露"技能投资的盈亏平衡点"(docs/DESIGN.md §6 明文要求"沙盘必须诚实地暴露这个盈亏平衡点")。

证据二:盈亏平衡点是真实的、可复现的。(演示数据,sim --months 24 扫描收入月增速,docs/drafts/evidence/gsim-sweep.txt)

收入月增速(bp) 24 个月后 C − B
0 −16863
25 −8429
50(出厂默认) +320(≈0.16%)
75 +9398
100 +18818

默认参数下,"节流+开源"对"只节流"的优势是 0.16%——沙盘在出厂设置上几乎打平,而且它如实告诉你。

证据三:模型学会了诚实,报告还没有。 同一份输出在增速为 0 时打印:

结论:24 个月后,C 比 A 多 -¥14732.00 —— 这就是「纪律的复利」。

负数照旧庆祝。可输性活在测试里,展示层还在嘴硬。

根因

"沙盘不是预测"如果只是免责声明,就没有任何约束力——下次改参数、加功能时,它会悄悄变成许愿机。把它落成可断言的性质(确定性、恒等性、允许失败)之后,模拟器才无法讨好:任何人改动了模型,测试会立刻告诉他"你把盈亏平衡点弄丢了"。

证据三则说明另一半:性质必须覆盖到用户看见的最后一层。模型层的诚实会被一行硬编码的文案出卖——而文案恰恰是用户唯一会读的部分。

约束与取舍

  • 放弃了随机性与波动建模(不引入蒙特卡洛、不模拟历史波动)。换来:同输入必同输出、性质可测、每个数字可解释。付出:长周期结果系统性偏移(固定收益率、无通胀假设,docs/DESIGN.md §9 明文承认"是沙盘不是预测")。
  • 定位决定取舍:对"个人沙盘",可解释性 > 拟真度——一个能被性质钉住的简化模型,好过一个无法验证的"逼真"模型。

可复用检查清单

  1. 每个模拟/预测器至少配一条"反直觉性质"测试:存在某输入让推荐政策输。
  2. 恒等性测试:政策参数归零必须精确还原现状。
  3. 假设声明(固定收益率、无波动、无通胀)写在结论句旁边,不是脚注。
  4. 展示层的结论句式必须处理反例输出(负数、打平),禁止硬编码庆祝语。
  5. 基线窗口(取哪几个月、含不含当月)显式单测,防"新用户无数据"与"当月污染基线"两类边界。

效果主张声明

任何"沙盘改变了我的真实决策 / 帮我省了钱"的叙事,在真实账本积累出可用样本前不写。本文只主张机制与性质——它们全部可复现。

证据与复现

cargo test 可验证三条性质测试;GSIM 扫描输出见仓库 docs/drafts/evidence/gsim-sweep.txt,一键重跑:bash docs/drafts/evidence/run-experiments.sh。扫描基于 seed --demo 的内置虚构数据。


本文是「给个人账本引擎挑刺」系列第 5/5 篇:①纯函数内核的污染 → ②没有事务的账本 → ③"72 小时冷却"的真相 → ④三种"没数据" → ⑤让政策会输的测试