# AIL-546 `search_person` 修改前后 Eval 调研

> 评测时间：2026-08-16（北京时间）  
> Before：PR #89 合并基线 `2530f171db4c3df03ff0cf3724ee78225a418878` / `irs-v1`  
> After：当前 `codex/ail-546-linear-minimal-v2` 工作树 / `irs-v2`  
> 网页版：[AIL-546 Before / After Eval Lab](./AIL-546-before-after-eval-study.html)  
> 机器结果：[AIL-546-before-after-eval-results.json](./AIL-546-before-after-eval-results.json)

## 结论

当前最小修改方向是成立的，但证据只支持一个有限结论：**`irs-v2` 修复了确定性 scorer 对历史职位、不可靠职位证据和“未知不等于冲突”的处理；现有 50+24 条目录还不能证明线上检索 Recall 或生产准确率。**

在 11 条由用户案例目录推导、带明确 provenance 的合成候选回放中：

| 指标 | 修改前 `irs-v1` | 修改后 `irs-v2` | 变化 |
|---|---:|---:|---:|
| Recall@3 | 100% | 100% | 0 pp |
| Top-1 | 37.5%（3/8） | 100%（8/8） | +62.5 pp |
| 应 abstain 的正确率 | 50%（2/4） | 100%（4/4） | +50 pp |
| 错误 `strong` | 2 | 0 | -2 |
| 非确定性排序 | 0 | 0 | 0 |
| 禁止字段泄漏 | 0 | 0 | 0 |

这不是一个可外推的线上 lift。11 条是专门覆盖本次修改机制的诊断集，目标是证明“代码改变了哪条规则”，不是估算真实用户流量分布。尤其 Recall@3 没有提升，因为 replay fixture 预先给了候选集合，只隔离测试 ranking/resolution；它没有调用 Provider。

因此建议：**保留当前生产修改，不继续调权重；下一步优先把 50+24 条目录编译成可执行 gold，而不是继续扩展 scorer。**

## 1. 证据边界与可复现方法

### 1.1 同一数据、两份真实代码

比较 runner 没有复制一份“近似旧逻辑”：

1. 用 `git show 2530f17:src/ailoha_agent/agent/tools/person_identity_score.py` 读取修改前实际源码；
2. 从当前工作树导入修改后源码；
3. 对两者输入完全相同的 clue 和 candidate snapshot；
4. 保存输入 fixture hash、两份 scorer source hash、逐 case 排序和解释字段；
5. 修改后契约失败、错误 strong、非确定性或禁止字段任一非零，Eval 即失败。

可追溯标识：

| 对象 | 标识 |
|---|---|
| Before Git revision | `2530f171db4c3df03ff0cf3724ee78225a418878` |
| Before scorer SHA-256 | `769cd78faa554024457661ec368882ae3340455354e0beef4cce46f8fa23416d` |
| After scorer SHA-256 | `aeca9b4aeb1546f2ae1fceeef6450dea271d1f809defe705686679b7c8eb28c2` |
| Replay fixture SHA-256 | `00b7e14ca147e919927578c763901627e01e80ede9ca80353941536e25a08dd1` |
| 50 条公开目录 SHA-256 | `354a6ba43504d56b49bfec7bec86ca473a19577934bdd4e640a91e7d6d0d08fc` |
| 24 条真实目录 SHA-256 | `9842e275d0355238cd2661580b65a2be85e3b0f5e6aa1983f8b3a254f853f3c5` |

### 1.2 指标定义

- `Recall@3`：有 target 的 case 中，target 是否出现在给定候选集合前三。它不等于 Provider retrieval Recall。
- `Top-1`：有 target 的 case 中，scorer 是否把 target 排第一。
- `abstain accuracy`：金标要求不强确认时，第一名是否没有被判为 `strong`。
- `false strong`：没有可确认 target，却把第一名判为 `strong`。
- `deterministic`：同一输入连续运行两次，公开输出必须完全相同。
- `forbidden output`：输出不得含联系方式、Provider 内部 ID、identity/merge 状态。

### 1.3 这次 Eval 能与不能证明什么

| 可以证明 | 不能证明 |
|---|---|
| 同一候选集上的确定性排序变化 | 线上 Provider 是否召回真人 |
| current / historical clue 是否按时间作用 | 50 个公开人物的生产 Recall@3 |
| 不可靠 evidence 是否仍会制造 `strong` | LLM 是否正确决定调用或不调用工具 |
| 未知 current role 是否被显式保留 | 公开页面是否仍是 2026 当前事实 |
| 输出是否确定、是否含禁止字段 | 延迟、成本、rate limit 和生产流量分布 |

## 2. 输入数据成熟度审计

### 2.1 50 条公开人物目录

源数据：[ail_546_public_case_catalog.md](./ail_546_public_case_catalog.md)

- 45 条人物检索/时效 case；
- 5 条应追问或不应搜索的 routing case；
- 具备姓名、难点、预期动作和一部分公开来源；
- **scorer-ready：0/50**，因为没有冻结的 Provider candidate snapshot、gold target/ref 和 snapshot hash；
- `P32/P33/P37/P39` 是 rolling freshness，不应进入永久静态 holdout。

其中 `R46–R50` 已有完整输入句子和预期动作，可以先进入 Agent routing Eval；但不能塞进 identity scorer 计算 Top-1，因为正确行为可能是完全不调用 `search_person`。

### 2.2 24 条真实任务目录

源数据：[ail_546_real_case_catalog_summary.json](./ail_546_real_case_catalog_summary.json)

| 成熟度 | 数量 | 当前可做什么 |
|---|---:|---|
| `can_label_now` / `can_label_safety_now` | 9 | 路由、安全与禁止副作用 Eval |
| strong identity/correction gold candidate | 6 | 补齐最小输入与候选快照后进入 blind adjudication |
| needs adjudication / segmentation | 8 | 先拆 case 或裁决 claim/source，不可直接算准确率 |
| baseline-only | 1 | 只做定性旧体验，不进入 ranking 指标 |
| scorer-ready | 0 | 当前描述没有 executable clues、candidate snapshots、target refs |

这批真实数据还有两个采样偏差必须进入调研结论：

1. Dashboard 报告 368 条，但严格 30 天只有 317 条，泄漏 51 条窗口外数据。若不在 compiler 再做时间窗校验，benchmark population 本身不可信。
2. 70 条人物候选任务里只有 40 条 bundle 可读，30 条不可读，缺失率 42.9%。可读样本可能系统性偏向成功、较新或结构完整任务，不能把 40 条当作无偏真实分布。

### 2.3 本轮为何只构造 11 条 replay

11 条 fixture 都标记为 `synthetic_candidate_replay`，由目录描述最小化转写，只覆盖当前 scorer 真正拥有的决策：时间语义、alias、同名排序、证据可靠性和 abstention。没有把 routing、Contact mutation、敏感关系推断或多 Provider recovery 强行折叠成 scorer case。

这种克制避免了两类伪指标：

- 用人工编造候选集合声称“线上 Recall@3=100%”；
- 用搜索排序分数声称“Contact 污染已经被端到端解决”。

## 3. 维度一：修改前代码与效果

修改前 `irs-v1` 的核心模型只有扁平的 `candidate.company/title`，默认这些字段是 current 且可靠。它不知道：

- 用户 clue 指的是历史公司还是当前公司；
- top-level `position` 是结构化事实还是模糊 headline 推断；
- 缺失字段是未知，还是候选与 clue 冲突；
- stale LinkedIn 与更新的其它来源应如何分层。

这产生三类可溯源失败：

1. **历史线索被当成当前线索**：P31、P35、REAL-007、REAL-013 的目标人物已经换岗，旧版把仍在旧机构的同名 distractor 排第一。
2. **stale top-level 字段压过更新职位**：REAL-014 的目标 candidate 有正确 current position，但旧版只读扁平旧公司，错误 candidate 排第一。
3. **弱推断制造 false strong**：REAL-010 的项目协作被当成任职，REAL-011 的“可能离职”被当成 current title；旧版均得到 80 分 `strong`。

旧版也有两点保持正确：

- R46 同名、线索不足时会 abstain；
- REAL-006 verified alias + 公司/职位组合能把正确 candidate 排第一。

这说明问题不是整个 scorer 无效，而是它缺少时间和 evidence-quality 两个必要维度。

## 4. 维度二：修改后代码与效果

`irs-v2` 没有换模型或扩大 Provider 调用，而是在同一确定性边界增加最小语义：

- clue 增加 `company_time/title_time = current|historical|any`；
- candidate 分为 `current_positions` 与 `historical_positions`；
- position 字段带 `company_reliable/title_reliable`；
- 缺失或不完整 evidence 进入 `unknown_signals`，不直接成为 hard conflict；
- RRF/retrieval score 仍只做同分排序，不成为身份事实。

逐 case 结果：

| Case | 修改前 | 修改后 | 变化机制 |
|---|---|---|---|
| P31 | DeepMind distractor，strong | Microsoft AI target，strong | historical company/title 命中目标履历 |
| P35 | Google-current distractor，strong | U of T target，strong | former employer 不再要求候选仍在 Google |
| REAL-007 | KTB-current distractor，plausible | current fund target，plausible | alias 与历史 KTB 证据组合 |
| REAL-013 | former-company distractor，strong | 已离职 target，strong | former/current 分层 |
| REAL-014 | stale distractor，weak | 新 current role target，strong | current position 投影覆盖 stale top-level 字段 |
| REAL-010 | 未验证 VAST 任职，strong | 同候选 plausible/ask | unverified evidence 不计 supporting anchor |
| REAL-011 | “Departing” rumor，strong | plausible/ask | 不可靠 title 不再制造 strong |
| P15 | weak，无未知解释 | weak，显式 current company/title unknown | 未知不等于冲突 |
| P41 | 正确 target | 正确 target | 保持同名消歧，无回归 |
| R46 | abstain | abstain | 保持线索不足时不强认人 |
| REAL-006 | alias target plausible | alias target plausible | 保持中英文 alias 行为 |

当前结果中 Top-1 由 3/8 升至 8/8，来自 5 条时间语义 case 被纠正；abstain 由 2/4 升至 4/4，来自 2 条不可靠 evidence 不再 `strong`。没有通过改变阈值“整体压低分数”，因为 P31/P35/REAL-013/REAL-014 仍能在两条可靠 anchor 下得到 strong。

## 5. 维度三：建议、挖掘与元层发现

### P0：先做 eval-case compiler，不继续调 scorer 权重

每条可发布 case 至少冻结：

```json
{
  "case_id": "stable id",
  "observed_at": "timestamp with timezone",
  "input_clues": {"name": "...", "company": "...", "company_time": "historical"},
  "candidate_snapshot": [{"candidate_ref": "opaque", "retrieved_at": "..."}],
  "snapshot_sha256": "...",
  "gold_target": "candidate_ref | target_absent | unresolved",
  "expected_action": "confirm | ask | gather | no_search",
  "claim_scope": "identity | current_role | routing | mutation_safety",
  "source_tier": "first_party | provider | public_web | user_report",
  "adjudication": {"status": "double_labeled", "owner": "..."},
  "privacy_check": {"passed": true}
}
```

没有 candidate snapshot 的 case 只能进入 catalog readiness 报告，不能计算 retrieval/ranking 指标。没有独立 gold owner 的 case 只能 shadow，不能成为 CI gate。

### P0：拆成五条 Eval lane

| Lane | 数据 | 指标 | Owner |
|---|---|---|---|
| Routing | R46–R50 + 真实 deterministic negatives | search call count、no-search precision、ask recall | Agent/prompt |
| Retrieval | 冻结 Provider snapshot 或受控 live shadow | Recall@3、empty/failed 分离、attempts、cost、latency | workflow/client |
| Ranking | 当前 replay + 人工 gold candidate set | Top-1、MRR、pairwise、false strong | scorer |
| Temporal | P32/P33/P37/P39 rolling suite | current-role accuracy、source age、conflict disclosure | evidence pipeline |
| Mutation safety | REAL-003/010/012/017/018/020–022 | auto-write=0、wrong-link=0、correction/revocation | Contact state machine |

不要用一个“总体准确率”混合这五层；否则改进 routing 可能掩盖 ranking 回归，或正确搜索掩盖错误 Contact 写入。

### P1：给 clue 增加 provenance/reliability，而不是继续只增强 candidate

当前 `irs-v2` 能表达 candidate position 是否可靠，但 `IdentityClues` 仍只有值和时间范围，没有“这是用户确认、截图 rumor、模型推断还是第一方来源”。REAL-011 的生产风险只有在上游把 rumor 映射为不可靠 evidence 时才能真正关闭。

建议在有真实 gold 后，最小增加 clue provenance（而不是立刻再改分数）：

- `source_kind`: user_confirmed / first_party / provider / inferred / rumor；
- `observed_at`；
- `reliability`: verified / unverified / unknown；
- scorer 只允许 verified anchor 促成 `strong`。

本轮不实现它，因为现有 24 条目录没有 executable clue provenance；现在编码会把假设固化成接口。

### P1：把 correction 当成 claim 撤销，不是搜索重跑

REAL-010 和 REAL-018 的核心不是“再搜准一点”，而是旧 claim 已污染 Contact。`search_person` 不应拥有修复长期状态的副作用。需要在 Contact claim 层记录来源、版本和撤销关系，使用户纠正能够：

1. 撤销错误 claim；
2. 保留纠正来源；
3. 重新打开 identity confirmation；
4. 不通过追加 note 掩盖旧事实。

这应另立任务，不能借 AIL-546 扩大修改面。

### P1：修复数据抽样窗口与 missingness，再看生产指标

- compiler 必须二次校验 `created_at` 是否落在严格窗口，拒绝 dashboard 泄漏行；
- 对 30 个 unreadable bundle 保存 failure taxonomy，而不是直接丢弃；
- 报告 readable 与 unreadable 的比例，并对来源/时间/任务类型分层；
- hidden holdout 与调参集按 identity/case family 分组切分，避免同一人物 alias 泄漏到两边。

### P2：统计解释要匹配样本量

8/8 Top-1 与 4/4 abstain 的点估计都是 100%，但样本很小；即使把它们当独立随机样本，95% 区间也很宽。更重要的是，这 11 条是针对已知机制的诊断样本，不是随机 production sample。因此网页和 Markdown 均保留分母，不只展示百分比。

### 暂不建议

- 不增加 LLM reranker、向量库或更多 Provider；
- 不因 11 条全过就调整 strong 阈值；
- 不把 50 个名人目录直接包装成“线上准确率 benchmark”；
- 不让 ranking Eval 覆盖 routing、Contact mutation 或敏感关系安全；
- 不把 rolling freshness case 固化成永久 current-role 字符串。

## 6. 最小修改建议清单

本轮已经完成且建议保留：

1. 新增只读 comparison runner，从 Git revision 加载真实 before scorer；
2. 新增 11 条带 catalog ref 的合成 replay，不改生产逻辑；
3. 原样保存 50+24 条用户输入，并用 SHA-256 校验；
4. 自动报告 catalog readiness，禁止把缺字段目录计作 scorer gold；
5. 保存机器结果、Markdown 和网页三种投影，唯一事实源仍是 fixture + runner。

下一轮最小动作：先由人工给 6 条 strong gold candidate 补齐脱敏 `input_clues + candidate_snapshot + target_ref`，然后原样接入 runner；只有新增真实 case 失败时，才讨论生产代码修改。

## 7. 复现

```bash
# 修改前/后对比（无网络、无 Provider 费用）
PYTHONPATH=src .venv/bin/python -m evals.person_search_comparison --json

# 当前 scorer 原有回归 gate
PYTHONPATH=src .venv/bin/python -m evals.person_search --json

# comparison 自身契约
PYTHONPATH=src .venv/bin/pytest -q tests/test_person_search_comparison_eval.py
```

机器结果里的每条 case 都包含 before/after 的 top ref、score、match level、matched/conflict/unknown signals、排序列表和 contract 状态，可从网页下钻，也可直接 diff JSON。

## 8. 证据索引

- 用户提供的公开目录原样副本：[ail_546_public_case_catalog.md](./ail_546_public_case_catalog.md)
- 真实目录聚合摘要（公开脱敏）：[ail_546_real_case_catalog_summary.json](./ail_546_real_case_catalog_summary.json)
- [Replay 隐私与复现说明](./privacy-and-provenance.md)
- [本地 runner / 测试复现边界](./privacy-and-provenance.md)
- 机器结果：[AIL-546-before-after-eval-results.json](./AIL-546-before-after-eval-results.json)
- 可视化网页：[AIL-546-before-after-eval-study.html](./AIL-546-before-after-eval-study.html)
