# ch04 召回与过滤模块 — 代码示例

本目录对应教材第 4 章，覆盖召回模块的**工程链路**与过滤限流模块的**策略体系**，共 **13 个可运行脚本**。目录编号与正文节次一一对应，均可直接 `python3 <文件>.py` 运行。本章定位是「链路与策略」，模型只做能跑通的最小实现（双塔/图 Embedding 的完整模型见第 10 章、粗排模型见第 11 章）；除 `ch03_retrieval_taxonomy/embedding_retrieval.py`、`graph_walk_retrieval.py`、`ch05_filter_and_throttle/risk_control_evolution.py` 依赖 **PyTorch** 外，其余脚本只用标准库 + NumPy。

---

## 目录结构

```
code/
├── ch01_why_retrieval/                       § 4.1 为什么需要召回
│   └── cascade_budget_simulation.py    算力预算推演 + 级联漏斗的效果损失实测
├── ch02_multi_channel/                       § 4.2 多路召回架构
│   └── multi_channel_retrieval_service.py  [MOCK] 召回服务：并发多路 + quota + 去重 + 兜底 + 压测
├── ch03_retrieval_taxonomy/                  § 4.3 召回技术分类
│   ├── collaborative_filtering_retrieval.py  § 4.3.1 Item-CF / Swing / User-CF 离线建表 + 在线 I2I·U2U2I
│   ├── content_and_rule_retrieval.py         § 4.3.2 Tag 倒排 + 类目/热门/冷启/运营五路规则召回
│   ├── embedding_retrieval.py                § 4.3.3 双塔训练 → 物品向量导出 → IVF / HNSW ANN 检索
│   ├── graph_walk_retrieval.py               § 4.3.4 二跳路径 / PPR 图扩散 / DeepWalk·Node2Vec
│   └── retrieval_path_patterns.py            § 4.3 关系路径视角：U2I/I2I/A2A/U2U2I/U2I2I/I2U2U/I2I2U
├── ch04_result_fusion/                       § 4.4 召回结果融合
│   └── retrieval_fusion_pipeline.py    去重(保留 reason) → Quota → 四种截断 → 监控 → 动态配额 → 兜底
└── ch05_filter_and_throttle/                 § 4.5 过滤限流模块
    ├── filter_positions_pipeline.py          § 4.5.1 召回前/召回后/排序后三段式过滤的算力账单
    ├── read_history_bloom_filter.py          § 4.5.1 已读过滤：布隆过滤器实现 + 假阳率实测
    ├── filter_and_throttle_strategies.py     § 4.5.2 正文策略表的可运行实现 + 疲劳度/频控/滑动窗/令牌桶
    ├── penalty_system.py                     § 4.5.3 负向判罚：信号衰减聚合 → 定级 → 四类处罚动作
    └── risk_control_evolution.py             § 4.5.4·4.5.5 任务专用模型 → 统一多模态 + 规则·小模型·大模型级联
```

---

## 快速运行

```bash
# § 4.1 为什么需要召回
python3 ch01_why_retrieval/cascade_budget_simulation.py

# § 4.2 多路召回架构
python3 ch02_multi_channel/multi_channel_retrieval_service.py

# § 4.3 召回技术分类
python3 ch03_retrieval_taxonomy/collaborative_filtering_retrieval.py
python3 ch03_retrieval_taxonomy/content_and_rule_retrieval.py
python3 ch03_retrieval_taxonomy/embedding_retrieval.py
python3 ch03_retrieval_taxonomy/graph_walk_retrieval.py
python3 ch03_retrieval_taxonomy/retrieval_path_patterns.py

# § 4.4 召回结果融合
python3 ch04_result_fusion/retrieval_fusion_pipeline.py

# § 4.5 过滤限流模块
python3 ch05_filter_and_throttle/filter_positions_pipeline.py
python3 ch05_filter_and_throttle/read_history_bloom_filter.py
python3 ch05_filter_and_throttle/filter_and_throttle_strategies.py
python3 ch05_filter_and_throttle/penalty_system.py
python3 ch05_filter_and_throttle/risk_control_evolution.py
```

批量跑全部脚本：

```bash
find . -name "*.py" | sort | while read f; do echo "=== $f ==="; python3 "$f" >/dev/null && echo OK || echo FAIL; done
```

全部 13 个脚本在单核 CPU 上退出码均为 0，单文件最慢 11 秒（图召回），总耗时约 46 秒。

---

## 各脚本要点

> 下列数字均为本机实测输出（固定 `random.seed(42)`，`torch` 脚本额外固定 `torch.manual_seed(42)` 并设 `torch.set_num_threads(1)`），可复现。

### § 4.1 为什么需要召回

- **`cascade_budget_simulation.py`**：两个实验回答「为什么不能全量精排」。
  - **算力预算推演**：先实测单物品打分代价——召回内积 **0.0515 μs** / 粗排单隐层 MLP **0.5881 μs** / 精排三层 MLP **1.6877 μs**（精排/召回 = **32.8×**）。据此推演 $N=10^6$：全量精排 **1687.7 ms** 超 300 ms 预算，而「召回 $10^6$ + 粗排 5000 + 精排 500」的级联只需 **55.27 ms**（加速 **30.5×**）。$N=10^7$ 时级联线性扫描召回已达 514.85 ms，提示召回侧必须上 ANN（正文所说对数级复杂度）。
  - **级联漏斗的效果损失**：全库 50000、Top-500、20 次请求平均。召回/粗排开口 5000/500 → 20000/2000 时 Recall@500 从 **0.6671** 升到 **0.9758**，精排打分占全量 1%→4%；而把上下游打分噪声 $\sigma$ 从 0.60/0.30 降到 0.30/0.15（链路更一致）在 10000/1000 开口下即可拿到 **0.9858**，比「开口翻倍」更划算（0.8824 → 0.9758 需 2 倍算力）。

### § 4.2 多路召回架构

- **`multi_channel_retrieval_service.py`**：[MOCK] 进程内召回服务，8 路召回（hot / realtime / i2i / u2i / tag / u2u2i / u2i2i / graph）用线程池并发，mock 组件包含 Redis KV、用户画像、倒排索引、向量索引、正排索引，全部带调用计数。
  - 场景 1（全健康）：前处理 7.7 ms + 并发执行 **63.8 ms** = 服务总耗时 77.7 ms；串行累加 172.0 ms，**并发收益 2.70×**。817 条原始候选去重到 405（去重率 **50.43%**，多源命中 258 个），轻量过滤丢弃 `{offline:19, blocked_author:21, illegal:12}`，最终候选 353。
  - 场景 3（极端降级，只剩 hot 一路）：候选只有 30 条，兜底补充 **37 个**才凑到最低候选规模 60 —— 正文「必须有兜底」的直接体现。
  - 场景 4（20 并发压测）：P50 **74.8 ms** / P90 77.5 ms / max 79.4 ms，候选集规模 336~357。存储调用计数：`inverted_index.seek` 780 次、`redis.get` 160 次、`vector_index.search` 40 次 —— 说明倒排类召回才是存储压力的主要来源。
  - u2i2i 一路被注入 60 ms 超时并被 `timeout` 熔断，不影响整体返回，对应正文「单路故障不能拖垮整个召回服务」。

### § 4.3 召回技术分类

- **`collaborative_filtering_retrieval.py`**（§4.3.1）：400 用户 / 300 物品 / 7733 条行为（训练 6347、评测 1292）。离线把 Item-CF（sqrt 归一化）、Swing（共同用户的共同物品数惩罚）、User-CF 三张相似表各截断 TopN=30 写入 mock KV，构建耗时 **16.3 / 49.9 / 54.0 ms**。在线给出单请求 trace：trigger = 最近正反馈 + 时间衰减权重，I2I 只需 5 次 KV 访问。批量评测 Top20：**Item-CF HitRate 0.0451 / 覆盖率 98.3%**、**Swing 0.0420 / 64.3%**、**User-CF(U2U2I) 0.0488 / 94.0%**，平均在线耗时 0.052 / 0.047 / 0.065 ms。Swing 相似表更锐利但覆盖更集中，是「抑制热门」的副作用。
- **`content_and_rule_retrieval.py`**（§4.3.2）：2000 物品 / 12 Tag / 3 类目。离线每个 Tag 建长度 200 的倒排列表（后验点赞率 TopK 140 + **随机 60**，即正文提到的随机采样缓解系统 bias），另建 82 条冷启池（发布 <24h 且曝光 <800）与 20 条运营白名单。在线五路规则召回按配额 150/60/50/40/20 合并去重得 273 条，reason 分布 `{tag:150, cat_hot:53, global_hot:30, cold_start:25, operation_boost:15}`。对比实验（random 比例 0.0→0.8）：去重候选数 **742→813**、长尾覆盖数 **341→377**、全库曝光基尼 **0.7510→0.7309**、候选平均内在质量 **0.0602→0.0571** —— 多样性与精度的直接权衡。脚本输出中已诚实标注两点：Tag 打分本身用后验点赞率 $like/(expo+\beta)$ 已有去热门效果，所以基尼改善幅度有限；ratio=0.1/0.3 时基尼略有上升属于合成数据噪声。
- **`embedding_retrieval.py`**（§4.3.3）：600 用户 / 4000 物品 / 12580 正样本 / 8 个隐含主题，双塔用 In-Batch Sampled Softmax（$\tau=0.1$）训练 6 epoch，loss 5.5487 → **3.9552**；表征检查同主题平均余弦 **+0.7301** vs 跨主题 **+0.6240**。自己实现了 IVF（k-means 粗聚类 + nprobe 探测）与 HNSW（分层图 + 有界 beam 搜索），以精确检索为 ground truth，200 次查询 TopK=50：

  | 索引 | Recall@50 | 平均扫描向量数 |
  |---|---|---|
  | BruteForce | 1.0000 | 4000.0 |
  | IVF(nlist=64, nprobe=1) | 0.4302 | 56.8 |
  | IVF(nprobe=4) | 0.8696 | 228.6 |
  | IVF(nprobe=8) | 0.9617 | 480.0 |
  | IVF(nprobe=16) | 0.9925 | 1054.5 |
  | HNSW(M=8, ef=50) | 0.9687 | 1399.1 |
  | HNSW(M=16, ef=100) | 0.9978 | 2288.5 |

  扫描量与 $N\cdot nprobe/nlist$ 的理论值一致。脚本输出里明确注明：只有 4000 个物品、暴力检索是一次 NumPy 矩阵乘（C 实现）而 HNSW 图遍历在 Python 层逐跳进行，因此**墙上耗时反而更慢**，ANN 的真实收益要看「平均扫描向量数」这一列。
- **`graph_walk_retrieval.py`**（§4.3.4）：300 用户 / 1200 物品二分图、5100 条训练边 + 300 条留一测试边、6 个兴趣社区。三类方法同台对比（TopK=50）：

  | 方案 | HitRate@50 | 物品覆盖率 | 同社区占比 |
  |---|---|---|---|
  | 二跳路径 U2I2I | 0.1633 | 97.00% | 0.8034 |
  | PPR 图扩散 α=0.05 | 0.2100 | 26.00% | 0.3484 |
  | PPR α=0.15 | **0.2167** | 52.92% | 0.4859 |
  | PPR α=0.4 | 0.1967 | 76.33% | 0.6588 |
  | DeepWalk (p=q=1) | 0.1600 | 91.25% | 0.8565 |
  | Node2Vec (p=1, q=0.5) | 0.1867 | 89.50% | 0.8693 |
  | Node2Vec (p=4, q=1) | 0.1800 | 90.33% | 0.8783 |

  随机游走部分自己实现了 SGNS（负采样 5、窗口 3、维度 32、每节点 4 条长度 12 的游走，共 5892 条），末轮 loss 1.5793 / 1.6464 / 1.6933。α 越小高阶信息权重越大但分布趋近全局 PageRank、覆盖率骤降（26%）—— 正文「过度扩散丢失个性化」。另打印高阶路径示例：PPR Top5 候选均为用户从未直接交互的物品，并列出桥梁用户。
- **`retrieval_path_patterns.py`**（§4.3 关系路径视角）：300 用户 / 240 物品 / 40 作者，统一建 i2i(240 键) / u2u(300 键) / a2a(40 键) 三张表，演示 7 条路径如何只靠 **5 个服务接口**拼出来（i2i 表被 I2I·U2I2I·I2I2U 三条路径复用、u2u 被 2 条、倒排被 2 条）。用户侧评测 Top20：**U2I 0.2715 / I2I 0.5155 / U2U2I 0.6082 / U2I2I 0.6117**，平均扇出 1.0 / 50.0 / 99.8 / **500.0**（两跳扇出 = $|Trigger|\times K_1\times K_2$，是一跳的 10 倍）；作者侧（Creator Recommendation）**I2U2U 0.5917 / I2I2U 0.5792**，输出的是用户列表而非物品列表，只能用于内容冷启分发。两跳换来命中率但物品覆盖率反降（I2I 99.6% → U2I2I 85.8%），因为相似度乘积把分数堆到枢纽物品 —— 正文「多路并存 + 融合配额」的理由。U2I 一路用「哈希桶计数向量」占位，脚本中已注明不代表真实向量召回效果。

### § 4.4 召回结果融合

- **`retrieval_fusion_pipeline.py`**：6 路召回共 21800 条原始候选（热门 10000 / 双塔 5000 / ItemCF 3000 / Tag 2000 / 图 1000 / 实时 800），完整走通正文的五步。
  - **Step 1 去重**：21800 → **19955**（去重率 8.46%），命中源数分布 `{1:18161, 2:1744, 3:49, 4:1}`。对比了 reason 的两种设置口径：按优先级 vs 按内部得分，热门召回的 reason 计数差 **−719**、ItemCF **+258** —— 正文指出 reason 不带 AB 收益，故工业界取实现最简的优先级口径。
  - **Step 2 静态 Quota**：热门召回从 10000 压到 500（保留 **5.0%**），图召回 1000 全保留，总计 4849 —— 配额的作用正是防止大候选源独占。
  - **Step 3 四种截断口径**对比来源构成：按 Quota 轮转（热门 10.3% / 双塔 29.7% / 图 19.8%）、按归并得分全局（双塔涨到 **44.3%**、图跌到 7.3%）、按业务优先级、按动态流量分配 —— 截断口径直接改变候选集构成。
  - **Step 4 监控表**：逐路打印召回数 / 耗时 / 覆盖率 / 去重率 / CTR / 时长 / 超时率 / 空率 / **独占贡献** / **下游通过率**（实时行为召回 CTR 0.0683、下游通过 6.62% 最高；热门召回下游通过仅 0.58%），并输出两两 Jaccard 重合度矩阵（最高为热门↔ItemCF 0.056，说明各路互补）。
  - **Step 5 动态配额** $quota_k\propto base_k^{1-\beta}\cdot gain_k^{\beta}$（$\beta=0.6$，$gain$ = 独占贡献率 × 下游通过率）：热门 500→**188**（−62.4%）、实时 400→**658**（+64.5%）；但静态/动态两套配额的粗排 Top500 重合度高达 **98.0%**，提示配额调整的收益需要 AB 验证而非想当然。
  - **Step 6 故障注入**：双塔返回空 + 图召回超时后候选只剩 2326（低于兜底阈值 3000），分层兜底补 **674** 条 `backup_cat_hot` 回到 3000；监控表同步显示双塔空结果率 100%、图召回超时率 100%。

### § 4.5 过滤限流模块

- **`filter_positions_pipeline.py`**（§4.5.1 过滤模块的位置）：20 万内容 / 4000 作者 / 6 个地域，模拟 200 次请求（单请求召回 2000 → 曝光 20）。无效内容占 **17.94%**（offline 5.95% / audit_pending 5.01% / copyright 4.04% / deleted 2.93%）。三种配置的算力账单：

  | 配置 | 索引条目数 | 召回扫描次数 | 召回打分浪费率 | 兜底触发 |
  |---|---|---|---|---|
  | A 全不过滤 | 200,000 | 807,070 | 17.94% | 0 |
  | B 只在召回后过滤 | 200,000 | 807,070 | 17.94% | 200 |
  | C 三段式（正文） | **164,126** | 806,305 | **0.00%** | 200 |

  并按正文三段分类统计各过滤动作的触发量：地域限制 61,401（69.47%）、黑名单作者 17,918（20.27%）、已读 7,185（8.13%）、用户屏蔽 1,837（2.08%）均属**召回后过滤**（依赖用户态，无法离线做）；广告占比超限 44（0.05%）属**排序后过滤**（约束的是最终列表构成）；无效状态 35,874 条在**召回前**就不进索引。配置 C 有 200/200 次请求触发兜底 —— 过滤越干净越要配兜底阈值。
- **`read_history_bloom_filter.py`**（§4.5.1 已读过滤）：从零实现布隆过滤器（`bytearray` 位图 + 双哈希派生 $k$ 个独立哈希）。
  - **假阳率实测 vs 理论** $p=(1-e^{-kn/m})^k$，$n=20000$、5 万未插入元素探测：$m/n=4,k=3$ → 实测 0.14662 / 理论 0.14689（偏差 −0.2%）；$m/n=8,k=4$ → 0.02486 / 0.02397；$m/n=12,k=8$ → 0.00284 / 0.00314；$m/n=16,k=11$ → 0.00040 / 0.00046。所有配置「无假阴」均为 True —— 布隆的误差是**单边**的，方向正好匹配已读过滤「绝不漏拦」的需求。
  - **存储对比**（最优参数 $m=-n\ln p/(\ln 2)^2$、$k=(m/n)\ln 2$）：1000 万已读 @ $p=0.001$ 需 $m/n=14.38$ bit、$k=10$，布隆 **17.1 MB** vs int64 数组 76.3 MB vs Python set 572.2 MB（**4.5×** 压缩，且每元素比特数 $\approx 1.44\log_2(1/p)$ 与 $n$ 无关）。
  - **[MOCK] 在线链路**：每天 300 条消费、按天分桶保留 7 天，候选中刻意 70% 取自已读窗口。10 天内过滤前重复率从 25.64% 涨到 61.42%，过滤后**全部降到 0.00%**；累计 19136 条候选误伤 131 个（实测误伤率 **0.6846%**），第 9 天仍只有 7 个分桶、位图 3.1 KB —— TTL 分桶保证内存不随历史长度无限增长。
- **`filter_and_throttle_strategies.py`**（§4.5.2）：把正文表「过滤限流策略示例」的 **7 行规则**逐条实现（封面/画面合规随机 80%、标题/文本违规随机 80%、内容主题风险 100% 仅创作者可见、内容质量低下随机 75% + 总曝光 ≤ X、原创性存疑随机 60%、用户行为反馈按活跃度分层 30%/60%/90%、发布者信誉公域 100%/小流量 50%）。
  - 随机过滤用**稳定随机**（请求 ID + 物品 ID 哈希），600 次请求实测比例贴合正文目标：封面 **79.72%**（目标 80%）、标题 **81.29%**（目标 80%）。
  - **链路漏斗**（600 次请求、单请求 60 候选，共 36000）：规则过滤后 26799（74.44%）→ 疲劳度后 26504 → 作者频控后 23050 → 类目频控后 22937 → 限流后最终曝光 **9772（占召回 27.14%）**。各限流器拦截量：疲劳度 295、作者频控 3454、类目频控 113、滑动窗 8433、令牌桶 4732 —— 每个限流器都真实生效，便于读者改参数观察。
  - **硬过滤 vs 软限流**对照（标签池 88 个低质内容，其中 24 个是 [MOCK] 误标的优质内容，每条模拟 200 次请求）：硬过滤风险残留 0.0% 但误杀率 **100.0%**；软限流 75% → 残留 24.4% / 误杀 74.6%；软限流 50% → 50.7% / 49.3% —— 这就是正文「相比完全过滤，限流能在降低风险的同时减少误伤」的量化含义。
- **`penalty_system.py`**（§4.5.3）：按正文列举的六类风险信号（内容安全标签 $w=3.0$ 硬风险 / 用户举报 0.8 / 人工审核结论 4.0 硬风险 / 账号风险等级 1.2 / 行为异常检测 1.0 / AIGC 风险标签 1.5）汇聚 30 天内 **429 条事件、覆盖 276/400 个物品**，上游风控系统均为 [MOCK]。
  - 聚合与定级：$s=\sum w\cdot conf\cdot 0.5^{\Delta t/H}$（$H=7$ 天），$risk=1-e^{-s/\lambda}$（$\lambda=2.0$）。单条人工审核结论的 risk 从 0 天 **0.8504**（高风险）衰减到 30 天 **0.0928**；单条举报从 0.3023 衰减到 0.0183。
  - 四类动作分布：**高风险 7 个（1.75%）→ 硬过滤**（保留流量 0.00%）、**中风险 12 个（3.00%）→ 软限流**（平均保留流量 17.57%、排序分乘子 0.1757）、**低风险 171 个（42.75%）→ 风险打标**（保留 100%、平均乘子 0.8633）、无风险 210 个（52.50%）放行。
  - **定向屏蔽**：成人内容对未成年用户 u0/u3 判为「定向屏蔽」而对普通用户放行；地域限制内容只对受限地区 u1/u5 屏蔽 —— 同一条内容对不同受众给出不同动作。
  - **风险打标透传排序**：$final = score\cdot(1-\beta\cdot risk)$（$\beta=0.8$），Top40 中 13 个被打标、平均降分 **19.93%**，例如 item173 从第 4 名降到第 10 名却仍留在候选里 —— 把「安全 vs 体验」的权衡交给排序模型，而不是过滤层用硬阈值做不可解释的误伤。
  - **申诉与恢复**：一条 $conf=0.80$ 的 AIGC 标签 + 两条举报，0 天时 $s=2.29$、risk 0.6818（中风险、仅 10.13% 流量），第 7 天降为低风险（100% 流量），第 28 天 risk 0.0691、乘子 0.9448 —— 半衰期 $H$ 是治理强度的关键旋钮，否则一次误判会永久压死创作者。
- **`risk_control_evolution.py`**（§4.5.4 + §4.5.5）：2400 条合成内容（训练 1680 / 测试 720），三路模态特征各 24 维，统一语义空间 16 维。5 类风险按正文举例设置信号所在模态：涉黄（图像+文本）、涉暴（图像+文本）、涉政 OCR（文本）、Deepfake（图像）、**隐晦跨模态（仅语音 —— 新风险，阶段一刻意不给专用模型）**。
  - **阶段一 任务专用模型**（每类风险一个独立小模型）vs **阶段二 统一多模态建模**（三路模态两两 InfoNCE 对齐 1200 步，$\tau=0.1$，末步 loss 2.6260；每类风险**仅用 8 条标注样本**建原型）：

    | 风险类型 | 专用 AUC | 专用 Recall@FPR5% | 统一 AUC | 统一 Recall@FPR5% |
    |---|---|---|---|---|
    | 涉黄 | 0.9947 | 95.45% | 0.9786 | 88.64% |
    | 涉暴 | 0.9939 | 97.22% | 0.9693 | 80.56% |
    | 涉政(OCR) | 0.9919 | 95.83% | 0.9018 | — |
    | Deepfake | 0.9990 | 100.00% | 0.9480 | 70.00% |
    | **隐晦跨模态** | **0.5214** | **3.33%** | **0.9446** | **60.00%** |

    隐晦跨模态一行的「专用模型」列是借用现有 4 个模型中表现最好的那个（画面/文本都看不到语音里的风险），AUC 0.5214 ≈ 随机 —— 精确复现正文「出现新的风险类型时需要重新构建数据集并训练新的模型」。统一模型只靠自监督对齐、不看风险标签，8-shot 原型即可把该风险的 AUC 拉到 0.9446。
  - **维护成本对照**：阶段一 4 个模型、主干参数 100、新增一类风险的边际成本 =「新建数据集 + 训练第 N+1 个模型」；阶段二 1 个模型、参数 1200、边际成本 =「仅补 8 条样本做原型/Prompt」。
  - **审核链路级联**（单位成本：规则 1 / 专用小模型 10 / 统一大模型 200；大模型阈值 = 训练集负样本 97 分位，小模型粗筛阈值 = 60 分位，成本以「全量过大模型」为 1.00）：

    | 配置 | 精确率 | 召回率 | 隐晦跨模态召回 | 相对成本 | 大模型调用占比 |
    |---|---|---|---|---|---|
    | A 仅规则（关键词） | 82.89% | 44.68% | 16.67% | 0.01 | 0.00% |
    | B 规则→专用小模型 | 79.41% | 76.60% | 23.33% | 0.05 | 0.00% |
    | C 全量过大模型 | 77.01% | 47.52% | 30.00% | 1.00 | 100.00% |
    | D 规则→大模型 | 73.17% | 63.83% | 33.33% | 0.90 | 89.44% |
    | E 规则→小模型→大模型 | 81.48% | 62.41% | 26.67% | 0.43 | 38.33% |

    （测试集 720 条中违规 141 条，其中隐晦跨模态 30 条。）配置 E 用 0.43 的成本拿到接近 D 的召回；但 B 的隐晦跨模态召回只有 23.33%，印证正文 §4.5.5 的挑战：**级联若用旧的小模型做第一道粗筛，新型跨模态风险会在粗筛阶段就被漏掉**，所以阈值即「安全 vs 误伤」旋钮，Recall@FPR 这类指标才是对齐业务的口径。

---

## 实现约定

- **自包含**：每个 `.py` 独立运行，文件之间不互相 import；文件头 docstring 用中文写明对应正文节次、本实现做了什么、关键公式。
- **固定种子**：所有 `__main__` 块固定 `random.seed(42)`，用到 NumPy / PyTorch 的脚本同时 `np.random.seed(42)` / `torch.manual_seed(42)`，并设 `torch.set_num_threads(1)` 保证单核可复现且更快。
- **不依赖外部服务**：ANN 索引（IVF / HNSW）、布隆过滤器、滑动窗与令牌桶限流器、Skip-gram 负采样均从零实现，不装第三方检索/限流库；Redis、用户画像、倒排/正排/向量索引、RPC 超时、上游风控系统等全部是进程内 [MOCK] 组件并在 docstring 中标注。
- **输出优先给数值表**：统一中文表头 + 等宽对齐，每个脚本末尾打印「结论（正文 §X.Y 的工程含义）」，而不是刷 loss 日志。
- **以已发布 PDF 为准**：实现对齐第 4 章发布 PDF 的对应小节，包括 §4.5.2 策略表的 7 行规则与各自的随机过滤比例、§4.5.3 的四类处罚动作、§4.5.4 的两阶段风控叙事。
- **玩具规模的诚实注记**：凡是合成数据规模会让结论失真的地方，脚本输出里直接写明，不做夸大。已标注的三处：(1) `embedding_retrieval.py` 只有 4000 个物品、暴力检索是 NumPy 矩阵乘，ANN 的墙上耗时反而更慢，真实收益看「平均扫描向量数」；(2) `retrieval_path_patterns.py` 与 `collaborative_filtering_retrieval.py` 的 U2I 用「哈希桶计数向量」占位，不代表真实双塔/多兴趣召回效果（完整模型见第 10 章）；(3) `content_and_rule_retrieval.py` 的基尼系数在 ratio=0.1/0.3 处非单调，属合成数据噪声。
- **本章定位**：召回与过滤的**链路与策略**，模型细节交给第 10 章（召回模型）与第 11 章（粗排模型）。`collaborative_filtering_retrieval.py` 输出末尾给出了交叉引用：相似度算法的完整推导与变体见 `ch10 召回模型/code/ch01_collaborative_filtering/`。
