把一堆 if-else 塞进决策模型,然后抱怨输出不稳定,通常不是模型本身的问题,而是边界没划。规则擅长的是确定性判断、需要逐条解释、口径很少变的场景;Startlux-Decision 适合的是输入带模糊、多个因子交叉、需要容忍缺失值并做泛化的判断。先别写新规则,先把现有规则过一遍,认清哪一层该承担什么,再决定调用哪些字段、接受什么输出、出问题怎么退。
先按输入确定性、可解释要求、变更频率、失败代价四项给现有规则打标:确定性高、需要逐字解释、失败代价高的判断留在规则层;输入模糊、因子交叉、允许带默认值的判断交给 Startlux-Decision。模型上线前必须先定义输出可接受区间与兜底规则,双跑对比只用来定位分歧来源,不作为准确率证明。任何一次模型决策都要能用 traceId 还原输入、输出与回退路径。
列出当前靠规则维护的决策点
不要按“能不能用 AI”来筛,而是按判断性质来筛。先把近一个迭代里所有写在代码、配置或运营后台里的决策点抄出来,逐条填下面这张表。表格不是为了好看,是为了让分歧暴露出来:同一行里如果“可解释要求高”和“变更频率高”同时出现,基本就是维护噩梦的开始。
| 决策点 | 输入确定性 | 可解释要求 | 变更频率 | 失败代价 |
|---|---|---|---|---|
| 优惠券是否可用 | 高,字段齐备 | 高,需向客服逐条解释 | 中,活动期调口径 | 中,可事后补偿 |
| 是否给用户发提醒 | 低,行为字段常缺 | 低,不解释也能接受 | 高,策略常换 | 低,打扰可忽略 |
| 是否拦截可疑订单 | 中,部分字段延迟 | 高,需给出理由 | 低,风控口径稳定 | 高,误拦影响下单 |
| 内容是否进入推荐池 | 中,文本质量难量化 | 中,只需大类原因 | 高,规则天天加 | 低,可下架 |
填表时的口径建议固定下来:输入确定性看字段是否稳定到达、是否常为空;可解释要求看是否需要向用户或客服复述判断依据;失败代价看错一次能不能撤销。填完之后,输入确定、可解释要求高、失败代价高的行,优先继续放在规则层;输入模糊、变更频率高的行,才是 Startlux-Decision 的候选。中间地带不要一刀切,可以让规则先出一版结果,模型只对规则未覆盖的样本打分。
把输入字段分成事实、偏好和策略
很多“模型输出不稳定”的根因,是把运营策略当成模型输入喂了进去。策略今天改、明天改,模型每次都要重新学一遍,输出自然漂移。调用 Startlux-Decision 前,先把字段按三类分开,并在调用边界上做校验。
- 事实字段:订单金额、支付状态、注册时间、券已用次数。来自业务库,只读,不允许在调用前二次加工。
- 偏好字段:通知时段偏好、渠道偏好、内容类型偏好。允许缺失,缺失时填默认值,并在请求里带上“已使用默认值”的标记。
- 策略字段:灰度比例、风控等级、业务优先级、临时提额开关。这类字段不作为模型输入,而是在调用前做过滤、调用后做兜底。
缺失值处理要区分开:事实字段缺失时不要猜,直接判定本次不可决策、回退规则;偏好字段缺失填默认值并打标;策略字段缺失说明配置有问题,拒绝调用并报警。调用前的通用校验骨架可以写成这样,具体枚举和范围按你的字段替换:
function validateInput(input) {
if (input.orderAmount === undefined || input.orderAmount <= 0) {
return { ok: false, reason: 'fact_missing' };
}
if (!ALLOWED_CHANNELS.includes(input.channel)) {
return { ok: false, reason: 'enum_invalid' };
}
if (input.usedCouponCount < 0) {
return { ok: false, reason: 'range_invalid' };
}
return { ok: true, defaultsUsed: ['notifyWindow'] };
}
校验放在调用 Startlux-Decision 之前,而不是之后。校验不通过的请求不要发出去,否则日志里会混入大量“模型答非所问”的假信号,后面排查会被带偏。
为 Startlux-Decision 定义输出可接受区间
模型输出通常包含标签和分数,可接受区间要同时约束两者:标签必须在允许集合内,分数要落在你能接受的范围里。区间之外不是“再想想”,而是明确回退。下面这份配置是通用骨架,阈值需要结合你的业务分布和历史样本确认,不要直接抄用:
decision: coupon_eligibility
accept:
minScore: 0.8
labels: [allow, deny]
review:
minScore: 0.5
maxScore: 0.8
fallback:
rule: coupon_rule_v3
onTimeoutMs: 300
onLabelOutOfSet: true
三档处理建议这样定:分数达到接受阈值且标签合法,直接采用;落在人工复核带,写入复核队列,由规则先出一版结果顶着;低于下限、标签越界、必填字段缺失或超时,直接回退规则。回退条件要写成可判定的表达式,不要写“效果不好时回退”这种没法执行的描述。人工复核带不宜过宽,否则复核量会挤压处理时间;也不宜过窄,否则模型的价值发挥不出来。
用双跑对比规则与模型决策分歧
双跑不是比赛谁更准,而是找出两者不一致的地方,判断分歧是否合理。抽样时至少覆盖三类样本:正常样本、边界样本、字段缺失样本。每条样本保留原始输入快照,否则分歧原因只能靠猜。对比表建议按下面这个模板落库,方便后续聚合:
| 样本 | 规则结果 | 模型结果 | 分歧原因 |
|---|---|---|---|
| 正常订单,字段齐备 | allow | allow | 无分歧 |
| 券状态同步延迟 | deny | allow | 输入事实字段过期 |
| 新用户无历史行为 | deny | review | 规则未覆盖,模型给复核带 |
| 分数处于阈值边界 | allow | deny | 阈值位置差异 |
| 策略字段被当输入 | deny | allow | 输入契约被污染 |
分歧原因建议先归成四类:输入字段问题、规则未覆盖、阈值边界差异、输出越界。归完类再决定动作——输入问题改契约,规则未覆盖补规则或扩大模型接受范围,阈值差异调阈值并记录理由,输出越界检查解析和版本。不要在这一步统计准确率,样本量和标签质量都不支持得出可靠结论,容易误导后续动作。
确定上线后的回退与审计记录
回退和审计不是上线后补的作业,而是调用链的一部分。每次决策至少要留下这些字段:traceId、决策类型、输入快照(脱敏后)、模型版本、原始输出、置信度、是否回退、兜底规则结果、耗时。人工覆盖入口单独开,运营在后台改结果时写一条覆盖记录,而不是直接改模型或改库,保证覆盖动作也能被追到。
回退触发条件建议显式列出:调用超时、必填字段缺失、输出标签不在允许集合、置信度低于下限、模型版本未登记。任何一个命中,就走兜底规则,并在日志里标记为回退,不要让调用方自己判断。审计日志按 traceId 可检索即可,保留期结合合规要求确认。用命令快速定位某次决策的链路可以这样:
grep 'traceId=abc123' /var/log/decision/decision.log | jq -c '{ts,modelVersion,score,fallback,ruleResult}'
判断边界这件事没有一次性的正确答案。比较稳的做法是:先让规则负责可解释、低变更、高代价的判断,把模糊和交叉的判断交给 Startlux-Decision;输入契约固定后,再根据双跑分歧微调接受区间和回退条件。每调整一次阈值或字段,都回到这几张表和日志里核对,别只看单次调用的输出。