Skip to content

决策引擎系统设计 ​

定位与分工:业务篇风控决策架构回答业务问题——风控在信贷里管什么(贷前/贷中/贷后)。以及规则模型策略三层各是什么、降级为什么按场景分级。本篇回答系统问题——这个引擎怎么造。规则怎么编排、特征怎么接入、版本怎么灰度回滚、P99 预算怎么拆到节点。平台无关的模式(缓存/限流/熔断)见 C 轨秒杀系统设计等,本篇只写离开信贷语境不成立的部分。

一、规则编排模型:决策树/决策表/规则流的选型 ​

形态表达力适用弱点
决策树条件分层嵌套准入判定(年龄→职业→区域准入)层深了不可读,改一层动全树
决策表多条件组合 → 动作映射额度/定价矩阵(评分档 × 期限 → 额度利率)维度一多组合爆炸
规则流节点编排(串/并/分支/短路)全流程策略主干只用规则流 = 把决策表拍扁了写

选型结论:不是三选一,是分层组合——规则流做主干(阶段编排:反欺诈→准入→评分→定价)。节点内部用决策表/决策树表达矩阵与判定:

json
{ "flow": "CREDIT_APPLY_v12",
  "nodes": [
    { "id": "anti_fraud", "type": "RULE_TABLE", "table": "anti_fraud_v3",
      "timeoutMs": 20, "degrade": "REJECT" },
    { "id": "score", "type": "MODEL", "model": "card_a_v7",
      "timeoutMs": 50, "degrade": "FALLBACK(card_a_v6)" },
    { "id": "pricing", "type": "RULE_TABLE", "table": "pricing_matrix_v9",
      "dependsOn": ["score"], "timeoutMs": 20, "degrade": "SKIP" } ],
  "shortCircuit": "ANY_REJECT" }
sql
CREATE TABLE decision_flow (           -- 编排版本化: 策略与代码分离的落点
  id             BIGINT PRIMARY KEY,
  scene          VARCHAR(32) NOT NULL,   -- CREDIT_APPLY / DRAW_DOWN
  flow_version   VARCHAR(16) NOT NULL,   -- v12
  flow_dsl       JSON NOT NULL,          -- 上面那份 DSL
  status         VARCHAR(16) NOT NULL,   -- DRAFT/GRAY/FULL/ROLLED_BACK
  gray_ratio     INT DEFAULT 0,          -- 灰度流量百分比
  created_by     VARCHAR(32) NOT NULL,   -- 策略分析师, 不是工程师
  UNIQUE KEY uk_scene_ver (scene, flow_version)
);

"策略与代码分离"的验收标准:策略分析师调整阈值、改编排、灰度上线全程不发版。信贷一年改几百次策略,每次都发版就是每次都是事故窗口(三层的业务职责见风控决策架构)。

二、特征接入层设计:点查缓存与批量预热 ​

特征是决策的原料,引擎不产特征只取特征——接入层同时伺候"点查"与"预热"两种工况:

                ┌─ 本地缓存(名单类: 秒级 TTL + 变更广播失效)
决策节点 ──取数──┼─ Redis 在线 KV(Flink 实时聚合落库)      ──▶ 点查 P99 < 50ms
                └─ 特征服务 RPC(三方源/大变量, 独立超时)

预热路径: 进件完成事件 ──▶ 异步预取该用户特征全集 ──▶ 决策时 0 冷启动
大促预热: 热点名单/公开特征 ──▶ 活动前批量装载 + 按用户分片打散
sql
CREATE TABLE feature_meta (            -- 特征注册: 口径与缺失策略配置化
  id             BIGINT PRIMARY KEY,
  feature_name   VARCHAR(64) NOT NULL,   -- loan_app_cnt_30d
  data_source    VARCHAR(32) NOT NULL,   -- REALTIME(FLINK)/OFFLINE/THIRD_PARTY
  importance     VARCHAR(8) NOT NULL,    -- CORE / NORMAL
  missing_policy VARCHAR(16) NOT NULL,   -- DEFAULT(-1)/CONSERVATIVE(拒绝或人工)/FAIL
  calib_version  VARCHAR(16) NOT NULL,   -- 口径版本(线上线下一致性的锚点)
  UNIQUE KEY uk_name_ver (feature_name, calib_version)
);

三个设计点:

  • 点查缓存分层:名单类走本地缓存(短 TTL + 变更失效广播,名单准实时生效),实时聚合特征走 Redis。三方源按需 RPC 且带独立超时——单源故障不瘫痪决策;
  • 批量预热买的是授信体验:进件是"可用性优先于延迟"的场景(P99 1s)。提交后异步预取,决策链路少一跳冷查。支用场景(200ms)不做单用户预热,靠热点散热与缓存扛峰值。两种场景两种策略,不搞统一;
  • 特征口径版本化:线上线下一致性是特征平台第一难题(同口径双算 + 定期比对,见风控决策架构)。引擎侧的锚就是 calib_version 随决策快照落库——回放历史决策时,特征口径可复原。

三、版本与灰度:规则回滚与冠军挑战者 ​

一次决策的全要素版本链:策略 flow_v12 → 规则表 v3/v9 → 模型 card_a_v7 → 特征口径 vN,全部随决策快照固化。系统层两个机制:

1)冠军/挑战者分流 ​

请求 ──▶ 分流器(user_id 哈希) ──┬─ 95% ─▶ 冠军流程(champion, 线上决策)
                              └─  5% ─▶ 挑战者流程(challenger, 同步决策+双记)
                                            │
                                双跑观察期: 通过率/时效/首逾率(FPD30)对比
                                            │
                            达标 ──▶ 全量切换(旧版本置 ROLLED_BACK, 随时可指回)
  • 分流键用 user_id 哈希而非随机:同一用户体验一致,且表现期数据可比。同一批人始终在挑战者组里,不互相污染;
  • 新模型先进影子模式(只记分不参与决策,验证分数分布与线上一致)→ 再进挑战者 → 最后切流量。三段式每段有退出条件。

2)回滚 ​

回滚 = 旧版本置 FULL、新版本置 ROLLED_BACK,配置生效秒级完成。依赖的是快照里的版本号,不是 Git:回滚后新决策用旧版本跑,历史决策仍按当时版本可解释。两者互不干扰,这是版本绑定请求(见追问 Q2)的直接收益。

四、性能设计:P99 预算分配到规则集 ​

总预算不是引擎的平均值,是硬分解到每个节点的合同(预算的业务口径见风控决策架构):

支用决策总预算 200ms(节点执行时序):
  ┌ 前置规则(决策表,纯内存)  20ms ── 命中硬拒绝立即短路返回, 特征都不取
  ├ 特征点查(并行取 N 个)   50ms ──┐ 无依赖节点并行;
  ├ 名单/黑名单(本地缓存)    5ms ──┘ 慢源独立超时走缺失策略
  ├ 模型推理               30ms ── 超时 fallback 旧模型/评分卡
  ├ 后置规则(定价矩阵,纯内存) 20ms ── dependsOn 满足即执行
  ├ 编排开销               10ms ── DSL 解析结果按版本缓存
  └ 决策快照落库            异步 ── 写 MQ 不占同步链路(要素同步应答,全量异步补)
  ────────────────────────────
  重试与网络抖动余量         65ms

四个性能手段:

  • 短路优先:硬拒绝命中立即返回,后续节点全部跳过——多数流量在第一层出结果,平均耗时远低于 P99;
  • 无依赖并行:特征批量取、独立规则集并行执行,编排 DSL 的 dependsOn 声明依赖图,引擎按图调度;
  • 每节点埋点:哪个规则集吃掉了 P99 在监控上直接可见,性能回归按节点归因,不做无差别 APM 大盘;
  • 影子压测:特征读放大是大促瓶颈(一次决策 N 个特征键)。压测流量带影子标记走真实链路但不落快照,容量按上次峰值 × 1.5 规划。

五、异常与降级 ​

降级动作声明在每个节点的 DSL 里(degrade 字段),故障时引擎自动执行预案而非人工拍板:

故障层级degrade 动作语义
准入类规则/名单源超时REJECT红线规则不可跳过(fail-close)
排序/定价类规则超时SKIP可跳过,损失精度不损失决策
模型服务超时FALLBACK(旧模型/评分卡)规则层完好,仅损失模型精度
特征源超时按 feature_meta 缺失策略(CORE→保守,NORMAL→默认值打标)单源故障局部化
引擎整体不可用上抛给授信/支用系统做场景级降级(授信 fail-close / 支用静态规则)引擎之外还有地板

熔断与预算联动:某数据源错误率超阈值自动熔断(进入缺失策略),恢复后半开探测。熔断的对象是数据源与模型服务,引擎自身对决策请求永不熔断。引擎熔断了决策就整体不可用,此时应由上游的场景级降级接管。授信与支用的分级差异见授信系统设计第六节。

六、追问链 ​

Q1:为什么不用 Drools 或干脆硬编码? 硬编码:改一条阈值发一次版,策略迭代速度归零。Drools 一类通用规则引擎:表达力过剩但运维复杂度同样过剩(RETE 网络调试成本高)。而决策表、模型服务接入、灰度分流这些信贷刚需它都要二次开发。自研轻量 DSL 的本质是"只实现信贷要的那 20%",代价是表达能力有边界。复杂到 DSL 撑不住的策略,正确动作是拆场景,不是给 DSL 加图灵完备性。

Q2:一次请求会不会穿过新旧两个版本? 不会。请求进入时绑定 flow_version,全程(含规则表版本、模型版本、特征口径)按该绑定解析;灰度切换只影响新请求。版本贯穿是快照可回放的前提——中途换版本的决策无法解释,等于没有版本。

Q3:特征缺失当 0 还是很差值,谁说了算? feature_meta 的 missing_policy 配置,配置权在策略分析师不在代码。核心特征(收入类)缺失走保守(拒绝或人工),普通特征给默认值并打标。写成代码常量的缺失策略,会在下一次"这个特征没那么重要"时变成一次发版事故。

Q4:冠军挑战者的表现期要多长才有结论? 信贷坏账表现要 6~12 个月才充分显现(见风控决策架构追问 Q4)。所以切换依据分两层:通过率、审批时效这类即时指标 + 首逾率(FPD30)这类早期风险指标支撑小步切换。完整表现周期数据做事后校准。等 12 个月才切换业务上不可接受,但必须清楚:小步决策买的是速度,不是结论。

Q5:决策快照那么大,全量落库扛得住吗? 快照分级:决策要素(版本号/命中明细/最终决策/原因码)必落库且与决策应答同步。全量特征值走 MQ 异步进宽表,异步失败补偿,最终必达。更省的方案是只存特征版本号 + 取数时间戳,依赖特征平台的时点回溯能力。两种方案按监管沟通结果选,但"要素同步"没有商量余地。

Q6:规则流支持循环吗?会不会死循环? 不支持任意循环,只支持 DAG:编排发布前做环检测与深度上限校验(如 ≤ 20 层),校验不过不允许上线。"回到上一步重评"类需求一律改写成显式的重评节点。图灵完备的编排 DSL 是规则引擎的深渊,策略分析师要的是流程图,不是编程语言。

Q7:P99 超了怎么证明是特征的锅? 预算分解 + 每节点耗时埋点:特征点查预算 50ms 实际 120ms,直接归因到慢源。没有节点级预算,性能回归只能得出"引擎变慢了"这种无法行动的结论。预算合同是性能问题的责任划分依据。

七、面试官视角小结 ​

面试官在决策引擎设计里检查四件事:

  • 编排选型:知不知道决策树/表/流不是三选一而是组合。"策略与代码分离"的验收标准是全程不发版;
  • 版本意识:全要素版本 + 快照绑定 + 指回式回滚——只会说"灰度开关"说明没做过策略回溯与审计;
  • 性能是预算不是感觉:能不能把 200ms 拆到节点,并说出短路、并行、异步快照三个手段;
  • 降级的边界感:节点级降级(缺失策略/fallback)与场景级降级(fail-close/静态规则)分层清晰。知道自己引用的是哪一层。

一句话总结:规则流串节点、特征分层取、版本绑快照、预算拆到点、降级分两级。策略跑在引擎上,解释留在快照里。

2026-10 新增(金融信贷域 · 系统设计层)。