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

836 lines
30 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.
# 标题æ˜è¦<C3A8>åˆ<C3A5>ç­æ¨¡å<C2A1>— - 详细开å<E282AC>计åˆï¼ˆMVP阶段ï¼?
> **文档版本�* V3.0
> **创建日期�* 2025-11-16
> **å¼€å<E282AC>卿œŸï¼š** 4 å‘?
> **负责团队ï¼?* ASL å¼€å<E282AC>组
> **最å<E282AC>Žæ´æ°ï¼š** 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. **åˆ<C3A5>ç­ç»“果视å¾**:统计æ¦è§ˆã€<C3A3>PRISMA æŽé™¤æ€»ç»“ã€<C3A3>结果导å‡?
### 技术栈
| 层级 | 技�| 说明 |
|------|------|------|
| **å‰<C3A5>端** | React 19 + TypeScript + Ant Design 5 + xlsx | Frontend-v2æž¶æž„ |
| **å<>Žç«¯** | Node.js + Fastify + TypeScript + Prisma | Backend/modules/asl/ |
| **LLM** | DeepSeek-V3 + Qwen3-72B | å¤<C3A5>用 common/llm/adapters/ |
| **æ•°æ<C2B0>®åº?* | PostgreSQL 15 (asl_schema) | Schema隔离 |
---
## ðŸ<C3B0>—ï¸?æž¶æž„å‰<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>æ±
> **详细规范**:[äºåŽŸç”Ÿå¼€å<E282AC>规范](../../../04-å¼€å<E282AC>è§„èŒ?08-äºåŽŸç”Ÿå¼€å<E282AC>è§„èŒ?md)
> **部署指å<E280A1>—**:[äºåŽŸç”Ÿéƒ¨ç½²æž¶æž„æŒ‡å<E280A1>—](../../../09-架构实施/03-äºåŽŸç”Ÿéƒ¨ç½²æž¶æž„æŒ‡å<E280A1>?md)
### 🎯 本地开å<E282AC>?+ äºç«¯éƒ¨ç½²å<C2B2>Œå…¼å®¹ç­ç•?
| 环境 | å­˜å¨æ¹å¼<C3A5> | é…<C3A9>ç½® | 说明 |
|------|---------|------|------|
| **本地开å<E282AC>?* | LocalAdapter | `STORAGE_TYPE=local` | 文件存储åˆ?`./uploads/` |
| **生产环境** | OSSAdapter | `STORAGE_TYPE=oss` | 文件存储到阿里云 OSS |
**核心原则**ï¼?- âœ?**Excel导入**:内存解æž<C3A6>(`xlsx.read(buffer)`),ä¸<C3A4>è<EFBFBD>½ç?- âœ?**PDF上传**(V1.0):使用 `StorageFactory`,本åœ?OSS自动切æ<E280A1>¢
- âœ?**异步任务**:LLMç­é€‰ä»»åŠ¡å¿…é¡»å¼æ­¥å¤„ç<E2809E>†ï¼ˆ> 10ç§ä»»åŠ¡ï¼‰
- âœ?**环境å<C692>˜é‡<C3A9>**:所有é…<C3A9>置从 `.env` 读å<C2BB>
- âœ?**æ•°æ<C2B0>®åº“连接池**:使用全局 `prisma` 实ä¾ï¼Œä¸<C3A4>æ°å»ºè¿žæŽ¥
### â<>?ç¦<C3A7>止的å<E2809E>šæ³?
| ç¦<C3A7>æ­¢æ“<C3A6>作 | 正确å<C2AE>𿳕 | 原因 |
|---------|---------|------|
| `fs.writeFileSync('./temp.xlsx')` | `xlsx.read(buffer)` 内存解æž<C3A6> | Serverless容器é‡<C3A9>å<EFBFBD>¯ä¸¢å¤±æ‡ä»¶ |
| `new PrismaClient()` æ¯<C3A6>次æ°å»ºè¿žæŽ¥ | 使用全局 `prisma` 实例 | é<>¿å…<C3A5>连接数暴å¢?|
| 硬编ç ?`apiKey = 'sk-xxx'` | `process.env.LLM_API_KEY` | é…<C3A9>置管ç<C2A1>†æ··ä¹± |
| å<>Œæ­¥å¤„ç<E2809E>†1000æ<30>¡æ‡çŒ®ç­é€?| 异步任务 + 进度轮询 | 超过30ç§è¶…æ—¶é™<C3A9>åˆ?|
### âœ?MVP阶段开å<E282AC>检查清å<E280A6>?
在æ<EFBFBD><EFBFBD>交代ç <EFBFBD>å‰<EFBFBD>,请确认ï¼?
- [ ] Excel导入是å<C2AF>¦ä½¿ç”¨å†…存解æž<C3A6>(`xlsx.read(buffer)`)?
- [ ] 是å<C2AF>¦ä½¿ç”¨å…¨å±€ `prisma` 实例(`import { prisma } from '@/config/database'`)?
- [ ] 是å<C2AF>¦æ‰€æœ‰é…<C3A9>置都从环境å<C692>˜é‡<C3A9>读å<C2BB>?
- [ ] LLMç­é€‰ä»»åŠ¡æ˜¯å<C2AF>¦å¼æ­¥å¤„ç<E2809E>†ï¼ˆ`POST /screening/start` ç«å<E280B9>³è¿”åžtaskId)?
- [ ] 是å<C2AF>¦é¢„ç•™äº?OSS 字段(`pdfUrl`, `pdfOssKey`, `pdfFileSize`)?
- [ ] 是å<C2AF>¦ä½¿ç”¨å­˜å¨æŠ½è±¡å±ï¼ˆ`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` 中添加:**
```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")
```
**执行è¿<C3A8>ç§»ï¼?*
```bash
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%测试通过
> **完æˆ<C3A6>报åŠ**:[å¹³å<C2B3>°åŸºç¡€è®¾æ½å®žæ½å®Œæˆ<C3A6>报åŠ](../../../08-项ç®ç®¡ç<C2A1>†/03-æ¯<C3A6>å¨è®¡åˆ/2025-11-17-å¹³å<C2B3>°åŸºç¡€è®¾æ½å®žæ½å®Œæˆ<C3A6>报åŠ.md)
> **使用指å<E280A1>—**:[backend/src/common/README.md](../../../../backend/src/common/README.md)
#### å¹³å<C2B3>°å·²æ<C2B2><C3A6>ä¾çš„8个核心模å<C2A1>—(无需ASL模å<C2A1>—实现ï¼?
**å¹³å<C2B3>°åŸºç¡€è®¾æ½è·¯å¾„**:`backend/src/common/`
| # | 模å<C2A1>— | 使用æ¹å¼<C3A5> | 功能说明 |
|---|------|---------|---------|
| 1 | **å­˜å¨æœ<C3A6>务** | `import { storage } from '@/common/storage'` | 文件上传下载(本åœ?OSS切æ<E280A1>¢ï¼?|
| 2 | **日志系统** | `import { logger } from '@/common/logging'` | 结构åŒJSON日志 |
| 3 | **缓存æœ<C3A6>务** | `import { cache } from '@/common/cache'` | 内存/Redis缓存 |
| 4 | **异步任务** | `import { jobQueue } from '@/common/jobs'` | 长时间任务处ç<E2809E>?|
| 5 | **å<>¥åº·æ£€æŸ?* | `import { registerHealthRoutes } from '@/common/health'` | SAEå<45>¥åº·æ£€æŸ?|
| 6 | **监控指标** | `import { Metrics } from '@/common/monitoring'` | 性能监控和告�|
| 7 | **æ•°æ<C2B0>®åº“连接池** | `import { prisma } from '@/config/database'` | 全局Prismaå®žä¾ |
| 8 | **环境é…<C3A9>ç½®** | `import { env } from '@/config/env'` | 统一é…<C3A9>置管ç<C2A1>† |
**å­˜å¨æœ<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/` 目录
```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 模å<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`(修改)�*
```typescript
// ============================================
// ã€<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`ï¼?*
```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: åˆå»ºæ<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`ï¼?*
```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 ç­é€‰æœ<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`(修改)�*
```typescript
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: 创建目录结构
```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: 实现路由é…<C3A9>ç½®
**`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 模å<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分éŸï¼?
---
## 📚 相关文档
- [å¼€å<EFBFBD>里ç¨ç¢](./01-å¼€å<E282AC>里ç¨ç¢.md)
- [任务分解(Todo List)](./03-任务分解.md)
- [è´¨é‡<EFBFBD>ä¿<EFBFBD>éšœç­ç•¥](../02-技术设è®?06-è´¨é‡<C3A9>ä¿<C3A4>障与å<C5BD>¯è¿½æº¯ç­ç•¥.md)
- [技术选型](../02-技术设è®?07-æ‡çŒ®å¤„ç<E2809E>†æŠ€æœ¯é€‰åž.md)
- [API设计规范](../02-技术设�02-API设计规范.md)
- [å‰<EFBFBD>å<EFBFBD>Žç«¯æ¨¡å<EFBFBD>—åŒæž¶æž„设计-V2](../../../00-系统总体设计/å‰<C3A5>å<EFBFBD>Žç«¯æ¨¡å<C2A1>—åŒæž¶æž„设计-V2.md)
---
**更新日志**ï¼?- 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>å§ç‰ˆæœ?