- Create platform infrastructure planning core document (766 lines) - Update architecture design to support cloud-native deployment - Update development specs and operations documentation - Simplify ASL module docs by removing duplicate implementations New Documents: - Platform Infrastructure Planning (04-骞冲彴鍩虹璁炬柦瑙勫垝.md) - Cloud-Native Development Standards (08-浜戝師鐢熷紑鍙戣鑼?md) - Git Commit Standards (06-Git鎻愪氦瑙勮寖.md) - Cloud-Native Deployment Guide (03-浜戝師鐢熼儴缃叉灦鏋勬寚鍗?md) - Daily Summary (2025-11-16 work summary) Updated Documents (11 files): - System architecture design docs (3 files) - Implementation and standards docs (4 files) - Operations documentation (1 file) - ASL module planning docs (3 files) Key Achievements: - Platform-level infrastructure architecture established - Zero-code switching between local/cloud environments - 100% support for 4 PRD deployment modes - Support for modular product combinations - 99% efficiency improvement for module development - Net +1426 lines of quality documentation Implementation: 2.5 days (20 hours) for 8 infrastructure modules
1027 lines
30 KiB
Markdown
1027 lines
30 KiB
Markdown
# 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: 创建文档,定义初筛、全文处理、文献下载技术选型
|
||
|
||
|
||
|