随机性
参伍从带命名和版本的随机流中抽取随机数。每条随机流归声明它的机制所有,每次抽样都会被记录下来。因此,相同的运行输入总能得到相同的结果,重新加载存档也无法重掷已有结果。准备给插件加入随机性时,可以参考本页。
一次抽样:从请求到重放
Section titled “一次抽样:从请求到重放”查看图表源码
flowchart LR
System["边界系统<br/>声明随机流"] --> Draw["random_range 或<br/>random_sample_for_operation"]
Draw --> Record["RandomDrawRecord<br/>流、地址、上界、<br/>结果、用途"]
Record --> Commit{"边界<br/>提交?"}
Commit -- "是" --> Saved["快照与<br/>边界证据"]
Commit -- "否" --> Undo["抽样记录与流位置<br/>一起回滚"]
Saved --> Replay["重放时逐次<br/>重算并比较"]
一个 RandomStreamKey 由 namespace、name 和 version 组成,例如:
canwu.core / knowledge-report-delay / 1内核用运行的根种子和随机流的键派生出每条流的种子。每个机制从自己的流中抽样,所以给一个系统增加一次抽样,其他系统拿到的数值保持不变。流状态保存算法、派生出的种子、当前位置和生成器状态。
新建的随机流使用 SplitMix64V2,用无偏的拒绝采样把原始值缩小到请求的范围。记录为 SplitMix64V1 的流仍按取模方式缩小范围,旧日志因此能按原来的数值重放。算法属于存档格式的一部分:要改变某条流的抽样方式,需要发布新的流版本。
先声明,再抽样
Section titled “先声明,再抽样”边界系统只能从 BoundarySystemContract::random_streams 中列出的流抽样。系统运行时,内核给它一个随机会话,里面是这些流的副本。从其他流抽样会以 UndeclaredRandomStream 失败。
每次抽样都要求:
- 上界大于零;
purpose非空,且首尾没有空白;- 对顺序抽样而言,流位置还能继续递增。
系统拿到的是一个有界整数,生成器状态留在内核内部。
操作定址随机抽样
Section titled “操作定址随机抽样”顺序抽样 SimulationView::random_range 从流中取下一个值。系统总按固定顺序抽样时,这就够了。可能重试、回滚或跨阶段继续的机制,应改用操作定址抽样:random_sample_for_operation、只返回数值的 random_range_for_operation,或者还会返回抽样证据 ID 的 random_draw_for_operation。
操作定址抽样按它所服务的操作来定位。它的地址由产生抽样的插件、操作种类、应用的操作 ID、目标和抽样槽位组成。目标是一个 RandomOperationTarget:实体、精确的决策票据版本、精确的领域记录版本、知识持有人,或一个规范键。抽样还要引用引发它的证据。内核根据根种子、随机流、地址、上界和用途计算数值,所以无论之前抽过多少次,结果都相同。用相同的地址、证据、上界和用途再次请求,会得到相同的值;用同一地址配不同的证据、上界或用途,则以 RandomOperationConflict 失败。
随机决策策略也走这条路径:RandomOperationTarget::DecisionTicket 把抽样绑定到精确的票据版本,DecisionOptionWeight 把抽到的值映射到票据中已有的某个选项,ResolveDecisionRandomly 指令让内核为下一个边界生成一条普通决策。前置规则效用策略的随机平局决胜也使用同一路径,只在分数接近的最高选项之间抽样。
控制者(controller)的策略种类(Utility、Rule、Random、Human、External 或 Llm)在注册控制者时确定,用同一个控制者 ID 再次注册会失败。随机决策策略只在票据选项之间做选择;天气、故障、疫情等世界事件应由领域系统从自己的随机流中抽样。
引用算出结果的那次抽样
Section titled “引用算出结果的那次抽样”SimulationView::random_draw_for_operation 的参数与 random_sample_for_operation 相同,返回 RandomOperationDraw { draw_id, sample }。其中 draw_id 是这次抽样的证据 ID(RandomDrawId),系统写出的记录或事件可以用它指明自己是根据哪一次抽样算出来的。
- 地址在更早的边界已经抽过时,
draw_id就是那次抽样的 ID,不论它的抽样记录仍被保留还是已经封存。 - 首次抽样时,
draw_id是边界提交后这条抽样记录将得到的 ID。边界失败时,抽样和系统提出的内容都不会提交,所以一个 ID 只会和它所指的抽样一起被保存。精确重放分配的 ID 完全相同。
当前边界里做的抽样,要等这个边界提交之后才算已保留的证据。在同一个边界里,你可以把它的 ID 写进记录的载荷或事件,也可以把它列入记录的身份证据依赖。但在下一个边界之前,evidence_exists 对它返回 false,引擎当作证据来校验的位置(知识来源、评估轨迹的项和人物来源证据)不接受它,它也不能充当另一次操作定址抽样的操作证据。
封存证据之后的重试
Section titled “封存证据之后的重试”完全相同的重试,即地址、证据引用、上界和用途都相同的请求,总能拿回已记录的抽样,不论它引用的证据后来怎样。所以,用 seal_evidence 封存过证据的模拟运行,对重试的回答与未封存时的精确重放完全一致。与已记录抽样不一致的请求以 RandomOperationConflict 失败。
只有首次抽样才要求操作证据在读取切面上存在:要么是仍然保留的证据,要么是同一边界里更早产生的证据。如果你的系统可能在封存之后才第一次抽某个地址,请引用封存后仍然存在的证据,例如某条记录的初始场景版本,或者比封存更新的证据。首次抽样以 InvalidRandomOperationEvidence 被拒绝时,要把错误原样返回,让边界失败。处理器如果吞掉这个错误,结算出来的边界会被这次模拟运行自己的精确重放拒绝,因为重放时证据还在,抽样会照常进行。
抽样之后还有哪些开销
Section titled “抽样之后还有哪些开销”引擎用操作定址抽样明细来回答操作定址请求,它记录模拟运行抽过、尚未退役的每个地址,无论抽样证据仍保留还是已封存。抽样明细以持久化映射保存在内存里,边界开始时先丢弃在该时刻或之前退役的抽样,提交时续写,边界失败时恢复,派生分支和封存共享它,恢复时只重建一次;接续检查点会带着其中在它的时刻尚未退役的抽样。抽样明细不计入哈希,操作定址抽样也不再是证据依赖,所以结算边界、写检查点和分页检查点、封存和派生分支的工作量都与模拟运行已经做过多少次抽样无关。
仍有一项开销。模拟运行抽过的每个地址在抽样明细里占一个条目(在记录的测量中每个条目约 522 字节,随机流名称、操作 ID 和目标越长,条目越大),直到该抽样退役;随机流没有声明定址抽样退役期(keyed-draw retirement)时,直到运行结束。
拥有随机流的边界系统在 BoundarySystemContract::keyed_draw_retirement 中为它声明一条 KeyedDrawRetirement { stream, after }。它属于插件描述,所以运行只能用相同的声明恢复和重放。该流的抽样在其边界时刻加 after 时退役,之后的第一个边界在任何系统运行之前把它丢弃。此后对同一地址的请求是一次新的抽样:用新的抽样 ID,引用的证据按首次抽样的规则检查,上界和用途相同时值也相同;跨过退役期的冲突不再被检出。抽样记录仍留在抽样日志里作为证据。从抽样日志恢复时逐条应用同一规则,所以日志可以在地址退役之后再抽一次,退役之前再抽仍被拒绝;接续检查点只列出尚未退役的抽样,列有已退役抽样的检查点在恢复时被拒绝。after 不应短于任何会重试该地址或引用该抽样的时间。
首次抽样引用的操作证据必须不受封存影响:同一边界内更早产生的证据,日志计数器能证明存在的事件、命令、命令尝试、输入记录、边界或抽样(封存与否都算),或者记录的已提交当前版本或初始场景版本。仍被保留但已不是当前版本的记录版本,在任何运行中都以 InvalidRandomOperationEvidence 被拒绝,所以封存过的运行与其精确重放接受同样的首次抽样。
每次抽样都记录一条 RandomDrawRecord:
| 字段 | 含义 |
|---|---|
stream 与 address |
哪条版本化的流,以及顺序位置或操作地址 |
upper_exclusive 与 value |
请求的上界和返回的值 |
purpose |
机制为什么抽样 |
producer |
执行抽样的边界系统或内核系统 |
outcome |
这次抽样决定了什么,例如一次决策选择或一份报告送达 |
cause 与 correlation_id |
因果来源和所属处理链 |
抽样记录和流状态都保存在快照和边界证据中,并计入哈希。边界失败时,它的流位置和抽样记录一起回滚,失败的尝试不会消耗随机数。
重载、平行现实与反复试验
Section titled “重载、平行现实与反复试验”确定性重放消除了最简单的重掷方式:相同的权威状态、命令或决策输入、插件语义环境和模拟时间,得到相同的抽样结果。因此,从事件发生前的快照重新加载并重放相同的输入,事件结果不变。
要从同一快照尝试不同的命令,先用 fork() 派生分支,再在分支上提交。结果是一个平行现实:一段新的因果历史,与原运行的精确重放不同。玩家能使用手动存档、单写入存档还是研究用派生分支,由上层应用的存档策略决定。
第一方扩展中的随机性
Section titled “第一方扩展中的随机性”第一方 crate 在哪里使用随机性,又在哪里刻意不用:
canwu-movement不抽样。由应用系统判定路段失败、道路封闭或发生扣押(通常用应用自己的操作定址抽样),再通过movement_incident_v1输入报告结果,并引用一条精确的证据记录。canwu-correspondence在自己的随机流canwu-correspondence / operation-resolution / 1上抽样。对于截取、遇灾、承运人扣押等事故请求,应用提供probability_per_mille,插件做一次操作定址抽样,决定事故是否发生。通信机会及其接收人也由同一条流选出。- 真伪判定背后的伪造是否被识破,由应用决定;
canwu-information记录判定结果。 - 运力预订分配按固定顺序进行:优先级、窗口开始时间、平局决胜键、准入序号和预订标识。
- 带表决单元的法律程序阶段,其平局处理是规则:取
For与取Against的单元数相等时,status-quo不增加单元,casting-seat:<seat>则在该席位本人投For时增加一个For单元。
给插件作者的建议
Section titled “给插件作者的建议”- 为每个独立机制设置一条稳定的命名随机流。
- 把抽样原因写进
purpose,让调试和重放输出能解释结果。 - 把随机流声明与插件版本、语义哈希放在一起审查。
- 不要用现实时间、线程顺序或容器遍历顺序作为种子或平局依据。
- 把未知事实、角色选择、离散事故和群体比率分别建模;不要用随机数补全缺失的事实。
与重放的关系
Section titled “与重放的关系”精确重放要求随机流身份、算法版本、根种子、顺序位置、操作地址、每一次抽样和每个最终结果都一致。加载快照时,内核检查每条顺序流的状态是否等于“种子推进已记录的抽样次数之后”的状态,并重新计算每个操作定址抽样的值。边界重放还会比较每次抽样的生产者、用途、原因和相关标识。