Files
AIclinicalresearch/docs/03-业务模块/ASL-AI智能文献/04-开发计划/07-Deep Research V2.0 开发计划.md
HaHafeng 8f06d4f929 feat(asl): Complete Deep Research V2.0 core development
Backend:
- Add SSE streaming client (unifuncsSseClient) replacing async polling
- Add paragraph-based reasoning parser with mergeConsecutiveThinking
- Add requirement expansion service (DeepSeek-V3 PICOS+MeSH)
- Add Word export service with Pandoc, inline hyperlinks, reference link expansion
- Add deep research V2 worker with 2s log flush and Chinese source prompt
- Add 5 curated data sources config (PubMed/ClinicalTrials/Cochrane/CNKI/MedJournals)
- Add 4 API endpoints (generate-requirement/tasks/task-status/export-word)
- Update Prisma schema with 6 new V2.0 fields on AslResearchTask
- Add DB migration for V2.0 fields
- Simplify ASL_DEEP_RESEARCH_EXPANSION prompt (remove strategy section)

Frontend:
- Add waterfall-flow DeepResearchPage (phase 0-4 progressive reveal)
- Add LandingView, SetupPanel, StrategyConfirm, AgentTerminal, ResultsView
- Add react-markdown + remark-gfm for report rendering
- Add custom link component showing visible URLs after references
- Add useDeepResearchTask polling hook
- Add deep research TypeScript types

Tests:
- Add E2E test, smoke test, and Chinese data source test scripts

Docs:
- Update ASL module status (v2.0 - core features complete)
- Update system status (v6.1 - ASL V2.0 milestone)
- Update Unifuncs DeepSearch API guide (v2.0 - SSE mode + Chinese source results)
- Update module auth specification (test script guidelines)
- Update V2.0 development plan

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-23 13:21:52 +08:00

859 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
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.
# 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 `请严格按照以下格式输出结果:
<REPORT_SECTION>
[此处撰写深度综合研究报告,使用 Markdown 格式,包括:
- 研究背景与目的
- 核心发现与共识
- 分歧点与研究空白
- 参考文献列表带编号和PubMed链接]
</REPORT_SECTION>
<JSON_LIST_SECTION>
[此处输出文献元数据的严格 JSON 数组,每条包含:
{"title":"...", "authors":"...", "journal":"...", "year":2024, "type":"RCT|Meta-analysis|Cohort|SR", "pmid":"...", "doi":"...", "pdfStatus":"OA|Restricted", "url":"https://pubmed.ncbi.nlm.nih.gov/..."}]
</JSON_LIST_SECTION>`;
}
```
### 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不引入 Zustandv1.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 各组件核心逻辑
**LandingViewLanding 大搜索框)**
- 居中大输入框 + "开始研究"按钮 + 推荐预置词
- 点击后携带输入值,平滑过渡到 SetupPanel
- 参考原型图 V4.2 的 `#landing-view` 部分
**SetupPanelStep 1: 配置)** *(v1.1 数据源更新)*
- 继承 Landing 输入值到 textarea
- 数据源 Checkbox基于实测精选 5 个,见 5.1.1
- 🌍 **英文数据源**
- [x] PubMed默认勾选不可取消
- [ ] ClinicalTrials.gov — ⚠️ 旁标橙色提示:"该站点需要英文查询,系统将自动为此数据源生成英文检索指令"
- [ ] Cochrane Library
- 🇨🇳 **中文数据源**
- [ ] 中国知网 CNKI
- [ ] 中华医学期刊网
- 高级过滤年份下拉、目标数量、OA 偏好 — 注意是偏好非强制)
- 点击"解析并生成检索需求书" → POST /generate-requirement
- Loading 后平滑展开 Step 2
**StrategyConfirmStep 2: HITL 确认)** *(v1.1 PICOS + MeSH)*
- 左侧 1/3AI 意图提炼卡片只读PICOS 结构化展示)
- 🎯 研究目标:`objective`
- 👥 研究人群:`population`
- 💊 干预措施:`intervention`(含 MeSH 英文术语)
- ⚖️ 对照组:`comparison`
- 📊 结局指标:`outcome`
- 📋 研究设计:`studyDesign` Tag 列表
- 🏷️ MeSH 术语:`meshTerms` 小标签展示
- 右侧 2/3可编辑 textarea内容为 `generatedRequirement`,自然语言对话风格)
- 提示文案:"这是 AI 以医学信息官的视角为您扩写的检索需求,您可以像写邮件一样直接编辑修改"
- 点击"确认需求,启动 Deep Research" → PUT /execute
**AgentTerminalStep 3: 暗黑终端)** *(v1.1 条件滚动)*
- 暗色背景bg-slate-900固定高度 550px内部滚动
- 顶部状态栏:红/黄/绿圆点 + "Running" 脉冲指示灯
- 日志渲染:
- `think` → 紫色 + 🧠 图标
- `action` → 蓝色 + 💻 图标
- `done` → 绿色 + ✅ 图标
- `summary` → 黄色 + 📋 图标
- 轮询逻辑:`useQuery` + refetchInterval: 3000running 时启用)
- **条件自动滚动**v1.1 新增):
- 用户**未手动上滚**时 → 新日志自动滚到底部
- 用户**已手动上滚**查看历史 → 停止自动滚动,避免打断阅读
- 实现:`onScroll` 检测 `scrollTop + clientHeight < scrollHeight - threshold``userScrolled` flag
- 完成后状态灯变灰 "Finished",终端可折叠
**ResultsViewStep 4: 结果)**
- 白色背景,与终端形成视觉分界
- 完成横幅(一行):文献数 + 耗时 + "导出 Word" 按钮
- AI 综合报告区:`react-markdown` 渲染 `synthesisReport`,可折叠,默认展开
- 文献清单表格Ant Design Table
- 列:标题(可点击跳转 PubMed、期刊、年份、类型 Tag、PDF 状态
- 支持简单搜索过滤
- 分页(前端分页即可,数据量 ~100 条)
- **降级展示**:若 `resultList` 为 nullJSON 解析失败),隐藏表格,仅展示综合报告
### 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 导出 | PandocPython 微服务) | 复用 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=18015分钟超时标记 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 方案确认(含审查建议),待开发启动