# 标题摘要初筛模块 - 详细开发计划(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 Schema(Week 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/`) - ✅ 云端SaaS:OSSAdapter(文件存储到阿里云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 ( } /> } /> } /> } /> ) } ``` **验收标准**: - ✅ 顶部导航显示"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 创建,初始版本