对照仿真:难点不在定价公式,在"可比性"¶
算法实验室 TSP 复盘 · 第 2 篇。experience-fair 这个模块要回答的问题是:把主观负效用(Pain)计入刚性成本之后,租金、交易量与群体效用的均衡会怎么变?这本质是一场对照实验。定价公式一行就写完了,真正决定结论能不能见人的是另一件事:让两模式的差异可以归因。
现象¶
模型本身不复杂。履约者接单有边际成本 ΔMC(最优可行插入的绕行距离)、客观溢价 Premium、每单保留底线 Umin,成交门槛:
- MONEY_ONLY:θ = ΔMC + Premium + Umin(Pain 外部化)
- EXPERIENCE_FAIR:θ = ΔMC + Premium + Umin + Pain(Pain 计入刚性成本)
其中 Pain = painCoef × (绕行距离 + 2×svc),是履约者的主观负效用。
直觉上,"实验"就是两个模式各跑一遍、对比汇总数字。但跑完第一遍就会撞上一个没法回答的问题:跑出来的差异,凭什么说是 Pain 内部化造成的,而不是仿真器自己的抖动?
我一开始的错误判断¶
这一篇的记录比较特殊:仓库里留着当时的实施规划文档,所以哪些是踩过的坑、哪些是提前防的,有据可查。按记录,真正的错误判断有两个,都发生得更早:
第一个:以为框架是负担,越零依赖越好。 这个问题原话记录在实施规划里——"框架是资产还是负担"。规划里的推演是:零依赖单 HTML 做第一种算法最快,但"第三种开始就要在多个 2000 行文件间复制图表代码——那才是负担";框架的成本是一次性脚手架,收益随算法数量线性增长。最终采用了框架化,但附带一条铁律:仿真引擎是纯 TS,永远不知道 React 的存在。回头看,这条铁律是后面一切可比性纪律的地基——引擎不知道 UI 的存在,才谈得上在 Node 里同 seed 复跑、才谈得上对照。
第二个:以为主要工作量在公式。 门槛公式一行就写完了,但从实施规划的篇幅分布能客观看出真正的大头:订单/履约者世界生成、带硬约束的插入路由、四步匹配流程、转单接受条件、守恒式验收——公式只是其中一小节。"公式正确"和"结论可信"之间隔着整整一层协议。
还有一点要坦白:这一篇没有"踩坑后修复"的戏剧性故事。 转单优化的接受条件——"交换/转移后总 S 不下降、硬约束合规、总 Pain 严格下降才接受"——在实施规划的任务书里就是这么写的,是规格先行,不是事后打补丁。为什么这条必须写在规格里,是根因一节的主题:因为这类错误发生时不会报错。
排查与关键证据¶
把确定性变成显式协议。 matcher.ts 的头注释现在写着的就是这套协议:
每轮四步:到达批次 → 试指派 → 转单优化 → 定价锁定。 全流程确定性:排序均有并列决胜(S 降序并列按 id 升序等),无随机数。
每一步的决胜规则全部显式化:试指派按 S(边际消费敏感度)降序处理;插入方案取 Pain 最小,并列取 ΔMC 小,再并列取履约者 id 小;定价锁定再按 S 降序过一遍。浮点比较全部走 EPS,"相等"不靠运气。验收清单里"同种子双模式确定性"是独立一项——两模式用同一种子的同一世界深拷贝,这是"唯一变量是 Pain"的字面保证。
把转单接受条件做成纯函数 + 反例测试。 四条接受条件,全部满足才接受:
- 涉及订单未锁定(已锁定的不许动);
- 移动后硬约束全部合规(取件时间窗 [r, r+30]、逐段车载容量、班次窗);
- 参与订单的总 S 不下降——不许用丢单换优化;
- 暂挂订单总 Pain 严格下降。
它独立成 checkTransfer 纯函数,测试把每个拒绝路径各钉了一个反例,用例名自述:
- 涉及订单已锁定 → 拒绝(swap 场景)
- 移动后硬约束违规 → 拒绝
- 参与订单总 S 下降(swap 丢了一单)→ 拒绝 ← 规格里"总 S 不下降"的实物化
- 总 Pain 不降(相等)→ 拒绝
- 总 Pain 上升 → 拒绝
- 合规且总 Pain 严格下降 → 接受
用守恒式给记账上锁。 验收清单里专门有一组守恒式断言:补贴消耗 = Σg、交易总额 = ΣB、FAIR 模式外部租金恒为 0 且成交单效用非负、MONEY 模式要构造出效用为负的案例。最后这条最容易被漏掉——如果 MONEY 模式跑不出负效用履约者,说明模型参数根本没制造出"受损群体",两模式的对照就成了空转。
对照组探针。 指标里专门设计了两个直接度量公平的量:负效用履约者占比(真实效用为负的活跃履约者),和高 S 流单量(S ≥ 0.75 的流单)——前者回答"成本被转嫁给了谁",后者回答"最需要这单的人是不是被挤掉了"。
诚实标注近似。 README 的"已知限制"第一条:转单为贪心局部搜索(首改进,上限 200 次迭代)非全局最优。这条必须写——结论的适用范围由它定义。
补测实验:换 seed 复跑(成稿时补做,可复跑:npx vitest run src/evidence-sweep.test.ts)。 "换 seed 结论稳不稳"不能靠嘴说,成稿时把同一组 12 个固定 seed(与游戏平衡测试同源)双模式各复跑了一遍,逐行数据落盘在 data/seed-sweep-experience-fair.json。结果分三档,比"全部成立"更有信息量:
- 结构性结论,12/12 seed 成立:FAIR 外部租金恒为 0(定义使然,复跑的意义在于确认实现没漏);FAIR 无负效用履约者。
- 方向性结论,出现即成立:MONEY 的负效用履约者在 12 个 seed 中只出现 7 次——不是每个 seed 都会长出受损群体;但出现的 7 次全部集中在高 painCoef 群体(负效用者平均 painCoef 高于全体活跃履约者均值,7/7)。
- 量化结论,11/12 seed 成立:FAIR 履约者总效用和高于 MONEY(11 局)。唯一例外是 seed 123:FAIR 低约 0.5%,同时少成交 2 单、补贴消耗 295/300——补贴池吃紧时,FAIR 的优势会被池约束吞掉。两模式成交数之比 0.985。
换 seed 复跑的价值就在第三档:它把"结论成立"细化成了"结论成立的边界在哪里"。
根因¶
无约束的局部搜索会把"违约"转嫁成"目标改进",而且发生时一声不响。 在这个模型里,丢掉一单是最便宜的降 Pain 方式:swap 之后某一侧插不进去、订单直接消失,总 Pain 立刻下降,汇总指标全面变好,高敏感度订单被系统性丢弃——整个过程没有任何报错,每个指标都在"改善"。这就是为什么这条约束必须写在规格里而不是等测试暴露:运行期的你只会看到一切都在变好。 检验这类设计的方法不是调试,是把优化器当对手做推演——它会找哪条最省力的路?堵上,再推演,再堵。
对照实验里,每一个未显式化的决策都是一个隐藏自由度。 遍历顺序、并列决胜、随机数、浮点相等判断——自由度不消灭,A/B 差异就不可归因:你以为在度量"Pain 内部化的效应",实际在度量仿真器的噪声。
约束与取舍¶
- 放弃全局最优。 贪心首改进 + 200 次迭代上限:代价是只能证明收敛到"局部改进不动点",README 已知限制里永久挂账;换来同 seed 严格可回归、两模式严格可比。
- 放弃随机化重启。 代价是可能停在较差的局部解;换来确定性。对对照实验而言,可比性 > 单点最优性。
- 放弃轮内过程快照。 回放粒度只到轮快照,轮内细节要翻事件日志拼:代价是复盘麻烦;换来状态量可控、快照结构稳定。
可复用检查清单¶
- 对照实验开始前,先列"唯一允许不同的点"清单;其余一切(排序决胜、遍历顺序、随机源、浮点相等)显式化,"同种子同一世界深拷贝"写成验收项。
- 优化器的每条接受条件配一个"必须拒绝"的反例测试;最容易漏的是"用丢资源换指标改进"这一条——它不会报错,只会让指标变好。
- 多目标优化用字典序约束(先保 X 再降 Y),不用加权合成——权重就是隐藏自由度。
- 给记账上守恒式断言(消耗 = Σ缺口、总额 = Σ出价),并专门断言"对照组必须出现要研究的现象"(如 MONEY 模式必须存在负效用履约者),防止对照空转。
- 每一处放弃的精确性(贪心、快照粒度、迭代上限)写进已知限制,说明换来了什么。
- 结论发布前换 seed 复跑,并把"哪几条 12/12 成立、哪几条有例外"分开写——例外往往指向边界条件(本篇的例外指向补贴池容量),这比"结论稳健"四个字有用得多。
记录核对情况(发布前删除本节)¶
已按仓库记录落实:
- 框架之争的原话与推演、引擎纯度铁律:
.zcode/plans/(experience-fair 实施规划 §一"回答你的问题")。 - 转单接受条件为规格先行(非事后补丁):同上 §三.3 原文"交换/转移后总 S 不下降、硬约束合规、总 Pain 严格下降才接受,迭代至上限 200 次或无改进"。
- 验收清单(含同种子双模式确定性、守恒式、MONEY 模式 u<0 案例构造):同上 §五。
- 其余代码与测试证据:matcher.ts / matcher.test.ts / simulate.ts / README。
对上一稿的更正:
- 上一稿"第一版只比较总 Pain、丢单后补 sDown 约束"为错误重建,与任务书记录矛盾,已删除。本篇现在如实呈现为"规格先行的预防性设计"。
仍待补充:
- 无。"换 seed 复跑"已由补测实验完成(见"排查与关键证据"末节),原占位撤销。
引用出处¶
.zcode/plans/(experience-fair 实施规划 §一、§三、§五)src/algorithms/experience-fair/engine/matcher.ts(头注释、checkTransfer、runTransferOptimization)src/algorithms/experience-fair/engine/matcher.test.ts(用例名)src/algorithms/experience-fair/engine/simulate.ts(纯函数头注释、computeMetrics)src/evidence-sweep.test.ts(补测实验 A:12 seed 双模式复跑)docs/articles/data/seed-sweep-experience-fair.json(逐 seed 数据 + 复现命令)README.md(模型速查、硬约束、指标、已知限制)