# ASL 文献处理技术选型

> **文档版本：** V1.0  
> **创建日期：** 2025-11-15  
> **适用模块：** AI 智能文献（ASL）  
> **目标：** 定义初筛、全文复筛、全文提取的技术栈和实现路径

---

## 📋 文档概述

ASL 模块涉及三种不同的文献处理场景，每种场景有不同的技术特点和实现方案：

| 场景 | 输入格式 | 核心技术 | 主要挑战 |
|------|---------|---------|---------|
| **标题摘要初筛** | Excel 文件 | Excel 解析 + LLM 筛选 | 批量处理效率 |
| **全文复筛** | PDF 全文 | PDF 提取 + LLM 筛选 | PDF 解析准确率 |
| **全文数据提取** | PDF 全文 | PDF 提取 + LLM 结构化提取 | 表格、公式准确提取 |

---

## 🎯 技术架构总览

```
┌─────────────────────────────────────────────────────────┐
│                    ASL 文献处理流程                        │
└─────────────────────────────────────────────────────────┘
           │
           ├─ 场景 1: 标题摘要初筛
           │   └─ 用户上传 Excel → 解析 → LLM 批量筛选 → 导出结果
           │
           ├─ 场景 2: 全文复筛
           │   └─ 用户上传 PDF → PDF 提取 → LLM 筛选 → 复核
           │
           └─ 场景 3: 全文数据提取
               └─ PDF → 提取 + 结构化 → LLM 提取数据 → 人工复核

┌─────────────────────────────────────────────────────────┐
│               技术栈分层架构（共享）                        │
├─────────────────────────────────────────────────────────┤
│  前端层: React 19 + Ant Design 5 + xlsx/exceljs          │
├─────────────────────────────────────────────────────────┤
│  后端层: Node.js (Fastify) + TypeScript                  │
├─────────────────────────────────────────────────────────┤
│  文档处理层: Python 微服务 (extraction_service)           │
│    ├─ PyMuPDF: 快速 PDF 提取                             │
│    ├─ Nougat: 英文科学文献高质量提取 ⭐                   │
│    └─ Language Detector: 自动语言检测                     │
├─────────────────────────────────────────────────────────┤
│  LLM 层: DeepSeek-V3 + Qwen3 / GPT-5 + Claude-4.5        │
├─────────────────────────────────────────────────────────┤
│  数据库: PostgreSQL 15 (asl_schema)                      │
└─────────────────────────────────────────────────────────┘
```

---

## 📌 场景 1: 标题摘要初筛

### 1.1 技术特点

- **输入格式**: Excel 文件 (`.xlsx` / `.xls`)
- **数据规模**: 50-500 篇文献/批次
- **主要字段**: 标题、摘要、DOI、作者、发表年份、期刊
- **处理重点**: 批量高效处理，无需 PDF 解析

### 1.2 技术选型

#### 前端：Excel 上传与解析

| 技术 | 库 | 用途 | 优势 |
|------|-----|------|------|
| **Excel 上传** | `antd Upload` | 文件上传组件 | 拖拽上传、进度条 |
| **Excel 解析** | `xlsx` / `exceljs` | 前端解析 Excel | 纯前端处理，快速预览 |
| **模板验证** | 自定义逻辑 | 校验列名和数据格式 | 提前发现格式错误 |

**推荐方案：`xlsx` 库（SheetJS）**
- ✅ 支持 `.xlsx` 和 `.xls` 格式
- ✅ 纯 JavaScript，前端直接解析
- ✅ 体积小（~600KB），性能好
- ✅ 支持大文件（1000+ 行）

**代码示例：**
```typescript
import * as XLSX from 'xlsx';

function parseExcel(file: File): Promise<Literature[]> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    
    reader.onload = (e) => {
      try {
        const data = new Uint8Array(e.target.result as ArrayBuffer);
        const workbook = XLSX.read(data, { type: 'array' });
        
        // 读取第一个工作表
        const sheetName = workbook.SheetNames[0];
        const worksheet = workbook.Sheets[sheetName];
        
        // 转换为 JSON
        const jsonData = XLSX.utils.sheet_to_json(worksheet);
        
        // 映射为标准格式
        const literatures = jsonData.map((row: any) => ({
          title: row['Title'] || row['标题'],
          abstract: row['Abstract'] || row['摘要'],
          doi: row['DOI'],
          authors: row['Authors'] || row['作者'],
          year: row['Year'] || row['年份'],
          journal: row['Journal'] || row['期刊'],
        }));
        
        resolve(literatures);
      } catch (error) {
        reject(new Error('Excel 解析失败'));
      }
    };
    
    reader.onerror = () => reject(new Error('文件读取失败'));
    reader.readAsArrayBuffer(file);
  });
}
```

#### 后端：批量筛选处理

**处理流程：**
```
Excel 数据 → 批量分组（10-20 篇/组）→ 并行调用 LLM → 汇总结果
```

**关键技术点：**
1. **批量分组**：避免单次请求过大，10-20 篇/组最优
2. **并行处理**：使用 `Promise.all` 并行调用 LLM
3. **进度推送**：WebSocket 实时推送处理进度
4. **断点续传**：支持任务中断后继续

**代码示例：**
```typescript
async function batchScreening(
  literatures: Literature[],
  protocol: Protocol,
  progressCallback: (progress: number) => void
) {
  const batchSize = 15;
  const batches = chunk(literatures, batchSize);
  const results = [];
  
  for (let i = 0; i < batches.length; i++) {
    const batch = batches[i];
    
    // 并行处理当前批次
    const batchResults = await Promise.all(
      batch.map(lit => dualModelScreening(lit, protocol))
    );
    
    results.push(...batchResults);
    
    // 推送进度
    const progress = Math.round(((i + 1) / batches.length) * 100);
    progressCallback(progress);
  }
  
  return results;
}
```

### 1.3 数据流

```
用户操作             前端处理              后端处理            LLM 处理
   │                    │                     │                   │
   ├─ 上传 Excel        │                     │                   │
   │    └──────────────→│                     │                   │
   │                    ├─ 解析 Excel         │                   │
   │                    ├─ 验证格式           │                   │
   │                    ├─ 显示预览           │                   │
   │                    │                     │                   │
   │                    ├─ 提交筛选任务       │                   │
   │                    │    └───────────────→│                   │
   │                    │                     ├─ 保存任务         │
   │                    │                     ├─ 分组（15 篇/组） │
   │                    │                     │                   │
   │                    │                     ├─ 批次 1           │
   │                    │                     │    └──────────────→│
   │                    │                     │                   ├─ DeepSeek 筛选
   │                    │                     │                   ├─ Qwen3 筛选
   │                    │                     │                   ├─ 对比结果
   │                    │                     │    ←──────────────┘
   │                    │                     ├─ 保存结果         │
   │                    │                     │                   │
   │                    │                     ├─ 批次 2...        │
   │                    │                     │                   │
   │                    │    ←───────────────┤ 返回完整结果       │
   │    ←──────────────┤ 显示结果            │                   │
   └─ 人工复核          │                     │                   │
```

---

## 📌 场景 2 & 3: 全文复筛与数据提取

### 2.1 技术特点

- **输入格式**: PDF 文件（英文医学文献）
- **文件特点**: 
  - 科学论文格式（标题、摘要、引言、方法、结果、讨论、参考文献）
  - 包含复杂表格、公式、图表
  - 通常 10-30 页
- **处理重点**: 高准确率提取，保留结构和格式

### 2.2 技术选型：PDF 提取

#### 核心方案：Nougat + PyMuPDF 顺序降级策略 ⭐

**现有架构**（已实现，位于 `extraction_service/`）：

```python
# 顺序降级策略
def extract_pdf(file_path: str):
    # Step 1: 检测语言
    language = detect_language(file_path)
    
    # Step 2: 中文 PDF → PyMuPDF（快速）
    if language == 'chinese':
        return extract_pdf_pymupdf(file_path)
    
    # Step 3: 英文 PDF → 尝试 Nougat
    if check_nougat_available():
        result = extract_pdf_nougat(file_path)
        
        # 质量检查（阈值 0.7）
        if result['quality_score'] >= 0.7:
            return result  # ✅ Nougat 成功
    
    # Step 4: 降级到 PyMuPDF
    return extract_pdf_pymupdf(file_path)
```

#### 技术对比

| 方案 | 优势 | 劣势 | 适用场景 |
|------|------|------|---------|
| **Nougat** ⭐ | • 专为科学文献设计<br>• 公式、表格准确率高<br>• 输出 Markdown 格式<br>• 保留文档结构 | • 速度慢（1-2 分钟/20 页）<br>• 需要 GPU 加速<br>• 内存占用大（~4GB） | 英文医学文献全文提取 |
| **PyMuPDF** | • 速度快（秒级）<br>• 内存占用低<br>• 部署简单 | • 公式、表格易丢失<br>• 纯文本输出<br>• 布局易混乱 | 中文文献、快速预览 |
| **Adobe API** | • 商业级准确率<br>• 云端处理 | • 需付费<br>• 网络依赖<br>• 隐私风险 | 不推荐（成本高） |
| **Tesseract OCR** | • 开源免费<br>• 支持多语言 | • 需要图像预处理<br>• 准确率不稳定 | 扫描版 PDF（备选） |

**推荐方案：Nougat（主） + PyMuPDF（降级） ⭐**

#### Nougat 核心优势（医学文献场景）

```
✅ 专为科学文献设计
   ├─ 训练数据：arXiv 论文 + 科学期刊
   ├─ 公式识别：LaTeX 格式输出
   ├─ 表格保留：Markdown 表格格式
   └─ 结构化输出：章节、段落清晰

✅ 输出格式：Markdown
   ├─ 标题层级：# ## ###
   ├─ 表格：| Header | Data |
   ├─ 公式：$$ formula $$
   └─ 引用：[1] [2] [3]

✅ 质量评估机制
   ├─ 自动质量评分（0-1）
   ├─ 低质量自动降级 PyMuPDF
   └─ 保证提取成功率
```

#### 实现细节

**服务架构：**
```
Node.js Backend (Port 3001)
    │
    ├─ 调用 ExtractionClient.ts
    │   └─ HTTP 请求 → Python 微服务
    │
Python Extraction Service (Port 8000)
    │
    ├─ /api/extract/pdf
    │   ├─ detect_language()
    │   ├─ extract_pdf_nougat() → Nougat Model
    │   └─ extract_pdf_pymupdf() → PyMuPDF
    │
    └─ /api/health
        └─ 检查 Nougat 可用性
```

**Node.js 调用代码：**
```typescript
import { extractionClient } from '@common/document/ExtractionClient';

async function extractLiteraturePDF(file: Buffer, filename: string) {
  try {
    // 方法 1: 自动选择（推荐）
    const result = await extractionClient.extractPdf(
      file, 
      filename, 
      'auto'
    );
    
    // 方法 2: 强制使用 Nougat
    // const result = await extractionClient.extractPdf(file, filename, 'nougat');
    
    return {
      text: result.text,
      method: result.method,  // "nougat" | "pymupdf"
      quality: result.metadata.quality_score,
      pageCount: result.metadata.page_count,
      hasTables: result.metadata.has_tables,
      hasFormulas: result.metadata.has_formulas
    };
  } catch (error) {
    console.error('PDF extraction failed:', error);
    throw error;
  }
}
```

**Python 提取代码：**
```python
# extraction_service/services/nougat_extractor.py

def extract_pdf_nougat(file_path: str) -> Dict[str, Any]:
    """
    使用 Nougat 提取 PDF 文本
    
    命令行调用：
    nougat <pdf_path> -o <output_dir> --markdown --no-skipping
    """
    cmd = [
        'nougat',
        file_path,
        '-o', output_dir,
        '--markdown',      # 输出 Markdown 格式
        '--no-skipping'    # 不跳过任何页面
    ]
    
    # 执行 Nougat（超时 5 分钟）
    process = subprocess.Popen(cmd, ...)
    stdout, stderr = process.communicate(timeout=300)
    
    # 读取输出文件（.mmd）
    markdown_text = read_output_file()
    
    # 质量评估
    quality_score = evaluate_nougat_quality(markdown_text)
    
    return {
        "success": True,
        "method": "nougat",
        "text": markdown_text,
        "format": "markdown",
        "metadata": {
            "quality_score": quality_score,
            "has_tables": detect_tables(markdown_text),
            "has_formulas": detect_formulas(markdown_text)
        }
    }
```

### 2.3 文本后处理

**Nougat 输出优化：**
```typescript
function postProcessNougatOutput(markdown: string): ProcessedText {
  return {
    // 原始 Markdown
    raw: markdown,
    
    // 章节分割
    sections: extractSections(markdown),  // {abstract, methods, results, ...}
    
    // 表格提取
    tables: extractTables(markdown),
    
    // 公式提取
    formulas: extractFormulas(markdown),
    
    // 纯文本（去除格式）
    plainText: markdownToPlainText(markdown),
    
    // 结构化数据（用于 LLM）
    structured: {
      title: extractTitle(markdown),
      abstract: extractAbstract(markdown),
      methodology: extractMethodology(markdown),
      results: extractResults(markdown),
    }
  };
}
```

---

## 📌 场景 4: 文献下载（Unpaywall API）⭐

### 3.1 技术背景

**Unpaywall** 是一个免费的开放获取（Open Access）文献 API，可以：
- ✅ 通过 DOI 查询文献是否有免费全文
- ✅ 获取合法的 PDF 下载链接
- ✅ 完全免费，无需付费
- ✅ 数据库覆盖 3000+ 万篇文献

**官网**: https://unpaywall.org/products/api

### 3.2 技术选型

#### API 调用方式

**基础信息：**
- **API 端点**: `https://api.unpaywall.org/v2/{doi}?email={your_email}`
- **请求方法**: GET
- **认证方式**: 无需 API Key，仅需提供邮箱
- **速率限制**: 100,000 次/天（免费）

**示例请求：**
```bash
curl "https://api.unpaywall.org/v2/10.1038/nature12373?email=YOUR_EMAIL"
```

**响应示例：**
```json
{
  "doi": "10.1038/nature12373",
  "title": "The genome of the woodland strawberry",
  "is_oa": true,
  "oa_status": "gold",
  "best_oa_location": {
    "url": "https://www.nature.com/articles/nature12373.pdf",
    "url_for_pdf": "https://www.nature.com/articles/nature12373.pdf",
    "url_for_landing_page": "https://www.nature.com/articles/nature12373",
    "license": "cc-by",
    "version": "publishedVersion"
  },
  "oa_locations": [...]
}
```

#### Node.js 实现

**服务封装：**
```typescript
// backend/src/common/literature/UnpaywallClient.ts

import axios from 'axios';
import { config } from '../../config/env';

export interface UnpaywallResult {
  doi: string;
  title: string;
  isOA: boolean;              // 是否开放获取
  oaStatus: string;           // "gold" | "green" | "hybrid" | "bronze" | "closed"
  pdfUrl: string | null;      // PDF 下载链接
  landingPageUrl: string;     // 文献页面链接
  license: string | null;     // 许可协议
  version: string | null;     // "publishedVersion" | "acceptedVersion"
}

class UnpaywallClient {
  private baseUrl = 'https://api.unpaywall.org/v2';
  private email: string;

  constructor(email: string = config.unpaywallEmail) {
    this.email = email;
  }

  /**
   * 通过 DOI 查询文献信息
   */
  async getByDoi(doi: string): Promise<UnpaywallResult> {
    try {
      const url = `${this.baseUrl}/${doi}?email=${this.email}`;
      const response = await axios.get(url, {
        timeout: 10000,  // 10 秒超时
      });

      const data = response.data;

      // 获取最佳下载位置
      const bestOA = data.best_oa_location;

      return {
        doi: data.doi,
        title: data.title,
        isOA: data.is_oa,
        oaStatus: data.oa_status,
        pdfUrl: bestOA?.url_for_pdf || null,
        landingPageUrl: bestOA?.url_for_landing_page || data.doi_url,
        license: bestOA?.license || null,
        version: bestOA?.version || null,
      };
    } catch (error) {
      if (axios.isAxiosError(error)) {
        if (error.response?.status === 404) {
          throw new Error(`DOI not found: ${doi}`);
        }
      }
      throw new Error(`Unpaywall API error: ${error.message}`);
    }
  }

  /**
   * 批量查询（带速率限制）
   */
  async getBatch(dois: string[]): Promise<UnpaywallResult[]> {
    const results = [];
    
    for (const doi of dois) {
      try {
        const result = await this.getByDoi(doi);
        results.push(result);
        
        // 速率限制：100ms/请求
        await new Promise(resolve => setTimeout(resolve, 100));
      } catch (error) {
        console.error(`Failed to fetch ${doi}:`, error.message);
        results.push(null);  // 失败项标记为 null
      }
    }
    
    return results.filter(r => r !== null);
  }

  /**
   * 下载 PDF 文件
   */
  async downloadPdf(pdfUrl: string, outputPath: string): Promise<void> {
    try {
      const response = await axios.get(pdfUrl, {
        responseType: 'arraybuffer',
        timeout: 60000,  // 1 分钟超时
      });

      const fs = require('fs');
      fs.writeFileSync(outputPath, response.data);
    } catch (error) {
      throw new Error(`PDF download failed: ${error.message}`);
    }
  }
}

export const unpaywallClient = new UnpaywallClient();
```

**环境变量配置：**
```env
# .env
UNPAYWALL_EMAIL=your-email@example.com
```

#### 业务集成

**场景 1：批量检查文献是否可下载**
```typescript
async function checkLiteratureAvailability(literatures: Literature[]) {
  const dois = literatures
    .map(lit => lit.doi)
    .filter(doi => doi);  // 过滤空 DOI

  const results = await unpaywallClient.getBatch(dois);

  return literatures.map(lit => ({
    ...lit,
    downloadable: results.find(r => r.doi === lit.doi)?.isOA || false,
    pdfUrl: results.find(r => r.doi === lit.doi)?.pdfUrl || null,
  }));
}
```

**场景 2：用户点击下载全文**
```typescript
async function downloadLiteratureFullText(doi: string) {
  // Step 1: 查询 Unpaywall
  const unpaywallResult = await unpaywallClient.getByDoi(doi);

  if (!unpaywallResult.pdfUrl) {
    throw new Error('该文献无免费全文');
  }

  // Step 2: 下载 PDF
  const filename = `${doi.replace(/\//g, '_')}.pdf`;
  const outputPath = `./downloads/${filename}`;
  
  await unpaywallClient.downloadPdf(unpaywallResult.pdfUrl, outputPath);

  // Step 3: 提取文本（调用 extraction_service）
  const extractionResult = await extractionClient.extractPdf(
    fs.readFileSync(outputPath),
    filename,
    'auto'
  );

  return {
    pdfPath: outputPath,
    text: extractionResult.text,
    method: extractionResult.method,
  };
}
```

### 3.3 前端集成

**批量下载按钮：**
```typescript
// 批量检查可下载性
async function checkDownloadable(selectedRows: Literature[]) {
  setLoading(true);
  
  const results = await api.checkLiteratureAvailability(selectedRows);
  
  const downloadableCount = results.filter(r => r.downloadable).length;
  
  message.success(`发现 ${downloadableCount} 篇可下载全文`);
  setLiteratures(results);
  setLoading(false);
}

// 下载全文
async function downloadFullText(literature: Literature) {
  if (!literature.downloadable) {
    message.warning('该文献无免费全文');
    return;
  }

  try {
    const result = await api.downloadLiteratureFullText(literature.doi);
    message.success('下载成功');
    
    // 打开 PDF 查看器
    openPdfViewer(result.pdfPath);
  } catch (error) {
    message.error(`下载失败: ${error.message}`);
  }
}
```

---

## 🔍 补充技术点

### 4.1 您提到的技术点总结

| 技术点 | 状态 | 说明 |
|--------|------|------|
| ✅ Nougat 模型 | 已实现 | `extraction_service/services/nougat_extractor.py` |
| ✅ PyMuPDF | 已实现 | `extraction_service/services/pdf_extractor.py` |
| ✅ 顺序降级策略 | 已实现 | 英文→Nougat，中文→PyMuPDF |
| 🆕 Unpaywall API | 需新增 | 本文档提供实现方案 |
| ✅ Excel 解析 | 需新增 | 使用 `xlsx` 库（前端） |

### 4.2 可能遗漏的技术点 ⭐

#### （1）表格提取增强

**问题**：Nougat 虽然保留表格结构，但 LLM 直接处理 Markdown 表格可能不准确。

**解决方案：Table Transformer**
```python
# 使用微软的 Table Transformer 模型
# https://github.com/microsoft/table-transformer

from transformers import TableTransformerForObjectDetection
import torch

def extract_tables_enhanced(pdf_path: str):
    """
    使用 Table Transformer 精确定位表格
    """
    model = TableTransformerForObjectDetection.from_pretrained(
        "microsoft/table-transformer-detection"
    )
    
    # 检测表格位置
    tables = model.detect_tables(pdf_path)
    
    # 提取每个表格
    for table in tables:
        table_image = crop_table(pdf_path, table.bbox)
        table_data = ocr_table(table_image)
        
    return structured_tables
```

**优先级：V2.0**（MVP 阶段 Nougat 足够）

#### （2）引用解析与链接

**问题**：科学文献包含大量引用 `[1] [2] [3]`，需要解析并链接到参考文献。

**解决方案：GROBID**
```python
# GROBID: 开源科学文献解析工具
# https://github.com/kermitt2/grobid

import requests

def parse_references(pdf_path: str):
    """
    使用 GROBID 解析参考文献
    """
    with open(pdf_path, 'rb') as f:
        files = {'input': f}
        response = requests.post(
            'http://localhost:8070/api/processFulltextDocument',
            files=files
        )
    
    # 返回结构化的引用列表
    return response.json()['references']
```

**优先级：V2.0**（非核心功能）

#### （3）公式识别与渲染

**问题**：Nougat 输出 LaTeX 公式，前端需要渲染。

**解决方案：KaTeX / MathJax**
```typescript
// 前端渲染 LaTeX 公式
import katex from 'katex';
import 'katex/dist/katex.min.css';

function renderFormula(latex: string) {
  return katex.renderToString(latex, {
    throwOnError: false,
    displayMode: true,
  });
}
```

**优先级：MVP**（提升用户体验）

#### （4）PDF 预览与标注

**问题**：人工复核时需要查看原文，并高亮标注。

**解决方案：PDF.js + Annotator.js**
```typescript
// React 组件
import { Viewer } from '@react-pdf-viewer/core';
import '@react-pdf-viewer/core/lib/styles/index.css';

function PdfViewer({ pdfUrl, annotations }) {
  return (
    <Viewer
      fileUrl={pdfUrl}
      plugins={[
        highlightPlugin({
          highlights: annotations  // 高亮位置
        })
      ]}
    />
  );
}
```

**优先级：MVP**（核心功能）

#### （5）文献去重

**问题**：Excel 上传可能包含重复文献（同一篇文献不同版本）。

**解决方案：基于 DOI 和标题的去重**
```typescript
function deduplicateLiteratures(literatures: Literature[]) {
  const seen = new Set();
  
  return literatures.filter(lit => {
    // 优先使用 DOI
    if (lit.doi) {
      if (seen.has(lit.doi)) return false;
      seen.add(lit.doi);
      return true;
    }
    
    // 否则使用标题（标准化后）
    const normalizedTitle = normalizeTitle(lit.title);
    if (seen.has(normalizedTitle)) return false;
    seen.add(normalizedTitle);
    return true;
  });
}

function normalizeTitle(title: string): string {
  return title
    .toLowerCase()
    .replace(/[^\w\s]/g, '')  // 去除标点
    .replace(/\s+/g, ' ')      // 规范化空格
    .trim();
}
```

**优先级：MVP**（必须功能）

#### （6）文献元数据补全

**问题**：Excel 上传的数据可能不完整（缺 DOI、年份等）。

**解决方案：Crossref API**
```typescript
// 通过标题查询 DOI
async function enrichMetadata(literature: Literature) {
  if (literature.doi) return literature;  // 已有 DOI

  // 调用 Crossref API
  const response = await axios.get(
    `https://api.crossref.org/works?query.title=${literature.title}`
  );

  const match = response.data.message.items[0];
  
  return {
    ...literature,
    doi: match.DOI,
    year: match['published-print']?.['date-parts'][0][0],
    journal: match['container-title'][0],
  };
}
```

**优先级：V1.0**（增强功能）

#### （7）批处理进度持久化

**问题**：批量筛选耗时长（1000 篇 > 10 分钟），需支持断点续传。

**解决方案：Redis + 任务队列**
```typescript
// 使用 Bull 队列
import Queue from 'bull';

const screeningQueue = new Queue('literature-screening', {
  redis: { host: 'localhost', port: 6379 }
});

// 添加任务
screeningQueue.add({
  projectId: 'xxx',
  literatures: [...],
  protocol: {...}
});

// 处理任务
screeningQueue.process(async (job) => {
  const { projectId, literatures, protocol } = job.data;
  
  for (let i = 0; i < literatures.length; i++) {
    // 处理单篇文献
    await screenLiterature(literatures[i], protocol);
    
    // 更新进度
    job.progress((i + 1) / literatures.length * 100);
  }
});
```

**优先级：V1.0**（体验优化）

#### （8）错误处理与重试

**问题**：LLM 调用可能失败（网络、超时、限流）。

**解决方案：指数退避重试**
```typescript
async function retryWithBackoff<T>(
  fn: () => Promise<T>,
  maxRetries: number = 3
): Promise<T> {
  for (let i = 0; i < maxRetries; i++) {
    try {
      return await fn();
    } catch (error) {
      if (i === maxRetries - 1) throw error;
      
      // 指数退避：1s, 2s, 4s
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

**优先级：MVP**（必须功能）

---

## 📊 技术选型总结

### MVP 阶段必选技术

| 层级 | 技术 | 用途 |
|------|------|------|
| **前端** | `xlsx` | Excel 解析 |
| **前端** | `PDF.js` | PDF 预览 |
| **前端** | `KaTeX` | 公式渲染 |
| **后端** | `ExtractionClient` | 调用 Python 微服务 |
| **后端** | `UnpaywallClient` | 文献下载 |
| **Python** | `Nougat` | 英文 PDF 提取 |
| **Python** | `PyMuPDF` | 快速 PDF 提取 |
| **数据库** | `asl_schema` | 数据存储 |

### V1.0 增强技术

| 技术 | 用途 |
|------|------|
| Crossref API | 元数据补全 |
| Bull Queue | 任务队列 |
| Redis | 进度持久化 |

### V2.0 高级技术

| 技术 | 用途 |
|------|------|
| Table Transformer | 表格精确提取 |
| GROBID | 引用解析 |
| Semantic Scholar API | 学术图谱 |

---

## 📁 测试数据存放建议

根据 ASL 模块的文件夹结构，测试数据应该放在：

```
AIclinicalresearch/docs/03-业务模块/ASL-AI智能文献/
└── 05-测试文档/
    ├── 01-测试计划.md
    ├── 02-标题摘要初筛测试用例.md
    └── 03-测试数据/  ← 新建文件夹
        ├── README.md  ← 说明文档
        ├── screening-test-data/
        │   ├── literature-list-199.xlsx  ← 199 篇文献列表
        │   ├── picos-criteria.txt        ← PICOS 标准
        │   └── expected-results.json     ← 预期结果（金标准）
        ├── pdf-samples/
        │   ├── sample-rct-01.pdf
        │   ├── sample-cohort-01.pdf
        │   └── README.md
        └── extraction-test-data/
            └── README.md
```

**推荐结构：**
```
05-测试文档/
├── 01-测试计划.md
├── 02-标题摘要初筛测试用例.md
└── 03-测试数据/
    ├── README.md  ← 重要！说明测试数据来源、版权、使用方法
    ├── screening/
    │   ├── literature-list-199.xlsx
    │   ├── picos-criteria.txt
    │   ├── inclusion-criteria.txt
    │   ├── exclusion-criteria.txt
    │   └── gold-standard.json  ← 人工标注的正确答案
    └── pdf-extraction/
        ├── sample-01-high-quality.pdf
        ├── sample-02-with-tables.pdf
        └── sample-03-chinese.pdf
```

**README.md 示例：**
```markdown
# ASL 测试数据集

## 📋 数据说明

### 1. 标题摘要初筛测试数据
- **文件**: `literature-list-199.xlsx`
- **数量**: 199 篇英文医学文献
- **字段**: 标题、摘要、DOI、作者、年份、期刊
- **来源**: [描述数据来源]
- **版权**: [说明版权信息]

### 2. PICOS 标准
- **文件**: `picos-criteria.txt`
- **内容**: Population, Intervention, Comparison, Outcome, Study Design
- **纳入标准**: 5 条
- **排除标准**: 8 条

### 3. 金标准（人工标注结果）
- **文件**: `gold-standard.json`
- **标注人**: [标注专家信息]
- **标注时间**: [时间]
- **预期准确率**: ≥ 90%

## 🎯 使用方法

### 运行测试
```bash
npm run test:asl:screening
```

### 评估准确率
```bash
npm run test:asl:evaluate -- --gold-standard gold-standard.json
```

## 📊 预期结果
- 纳入: 45 篇
- 排除: 132 篇
- 不确定: 22 篇
```

---

## 📚 相关文档

- [质量保障与可追溯策略](./06-质量保障与可追溯策略.md)
- [数据库设计](./01-数据库设计.md)
- [API 设计规范](./02-API设计规范.md)
- [文档提取微服务](../../../../extraction_service/README.md)

---

**更新日志**：
- 2025-11-15: 创建文档，定义初筛、全文处理、文献下载技术选型



