Havenlon 执行控制工程 II 02|裁决面与执行面,为什么必须分开?
在很多系统里,策略判断和真正执行之间只隔着一行代码:规则返回允许,于是调用执行接口。
这种写法非常自然。策略存在的意义本来就是回答"这件事能不能做",既然答案已经是允许,继续做下去似乎顺理成章。
但把它放进高风险场景,问题就浮出来了:负责判断"能不能做"的组件,同时拥有了"真正去做"的能力。裁决与执行被重新绑回同一处,裁决运行时表面上只是判断者,实际上已经成为能够直接改变现实的执行主体。前面辛苦建立的职责分离,在最后一行代码上失效。
所以在真正动手构建执行边界时,一条基础原则是:运行时只产生裁决,不直接产生现实动作。 裁决是一个结果,执行是另一个独立步骤,两者之间必须存在明确边界。
一、裁决与执行本来就是两个对象
先把概念拆开。裁决表示的是"根据当前输入、规则和状态,这项操作是否获得继续执行的资格",它的输出是允许、拒绝或其他有限的结论。执行表示的是"真正调用某个能够改变现实的能力"——发送交易、调用支付接口、修改生产环境、删除对象、驱动一台设备。
前者是关于现实的判断,后者是现实本身发生变化的过程。它们天然不是一回事,只是在普通业务代码里很容易被压缩成一句"如果允许就执行"。
一旦这样写,运行时的输出就不再只是一个事实,而变成了执行触发器。允许的含义也从"当前规则认为可以继续",悄悄升格成"现实现在必须发生变化"。
这一步的代价,可以从能力角度看得更清楚。假设策略在判断一笔付款:它读取金额、账户、审批状态和额度,条件都成立,于是直接发起转账。功能上非常高效,但这时运行时同时握有三种能力——读取事实、解释规则、改变现实。它既是裁判,又是执行者。
于是一旦运行时自身出现规则错误、输入被污染、版本错配或运行环境失陷,错误的裁决就会直接变成现实结果,中间不再有第二道独立边界。运行时的一次失陷,等于裁决面与执行面一起失陷。从故障隔离的角度看,这是相当危险的耦合。
二、"方便"如何把裁决者变成执行平台
现实工程中这两者被绑在一起,往往不是因为设计者忽视安全,而是因为顺手。
策略需要查数据,就给它数据访问;需要调用外部风险服务,就给它网络权限;判断通过之后要转账,顺手给上支付接口;部署规则要看环境,顺手让它去问编排系统。每一步都有充分理由,累积下来,运行时逐渐拥有读取、判断、修改、调用、执行、恢复的全套能力。
越方便,职责越多;职责越多,攻击面越大。一个原本纯粹的裁决引擎,就这样演化成一个握有大量现实能力的自动化平台——恰好与"小而确定的可信边界"背道而驰。
三、两个平面
更清晰的做法是把整个过程拆成两个平面。
裁决面负责消费已经明确提供的事实、运行规则、检查不变量、输出裁决,但不直接改变现实。执行面负责接收经过验证的执行资格、重新核对必要条件、构造最终执行参数、调用具体能力、产出结果与证据。
事实 → 运行时 → 裁决 → 执行边界 → 执行器 → 现实
运行时的终点是裁决,不是现实。
这条链看上去比直接调用多了一层。而正是这一层,给系统留下了继续验证的机会。
四、二次验证,验证的是绑定关系
既然运行时已经判断过一次,为什么执行前还要再检查?因为运行时判断的对象,和最终执行的对象之间可能已经不同。
运行时对某个收款方和某个金额作出了允许,而进入执行环节时,收款方已经被换成另一个。如果执行侧只检查"裁决结论是允许",然后照做,那么前面的判断实际上已经失效——被判断的是一个对象,被执行的是另一个。裁决必须与具体执行对象绑定,执行侧至少要能确认:我现在准备做的,是不是运行时当时真正裁决过的那件事。
这里容易产生误解:如果运行时已经检查了额度,执行器是否也要再实现一遍完整的额度规则?未必。那样会出现两套规则,反而引入不一致的风险。更合理的分工是——运行时根据策略判断这个明确对象是否被允许,执行器确认当前准备执行的对象仍然等于被裁决的对象:动作类型没变,目标没变,关键参数没变,裁决未过期,协议语义一致。执行侧验证的是绑定关系,而不是重新承担全部策略计算。
这件事必须发生在靠近执行的位置,因为一次请求在系统里会经历多次转换:业务对象、意图、策略输入、裁决对象、执行参数、底层请求。每一次转换都可能带来语义漂移。如果运行时直接执行,它就同时承担了策略判断和参数映射,事后很难分清错误来自规则还是来自转换。独立的执行侧则可以把最终参数单独暴露成一个明确对象,让系统重新确认:最终的目标是什么,金额是什么,环境是什么,动作是什么——这些才是现实即将变化时真正有意义的事实。
状态同样如此。运行时作出判断时看到的是某一个时刻的状态,而执行器动手时面对的是另一个时刻的现实,中间隔着队列、网络、异步调度、人工等待或设备通信。运行时判断某台设备处于安全状态因而放行,几秒钟后设备已经进入故障状态——如果执行侧只相信那份旧结论,现实已经变了,执行却仍在沿用过去的判断。这正是第一季讨论过的过期状态。独立执行侧的价值之一,就是留出最后一次状态确认的机会:不必重跑完整策略,但至少检查那些一旦变化就会让执行资格失效的关键条件。
五、隔离必须落到权限层
如果运行时和执行器跑在同一个进程里、拥有相同的系统权限、使用同一套凭据、访问同一片网络,并且由同一个模块直接调用,那么逻辑上虽然有两个名字,安全上未必存在真正的隔离。
这一季谈"怎么造",就不能停留在代码结构的抽象上,还要落到能力集合:运行时不需要支付凭据、设备控制能力、广播能力或生产环境写权限;执行器不需要修改策略、加载规则、解释任意脚本或访问全部业务数据。两边都只拿完成自身职责所必需的最小能力。只有这样,两个平面才不只是代码分层,而是权限分层。
这带来一个很实际的收益:缩小高价值凭据的暴露范围。执行一项动作可能需要私钥、接口凭据、设备证书或环境令牌。如果运行时直接执行,这些东西必须暴露在运行时所处的环境里——而运行时通常还承担规则解析、复杂输入处理、策略加载和上下文判断,攻击面比执行器大得多。把执行能力独立出去,就能让最危险的能力停留在尽可能小的可信范围内。
分开之后还要防止反向问题:执行器越写越胖,开始查业务数据、跑规则、处理审批和工作流,最终又变成一个大型应用。
执行器应该知道"怎样执行",但不应该重新决定"业务上为什么应该执行"。
它的职责应该是窄的:接收明确的执行对象,验证必要的绑定,做有限的状态检查,映射到具体接口,触发动作,取回结果,生成证据。复杂的业务判断留在上层,执行边界只保留那些必须靠近现实的少量不变量。窄,才容易审计、测试、隔离和加固,也才有可能在将来迁移到不同的运行环境。
六、执行侧必须能够拒绝一份合法裁决
这是整个架构成立与否的关键。运行时已经输出允许,执行侧有没有资格返回拒绝?如果没有,它仍然只是一个远程调用的代理。
真正独立的执行边界必须允许"裁决为允许、执行为拒绝"同时成立,理由可以是最终参数不匹配、裁决已过期、状态已变化、执行对象不一致、前序证据不完整、协议版本不符、必要的本地条件不成立。这不是执行侧在推翻策略,它表达的是另一件事:这份裁决现在已经不足以支持当前这一次现实动作。
由此,允许这个结论本身也应当被降权理解——它不是命令,而是继续资格:当前阶段没有发现阻止条件,可以进入下一层验证。只有后续边界继续确认了对象、参数、状态、上下文和有效性,执行资格才真正成立。这样,任何一个上游结论都不会具备穿透整条链路的力量。
要做到这一点,裁决就不能只是一个布尔值。如果运行时最终只返回真,下游几乎无法知道它为什么产生、针对哪个对象、何时形成、属于哪套策略、有没有过期、对应哪份意图。裁决更适合成为一个明确的协议对象,在概念上能够表达:这是哪一次判断,针对哪个执行对象,结论是什么,在什么策略语义和时间上下文下形成,带有哪些必要的绑定关系。这里不需要展开具体结构,重要的是它可以被验证和引用,而不只是一次函数返回值——只有成为对象,执行侧才谈得上判断"当前动作是否仍然属于这份裁决"。
同样,裁决不该拥有无限生命周期。上午形成的一个允许,到了下午是否还成立?明天呢?如果没有时间语义,一个过去合法的结论很容易变成永久通行证。裁决与意图、审批一样需要新鲜度和有效范围,而确认它是否仍在时效之内,正是执行侧必须承担的一项职责。
七、能力分离、失败处理与最后的可拒绝窗口
上一篇强调运行时最好无副作用,这里可以补上另一半:即使运行时想产生副作用,也不应该天然拥有通往执行接口的能力。
否则会出现一种荒谬的状态——规则表面上返回了拒绝,但在返回之前已经悄悄触发了现实动作。语言层不允许,权限层也不给,两边共同构成边界。这样即便运行时自身出现严重逻辑问题,也更难直接转化为任意的现实执行。
执行失败之后的处理同样需要克制。常见的自动化做法是把错误回抛给上游,由它改参数、换接口、找备用路径再试一次。这在普通系统里很灵活,但在执行控制里意味着上游正在逐渐获得"自主寻找可执行路径"的能力。一个安全边界返回拒绝,不能被理解成"换个办法"。操作性错误、安全拒绝、状态冲突、执行失败,应当具有不同的后续语义:可以重新收集事实、重新提交裁决,但不能因为"任务还没完成"就不断试探能穿过边界的路径。
在真正靠近不可逆点的地方,还可以进一步拆分准备与提交。很多高风险动作天然存在这两个阶段:先解析最终参数、验证对象、检查状态、构建待执行内容,此时现实尚未改变;只有最后的提交才触发不可逆动作。这个结构的价值在于,它让系统在最后时刻能够明确看到"现在真正准备发生的动作究竟是什么",并在提交前完成最终的绑定检查、状态确认和证据准备。原则是:在不可逆点之前,尽量保留最后一次可拒绝的窗口。
分离之后,证据链也会更清楚。一次执行最终至少要能解释:哪一份意图,产生了哪一个裁决,由哪个执行侧接收,执行前看到了什么关键状态,最终触发了什么动作,结果是什么。这条链上的每一步都不能只靠"系统说它们属于同一件事",而需要明确的绑定关系。把判断与执行拆开,不只是安全隔离,也让"谁判断"和"谁执行"终于成为两个不同的责任节点。
八、把顺手做掉的事情重新拆开
从责任模型看,这两者对应两种不同性质的权力:一种是解释权——根据规则解释当前事实;另一种是操作权——真正把一个数字决定变成现实变化。长期绑在同一个组件里,一次失陷的影响就会覆盖两者;拆开之后至少可以做到:运行时出错不等于自动执行错误,执行器被调用不等于自动获得业务授权。这不是假设任何一边永远正确,而是让单边错误更难独自走完一条完整的失败链。
Agent 场景让这条分界更紧迫。一个能力很强的自主系统已经具备理解、规划、工具调用、策略生成、状态分析和错误恢复;如果它所处的运行时还直接握有支付、部署、设备控制和密钥使用能力,那么一次判断偏差、一次提示注入或一次上下文污染,都可能迅速变成现实动作。更稳妥的结构是:它可以产生意图、计划、建议、策略输入,甚至候选裁决,但真正进入执行时仍要穿过独立边界。
越聪明的系统,越应该明确区分判断能力和改变现实的能力。
软件工程天生喜欢合并:既然运行时已经知道可以执行,顺手调用;既然执行器已经拿到状态,顺手判断;既然规则需要上下文,顺手查询;既然出错了,顺手恢复。每一处都合理。而高风险执行控制做的事情常常相反——把本来可以顺手完成的事情重新拆开:谁收集事实,谁判断,谁授权,谁执行,谁记录证据。这些拆分不是为了架构图好看,而是为了不让任何一个局部组件同时握有解释、授权、执行和改写历史的完整能力。
需要说明的是,这套结构不消除风险。它降低的是单点失陷直接兑换成现实后果的概率,增加的是独立验证的位置,并在最靠近现实的地方保留一次拒绝的余地。
所以最终的形态不该是"规则放行,现实随之发生",而更接近:
事实 → 运行时 → 裁决 → 执行边界 → 执行器 → 结果 → 证据
裁决与现实之间被刻意留出了一段距离。这段距离,就是系统最后一次确认"允许"是否仍有资格变成"发生"。
裁决者不应该天然拥有改变现实的能力。
真正的执行边界,不是让判断系统变得越来越强,而是让判断与执行之间始终保持一种关系:你可以说"可以",但现实不会仅仅因为你说了"可以"就自动发生。
更多推荐


所有评论(0)