Metadata-Version: 2.4
Name: alben
Version: 0.1.0
Summary: 复刻 TypeSafe System One (Jev) 的开源实现：运行时任意 criteria + 校准概率
Author: AlbenW
License: Apache-2.0
Keywords: system-one,decision-model,calibration,typesafe,jev
Classifier: Development Status :: 3 - Alpha
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: transformers>=4.48
Requires-Dist: numpy<3,>=2.0
Requires-Dist: safetensors>=0.4
Requires-Dist: huggingface_hub>=0.23
Requires-Dist: tokenizers>=0.20
Provides-Extra: serve
Requires-Dist: torch>=2.0; extra == "serve"
Requires-Dist: fastapi>=0.115; extra == "serve"
Requires-Dist: uvicorn>=0.27; extra == "serve"
Requires-Dist: pydantic<3,>=2.5; extra == "serve"
Provides-Extra: cli
Requires-Dist: torch>=2.0; extra == "cli"
Requires-Dist: PyYAML>=6.0; extra == "cli"
Provides-Extra: train
Requires-Dist: torch>=2.0; extra == "train"
Requires-Dist: PyYAML>=6.0; extra == "train"
Provides-Extra: onnx
Requires-Dist: onnx>=1.16; extra == "onnx"
Requires-Dist: onnxruntime>=1.18; extra == "onnx"
Requires-Dist: numpy<3,>=2.0; extra == "onnx"
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == "mcp"
Requires-Dist: torch>=2.0; extra == "mcp"
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.3; extra == "langchain"
Requires-Dist: torch>=2.0; extra == "langchain"
Provides-Extra: langgraph
Requires-Dist: langchain-core>=0.3; extra == "langgraph"
Requires-Dist: langgraph>=0.2; extra == "langgraph"
Requires-Dist: torch>=2.0; extra == "langgraph"
Provides-Extra: dev
Requires-Dist: torch>=2.0; extra == "dev"
Requires-Dist: fastapi>=0.115; extra == "dev"
Requires-Dist: uvicorn>=0.27; extra == "dev"
Requires-Dist: pydantic<3,>=2.5; extra == "dev"
Requires-Dist: httpx<0.29,>=0.28; extra == "dev"
Requires-Dist: PyYAML>=6.0; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"

# Alben

> **一个以作者名字命名的仓库，目标是复刻 TypeSafe System One（Jev）的全部功能。**

---

## 先读这一段（诚实声明）

**默认产物已经是「通用产物」**（`artifacts/alben-scorer.pt`，1,882 集合 / 56,160 条）。

### 出厂验收（第 39 轮，走真实服务路径）

| 组 | 实测 | 出厂线 | 结果 |
|---|---|---|---|
| MASSIVE intent en-US（60 意图，随机 0.0303） | **0.6975** | 0.60 | PASS |
| MASSIVE intent 其他 9 语言 | **0.3883** | 0.30 | PASS |
| BANKING77（77 类，随机 0.05） | **0.8817** | 0.70 | PASS |
| SST-5（5 级有序，随机 0.20） | **0.3133** | 0.25 | PASS |
| 四条干净尺子（平均 / 最低） | **0.6465 / 0.6178** | 0.60 / 0.58 | PASS |
| 官方 Python SDK 5/5 · 官方 JS SDK 30/30 | **全通过** | 全通过 | PASS |

**测试：常规 197 passed / 三 E2E 全开 204 passed，零跳过。**

> **必须一起读的限定语**：
> **「通用」现在的准确含义是【见过同类标签、换文本 / 换语言】，**
> **不是【换全新维度】—— 后者需示例微调（`POST /admin/teach`，实测 0.49 → 0.99）。**
> 完整边界见 [通用化结论.md](https://github.com/XiaoBinGan/Alben/blob/main/通用化结论.md) 与 [能力边界.md](https://github.com/XiaoBinGan/Alben/blob/main/能力边界.md)。

### 能力边界（一句话版）

| 场景 | 能否用 | 实测 |
|---|---|---|
| 契约层：任意 criteria / 255 选项 / 官方 SDK | ✅ 完整 | F1–F24 24/24 |
| 同类维度、新文本 | ✅ 可用 | 干净尺子 0.62–0.71 |
| 同类维度、新语言 | ✅ 可用 | 非英语 0.0744 → 0.3883 |
| **换全新维度** | ⚠️ 几乎不行 | 跨族 0.3150 vs 随机 0.3211 |
| **换全新维度 + 几十条示例** | ✅ 能教会 | `/admin/teach`：0.4917 → 0.9917 |

---

**✅ 已有**：**JS SDK（`sdk/js/`，零依赖 fetch）** ·
**官方 Python SDK 原样可用（F21，5/5）** · **官方 JS SDK 原样可用（F22，30/30）**。

**⬜ 仍没有**（**不在 F1–F24 范围内**，是官方也有的服务端能力，非契约复刻项）：
限流（429）· 计费 · 多 key 管理 · **`sdk/python/` 自研 Python 客户端**
（A4 设计里有，未实现 —— 但 F21「官方 Python SDK 原样可用」已完成，
**这意味着不需要自研客户端也能用**）。

已完成的研究是一套**可复现的实验**和**基于实测的事实基线**。这些实验得出了一些**与直觉相反的结论**，
其中**四条**推翻了我们自己在更早阶段的判断 —— 详见「十轮实验」。

**最重要的一条**：

> **在【冻结的小 encoder + 监督头】架构下，无法达成 Jev 的核心能力「运行时任意 criteria」。**
> **但【训练 encoder】可以 —— 已实测有效：未见 criteria 准确率 0.2853 → 0.8000（随机基线 0.2920，即 2.74×）。**
> **瓶颈已定位：不是架构形状，是【训练配置】与【criteria 集合的广度】。**
>
> **★★ 上面这段是早期结论，第 30–32 轮把它精确化了（请以下面为准）：**
> 1. 早期那个 0.80 是在**同一个维度族**的留出上测的 —— 它是真的，但它**不代表能处理新维度**。
> 2. 换到**没训过的维度族**：0.3150 vs 随机 0.3211 → **等于随机**（干净跨族 4183 条）。
> 3. 第 30 轮：合成 150 个维度训、留出**没见过组合** → 0.5936（**冻编码器就已经 0.5036**）。
> 4. 第 31 轮：留出**整根全新语义轴** → 0-shot **0.4961**（冻编码器 0.4789）；
>    1-shot 0.6018；**4-shot 0.8867**。
> 5. 第 32 轮：训练轴 2→4→7 根，新轴 0-shot 0.4898/0.4826/0.4961 —— **加轴不涨**。
>
> **→ 结论：Alben 学的是【维度族内的语义匹配】，不是【任意新维度的推理】。
>   新维度的正确用法是【少量示例微调】（`teach_alben.py`，实测 0.49 → 0.99），
>   不是指望它自己猜出来。**

---

## 最终目标

**原话（作者）**：

> 「我想做个和自己名字命名的仓库，需要复刻全部的 jev 的功能，仓库就叫 Alben」

**「全部功能」的操作性定义 = F1–F24 清单**（见 `design/总设计-Alben.md` §1.2）。

### F1–F24 状态（诚实版）

| 状态 | 数量 |
|---|---|
| ✅ **已复刻 / 已裁定** | **24 / 24** |
| ⚠️ **未完成** | **0** |

> **★ F22 是最后一项，第 23 轮完成** —— 用【官方】`@typesafe-ai/sdk@0.6.0` 原样打 Alben，
> **30/30 断言通过**，并抓出 3 个真实契约 bug（见下文「F22 完成」一节）。

| # | 功能 | 状态 |
|---|---|---|
| **F7** | **运行时任意 criteria** | ✅ **契约已实现**（255 选项、任意判据描述都接受并作答）。⚠️ **能力限定**：训练过的维度族内 模型−多数类 +0.20~+0.34；**没训过的维度族 ≈ 随机**（第 30–32 轮）。 |
| **F8** | 255 选项不截断 | ✅ **已实现**：测试断言 255 个选项全部被打分 |
| **F12** | Score 置信度口径 | ✅ **已实现**：用归一化峰度（与 Choice 同口径），响应含 legend + confidence |
| **F22** | JS SDK | ✅ **已完成**（官方 SDK 30/30，见下文） |

**「已复刻」需要澄清**：现有 `legacy/`（原 `jev/`）复刻的是官方 API 的**形状**，不是**能力**。
它是固定问题分类器（遇到没见过的 question → 422），而官方核心能力是运行时任意 `criteria`。

---

## 实现（第 10 轮）

**`alben` 包已实现并通过端到端验收。** 完整状态见 `实现状态.md`。

| | legacy（原 `jev/`） | **alben** |
|---|---|---|
| 复刻的是 | API 的**形状** | API 的**能力** |
| 未见的 question | **422** | **正常作答** |
| 选项来源 | 训练时固定集合 | **运行时请求** |

**端到端验收（走 `alben.Runtime` + `alben.server` 的真实服务路径）：**

> **⚠️ 下面这一栏是第 10 轮的旧成绩，保留作历史记录。**
> 它当时用的是**有重叠**的留出集（tpl 9.9% / free 11.1% / diverse 9.8% / real 36.8%），
> 第 28 轮修掉重叠与占位问题后，**干净成绩见本页开头**。

```
未见 criteria 准确率 = 0.8055   （旧，含 10~37% 文本重叠）
随机基线             = 0.2925
相对随机             = 2.75x
```

与实验脚本的 0.8020 一致 → **包没有引入额外损失**（这条结论仍然成立）。

### 发现并修复了一个真实缺陷：置信度未标定

端到端准确率 0.8055，但平均置信度 **0.8900** → 过度自信。**ECE(10箱) = 0.1100**。

用**温度缩放**修复（在留出集的一半上拟合，另一半评估）：

| 指标 | 修复前 | 修复后 |
|---|---|---|
| 准确率 | 0.8055 | **0.8055**（不变 ✓） |
| 平均置信 | 0.8900 | 0.7576 |
| **ECE (10 箱)** | **0.1100** | **0.0604** |
| Brier | 0.1458 | 0.1243 |

> **★ 温度缩放只改置信度，不改 argmax —— 准确率不变这一点在脚本里显式断言。**
> **修复后置信度是单调可信的信号：最低箱实际 0.40 → 最高箱 0.958。**

### 契约实现：实测再次胜过文档

读官方 live 文档（`docs.typesafe.ai/api.md`）后发现 **4 处文档与实测冲突**：

| # | 文档说 | 实测 | 处理 |
|---|---|---|---|
| 1 | 缺 key → **401** | **403** | 按实测 |
| 2 | Score 无 confidence | **有** confidence + legend | 补上 |
| 3 | Noul 是 yes/no | `noul` 是 **0..1 浮点数** | 返回浮点，不是布尔 |
| 4 | — | Score criteria **≥2 且 ≤10** 级 | 加校验 |

另修正 3 处早期实现错误：概率精度 3→**2** 位；`/v1/models` 用 `data`→**`models`**；Noul 布尔→**浮点**。

**`tests/` 50 个测试全通过**，每个对应一条实测契约事实或一个已修的真实缺陷。

---

### ★★★ 第 13 轮：换机 + 三个基础设施缺陷（其中一个是真科学发现）

第 13 轮不是实验轮，而是「换一台电脑继续」时暴露的三个缺陷。
**其中第三个不是环境问题，是真正影响所有结论的尺子问题。**

#### 缺陷 ① 15 份重复的 API key 读取代码，且都在模块级

新机上 `pytest` 挂 6 个测试：`AttributeError: 'NoneType' object has no attribute 'group'`。

**根因**：15 个脚本各自写了同一行**模块级**代码读凭证。新机凭证里没有
`TYPESAFE_API_KEY` → `re.search` 返回 `None` → **一 import 就炸**。

**这是两个问题叠在一起**：模块级执行（而训练/评测/服务**根本不需要 key**）
+ 15 份拷贝（方法学陷阱 ⑤「一个问题两套策略，本身就是缺陷源」）。

**修复**：`corpus/apikey.py` 单点定义 + `LazyHeaders`（只在真发请求时读凭证）。
调用处一个字不改：`HDR = lazy_headers()`。自动迁移 15 个文件，**全部成功**。

#### 缺陷 ② 仓库里没有任何依赖清单

`git ls-files` 确认：**没有 `requirements.txt` / `pyproject.toml`**。
换机只能照版本表手抄，而**手抄必漏「torch 必须带 index-url」**。

**修复**：`requirements.txt` + `requirements-torch.txt`（torch 单独）。

> **★ 为什么 torch 必须单独**：PyPI 上的 torch 默认是 **CPU 版**。
> 当普通依赖写，pip 拉 CPU 版 → `cuda.is_available()` 为 `False` →
> **训练脚本自动回退 CPU，不报错，只是慢 8 倍**。这是「静默降级」。

#### ★★★ 缺陷 ③ 四个数据集之间互相泄漏 9~31%

第 12 轮已经做过「训练 vs 各自留出」的去泄漏，**但四个数据集彼此之间没有做**。
实测把同一批数据按同族切分后：

| 尺子的留出集 | 与哪个训练侧重叠 | 重叠率 |
|---|---|---|
| `tpl` 留出 | `diverse` 训练 | **29.6%** |
| `free` 留出 | `diverse` 训练 | **31.1%** |
| `real` 留出 | `diverse` 训练 | **29.2%** |
| 其余组合 | — | 9~11% |

**后果**：在「A/B/C/D 逐步加数据」的消融里，
**配置 D 训练时见过配置 A/B/C 的留出文本** ——
「加数据带来的提升」里混进了「泄漏带来的虚高」。

**修复**：`corpus/samefam_joint.py` 做**联合去泄漏**：
汇总所有留出集的文本作为全局禁入集，从所有训练侧删掉，并断言
**任意留出 × 任意训练 = 0 重叠**。删了 tpl 671 / free 645 / diverse 1279 / real 668 条。

> **★ 为什么必须「联合」**：各自去泄漏只能保证「留出集不在自己的训练集里」，
> 但 A 配置的留出集**仍然是 B 配置训练数据的子集** —— 配置之间的比较仍不公平。
>
> **★ 这是本轮唯一一次「先停实验、再修尺子」**：第一次跑消融时我写了个泄漏检查，
> 结果 31.1% —— **立刻 kill 掉正在跑的消融**（已跑完配置 A），修完重跑。
> **一个正在产出的实验，如果尺子是歪的，跑完也是废的。**

#### ★★★ 第 13 轮结果：每加一个分布，就在那个分布上大幅提升

**尺子**：同族留出 + 联合去泄漏（任意留出 × 任意训练 = 0 重叠）。

| 配置 | n | tpl | free | diverse | real |
|---|---|---|---|---|---|
| A) 只 tpl | 4,745 | 0.7566 | 0.3873 | 0.3641 | 0.4076 |
| B) tpl+free | 9,356 | 0.7558 | **0.6835** | 0.3944 | 0.4472 |
| C) tpl+free+diverse | 24,291 | 0.7515 | 0.6678 | **0.7787** | 0.4625 |
| **D) 全部** | 29,383 | **0.7573** | **0.6861** | **0.7846** | **0.8257** |
| 随机基线 | — | 0.3625 | 0.3423 | 0.3640 | 0.3657 |

**配对自助法**：A→B 在 free 上 **+0.2962 ★**；B→C 在 diverse 上 **+0.3843 ★**；
C→D 在 real 上 **+0.3632 ★**。**而每次对其他分布都不显著（−0.016 ~ +0.018）。**

> **每加一个分布的数据，就在那个分布上大幅提升（+0.30 ~ +0.38），对其他分布几乎无代价。**
> **模型能同时装下多个分布。**

#### ★★★ 但必须同时给出【诚实基线】

**「相对随机 X 倍」是被高估的对照。** 真正的诚实对照是**族先验基线**
（只用该配置训练侧的族标签分布）。

| 配置 | 尺子 | 随机 | 多数类(作弊) | **族先验(诚实)** | 模型 | 模型−随机 | **模型−先验** |
|---|---|---|---|---|---|---|---|
| A | tpl | 0.3625 | 0.7180 | 0.6746 | 0.7566 | +0.3941 | **+0.0820** |
| A | diverse | 0.3638 | 0.7916 | 0.3638 | 0.3641 | +0.0003 | **+0.0003** |
| B | free | 0.3425 | 0.6763 | 0.6245 | 0.6835 | +0.3410 | **+0.0590** |
| C | diverse | 0.3638 | 0.7916 | 0.6332 | 0.7787 | +0.4148 | **+0.1455** |
| D | real | 0.3657 | 0.7563 | 0.7417 | 0.8257 | +0.4600 | **+0.0840** |

> **★★★ 诚实的结论：模型真正「从文本里读出来」的增量是 +0.04 ~ +0.15，
> 不是「2.4 倍」。**
>
> 大部分表观准确率来自**标签偏斜**（族先验 0.62~0.74），
> 而「相对随机」把这个偏斜算成了模型的功劳。
>
> **最诚实的一行**：配置 A 在 `diverse` 尺子上，模型 0.3641 / 先验 0.3638 → **+0.0003**。
> **那是模型面对没见过的族时的真实水平 —— 和随机猜没有区别。**
> 这与第 12 轮的发现一致：**模型不做跨族迁移。**

> **本仓库历史上所有「相对随机 X 倍」都要按这个口径重新理解。**
> 以后报成绩必须两个数一起给。

#### ★★★ 第 14 轮：标签平衡的尺子 —— 上面那个悲观解读被推翻了

第 13 轮的「诚实基线」还留着一个漏洞：**族先验基线依赖「模型能估准先验」这个假设**。
最干净的办法是**让先验基线 = 随机**：对留出集做**标签平衡下采样**，
把每个 criteria 集合的每个标签压到与最少标签持平。

**平衡效果**：多数类从 **0.68~0.79** 压到 **0.39~0.43**。

**平衡尺子上的结果：**

| 配置 | n | tpl | free | diverse | real |
|---|---|---|---|---|---|
| A) 只 tpl | 4,745 | **0.5988** | 0.3692 | 0.3966 | 0.4174 |
| B) tpl+free | 9,356 | 0.6047 | **0.6141** | 0.4147 | 0.4427 |
| C) +diverse | 24,291 | 0.6145 | 0.5733 | **0.5991** | 0.4862 |
| **D) 全部** | 29,383 | **0.6184** | **0.5955** | **0.6215** | **0.6583** |
| 随机 | — | 0.3593 | 0.3342 | 0.3621 | 0.3646 |
| 族先验 | — | 0.4167 | 0.3860 | 0.4188 | 0.4010 |

**配对自助法**：B→C diverse **+0.1844 ★** · C→D real **+0.1720 ★** · A→B free **+0.2449 ★**

#### ★★★ 两把尺子，两个结论 —— 都对，但回答不同问题

| 尺子 | 模型(tpl, 配置A) | 诚实基线 | **模型−基线** |
|---|---|---|---|
| **自然**（第 13 轮） | 0.7566 | 族先验 0.6746 | **+0.0820** |
| **平衡**（第 14 轮） | 0.5988 | 族先验 0.4167 | **+0.1822** |

> **第 13 轮那句「模型真正读出来的只有 +0.04~+0.15」是被自然分布【压低】的。**
> 自然分布下族先验本身就有 0.67，先验已经很强，**文本没多少可加的**。
> **平衡之后先验被压到 0.42，模型仍有 0.60 —— 增量 +0.18。**
>
> - 自然尺子问：`在偏斜的真实分布上，模型比"猜先验"强多少？` → **+0.08**
> - 平衡尺子问：`把偏斜这条捷径堵掉后，模型还剩多少能力？` → **+0.18**
>
> **边际增量小 ≠ 能力弱。要分清「基线太强」与「模型太弱」。**

#### 平衡尺子还修正了「跨族迁移」的结论

| 配置 | 未训练的尺子 | 模型 | 随机 | 增量 |
|---|---|---|---|---|
| A | free | 0.3692 | 0.3342 | **+0.0350** |
| A | diverse | 0.3966 | 0.3621 | **+0.0345** |
| A | real | 0.4174 | 0.3646 | **+0.0528** |

> **第 12 轮「模型不做跨族迁移」应修正为：跨族有【微弱】正向迁移（+0.03 ~ +0.08），
> 远低于同族（+0.18 ~ +0.26），但不是零。**
> 第 13 轮看到的「+0.0003」是自然分布下先验恰好等于随机的巧合。

---

### ★★★ 第 15 轮：编码器容量 —— 一个【否定】结果

第 14 轮证明模型确实在读文本，所以下一步是**把能力做强**。
最可疑的瓶颈是编码器：当前 `bge-small-zh-v1.5` 只有 **4 层 / 512 维 / 24M 参数**。

**新机 HuggingFace 可达**（原机不可达），所以本轮下载了
`bge-base-zh-v1.5`（**12 层 / 768 维 / 102.5M 参数**，4.25 倍大）。

| 编码器 | 参数量 | tpl | free | diverse | real | 耗时 |
|---|---|---|---|---|---|---|
| small | 24.1M | **0.6184** | **0.5955** | **0.6215** | 0.6583 | 1105s |
| base | **102.5M** | 0.6086 | 0.5622 | 0.6002 | **0.6766** | 3123s |

**配对自助法**：四个尺子 **全都不显著**（Δ = −0.033 ~ +0.018，CI 全部跨 0）。

> **★★★ 结论：编码器大 4.25 倍，没有任何一个尺子显著改善，三个方向甚至是负的。**
> **瓶颈不在编码器容量。**
>
> **与第 8 轮一致**（当时测网络形状 S2/S3/S4，差异 ≤0.02）——
> **表征容量的方向已被两次独立实验排除。**

**这个否定结果为什么可信**：train loss 已趋近 0（0.0004/0.0002）不是欠训练；
base 耗时是 small 的 2.8 倍确认换了模型；四个尺子方向不一致；
**CI 宽度 ±0.03~0.05，而第 14 轮的真实增量是 +0.18~+0.26 —— 有真实提升一定测得出来。**

> **陷阱 ⑯（新增）：换更大模型之前，先确认瓶颈在容量。**
> 「更大模型更好」是默认假设，需要验证。验证成本（409 MB + 52 分钟）远低于盲目投入。

---

### ★★★ 第 16 轮：硬负例 —— 一个我自己造的 bug + 一次欠训练误判

#### 动机

第 15 轮排除了编码器容量。剩下最可疑的是**训练目标太容易**：
当前 InfoNCE 的负例只有**同一集合内的其他选项**（平均 2.9 个），
模型只要「认出这是哪个集合、挑最常见的」就能把 loss 压到 0。

#### 第一次结果：全面下降 —— 但结论是错的

| 负例池 | 池大小 | tpl | free | diverse | real | train loss |
|---|---|---|---|---|---|---|
| 组内 | 2.9 | **0.6184** | **0.5955** | **0.6215** | **0.6583** | 0.0004 |
| 族内池 | 19.0 | 0.5636 | 0.5102 | 0.5618 | 0.6078 | **0.4799** |

**「模型连训练集都拟合不了」—— 这个信号必须先怀疑实验本身。**

#### ★ 我先做了一个错误的推断，被自己的验证脚本否定

我推断：族内池里混进了**与正例文本完全相同**的负例（任务逻辑矛盾）。
依据是 `_diag_hardneg.py` 的数字：正例与族内负例撞车 **17.0%**、纯描述撞车 **79.4%**。

**但这是推断。** 我写了 `_diag_pools.py` **直接数实际候选列表**：

| 检查 | 结果 |
|---|---|
| `group` / `family` / `global` 池的重复条数 | **0 / 0 / 0** |
| **正例出现在候选列表负例部分的集合** | **0 / 849** |

> **推断错了。** `build_pools` 本来就有 `extra = [e for e in extra if e not in set(opts)]`，
> 已把撞车负例全部剔除。那 17% / 79% 是**另一个口径**（没施加这个过滤）的数字。
>
> **★ 陷阱 ⑰：一个看起来能解释现象的数字，必须确认它描述的是【实际生效的那份数据】。**
> 差点给出一个完全错误的原因。

#### 真正的原因：欠训练

| epoch | train loss |
|---|---|
| 16 | 2.3073 |
| 32 | **0.9282** |
| 48 | **0.0455** |
| 64 | 0.0158 |

> **32 epoch 时 loss 还有 0.93，48 epoch 就掉到 0.05。**
> **第一版在 32 epoch 停下看到 0.48 就下结论「硬负例更差」—— 那是欠训练。**
> **第 16 轮第一版结论作废（VOID）。**

> **★ 这是第 7 轮教训的复发**：第 7 轮（数据量）与第 16 轮（硬负例）
> **在同一个坑里各摔了一次**，形态完全相同：**loss 没降下去 → 误判为「方法无效」**。
>
> **防复发措施**：任何训练实验，**报告准确率之前先报告 loss 收敛曲线**；
> 若 loss 仍在下降，准确率数字**一律不得用于比较**。

**修复**：epoch 数按候选池大小缩放（2.9→32ep / 19→96ep / 38→128ep），
保证每个配置都训到收敛再比。

---

## ★★★★ 独立复核（第 14–17 轮）—— 三个发现

> **一个独立 teammate（verifier）换【另一条实现路径】重算了第 14–17 轮的核心数字。**
> 它不调用、不复制 round14-17 的任何实现：自己写切分、自己写平衡（确定性排序而非随机洗牌）、
> 自己写训练循环、自己写配对自助法，并加了 **Holm–Bonferroni 多重比较校正**。
> 报告：`corpus/_verify_report.md`。

### 发现 ①：第 16 轮的因果结论【不成立】

verifier 复刻了负例池的构造（只构造、不训练），逐集合审计候选列表：

| 模式 | 池大小 | 额外候选带**别的问题前缀** | 描述与本集合某选项**逐字相同** | 碰撞的是**该行正确答案** |
|---|---|---|---|---|
| group | 2.9 | — | 0 | 0 |
| **family** | 19.0 | **100%** | **62.4%** | **36.3%** |
| **global** | 38.1 | **100%** | 1.1% | 0.6% |

**两个构造伪影**：

1. **前缀线索** —— `option_texts` 把「问题 + 选项描述」拼成一条文本。family/global 池把
   兄弟集合的**整串**搬过来，所以外来候选带着**别的问题前缀**；而评测时每个候选都带**本行自己的问题**。
   **训练时「看到陌生问题前缀」是一个免费的、评测时不可用的判别线索。**
2. **描述级碰撞** —— 去重按**整串**做，「别的问题 + 相同描述」能存活。
   family 池里 **36.3% 的集合中，一个与正确答案逐字相同的文本被当作负例** ——
   **loss 在主动排斥正确答案。**

> **family 池相对 group 池同时改变了三件事：(a) 负例数量、(b) 前缀线索、(c) 正确答案被排斥。**
> **三者互相混淆，−0.11~−0.18 的下降【不能】归因于「跨判据负例有害」。**
>
> **所以本文档上面那句「负例必须来自同一判据空间」降级为：
> 「未被证实，也无法据此证伪」。**

> **★ 一个讽刺的细节**：仓库里有 `round16_ablation.py`（前缀统一 + 描述级去重），
> **它的文档串里就提到了 36.3% 这个数** —— 当时的我已经意识到了问题。
> **但它从未产出 JSON，也从未被运行。「写了修正版但没跑」等于没写。**

### 发现 ②：第 14 轮「比随机高 +0.26」被【系统性高估 0.015~0.018】

**口径混用**：基线的 `random` 是**按集合取 1/n 再宏平均**，而模型准确率是**按行平均（micro）**。

| 尺子 | 随机(宏) | 随机(按行) | 模型 | 模型−随机(宏) | **模型−随机(按行)** |
|---|---|---|---|---|---|
| tpl | 0.3593 | 0.3768 | 0.6184 | +0.2591 | **+0.2416** |
| diverse | 0.3621 | 0.3775 | 0.6215 | +0.2595 | **+0.2440** |

> **tpl / diverse 的「+0.26」应读作「+0.24」。**
> **陷阱 ⑳：宏平均与微平均不能混用 —— 基线按集合算、模型按行算，就是不可比的。**

### 发现 ③：四个轮的「对照」是【同一份预测数组】

`round14['D']`、`round15['small']`、`round16['group']`、`round17['自然']`
**逐元素完全相同**。这是合理复用（同一配置只训一次），但
**4 个 JSON 看起来像独立实验，引用时必须说明是同一次运行**；
且**第 15/16/17 轮都没有重跑 baseline**，第 17 轮的「自然」基线**无法反映随机性**。

### 复核确认成立的部分

| 结论 | 判定 |
|---|---|
| 第 14 轮随机基线 / 诚实族先验 | **精确复现**（差 0.0000） |
| 第 15 轮 base vs small 四尺子全不显著 | **复现**，Holm 校正后仍 4/4 |
| 第 16 轮数字（8/8 显著） | **复现**（但解释不成立） |
| 第 17 轮 1/8 显著 | **复现**；Holm 校正后 **0/8** |
| `build_joint`「任意留出 × 任意训练 = 0」 | **自己逐对验证成立**（16 对全 0，双通道） |

**泄漏审计（四通道 × 16 对）**：text 原文 0 / sha 0 / 集合名 0；`text_sha` 跨数据集一致；
命名空间化后跨数据集同名 0；留出族 ⊆ 训练族成立。

**另一个影响解读的观察**：**配置之间训练数据不独立** ——
tpl×diverse 有 1,504 条文本同时出现在两个数据集的训练侧（同一文本挂不同 criteria_name）。
**A/B/C/D 做配置间比较时应记住这一点。**

> **★ 一次独立复核的价值就在这里**：它没有推翻任何数字，但
> **推翻了一个因果结论**、**发现了一个口径高估**、**澄清了「四个实验其实是同一次运行」**。
> **这三件事，原实现者（我）都看不出来 —— 因为我在自己的框架里。**

---

## ★★★★★ 两个重大发现（第 18 轮）

### 发现 A：「族」这个切分单位本身是坏的

nearsyn 做「同维度近义等级硬负例」（第 16 轮建议的方向），**四个尺子都没有正收益**。
追问原因，发现四个数据集的族内结构完全不同（Lead 独立验证）：

| 数据集 | 集合 | 族数 | 同标签对 | **描述逐字相同** | 比例 |
|---|---|---|---|---|---|
| tpl | 144 | 18 | 1,246 | 119 | **9.6%** |
| free | 137 | 39 | 611 | 67 | **11.0%** |
| diverse | 424 | 53 | 13 | 0 | **0.0%** |
| **real** | 144 | 18 | 1,090 | **1,090** | **100.0%** |

```
real/reversibility_00:  easy -> "Can be undone with a single click..."
real/reversibility_01:  easy -> "Can be undone with a single click..."   ← 逐字相同
tpl/certainty_00:  likely -> "The certainty is likely."
tpl/certainty_01:  likely -> "Characterized by likely certainty."        ← 不同措辞
```

> **「族」在两种意义上都不是「同一判据空间」：**
> - **real**：同一套判据的**重复** → 扩池只产生**伪负例**
> - **tpl/free/diverse**：同名但**不同判据** → 扩池 = **跨判据干扰**
>
> **所以第 16 轮的正确表述应当是：**
> **「在 Alben 的现有语料里，'族'不构成'同一判据空间'，
> 因此无法用它构造同判据空间的硬负例。」**
> —— 这是**数据构造的结论**，不是**模型性质的结论**。

> **★ 陷阱 ㉓：切分单位本身可能是坏的 —— 它需要被验证，不能假定。**
> `samefam.py` 把 `reversibility_00` 与 `_01` 归为同族，**名字上成立、语义上不成立**。
> 而基于"族"的切分是**第 12–18 轮全部实验的基础**。
> **验证成本只有一次 20 行的脚本；六轮实验之后才发现。**

### 发现 B：生产产物的 0.998 是【记忆】，不是能力

把新旧产物放在**同一把尺子**（第 14 轮标签平衡同族留出集）上：

| 产物 | tpl | free | diverse | real |
|---|---|---|---|---|
| 旧（3 数据集 / 887 集合） | **0.9980** | **0.9981** | **0.9957** | 0.4472 |
| 新（4 数据集 / 849 集合） | 0.6243 | 0.6160 | 0.6599 | **0.6881** |

**旧产物的 0.998 必须被怀疑** —— 验证：

| 尺子 | 留出条数 | 落在**旧产物训练集**里 | 比例 |
|---|---|---|---|
| tpl | 1,372 | 1,372 | **100.0%** |
| free | 1,526 | 1,526 | **100.0%** |
| diverse | 4,062 | 4,062 | **100.0%** |
| real | 1,440 | 721 | **50.1%** |
| **新产物（四个尺子）** | — | — | **0.0%** |

> **根因**：`build_joint` 的**留出侧取自 `*_train*.jsonl` 文件本身**，
> 而**旧产物正是用这三个文件训练的** —— 对旧产物来说，「留出集」就是它的训练集子集。
> **那 0.998 是逐条记忆。**
>
> **新产物在四个尺子上都是 0% 泄漏**，它的 **0.62 / 0.62 / 0.66 / 0.69**
> 才是**真实的、可比的**水平。

> **★ 陷阱 ㉔：把"生产产物"当"实验模型"评测，会踩到训练集边界不同的问题。**
> 实验模型训练集 = `build_joint` 的训练侧；生产产物训练集 = **整个文件**（含留出侧）。
> **评测已发布产物的第一件事：查它的 `train_sources` 与尺子的关系。**
>
> **这次是被「0.998 太漂亮」抓住的。如果旧产物得的是 0.75，这个污染可能永远不会被发现。**

> **★ 第 14–17 轮的实验数字不受影响** —— 那些实验都是用 build_joint 的训练侧重训，
> verifier 已逐对验证「任意留出 × 任意训练 = 0」。
> **被污染的只有「拿旧生产产物去比 build_joint 留出集」这一个比较。**

### 发现 C：四把「尺子」量的根本不是同一件事

由发现 A 继续追问，先看每个数据集有多少**真正不同**的 criteria：

| 数据集 | 集合名 | 族数 | **唯一 criteria 内容** | 重复倍数 |
|---|---|---|---|---|
| tpl | 180 | 18 | 179 | 1.0× |
| free | 177 | 39 | 174 | 1.0× |
| diverse | 530 | 53 | 523 | 1.0× |
| **real** | 180 | 18 | **86** | **2.1×** |

**要害：留出集合的 criteria 内容是否在训练侧出现过？**

| 数据集 | 留出集合 | 内容**已见过** | 内容**全新** | 已见比例 |
|---|---|---|---|---|
| tpl | 36 | 1 | 35 | **2.8%** |
| free | 40 | 1 | 39 | **2.5%** |
| diverse | 106 | 7 | 99 | **6.6%** |
| **real** | 36 | **28** | 8 | **77.8%** |

> **★★★ real 的留出集合里 77.8% 的 criteria 内容，模型在训练时【逐字见过】。**
> 它只需把已学会的判据套到新文本 —— **那是最简单的任务**。
> 而 tpl/free/diverse 的留出集合 93~97% 是**全新 criteria 内容**。
>
> **四把尺子的难度根本不同，跨尺子比较是苹果比橘子。**

**这解释了第 14 轮一个此前无法解释的数字**：配置 D 在 real 上 0.6583（四把尺子最高）。
**不是 real 学得最好，而是 real 那把尺子最容易**（77.8% 是同判据换文本）。

| 结论 | 是否受影响 |
|---|---|
| 第 14 轮「模型确实读文本」（模型 − 族先验 +0.16~+0.29，12/12 显著） | **不受影响**（尺子内部自比，口径一致） |
| 第 15/16/17 轮 | **不受影响**（都在同一把尺子内比） |
| 「real 比别的分布学得好」这类**跨尺子**表述 | **需改** |

> **★ 陷阱 ㉕：留出集的难度必须被量化 —— 「同族」不等于「同难度」。**
> `samefam` 保证了「留出的族 ⊆ 训练的族」，但**没保证留出的 criteria 内容是新见过的**。

**修复**：新增 `corpus/content_split.py` —— 按 **criteria 内容签名**切分，
断言「留出的内容在训练侧不存在」（+ 文本无重叠）。10 个测试。
产物：`corpus/round19_contentsplit.py`（用干净尺子重跑四配置消融）。

### 发现 D：换新尺子重跑，第 14 轮的**结构性结论完整复现**

用「**内容切分 + 标签平衡**」这把**最严格**的尺子（既要求留出内容是新判据、又无标签偏斜）
重跑四配置消融：

| 配置 | tpl | free | diverse | real |
|---|---|---|---|---|
| A) 只 tpl | .6169 | .4185 | .4260 | .4123 |
| B) tpl+free | .6250 | .6320 | .4336 | .3477 |
| C) tpl+free+diverse | .6593 | .6096 | .6065 | .4277 |
| **D) A+B+C+real** | **.6492** | **.5899** | **.6113** | **.6892** |

**第 14 轮的三个关键效应，在两把完全不同的尺子上全部复现**：

| 效应 | 旧尺子（samefam 平衡） | **新尺子（内容切分平衡）** |
|---|---|---|
| 加 free → **free 尺子大涨** | .3692 → .6141（**+0.245**） | .4185 → .6320（**+0.214**） |
| 加 diverse → **diverse 尺子大涨** | .4147 → .5991（**+0.184**） | .4336 → .6065（**+0.173**） |
| 加 real → **real 尺子大涨** | .4862 → .6583（**+0.172**） | .4277 → .6892（**+0.262**） |
| 加 free → tpl 基本不动 | +0.006 | +0.008 |

> **★★★ 「criteria 多样性是跨分布泛化的决定性杠杆」不依赖于尺子的选择。**
> **换切分口径、换平衡子集、换留出集合（两者留出完全不同），效应量几乎不变。**
> **这是对第 14 轮结论最强的一次稳健性检验。**

**诚实增量（模型 − 多数类）—— 对角线最大**：

| 配置 | tpl | free | diverse | real |
|---|---|---|---|---|
| A) 只 tpl | **+0.2424** | +0.0019 | +0.0019 | +0.0117 |
| B) tpl+free | +0.2505 | **+0.2154** | +0.0095 | −0.0529 |
| C) tpl+free+diverse | +0.2848 | +0.1929 | **+0.1824** | +0.0271 |
| **D) A+B+C+real** | +0.2747 | +0.1732 | +0.1872 | **+0.2886** |

> **训练里包含哪个数据集，就在那个尺子上增量最大。**
> **且 A 行是漂亮的负对照**：只训 tpl 时 tpl +0.24，而 free/diverse/real 只有 **+0.002 / +0.002 / +0.012** ——
> **跨判据泛化确实几乎不存在**（与第 13 轮「跨维度族不可迁移」一致）。

> **★ 发现 C 的正确表述**：**跨尺子的绝对值不可比；但同一把尺子内的配置比较完全可靠。**
> **而本项目几乎所有结论都是同一把尺子内的比较。**

### 发现 E：模型学的是**判据匹配**，不是**记住答案分布**

**方法**：用已发布的生产产物直接分解（无需训练，两把尺子都标签平衡）。

| 尺子 | 定义 | 准确率 | 多数类 | **模型−多数类** |
|---|---|---|---|---|
| **Ruler S** | 判据内容**已见** + 文本全新 | 0.6840 | 0.4629 | **+0.2211** |
| **Ruler F** | 判据内容**全新** + 文本全新 | 0.6348 | 0.4150 | **+0.2197** |

**逐行配对自助法（模型 vs 诚实族先验）**：S **+0.2400** [+0.2125,+0.2676] · F **+0.2054** [+0.1794,+0.2325]，**p<0.0001，两把都显著**。

> **★★★ 以「模型 − 多数类」这个同口径增量看，
> 在【全新判据】上与【已见判据】上几乎完全相同（差 0.0014）。**
>
> **模型不是靠记住判据集合的答案分布 —— 它在没见过的判据上同样有效。**
>
> **那 0.0492 的裸准确率差来自两把尺子的【多数类基线不同】（0.4629 vs 0.4150），
> 不是模型能力的差异。★ 比较不同尺子的裸准确率是错的，必须减去各自的多数类基线。**

### ★ 与第 19 轮 A 行不矛盾 —— 需要精确区分

| 陈述 | 含义 |
|---|---|
| 第 19 轮 A 行（只训 tpl，free/diverse/real 增量 +0.002/+0.002/+0.012） | 训练里没有那个**数据集/维度族** → **跨维度族泛化≈0** |
| 第 20 轮 Ruler F（判据全新，增量 +0.22） | 训练里没有那个**具体判据内容**，但同属已训练的维度族 → **泛化成立** |

> **★★★ 合起来的精确结论：**
> **模型的泛化能力是【维度族内】的，不是【跨维度族】的。**
>
> **它能在同一维度族里对没见过的具体判据做零样本匹配（+0.22）；
> 但换一个维度族就完全做不到（+0.002）。**
> **这是第 13 轮「跨集合可迁移、跨维度族不可迁移」的第三种独立验证。**

**实际意义**：

| 问题 | 答案 |
|---|---|
| 部署新 criteria 集合（同维度族）要重训吗？ | **不需要**（零样本 +0.22） |
| 部署全新维度族要重训吗？ | **需要**（跨族 ≈0） |
| 生产产物的能力是真的吗？ | **是**（模型−诚实族先验 +0.24/+0.21，p<0.0001） |

---

## ★★ F22 完成：官方 JS SDK 原样打 Alben，抓出 3 个真 bug

**F22 的验收标准不是「我写了一个 JS 客户端」，而是「官方 SDK 能原样用」** —— 与 F21 对称。

`npm install @typesafe-ai/sdk@0.6.0`，用官方 `TypeSafeClient` + `noul()`/`choice()`/`score()` helper 原样打 Alben：

### 结果：**30 / 30 断言通过**

| 组 | 断言数 |
|---|---|
| `client.models.list()` 解析 | 5 ✅ |
| 三种原语（官方 helper 构造） | 14 ✅ |
| 255 选项不截断 | 1 ✅ |
| 错误分流（403/401） | 4 ✅ |
| 官方 `ModelMetadata` 三字段 | 4 ✅ |
| 未知 model → 400 | 2 ✅ |
| 别名 `jev-latest` 被接受 | 1 ✅ |

### ★★★ 官方 SDK 抓出三个真实 bug

| # | bug | 后果 |
|---|---|---|
| 1 | `/v1/models` 返回 `{id, encoder}` 而非官方 `{name, description, release_date}` | 官方 SDK 拿到**三个 `undefined`** —— **JS 动态类型不抛错，静默错** |
| 2 | `/v1/models` **完全没有鉴权检查** | 官方 `security=[{"HTTPBearer": []}]` 要求鉴权；Alben 此前**任何人可列模型** |
| 3 | `model` 字段**完全不解析** | 官方语义：未知名 → 400 `Unknown model: <x>`；别名 → 响应报具体版本 |

> **★★★ 这三个 bug 都是【官方 SDK 原样打一遍】才暴露的。**
> **自己写的 JS 客户端（第 18 轮）没有发现它们 —— 因为它按我理解的契约写的。**
>
> **这正是 F21/F22 作为「最强验收手段」的价值：它不接受你的理解，只接受官方实现。**
>
> **★ 与第 8 个硬发现完全同构**：A5 用官方 Python SDK 打旧 `jev/` 抓到 2 个 bug；
> **现在同一招在 JS SDK 上又抓出 3 个。**

**修复**：`alben/contracts.py`（新增 `resolve_model`/`model_card`）· `alben/server.py`（models 加鉴权 + 官方三字段 + model 解析）· `sdk/js/index.d.ts` · `sdk/js/e2e.test.mjs` · **新增** `tests/test_official_js_sdk.py`。

> **★ 陷阱 ㉗：自己写的客户端不能验证契约 —— 它验证的是「我的理解是否自洽」。
> 只有官方实现能验证契约。**

---

## ★★ 第 27 轮：部署产物的标定温度一直是错的

**触发**：核对重训后的生产产物时发现三处温度不一致。

### 缺陷 A：拟合出来的温度【从未生效】

| 位置 | 值 |
|---|---|
| `alben-scorer.pt` 的 `meta.calibration_temperature` | **5.2976** ← **Scorer 读这个** |
| `calibration.json` 的 `learned_temperature` | **6.2246** ← 新拟合的 |

**根因**：`fit_calibration.py` **只写 `calibration.json`，从不回写 `.pt` 的 meta**。
**部署的模型一直用着【为旧模型拟合的温度】。**

### 缺陷 B：拟合目标与报告指标【不一致】

用 **NLL** 拟合 T，但置信度口径是 **`peak_confidence`**（非线性），对外报告的是 **ECE**。

| T | 来源 | NLL | **ECE** |
|---|---|---|---|
| **3.4** | **ECE 最优** | 1.0455 | **0.1125** |
| **5.2976** | **当前生效** | 0.9985 | 0.1980 |
| **6.2246** | **NLL 最优（新拟合）** | **0.9959** | **0.2357** |

> **★★★ 两个缺陷叠加的结果是「碰巧没那么糟」**：旧温度恰好比新拟合的更接近 ECE 最优。
> **如果缺陷 A 单独修好，ECE 反而会从 0.1980 恶化到 0.2357。**

### 修复后（实测）

| T 的来源 | 准确率 | 平均置信 | 差 | **ECE** |
|---|---|---|---|---|
| 未标定 | 0.4883 | 0.7541 | +0.2658 | 0.3044 |
| NLL 最优（旧做法） | 0.4883 | 0.2486 | −0.2397 | 0.2399 |
| **ECE 最优（新）** | 0.4883 | 0.3823 | −0.1060 | **0.1174** |

> **★ ECE 0.3044 → 0.1174（降低 61.4%），准确率不变。**

### ★★ 用交叉验证决定「按哪个目标选 T」

评估集上 Brier 最优（0.1081）比 ECE 最优（0.1174）更好 —— **但用评估集选就是泄漏**。
正确做法：在**拟合集内部**再切 5 折来选准则：

| 准则 | 平均留出 ECE | 标准差 | 平均 T |
|---|---|---|---|
| **nll** | **0.2351** | 0.0192 | 6.23 |
| **ece** | **0.1379** | 0.0193 | 3.73 |
| **brier** | **0.1391** | 0.0139 | 3.27 |

> **1. NLL 比 ECE 差 0.0972 —— 证实缺陷 B 是真的。**
> **2. ECE 与 Brier 的差异（0.0012）小于它们各自的标准差（±0.019）—— 统计上不可区分。**
> **3. 所以评估集上「Brier 更好」是噪声；选 ECE 最优是安全的。**

> **★ 陷阱 ㉝：用评估集选择超参数（哪怕"看起来更优的那个"）就是泄漏。**
> **★ 陷阱 ㉞：两个指标的差异必须与它们的【标准差】比较** ——
> 差 0.0012 vs 标准差 0.019，这个"差异"比噪声小一个数量级。

**防再犯**：新增 `tests/test_calibration_consistency.py`（4 测试）——
守 `.pt` meta 与 `calibration.json` 一致、Scorer 实际生效、
准确率不变/ECE 下降、以及**选择准则与候选对照必须落盘**。
**`train_alben.py` 的常量改为从 `calibration.json` 读取**（单一数据源）。

---

## ★★ 第 28 轮：端到端评测路径的三个真实缺陷

### 缺陷 C：评测用了【占位问题】

`evaluate_alben.py` 写死 `"text": "Pick one."`，但训练时 `option_text` 拼的是**该集合自己的 `instructions`** —— **问题通道不一致**。

| 留出集 | 真实问题 | 占位问题 | 差 |
|---|---|---|---|
| free_held | 0.6725 | 0.6925 | −0.0200 |
| **diverse_held** | **0.3175** | **0.4250** | **−0.1075** |

> **差 0.1075，而且方向是「占位问题更好」—— 这个不一致会系统性扭曲评测数字。**

### 缺陷 D：评测没有检查留出集与训练集的重叠

| 留出集 | 与 `prod_train.jsonl` 重叠 |
|---|---|
| 原始 `tpl_held.jsonl` | **9.9%** |
| 原始 `free_held.jsonl` | **11.1%** |
| 原始 `diverse_held.jsonl` | **9.8%** |
| 原始 `real_held.jsonl` | **36.8%** |
| **`build_joint` 留出侧** | **0.0%** |

> **★ `evaluate_alben.py` 的默认 `--held` 就是 `corpus/*_held.jsonl` —— 默认路径一直在用有 10~37% 重叠的留出集。**
>
> **根因**：`*_held.jsonl` 是第 12 轮的原始留出文件，而训练集是 `build_joint` 从 `*_train*.jsonl` 切出的**同族训练侧** —— **两者不是同一划分**。

### ★★ 修正后的干净成绩（四把尺子，重叠 0% + 标签平衡 + 真实问题）

| 尺子 | 留出 | 准确率 | 随机 | **多数类** | **模型−多数类** | 重叠 |
|---|---|---|---|---|---|---|
| tpl | 511 | **0.6243** | 0.3768 | 0.4167 | **+0.2076** | **0%** |
| free | 539 | **0.6160** | 0.3310 | 0.3860 | **+0.2299** | **0%** |
| diverse | 938 | **0.6599** | 0.3775 | 0.4250 | **+0.2349** | **0%** |
| **real** | 436 | **0.6881** | 0.3677 | 0.4010 | **+0.2870** | **0%** |

> **★★★ 这是本项目第一次在【干净尺子 + 标签平衡 + 真实问题】三个条件同时满足下报出的成绩。**
>
> **★ 旧口径 vs 新口径（同一模型，free 尺子）**：
> - 旧：0.7067，重叠 11.7%，**模型−多数类 = −0.0552（不如多数类！）**
> - **新：0.6160，重叠 0%，模型−多数类 = +0.2299（大幅优于多数类）**
>
> **这个翻转完全来自【尺子是否干净、是否平衡】，与模型无关。**

### ★ ECE 最优的温度会【过度校正】平均置信度

| T | 平均置信 | 准确率 | **gap** | ECE |
|---|---|---|---|---|
| 1.00 | 0.7596 | 0.4909 | +0.2687 | 0.2986 |
| **3.05** | 0.4508 | 0.4909 | −0.0401 | **0.1121** ← ECE 最优 |
| **2.70** | 0.4883 | 0.4909 | **−0.0025** | 0.1258 ← **gap=0** |

> **★ 这是 peak 口径非线性的必然结果，不是 bug。** 报 ECE 用 ECE 最优；要平均置信无偏用 gap=0。
> `fit_calibration.py` 现在**同时报出 gap=0 候选**并把权衡写进 `known_tradeoff`。

> **★ 陷阱 ㉟：评测脚本必须走【与训练完全相同的输入构造路径】。**
> **★ 陷阱 ㊱：`--held` 指向哪个文件，必须验证它与训练集的关系。**
> **任何评测脚本都应当自动打印重叠率并在超标时警告。**

**验证**：`pytest tests/ -q` → **142 passed, 7 skipped** · 四把尺子重叠 **全部 0.0%**

---

## ★ 第 29 轮：全量测试暴露的「测试自己骗人」陷阱

把**三个端到端全开**跑一遍，发现 JS 端到端**失败 11/17**，报的是满屏 `AlbenAuthError`。

**看起来像 SDK 坏了 —— 但不是。**

### 根因链条

1. `e2e.test.mjs` `spawn` 自己起服务，**但不检查端口是否被占用**
2. 8099 上有一个**我手工起的服务**（`--key test-key`），而 e2e 用 `sk-e2e-key`
3. `spawn` 因端口占用**失败**，但 `/health` **仍由别人的服务响应**
4. 循环判定「服务就绪」→ **误以为那是自己的服务**
5. 拿错误的 key 去打它 → **401**

> **★★★ 这不是 SDK 的 bug，是【测试骗了人】。**
> **它把「端口冲突」伪装成「SDK 鉴权坏了」—— 如果我不追下去，会去改一个根本没坏的 SDK。**

### 修复（两层）

| 层 | 内容 |
|---|---|
| `e2e.test.mjs` | 检测 `address already in use` / `EADDRINUSE` → **立刻失败**；`/health` 就绪后**再用自己的 key 探一次** `/v1/models`，401/403 就**立刻失败** |
| `tests/test_js_sdk.py` | 改用 `--port 8199`，**从源头避开** 8099 上的手工服务 |

> **★ 关键：把「还没起来」与「服务不是我们的」区分开 —— 后者必须立刻失败，不能继续等 240 秒。**

### ★★ 验证：第一次【零跳过】跑完全部测试

| 命令 | 结果 |
|---|---|
| 常规 | **160 passed, 6 skipped** |
| **三个 E2E 全开** | **166 passed, 0 skipped, 0 failed** |

**含官方 Python SDK（5）· 官方 JS SDK（30 项断言）· 自研 JS SDK（25）。**

> **★ 陷阱 ㊲：测试脚本必须验证「它连上的服务确实是它自己起的」。**
> 「起服务 → 探 `/health` → 认为就绪」这个模式**有静默漏洞**：端口被占用时 `/health` 由**别人的服务**响应。
> **正确做法：就绪后用自己的凭据探一次受保护端点，被拒就立刻失败。**
>
> **★ 更一般的教训：「测试失败」的第一反应不应是「被测代码坏了」，而应是「测试真的测到了我想测的东西吗」。**
> 这与第 21 轮（消融干预了死通道）、第 23 轮（自己写的客户端验证不了契约）**是同一类错误的第三次出现**：
> **验证者本身没有被验证。**

---

---

## ★★★ 第 30–33 轮：能力边界的最终定位（**最重要的一节**）

> **此前所有"未见 criteria 准确率"都是【同一个维度族】的留出。**
> **第 30–33 轮把"跨维度族"这个真正的缺口补上了。**

### 三个数字

| 情况 | 结果 |
|---|---|
| 训练过的维度族内（干净尺子） | tpl 0.6243 / free 0.6160 / diverse 0.6599 / real 0.6881，**模型−多数类 +0.21~+0.29** |
| **没训过的维度族**（4183 条干净跨族） | **0.3150 vs 随机 0.3211 → 等于随机** |
| **全新语义轴 + 少量示例** | 0-shot **0.4961** → 1-shot 0.6018 → **4-shot 0.8867** |

### 第 30 轮的陷阱：只跟随机比是不够的

留出没见过的维度组合，得到 **0.5936**（随机 0.4133），看起来"跨维度可迁移"。
补测冻编码器余弦基线后：

| | 准确率 |
|---|---|
| B0 随机 | 0.4133 |
| **B1 冻编码器余弦（零训练）** | **0.5036** |
| M 训练塔 | 0.5936 |
| **训练塔 − B1** | **+0.0900** |

> **★ 0.5936 里有 0.5036 是 BGE 编码器自带的语义相似度。**
> **★ 陷阱 ㊳：报「模型比随机好」之前，先问「冻结的编码器本身是不是就够了」。**
> **★ 陷阱 ㊴：合成的"新维度"必须先证明措辞与训练【零交集】——**
> 第 30 轮第一版就栽在这：200 个维度共用 120 个 cue 短语，留出判据逐字在训练里出现过，
> 得到虚假的 1.0000。现在写成自断言（训练 299 种 / 留出 176 种 / **交集 0**）。

### 第 31 轮：留出【整根全新语义轴】（最重要的一张表）

训练 7 根轴，留出 novelty / compliance / reversibility **三根整轴**：

| 条件 | 准确率 | 模型−随机 | **模型−冻编码器** |
|---|---|---|---|
| B1 冻编码器（零训练） | 0.4789 | — | — |
| **C0 零样本** | **0.4961** | +0.0817 | **+0.0171** |
| C1 1-shot（每轴 12 条） | 0.6018 | +0.1874 | +0.1228 |
| **C4 4-shot（每轴 48 条）** | **0.8867** | +0.4724 | **+0.4078** |

> **全新语义轴零样本几乎学不到（只比冻编码器高 0.017），但 4-shot 就到 0.8867。**
> **模型有能力，缺的是"告诉它这个维度长什么样"。**
>
> **★ 陷阱 ㊵：留出组合 ≠ 留出轴。** 轴见过的话，测的只是"同轴换说法"。
> **★ 陷阱 ㊶：few-shot 的示例集合绝不能出现在评测集里。**

### 第 32 轮：加轴不是杠杆

训练轴 2 → 4 → 7 根，新轴 0-shot = 0.4898 / 0.4826 / 0.4961，**3.5 倍只涨 +0.0062**。

> **★ 陷阱 ㊷：「数量不够」是一个可以否证的假设，要去测它，不要靠感觉。**

### 第 33 轮：教新维度的收益与代价

| 产物 | 训练集 | 新维度 | 四条真实尺子平均 |
|---|---|---|---|
| v1 | prod_train（849 集合） | 0.4862 | **0.6471** |
| v2 | +150 合成维度（1749 集合） | 0.5532 | — |
| **v3** | +每根新轴 4 集合教学（1789 集合） | **0.9372** | 0.6255 (**−0.0216**) |

**新维度 +0.4510，代价 −0.0216。**

### 落地工具：`teach_alben.py`

热启动微调（不是从零训），实测**留出新维度 0.4917 → 0.9917**（12 epoch / 约 6 分钟）。

三个防自欺机制：① 按**集合**切留出（不按行）；② 旧数据里另留一批**不重放**的集合当遗忘探针；
③ 同时报"微调前 / 微调后"。

> **★ 陷阱 ㊸："遗忘"必须用【没重放过的】旧数据去量** —— 用重放过的测必然 1.0，等于没测。
> **★ 陷阱 ㊹：热启动微调要同时报"教的能力"与"旧能力"** —— 只报一边能造出任何结论。

### 守门测试

`tests/test_round30_33_experiments.py`（**17 个**）把上面每一条不变量钉死，包括
**"加轴不是杠杆"**（delta >0.05 就报错，逼着回来改文档）。

**完整说明见 [能力边界.md](https://github.com/XiaoBinGan/Alben/blob/main/能力边界.md)。**

---

## ★★ 安全修复：仓库历史里的密钥已彻底清除（第 25 轮）

**发现**：`design/_ref/` 下有 **5 个早期探针脚本把 `TYPESAFE_API_KEY` 硬编码在源码里**，
且从**最早的第 7 轮提交**起就在 git 历史里。

| 项 | 值 |
|---|---|
| 含密钥的提交 | **64 / 66** |
| 远端 `main` 是否含 | **是** |
| 仓库可见性 | **私有** |

**处置**：

| 步 | 动作 | 结果 |
|---|---|---|
| ① | 5 个文件改为从 `corpus/apikey.py` 单点解析 | ✅ 工作区已无明文密钥 |
| ② | `git filter-branch --index-filter` **重写全部提交** | ✅ **全历史 0 / 65 含密钥** |
| ③ | `git push --force-with-lease=main:<真实远端>` | ✅ 远端已覆盖 |

**★ 为什么用 index-filter 而不是 tree-filter**：本仓库用 Git LFS 跟踪 91 MB 权重，
tree-filter 会逐个提交 checkout 触发 LFS smudge（**66 × 91 MB 不可行**）。
index-filter 只操作 index/blob，**不 checkout、不碰 LFS**。

**完整性核对**：文件树路径 **357/357 相同** · 内容 blob **0 个不同** ·
LFS 对象完好 · 提交数 66→65（少的是**空提交**「启用 Git LFS 跟踪」，tree 与父提交逐字节相同）·
**137 passed**。

**防再犯**：新增 `tests/test_no_secrets.py`（4 测试），扫描工作区 + git 跟踪文件，
**并自检「扫描器本身能检出植入的密钥」**。

> **★★ 陷阱 ㉘：清理脚本的匹配式必须与【实际形态】一致。**
> 我的第一版按 `apikey-` 写，实际是 `apikey_<hex>_<hex>`（下划线）。
> 脚本报告「已清理 0 个文件」，**我差点以为干净了**。
> **一个不检测的检测器，比没有检测器更危险 —— 它给你虚假的安全感。**
>
> **★ 陷阱 ㉙：批量改写脚本时，不要顺手加「看起来有用」的东西。**
> 我只该替换一行，却加了 3 行，其中 `_hdr()` 是**死代码**。
>
> **★ 陷阱 ㉚：`--force-with-lease` 的基准是【本地跟踪引用】，
> 而 filter-branch 会把它一起重写** —— 所以必然因「stale info」被拒。
> 正确做法是显式指定真实远端 SHA。
>
> **与第 21 轮那次对称**：那次我设计错消融实验（换标签键 = 干预死通道），也是因为「按自己的理解设计」。
> **两次的解法相同：用【官方实现】而不是【我的理解】当裁判。**

### 发现 F：模型学的是**文本 ↔ 判据描述的匹配** —— 三轮消融的完整图景

**用生产产物做三个干预**（只动描述这一条通道，标签键对模型完全无影响）：

| 轮次 | 干预 | Ruler S | Ruler F | 说明 |
|---|---|---|---|---|
| — | 原描述 | 0.6840 | 0.6348 | — |
| **第 22 轮** | **打乱描述与选项的配对** | **0.1911** | **0.2199** | **崩到低于随机** |
| **第 22 轮** | 描述**逐字倒序** | 0.3680 | 0.3978 | 掉到随机 |
| 第 21 轮 | 描述换成**无意义填充** | 0.3619 | 0.3537 | 掉到随机 |
| 第 21 轮 | 描述换成**标签名** | 0.5542 | 0.5736 | 标签名也带信号 |
| 第 20 轮 | 判据**全新**（描述完整） | 0.6840 | 0.6348 | **几乎不退化** |

> **★★★ 打乱配对后准确率 0.684 → 0.191 —— 【低于随机】。**
> 低于随机说明模型是**主动跟着描述走**的：描述被移到哪个选项它就选哪个（于是系统性选错）。
> **这是「配对是模型决策依据」的最强证据 —— 比"掉到随机"更强。**
>
> **描述逐字倒序 → 掉到随机**，说明模型依赖**词序与语义**，不是字符 n-gram 表面匹配。

**第 20–22 轮合起来**：

| 结论 | 证据 |
|---|---|
| 模型在**全新判据**上几乎不退化 | Ruler F 增量 +0.2197 vs Ruler S +0.2211 |
| 决策依据是**描述-选项配对** | 打乱配对 → 崩到 0.19（低于随机） |
| 依赖**词序/语义** | 逐字倒序 → 崩到随机 |
| 泛化是**维度族内**的 | 第 19 轮 A 行跨族增量 +0.002 |

> **★ 方法论收获**：第 21 轮我设计错了实验 —— 换的是**标签键**，
> 而 `option_text` **根本不读标签键**，等于干预了一个死通道。
> **「干预后结果不变」有两种可能：(a) 模型不依赖它，(b) 干预没生效。必须先排除 (b)。**

---

## ★ A2 待办复核完成（第 23 轮）：官方 Score confidence 的真实形态

**待办**：「复核『15/46 个 `confidence > p_max`』按 `(1−p_max)/(n−1)` 分层」。
**分层变量来历**：`p_max − peak = (1 − p_max)/(n − 1)` —— 即「p_max 与 peak 的间距」。

### 分层假设【成立】

| 间距区间 | 点数 | 超 p_max | **超 p_max + 舍入** | 最大超出 |
|---|---|---|---|---|
| [0.00, 0.02) | 233 | 0 | 0 | 0.0000 |
| [0.02, 0.05) | 94 | 0 | 0 | 0.0000 |
| [0.05, 0.10) | 71 | 1 | **0** | 0.0000 |
| [0.10, 0.20) | 78 | 2 | **0** | +0.0100 |
| [0.20, 1.01) | 3 | 0 | 0 | −0.1000 |

> **超 p_max 的 3 个点全部落在间距 ≥ 0.05 的两桶里，且超出量全部 ≤ 一个舍入步长。
> 扣掉舍入后 `conf > p_max` = 0 / 479。**

**仍不可复核**：「15/46」的**分母 46**（46 条探针原始响应从未落盘）；
**479 点全部 n≡4**，所以「只在 n≥5 出现」这个假设**本数据无法验证**。
→ **「15/46」这个具体比例不应继续引用。**

### ★★★ 但分层暴露了更重要的事实

| 候选公式 | MAE | **中位数** | 最大偏差 |
|---|---|---|---|
| **p_max** | **0.0142** | **0.0000** | **0.4200** |
| normalized_peak | 0.0346 | 0.0133 | 0.2267 |

> **p_max 的 MAE 只有 0.0142，但中位偏差 = 0.0000、最大 0.42 ——**
> **这不是「接近 p_max」，而是「多数点上恰好等于 p_max，少数点完全不同」：分段行为。**

| p_max 区间 | 点数 | **平均 conf − p_max** | 中位归一熵 |
|---|---|---|---|
| [0.30, 0.50) | 16 | **−0.1325** | 0.7920 |
| [0.50, 0.70) | 62 | −0.0271 | 0.5535 |
| [0.85, 0.95) | 104 | −0.0071 | 0.2182 |
| **[0.95, 1.01)** | **231** | **−0.0021** | **0.0404** |

> **★ `conf = p_max` 只在【尖锐分布】上成立（p_max ≥ 0.95 的 231 个点上偏差仅 −0.002）；
> 分布平坦时 conf 系统性低于 p_max（p_max ∈ [0.3,0.5) 时低 0.13）。**
>
> **「相等组 vs 不等组」**（±0.01）：相等组 397 点（82.9%，归一熵中位 **0.121**）；
> 不等组 82 点（17.1%，归一熵中位 **0.507** —— **4.2 倍**）。

**公式拟合** `conf = a·p_max + b·归一化熵 + c`：a=+0.8882, b=−0.1749, c=+0.1244，
**MAE 0.0181（比纯 p_max 的 0.0142 更差）** → 官方公式**不是** p_max 与熵的线性组合。

> **★ Alben 维持 `normalized_peak` 不变** —— 选它的依据是**「与 Choice 同尺度、可解释」**，
> 不是「更接近官方」。**这是已知的、已公布的偏差，不是缺陷。**

#### 修正后的完整结果：负例池越大越差，**每个对比都显著**

| 负例池 | 池大小 | epoch | tpl | free | diverse | real | train loss |
|---|---|---|---|---|---|---|---|
| 组内（当前做法） | 2.9 | 32 | **0.6184** | **0.5955** | **0.6215** | **0.6583** | 0.0004 |
| 族内池 | 19.0 | 96 | 0.5108 | 0.4323 | 0.4733 | 0.4885 | 0.0001 |
| 全局池 | 38.1 | 128 | 0.4736 | 0.4193 | 0.4488 | 0.4954 | 0.0003 |

**配对自助法**：group→family 四个尺子 **−0.11 ~ −0.17，全显著**；
group→global **−0.14 ~ −0.18，全显著**。

> **★★★ 负例池越大越差。这不是欠训练（loss 已收敛到 0.0001~0.0003）。**
>
> **原因**：「组内负例」看似太容易，其实给了模型一个**有结构的任务** ——
> 同一集合里的选项是**互斥的等级**（`easy / costly / impossible`），
> 模型学的是「这条文本落在该维度哪一档」。
> 而把**不同问题的选项**混进来（urgency 的选项和 sentiment 的选项同时当候选），
> 模型被迫分辨「这个选项属于哪个问题」—— **那是另一个任务，且会干扰原本要学的分档。**
>
> **正确的表述不是「硬负例没用」，而是：负例必须来自【同一个判据空间】才有意义。**
> **想让它变难，应当加深【同维度内】的难度（`easily_reversed` vs
> `reversible_with_effort` 这类近义等级），而不是把别的维度拉进来。**

### ⚠️ 第 11 轮的结论【暂时撤回】，等待第 12 轮重测

第 12 轮在扩大多样性时发现**三个数据构造缺陷**，它们波及第 11 轮用到的数据：

1. **criteria 的每个选项描述完全相同** —— 我把「问题」写进了描述位置，
   模型无法区分选项（train loss 1.0169，正常 0.02 量级）
2. **留出集 100% 泄漏进训练**（集合名重叠 100/100）
3. **`instructions` 被完全忽略** —— 实测 Jev 换问题会换答案，Alben 却不会

**所以下面第 11 轮的数字建立在有缺陷的数据上，必须重测后才能引用。**
修复已在进行：新建 `corpus/criteria_build.py` 作为 criteria 的单点定义，
旧数据用 `fix_free.py` 原地修复（标签未变，纯结构修复）。

> **★ 教训（已写入方法论）**：
> **一个退化得太彻底的数字，先怀疑实验，不要接受它。**
> 多样留出只有 0.35、恰好等于随机基线 0.3758 —— 正是这个信号引出了上面三个缺陷。

---

### ★★★ 第 11 轮（数字待重测）：criteria 多样性 —— 跨分布泛化的杠杆

第 10 轮暴露了真实瓶颈：同分布 0.8055，**跨分布只有 0.3715**；
5 个临时真实任务**期望首选命中仅 2/5**。

> **中途我曾假设「中文 criteria 是短板」（训练数据是英文 criteria）——
> 这个假设被消融实验【推翻】：EN = 2/5，ZH = 2/5，完全相同。**
> **真正的原因是【criteria 分布不匹配】。**

**于是加入 177 个【自由撰写】的 criteria 集合。结果：**

| 配置 | n | 模板留出 | 自由留出 |
|---|---|---|---|
| A) 360 模板（原状） | 13,129 | **0.8020** | 0.3933 |
| **B) 360 模板 + 177 自由** | 20,209 | **0.7980** | **0.6956** |
| C) 只 177 自由 | 7,080 | 0.3155 | 0.7094 |
| D) 180 模板 + 177 自由 | 13,643 | 0.5940 | 0.6872 |

**配对自助法：**
- A→B **自由留出 Δ=+0.3022 CI [+0.2728, +0.3328] ★ 显著**
- A→B 模板留出 **Δ=−0.0040 CI [−0.0160, +0.0085] 不显著** ← **几乎零代价**
- C→D 模板留出 **Δ=+0.2785 ★ 显著**，自由留出 −0.0222 不显著

> **★★★ 「多样性」与「数量」是两个独立杠杆，各自作用于自己那个分布。**
> **模型能同时装下两个分布，不必二选一。**

**端到端（走真实服务路径）：**

| 留出集 | 准确率 | 相对随机 |
|---|---|---|
| 模板分布 | **0.8025** | 2.74× |
| **自由分布** | **0.7144** | **2.00×** |

> **跨分布从 0.3933（1.27×）提升到 0.7144（2.00×）。**

**本轮修掉一个真实 bug**：结构化 `state`（object/array）导致 400 —— 官方明确支持，早期实现没处理。

**本轮一个失败的方向（留痕）**：按分布分别标定温度，加权 ECE 0.1166 **比**单一温度的 0.1112 **更差**。
原因是跨分布时模型「既不准又自信」，温度缩放修不了这个结构性问题。

---

## 十二轮实验（本仓库最有价值的部分）

每一轮都推翻了上一轮的一个假设。**这些否定结论本身就是产出。**

| 轮次 | 假设 | 结果 |
|---|---|---|
| 1 | 10 类固定任务能区分架构形状 | ❌ 全部 1.0000，无判别力 |
| 2 | 形状（S2/S3/S4）是瓶颈 | ❌ 换双线性/交叉仍 ≈ 随机 |
| 3 | 拼接特征够用 | ❌ 线性打分器**结构上**无法泛化到新选项 |
| 4 | 「同义改写 0.7242」说明学会匹配 | ❌ 那是**选项向量自带的类别信息** |
| 5 | **选项多样性是瓶颈** | ❌ **40 个 criteria 集合仍 ≈ 随机（0.2650 vs 0.2552）** |
| **6** | **encoder 没被训练是瓶颈** | ✅ **成立（0.2853 → 0.3940）** |
| **7** | 数据量是瓶颈 | ❌ **结论作废** —— 该曲线在 epochs=2 下测得，是**欠训练伪影** |
| **8** | **训练配置 / 数据广度是瓶颈** | ✅ **成立（0.6337 → 0.8000）** |
| **9** | **广度仍显著、深度已饱和** | ✅ **成立（同分布配对证据）** |

### 第 8 轮：训练配置才是主因（★ 推翻第 7 轮）

**第 7 轮的曲线是在 `epochs=2, lr=5e-6` 下测的 —— 严重欠训练。修正配置后：**

| #criteria | #samples | 第 7 轮（欠训练） | **第 8 轮（修正）** | 差距 |
|---|---|---|---|---|
| 20 | 1,305 | 0.4174 | **0.5413** | +0.124 |
| 80 | 5,227 | 0.5935 | **0.7207** | +0.127 |
| **180** | **11,793** | 0.6337 | **0.8000** | **+0.166** |

**配置**：`freeze=3, lr=1e-4, OneCycle, epochs=32` ｜ 随机基线 0.2920

**★ 关键**：所有配置的 train loss 都降到 **0.0007–0.0013**（几乎完美记忆），而 acc 仍随数据上升 → **瓶颈在泛化，不在拟合能力**。

### 第 8 轮的决定性发现：**广度 > 深度**

**固定 180 个 criteria 集合，只改变每集合用多少条样本：**

| 每集合条数 | 未见 acc | 对比 | Δ | 95% CI | 判定 |
|---|---|---|---|---|---|
| 10 | 0.7255 | — | — | — | — |
| 20 | 0.7603 | 10→20 | +0.0348 | [+0.0136, +0.0554] | **★ 显著** |
| 40 | 0.7859 | 20→40 | +0.0255 | [+0.0092, +0.0418] | **★ 显著** |
| 65 | 0.7870 | **40→65** | **+0.0011** | **[−0.0120, +0.0141]** | **不显著** |

> **每集合样本数超过约 40 条后收益归零。**
> **而「更多 criteria 集合」的边际增益在 180 处仍有 +0.0223。**
> **→ 【广度】比【深度】更值钱。**

### 第 9 轮：广度实验（含一次设计缺陷自查）

**第一版有缺陷**：新增的 180 个集合来自**全新的 20 个维度**，而留出集是**原 20 个维度**的变体
→ **分布不匹配**。实测 new180 只有 **0.3715**，说明第一版看到的差距主要来自分布不匹配。

**第二版（修正：训练与留出同维度，只改集合数）：**

| #criteria | 未见 acc | 相对随机 | **边际增益** | 配对自助法 |
|---|---|---|---|---|
| 20 | 0.5055 | 1.73× | — | — |
| 40 | 0.6255 | 2.14× | +0.1200 | **★ 显著** |
| 80 | 0.7220 | 2.47× | +0.0965 | **★ 显著** |
| 120 | 0.7585 | 2.59× | +0.0365 | **★ 显著** |
| **180** | **0.7850** | **2.68×** | **+0.0265** | **★ 显著** [+0.0125, +0.0405] |

> **每个翻倍点都配对显著，包括最后一个。边际增益在衰减，但没有饱和。**

**第三版：把外推变成实测（180 → 360，同分布）**

用**同样的 20 个维度**再生成 180 个新集合（`_10.._18`）× 40 条 = 7,200 条（$0.117 / 0 失败）

| 配置 | 未见 acc | 相对随机 | n |
|---|---|---|---|
| A) 180 集合 × 40 | 0.7850 | 2.68× | 7,200 |
| **B) 360 集合 × 40** | **0.8020** | **2.74×** | 13,129 |

**配对自助法：Δ=+0.0170，CI [+0.0025, +0.0315] → ★ 显著**

> **同分布下 180→360 仍显著 → 120→180 的观察得到独立实测确认。**
> **完整衰减序列：+0.120 → +0.0965 → +0.0365 → +0.0265 → +0.0170**

**★★ 合并第 8 轮的完整资源分配结论：**

| 维度 | 边际收益 | 判定 |
|---|---|---|
| 每集合样本数（>40 条） | Δ=+0.0011，CI 跨 0 | **已饱和** |
| **criteria 集合数**（120→180） | **Δ=+0.0265，CI 全正** | **仍显著** |

### 三个反复出现的陷阱（本项目踩了四次）

> **① 恰好等于基线的数字，必须被怀疑，而不是被接受。**
>
> 三次实例：形状实验首版 **0.1000**（恰好 1/10）· TF-IDF 版 **0.3333**（恰好多数类）· 第 5 轮 **0.2650**（恰好随机）。
> 前两次经诊断都是**退化伪影**（模型恒预测同一个选项）。

> **② 每个「看起来变好了」的数字，先问：测试条件是否仍在训练分布内？**
>
> 第 4 轮的 0.7242 就是这么来的 —— 选项只换措辞，**集合本身仍是训练时见过的 10 个**。

> **③ 训练/测试划分必须自查重叠，不能假定。**
>
> 第 5 轮留出集有 **86.9% 的文本**在训练集出现过（criteria 不同但 state 文本相同）→ 高估泛化。已修正为重叠 = 0。

> **④ 训练集与测试集必须【同分布】—— 否则「广度有效」这类结论会被伪影冒充。**
>
> 第 9 轮首版用**全新维度**做广度实验，而留出集是**原维度**的变体 →
> 分布不匹配，new180 只有 **0.3715** 而 old180 是 **0.7850**。
> 如果没追问「增益来自哪里」，就会把这个伪影当成「广度显著」写进文档。
> 修正为同维度后重做，才拿到可信的 **+0.0265** 和 **+0.0170**。

> **⑤（实现层）交付的数字必须做【标定】检查，不能只看准确率。**
>
> 端到端准确率 0.8055 看起来很干净，但置信度 ECE = **0.1100**（严重未标定）。
> **准确率合格 ≠ 概率可用。** 已用温度缩放修复到 0.0604。

---

## 已完成的可复现产出

### 1. corpus-v2（去泄漏语料）

- **2,400 条**，`sha256 = 6244baecc1510ce73d7e52d555a56abc7da422ab583f57b8cdba1d1701672949`（600,581 B）
  - **★ 所有产物的哈希与规模统一记录在 `corpus/MANIFEST.json`**（由 `corpus/make_manifest.py` 计算生成，**不手工抄录**）
  - **立此规则的原因**：此前文档里记录的 `6c336907...` 与实测 `6244baec...` 不符，**而文件本身未变**（600,581 B / 2,400 行）—— **手工抄录的哈希会漂移**
- **spec 在任何生成之前冻结**（`corpus/corpus_spec.yaml`）
- **C1a PASS：17 个条件，0 违规** —— 10 个部门 `P(refund|dept) = 0.3500`，极差 **0.0000**（v1 是 0.936）
- **最佳 oracle 规则 0.7433**，多数类基线 0.6500 → **比 v1 更难**（「请不要给我退款」含「退款」，必须理解否定）

### 2. 官方基线（479 次真实调用）

| 指标 | ZH (n=398) | EN (n=81) | ALL (n=479) |
|---|---|---|---|
| dept | 0.8417 | 0.7654 | **0.8288** |
| urgency | 0.6256 | 0.6296 | 0.6263 |
| refund | 0.8442 | 0.8519 | 0.8455 |

**ZH−EN = +0.0763（z=1.66，不显著）** —— 官方文档「CJK 表现不等」**未复现**。
但 80% 功效的检出下限约 **0.13** → **「未复现」≠「已证伪」**。

### 3. 契约事实（10 处文档与实测不符，全部以实测为准）

- 仅 2 个端点；`/v1/models` 必须返回 `{"models":[...]}`（现有实现返回 `{"data":[...]}`，**是错的**）
- **缺失 key → 403**，无效 key → 401（不是常规顺序）
- **概率四舍五入到 2 位小数**（现有实现用 `round(p,3)`）
- `ScoreQuestion.criteria` 是 `minItems: 1`（散文说「至少两个」，**是错的**）
- 错误响应体有 **4 种形状**（按 `detail` 类型区分）
- 422 的 `input` 会**回显整个请求体（含 state）** → 日志必须脱敏

### 4. 一个可验证的数学事实

**P4（20,000 次随机分布验证，最大残差 1.110e-16）**：

```
p_max − peak = (1 − p_max) / (n − 1)      0 ≤ p_max − peak ≤ 1/n
```

`peak = max(0, min(1, (n·p_max − 1)/(n − 1)))`。
**→ 这解释了官方文档 8 个 Score 例子中，6 个符合一种口径、2 个符合另一种。**

---

## 目录结构

```
Alben/
├── design/                 设计文档（总设计 + 5 份分层设计）
│   ├── 总设计-Alben.md      ★ 主知识库（含 F1–F24 权威定义）
│   ├── A1-模型与训练层.md
│   ├── A2-API契约与类型系统.md
│   ├── A3-标定与评测层.md
│   ├── A4-SDK与开发者工具.md
│   ├── A5-运行时与工程化.md
│   └── _ref/               调研事实库（RECON-FINDINGS.md 52 KB）
├── corpus/                 ★ 语料与实验（本仓库最有价值的部分）
│   ├── MANIFEST.json       ★ 单一权威哈希/规模记录（生成而非手抄）
│   ├── corpus_spec.yaml    生成前冻结的规格
│   ├── corpus_v2.jsonl     2,400 条去泄漏语料
│   ├── shape_*.py          S1–S4 形状对照实验（四轮）
│   ├── distill.py          官方 API 蒸馏（2,400 次调用 / $0.036）
│   ├── contrastive2.py     ★ 第六轮：训练 encoder（有效）
│   └── scale_gen.py        ★ 扩数据验证
├── 调研结论与实施计划.md    面向实施的调研总结
└── 迁移交接书.md            换机交接（含已知坑 9 类）
```

---

## 复现全部实验

```powershell
$env:PYTHONIOENCODING='utf-8'   # Windows 中文环境必须
cd Alben

# 语料质量门
python corpus\gates.py

# 形状实验（四轮）
python corpus\shape_experiment_real.py corpus\corpus_v2.jsonl   # 轮 1
python corpus\shape_unseen.py          corpus\corpus_v2.jsonl   # 轮 2
python corpus\_diag_bilinear.py                                  # 轮 3
python corpus\_diag_para.py                                      # 轮 4

# 蒸馏与验证（需要 TYPESAFE_API_KEY）
python corpus\distill.py pilot
python corpus\distill.py generate 2000 400
python corpus\distill.py generate_clean
python corpus\distill_eval.py                                    # 轮 5（≈随机）
python corpus\contrastive2.py                                    # 轮 6（有效）
python corpus\scale_gen.py                                       # 扩到 200 个 criteria（20min / $0.22）
python corpus\scale_curve.py                                     # 轮 7（★ 结论已作废）
python corpus\capacity_sweep2.py                                 # 轮 8：容量/超参扫描
python corpus\best_config.py                                     # 轮 8：最优配置长训练
python corpus\scale_curve2.py                                    # 轮 8：修正后的数据曲线
python corpus\per_criteria.py                                    # 轮 8：广度 vs 深度
python corpus\breadth_gen.py                                     # 轮 9：新增 180 集合（$0.11）
python corpus\breadth_matched.py                                 # 轮 9：修正版（同分布）
python corpus\breadth2_gen.py                                    # 轮 9：180 个同维度新集合（$0.12）
python corpus\breadth2_eval.py                                   # 轮 9：180 vs 360 决定性验证
```

**环境**（实测）：Python 3.12.11 · torch 2.11.0+cu128 · transformers 4.57.6 · numpy 2.3.3 ·
fastapi 0.109.2 · pydantic 2.13.4 · httpx 0.28.1 · pytest 9.1.1 · Windows 11 · RTX 5090

**注意**：`numpy` 存在双安装（2.4.4 与 2.3.3），实验统一用 **2.3.3**。

---

## 立场

**证据优先级**：`S3 实测 > S0 SDK 源码 > S1 openapi.json > S2 散文/docstring`

本项目发现 **10 处**文档与实测不符，**全部以实测为准**。

**方法论上最重要的一条（单点定义原则）**：

> 公式 / 指标 / 口径 / 证据状态，**在仓库里只能有一处定义**，其他位置只引用 `claim_id`。
>
> 违反它的后果：两份实现各自漂移 · **AUROC 出现 0.085 分歧** · 把不可比的量合并 ·
> **同一条断言在不同位置漂移成不同状态**。
>
> **每个位置单独看都自洽，只有把同一条断言在多处复述时才会暴露。**

---

## 下一步（按证据排序）

**F1–F24 全部完成。** 剩余的是**已排除方向之外的新方向**与**明确的未测项**。

### 已排除（不要重试）

| 方向 | 结论 |
|---|---|
| 改网络形状（S2/S3/S4） | 差异 ≤0.02 |
| 全参数微调 | 冻结更多反而更好 |
| 单纯训练更久 | ep24 后收敛 |
| 每集合更多样本（>40） | Δ=+0.0011，CI 跨 0 |
| 语言特定训练 | EN = ZH |
| 按分布分别标定温度 | 比单一温度更差 |
| **编码器容量**（第 15 轮） | 4.25 倍大无显著改善 |
| **跨判据空间负例**（第 16/18 轮） | 等大对照证明是「池变大」而非「更难」 |
| **换训练目标**（第 19 轮） | BCE 四尺度 × 四尺子 16 格方向全负；交叉编码器成本高 14~29 倍且不优于双塔 |

### 仍值得做

1. **扩大 criteria 集合广度** —— 已实测 20→360 集合每个翻倍点均配对显著；$210 预算按此投入
2. **★ 造等级序标注** —— 第 19 轮确认语料里**没有**序数语义（choice 的 criteria 是 dict）。
   造出标注后才能做 ordinal 标定指标 / smooth_ece / TOST（§10.1 剩余两项之一）
3. **★ 解决权重分发** —— 单文件上限 100 MB，产物 91.9 MB + encoder 91.4 MB **不能放同一个 wheel**（第 24 轮）
4. **轮换 `TYPESAFE_API_KEY`** —— 唯一未做的运维项
5. **修复 `round16_ablation.py`** —— 它从未运行过；第 18 轮已用 nearsyn 的实现覆盖了这个方向，可考虑删除

**★ 已排除的方向（不要再试）**：架构形状（S2/S3/S4 差异 ≤0.02）· 全参数微调（冻结更多反而更好）· 单纯加长训练（ep24 后收敛）· 每集合加样本（>40 条收益归零）

**★ 已写好但【尚未运行】的脚本（诚实标注）**：

> **第 26 轮已把两个「写了但从未运行」的脚本裁定为【退役】**，
> 并把理由写进文件头 + 加了运行守卫（跑它会直接报错退出）：

| 脚本 | 裁定 | 理由 |
|---|---|---|
| `corpus/hardneg.py` | **退役** | ① 数据源 `scale_train.jsonl` **缺 instructions 的行 = 100%**（第 12 轮缺陷 #3）→ 训出的模型不读问题；② 扩负例池的方向已被第 16/18 轮覆盖，结论是**池变大本身有害** |
| `corpus/round16_ablation.py` | **退役** | 它要回答的问题已被 `round18_nearsyn.py` 用更干净的实现回答，且 round18 多了 **hard/far 逐集合等大对照**（证明「难度无关」） |
| `corpus/capacity_sweep.py` | **保留作留痕** | 首版容量扫描有**两个混淆**（freeze 与 epochs 未分离；bge-small 只有 4 层，freeze=4 会冻住全部层），结论已被 `capacity_sweep2.py` 取代 |

审计脚本：`corpus/audit_never_run.py` · 结果：`corpus/_never_run_audit.json`

> **★ 教训：「写了但没跑」等于没写**（verifier 第 18 轮的原话）。
> **但「跑了但数据有缺陷」比「没跑」更糟** —— 它会产生一个看起来可信的错结论。
> **所以退役时必须写清【为什么不需要跑】，而不只是删掉。**

**关于 $210 蒸馏预算（最终裁定）**：

> **✅ 应该花，全部花在【更多 criteria 集合】上。**
>
> - 每个 criteria 集合配 **约 40 条样本即可**（超过 40 收益归零，是浪费）
> - $210 ≈ 5M input token ≈ 约 **50 万个 (state, criteria) 对** → 约 **12,500 个 criteria 集合**（当前 180）
> - **已实测区间：集合数 20 → 360，准确率 0.5055 → 0.8020（+0.30）**，每个翻倍点都配对显著
>
> **★ 诚实边界**：360 之后仍是**外推**。边际增益衰减到 +0.017/翻倍，
> 若按此速率继续衰减，集合数再翻 5 倍（360 → ~11,500）的累计增益量级约 **+0.05**。

---

## 许可

未定。本仓库是个人学习性质的复刻项目。

**`legacy/`（原 `jev/`）保留不删** —— README 首段标注 `known_leak: true` 与 `question_channel_closed: true`。
**它是本项目最重要的反面教材：复刻了官方 API 的形状，但没有复刻能力。**
