文档目录

5.8 配套代码:结论强度自评与反模式检查

对应小节:5.8 实验记录与常见反模式 两份工具:① 报告前的结论强度自评;② 自动扫描实验档案里的反模式信号。

一、结论强度自评(交互式)

#!/usr/bin/env bash
# tools/check-conclusion-strength.sh
#
# 报告任何性能结论前,过一遍这十二条。
# 这不是形式主义——每一条都对应一个真实的反模式。
set -uo pipefail

FAIL=0
WARN=0

ask() {
  local q="$1"
  local hint="$2"
  echo
  echo "── $q"
  echo "   ($hint)"
  read -r -p "   [y=是 / n=否 / s=跳过] " ans
  case "${ans,,}" in
    y) echo "   ✅" ;;
    n) echo "   ❌ 这一条答「否」,结论强度要打折扣"; FAIL=$((FAIL+1)) ;;
    *) echo "   ⏭  跳过"; WARN=$((WARN+1)) ;;
  esac
}

echo "═══════════════════════════════════════════════════════════"
echo "结论强度自评(十二条)"
echo "═══════════════════════════════════════════════════════════"

ask "① 实验前写了假设和判定标准吗?" \
    "如果没写,可能事后调整了标准(反模式 ❼)"

ask "② 只改了一个变量吗?" \
    "如果有多个改动,无法归因(第 5.1 节)"

ask "③ 有对照组吗?" \
    "没有对照组的话,只有一组数字,不构成结论"

ask "④ 跑了几轮?≥ 3 吗?" \
    "单轮定论是反模式 ❹"

ask "⑤ 报了中位数和波动范围吗?" \
    "只报平均值或最好的一次可能是反模式 ❶❽"

ask "⑥ 知道环境的噪声底线吗?" \
    "不知道就无法判断差异真假(反模式 ❺,第 5.7 节)"

ask "⑦ 差异大于噪声底线和 MDD 吗?" \
    "小于的话可能是噪声(反模式 ❺)"

ask "⑧ 做了统计检验吗?" \
    "缺少显著性的证据(第 5.6 节)"

ask "⑨ 异常轮有成因解释吗?" \
    "没解释就剔除可能是删掉了真实问题(第 5.3 节)"

ask "⑩ 观测配置前后一致吗?" \
    "差异可能来自观测开销(第 5.4 节)"

ask "⑪ 有没有记录失败的实验?" \
    "不记录失败实验会丢失信息(反模式 ❻)"

ask "⑫ 结论里写了「未验证的场景」吗?" \
    "不写等于过度承诺"

echo
echo "═══════════════════════════════════════════════════════════"
echo "结果:$FAIL 条答"否",$WARN 条跳过"
echo
if [ "$FAIL" -eq 0 ]; then
  echo "✅ 结论强度良好,可以报告"
elif [ "$FAIL" -le 2 ]; then
  echo "⚠️  有 $FAIL 条不足 —— 建议补上后再报告"
  echo "   或者在报告里明确声明这些局限"
else
  echo "❌ 有 $FAIL 条不足 —— 结论目前不可信"
  echo "   建议:补做实验,而不是急着写报告"
fi
echo "═══════════════════════════════════════════════════════════"

提示:脚本里的交互提示如果需要引用「否」这类词,请用中文引号 「」——在 shell 字符串里嵌 ASCII 引号会直接破坏语法。

二、反模式自动扫描

# tools/scan_antipatterns.py <experiment_readme.md>
"""
扫描实验档案,检出九种反模式的"信号词"。

注意:这只能发现"可能的"反模式,不能替代人工判断。
它的价值是——提醒你去检查那些容易忽略的点。
"""
import re
import sys
import pathlib


PATTERNS = [
    ("❶ Cherry-picking(挑好看的一次)",
     r"最好的一次|最快的一次|最佳一轮|挑了一轮",
     "报告应该用中位数,并列出所有轮次"),

    ("❷ 忽略预热",
     r"(?<!未)(?<!不)预热|warmup",
     "确认有【独立】的预热阶段,且数据不计入统计(反向检查:如果是"未预热"则是问题)"),

    ("❸ 指标漂移(中途改东西)",
     r"中途|期间改|顺手|顺便也",
     "确认实验期间没有其他改动"),

    ("❹ 单轮定论",
     r"跑了一次|只跑了一轮|单次运行",
     "至少 3 轮,报中位数与波动范围"),

    ("❺ 把噪声当成果",
     r"提升\s*\d(\.\d)?%|优化\s*\d%",
     "确认差异 > 噪声底线(第 5.7 节)——小幅度提升需要特别警惕"),

    ("❻ 不记录失败实验",
     r"(未|没有)(记录|写下).*(失败|无效)",
     "失败实验的信息量常常更大"),

    ("❼ 事后定标准",
     r"后来把标准|改成.*就算|放宽.*阈值",
     "判定标准必须在实验前定好"),

    ("❽ 用平均值汇报",
     r"平均延迟|平均\s*P\d|均值.*ms",
     "改用 P50/P95/P99 + 中位数"),

    ("❾ 混淆显著与值得",
     r"p\s*[<=]\s*0\.0[0-9].*所以.*采纳|显著.*因此.*应该",
     "统计显著 ≠ 值得做,还要看用户是否可感知"),
]

NEGATIVE_PATTERNS = [
    ("✅ 有对照组", r"基线|对照|before|A/B"),
    ("✅ 有轮数", r"\d+\s*轮|round"),
    ("✅ 有噪声底线", r"噪声底线|noise.?floor|MDD"),
    ("✅ 有统检计验", r"Mann.?Whitney|bootstrap|p\s*[<=]|置信区间"),
    ("✅ 有置信区间", r"置信区间|CI|\[[\d.]+,\s*[\d.]+\]"),
    ("✅ 有环境元数据", r"env\.txt|环境元数据|commit|jvm_args"),
    ("✅ 有未验证场景", r"未验证|未覆盖|遗留问题|局限"),
    ("✅ 有异常值解释", r"异常.*(原因|因为|由.*造成)|剔除.*原因"),
]


def scan(path):
    text = pathlib.Path(path).read_text(encoding="utf-8", errors="ignore")

    print("═" * 74)
    print(f"反模式扫描:{path}")
    print("═" * 74)
    print()

    # 检查反模式信号
    print("【可能的反模式信号】")
    hits = []
    for name, pattern, advice in PATTERNS:
        matches = re.findall(pattern, text, re.IGNORECASE)
        if matches:
            hits.append((name, matches[:3], advice))

    if hits:
        for name, matches, advice in hits:
            print(f"  ⚠️  {name}")
            print(f"      匹配: {matches}")
            print(f"      建议: {advice}")
    else:
        print("  ✅ 未检测到明显的反模式信号")
    print()

    # 检查正面要素
    print("【应该有的要素】")
    missing = []
    for name, pattern in NEGATIVE_PATTERNS:
        if re.search(pattern, text, re.IGNORECASE):
            print(f"  {name}")
        else:
            missing.append(name.replace("✅ ", ""))
            print(f"  ❌ 缺少:{name.replace('✅ ', '')}")
    print()

    print("═" * 74)
    if missing:
        print(f"缺少 {len(missing)} 项:{', '.join(missing)}")
        print()
        print("补充建议:")
        for m in missing:
            if "对照组" in m:
                print("  - 对照组:说明基线是什么(哪个实验编号、什么条件)")
            elif "轮数" in m:
                print("  - 轮数:每轮的结果都要列出,并给出中位数")
            elif "噪声底线" in m:
                print("  - 噪声底线:引用你的 E0x-noise 实验,或明确说明未测")
            elif "统计检验" in m or "置信区间" in m:
                print("  - 统计检验:用 Mann-Whitney U 或 Bootstrap 给出 p 值/区间")
            elif "环境元数据" in m:
                print("  - 环境元数据:粘贴 results/env.txt 的内容")
            elif "未验证" in m:
                print("  - 未验证场景:诚实列出本次没覆盖的情况")
            elif "异常值" in m:
                print("  - 异常值解释:每个异常轮都要有成因分析")
    else:
        print("✅ 要素齐全")
    print("═" * 74)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("usage: scan_antipatterns.py <experiment_readme.md>")
        sys.exit(1)
    scan(sys.argv[1])

三、实验档案的「结果」章节模板(本章新增四项)

## 结果

### 环境(实验期间的固定条件)
- 噪声底线(本环境,P99):±13.2%,MDD ±26.4%(来源:E05-noise 实验)
- 环境事件:T+15min 有一次日志轮转(已记录,未造成异常)
- 与基线的环境差异:无(同一台机器、同一套 JVM 参数)

### 逐轮结果
| 轮次 | QPS | P50 | P95 | P99 | 错误率 | 备注 |
| --- | --- | --- | --- | --- | --- | --- |
| 1 | 498 | 32.1 | 68.4 | 93.2 | 0.00% | |
| 2 | 499 | 33.0 | 70.2 | 96.1 | 0.00% | |
| 3 | 497 | 32.4 | 67.9 | 94.0 | 0.00% | |
| 4 | 498 | 34.2 | 74.1 | 103.5 | 0.00% | ⚠️ 日志轮转期间 |
| 5 | 499 | 32.8 | 69.3 | 95.2 | 0.00% | |

### 汇总(稳健统计)
- P99 中位数:95.2 ms(全部 5 轮)/ 94.6 ms(剔除第 4 轮)
- 四分位距:93.8 – 96.1 ms
- 轮间 CV:3.1%
- 趋势:无漂移(r = 0.21)

### 异常值说明
- 第 4 轮 P99 = 103.5 ms,超出正常范围(93–96 ms)
- 排查:环境事件时间线显示 T+2:15 有 logrotate,时间吻合
- 验证:手动触发 logrotate 后异常复现 → **环境异常**(非系统异常)
- 处理:保留记录,用其余 4 轮的中位数

### 统计检验
| 对比 | 指标 | 差异 | p 值 | 判定 |
| --- | --- | --- | --- | --- |
| vs 基线(E03) | P99 | -54.7% | 0.008 | ✅ 显著 |
| vs 噪声底线 | P99 | -54.7% vs ±13.2% | — | ✅ 远超噪声 |
| vs MDD | P99 | -54.7% vs ±26.4% | — | ✅ 超过 MDD |

### 结论
优化有效且可信:P99 从 210.3 ms 降到 95.2 ms(-54.7%),
统计显著(p = 0.008),远超噪声底线(13.2%)与 MDD(26.4%),
环境漂移可忽略(回滚验证 A' = 208.7 ms,漂移 0.8%)。

### 未验证的场景
- 冷启动路径(缓存为空时)
- 数据量增长 10 倍后的表现
- 多副本部署下的负载分配

四、动手改造

改动 观察什么
用 scan_antipatterns.py 扫一份旧报告 看它能检出哪些信号——理解"信号词"的局限
故意在报告里写「平均延迟」 脚本会提示改用百分位
把自评脚本接进团队的报告模板 从流程上防止反模式
统计你自己过去写的性能报告踩了几条 这是最有教育意义的一次练习

五、这段代码的局限

  • 正则扫描只能发现"信号词",无法理解语义——例如「我们没有只跑一轮」会被误报为「单轮定论」。
  • 它不能替代人工判断:一段诚实的报告里可能提到「平均延迟」(作为对比),这不构成反模式。
  • 自评脚本依赖自评者的诚实:如果实验者有意隐瞒,任何工具都救不了。
  • 最有效的机制不是工具,而是流程:把噪声底线、统计检验、异常值解释做成报告模板的必填项,比事后扫描有效得多。