---
title: 从零搭建实验室文献调研工具链：一次五天的 multi-agent 工程复盘
author: 朱奕樟
date: 2026-05-17
summary: 围绕 XR-Scholar 分区数据构建的文献调研工具链 xrlit 完整工程复盘：22,299 期刊、298,875 篇论文、multi-agent 并行编排、6 处接口契约漂移踩坑实录。
canonical: https://blog.sysu-sam.com/@zhuyizhang/xrlit-literature-review-2026
---

# 从零搭建实验室文献调研工具链：一次五天的 multi-agent 工程复盘

> 起因是一句抱怨："跨出版社找论文太麻烦了，我还不知道哪些期刊跟我们实验室相关。"五天后，这句话变成了 22,299 个期刊、298,875 篇论文、毫秒级全文检索的工具链。本文记录整个过程，包括架构决策、踩坑实录和多 agent 编排的真实体感。

---

## 缘起：三个没人帮你解决的痛点

实验室做文献调研，卡在几个反复出现的问题上：

**痛点一：跨出版社找论文要开好几个标签页。** OpenAlex、Semantic Scholar、Google Scholar、arXiv……每个来源覆盖不同，没有人帮你把结果合并去重，更没有人告诉你哪篇值得优先读。

**痛点二：不知道哪些期刊跟实验室相关。** "智能制造"、"工业 AI"、"健康监测"……这些方向横跨机械、电气、计算机好几个学科，光是列出相关期刊清单就得花不少时间，更别说区分哪些是 Q1、哪些是 Q2。

**痛点三：不知道哪里有 Special Issue 投稿窗口。** 论文写完了，想找一个主题匹配、截止日期合适的 SI，需要手动去各出版社官网翻，还不一定找得全。

这三个问题的本质是：**没有一个以实验室视角组织的、可编程调用的文献基础设施。**

解决方案的起点是发现 [XR-Scholar 新锐期刊分区表](https://www.xr-scholar.com/)。它提供了一份权威的中文语境期刊分区数据（2026 年版），覆盖 22,299 个期刊，21 个大类，有完整的 ISSN 映射。这就是数据底座。

---

## 系统架构：四个子系统，一个数据底座

整个项目叫 `xrlit`，围绕 XR-Scholar 分区数据构建，对外暴露四个 CLI 子命令：

```
xrlit search   — 实时跨 6 出版社并行检索，XR 区位加权排序
xrlit harvest  — OpenAlex 批量收割实验室相关期刊近 5 年摘要入 SQLite
xrlit query    — 本地 FTS5 全文检索（毫秒级），支持区位/年份/lab_tier 过滤
xrlit cfp      — 抓 7 个出版社 + WikiCFP 的 SI/CFP，xAI Grok 兜底
```

目录结构：

```
literature-review/
├── xr-scholar/           # 分区数据
│   └── data/
│       ├── journals_2026_all.csv     # 22,299 期刊
│       ├── journals_2026_q12.csv     # 5,389 个 Q1/Q2
│       ├── lab_journals.csv          # 1,485 个实验室相关期刊（手工评分）
│       ├── lab_issns.json            # 2,628 ISSN/EISSN（收割 universe）
│       └── issn_lookup_2026.json     # 39,165 keys → {rank, top, cats}
├── xrlit/                # Python package
│   ├── rank.py           # ISSN → 区位 lookup
│   ├── adapters/         # 6 个出版社 adapter
│   ├── search.py         # 并行 fan-out + 去重 + 加权排序
│   ├── harvest.py        # OpenAlex 异步收割 + 断点续抓
│   ├── corpus.py         # FTS5 索引 + BM25 查询
│   └── cfp.py            # CFP 多源抓取
├── corpus/
│   └── papers.db         # SQLite WAL：papers + papers_fts + cfps + harvest_state
└── tests/                # pytest，含真实网络集成测试
```

---

## 数据底座：从 22,299 到 1,485

整个工具链的核心是三层数据漏斗：

| 层级 | 数量 | 来源 |
|------|------|------|
| XR-Scholar 全量期刊 | 22,299 | 抓取 XR-Scholar 2026 大类(ZKY) |
| Q1/Q2 期刊 | 5,389 | 过滤分区 |
| 实验室相关 Q1/Q2 | **1,485** | 用户手工评分（智能制造/工业 AI/自动化/仪器仪表/健康监测） |
| 收割 ISSN universe | **2,628** | lab_journals.csv 展开 ISSN + EISSN |

1,485 这个数字来自手工标注。给每个 Q1/Q2 期刊打分，判断它是否与实验室的研究方向相关，是个只有人能做的决策。这批 ISSN 构成了 `harvest` 的输入集合，也是 `query` 过滤时的 `lab_tier` 字段来源。

`issn_lookup_2026.json` 是检索时的核心查表结构：39,165 个 ISSN/EISSN 键，每个键对应 `{name, rank, top, cats}`，加载时用 `functools.lru_cache` 保证只读一次。

---

## 工程实践亮点

### 1. multi-agent 并行编排，不是单线程写代码

这个项目最值得记录的工程决策不是某个算法，而是开发模式本身。

整个项目由一个主 agent 负责规划和航向校准，具体编码全部委派给子 agent 执行，并且 executor 和 reviewer 同时运行、各自负责不同文件：

```
Phase 1 (检索): A1(executor) 写 xrlit/adapters/* + search.py
                A2(test-engineer) 同步写 tests/test_search.py
Phase 2 (收割): B1(executor) 写 harvest.py
                B2(test-engineer) 写 tests/test_harvest.py
Phase 2b (CFP): D1(executor) 写 cfp.py + cfp_adapters/*
                D2(test-engineer) 写 tests/test_cfp.py
Phase 3 (查询): C1(executor) 写 corpus.py + FTS5
                C2(reviewer) 构造 10 个手工 query 验证
```

每个 phase 的完成判据在 `PLAN.md` 里事先写好，不是"差不多了"，而是具体的命令和输出断言。比如 Phase 1 的判据是：

> `xrlit search "diffusion model" --limit 10 --jsonl` 输出 ≥ 8 条带 abstract、≥ 5 条带 xr_rank 非空。

这迫使 executor 和 reviewer 对接口契约有共同理解，而不是各自理解各自的。

### 2. 6 个 adapter 并行 fan-out + DOI 去重 + XR 加权排序

`search()` 的执行路径：

```python
async def search(query, *, limit=20, adapters=None, year_from=None,
                 only_with_abstract=False) -> list[Paper]:
    # 1) 并行 fan-out 到所有 adapter（asyncio.gather）
    # 2) DOI 规范化：strip https://doi.org/, doi:, 转小写
    # 3) 去重：优先 DOI；无 DOI 回退 (归一化标题 | 第一作者姓 | year)
    # 4) 合并：abstract 取最长；citations 取最大；extra.sources 累积
    # 5) 计算 score：
    #    title_match×15 + boost(rank) + log10(cites+1)×5
    #    + max(0, 5−(2026−year)) + abstract_kw_hits×5
    # 6) 排序，返回前 limit 条（每条带 xr_rank/xr_top/xr_cats）
```

`boost_score` 的分级：

| 分区 | 加分 |
|------|------|
| Top 期刊 | +30 |
| 1 区 | +20 |
| 2 区 | +10 |
| 3 区 | +3 |
| 4 区 | +1 |
| 未命中 | 0 |

xAI Grok 只在所有公开 adapter 返回空时才触发，作为最后兜底。要强制使用，传 `adapters=["xai_grok"]`。

### 3. OpenAlex 异步收割 + Crossref 降级 + 断点续抓

`harvest.py` 的设计核心是三个机制：

**并发控制**：8 个 ISSN 同时跑，每个 ISSN 内部 cursor 串行分页（per_page=200）。OpenAlex polite pool 要求带 `mailto=`，实测 ~140 papers/s 持续吞吐。

**断点续抓**：`harvest_state` 表记录每个 ISSN 的 cursor 和 `done` 状态。`--resume` 时跳过 `done=1` 的 ISSN，半途中断的 ISSN 从存储的 cursor 继续。

**降级策略**：单个 ISSN 连续 5 次 5xx → 切换到 Crossref（只有 meta，无 abstract）。两个来源都失败 → 写入 `corpus/harvest_failed_issns.json`，不阻塞其他 ISSN 继续跑。

自验证结果（5 ISSNs × 2024-2026 试跑）：
- 14.8 秒，2,066 行，abstract 覆盖率 60.8%
- Resume run：0.1 秒，0 重复

### 4. FTS5 + BM25 + 多维过滤

`corpus.py` 用 SQLite FTS5 虚拟表：

```sql
CREATE VIRTUAL TABLE papers_fts USING fts5(
  title, abstract,
  content='papers',
  content_rowid='rowid',
  tokenize='unicode61 remove_diacritics 2'
);
```

三个同步触发器（papers_ai / papers_ad / papers_au）保持主表与 FTS 索引的一致性。查询排序用 `ORDER BY bm25(papers_fts)`（值越小越相关）。

用户输入经过 FTS5 escape 处理，防止 operator injection：内部双引号转义后整体加引号包裹，然后拆词做 OR prefix 匹配。

过滤维度：`--rank 1,2`（XR 分区）、`--year-from`/`--year-to`、`--lab-tier Q1`、`--with-abstract`。

### 5. CFP 7 出版社 + WikiCFP + xAI 兜底

CFP 子系统覆盖：MDPI、Springer、Elsevier、Wiley、Frontiers、IEEE、WikiCFP，最后 xAI Grok 兜底（用 `"<journal> special issue 2026 call for papers"` 查询）。

出版社判定用两层逻辑：先从 OpenAlex `/sources/issn:` 拿 `host_organization_name`（有缓存）；拿不到则用期刊名后缀启发式映射。

CFP 数据存同一个 `papers.db`，表 `cfps`，查询支持按 `--rank`、`--deadline-after`、`--topic` 过滤。

---

## 踩坑实录

### 坑 1：MDPI Akamai 挡死了

MDPI 的 Special Issue 列表页被 Akamai WAF 守着，非住宅 IP 直接 403，换 User-Agent 没用，请求头改成浏览器字符串也没用。最终处理方式：测试里 `gracefully skip`，生产里降级到 xAI Grok 兜底。

这个坑的教训是：**不要假设官方 API 或公开页面一定可访问**。Akamai 的拦截逻辑不是基于请求内容，是基于 IP 信誉。

### 坑 2：xAI live search API 410 了

`IMPL_NOTES.md` 里有一行注释说得很清楚：

> **xAI live search deprecated (HTTP 410).** Removed `extra_body.search_parameters` from both adapters. Fallback now uses plain chat completion (model knowledge only). TODO: migrate to xAI Agent Tools API.

原来的方案是给 xAI API 的请求加 `search_parameters={"mode":"on"}`，通过 `extra_body` 传入，让模型能实时搜索网页。结果这个接口悄悄 deprecated 了，返回 410，没有任何预告。

现在 xAI adapter 退化成用模型知识兜底，而不是真正的实时检索。后续需要迁移到 xAI Agent Tools API。

### 坑 3：Semantic Scholar 429 限速

没有 API key 的情况下，Semantic Scholar 公共端点 1 req/s，稍微并发一点就 429。tenacity 5 次退避加到 5 秒能扛住，但冷跑时还是可能失败。

处理方式：标记为辅助源，测试里用 `skipif` 跳过空结果，不阻塞主流程。`errors.log` 里会有记录。

### 坑 4：估算偏差——以为 3-5 小时，实际用了 5 天

`PLAN.md` 里的原始估算：

> 收割 2,628 个 ISSN × 5 年，预估 ~1.5–2.5 M 论文，~3–5 小时。

实际用了约 5 天（当然不是连续 5 天跑，是开发加上调试加上几轮 harvest 的累计时间）。

为什么偏差这么大？

1. **接口契约漂移耗了一整天**（见下一个坑）
2. **harvest 的全量数据远比 pilot 复杂**：某些重量级期刊（比如 IEEE Access）一个 ISSN 就有数万篇，cursor 分页要跑很久
3. **CFP 子系统的反爬问题**：MDPI 403、xAI API 410，每个都要花时间调试和绕过
4. **FTS5 的 `_fts_escape` 函数写错了**：第一版是把整个查询串包在双引号里当 phrase 搜索，结果多词查询几乎没有召回。发现是在 Phase 4 harmonization 时，重写成分词 + OR prefix 匹配才修好

### 坑 5：executor 和 reviewer 接口契约漂移，6 处不匹配

这是最值得记录的工程教训。

Executor 和 reviewer 并行工作，接口契约写在 `AGENTS_BRIEF.md` 里。理论上他们对着同一份文档，应该不会有分歧。

实际上，Phase 4 harmonization 时发现了 **6 处接口不匹配**：

1. `harvest.run()` 原来只有 async 版本，测试期望的是 sync wrapper
2. `_connect()` 的默认 db path 在 def time 固化了，测试传入不同路径时无效
3. `corpus._fts_escape()` 的语义：executor 实现的是 phrase search，reviewer 测试期望的是多词 OR
4. `stats()` 的返回键：executor 用 `total_papers`，reviewer 测试读 `papers`；`abstract_coverage` 返回的是分数还是百分比
5. `cfp.refresh()` 的调用签名：async 还是 sync，`keyword=` 参数存不存在
6. `guess_publisher()` 的入参：dict 还是 kwargs，返回值大小写

这些不匹配在 Phase 4 之前都没有爆发，因为 executor 和 reviewer 各自跑各自的测试，没有真正集成。集成那一天，`pytest -v` 跑出来一堆红，逐条 debug 才发现根源是契约理解不一致。

修复方案：`IMPL_NOTES.md` 里记录了每处 fix，关键的是加了双向 alias（比如 `stats()` 同时返回 `total_papers` 和 `papers` 两个键），让新旧代码都能用。

---

## 当前数据快照

> 数据快照生成时间：2026-05-16T18:00:39Z

### 论文库

| 指标 | 数值 |
|------|------|
| 总论文数 | **298,875** |
| 含 abstract | **178,023**（59.6%） |
| ISSN 进度 | 87/95（91.6%） |

按年份分布：

| 年份 | 论文数 |
|------|--------|
| 2021 | 49,025 |
| 2022 | 51,527 |
| 2023 | 52,679 |
| 2024 | 58,548 |
| 2025 | 64,250 |
| 2026 | 22,846 |

按 XR 分区：

| 分区 | 论文数 |
|------|--------|
| 1 区 | 92,429 |
| 2 区 | 184,734 |
| 3 区 | 21,712 |

按 lab tier：

| tier | 论文数 |
|------|--------|
| Q1 | 92,157 |
| Q2 | 206,718 |

### CFP / Special Issues

- 总计 43 条（全部来自 WikiCFP）
- 仍开放（未截止）：32 条

---

## 如何使用

### 安装

```bash
cd /path/to/literature-review
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```

在 `.env` 里加入 `XAI_API_KEY`（用于 xAI Grok 兜底）。

### 实时跨源检索

```bash
# 基础检索
xrlit search "graph neural network for fault diagnosis" --limit 10

# 只要有摘要的、2023 年后的、输出 JSONL
xrlit search "structural health monitoring" --year-from 2023 --only-with-abstract --jsonl

# 只用特定 adapter
xrlit search "transformer architecture" --adapter openalex,europepmc
```

### 本地 FTS5 查询（毫秒级）

```bash
# 建索引（首次或数据更新后跑）
xrlit query reindex

# 查看数据库统计
xrlit query stats

# 全文检索，限 1 区 2 区，2023 年后
xrlit query search "digital twin" --rank 1,2 --year-from 2023 --top 20 --jsonl

# 只在 Q1 lab_tier 里查
xrlit query search "anomaly detection" --lab-tier Q1 --with-abstract --top 30
```

### 收割（一次性或增量）

```bash
# 试跑 50 个 ISSN
xrlit harvest run --limit-issns 50 --year-from 2024

# 全量收割（后台运行，可中断续抓）
xrlit harvest run --year-from 2021 --year-to 2026

# 查看进度
xrlit harvest status
```

### CFP / Special Issue

```bash
# 抓所有出版社
xrlit cfp refresh --subset lab

# 查 2026 年 6 月后截止、1/2 区、主题匹配的 SI
xrlit cfp list --rank 1,2 --deadline-after 2026-06-01 --topic "smart manufacturing"
```

### AI agent 集成示例

```python
import subprocess, json

def lit(cmd: str):
    out = subprocess.check_output(cmd, shell=True, text=True)
    return [json.loads(line) for line in out.strip().splitlines() if line]

# 实时跨源（最新+长尾话题）
realtime = lit(
    'xrlit search "transformer for time series" --limit 20 --jsonl --only-with-abstract'
)

# 本地高吞吐（实验室期刊 5 年全量）
local = lit(
    'xrlit query search "transformer for time series" --rank 1,2 --jsonl --top 50'
)

# 投稿窗口
cfps = lit(
    'xrlit cfp list --deadline-after 2026-06-01 --topic transformer --jsonl'
)

# 每条结果包含：
# title, abstract, authors, year, doi, venue/journal,
# xr_rank, xr_top, xr_cats, citations, oa_url, source
```

或者直接调 Python API：

```python
import asyncio
from xrlit.search import search
from xrlit.corpus import query as fts_query

papers = asyncio.run(search("CRISPR base editing", limit=15, year_from=2023))
local  = fts_query("structural health monitoring", rank=["1 区","2 区"], top=30)
```

---

## 测试覆盖

```bash
# 全套（含集成测试，需联网）
pytest -v

# 仅 unit（无网络）
pytest -m "not integration" -v
```

各模块通过情况（Phase 4 harmonization 后）：

| 测试文件 | 结果 |
|----------|------|
| test_rank.py | 11/11 |
| test_harvest_unit.py | 20/20 |
| test_harvest.py | 5/5（真实 OpenAlex） |
| test_corpus.py | 12/12 |
| test_cfp_unit.py | 6/6 |
| test_cfp.py | 5/5，1 skip（MDPI Akamai） |
| test_adapters.py | 27 pass，≤2 skip（SS 429） |
| test_search.py | 5/5 |
| test_cli.py | 3/3 |

---

## 反思：multi-agent 编排 vs 单 agent 各自的取舍

### multi-agent 编排的优势

**速度**：executor 和 reviewer 并行工作，Phase 1 的实现和测试几乎同时完成，而不是串行的"先写代码再写测试"。

**接口稳定性**：让 reviewer 事先写测试，迫使 executor 实现时对着测试调整，而不是实现完了再回头补测试。

**隔离性**：文件分工明确——executor 只动 `xrlit/**`，reviewer 只动 `tests/**`——冲突几乎为零。

### multi-agent 编排的代价

**契约漂移不可避免**：即使有 `AGENTS_BRIEF.md`，自然语言描述的接口总会被不同 agent 有不同解读。本次 6 处漂移全在 Phase 4 才暴露，说明"各自通过"不等于"集成通过"。

**修复漂移的成本比预期高**：需要主 agent 逐条 debug，理解两个 agent 各自的理解偏差，再写兼容 fix（双向 alias、sync wrapper 等）。这不是技术问题，是沟通问题。

**建议**：每个 phase 结束时加一个"dry-run integration"步骤——不是跑全套测试，只是把 executor 的实现和 reviewer 的测试放在同一个环境里跑一遍，提前暴露漂移，不要等到 Phase 4。

### 单 agent 的优势

单 agent 写代码和测试时对接口的理解是一致的，不会有"两个人读同一份文档、理解不同"的问题。对于较小的项目，这个优势很明显。

### 结论

对于这种规模（4 个子系统、6 个 adapter、3 个阶段并行）的项目，multi-agent 编排带来的速度收益超过了契约漂移的调试成本。但是**契约文档（`AGENTS_BRIEF.md`）的精确度要足够高**，特别是函数签名、返回类型、调用约定这些细节，不能用"大概是这样"的语气描述。

---

## 待办 / 已知限制

- xAI live search API 迁移到 xAI Agent Tools（当前 fallback 退化为模型知识）
- MDPI SI 数据目前通过 xAI 兜底，覆盖有限
- ~10–15% 的 OpenAlex works 无 DOI，当前直接丢弃（未来可用 `openalex:<id>` 做 synthetic key）
- Crossref 降级时无 abstract（JATS XML 解析复杂度高，暂时留空）
- harvest 全量（2,628 ISSN × 5 年）仍在后台运行，当前覆盖 91.6%

---

_项目地址：`/Volumes/D/code/literature-review`_  
_工具版本：xrlit 0.1.0，Python 3.11+，SQLite FTS5_
