决策系统
决策系统回答三个问题:某个角色或机构要在哪些选项之间选择,由谁来选,选中之后怎样生效。canwu-decision 定义决策票据(DecisionTicket)、控制者(controller,DecisionControllerBinding)和决策策略 SDK;canwu-sim 负责决策输入的准入、持久化、校验、取消和重放;上层应用通过 canwu-api 使用这两部分。让玩家、AI、外部服务或 LLM 替角色做决定时,就要用到它。
crate 与所有权
Section titled “crate 与所有权”| crate | 负责什么 | 发布情况 |
|---|---|---|
canwu-decision |
票据、选项、控制者绑定、决策尝试和决策轨迹的数据类型;DecisionState 的状态转移、截止时间和历史归档;DecisionController::evaluate;决策策略 SDK |
crates.io |
canwu-sim |
决策输入的入队与准入、嵌套命令、ResolveDecisionRandomly 指令、人物不可用时的票据取消、快照校验和精确重放 |
crates.io,应用经由 canwu-api 使用 |
canwu-api |
Canwu 上的决策方法(enqueue_decision、prepare_decision、drive_decision、decision_ticket、decision_attempt、decision_trace 等),以及决策相关类型的再导出 |
crates.io |
canwu-core |
DecisionTicketId、DecisionRequestId、DecisionTraceId、RandomDrawId 等 ID 类型 |
crates.io |
查看图表源码
flowchart TB
App["上层应用<br/>票据内容、策略对象、<br/>玩家与模型的答复"] --> Api["canwu-api<br/>Canwu 上的决策方法"]
Ext["领域扩展<br/>canwu-law、canwu-society 等"] --> Api
Api --> Sim["canwu-sim<br/>准入、持久化、校验、重放"]
Api --> Decision["canwu-decision<br/>票据、控制者、策略 SDK"]
Sim --> Decision
Decision --> Core["canwu-core<br/>决策相关 ID"]
Decision --> Time["canwu-time<br/>SimTime"]
依赖是单向的。canwu-decision 只依赖 canwu-core 和 canwu-time,选项里的命令对它来说只是一段 JSON。canwu-sim 把决策状态当作权威状态的一部分保存和校验。领域扩展通过 canwu-api 使用决策类型,并提供构造票据草案的函数,例如 canwu-society 的 institutional_policy_ticket 和 canwu-correspondence 的 correspondence_decision_ticket。canwu-law 为每个席位持有人生成票据草案,交接过程见社会、文化与法律。
决策策略只拿到票据,只能返回票据上已有的一个选项 ID,或者推迟、等待。签发者、权限和命令内容都来自持久化的控制者绑定和选项,策略碰不到它们。
| 类型 | 表示什么 |
|---|---|
DecisionControllerBinding |
控制者:控制者 ID、策略身份、权限来源,可选的席位与权限配置(with_seat)、命令主体(with_command_subject)和随机平局决胜许可(with_random_tie_break)。注册后不可修改。 |
DecisionPolicyIdentity、DecisionPolicyKind |
策略身份:种类(Utility、Rule、Random、Human、External、Llm)、ID、版本,以及可选的配置哈希 semantic_hash。 |
DecisionAuthority |
控制者以谁的名义行事:Actor、Institution(可指定责任角色)、Council 或 NoResponsibleActor。 |
DecisionTicketDraft、DecisionTicket |
决策票据:定义名、决策者(decision_maker)、指派的控制者、摘要、上下文、选项、可选的截止时间和前序票据。保存后还带有版本号、开启与更新时间和状态。 |
DecisionTicketState |
Open、Resolved(记下选中的选项和决策轨迹 ID)、Cancelled(带原因)或 Expired。 |
DecisionOption |
决策选项:ID、标签、说明、动作(DecisionAction::None 或一条序列化的 Command)、效用输入、阻碍项 blockers 和元数据。阻碍项非空的选项不可选。 |
DecisionContext |
票据上下文:一个 schema 名加一段 JSON 载荷,内容由应用定义。 |
DecisionMutation |
五种决策变更:RegisterController、Open、ReplaceOptions、Resolve 和 Cancel。 |
DecisionIngressRequest |
一条决策输入:请求 ID、预期修订版本、一项变更,以及可选的嵌套命令请求。 |
PolicyDecision、DecisionOutcome |
策略的答复:Selected、Deferred、Pending 或 PendingRandom,附带逐项评估、外部证据、抽样证据、决策阶段和触发的前置规则。 |
DecisionAttemptRecord |
决策尝试:每条被准入的决策输入都留下一条,结果为 Accepted,或带 DecisionAttemptErrorCode 的 Rejected。 |
DecisionTrace |
决策轨迹:一次 Resolve 的证据,包括票据版本、控制者、策略身份、结果、逐项评分、外部或抽样证据、嵌套命令的请求 ID 和前序票据。 |
DecisionRandomEvidence、DecisionOptionWeight |
抽样证据与决策选项权重:抽样 ID、抽到的值、上界,以及把值映射到选项的整数权重。 |
决策策略 SDK
Section titled “决策策略 SDK”DecisionPolicy trait 只有两个方法:identity 返回策略身份,decide 读取 &DecisionTicket 并返回 PolicyDecision。SDK 自带下面这些实现:
| 种类 | SDK 类型 | 怎样作答 |
|---|---|---|
Utility |
WeightedUtilityPolicy |
用 UtilityProfile 的权重乘以各选项的 utility_inputs 并求和,最高分胜出,同分取选项 ID 最小的选项;所有选项都被阻碍时推迟。 |
Utility |
GuardedUtilityPolicy(前置规则效用策略) |
先按顺序运行前置规则(DecisionRule 返回 RuleChoice 的 Select、Defer、Exclude 或 NoMatch),再为剩下的选项打分。开启 random_tie_break 且至少两个选项与最高分相差不超过 near_equivalence_margin 时,返回 PendingRandom。 |
Rule |
OrderedRulePolicy |
按顺序运行规则,遇到第一条 Select 或 Defer 即结束;没有规则命中时推迟。 |
Random |
无宿主侧实现 | 由边界系统抽样结算,见下文。 |
Human |
QueuedHumanPolicy |
提交 HumanDecisionResponse 之前返回 Pending。 |
External |
QueuedExternalPolicy |
提交 ExternalDecisionResponse 之前返回 Pending。 |
Llm |
QueuedLlmPolicy |
同上;回答中的 provider 还必须与 LlmModelIdentity 一致。 |
人类、外部服务和 LLM 的答复都写明针对哪个票据版本。针对旧版本的答复返回 VersionConflict,同一版本重复提交返回 DuplicateResponse,针对更新版本的答复会替换旧答复。GuardedUtilityPolicy 的 semantic_hash 覆盖前置规则策略的身份、规则 ID、效用权重、容差和平局决胜开关;规则的具体行为属于应用代码,行为变化时要改规则 ID 或策略版本。
Deferred 也会写入决策轨迹并提升票据版本,票据保持开启。Pending 和 PendingRandom 只是中间结果,不能作为 Resolve 提交。
一个决策的生命周期
Section titled “一个决策的生命周期”查看图表源码
flowchart TB
Host["上层应用或领域扩展<br/>RegisterController、Open"] --> Queue["规范化输入<br/>DecisionIngressRequest"]
Queue --> Admit["边界第 1 阶段准入<br/>写入 DecisionAttemptRecord"]
Admit --> Ticket["DecisionTicket<br/>Open,带版本号"]
Ticket --> Policy{"控制者的策略<br/>只读票据"}
Policy -- "Pending" --> Wait["等待玩家、服务<br/>或模型答复"]
Wait --> Policy
Policy -- "Selected 或 Deferred" --> Drive["drive_decision<br/>排入 Resolve 请求"]
Policy -- "PendingRandom,或 Random 控制者" --> Draw["边界系统抽样<br/>ResolveDecisionRandomly"]
Drive --> Queue
Draw -- "内核生成 Resolve 输入" --> Queue
Admit -- "Resolve 通过" --> Trace["DecisionTrace<br/>嵌套命令进入常规命令准入"]
- 注册控制者。 上层应用用
enqueue_decision排入RegisterController。决策输入属于规范化输入的Decision类,到期时间相同时排在命令、通信、确认和信息之后,调度工作之前。控制者 ID 只能注册一次;权限和命令主体引用的实体必须存在。 - 开启票据。
Open携带一份DecisionTicketDraft。准入时,票据 ID 必须非零且未被使用,至少有一个选项且选项 ID 互不相同,指派的控制者已经注册,截止时间不早于准入时间,决策者存在且可用,控制者的权限人物也可用,前序票据(如有)符合接续规则。票据以版本 1、Open状态保存,选项按 ID 排序。 - 更新票据。 局势变化时排入
ReplaceOptions,带上expected_version,替换上下文和选项。版本号加一,为旧版本准备的答复随之作废。Cancel带一个原因关闭票据。 - 求值。 上层应用调用
prepare_decision,把策略对象交给引擎。引擎确认票据开启、未过截止时间、决策者和权限人物可用、策略身份与绑定一致,然后调用DecisionController::evaluate。结果为Pending时不入队任何内容;结果为权威答复时,返回一条Resolve请求。选中的选项带命令时,调用方必须给出CommandRequestId,引擎用控制者绑定生成嵌套命令。drive_decision把求值和入队合成一步。 - 准入。 下一个边界的第 1 阶段
EventIngress准入到期的决策输入。引擎依次检查预期修订版本、嵌套命令的请求 ID 是否唯一、引用的实体是否存在、人物是否可用,再交给DecisionState::apply检查票据版本、票据是否关闭、提交者是否为指派的控制者、策略身份和选项。预期内的失败都记为Rejected决策尝试,队列接着处理后面的输入。 - 写入。
Resolve通过后,引擎分配DecisionTraceId并写入决策轨迹。Selected把票据改为Resolved;Deferred只提升版本。决策尝试记为Accepted。 - 执行命令。 嵌套命令紧接着走常规命令准入,
CommandContext::decision_controller_id带上控制者 ID。命令处理器可以要求这个字段,以确认命令来自一张票据。预期内的命令拒绝记为命令尝试,票据仍是Resolved。 - 到期。 准入完成后、各阶段运行之前,截止时间早于本边界时间的开启票据变为
Expired。
决策输入带着预期修订版本。入队时它必须等于当前修订版本;如果准入之前修订版本已经变化,例如又有别的边界提交,请求会以 SimulationRevisionConflict 记为被拒绝的尝试。同一请求 ID 再次入队时,内容、到期时间和优先级都相同就返回原来的回执,否则返回 IdempotencyConflict。声明为只读交互的运行拒绝新的决策输入(InteractionReadOnly)。
决策系统本身不注册边界系统,也不发出事件。插件的边界系统在读取中声明 StateKey::core_decisions() 后,可以用 SimulationView::decision_ticket、decision_controller 和 decision_attempt 读取决策状态。只有第 7、10、12、13 阶段接受普通指令(第 4 阶段只接受知识发布),所以提出 ResolveDecisionRandomly 的系统必须位于其中之一,仓库示例放在第 12 阶段 StrategicAggregation。名字里带“决策”的第 5 阶段 DecisionAndAcceptedEffectIntake 不处理决策输入。
边界记录(BoundaryRecord)列出本边界准入和生成的输入、随机抽样,以及人物可用性变化导致取消的票据;嵌套命令照常产生自己的事件。阶段顺序见结算系统。
谁可以结算票据
Section titled “谁可以结算票据”- 每张票据指派给一个控制者(
assigned_controller),之后不能改派。只接受这个控制者的Resolve,其他控制者的请求记为InvalidController。 Resolve带的策略身份必须与控制者注册时的绑定完全一致,包括semantic_hash,否则记为PolicyMismatch。- 选中的选项必须存在于当前票据版本,并且没有阻碍项。
- 抽样证据只能来自边界的
ResolveDecisionRandomly,而且只有Random控制者和用with_random_tie_break开启许可的Utility控制者接受它。因此Random票据只能由边界系统结算。 - 嵌套命令的签发者和权限都从绑定推导:
Human策略对应Issuer::Human,其他策略对应Issuer::Ai,两者都带控制者 ID。CommandAuthority的decision_origin取自绑定中的权限,席位、权限配置和命令主体也取自绑定。在声明了运行配置的运行中,Issuer::Human还必须与运行的席位绑定(SeatBinding)和ControllerPolicy::HumanRoleBound一致,Issuer::Ai的权限不能是NoResponsibleActor。
人物可用性也会限制结算。人物死亡、失踪、被拘押或被俘时即为不可用。决策者不可用时,票据不能开启,也不能交给 prepare_decision 求值(DecisionMakerUnavailable);控制者的权限人物不可用时,票据不能开启,不能交给 prepare_decision 求值,也不能结算(IssuerUnavailable)。人物在某个边界中变为不可用,该边界结束时(在本边界的随机决策生成之后)引擎取消相关的开启票据,原因记为 decision_maker_unavailable 或 controller_authority_unavailable,票据 ID 写入 BoundaryPersonAvailabilityChange 的 cancelled_tickets 和 cancelled_controller_tickets。
被取消的票据不会恢复。要继续这项决策,就开启新票据,并把 parent_ticket 设为被取消的票据。前序票据必须已经终止且尚未归档(见决策历史归档),而且要么与新票据的决策者相同,要么两张票据的控制者绑定了同一个 seat_id,即席位继任。完整规则和两种接续方式见已无法行事的人物。
策略能看到什么
Section titled “策略能看到什么”- 策略只看到
&DecisionTicket,看不到可变的模拟状态,也拿不到权限。票据上下文就是它掌握的全部信息,因此上下文应当取自决策者的持有人相对知识。 - 外部服务和 LLM 收到的
ExternalDecisionRequest更窄:票据 ID 与版本、定义名、摘要、上下文,以及可用选项的 ID、标签、说明和元数据。命令载荷、效用输入和阻碍项都不在其中。 - 上层应用通过
Canwu::decision_ticket、decision_controller、decision_attempt、decision_trace和decision_hot_state读取决策状态。这些是受信任读取,能读到全部内容;CanwuViewer不提供决策读取,给玩家展示哪些票据和多少细节由上层应用决定。 - 边界系统只能读取已声明的内容:决策状态需要
StateKey::core_decisions(),人物可用性需要StateKey::core_person_availability()。
随机性、持久化与重放
Section titled “随机性、持久化与重放”决策系统不拥有随机流。结算票据的边界系统在 random_streams 中声明自己的随机流,例如示例中的 example-uncertainty / decision-selection / 1,然后用 random_sample_for_operation 以 RandomOperationTarget::DecisionTicket 为目标抽样,目标绑定票据 ID 和当前版本。选项权重由应用给出:Random 控制者的权重必须按选项 ID 顺序覆盖所有可用选项,平局决胜的权重必须等于 PendingRandom 中的候选。权重之和必须等于抽样上界。
内核在抽样提交前检查:
- 控制者种类和随机流声明;
- 抽样地址由本插件产生并绑定当前票据版本,样本来自本次提案;
- 权重;
- 选中选项带命令时必须有命令请求 ID,不带命令时不能有;
- 人物可用性,按本边界开始前已提交的状态检查。
随后内核记下 RandomDrawRecord,其结果为 RandomDrawOutcome::DecisionSelection,并生成一条到期时间相同的 Resolve 决策输入,由下一个边界准入。决策轨迹的 random 字段(DecisionRandomEvidence)与抽样记录互相引用。来源边界失败时,抽样和生成的输入一起回滚。上层应用自己排入的、带抽样证据的决策输入,入队时和加载快照时都会被拒绝。完整示例见随机决策与 LLM 选择。
LLM、外部服务和人类
Section titled “LLM、外部服务和人类”这三类答复都在模拟之外产生。上层应用把票据或请求交给玩家、服务或模型,收到答复后提交给对应的 Queued*Policy,再调用 drive_decision。排入队列的 Resolve 请求保存整个 PolicyDecision,其中的 DecisionExternalEvidence 记录 provider、模型、提示契约、请求 ID 和元数据。之后的重放只使用这条记录,模型不会被再次调用。
快照与精确重放
Section titled “快照与精确重放”快照的 decisions 字段保存整个 DecisionState:控制者、票据(含截止时间、版本和状态)、决策尝试、决策轨迹和归档回执。决策状态在 CommitmentRoots 中有自己的 decisions 承诺根。加载快照时,引擎检查引用的实体,用已准入的决策输入重建决策状态并与保存的状态比对,还会针对每个边界重新检查它生成的随机决策。
精确重放(Canwu::replay_from_journal)重新排入记录下来的决策输入,并重新运行边界系统。边界生成的输入必须在重放中原样生成出来,否则报 ReplayMismatch。任何策略都不会重新运行,包括确定性的效用策略。想让同一局面换一个选择,就调用 fork() 派生分支并提交新的决策输入,得到的是平行现实。
决策历史归档
Section titled “决策历史归档”已终止的票据、决策尝试,以及所属票据已终止的决策轨迹,可以移入按内容寻址的决策归档;开启的票据始终留在热状态中。归档提交作为维护输入在普通边界准入,因此重放会得到同样的归档转换。Canwu::decision_history_location 对一个 DecisionHistoryKey 返回 Hot、Archived、Unresolved 或 Absent;Unresolved 表示需要归档提供方(DecisionArchiveProvider)才能确认,不能当作不存在。
上层应用需要提供什么
Section titled “上层应用需要提供什么”- 决策何时出现:票据定义名、决策者、摘要、上下文和选项,以及选项对应的命令、效用输入和阻碍项。
- 控制者绑定:策略身份、权限来源、席位与权限配置、命令主体。
- 策略对象本身:效用权重、规则、前置规则和容差。身份必须与绑定一致。
- 与玩家、外部服务或模型之间的通道:展示票据、解析答复,并确认提交答复的一方有权操作这个控制者。
- 随机选择所需的边界系统、随机流声明和各选项的权重,也就是概率。
- 非零且全局唯一的
DecisionRequestId和CommandRequestId,以及到期时间和优先级。 - 人物死亡、被俘或获释的时机,由边界系统写入人物可用性;继任者的控制者和接续用的新票据。
- 如果要归档决策历史,还要提供归档存储(
DecisionArchiveStore、DecisionArchiveProvider)。 - 所有界面与呈现。
| 常量 | 值 | 限制什么 |
|---|---|---|
MAX_DECISION_ARCHIVE_BATCH_ENTRIES |
4,096 | 一个归档批次的键数 |
MAX_DECISION_HISTORY_PAGE_SIZE |
512 | 一页归档历史查询的结果数上限 |
MAX_DECISION_HISTORY_PAGE_BYTES |
16 MiB | 一页归档历史查询可解码的字节数上限 |
DecisionHistoryQueryBudget 默认值 |
128 条结果、128 次提供方调用、4 MiB | 一次历史查询的默认预算 |
归档定位页的布局常量见 canwu-decision 的 rustdoc。
另有几条固定规则:
- 一个边界最多生成一条随机决策结算;互相独立的随机决策要放在不同的来源边界里。
- 随机平局决胜至少需要两个候选,每个候选的权重为正。
- 每张票据至少有一个选项;票据 ID、请求 ID 和抽样 ID 都必须非零。
- 效用得分用带溢出检查的
i64计算,选项权重用u64且总和必须为正,溢出时报错。
cargo run -p canwu-api --example decision_ticketcargo run -p canwu-api --example uncertainty_resolution第一个示例用效用策略结算一张军阀援助票据,打印决策轨迹,再验证快照恢复和精确重放,走读见军阀请求邻军援助。第二个示例先用 Random 控制者抽样结算一张法案票据,再为一张由 LLM 控制者负责的票据打印 ExternalDecisionRequest,并通过 QueuedLlmPolicy 提交一份预先写好的答复,走读见随机决策与 LLM 选择。人物不可用和票据接续见接入并驱动模拟运行。
这些测试覆盖决策契约:
cargo test -p canwu-api --test decision_frameworkcargo test -p canwu-api --test gap_g08_decision_guarded_utility_policycargo test -p canwu-api --test gap_g09_decision_ticket_lineagecargo test -p canwu-api --test gap_g02_sim_person_availabilitycargo test -p canwu-decision --test policy_contracts