Files
AIclinicalresearch/docs/03-业务模块/ASL-AI智能文献/04-开发计划/02-标题摘要初筛开发计划.md
HaHafeng 1b53ab9d52 feat(aia): Complete AIA V2.0 with universal streaming capabilities
Major Changes:
- Add StreamingService with OpenAI Compatible format
- Upgrade Chat component V2 with Ant Design X integration
- Implement AIA module with 12 intelligent agents
- Update API routes to unified /api/v1 prefix
- Update system documentation

Backend (~1300 lines):
- common/streaming: OpenAI Compatible adapter
- modules/aia: 12 agents, conversation service, streaming integration
- Update route versions (RVW, PKB to v1)

Frontend (~3500 lines):
- modules/aia: AgentHub + ChatWorkspace (100% prototype restoration)
- shared/Chat: AIStreamChat, ThinkingBlock, useAIStream Hook
- Update API endpoints to v1

Documentation:
- AIA module status guide
- Universal capabilities catalog
- System overview updates
- All module documentation sync

Tested: Stream response verified, authentication working
Status: AIA V2.0 core completed (85%)
2026-01-14 19:15:01 +08:00

30 KiB
Raw Blame History

标题æ˜è¦<EFBFBD>åˆ<EFBFBD>ç­æ¨¡å<EFBFBD>— - 详细开å<E282AC>计åˆï¼ˆMVP阶段ï¼?

*文档版本� V3.0
*创建日期� 2025-11-16
å¼€å<EFBFBD>卿œŸï¼š 4 å‘? *负责团队ï¼? ASL å¼€å<E282AC>组
最å<EFBFBD>Žæ´æ°ï¼š 2025-11-16
*â­?é‡<C3A9>è¦<C3A8>:基于真实架构(Frontend-v2 + Backend增é‡<C3A9>æ¼”è¿ + asl_schemaï¼?


📋 模å<C2A1>—æ¦è¿°

标题æ˜è¦<EFBFBD>åˆ<EFBFBD>ç­æ˜?ASL 模å<C2A1>—的第一个核心功能,也是 MVP 阶段的唯一交付功能ã€?

功能范围

  1. **设置与å<C5BD>¯åŠ¨è§†å?*:PICO 标准展示ã€<C3A3>Excel æ‡çŒ®å¯¼å…¥ã€<C3A3>å<EFBFBD>¯åЍç­é€‰ä»»åŠ?2. **审核工作å<C593>°è§†å?*:å<C5A1>Œæ¨¡åžåˆ¤æ­å¯¹æ¯”ã€<C3A3>冲çª<C3A7>标记ã€<C3A3>人工å¤<C3A5>æ ?3. åˆ<EFBFBD>ç­ç»“果视å¾ï¼šç»Ÿè®¡æ¦è§ˆã€<EFBFBD>PRISMA æŽé™¤æ€»ç»“ã€<C3A3>结果导å‡?

技术栈

层级 技� 说明
å‰<EFBFBD>端 React 19 + TypeScript + Ant Design 5 + xlsx Frontend-v2æž¶æž„
å<EFBFBD>Žç«¯ Node.js + Fastify + TypeScript + Prisma Backend/modules/asl/
LLM DeepSeek-V3 + Qwen3-72B å¤<EFBFBD>用 common/llm/adapters/
*æ•°æ<EFBFBD>®åº? PostgreSQL 15 (asl_schema) Schema隔离

ðŸ<EFBFBD>—ï¸?æž¶æž„å‰<C3A5>æ<EFBFBD><C3A6>(已完æˆ<C3A6>ï¼?

âœ?Frontend-v2 架构(Week 2 Day 6-7 完æˆ<C3A6>ï¼?```

frontend-v2/src/ ├── framework/layout/ â”? ├── MainLayout.tsx # âœ?顶部导航布局 â”? └── TopNavigation.tsx # âœ?6个模å<C2A1>—导èˆ?├── framework/modules/ â”? ├── moduleRegistry.ts # âœ?模å<C2A1>—注册中心 â”? └── types.ts # âœ?ModuleDefinition接å<C2A5>£ └── modules/asl/ └── index.tsx # 🚧 å<> ä½<C3A4>页é<C2B5>¢ï¼ˆå¾…æ¿æ<C2BF>¢ï¼?```

âœ?Backend 架构(Week 2 Day 8-9 完æˆ<C3A6>ï¼?```

backend/src/ ├── common/llm/adapters/ # âœ?LLMFactoryå<79>¯å¤<C3A5>ç”?├── common/utils/jsonParser.js # âœ?JSONè§£æž<C3A6>å<EFBFBD>¯å¤<C3A5>ç”?└── modules/ └── asl/ # 🚧 空目录(待创建)


### âœ?Database Schema(Week 1 完æˆ<C3A6>ï¼?```prisma
// backend/prisma/schema.prisma
datasource db {
  schemas = [
    "asl_schema",  # �已预留,待定义表结构
    // ...其仸ªSchema
  ]
}

🌥ï¸?äºåŽŸç”Ÿå¼€å<E282AC>注æ„<C3A6>äºé¡¹ï¼ˆ2025-11-16 新增ï¼?

**â­?é‡<C3A9>è¦<C3A8>æ´æ°**:本模å<C2A1>—å¼€å<E282AC>需é<E282AC>µå¾ªé˜¿é‡Œäº?Serverless 部署架构è¦<C3A8>æ±
详细规范:äºåŽŸç”Ÿå¼€å<EFBFBD>规范
**部署指å<E280A1>—**:äºåŽŸç”Ÿéƒ¨ç½²æž¶æž„æŒ‡å<EFBFBD>

🎯 本地开å<E282AC>?+ äºç«¯éƒ¨ç½²å<C2B2>Œå…¼å®¹ç­ç•?

环境 å­˜å¨æ¹å¼<EFBFBD> é…<EFBFBD>ç½® 说明
*本地开å<EFBFBD>? LocalAdapter STORAGE_TYPE=local 文件存储åˆ?./uploads/
生产环境 OSSAdapter STORAGE_TYPE=oss 文件存储到阿里云 OSS

**核心原则**ï¼?- âœ?**Excel导入**:内存解æž<C3A6>(xlsx.read(buffer)),ä¸<EFBFBD>è<EFBFBD>½ç?- âœ?**PDF上传**(V1.0):使用 StorageFactory,本åœ?OSS自动切æ<E280A1>¢

  • âœ?**异步任务**:LLMç­é€‰ä»»åŠ¡å¿…é¡»å¼æ­¥å¤„ç<E2809E>†ï¼ˆ> 10ç§ä»»åŠ¡ï¼‰
  • âœ?**环境å<C692>˜é‡<C3A9>**:所有é…<C3A9>置从 .env 读å<C2BB>
  • âœ?**æ•°æ<C2B0>®åº“连接池**:使用全局 prisma 实ä¾ï¼Œä¸<C3A4>æ°å»ºè¿žæŽ¥

â<EFBFBD>?ç¦<C3A7>止的å<E2809E>šæ³?

ç¦<EFBFBD>æ­¢æ“<EFBFBD>作 正确å<EFBFBD>𿳕 原因
fs.writeFileSync('./temp.xlsx') xlsx.read(buffer) 内存解æž<C3A6> Serverless容器é‡<EFBFBD>å<EFBFBD>¯ä¸¢å¤±æ‡ä»¶
new PrismaClient() æ¯<C3A6>次æ°å»ºè¿žæŽ¥ 使用全局 prisma 实例 é<EFBFBD>¿å…<EFBFBD>连接数暴å¢?
硬编ç ?apiKey = 'sk-xxx' process.env.LLM_API_KEY é…<EFBFBD>置管ç<EFBFBD>†æ··ä¹±
å<EFBFBD>Œæ­¥å¤„ç<EFBFBD>†1000æ<EFBFBD>¡æ‡çŒ®ç­é€? 异步任务 + 进度轮询 超过30ç§è¶…æ—¶é™<EFBFBD>åˆ?

âœ?MVP阶段开å<E282AC>检查清å<E280A6>?

在æ<EFBFBD><EFBFBD>交代ç <EFBFBD>å‰<EFBFBD>,请确认ï¼?

  • Excel导入是å<EFBFBD>¦ä½¿ç”¨å†…存解æž<EFBFBD>(xlsx.read(buffer))?
  • 是å<EFBFBD>¦ä½¿ç”¨å…¨å±€ prisma 实例(import { prisma } from '@/config/database')?
  • 是å<EFBFBD>¦æ‰€æœ‰é…<EFBFBD>置都从环境å<EFBFBD>˜é‡<EFBFBD>读å<EFBFBD>?
  • LLMç­é€‰ä»»åŠ¡æ˜¯å<EFBFBD>¦å¼æ­¥å¤„ç<EFBFBD>†ï¼ˆPOST /screening/start ç«å<E280B9>³è¿”åžtaskId)?
  • 是å<EFBFBD>¦é¢„ç•™äº?OSS 字段(pdfUrl, pdfOssKey, pdfFileSize)?
  • 是å<EFBFBD>¦ä½¿ç”¨å­˜å¨æŠ½è±¡å±ï¼ˆStorageFactory.create())?

预留字段说明ï¼?- MVP阶段仅å<E280A6>šæ ‡é¢˜æ˜è¦<C3A8>ç­é€‰ï¼Œä¸<C3A4>处ç<E2809E>†PDF

  • V1.0阶段实现全æ‡PDFç­é€‰æ—¶ï¼Œä½¿ç”¨é¢„留的OSS字段

📅 åå¨å¼€å<E282AC>计åˆ?

Week 1: æ•°æ<C2B0>®åº“Schema + å<>Žç«¯API框架 + 存储抽象å±?Week 2: LLMç­é€‰æ ¸å¿?+ 弿­¥æ‰¹å¤„ç<E2809E>†é€»è¾
Week 3: å‰<C3A5>端模å<C2A1>—å¼€å<E282AC>?+ 审核工作å<C593>°ï¼ˆå†…存解æž<C3A6>Excelï¼?Week 4: 结果展示 + 导出 + 醿ˆ<C3A6>æµè¯•

🗓ï¸?Week 1: æ•°æ<C2B0>®åº“Schema与å<C5BD>Žç«¯API框架

Day 1: Prisma Schema 设计

任务1: 设计 asl_schema 表结�

�backend/prisma/schema.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
  
  // ç­é€‰é…<C3A9>ç½?  screeningConfig Json?    @map("screening_config") // { models: ["deepseek", "qwen"], temperature: 0 }
  
  // å…³è<C2B3>”
  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 æ‡çŒ®æ<C2AE>¡ç®è¡?====================
model AslLiterature {
  id              String   @id @default(uuid())
  projectId       String   @map("project_id")
  project         AslScreeningProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
  
  // æ‡çŒ®åŸºæœ¬ä¿¡æ<C2A1>¯
  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")  // æ‡ä»¶å¤§å°<C3A5>(字èŠï¼‰
  
  // å…³è<C2B3>”
  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模åžè¯<C3A8>æ<EFBFBD>®
  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模åžè¯<C3A8>æ<EFBFBD>®
  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
  
  // 冲çª<C3A7>状æ€?  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处ç<E2809E>†çжæ€?  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
  
  // å<>¯è¿½æº¯ä¿¡æ<C2A1>?  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")
  
  // æ—¶é—´ä¿¡æ<C2A1>¯
  startedAt       DateTime? @map("started_at")
  completedAt     DateTime? @map("completed_at")
  estimatedEndAt  DateTime? @map("estimated_end_at")
  
  // 错误信æ<C2A1>¯
  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])
}

// ==================== 用户表关è<C2B3>”(添加到User模åžï¼?===================
// �platform_schema �User 模型中添加:
// aslProjects AslScreeningProject[] @relation("AslProjects")

*执行è¿<EFBFBD>ç§»ï¼?

cd backend
npx prisma migrate dev --name add_asl_screening_tables
npx prisma generate

**验收标准**ï¼?- âœ?æ•°æ<C2B0>®åº“表åˆå»ºæˆ<C3A6>功ï¼?张表ï¼?- âœ?Prisma Client 生æˆ<C3A6>æˆ<C3A6>功

  • âœ?å<>¯æŸ¥è¯?asl_schema è¡?

Day 2: å<>Žç«¯ç®å½•结构åˆå»º

**â­?å‰<C3A5>ç½®æ<C2AE>¡ä»¶ï¼?025-11-17 æ›´æ–°ï¼?*:平å<C2B3>°åŸºç¡€è®¾æ½å·²å®Œæˆ<C3A6>实æ?âœ? **完æˆ<C3A6>状æ€?*ï¼?个核心模å<C2A1>—,100%测试通过
完æˆ<EFBFBD>报åŠï¼šå¹³å<EFBFBD>°åŸºç¡€è®¾æ½å®žæ½å®Œæˆ<EFBFBD>报åŠ
**使用指å<E280A1>—**:backend/src/common/README.md

å¹³å<EFBFBD>°å·²æ<EFBFBD><EFBFBD>ä¾çš„8个核心模å<EFBFBD>—(无需ASL模å<EFBFBD>—实现ï¼?

**å¹³å<C2B3>°åŸºç¡€è®¾æ½è·¯å¾„**:backend/src/common/

# 模å<EFBFBD> 使用æ¹å¼<EFBFBD> 功能说明
1 å­˜å¨æœ<EFBFBD>务 import { storage } from '@/common/storage' 文件上传下载(本åœ?OSS切æ<E280A1>¢ï¼?
2 日志系统 import { logger } from '@/common/logging' 结构åŒJSON日志
3 缓存æœ<EFBFBD>务 import { cache } from '@/common/cache' 内存/Redis缓存
4 异步任务 import { jobQueue } from '@/common/jobs' 长时间任务处ç<EFBFBD>?
5 *å<EFBFBD>¥åº·æ£€æŸ? import { registerHealthRoutes } from '@/common/health' SAEå<EFBFBD>¥åº·æ£€æŸ?
6 监控指标 import { Metrics } from '@/common/monitoring' 性能监控和告�
7 **æ•°æ<C2B0>®åº“连接池** import { prisma } from '@/config/database' 全局Prisma实ä¾
8 环境é…<EFBFBD>ç½® import { env } from '@/config/env' 统一é…<EFBFBD>置管ç<EFBFBD>

**å­˜å¨æœ<C3A6>务使用示ä¾**ï¼?```typescript // ASL模å<C2A1>—ç´æŽ¥ä½¿ç”¨ï¼ˆä¸€è¡Œä»£ç <C3A7>) import { storage } from '@/common/storage'

// 上传æ‡ä»¶ï¼ˆä¸<C3A4>关心本地还是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')


**支æŒ<C3A6>的部署环å¢?*ï¼?- âœ?本地开å<E282AC>:LocalAdapter(æ‡ä»¶å­˜å¨åˆ° `./uploads/`ï¼?- âœ?äºç«¯SaaS:OSSAdapter(æ‡ä»¶å­˜å¨åˆ°é˜¿é‡ŒäºOSSï¼?- âœ?ç§<C3A7>有åŒéƒ¨ç½²ï¼šLocalAdapter(æ‡ä»¶å­˜å¨åˆ°æœ<C3A6>务器)
- âœ?å<>•机版:LocalAdapter(æ‡ä»¶å­˜å¨åˆ°ç”¨æˆ·æœ¬åœ°ï¼?
**环境切æ<E280A1>¢**:修改一个环境å<C692>˜é‡<C3A9>å<EFBFBD>³å<C2B3>?```bash
# 本地开å<E282AC>?STORAGE_TYPE=local

# 生产环境
STORAGE_TYPE=oss

**核心优势**ï¼?- âœ?ASL模å<C2A1>—无需关心基础设æ½å®žçŽ°ç»†èŠ

  • âœ?代ç <C3A7>é¶æ”¹åŠ¨åˆ‡æ<E280A1>¢çŽ¯å¢ƒï¼ˆæœ¬åœ° â†?云端ï¼?- âœ?所有业务模å<C2A1>—(AIA/PKB/DC等)å¤<C3A5>用å<C2A8>Œä¸€å¥—基础设æ½
  • âœ?统一维护ã€<C3A3>统一å<E282AC>‡çº§ã€<C3A3>ç»Ÿä¸€çæŽ§

任务1: 创建 backend/src/modules/asl/ 目录

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ï¼?

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 模å<C2A1>—路由注册
 * 
 * @description
 * - 注册åˆ?/api/v1/asl å‰<C3A5>ç¼€
 * - å<>è€?legacy/routes/ 的风æ ? * 
 * @version Week 3 Day 2
 */
export async function aslRoutes(fastify: FastifyInstance) {
  // 项ç®ç®¡ç<C2A1>  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)
  
  // æ‡çŒ®ç®¡ç<C2A1>  fastify.post('/projects/:projectId/literatures/import', literatureController.importLiteratures)
  fastify.get('/projects/:projectId/literatures', literatureController.listLiteratures)
  
  // ç­é€‰ç®¡ç<C2A1>?  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)
}

**验收标准**�- �目录结构清晰

  • âœ?路由æ‡ä»¶åˆå»ºå®Œæˆ<C3A6>
  • âœ?**å<>¯æ­£å¸¸ä½¿ç”¨å¹³å<C2B3>°æœ<C3A6>åŠ?*ï¼? - âœ?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(修改)�

// ============================================
// ã€<C3A3>æ°æž¶æž„ã€ASL 模å<C2A1>— - Week 3 新增
// ============================================
import { aslRoutes } from './modules/asl/routes/index.js';

// ... å…¶ä»ä»£ç <C3A7>

// 注册 ASL 模å<C2A1>—路由
await fastify.register(aslRoutes, { prefix: '/api/v1/asl' });

console.log('�ASL 路由已注册到 /api/v1/asl/*');

**验收标准**ï¼?- âœ?å<>Žç«¯å<C2AF>¯åЍæˆ<C3A6>功

  • âœ?访问 http://localhost:3001/api/v1/asl/projects 返回 200(å<CB86>³ä½¿æ˜¯ç©ºåˆ—表)

🗓ï¸?Week 2: LLMç­é€‰æ ¸å¿?

Day 4-5: LLMç­é€‰æœ<C3A6>务实çŽ?

任务1: 定义 JSON Schema

*backend/src/modules/asl/schemas/screening.schema.tsï¼?

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: åˆå»ºæ<C2BA><C3A6>示è¯<C3A8>模æ<C2A1>?

*backend/prompts/asl/screening/v1.0.0-basic.txtï¼?

你是一ä½<EFBFBD>医学æ‡çŒ®ç­é€‰ä¸“å®¶ã€è¯·æ ¹æ<EFBFBD>®ä»¥ä¸ PICO 标准判æ­è¿™ç¯‡æ‡çŒ®æ˜¯å<C2AF>¦åº”该纳入系统评价ã€?
# PICO 标准
- **Population (研究对象)**: {{population}}
- **Intervention (干预措施)**: {{intervention}}
- **Comparison (对照措施)**: {{comparison}}
- **Outcome (结局指标)**: {{outcome}}
- **Study Design (研究设计)**: {{studyDesign}}

# 纳入标准
{{inclusionCriteria}}

# 排除标准
{{exclusionCriteria}}

# 待筛选文�**标题**: {{title}}

**æ˜è¦<C3A8>**: {{abstract}}

# 输出è¦<C3A8>æ±

请严格按照以ä¸?JSON Schema 输出结果,输出纯JSON(ä¸<C3A4>è¦<C3A8>包å<E280A6>«ä»»ä½•其仿‡å­—)ï¼?
{
  "decision": "include/exclude/uncertain",
  "reason": "判æ­ç<C2AD>†ç”±ï¼?0-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": "原æ‡ä¸­çš„关键è¯<C3A8>æ<EFBFBD>®çŸ­è¯­",
    "intervention": "原æ‡ä¸­çš„关键è¯<C3A8>æ<EFBFBD>®çŸ­è¯­",
    "comparison": "原æ‡ä¸­çš„关键è¯<C3A8>æ<EFBFBD>®çŸ­è¯­",
    "outcome": "原æ‡ä¸­çš„关键è¯<C3A8>æ<EFBFBD>®çŸ­è¯­"
  }
}

# 注æ„<C3A6>äºé¡¹
1. decision å<>ªèƒ½æ˜?"include"(纳入)ã€?exclude"(排除)æˆ?"uncertain"(ä¸<C3A4>确定ï¼?2. reason 必须具体说明判æ­ä¾<C3A4>æ<EFBFBD>®
3. confidence ä¸?0-1 之间的数å€?4. pico 字段é€<C3A9>项评估匹é…<C3A9>ç¨åº¦
5. evidences 字段æ<C2B5><C3A6>å<EFBFBD>原æ‡ä¸­çš„关键短语作为è¯<C3A8>æ<EFBFBD>®

任务3: 实现 LLM ç­é€‰æœ<C3A6>åŠ?

*backend/src/modules/asl/services/llmScreeningService.tsï¼?

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 ç­é€‰æœ<C3A6>åŠ? * 
 * @description
 * - å¤<C3A5>用 common/llm/adapters/LLMFactory.ts
 * - å<>Œæ¨¡åžå¹¶è¡Œè°ƒç”¨ï¼ˆDeepSeek + Qwenï¼? * - JSON Schema 验è¯<C3A8>
 * - 冲çª<C3A7>检æµ? * 
 * @version Week 3 Day 4-5
 */
class LLMScreeningService {
  /**
   * å<>Œæ¨¡åžå¹¶è¡Œç­é€?   */
  async dualModelScreening(literature: any, protocol: any) {
    // 构建æ<C2BA><C3A6>示è¯?    const prompt = this.buildPrompt(literature, protocol);

    // 并行调用两个模型
    const [resultA, resultB] = await Promise.all([
      this.callModel('deepseek', prompt),
      this.callModel('qwen', prompt)
    ]);

    // è§£æž<C3A6>JSON结果
    const decisionA = await this.parseModelOutput(resultA.content, 'deepseek');
    const decisionB = await this.parseModelOutput(resultB.content, 'qwen');

    // 一致性判�    const { consensus, conflictFields } = this.compareDecisions(decisionA, decisionB);

    // 自动分æµ<C3A6>
    const needReview = this.shouldReview(consensus, decisionA, decisionB);

    return {
      consensus,
      finalDecision: consensus === 'high' ? decisionA.decision : 'uncertain',
      needReview,
      conflictFields,
      modelA: decisionA,
      modelB: decisionB
    };
  }

  /**
   * 调用LLM模åžï¼ˆå¤<C3A5>用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;
  }

  /**
   * 构建æ<C2BA><C3A6>示è¯?   */
  private buildPrompt(literature: any, protocol: any): string {
    // 读å<C2BB>æ<E28093><C3A6>示è¯<C3A8>模æ<C2A1>?    const templatePath = path.resolve(__dirname, '../../../../prompts/asl/screening/v1.0.0-basic.txt');
    let template = fs.readFileSync(templatePath, 'utf-8');

    // æ¿æ<C2BF>¢å<C2A2>˜é‡<C3A9>
    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;
  }

  /**
   * è§£æž<C3A6>模åžè¾“出
   */
  private async parseModelOutput(content: string, modelName: string) {
    // 使用JSONè§£æž<C3A6>器(å¤<C3A5>用common/utilsï¼?    const parsed = parseJSON(content);

    // JSON Schema 验è¯<C3A8>
    const valid = validateSchema(parsed);
    if (!valid) {
      console.error('JSON Schema验è¯<C3A8>失败ï¼?, validateSchema.errors);
      throw new Error(`模型${modelName}输出格å¼<C3A5>ä¸<C3A4>符å<C2A6>ˆ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å<4F>„ç»´åº?    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 };
  }

  /**
   * 自动分æµ<C3A6>è§„åˆ   */
  private shouldReview(consensus: string, decisionA: any, decisionB: any): boolean {
    // 规则1:冲çª?â†?å¿…é¡»å¤<C3A5>æ ¸
    if (consensus === 'conflict') {
      return true;
    }

    // 规则2:低置信åº?â†?需è¦<C3A8>å¤<C3A5>æ ?    const avgConfidence = (decisionA.confidence + decisionB.confidence) / 2;
    if (avgConfidence < 0.7) {
      return true;
    }

    // 规则3:高置信�+ 一��自动通过
    return false;
  }

  /**
   * 批é‡<C3A9>ç­é€?   */
  async batchScreening(literatures: any[], protocol: any, progressCallback?: (progress: number) => void) {
    const batchSize = 15;  // æ¯<C3A6>批15ç¯?    const results = [];

    for (let i = 0; i < literatures.length; i += batchSize) {
      const batch = literatures.slice(i, i + batchSize);

      // 并行处ç<E2809E>†å½“å‰<C3A5>批次
      const batchResults = await Promise.all(
        batch.map(lit => this.dualModelScreening(lit, protocol))
      );

      results.push(...batchResults);

      // 推é€<C3A9>è¿åº?      const progress = Math.round(((i + batch.length) / literatures.length) * 100);
      progressCallback?.(progress);
    }

    return results;
  }
}

export const llmScreeningService = new LLMScreeningService();

**验收标准**ï¼?- âœ?LLMå<4D>Œæ¨¡åžè°ƒç”¨æˆ<C3A6>åŠ?- âœ?JSON Schema验è¯<C3A8>通过çŽ?> 95%

  • âœ?冲çª<C3A7>检æµå‡†ç¡?

🗓ï¸?Week 3: å‰<C3A5>端模å<C2A1>—å¼€å<E282AC>?

Day 6-7: å‰<C3A5>端模å<C2A1>—结构åˆå»º

任务1: æ´æ°æ¨¡å<C2A1>—定义

*frontend-v2/src/modules/asl/index.tsx(修改)�

import { lazy } from 'react'
import { ModuleDefinition } from '@/framework/modules/types'
import { FileSearchOutlined } from '@ant-design/icons'

/**
 * ASL 模å<C2A1>—定义
 * 
 * @description
 * - 移除å<C2A4> ä½<C3A4>标记
 * - 实现真实模å<C2A1>—路由
 * 
 * @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驱动的æ‡çŒ®ç­é€‰åŒåˆ†æž<C3A6>系统',
}

export default ASLModule

任务2: 创建目录结构

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: 实现路由é…<C3A9>ç½®

*frontend-v2/src/modules/asl/routes.tsxï¼?

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 模å<C2A1>—路由
 * 
 * @description
 * - /literature - 项目列表
 * - /literature/project/:id/settings - 设置与å<C5BD>¯åŠ? * - /literature/project/:id/workbench - 审核工作å<C593>? * - /literature/project/:id/results - åˆ<C3A5>ç­ç»“æžœ
 * 
 * @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智能æ‡çŒ®"(ä¸<C3A4>å†<C3A5>是å<C2AF> ä½<C3A4>ï¼?- âœ?ç¹å‡»å<C2BB>Žè¿å…¥é¡¹ç®åˆ—表页(å<CB86>³ä½¿æ˜¯ç©ºåˆ—表)


Day 8-10: 实现核心页é<C2B5>¢

(由于篇幅é™<EFBFBD>制,核心实现代ç <EFBFBD>请å<EFBFBD>è€ƒä»»åŠ¡åˆ†è§£æ‡æ¡£ï¼‰

**验收标准**�- �Excel上传功能正常

  • âœ?审核工作å<C593>°å<C2B0>¯å±•示ç­é€‰ç»“æž?- âœ?å<>Œè§†å¾æ¨¡æ€<C3A6>框å<E280A0>¯å¼¹å‡?

🗓ï¸?Week 4: 醿ˆ<C3A6>æµè¯•与验æ”?

Day 11-14: 端到端测�

(详细æµè¯•计åˆè§<EFBFBD>ä»»åŠ¡åˆ†è§£æ‡æ¡£ï¼? **验收标准**ï¼?- âœ?完整æµ<C3A6>ç¨ï¼šä¸Šä¼?â†?ç­›é€?â†?å¤<C3A5>æ ¸ â†?导出

  • âœ?准确çŽ?â‰?85%
  • âœ?性能达标ï¼?00ç¯?< 10分éŸï¼?

📚 相关文档


**更新日志**ï¼?- 2025-11-18: V3.1 æ´æ°ï¼Œè¡¥å……å¹³å<C2B3>°åŸºç¡€è®¾æ½å®Œæˆ<C3A6>状æ€<C3A6>(¸ªæ ¸å¿ƒæ¨¡å<C2A1>—)

  • 2025-11-16: V3.0 é‡<C3A9>写,基于真实架构(Frontend-v2 + Backend + asl_schemaï¼?- 2025-11-16: V2.0 é‡<C3A9>写,详细到æ¯<C3A6>天的任务åŒä»£ç <C3A7>示ä¾
  • 2025-10-29: V1.0 åˆå»ºï¼Œåˆ<C3A5>å§ç‰ˆæœ?