Clef 做决策推理、规则引擎做兜底拦截

文章导读
把 Clef 接进已有规则系统,接口通不通是小事,职责边界才是主要麻烦。常见的分法是:能被写成阈值、名单、开关、时间窗的判断,全部收在规则引擎里做硬性拦截;需要在多个因素之间折中、没有唯一正确答案的判断,才交给 Clef 打分排序。规则引擎既做前置闸门,也做最终复核,Clef 只在中间给候选分数,不直接决定放行还是拒绝。
📋 目录
  1. 一 列出必须由规则引擎硬性拦截的条件
  2. 二 把可量化的复杂权衡交给 Clef 评分
  3. 三 设计调用顺序:先规则过滤,再 Clef 评分,最后规则复核
  4. 四 在日志中记录两段决策的输入输出和最终结果
  5. 五 用一组边界案例验证职责划分是否生效
A A

把 Clef 接进已有规则系统,接口通不通是小事,职责边界才是主要麻烦。常见的分法是:能被写成阈值、名单、开关、时间窗的判断,全部收在规则引擎里做硬性拦截;需要在多个因素之间折中、没有唯一正确答案的判断,才交给 Clef 打分排序。规则引擎既做前置闸门,也做最终复核,Clef 只在中间给候选分数,不直接决定放行还是拒绝。

判断方向:金额上限、黑名单、禁入地区、频次与资质这类布尔条件必须由规则引擎硬拦截,Clef 不参与;收益与风险的权衡、候选排序交给 Clef 打分,但分数只是输入,最终动作仍回到规则复核。适用场景是有存量规则系统的决策服务。动作是先固化硬条件清单,再定 Clef 的输入输出契约,最后补日志与边界用例。验证方式是构造越界与冲突样例,确认规则层能拦住 Clef 的越界输出。
判断类型归属典型内容输出形态冲突时以谁为准
硬性拦截规则引擎(预过滤 + 复核)金额上限、黑名单、禁入地区、频次、资质、时间窗布尔结果 + 命中规则 id + 拒绝原因规则引擎
多因素权衡Clef风险与收益折中、候选排序、相似度、人工审核优先级分数 + 理由标签 + 版本号不能直接决定放行,必须过规则复核
阈值参数规则引擎保存,Clef 只读上限、下限、权重档位带版本号的配置规则引擎版本为准

这张表建议先和业务方对齐再改代码。一旦某条判断落进“规则引擎”这一列,就不要在 Clef 的输入或提示里再复现一遍,否则两处各有一套阈值,出问题时很难判断是谁在生效。

列出必须由规则引擎硬性拦截的条件

先把硬条件写成一份可评审的清单。格式用什么无所谓,关键是每条都有 id、判断表达式、动作和拒绝原因,能被日志直接引用。

hard_rules:
  - id: amount_over_limit
    when: order.amount > LIMIT_AMOUNT
    action: reject
    reason: over_amount_limit
  - id: in_blocklist
    when: user.id in blocklist or device.id in blocklist
    action: reject
    reason: blocklist_hit
  - id: region_forbidden
    when: order.region not in allowed_regions
    action: reject
    reason: region_forbidden
  - id: freq_limit
    when: count(user.id, 1h) > FREQ_LIMIT
    action: reject
    reason: freq_limit

清单里的上限值、名单、允许地区不要写死在代码里,放进带版本的配置,改动留审计记录。判断表达式建议只允许有限的字段和运算,别让规则本身变成第二套编程语言。

这四类条件不适合交给 Clef。一是要求确定性和可复现,同一输入同一规则版本必须得到同一结果,模型的输出通常带随机性;二是审计需要指到具体条目,例如“被 amount_over_limit 拦下”,模型的理由标签给不到这个粒度;三是输入里的软性描述容易带偏判断,“必须拒绝”这种结论不适合由概率输出承担。

把可量化的复杂权衡交给 Clef 评分

典型场景是订单要不要转人工审核。预过滤之后剩下的候选,通常还带着近 30 天退款次数、设备启用天数、收货地址是否与常用地址一致、下单时段、历史人工审核结果这些因素。没有哪个因素能单独决定,但合在一起可以排出优先级,这类判断适合交给 Clef。

输入按“请求上下文 + 信号 + 已通过的规则”三段组装,输出只保留分数、建议档位、理由和版本号。字段名按你的系统替换,下面只是形状:

Clef 做决策推理、规则引擎做兜底拦截
{
  "request_id": "req-001",
  "signals": {
    "refund_count_30d": 0,
    "device_age_days": 3,
    "address_mismatch": true,
    "order_hour": 2
  },
  "passed_rules": ["amount_limit", "blocklist"]
}
{
  "score": 0.63,
  "decision_hint": "review",
  "reasons": ["device_age_days_low", "address_mismatch"],
  "model_version": "clef-vX"
}

两条约束要写进契约:score 只是排序和分档信号,不是放行凭证;decision_hint 只允许枚举值,比如 pass / review / reject,调用方拿到之后仍然要过规则复核。输出体要做字段校验,score 为 null、缺字段或超出约定区间,都按无效输出处理。把 passed_rules 传进去,是让 Clef 知道哪些约束已经成立,不用在评分里再权衡一遍。

设计调用顺序:先规则过滤,再 Clef 评分,最后规则复核

  1. 规则预过滤:加载硬规则配置,对请求逐条求值。命中 reject 直接返回,Clef 不被调用,日志里 clef_called 记 false。
  2. 组装 Clef 输入:只放通过预过滤的字段,同时带上已命中的规则 id。
  3. 调用 Clef 评分:设超时上限,拿回分数、理由、档位和版本号,并做一次字段校验。
  4. 规则复核:把 decision_hint 映射成候选动作,再过一遍硬规则。规则与 Clef 意见冲突时以规则为准,冲突写入日志。
  5. 返回并落日志:最终动作、两段命中、耗时,一个请求一行。
def decide(req):
    pre = rules_pre_check(req)
    if pre.blocked:
        return respond(pre, clef_called=False)

    try:
        scored = clef.score(build_input(req, pre.passed_rules), timeout_ms=TIMEOUT)
        validate(scored)          # 字段齐全、score 在约定区间内
    except (TimeoutError, BadResponse):
        return respond(fallback_by_rules(req), degraded=True)

    post = rules_post_check(scored)
    if post.blocked:
        return respond(post, conflict=True)
    return respond(merge(scored, post))

每一步的失败处理要提前定,不要临时决定:

  • 规则配置加载失败:走保守默认动作,通常是拒绝或转人工,不要默认放行。
  • Clef 超时或报错:降级到规则兜底动作,日志记 degraded=true,异常分支里同样不默认放行。
  • Clef 返回无效:字段缺失、score 越界,按兜底处理,不把异常值直接映射成动作。
  • 复核与 Clef 相反:以规则为准,记 conflict=true,后续用来判断规则是否过严或 Clef 输入是否缺信号。

在日志中记录两段决策的输入输出和最终结果

一个请求一行日志,字段尽量扁平,便于按 request_id 把两段决策串起来。下面这套字段可以直接照搬改字段名:

  • request_id、ts:串联与时间定位。
  • rules_pre_hit、rules_pre_blocked:预过滤命中的规则 id,以及是否被拦下。
  • clef_called、clef_input_digest:是否真的调了 Clef,以及脱敏后的关键信号或输入摘要。
  • clef_score、clef_reasons、clef_version、clef_latency_ms:Clef 的输出与版本。
  • rules_post_hit、conflict:复核命中的规则,是否与 Clef 意见冲突。
  • final_decision、degraded、reason:最终动作、是否降级、原因码。
{"request_id":"req-001","rules_pre_hit":[],"clef_called":true,"clef_score":0.63,"clef_reasons":["address_mismatch"],"clef_version":"clef-vX","rules_post_hit":["manual_review_required"],"conflict":false,"final_decision":"review","degraded":false}

原始敏感字段不要直接落盘,手机号、证件号、完整地址这类做掩码或哈希,只留判据不留原文。字段固定下来之后,事后归因时能直接回答三个问题:被哪条规则拦下、Clef 给了多少分和什么理由、最终动作是按规则还是按评分做出的。

用一组边界案例验证职责划分是否生效

案例构造方式期望结果重点看的字段
正常金额低于上限、不在名单、信号平稳,Clef 给高分并提示 pass预过滤不拦,复核不拦,最终通过clef_called=true、conflict=false、final_decision=pass
越界金额超过上限,Clef 仍给出高分和 pass 提示预过滤直接拦下,Clef 不应被调用clef_called=false、rules_pre_hit 含金额规则
冲突命中必须放行的白名单,Clef 却给 review以规则为准,动作按白名单执行conflict=true、final_decision 与规则一致

验证方法:把这三类样例做成可回放的固定请求集,跑一遍之后只看日志,不靠记忆。确认三件事——越界样例的 clef_called 必须是 false;把越界样例绕过预过滤直接喂给 Clef 时,复核阶段仍要拦下它的 pass 输出;降级样例里 degraded=true 且 final_decision 不是放行。规则配置版本变更后重跑同一组样例,结果不应出现与规则无关的漂移。