# Deep Research V2.0 开发计划 > **文档版本:** v1.1 > **创建日期:** 2026-02-22 > **维护者:** 开发团队 > **前置文档:** PRD V4.1 / 原型图 V4.2 / 技术设计 V4.1 > **预计工期:** 5 天 > **核心理念:** 单页瀑布流 + 自然语言需求扩写 + 异步执行 + 务实结果展示 > **v1.1 更新:** 融入审查建议(Worker 重试、JSON 防崩溃、条件滚动、MeSH 扩展、Prompt 管理、数据源精简) --- ## 1. 升级概述 ### 1.1 V1.x → V2.0 变化总结 | 维度 | V1.x (当前) | V2.0 (目标) | |------|------------|------------| | **交互模式** | 单输入框 → 直接搜索 | 四步瀑布流 Landing → 配置 → HITL 确认 → 终端 → 结果 | | **需求理解** | 用户原文直传 unifuncs | 内置 LLM 需求扩写 + 用户人工核验修改 | | **API 协议** | OpenAI 兼容(SSE 流式) | **Unifuncs 异步模式**(create_task + query_task 轮询) | | **执行展示** | 混合文字流(打字机效果) | 暗黑终端 + 分类结构化日志(每 3-5s 弹出一条) | | **结果展示** | PubMed 链接列表 | 综合报告(Markdown)+ 文献清单表格 + Word 导出 | | **可靠性** | 离开页面任务丢失 | pg-boss 队列,离开页面任务继续,回来可恢复 | ### 1.2 设计决策记录 | 决策 | 选择 | 理由 | |------|------|------| | SSE vs 异步 | **异步模式** | Deep Research 任务 3-10 分钟,SSE 连接不稳定;异步模式用户可离开回来,可靠性远高于 SSE | | 异步下的实时性 | **Worker 5s 轮询 + 前端 3s 轮询** | 用户每 3-5s 看到一条新日志,对分钟级 Agent 任务来说体验自然,比逐字流更适合终端 UI | | 结果展示复杂度 | **报告 + 表格,不做图表看板** | 研究人员要的是内容本身(综合报告 + 文献清单),图表是锦上添花非刚需,MVP 不做 | | Word 导出 | **复用 Pandoc** | Protocol Agent 已验证 Pandoc → Word 方案,零额外依赖 | | 需求扩写 Prompt | **Prompt 管理服务(运营端可配)** | 使用 `ASL_DEEP_RESEARCH_EXPANSION`,运营管理端可在线调优,代码中写兜底 Fallback | | 数据源范围 | **精简为 5 个(3英文+2中文)** | 基于 18 站实测结果精选,PubMed 默认勾选,ClinicalTrials 标记需英文查询 | | 状态管理 | **React Query + useState** | 服务端状态用 useQuery 轮询(自带缓存+去重),页面步骤用 useState,不引入 Zustand | --- ## 2. 系统数据流 ``` ┌──────────────────────────────────────────────────────────────────────┐ │ Step 1-2: 需求扩写(同步,本系统内部) │ │ │ │ 前端 Landing/Setup ──POST──→ Node.js ──LLMFactory──→ DeepSeek-V3 │ │ original_query "需求扩写Prompt" │ │ │ │ 返回:taskId + generatedRequirement(结构化自然语言检索指令书) │ │ 前端展示指令书,用户可编辑修改 │ └──────────────────────────────────────────────────────────────────────┘ ↓ 用户确认 ┌──────────────────────────────────────────────────────────────────────┐ │ Step 3: 异步执行(pg-boss + Unifuncs 异步 API) │ │ │ │ 前端 ──PUT──→ Node.js ──pg-boss push──→ Worker │ │ confirmed_requirement │ │ │ │ Worker: │ │ 1. POST unifuncs/v1/create_task(传入 confirmed_requirement) │ │ 2. 每 5s GET unifuncs/v1/query_task │ │ 3. 解析 reasoning_content → 增量日志写 DB (execution_logs) │ │ 4. 完成后解析 content → synthesis_report + result_list │ │ │ │ 前端每 3s GET /tasks/:id → 渲染 execution_logs 到暗黑终端 │ └──────────────────────────────────────────────────────────────────────┘ ↓ status === 'completed' ┌──────────────────────────────────────────────────────────────────────┐ │ Step 4: 结果展示(读 DB 渲染) │ │ │ │ 终端折叠 → 白底结果区展开 │ │ ├── ✅ 完成横幅(一行 + 导出 Word 按钮) │ │ ├── 📄 AI 综合报告(synthesis_report → Markdown 渲染) │ │ └── 📋 文献清单表格(result_list → Ant Design Table) │ └──────────────────────────────────────────────────────────────────────┘ ``` --- ## 3. 数据库 Schema 变更 在现有 `AslResearchTask` 基础上**新增 6 个字段**,不删除任何现有字段(向后兼容)。 ```prisma model AslResearchTask { // ── 现有字段(保留不动)────────────────────────── id String @id @default(uuid()) projectId String @map("project_id") userId String @map("user_id") query String // 原始粗略输入(Step 1) filters Json? // 高级筛选配置 externalTaskId String? @map("external_task_id") // unifuncs task_id status String @default("pending") errorMessage String? @map("error_message") resultCount Int? @map("result_count") rawResult String? @map("raw_result") @db.Text reasoningContent String? @map("reasoning_content") @db.Text literatures Json? tokenUsage Json? @map("token_usage") searchCount Int? @map("search_count") readCount Int? @map("read_count") iterations Int? createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") completedAt DateTime? @map("completed_at") // ── V2.0 新增字段 ────────────────────────────── targetSources Json? @map("target_sources") // 选中的数据源 ["pubmed.ncbi.nlm.nih.gov", ...] confirmedRequirement String? @map("confirmed_requirement") @db.Text // 用户核验后的自然语言检索指令书 aiIntentSummary Json? @map("ai_intent_summary") // AI提炼的结构化摘要(左侧卡片用) executionLogs Json? @map("execution_logs") // 终端日志数组 [{type, title, text, timestamp}] synthesisReport String? @map("synthesis_report") @db.Text // AI综合报告(Markdown) resultList Json? @map("result_list") // 结构化文献元数据列表 // ── 索引(保留现有)──────────────────────────── @@index([projectId], map: "idx_research_tasks_project_id") @@index([userId], map: "idx_research_tasks_user_id") @@index([status], map: "idx_research_tasks_status") @@index([createdAt], map: "idx_research_tasks_created_at") @@map("research_tasks") @@schema("asl_schema") } ``` **Status 枚举扩展:** | 状态 | 含义 | 触发时机 | |------|------|---------| | `draft` | 需求已扩写,等待用户确认 | POST /generate-requirement | | `pending` | 用户已确认,等待 Worker 拾取 | PUT /tasks/:id/execute | | `running` | Worker 已创建 unifuncs 任务,轮询中 | Worker 内部 | | `completed` | unifuncs 完成,结果已解析入库 | Worker 内部 | | `failed` | 执行失败 | Worker 内部 | **迁移命令:** ```bash npx prisma migrate dev --name add_deep_research_v2_fields ``` --- ## 4. 需求扩写 Prompt 设计 ### 4.1 设计原则 需求扩写 Prompt 通过 **Prompt 管理服务**(`ASL_DEEP_RESEARCH_EXPANSION`)进行管理,运营端可在线调优,代码内置 Fallback 兜底。 | 原则 | 说明 | |------|------| | PICOS 结构化 | 引导 LLM 按 Population-Intervention-Comparison-Outcome-Study Design 拆解用户模糊需求 | | MeSH 同义词扩展 | 自动补充专业 MeSH 术语(如 "他汀" → "Statins, Hydroxymethylglutaryl-CoA Reductase Inhibitors") | | 默认高质量研究设计 | 若用户未指定,默认偏向 RCT、SR/MA、Cohort Study | | 自然语言对话风格 | 输出"像资深医学信息官写给检索助手的一封邮件",方便 HITL 编辑 | | 数据源感知 | Prompt 接收用户选择的数据源列表,ClinicalTrials.gov 时自动生成英文指令段 | | 不硬编码约束 | 不强制 Open Access / 不排除非英文文献 — 这些由用户在配置面板自主选择 | ### 4.2 Prompt 模板结构(Fallback) ```typescript // backend/src/common/prompt/prompt.fallbacks.ts(新增) const ASL_DEEP_RESEARCH_EXPANSION = ` 你是一位经验丰富的医学信息官(Medical Information Officer), 擅长将研究者的模糊想法转化为精准的文献检索需求指令。 ## 任务 根据用户输入的粗略研究想法,生成一份结构化的深度文献检索指令书。 ## 输出规则 1. **自然语言风格**:像写邮件一样,口语化但专业,方便研究者阅读和编辑 2. **PICOS 拆解**:明确 Population / Intervention / Comparison / Outcome / Study Design 3. **MeSH 扩展**:为关键术语补充 MeSH 同义词(用括号标注英文 MeSH 术语) 4. **研究设计偏好**:若用户未指定,默认优先 RCT、Systematic Review/Meta-Analysis、Cohort Study 5. **数据源适配**:根据用户选择的数据源列表调整语言和策略 - 若包含 ClinicalTrials.gov → 追加一段英文检索指令 - 若包含中文数据源(CNKI/中华医学期刊网)→ 保留中文检索关键词 6. **不得自行添加约束**:不要擅自限定"仅开放获取"或"仅英文文献" ## 用户输入 - 研究想法:{{originalQuery}} - 选择的数据源:{{targetSources}} - 时间范围:{{yearRange}} - 目标数量:{{targetCount}} ## 输出格式 请同时输出两部分: ### Part 1: 自然语言检索指令书 (可编辑的完整检索需求描述) ### Part 2: 结构化摘要(JSON) \`\`\`json { "objective": "...", "population": "...", "intervention": "...", "comparison": "...", "outcome": "...", "studyDesign": ["RCT", "Meta-analysis", ...], "meshTerms": ["term1", "term2", ...], "condition": "..." } \`\`\` `; ``` ### 4.3 Prompt 管理集成 | 层级 | 说明 | |------|------| | **运营端** | Prompt 管理界面 → `ASL_DEEP_RESEARCH_EXPANSION` → 可在线编辑、版本管理、A/B 测试 | | **代码 Fallback** | `prompt.fallbacks.ts` 写入默认模板,数据库无记录时自动使用 | | **调用方式** | `promptService.getPrompt('ASL_DEEP_RESEARCH_EXPANSION')` → 填充变量 → LLMFactory 调用 | --- ## 5. API 契约 ### 5.1 需求扩写(同步) **POST /api/v1/asl/research/generate-requirement** ```typescript // 请求 { originalQuery: string, // "他汀预防心血管疾病,要能下载PDF的" targetSources: string[], // 从精选数据源列表中选择(见 5.1.1) filters: { yearRange?: string, // "2010至今" | "过去5年" | "不限" targetCount?: string, // "~100篇" | "全面检索" requireOpenAccess?: boolean // true } } // 响应 { success: true, data: { taskId: "uuid", // 已创建DB记录(status=draft) generatedRequirement: "请帮我执行一次深度的医学文献检索...", // LLM扩写结果 intentSummary: { // PICOS + MeSH 结构化摘要 objective: "为Meta分析构建测试语料库", population: "心血管疾病高危患者", intervention: "他汀类药物 (Statins, HMG-CoA Reductase Inhibitors)", comparison: "安慰剂/未使用他汀", outcome: "主要不良心血管事件 (MACE) 发生率", studyDesign: ["RCT", "Meta-analysis", "Cohort"], meshTerms: ["Hydroxymethylglutaryl-CoA Reductase Inhibitors", "Cardiovascular Diseases", "Primary Prevention"], condition: "心血管疾病 (CVD)" } } } ``` #### 5.1.1 精选数据源配置 基于 Unifuncs API 18 站实测结果精选的 5 个数据源: | 类别 | 数据源 | domain_scope 值 | 默认 | 备注 | |------|--------|-----------------|------|------| | 🌍 英文 | **PubMed** | `https://pubmed.ncbi.nlm.nih.gov/` | ✅ 默认勾选 | 一级可用,核心数据源 | | 🌍 英文 | **ClinicalTrials.gov** | `https://clinicaltrials.gov/` | ☐ 可选 | ⚠️ 前端提示"需英文查询" | | 🌍 英文 | **Cochrane Library** | `https://www.cochranelibrary.com/` | ☐ 可选 | 一级可用,系统综述金标准 | | 🇨🇳 中文 | **中国知网 CNKI** | `https://www.cnki.net/` | ☐ 可选 | 二级可达,中文文献 | | 🇨🇳 中文 | **中华医学期刊网** | `https://medjournals.cn/` | ☐ 可选 | 二级可达,中文文献 | **实现要点:** - 调用 `promptService.getPrompt('ASL_DEEP_RESEARCH_EXPANSION')` 获取 Prompt(无数据库记录时走 Fallback) - 填充变量 `{{originalQuery}}`、`{{targetSources}}`、`{{yearRange}}`、`{{targetCount}}` - 调用 `LLMFactory.getAdapter('deepseek-v3')` 执行扩写 - 解析 LLM 输出:Part 1 → `generatedRequirement`,Part 2 JSON → `intentSummary` - 创建 DB 记录,status = `draft` ### 5.2 启动执行(进入异步队列) **PUT /api/v1/asl/research/tasks/:id/execute** ```typescript // 请求 { confirmedRequirement: string // 用户核验修改后的最终指令书 } // 响应 { success: true } ``` **实现要点:** - 更新 DB 的 `confirmed_requirement` 和 `target_sources` - `jobQueue.push('asl_deep_research_v2', { taskId })` 推入 pg-boss - status 更新为 `pending` ### 5.3 任务状态与日志轮询 **GET /api/v1/asl/research/tasks/:id** ```typescript // 响应 { success: true, data: { taskId: "uuid", status: "running", // draft/pending/running/completed/failed executionLogs: [ // 终端日志(增量) { type: "think", title: "任务理解", text: "已收到检索需求...", ts: "..." }, { type: "action", title: "Search", text: "executing search across PubMed...", ts: "..." }, { type: "done", title: "搜索轮次完成", text: "", ts: "..." }, ], progress: { current: 60, total: 100 }, // 仅 completed 时有: synthesisReport: "## 研究背景\n他汀类药物...", resultList: [ { title: "...", authors: "...", journal: "...", year: 2010, type: "Meta-analysis", pmid: "...", doi: "...", pdfStatus: "OA" }, ], resultCount: 103, errorMessage: null } } ``` ### 5.4 Word 导出 **GET /api/v1/asl/research/tasks/:id/export-word** - 读取 DB 的 `synthesis_report`(Markdown)和 `result_list`(JSON) - 拼接为完整 Markdown(报告 + 文献清单表格) - 调用 Pandoc 转 Word - 返回 `.docx` 文件流 ### 5.5 路由汇总 | 方法 | 路径 | 说明 | 新增/改造 | |------|------|------|----------| | POST | `/research/generate-requirement` | 需求扩写 | **新增** | | PUT | `/research/tasks/:id/execute` | 启动执行 | **新增** | | GET | `/research/tasks/:id` | 状态+日志+结果 | **改造** | | GET | `/research/tasks/:id/export-word` | Word 导出 | **新增** | | POST | `/research/stream` | V1 SSE(保留兼容) | 不动 | | POST | `/research/tasks` | V1 异步创建(保留) | 不动 | --- ## 6. 后台 Worker 逻辑 ### 6.1 核心流程(伪代码) ```typescript // backend/src/modules/asl/workers/deepResearchV2Worker.ts export async function processDeepResearchV2(job: Job) { const { taskId } = job.data; const task = await prisma.aslResearchTask.findUnique({ where: { id: taskId } }); // 1. 调用 Unifuncs 创建异步任务 const unifuncsPayload = { model: "s2", messages: [{ role: "user", content: `请根据以下详细检索需求执行深度研究:\n${task.confirmedRequirement}` }], introduction: buildIntroduction(), max_depth: 25, domain_scope: task.targetSources || ["https://pubmed.ncbi.nlm.nih.gov/"], domain_blacklist: ["wanfang.com", "cnki.net"], output_prompt: buildOutputPrompt(), reference_style: "link", generate_summary: true, }; const createRes = await unifuncsClient.createTask(unifuncsPayload); const unifuncsTaskId = createRes.data.task_id; await prisma.aslResearchTask.update({ where: { id: taskId }, data: { externalTaskId: unifuncsTaskId, status: 'running' } }); // 2. 轮询 Unifuncs 直到完成(含指数退避重试) let previousReasoning = ''; const MAX_POLLS = 180; // 最多 15 分钟(180 × 5s) let consecutiveErrors = 0; // 连续错误计数 const MAX_CONSECUTIVE_ERRORS = 5; for (let i = 0; i < MAX_POLLS; i++) { await sleep(5000); let data: any; try { const queryRes = await unifuncsClient.queryTask(unifuncsTaskId); data = queryRes.data; consecutiveErrors = 0; // 成功后重置 } catch (err) { consecutiveErrors++; logger.warn(`Unifuncs query_task 第 ${consecutiveErrors} 次失败`, { taskId, error: err.message }); if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { throw new Error(`Unifuncs 连续 ${MAX_CONSECUTIVE_ERRORS} 次查询失败: ${err.message}`); } // 指数退避:2s → 4s → 8s → 16s → 32s const backoffMs = Math.min(2000 * Math.pow(2, consecutiveErrors - 1), 32000); await sleep(backoffMs); continue; } // 解析增量日志 const currentReasoning = data.result?.reasoning_content || ''; if (currentReasoning.length > previousReasoning.length) { const increment = currentReasoning.slice(previousReasoning.length); const newLogs = parseReasoningToLogs(increment); await appendExecutionLogs(taskId, newLogs); previousReasoning = currentReasoning; } // 同步进度 if (data.progress) { // progress 信息可通过 executionLogs 的最后一条体现 } // 检查完成 if (data.status === 'completed') { const content = data.result?.content || ''; const report = extractSection(content, 'REPORT_SECTION'); const jsonList = extractSection(content, 'JSON_LIST_SECTION'); const parsedList = safeParseJsonList(jsonList); // 防崩溃 JSON 解析,见 6.4 await prisma.aslResearchTask.update({ where: { id: taskId }, data: { status: 'completed', rawResult: content, reasoningContent: currentReasoning, synthesisReport: report || content, resultList: parsedList, resultCount: parsedList?.length || 0, tokenUsage: data.statistics?.token_usage, searchCount: data.statistics?.search_count, readCount: data.statistics?.read_count, iterations: data.statistics?.iterations, completedAt: new Date(), } }); return; } if (data.status === 'failed') { throw new Error(data.result?.content || 'Unifuncs 任务失败'); } } throw new Error('任务超时(15分钟)'); } ``` **v1.1 新增 — 轮询韧性设计:** | 机制 | 策略 | 说明 | |------|------|------| | 瞬态失败重试 | 指数退避 2s → 4s → 8s → 16s → 32s | 网络抖动、Unifuncs 临时不可用时自动恢复 | | 连续失败阈值 | `MAX_CONSECUTIVE_ERRORS = 5` | 连续 5 次查询全失败才标记任务 failed | | 成功后重置 | `consecutiveErrors = 0` | 中间穿插成功不累计 | ### 6.2 日志解析逻辑 ```typescript function parseReasoningToLogs(increment: string): LogEntry[] { const logs: LogEntry[] = []; const lines = increment.split('\n').filter(l => l.trim()); for (const line of lines) { if (line.includes('搜索') || line.includes('search') || line.includes('Search')) { logs.push({ type: 'action', title: 'Search', text: line.trim(), ts: new Date().toISOString() }); } else if (line.includes('阅读') || line.includes('read') || line.includes('Read')) { logs.push({ type: 'action', title: 'Read', text: line.trim(), ts: new Date().toISOString() }); } else if (line.includes('完成') || line.includes('成功') || line.includes('OK')) { logs.push({ type: 'done', title: '阶段完成', text: line.trim(), ts: new Date().toISOString() }); } else if (line.includes('汇总') || line.includes('总结') || line.includes('发现')) { logs.push({ type: 'summary', title: '阶段总结', text: line.trim(), ts: new Date().toISOString() }); } else if (line.trim().length > 10) { logs.push({ type: 'think', title: 'Thinking', text: line.trim(), ts: new Date().toISOString() }); } } return logs; } ``` ### 6.3 output_prompt 设计 ```typescript function buildOutputPrompt(): string { return `请严格按照以下格式输出结果: [此处撰写深度综合研究报告,使用 Markdown 格式,包括: - 研究背景与目的 - 核心发现与共识 - 分歧点与研究空白 - 参考文献列表(带编号和PubMed链接)] [此处输出文献元数据的严格 JSON 数组,每条包含: {"title":"...", "authors":"...", "journal":"...", "year":2024, "type":"RCT|Meta-analysis|Cohort|SR", "pmid":"...", "doi":"...", "pdfStatus":"OA|Restricted", "url":"https://pubmed.ncbi.nlm.nih.gov/..."}] `; } ``` ### 6.4 JSON 解析防崩溃(v1.1 新增) LLM 输出的 JSON 常携带 ` ```json ` 代码围栏或尾部逗号,直接 `JSON.parse` 会崩溃。 ```typescript function safeParseJsonList(raw: string | null): any[] | null { if (!raw) return null; // Step 1: 去除 Markdown 代码围栏 let cleaned = raw.replace(/```json\s*/gi, '').replace(/```\s*/g, ''); // Step 2: 去除尾部逗号(数组/对象末尾的 ,] 或 ,}) cleaned = cleaned.replace(/,\s*([}\]])/g, '$1'); // Step 3: 尝试解析 try { const parsed = JSON.parse(cleaned); return Array.isArray(parsed) ? parsed : [parsed]; } catch (e) { logger.warn('JSON 解析失败,尝试正则提取', { error: e.message }); // Step 4: 降级 — 尝试逐行提取 JSON 对象 const objects: any[] = []; const regex = /\{[^{}]*\}/g; let match; while ((match = regex.exec(cleaned)) !== null) { try { objects.push(JSON.parse(match[0])); } catch { /* 跳过无法解析的单条 */ } } return objects.length > 0 ? objects : null; } } ``` **防崩溃策略总结:** | 层级 | 处理 | 覆盖场景 | |------|------|---------| | L1 | 去除 ` ```json ` 围栏 | LLM 习惯性包裹代码块 | | L2 | 去除尾部逗号 | `[{...}, {...},]` → `[{...}, {...}]` | | L3 | 标准 JSON.parse | 正常路径 | | L4 | 正则逐条提取 | JSON 结构被破坏但单条仍有效 | | L5 | 返回 null | 彻底无法解析,前端降级展示报告 | --- ## 7. 前端组件设计 ### 7.1 页面结构 ``` frontend-v2/src/modules/asl/pages/ └── DeepResearchPage.tsx # V2.0 主页面(替代 ResearchSearch.tsx) frontend-v2/src/modules/asl/components/ ├── deep-research/ │ ├── LandingView.tsx # Landing 大搜索框 │ ├── SetupPanel.tsx # Step 1: 配置面板 │ ├── StrategyConfirm.tsx # Step 2: HITL 策略确认(左右分栏) │ ├── AgentTerminal.tsx # Step 3: 暗黑执行终端 │ └── ResultsView.tsx # Step 4: 结果展示(报告+表格) ``` ### 7.2 状态管理 **设计决策:** React Query + useState,不引入 Zustand(v1.1 确认) | 状态类型 | 工具 | 理由 | |---------|------|------| | 服务端数据(任务状态、日志、结果) | `@tanstack/react-query` | 自带缓存、去重、条件轮询(refetchInterval),完美匹配轮询场景 | | 页面步骤流转 | `useState` | 仅 5 个步骤的 FSM,组件树内部流转,无需全局状态 | | 组件间共享(如 taskId) | `props drilling` / `React.memo` | 组件层级仅 2-3 层,prop 传递足够,不需要 Context/Zustand | ```typescript // 页面级状态(useState 即可,不引入 Zustand) interface DeepResearchState { currentStep: 'landing' | 'setup' | 'strategy' | 'terminal' | 'results'; taskId: string | null; originalQuery: string; generatedRequirement: string; intentSummary: IntentSummary | null; isGenerating: boolean; // 需求扩写中 } // PICOS + MeSH 结构化摘要(v1.1 新增) interface IntentSummary { objective: string; population: string; intervention: string; comparison: string; outcome: string; studyDesign: string[]; meshTerms: string[]; condition: string; } ``` ### 7.3 各组件核心逻辑 **LandingView(Landing 大搜索框)** - 居中大输入框 + "开始研究"按钮 + 推荐预置词 - 点击后携带输入值,平滑过渡到 SetupPanel - 参考原型图 V4.2 的 `#landing-view` 部分 **SetupPanel(Step 1: 配置)** *(v1.1 数据源更新)* - 继承 Landing 输入值到 textarea - 数据源 Checkbox(基于实测精选 5 个,见 5.1.1): - 🌍 **英文数据源** - [x] PubMed(默认勾选,不可取消) - [ ] ClinicalTrials.gov — ⚠️ 旁标橙色提示:"该站点需要英文查询,系统将自动为此数据源生成英文检索指令" - [ ] Cochrane Library - 🇨🇳 **中文数据源** - [ ] 中国知网 CNKI - [ ] 中华医学期刊网 - 高级过滤(年份下拉、目标数量、OA 偏好 — 注意是偏好非强制) - 点击"解析并生成检索需求书" → POST /generate-requirement - Loading 后平滑展开 Step 2 **StrategyConfirm(Step 2: HITL 确认)** *(v1.1 PICOS + MeSH)* - 左侧 1/3:AI 意图提炼卡片(只读,PICOS 结构化展示) - 🎯 研究目标:`objective` - 👥 研究人群:`population` - 💊 干预措施:`intervention`(含 MeSH 英文术语) - ⚖️ 对照组:`comparison` - 📊 结局指标:`outcome` - 📋 研究设计:`studyDesign` Tag 列表 - 🏷️ MeSH 术语:`meshTerms` 小标签展示 - 右侧 2/3:可编辑 textarea(内容为 `generatedRequirement`,自然语言对话风格) - 提示文案:"这是 AI 以医学信息官的视角为您扩写的检索需求,您可以像写邮件一样直接编辑修改" - 点击"确认需求,启动 Deep Research" → PUT /execute **AgentTerminal(Step 3: 暗黑终端)** *(v1.1 条件滚动)* - 暗色背景(bg-slate-900),固定高度 550px,内部滚动 - 顶部状态栏:红/黄/绿圆点 + "Running" 脉冲指示灯 - 日志渲染: - `think` → 紫色 + 🧠 图标 - `action` → 蓝色 + 💻 图标 - `done` → 绿色 + ✅ 图标 - `summary` → 黄色 + 📋 图标 - 轮询逻辑:`useQuery` + refetchInterval: 3000(running 时启用) - **条件自动滚动**(v1.1 新增): - 用户**未手动上滚**时 → 新日志自动滚到底部 - 用户**已手动上滚**查看历史 → 停止自动滚动,避免打断阅读 - 实现:`onScroll` 检测 `scrollTop + clientHeight < scrollHeight - threshold` 设 `userScrolled` flag - 完成后状态灯变灰 "Finished",终端可折叠 **ResultsView(Step 4: 结果)** - 白色背景,与终端形成视觉分界 - 完成横幅(一行):文献数 + 耗时 + "导出 Word" 按钮 - AI 综合报告区:`react-markdown` 渲染 `synthesisReport`,可折叠,默认展开 - 文献清单表格:Ant Design Table - 列:标题(可点击跳转 PubMed)、期刊、年份、类型 Tag、PDF 状态 - 支持简单搜索过滤 - 分页(前端分页即可,数据量 ~100 条) - **降级展示**:若 `resultList` 为 null(JSON 解析失败),隐藏表格,仅展示综合报告 ### 7.4 轮询 Hook ```typescript // hooks/useDeepResearchTask.ts function useDeepResearchTask(taskId: string | null) { return useQuery({ queryKey: ['deep-research-task', taskId], queryFn: () => apiClient.get(`/api/v1/asl/research/tasks/${taskId}`), enabled: !!taskId, refetchInterval: (query) => { const status = query.state.data?.data?.status; return (status === 'pending' || status === 'running') ? 3000 : false; }, }); } ``` --- ## 8. 复用清单(不重复造轮子) | 能力 | 来源 | 用法 | |------|------|------| | LLM 调用 | `common/llm/LLMFactory` | DeepSeek-V3 需求扩写 | | **Prompt 管理** | `common/prompt/promptService` | `ASL_DEEP_RESEARCH_EXPANSION` Prompt 获取(运营端可配 + 代码 Fallback) | | pg-boss 队列 | `common/jobs/jobQueue` | Worker 注册与任务推送 | | 日志服务 | `common/logging/logger` | 全程结构化日志 | | 认证中间件 | `common/auth/authenticate` | 所有 API 路由 | | Prisma 全局实例 | `config/database` | 数据库操作 | | Word 导出 | Pandoc(Python 微服务) | 复用 Protocol Agent 验证的方案 | | DeepResearch 引擎 | `common/deepresearch/` | Unifuncs API 封装(create_task / query_task) | | 前端 API Client | `common/api/axios` | 带认证的请求 | | 前端布局 | `ASLLayout.tsx` | 左侧导航 | --- ## 9. 分阶段开发计划 ### Phase 1: 数据库 + Prompt 管理 + 需求扩写(Day 1) **目标:** 用户输入粗略想法 → AI 按 PICOS 框架扩写为结构化指令书 → 用户可编辑修改 | 任务 | 文件 | 说明 | |------|------|------| | Schema 迁移 | `prisma/schema.prisma` | 新增 6 个字段,`prisma migrate dev` | | **Prompt Fallback** | `common/prompt/prompt.fallbacks.ts` | 新增 `ASL_DEEP_RESEARCH_EXPANSION` 兜底模板(PICOS + MeSH) | | 需求扩写服务 | `services/requirementExpansionService.ts` | 新建,调用 promptService → LLMFactory → 解析 Part 1/Part 2 输出 | | 扩写 API | `controllers/researchController.ts` | 新增 `POST /generate-requirement` | | 启动 API | `controllers/researchController.ts` | 新增 `PUT /tasks/:id/execute` | | 状态 API 改造 | `controllers/researchController.ts` | 改造 `GET /tasks/:id`,返回新字段(含 PICOS intentSummary) | | **数据源配置** | `config/dataSources.ts` | 新建,定义 5 个精选数据源常量(domain、label、默认状态、备注) | | 路由注册 | `routes/index.ts` | 注册新端点 | **验收标准:** - [ ] `POST /generate-requirement` 返回 PICOS 结构化摘要 + 自然语言指令书 - [ ] Prompt 从数据库加载,无记录时自动走 Fallback - [ ] `PUT /tasks/:id/execute` 成功推入 pg-boss 队列 - [ ] `GET /tasks/:id` 返回含新字段的完整数据 - [ ] 选择 ClinicalTrials.gov 时,扩写结果包含英文检索指令段 ### Phase 2: Worker 改造 — Unifuncs 异步模式(Day 2) **目标:** Worker 使用 create_task + query_task 轮询(含指数退避),增量日志写入 DB | 任务 | 文件 | 说明 | |------|------|------| | Unifuncs 异步客户端 | `services/unifuncsAsyncClient.ts` | 新建,封装 create_task / query_task | | V2 Worker | `workers/deepResearchV2Worker.ts` | 新建,轮询 + 指数退避重试 + 日志解析 + 结果切割 | | 日志解析器 | `utils/reasoningParser.ts` | 新建,reasoning_content → 结构化日志 | | **结果解析器** | `utils/resultParser.ts` | 新建,XML 标签切割 + `safeParseJsonList` 防崩溃解析 | | Worker 注册 | `workers/researchWorker.ts` | 注册新 Worker `asl_deep_research_v2` | **验收标准:** - [ ] Worker 成功调用 unifuncs create_task - [ ] 轮询期间 execution_logs 持续增量更新 - [ ] 完成后 synthesis_report 和 result_list 正确入库 - [ ] **韧性测试**:模拟单次 query_task 失败 → 指数退避后自动恢复 - [ ] **JSON 防崩溃**:LLM 输出带 ` ```json ` 围栏 → safeParseJsonList 正确解析 - [ ] 超时保护(15 分钟)和错误处理正常 ### Phase 3: 前端 — Landing + 配置 + HITL 确认(Day 3) **目标:** 完成 Step 1-2 的前端交互,瀑布流渐进展开,PICOS 结构化展示 | 任务 | 文件 | 说明 | |------|------|------| | 主页面骨架 | `pages/DeepResearchPage.tsx` | 新建,useState 管理瀑布流步骤 | | Landing 组件 | `components/deep-research/LandingView.tsx` | 大搜索框 + 推荐预置词 | | **配置面板** | `components/deep-research/SetupPanel.tsx` | **精选 5 数据源 Checkbox**(含 ClinicalTrials.gov 英文提示)+ 高级过滤 | | **HITL 确认** | `components/deep-research/StrategyConfirm.tsx` | 左侧 **PICOS + MeSH 卡片** + 右侧可编辑 textarea | | API 函数 | `api/index.ts` | 新增 generateRequirement / executeTask | | 路由注册 | `pages/index.tsx` | 新增 V2 路由 | **验收标准:** - [ ] Landing 输入 → Step 1 配置面板流畅过渡 - [ ] 数据源显示 5 个选项,PubMed 默认勾选,ClinicalTrials.gov 标注英文提示 - [ ] 点击"生成需求书" → Loading → Step 2 展开 - [ ] Step 2 左侧 PICOS 结构化摘要 + MeSH 术语标签正确展示 - [ ] Step 2 右侧 textarea 显示自然语言对话风格的检索指令书,可编辑 - [ ] 点击"启动 Deep Research" → 进入 Step 3 ### Phase 4: 前端 — 终端 + 结果展示(Day 4) **目标:** 完成 Step 3-4,终端实时日志(条件滚动)+ 结果报告/表格(含降级) | 任务 | 文件 | 说明 | |------|------|------| | **暗黑终端** | `components/deep-research/AgentTerminal.tsx` | 日志渲染 + **条件 auto-scroll** + 状态灯 | | 结果视图 | `components/deep-research/ResultsView.tsx` | 横幅 + 报告 + 文献表格 + **降级展示** | | 轮询 Hook | `hooks/useDeepResearchTask.ts` | React Query 3s 轮询,running 时启用 | | 终端样式 | CSS / Tailwind | 暗色主题 + 日志类型着色 | **验收标准:** - [ ] 终端日志按类型着色,未手动滚动时 auto-scroll,手动上滚时暂停 - [ ] 完成后终端折叠,结果区展开 - [ ] 综合报告 Markdown 渲染正确 - [ ] 文献清单表格展示(标题可点击跳转 PubMed) - [ ] **降级验证**:resultList 为 null 时,隐藏表格仅展示报告 - [ ] 全流程端到端联调通过 ### Phase 5: Word 导出 + 收尾(Day 5) **目标:** Word 导出功能 + 全流程打磨 + 测试 | 任务 | 文件 | 说明 | |------|------|------| | Word 导出 API | `controllers/researchController.ts` | `GET /tasks/:id/export-word` | | Markdown 拼接 | `services/wordExportService.ts` | 报告 + 文献表格 → 完整 Markdown | | Pandoc 调用 | 复用 Python 微服务 | Markdown → .docx | | 前端导出按钮 | `ResultsView.tsx` | 下载 Word 文件 | | 全流程测试 | 手动 + 脚本 | 端到端验证 | | 文档更新 | 模块状态文档 | 更新 ASL 模块当前状态 | **验收标准:** - [ ] 点击"导出 Word" → 下载包含报告和文献清单的 .docx - [ ] 全流程:Landing → 配置 → 扩写 → 确认 → 执行 → 日志 → 结果 → 导出 - [ ] 离开页面回来,能恢复查看正在执行/已完成的任务 - [ ] 错误情况处理(unifuncs 超时、API 报错、网络中断) --- ## 10. 验收标准总览 ### 功能验收 - [ ] **Landing 引导**:用户输入粗略想法 → 进入配置 - [ ] **需求扩写**:AI 自动扩写为结构化自然语言指令书 - [ ] **HITL 核验**:用户可直接编辑修改指令书 - [ ] **异步执行**:pg-boss 队列,离开页面任务不中断 - [ ] **终端日志**:每 3-5s 弹出一条结构化日志 - [ ] **综合报告**:Markdown 渲染,内容来自 Unifuncs 输出 - [ ] **文献清单**:表格展示,标题可跳转 PubMed - [ ] **Word 导出**:一键导出报告 + 文献清单 ### 非功能验收 - [ ] V1.x SSE 端点保留不动(向后兼容) - [ ] 所有 API 经过 authenticate 中间件 - [ ] 日志使用 `logger`(非 console.log) - [ ] 无硬编码配置(API Key 来自环境变量) - [ ] 数据库变更通过 Prisma migrate(非 db push) --- ## 11. 风险与应对 | 风险 | 概率 | 影响 | 应对措施 | |------|------|------|---------| | Unifuncs 异步模式下 reasoning_content 不增量更新 | 低 | 终端日志为空 | 降级方案:只显示 progress.message | | output_prompt XML 标签分割不可靠 | 中 | 报告和列表无法分离 | 降级方案:整体作为报告展示,文献从 PubMed 链接提取 | | **LLM 输出 JSON 格式不规范** | 中 | 文献列表解析失败 | `safeParseJsonList` 四层防崩溃(围栏清理 → 尾逗号 → 标准解析 → 正则逐条),见 6.4 | | **Unifuncs query_task 瞬态失败** | 中 | 轮询中断 | 指数退避重试(2s→32s),连续 5 次失败才标记 failed,见 6.1 | | Unifuncs 长任务超时 | 低 | 任务失败 | MAX_POLLS=180(15分钟),超时标记 failed,用户可重试 | | **ClinicalTrials.gov 中文查询失败** | 高 | 临床试验检索无结果 | Prompt 自动为该数据源生成英文检索指令段,前端标注提示 | | Pandoc Word 导出在 SAE 不可用 | 低 | 导出失败 | 降级方案:导出为 Markdown 文件 | | **Prompt 管理服务不可用** | 低 | 需求扩写失败 | 代码内置 Fallback 模板,数据库无记录时自动使用 | --- ## 附录:v1.1 更新变更记录 | 变更项 | 章节 | 说明 | |--------|------|------| | Prompt 管理集成 | §4(新增) | 需求扩写 Prompt 通过 Prompt 管理服务配置,含 PICOS + MeSH 扩展 | | 精选数据源 | §5.1.1(新增) | 基于 18 站实测精选 5 个数据源(3英文+2中文) | | 指数退避重试 | §6.1(更新) | Worker 轮询增加瞬态失败指数退避(2s→32s) | | JSON 防崩溃 | §6.4(新增) | safeParseJsonList 四层解析策略 | | PICOS 摘要 | §7.2, §7.3(更新) | IntentSummary 扩展为 PICOS + MeSH 结构 | | 条件自动滚动 | §7.3(更新) | AgentTerminal 手动上滚时暂停 auto-scroll | | 状态管理确认 | §1.2, §7.2(更新) | 确认 React Query + useState,不引入 Zustand | | 降级展示 | §7.3(更新) | ResultsView 在 resultList=null 时仅展示报告 | --- **文档维护者:** 开发团队 **最后更新:** 2026-02-22 **文档状态:** ✅ v1.1 方案确认(含审查建议),待开发启动