首页/设计深潜/闭环与契约

闭环与契约

判定触发了、行动也外送了——然后呢? 审计线如果止于「已发出」,产品就只是告警器。这页讲回来的路: 送出 N 天之后把那条度量测一次(效果验收),按规则版本分桶回评 「判得准不准」,出一份字节稳定的质量报告;以及主动扫描怎样把 「没人报的异常」变回判定规则草稿——让环闭回起点。全程复用同一条 噪声带与同一个 2×2—— 「判定说超带」与「验收说超带」,是同一句话。

闭环缺的是最后一段

没有验收的告警系统,连自己准不准都不知道:触发率 100% 的规则可能在报噪音, 一条从未触发过的规则可能口径早就烂了。回评需要三样以前没有的东西—— 一次诚实的测量(不是「跟以前不一样」,而是「超出噪声带了吗」)、 一个时间的时钟(行动当天就测,测到的是行动前的数)、和一个 版本账本(规则被改过之后,旧判定不能算到新口径头上)。 这页的四个模块各自解决一样,全部纯派生、零 LLM。

flowchart LR
    V["判定 verdict
(含触发日与规则版本)"]:::gate --> P["提案 → 审批 → 外送"]:::act P --> W["N 天后的小时级 sweep
measure_due"]:::entry W --> M["verifier(包外执行面)
ITS + 可选 DiD 净效应"]:::exec M --> O["outcome 行
超带 / 无变化 / 判不了"]:::entry O --> S["回评分桶
(rule_id, rule_rev)"]:::entry S --> R["质量报告 / 全局面"]:::entry D["主动扫描
扫描发现 → 规则草稿"]:::entry --> V classDef entry fill:#71717a14,stroke:#71717a59,stroke-width:1px; classDef gate fill:#f59e0b14,stroke:#f59e0b99,stroke-width:1.5px; classDef act fill:#8b5cf614,stroke:#8b5cf699,stroke-width:1.5px; classDef exec fill:#14b8a614,stroke:#14b8a699,stroke-width:1.5px; linkStyle 6 stroke:#8b5cf6,stroke-width:1.5px
图 1 · 闭环:扫描把发现变成规则草稿(人确认),规则判定触发行动,行动外送后由验收测回效果,回评分桶统计「判得准不准」——四个环节共用同一套噪声带口径。 上面这段以源码显示:图渲染库没加载起来(离线、CDN 被拦或浏览器禁用了 JavaScript 时会这样)。这段 Mermaid 文本本身就说明了上面那些框与连线。

验收:三层结局,没有第四种

效果测量的核心是一个纯函数 effect.py:131 measure_effect: 把行动前历史块与行动后水平放在一起,给出 ITS(中断时间序列)核心量—— 中心量一律取 median(与噪声带 center 同口径:「行动后水平」恰恰是要抗单点的那一侧), 跨零/超带判定用与判定侧同一个稳健 z 阈值 effect.py:46 EFFECT_K。

结局是三层的,没有第四种:outside_band = True(行动后超出行动前噪声带)/ False(无可辨识变化)/ None(判不了,原因单列 degraded)。 False 不是失败——它是最诚实也最有价值的一档:「这次行动没动到数」是一个结论; 而「可能动了、但 n 不够/带退化」绝不冒充 False,与判定侧「算不出 ≠ 在带内」是同一条纪律。

还有一条安静但决定成败的选择:带只由行动前的块构成 effect.py:76 interrupted_design。 把行动后的点混进分布会自我稀释——那正是「行动有效」的假阳性来源,与扫描侧的历史块纪律同款。 规则声明了 causal.control 且对照帧取得到时,净效应按同一个 2×2 附加 effect.py:161 attribute_effect: method 升为 "its+did"——方法名描述的是最强的那层主张, delta/pct 仍来自 ITS,两者不是同一件事,方法名把她们分开; 对照取不到 → causal: "unavailable" + 原因,ITS 结论原样保留(对照不可用是验收侧最常见的形态,不是错误)。

verifier 契约:能力从外面给

测量要读业务库,但行动包(行动安全架构)的姿态是结构性的—— 构造签名不收连接器、整包不 import 数据源。所以执行面留在包外: ActionService 以可注入 callable 的形式接收 verifier outcome.py:98 make_verifier, 与 ActionDispatcher 的 transport 是同款姿态——能力从外面给,包本身够不到。

契约含义为什么这样定
async (proposal) -> dict | None None = 「还不能测」:规则的测量期还没滚过行动日,跳过、下次再试、不写行 与「测了」严格区分——安静跳过与安静失败长得一样,是最危险的混淆
dict 带 error 键 一次响亮的失败:规则被删/被停用、窗口不可解析、编译 MISS、查询失败、组不可解析……落成 error outcome,一行,不重试 闭环断了必须看得见,不能像「还没到期」一样安静
异常 未预期故障,同样被接成 error outcome 一次测量失败 = 一行 error,不是崩
窗口按规则的时钟重算 与触发它的判定用同一套 period 语义(grain 对齐 / same_phase / 过滤口径),复用 DecisionService 的同一批解析 helper 测量与判定读到不同口径的数字,正是闭环验收最不能出的错——同一批 helper 是结构性保证
期必须滚过行动日 window_end > 行动日 才测;否则返回 None 行动当天就在期内的测量,测的是「行动前的数」,不是效果
只测点名的那一组 提案创建时把消息点名的那一组钉进 evidence_refs["group"],验收读它不重算;per_dimension 规则取不到组 → group_unresolved error 重算会读到现在的证据,而验收要的是当时点的那一组;拿别的总体冒充被点名的组,是这一层最不能出的错
一次测量,一个结局 不是重试循环;verifier 恢复后不回填 回填时窗口已经滚走,填出来的会是另一份测量(测的是另一个期间)——拿它冒充当初的验收结果,是这一层最不能出的错

测量记录里还带着两个给审计看的数:exposure_days outcome.py:77 _exposure_days (测量期对行动的实际暴露天数——一个 10 月 5 日发出的行动,在「上个月」这种窗口下最早 10 月 31 日之后才被覆盖: 31 天里 26 天在行动之后,读者看得见这个数,就看得见测量滞后有多少是「期间还没滚完」); 以及 rule_rev 与 rule_rev_judged 两个版本—— 前者是测量所依据的规则内容,后者是判定当时那一版,两者不同说明规则在丈量之前被改过。

驱动:四道门,各有各的语义

到期测量由 measure_due service.py:605 measure_due 驱动。默认档全关(outcome_after_days <= 0 立即返回 0、不查库——未配置的部署逐字节不变); 刻度是天(测量是 N 天语义),所以挂在小时级的 sweep 腿上,不是 30 秒的 job tick。四道门:

门行为语义
未到 outcome_after_days跳过,不算已测到期后那次 sweep 会再看到它——「还没到时候」≠「测了」
verifier 返回 None跳过、不写行测量期没滚过行动日(verifier 的语义,不是失败)
verifier 缺席或抛异常落一行 error outcome,不重试闭环断了必须看得见——不能像「还没到期」一样安静
enabled不是门测量是读(只读查询 + 自己的账本),不是外送——关掉外送之后,已经出去的那批更需要验收

结果落在 outcomes 表 store.py:151 _OUTCOMES_SQL (store 迁移 v2 加的;outside_band 列用 0/1/NULL 三态存储—— 0 绝不是「没测到」,读侧对此有严格落位约定)。

回评:按规则版本分桶

质量回评是一个纯派生报告 score.py:109 score_history, 分桶键 = (rule_id, rule_rev) score.py:40 bucket_key。 这是 digest 与 rev 之分 的延续:digest 是整份 decisions.yml 的字节 sha256, 编辑 B 规则会让 A 规则的历史整段错位;rev 是单条规则的内容版本——改 A 才开 A 的新桶。 更老的 verdict 没有 rev → 落 rev_unknown 桶(不猜、不丢:没有版本信息的判定,仍然是一段可以被看的历史)。

三个口径上的坚持:

报告:同一份数字,逐字节可复算

呈现层是三个纯函数 quality_report.py:48 build_report: build_report(单源)/ fleet_report(全局面)/ render_markdown(markdown 呈现)。 口径的唯一权威在 score.py——呈现层不重写任何指标,只做包装与措辞; 两个出口各算一遍的后果不是「重复」,是漂移:同一段历史在两个地方给出两个有效率, 比没有报告更糟(与离线回归门 eval/gate.py 的教训同款)。 generated_at 由调用方给、缺省空串,同一输入 → 逐字节同一输出(dict 键序固定)。

两处措辞纪律:跨源汇总只给总量——同名规则在两个数据源是两条规则, 把两个源的 revenue_drop 拼成一条「历史」会造出一段从未存在过的历史(per-rule 比率只在各自桶里); 比率算不出时渲染成「—」而不是 0%——「样本不足」不许被折成任何百分比。 admin 面上:GET /v1/admin/decisions/quality decisions.py:552 decision_quality 给单源分桶明细,GET /v1/admin/quality/overview quality.py:435 admin_quality_overview 给逐源 fleet 报告,提案级效果在 GET /v1/admin/actions/proposals/{id}/outcomes actions.py:198 get_action_outcomes。

主动扫描:把发现变回规则

环的另一半起点是没人报的异常。主动扫描按 ScanSpec(metric × dim 单元列表) 定期跑一遍:每个单元至多一条 SQL scanner.py:117 scan_unit ——一条查询同时取回「历史块 + 当期窗口」,标签切分决定谁进带、谁是当期; 判据与判定侧完全同款(history-only 的带、稳健 z、k 默认 3.5)。 发现分两类 FINDING_KINDS = ("anomaly", "unverifiable"): 「超带」是发现,「判不了」也是发现——算不出的单元恰好一行 unverifiable,从不静默少一行; 截断(top_k)只截 anomaly scanner.py:249 cap_findings—— 截掉的是「还不值得人看」的,不是「说不清」的。 一条容易写错的边界在这里被写死:缺失 ≠ 0——当期无行可能是数据未到/未落, 不是「掉到零」的证据(no_current_block 记账,不产发现)。

发现的出口是判定规则草稿(人确认后成为规则):扫描→草稿→规则→判定→行动→验收, 环从这里闭回起点。规格在写时校验 scanner.py:89 spec_issues: 度量/维度/时间字段/锚定数据集逐条解析——没有时间字段的度量从来给不出块, 注定每次 tick 都只产 unverifiable 的规格不该被写进任务(错误在创建时说得清,在运行记录里说不清)。

扫描的 hypotheses 档(默认关)多了第二层:让 LLM 就发现起草候选假设 (「假设华东下滑 ≥10% 由车贷逾期引起」这类可检验陈述),但裁决是纯函数 hypotheses.py:266 _adjudicate: 取当期与基期两个数,按声明方向与最小幅度判 supported / refuted / unverifiable(判不了说判不了——基期为 0 且声明了最小幅度时,方向还能看但幅度不可比, 不硬判)/ no_data。起草是 LLM 的活(parse_candidates 解析 + 引用校验门槛), 裁决是确定性的活——每条假设恰好一行 outcome,输入序保持不变。

怎么用

# conf/agent.yml(或管理端设置)
action:
  outcome_after_days: 14      # 外送满 14 天才测;0 = 关闭(默认)
scan:
  hypotheses: false           # 假设起草档(默认关)
  max_queries: 12             # 扫描每轮的查询预算
  k: 3.5                      # 与判定同一条带

扫描作为定时任务运行(job 带 scan_spec 段;任务类型优先级 decision_rule > scan_spec > 自然语言管线),报告落在 run 证据里 (ScanReport:发现 / 判不了 / 截断数 / 草稿 / 预算 / 证据 SQL 全文)。 验收不需要新配置以外的操作——到期自动测、自动回评分桶;出错自动进 admin 待办。 管理端操作与判定历史界面见判定规则与 行动提案。

取舍

选择放弃了什么为什么值得
一次测量,一个结局,不回填 verifier 修好后把历史缺口补上 回填时窗口已滚走,填出来的是另一个期间的测量——冒充当初的验收结果是这一层最不能出的错
error 行不重试 自动恢复的测量 测量不是重试循环;一行 error 让断链看得见,比悄悄重试到「成功」更诚实
按 rule_rev 分桶(非 digest) 「一个规则一个桶」的简单分桶 编辑 B 规则不该让 A 的判定史错位;改过口径的规则,旧判定不能算到新账上
MIN_EFFECTS = 3 比率门 样本很少时也给一个「有效率」 1/1 不是 100%;伪精度比没有数字更糟。不足时给原因(few_effects…)而不是给 0
报告纯派生、不写回 预计算好的质量表和它的查询速度 同一段历史在任何时刻重算都逐字节相同——可复算是这层的意义本身
enabled 不是测量门 「关了外送就一切停」的直觉 测量是读不是写;关掉外送之后,已经出去的那批更需要验收
截断只截 anomaly 发现列表的整齐(全部 top_k) 「判不了」的数量由扫描单元数(metric × dim)封顶,保留它才是「判不了要看得见」的兑现

优势在哪

继续读

想了解去哪
噪声带本身的统计器械统计与噪声带
因果升级梯与 what-if(验收侧复用同一个 2×2)因果与模拟
提案、审批、外送与只读姿态行动安全架构
验收与质量的管理端操作行动提案 · 效果
主动扫描的完整字段与任务配置判定规则 · 扫描
本页源码锚点的基准提交:anchor-base: 211ae86 GitHub 仓库 文档首页