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 扫一份旧报告 |
看它能检出哪些信号——理解"信号词"的局限 |
| 故意在报告里写「平均延迟」 | 脚本会提示改用百分位 |
| 把自评脚本接进团队的报告模板 | 从流程上防止反模式 |
| 统计你自己过去写的性能报告踩了几条 | 这是最有教育意义的一次练习 |
五、这段代码的局限
- 正则扫描只能发现"信号词",无法理解语义——例如「我们没有只跑一轮」会被误报为「单轮定论」。
- 它不能替代人工判断:一段诚实的报告里可能提到「平均延迟」(作为对比),这不构成反模式。
- 自评脚本依赖自评者的诚实:如果实验者有意隐瞒,任何工具都救不了。
- 最有效的机制不是工具,而是流程:把噪声底线、统计检验、异常值解释做成报告模板的必填项,比事后扫描有效得多。