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%)
30 KiB
æ ‡é¢˜æ‘˜è¦<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 阶段的唯一交付功能ã€?
功能范围
- **设置与å<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", # �已预留,待定义表结构
// ...其他9个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分钟ï¼?
📚 相关文档
- å¼€å<EFBFBD>‘里程碑
- 任务分解(Todo List)
- è´¨é‡<EFBFBD>ä¿<EFBFBD>éšœç–ç•¥
- 技术选型
- API设计规范
- å‰<EFBFBD>å<EFBFBD>Žç«¯æ¨¡å<EFBFBD>—化架构设计-V2
**更新日志**ï¼?- 2025-11-18: V3.1 更新,补充平å<C2B3>°åŸºç¡€è®¾æ–½å®Œæˆ<C3A6>状æ€<C3A6>(8ä¸ªæ ¸å¿ƒæ¨¡å<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>始版æœ?