Files
AIclinicalresearch/docs/03-业务模块/ASL-AI智能文献/04-开发计划/02-标题摘要初筛开发计划.md
HaHafeng 3634933ece refactor(asl): ASL frontend architecture refactoring with left navigation
- feat: Create ASLLayout component with 7-module left navigation
- feat: Implement Title Screening Settings page with optimized PICOS layout
- feat: Add placeholder pages for Workbench and Results
- fix: Fix nested routing structure for React Router v6
- fix: Resolve Spin component warning in MainLayout
- fix: Add QueryClientProvider to App.tsx
- style: Optimize PICOS form layout (P+I left, C+O+S right)
- style: Align Inclusion/Exclusion criteria side-by-side
- docs: Add architecture refactoring and routing fix reports

Ref: Week 2 Frontend Development
Scope: ASL module MVP - Title Abstract Screening
2025-11-18 21:51:51 +08:00

934 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 标题摘要初筛模块 - 详细开发计划MVP阶段
> **文档版本:** V3.0
> **创建日期:** 2025-11-16
> **开发周期:** 4 周
> **负责团队:** ASL 开发组
> **最后更新:** 2025-11-16
> **⭐ 重要基于真实架构Frontend-v2 + Backend增量演进 + asl_schema**
---
## 📋 模块概述
标题摘要初筛是 ASL 模块的第一个核心功能,也是 MVP 阶段的唯一交付功能。
### 功能范围
1. **设置与启动视图**PICO 标准展示、Excel 文献导入、启动筛选任务
2. **审核工作台视图**:双模型判断对比、冲突标记、人工复核
3. **初筛结果视图**统计概览、PRISMA 排除总结、结果导出
### 技术栈
| 层级 | 技术 | 说明 |
|------|------|------|
| **前端** | React 19 + TypeScript + Ant Design 5 + xlsx | Frontend-v2架构 |
| **后端** | Node.js + Fastify + TypeScript + Prisma | Backend/modules/asl/ |
| **LLM** | DeepSeek-V3 + Qwen3-72B | 复用 common/llm/adapters/ |
| **数据库** | PostgreSQL 15 (asl_schema) | Schema隔离 |
---
## 🏗️ 架构前提(已完成)
### ✅ Frontend-v2 架构Week 2 Day 6-7 完成)
```
frontend-v2/src/
├── framework/layout/
│ ├── MainLayout.tsx # ✅ 顶部导航布局
│ └── TopNavigation.tsx # ✅ 6个模块导航
├── framework/modules/
│ ├── moduleRegistry.ts # ✅ 模块注册中心
│ └── types.ts # ✅ ModuleDefinition接口
└── modules/asl/
└── index.tsx # 🚧 占位页面(待替换)
```
### ✅ Backend 架构Week 2 Day 8-9 完成)
```
backend/src/
├── common/llm/adapters/ # ✅ LLMFactory可复用
├── common/utils/jsonParser.js # ✅ JSON解析可复用
└── modules/
└── asl/ # 🚧 空目录(待创建)
```
### ✅ Database SchemaWeek 1 完成)
```prisma
// backend/prisma/schema.prisma
datasource db {
schemas = [
"asl_schema", # ✅ 已预留,待定义表结构
// ...其他9个Schema
]
}
```
---
## 🌥️ 云原生开发注意事项2025-11-16 新增)
> **⭐ 重要更新**:本模块开发需遵循阿里云 Serverless 部署架构要求
> **详细规范**[云原生开发规范](../../../04-开发规范/08-云原生开发规范.md)
> **部署指南**[云原生部署架构指南](../../../09-架构实施/03-云原生部署架构指南.md)
### 🎯 本地开发 + 云端部署双兼容策略
| 环境 | 存储方式 | 配置 | 说明 |
|------|---------|------|------|
| **本地开发** | LocalAdapter | `STORAGE_TYPE=local` | 文件存储到 `./uploads/` |
| **生产环境** | OSSAdapter | `STORAGE_TYPE=oss` | 文件存储到阿里云 OSS |
**核心原则**
-**Excel导入**:内存解析(`xlsx.read(buffer)`),不落盘
-**PDF上传**V1.0):使用 `StorageFactory`,本地/OSS自动切换
-**异步任务**LLM筛选任务必须异步处理> 10秒任务
-**环境变量**:所有配置从 `.env` 读取
-**数据库连接池**:使用全局 `prisma` 实例,不新建连接
### ❌ 禁止的做法
| 禁止操作 | 正确做法 | 原因 |
|---------|---------|------|
| `fs.writeFileSync('./temp.xlsx')` | `xlsx.read(buffer)` 内存解析 | Serverless容器重启丢失文件 |
| `new PrismaClient()` 每次新建连接 | 使用全局 `prisma` 实例 | 避免连接数暴增 |
| 硬编码 `apiKey = 'sk-xxx'` | `process.env.LLM_API_KEY` | 配置管理混乱 |
| 同步处理1000条文献筛选 | 异步任务 + 进度轮询 | 超过30秒超时限制 |
### ✅ MVP阶段开发检查清单
在提交代码前,请确认:
- [ ] Excel导入是否使用内存解析`xlsx.read(buffer)`
- [ ] 是否使用全局 `prisma` 实例(`import { prisma } from '@/config/database'`
- [ ] 是否所有配置都从环境变量读取?
- [ ] LLM筛选任务是否异步处理`POST /screening/start` 立即返回taskId
- [ ] 是否预留了 OSS 字段(`pdfUrl`, `pdfOssKey`, `pdfFileSize`
- [ ] 是否使用存储抽象层(`StorageFactory.create()`
**预留字段说明**
- MVP阶段仅做标题摘要筛选不处理PDF
- V1.0阶段实现全文PDF筛选时使用预留的OSS字段
---
## 📅 四周开发计划
```
Week 1: 数据库Schema + 后端API框架 + 存储抽象层
Week 2: LLM筛选核心 + 异步批处理逻辑
Week 3: 前端模块开发 + 审核工作台内存解析Excel
Week 4: 结果展示 + 导出 + 集成测试
```
---
## 🗓️ Week 1: 数据库Schema与后端API框架
### Day 1: Prisma Schema 设计
#### 任务1: 设计 asl_schema 表结构
**在 `backend/prisma/schema.prisma` 中添加:**
```prisma
// ==================== ASL 筛选项目表 ====================
model AslScreeningProject {
id String @id @default(uuid())
userId String @map("user_id")
user User @relation("AslProjects", fields: [userId], references: [id], onDelete: Cascade)
projectName String @map("project_name")
// PICO标准
picoCriteria Json @map("pico_criteria") // { population, intervention, comparison, outcome, studyDesign }
// 筛选标准
inclusionCriteria String @map("inclusion_criteria") @db.Text
exclusionCriteria String @map("exclusion_criteria") @db.Text
// 状态
status String @default("draft") // draft, screening, completed
// 筛选配置
screeningConfig Json? @map("screening_config") // { models: ["deepseek", "qwen"], temperature: 0 }
// 关联
literatures AslLiterature[]
screeningTasks AslScreeningTask[]
screeningResults AslScreeningResult[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("screening_projects")
@@schema("asl_schema")
@@index([userId])
@@index([status])
}
// ==================== ASL 文献条目表 ====================
model AslLiterature {
id String @id @default(uuid())
projectId String @map("project_id")
project AslScreeningProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
// 文献基本信息
pmid String?
title String @db.Text
abstract String @db.Text
authors String?
journal String?
publicationYear Int? @map("publication_year")
doi String?
// 云原生存储字段V1.0 阶段使用MVP阶段预留
pdfUrl String? @map("pdf_url") // PDF访问URL
pdfOssKey String? @map("pdf_oss_key") // OSS存储Key用于删除
pdfFileSize Int? @map("pdf_file_size") // 文件大小(字节)
// 关联
screeningResults AslScreeningResult[]
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("literatures")
@@schema("asl_schema")
@@index([projectId])
@@index([doi])
@@unique([projectId, pmid])
}
// ==================== ASL 筛选结果表 ====================
model AslScreeningResult {
id String @id @default(uuid())
projectId String @map("project_id")
project AslScreeningProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
literatureId String @map("literature_id")
literature AslLiterature @relation(fields: [literatureId], references: [id], onDelete: Cascade)
// DeepSeek模型判断
dsModelName String @map("ds_model_name") // "deepseek-chat"
dsPJudgment String? @map("ds_p_judgment") // "match" | "partial" | "mismatch"
dsIJudgment String? @map("ds_i_judgment")
dsCJudgment String? @map("ds_c_judgment")
dsSJudgment String? @map("ds_s_judgment")
dsConclusion String? @map("ds_conclusion") // "include" | "exclude" | "uncertain"
dsConfidence Float? @map("ds_confidence") // 0-1
// DeepSeek模型证据
dsPEvidence String? @map("ds_p_evidence") @db.Text
dsIEvidence String? @map("ds_i_evidence") @db.Text
dsCEvidence String? @map("ds_c_evidence") @db.Text
dsSEvidence String? @map("ds_s_evidence") @db.Text
dsReason String? @map("ds_reason") @db.Text
// Qwen模型判断
qwenModelName String @map("qwen_model_name") // "qwen-max"
qwenPJudgment String? @map("qwen_p_judgment")
qwenIJudgment String? @map("qwen_i_judgment")
qwenCJudgment String? @map("qwen_c_judgment")
qwenSJudgment String? @map("qwen_s_judgment")
qwenConclusion String? @map("qwen_conclusion")
qwenConfidence Float? @map("qwen_confidence")
// Qwen模型证据
qwenPEvidence String? @map("qwen_p_evidence") @db.Text
qwenIEvidence String? @map("qwen_i_evidence") @db.Text
qwenCEvidence String? @map("qwen_c_evidence") @db.Text
qwenSEvidence String? @map("qwen_s_evidence") @db.Text
qwenReason String? @map("qwen_reason") @db.Text
// 冲突状态
conflictStatus String @default("none") @map("conflict_status") // "none" | "conflict" | "resolved"
conflictFields Json? @map("conflict_fields") // ["P", "I", "conclusion"]
// 最终决策
finalDecision String? @map("final_decision") // "include" | "exclude" | "pending"
finalDecisionBy String? @map("final_decision_by") // userId
finalDecisionAt DateTime? @map("final_decision_at")
exclusionReason String? @map("exclusion_reason") @db.Text
// AI处理状态
aiProcessingStatus String @default("pending") @map("ai_processing_status") // "pending" | "processing" | "completed" | "failed"
aiProcessedAt DateTime? @map("ai_processed_at")
aiErrorMessage String? @map("ai_error_message") @db.Text
// 可追溯信息
promptVersion String @default("v1.0.0") @map("prompt_version")
rawOutput Json? @map("raw_output") // 原始LLM输出备份
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("screening_results")
@@schema("asl_schema")
@@index([projectId])
@@index([literatureId])
@@index([conflictStatus])
@@index([finalDecision])
@@unique([projectId, literatureId])
}
// ==================== ASL 筛选任务表 ====================
model AslScreeningTask {
id String @id @default(uuid())
projectId String @map("project_id")
project AslScreeningProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
taskType String @map("task_type") // "title_abstract" | "full_text"
status String @default("pending") // "pending" | "running" | "completed" | "failed"
// 进度统计
totalItems Int @map("total_items")
processedItems Int @default(0) @map("processed_items")
successItems Int @default(0) @map("success_items")
failedItems Int @default(0) @map("failed_items")
conflictItems Int @default(0) @map("conflict_items")
// 时间信息
startedAt DateTime? @map("started_at")
completedAt DateTime? @map("completed_at")
estimatedEndAt DateTime? @map("estimated_end_at")
// 错误信息
errorMessage String? @map("error_message") @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("screening_tasks")
@@schema("asl_schema")
@@index([projectId])
@@index([status])
}
// ==================== 用户表关联添加到User模型====================
// 在 platform_schema 的 User 模型中添加:
// aslProjects AslScreeningProject[] @relation("AslProjects")
```
**执行迁移:**
```bash
cd backend
npx prisma migrate dev --name add_asl_screening_tables
npx prisma generate
```
**验收标准**
- ✅ 数据库表创建成功4张表
- ✅ Prisma Client 生成成功
- ✅ 可查询 asl_schema 表
---
### Day 2: 后端目录结构创建
> **⭐ 前置条件2025-11-17 更新)**:平台基础设施已完成实施 ✅
> **完成状态**8个核心模块100%测试通过
> **完成报告**[平台基础设施实施完成报告](../../../08-项目管理/03-每周计划/2025-11-17-平台基础设施实施完成报告.md)
> **使用指南**[backend/src/common/README.md](../../../../backend/src/common/README.md)
#### 平台已提供的8个核心模块无需ASL模块实现
**平台基础设施路径**`backend/src/common/`
| # | 模块 | 使用方式 | 功能说明 |
|---|------|---------|---------|
| 1 | **存储服务** | `import { storage } from '@/common/storage'` | 文件上传下载(本地/OSS切换 |
| 2 | **日志系统** | `import { logger } from '@/common/logging'` | 结构化JSON日志 |
| 3 | **缓存服务** | `import { cache } from '@/common/cache'` | 内存/Redis缓存 |
| 4 | **异步任务** | `import { jobQueue } from '@/common/jobs'` | 长时间任务处理 |
| 5 | **健康检查** | `import { registerHealthRoutes } from '@/common/health'` | SAE健康检查 |
| 6 | **监控指标** | `import { Metrics } from '@/common/monitoring'` | 性能监控和告警 |
| 7 | **数据库连接池** | `import { prisma } from '@/config/database'` | 全局Prisma实例 |
| 8 | **环境配置** | `import { env } from '@/config/env'` | 统一配置管理 |
**存储服务使用示例**
```typescript
// ASL模块直接使用一行代码
import { storage } from '@/common/storage'
// 上传文件不关心本地还是OSS
const url = await storage.upload('asl/literature/123.pdf', pdfBuffer)
// 下载文件
const buffer = await storage.download('asl/literature/123.pdf')
// 删除文件
await storage.delete('asl/literature/123.pdf')
```
**支持的部署环境**
- ✅ 本地开发LocalAdapter文件存储到 `./uploads/`
- ✅ 云端SaaSOSSAdapter文件存储到阿里云OSS
- ✅ 私有化部署LocalAdapter文件存储到服务器
- ✅ 单机版LocalAdapter文件存储到用户本地
**环境切换**:修改一个环境变量即可
```bash
# 本地开发
STORAGE_TYPE=local
# 生产环境
STORAGE_TYPE=oss
```
**核心优势**
- ✅ ASL模块无需关心基础设施实现细节
- ✅ 代码零改动切换环境(本地 ↔ 云端)
- ✅ 所有业务模块AIA/PKB/DC等复用同一套基础设施
- ✅ 统一维护、统一升级、统一监控
---
#### 任务1: 创建 `backend/src/modules/asl/` 目录
```bash
cd backend/src/modules/asl
mkdir routes controllers services schemas types utils
touch routes/index.ts
touch controllers/projectController.ts
touch controllers/literatureController.ts
touch controllers/screeningController.ts
touch services/projectService.ts
touch services/literatureService.ts
touch services/llmScreeningService.ts
touch schemas/screening.schema.ts
touch types/screening.types.ts
```
#### 任务2: 创建路由文件
**`backend/src/modules/asl/routes/index.ts`**
```typescript
import { FastifyInstance } from 'fastify'
import * as projectController from '../controllers/projectController.js'
import * as literatureController from '../controllers/literatureController.js'
import * as screeningController from '../controllers/screeningController.js'
/**
* ASL 模块路由注册
*
* @description
* - 注册到 /api/v1/asl 前缀
* - 参考 legacy/routes/ 的风格
*
* @version Week 3 Day 2
*/
export async function aslRoutes(fastify: FastifyInstance) {
// 项目管理
fastify.post('/projects', projectController.createProject)
fastify.get('/projects', projectController.listProjects)
fastify.get('/projects/:projectId', projectController.getProject)
fastify.put('/projects/:projectId', projectController.updateProject)
fastify.delete('/projects/:projectId', projectController.deleteProject)
// 文献管理
fastify.post('/projects/:projectId/literatures/import', literatureController.importLiteratures)
fastify.get('/projects/:projectId/literatures', literatureController.listLiteratures)
// 筛选管理
fastify.post('/projects/:projectId/screening/start', screeningController.startScreening)
fastify.get('/projects/:projectId/screening/results', screeningController.getScreeningResults)
fastify.put('/screening/results/:resultId', screeningController.updateScreeningResult)
fastify.post('/screening/results/batch-update', screeningController.batchUpdateResults)
fastify.get('/screening/tasks/:taskId', screeningController.getTaskStatus)
fastify.get('/screening/tasks/:taskId/progress', screeningController.getTaskProgress)
}
```
**验收标准**
- ✅ 目录结构清晰
- ✅ 路由文件创建完成
-**可正常使用平台服务**
-`import { storage } from '@/common/storage'` 可用
-`import { logger } from '@/common/logging'` 可用
-`import { prisma } from '@/config/database'` 可用
-`import { jobQueue } from '@/common/jobs'` 可用
-`import { cache } from '@/common/cache'` 可用
---
### Day 3: 在 index.ts 中注册ASL路由
**`backend/src/index.ts`(修改):**
```typescript
// ============================================
// 【新架构】ASL 模块 - Week 3 新增
// ============================================
import { aslRoutes } from './modules/asl/routes/index.js';
// ... 其他代码
// 注册 ASL 模块路由
await fastify.register(aslRoutes, { prefix: '/api/v1/asl' });
console.log('✅ ASL 路由已注册到 /api/v1/asl/*');
```
**验收标准**
- ✅ 后端启动成功
- ✅ 访问 `http://localhost:3001/api/v1/asl/projects` 返回 200即使是空列表
---
## 🗓️ Week 2: LLM筛选核心
### Day 4-5: LLM筛选服务实现
#### 任务1: 定义 JSON Schema
**`backend/src/modules/asl/schemas/screening.schema.ts`**
```typescript
export const screeningOutputSchema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"required": ["decision", "reason", "confidence", "pico"],
"properties": {
"decision": {
"type": "string",
"enum": ["include", "exclude", "uncertain"]
},
"reason": {
"type": "string",
"minLength": 10,
"maxLength": 500
},
"confidence": {
"type": "number",
"minimum": 0,
"maximum": 1
},
"pico": {
"type": "object",
"required": ["population", "intervention", "comparison", "outcome"],
"properties": {
"population": {
"type": "string",
"enum": ["match", "partial", "mismatch"]
},
"intervention": {
"type": "string",
"enum": ["match", "partial", "mismatch"]
},
"comparison": {
"type": "string",
"enum": ["match", "partial", "mismatch", "not_applicable"]
},
"outcome": {
"type": "string",
"enum": ["match", "partial", "mismatch"]
}
}
},
"studyDesign": {
"type": "string",
"enum": ["RCT", "cohort", "case-control", "cross-sectional", "review", "other"]
},
"evidences": {
"type": "object",
"properties": {
"population": { "type": "string" },
"intervention": { "type": "string" },
"comparison": { "type": "string" },
"outcome": { "type": "string" }
}
}
}
};
```
#### 任务2: 创建提示词模板
**`backend/prompts/asl/screening/v1.0.0-basic.txt`**
```
你是一位医学文献筛选专家。请根据以下 PICO 标准判断这篇文献是否应该纳入系统评价。
# PICO 标准
- **Population (研究对象)**: {{population}}
- **Intervention (干预措施)**: {{intervention}}
- **Comparison (对照措施)**: {{comparison}}
- **Outcome (结局指标)**: {{outcome}}
- **Study Design (研究设计)**: {{studyDesign}}
# 纳入标准
{{inclusionCriteria}}
# 排除标准
{{exclusionCriteria}}
# 待筛选文献
**标题**: {{title}}
**摘要**: {{abstract}}
# 输出要求
请严格按照以下 JSON Schema 输出结果输出纯JSON不要包含任何其他文字
{
"decision": "include/exclude/uncertain",
"reason": "判断理由10-500字",
"confidence": 0.95,
"pico": {
"population": "match/partial/mismatch",
"intervention": "match/partial/mismatch",
"comparison": "match/partial/mismatch/not_applicable",
"outcome": "match/partial/mismatch"
},
"studyDesign": "RCT/cohort/case-control/cross-sectional/review/other",
"evidences": {
"population": "原文中的关键证据短语",
"intervention": "原文中的关键证据短语",
"comparison": "原文中的关键证据短语",
"outcome": "原文中的关键证据短语"
}
}
# 注意事项
1. decision 只能是 "include"(纳入)、"exclude"(排除)或 "uncertain"(不确定)
2. reason 必须具体说明判断依据
3. confidence 为 0-1 之间的数值
4. pico 字段逐项评估匹配程度
5. evidences 字段提取原文中的关键短语作为证据
```
#### 任务3: 实现 LLM 筛选服务
**`backend/src/modules/asl/services/llmScreeningService.ts`**
```typescript
import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js';
import { parseJSON } from '../../../common/utils/jsonParser.js';
import Ajv from 'ajv';
import { screeningOutputSchema } from '../schemas/screening.schema.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const ajv = new Ajv();
const validateSchema = ajv.compile(screeningOutputSchema);
/**
* LLM 筛选服务
*
* @description
* - 复用 common/llm/adapters/LLMFactory.ts
* - 双模型并行调用DeepSeek + Qwen
* - JSON Schema 验证
* - 冲突检测
*
* @version Week 3 Day 4-5
*/
class LLMScreeningService {
/**
* 双模型并行筛选
*/
async dualModelScreening(literature: any, protocol: any) {
// 构建提示词
const prompt = this.buildPrompt(literature, protocol);
// 并行调用两个模型
const [resultA, resultB] = await Promise.all([
this.callModel('deepseek', prompt),
this.callModel('qwen', prompt)
]);
// 解析JSON结果
const decisionA = await this.parseModelOutput(resultA.content, 'deepseek');
const decisionB = await this.parseModelOutput(resultB.content, 'qwen');
// 一致性判断
const { consensus, conflictFields } = this.compareDecisions(decisionA, decisionB);
// 自动分流
const needReview = this.shouldReview(consensus, decisionA, decisionB);
return {
consensus,
finalDecision: consensus === 'high' ? decisionA.decision : 'uncertain',
needReview,
conflictFields,
modelA: decisionA,
modelB: decisionB
};
}
/**
* 调用LLM模型复用common/llm
*/
private async callModel(modelName: string, prompt: string) {
const llm = LLMFactory.createLLM(modelName);
const response = await llm.chat({
messages: [
{ role: 'user', content: prompt }
],
temperature: 0, // 确定性输出
max_tokens: 1000
});
return response;
}
/**
* 构建提示词
*/
private buildPrompt(literature: any, protocol: any): string {
// 读取提示词模板
const templatePath = path.resolve(__dirname, '../../../../prompts/asl/screening/v1.0.0-basic.txt');
let template = fs.readFileSync(templatePath, 'utf-8');
// 替换变量
template = template.replace('{{population}}', protocol.picoCriteria.population);
template = template.replace('{{intervention}}', protocol.picoCriteria.intervention);
template = template.replace('{{comparison}}', protocol.picoCriteria.comparison);
template = template.replace('{{outcome}}', protocol.picoCriteria.outcome);
template = template.replace('{{studyDesign}}', protocol.picoCriteria.studyDesign);
template = template.replace('{{inclusionCriteria}}', protocol.inclusionCriteria);
template = template.replace('{{exclusionCriteria}}', protocol.exclusionCriteria);
template = template.replace('{{title}}', literature.title);
template = template.replace('{{abstract}}', literature.abstract);
return template;
}
/**
* 解析模型输出
*/
private async parseModelOutput(content: string, modelName: string) {
// 使用JSON解析器复用common/utils
const parsed = parseJSON(content);
// JSON Schema 验证
const valid = validateSchema(parsed);
if (!valid) {
console.error('JSON Schema验证失败', validateSchema.errors);
throw new Error(`模型${modelName}输出格式不符合Schema`);
}
return {
modelName,
decision: parsed.decision,
reason: parsed.reason,
confidence: parsed.confidence,
pico: parsed.pico,
evidences: parsed.evidences,
studyDesign: parsed.studyDesign
};
}
/**
* 对比两个模型的决策
*/
private compareDecisions(decisionA: any, decisionB: any) {
const conflicts: string[] = [];
// 比较最终决策
if (decisionA.decision !== decisionB.decision) {
conflicts.push('decision');
}
// 比较PICO各维度
if (decisionA.pico.population !== decisionB.pico.population) conflicts.push('P');
if (decisionA.pico.intervention !== decisionB.pico.intervention) conflicts.push('I');
if (decisionA.pico.comparison !== decisionB.pico.comparison) conflicts.push('C');
if (decisionA.pico.outcome !== decisionB.pico.outcome) conflicts.push('O');
const consensus = conflicts.length === 0 ? 'high' : 'conflict';
return { consensus, conflictFields: conflicts };
}
/**
* 自动分流规则
*/
private shouldReview(consensus: string, decisionA: any, decisionB: any): boolean {
// 规则1冲突 → 必须复核
if (consensus === 'conflict') {
return true;
}
// 规则2低置信度 → 需要复核
const avgConfidence = (decisionA.confidence + decisionB.confidence) / 2;
if (avgConfidence < 0.7) {
return true;
}
// 规则3高置信度 + 一致 → 自动通过
return false;
}
/**
* 批量筛选
*/
async batchScreening(literatures: any[], protocol: any, progressCallback?: (progress: number) => void) {
const batchSize = 15; // 每批15篇
const results = [];
for (let i = 0; i < literatures.length; i += batchSize) {
const batch = literatures.slice(i, i + batchSize);
// 并行处理当前批次
const batchResults = await Promise.all(
batch.map(lit => this.dualModelScreening(lit, protocol))
);
results.push(...batchResults);
// 推送进度
const progress = Math.round(((i + batch.length) / literatures.length) * 100);
progressCallback?.(progress);
}
return results;
}
}
export const llmScreeningService = new LLMScreeningService();
```
**验收标准**
- ✅ LLM双模型调用成功
- ✅ JSON Schema验证通过率 > 95%
- ✅ 冲突检测准确
---
## 🗓️ Week 3: 前端模块开发
### Day 6-7: 前端模块结构创建
#### 任务1: 更新模块定义
**`frontend-v2/src/modules/asl/index.tsx`(修改):**
```typescript
import { lazy } from 'react'
import { ModuleDefinition } from '@/framework/modules/types'
import { FileSearchOutlined } from '@ant-design/icons'
/**
* ASL 模块定义
*
* @description
* - 移除占位标记
* - 实现真实模块路由
*
* @version Week 3 Day 6
*/
const ASLModule: ModuleDefinition = {
id: 'literature-platform',
name: 'AI智能文献',
path: '/literature',
icon: FileSearchOutlined,
component: lazy(() => import('./routes')),
placeholder: false, // ✅ 改为 false
requiredVersion: 'advanced',
description: 'AI驱动的文献筛选和分析系统',
}
export default ASLModule
```
#### 任务2: 创建目录结构
```bash
cd frontend-v2/src/modules/asl
mkdir pages components api hooks types utils
touch routes.tsx
touch pages/ProjectList.tsx
touch pages/ScreeningSettings.tsx
touch pages/ScreeningWorkbench.tsx
touch pages/ScreeningResults.tsx
touch api/index.ts
```
#### 任务3: 实现路由配置
**`frontend-v2/src/modules/asl/routes.tsx`**
```typescript
import { lazy } from 'react'
import { Routes, Route, Navigate } from 'react-router-dom'
const ProjectList = lazy(() => import('./pages/ProjectList'))
const ScreeningSettings = lazy(() => import('./pages/ScreeningSettings'))
const ScreeningWorkbench = lazy(() => import('./pages/ScreeningWorkbench'))
const ScreeningResults = lazy(() => import('./pages/ScreeningResults'))
/**
* ASL 模块路由
*
* @description
* - /literature - 项目列表
* - /literature/project/:id/settings - 设置与启动
* - /literature/project/:id/workbench - 审核工作台
* - /literature/project/:id/results - 初筛结果
*
* @version Week 3 Day 6
*/
export default function ASLRoutes() {
return (
<Routes>
<Route index element={<ProjectList />} />
<Route path="project/:projectId">
<Route path="settings" element={<ScreeningSettings />} />
<Route path="workbench" element={<ScreeningWorkbench />} />
<Route path="results" element={<ScreeningResults />} />
</Route>
</Routes>
)
}
```
**验收标准**
- ✅ 顶部导航显示"AI智能文献"(不再是占位)
- ✅ 点击后进入项目列表页(即使是空列表)
---
### Day 8-10: 实现核心页面
(由于篇幅限制,核心实现代码请参考任务分解文档)
**验收标准**
- ✅ Excel上传功能正常
- ✅ 审核工作台可展示筛选结果
- ✅ 双视图模态框可弹出
---
## 🗓️ Week 4: 集成测试与验收
### Day 11-14: 端到端测试
(详细测试计划见任务分解文档)
**验收标准**
- ✅ 完整流程:上传 → 筛选 → 复核 → 导出
- ✅ 准确率 ≥ 85%
- ✅ 性能达标100篇 < 10分钟
---
## 📚 相关文档
- [开发里程碑](./01-开发里程碑.md)
- [任务分解Todo List](./03-任务分解.md)
- [质量保障策略](../02-技术设计/06-质量保障与可追溯策略.md)
- [技术选型](../02-技术设计/07-文献处理技术选型.md)
- [API设计规范](../02-技术设计/02-API设计规范.md)
- [前后端模块化架构设计-V2](../../../00-系统总体设计/前后端模块化架构设计-V2.md)
---
**更新日志**
- 2025-11-18: V3.1 更新补充平台基础设施完成状态8个核心模块
- 2025-11-16: V3.0 重写基于真实架构Frontend-v2 + Backend + asl_schema
- 2025-11-16: V2.0 重写,详细到每天的任务和代码示例
- 2025-10-29: V1.0 创建,初始版本