From 371e1c069cfcf41b51b9ef794229990b5f8844dd Mon Sep 17 00:00:00 2001 From: HaHafeng Date: Sat, 21 Feb 2026 18:15:53 +0800 Subject: [PATCH] feat(ssa): Complete QPER architecture - Query, Planner, Execute, Reflection layers Implement the full QPER intelligent analysis pipeline: - Phase E+: Block-based standardization for all 7 R tools, DynamicReport renderer, Word export enhancement - Phase Q: LLM intent parsing with dynamic Zod validation against real column names, ClarificationCard component, DataProfile is_id_like tagging - Phase P: ConfigLoader with Zod schema validation and hot-reload API, DecisionTableService (4-dimension matching), FlowTemplateService with EPV protection, PlannedTrace audit output - Phase R: ReflectionService with statistical slot injection, sensitivity analysis conflict rules, ConclusionReport with section reveal animation, conclusion caching API, graceful R error classification End-to-end test: 40/40 passed across two complete analysis scenarios. Co-authored-by: Cursor --- backend/scripts/seed-ssa-intent-prompt.ts | 172 +++ backend/scripts/seed-ssa-reflection-prompt.ts | 174 +++ backend/scripts/test-ssa-phase-q-e2e.ts | 494 +++++++ backend/scripts/test-ssa-planner-pipeline.ts | 395 +++++ backend/scripts/test-ssa-qper-e2e.ts | 663 +++++++++ .../src/modules/ssa/config/ConfigLoader.ts | 85 ++ .../modules/ssa/config/decision_tables.json | 132 ++ .../modules/ssa/config/flow_templates.json | 69 + backend/src/modules/ssa/config/index.ts | 48 + backend/src/modules/ssa/config/schemas.ts | 91 ++ .../modules/ssa/config/tools_registry.json | 87 ++ .../src/modules/ssa/routes/config.routes.ts | 30 +- .../src/modules/ssa/routes/workflow.routes.ts | 174 +++ .../services/ConclusionGeneratorService.ts | 47 +- .../ssa/services/DataProfileService.ts | 2 + .../ssa/services/DecisionTableService.ts | 172 +++ .../ssa/services/FlowTemplateService.ts | 255 ++++ .../src/modules/ssa/services/QueryService.ts | 457 ++++++ .../modules/ssa/services/ReflectionService.ts | 341 +++++ .../ssa/services/WorkflowExecutorService.ts | 81 +- .../ssa/services/WorkflowPlannerService.ts | 164 ++- backend/src/modules/ssa/types/query.types.ts | 161 ++ .../src/modules/ssa/types/reflection.types.ts | 153 ++ .../00-系统当前状态与开发指南.md | 58 +- .../SSA-智能统计分析/00-模块当前状态与开发指南.md | 515 ++----- .../04-开发计划/09-智能化差距分析与演进路线图.md | 400 +++++ .../04-开发计划/10-QPER架构开发计划-智能化主线.md | 1297 +++++++++++++++++ .../QPER V3.0 架构审查与工程护航报告.md | 69 + .../J技术报告审核评估与建议/QPER架构审查与工程避坑指南.md | 90 ++ .../SSA-Pro MVP 智能化增强指南.md | 0 .../SSA-Pro Prompt体系与专家配置边界梳理.md | 0 .../SSA-Pro R 服务代码深度审查报告.md | 0 .../SSA-Pro V1.2 终极审查与发令报告V3.0.md | 0 .../SSA-Pro 前端 UI 改进计划审查报告.md | 0 .../SSA-Pro 前端UI改进计划-审查回应.md | 0 .../SSA-Pro 动态结果渲染与通信协议规范.md | 0 .../SSA-Pro 方案深度审查与风险评估报告 V2.0.md | 0 .../SSA-Pro 方案深度审查与风险评估报告.md | 0 .../SSA-Pro 智能化演进路径评估报告.md | 0 .../{ => J技术报告审核评估与建议}/SSA-Pro 智能化演进阶梯.md | 0 .../SSA-Pro 架构审查反馈与智能化路径讨论.md | 0 .../SSA-Pro 架构诊断与复合工具扩展方案.md | 109 ++ .../SSA-Pro-V11-UI-Development-Summary-2026-02-20.md | 0 .../{ => J技术报告审核评估与建议}/UI遮挡Bug终极修复指南.md | 0 .../J技术报告审核评估与建议/医疗AI统计助手架构研究.md | 194 +++ .../外部架构调研总结与研发共识备忘录.md | 78 + .../J技术报告审核评估与建议/智能统计分析助手开发探讨.md | 189 +++ .../J技术报告审核评估与建议/架构与产品委员会综合评估报告.md | 102 ++ .../架构审查报告:Phase 2A 核心开发计划 .md | 0 .../架构审查报告:SSA-Pro 愿景与落地策略.md | 0 .../终极架构共识与智能化演进备忘录 (1).md | 0 .../06-开发记录/SSA-QPER架构开发总结-2026-02-21.md | 108 ++ extraction_service/operations/data_profile.py | 26 + .../src/modules/aia/styles/chat-workspace.css | 6 +- .../ssa/components/ClarificationCard.tsx | 69 + .../ssa/components/ConclusionReport.tsx | 328 +++-- .../modules/ssa/components/DynamicReport.tsx | 183 +++ .../modules/ssa/components/SSAChatPane.tsx | 97 +- .../ssa/components/SSAWorkspacePane.tsx | 302 ++-- .../ssa/components/WorkflowTimeline.tsx | 26 + .../src/modules/ssa/hooks/useWorkflow.ts | 90 +- .../src/modules/ssa/styles/ssa-workspace.css | 277 ++++ frontend-v2/src/modules/ssa/types/index.ts | 73 +- .../modules/ssa/utils/exportBlocksToWord.ts | 284 ++++ r-statistics-service/plumber.R | 1 + r-statistics-service/tools/chi_square.R | 55 + r-statistics-service/tools/correlation.R | 40 +- r-statistics-service/tools/descriptive.R | 79 +- r-statistics-service/tools/logistic_binary.R | 55 +- r-statistics-service/tools/mann_whitney.R | 57 + r-statistics-service/tools/t_test_ind.R | 65 +- r-statistics-service/tools/t_test_paired.R | 94 +- r-statistics-service/utils/block_helpers.R | 85 ++ 73 files changed, 9242 insertions(+), 706 deletions(-) create mode 100644 backend/scripts/seed-ssa-intent-prompt.ts create mode 100644 backend/scripts/seed-ssa-reflection-prompt.ts create mode 100644 backend/scripts/test-ssa-phase-q-e2e.ts create mode 100644 backend/scripts/test-ssa-planner-pipeline.ts create mode 100644 backend/scripts/test-ssa-qper-e2e.ts create mode 100644 backend/src/modules/ssa/config/ConfigLoader.ts create mode 100644 backend/src/modules/ssa/config/decision_tables.json create mode 100644 backend/src/modules/ssa/config/flow_templates.json create mode 100644 backend/src/modules/ssa/config/index.ts create mode 100644 backend/src/modules/ssa/config/schemas.ts create mode 100644 backend/src/modules/ssa/config/tools_registry.json create mode 100644 backend/src/modules/ssa/services/DecisionTableService.ts create mode 100644 backend/src/modules/ssa/services/FlowTemplateService.ts create mode 100644 backend/src/modules/ssa/services/QueryService.ts create mode 100644 backend/src/modules/ssa/services/ReflectionService.ts create mode 100644 backend/src/modules/ssa/types/query.types.ts create mode 100644 backend/src/modules/ssa/types/reflection.types.ts create mode 100644 docs/03-业务模块/SSA-智能统计分析/04-开发计划/09-智能化差距分析与演进路线图.md create mode 100644 docs/03-业务模块/SSA-智能统计分析/04-开发计划/10-QPER架构开发计划-智能化主线.md create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER V3.0 架构审查与工程护航报告.md create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER架构审查与工程避坑指南.md rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro MVP 智能化增强指南.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro Prompt体系与专家配置边界梳理.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro R 服务代码深度审查报告.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro V1.2 终极审查与发令报告V3.0.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 前端 UI 改进计划审查报告.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 前端UI改进计划-审查回应.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 动态结果渲染与通信协议规范.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 方案深度审查与风险评估报告 V2.0.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 方案深度审查与风险评估报告.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 智能化演进路径评估报告.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 智能化演进阶梯.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro 架构审查反馈与智能化路径讨论.md (100%) create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 架构诊断与复合工具扩展方案.md rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/SSA-Pro-V11-UI-Development-Summary-2026-02-20.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/UI遮挡Bug终极修复指南.md (100%) create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/医疗AI统计助手架构研究.md create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/外部架构调研总结与研发共识备忘录.md create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/智能统计分析助手开发探讨.md create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构与产品委员会综合评估报告.md rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/架构审查报告:Phase 2A 核心开发计划 .md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/架构审查报告:SSA-Pro 愿景与落地策略.md (100%) rename docs/03-业务模块/SSA-智能统计分析/06-开发记录/{ => J技术报告审核评估与建议}/终极架构共识与智能化演进备忘录 (1).md (100%) create mode 100644 docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-QPER架构开发总结-2026-02-21.md create mode 100644 frontend-v2/src/modules/ssa/components/ClarificationCard.tsx create mode 100644 frontend-v2/src/modules/ssa/components/DynamicReport.tsx create mode 100644 frontend-v2/src/modules/ssa/utils/exportBlocksToWord.ts create mode 100644 r-statistics-service/utils/block_helpers.R diff --git a/backend/scripts/seed-ssa-intent-prompt.ts b/backend/scripts/seed-ssa-intent-prompt.ts new file mode 100644 index 00000000..31c7d790 --- /dev/null +++ b/backend/scripts/seed-ssa-intent-prompt.ts @@ -0,0 +1,172 @@ +/** + * SSA Intent Prompt Seed 脚本 + * + * 将 SSA_QUERY_INTENT prompt 写入 capability_schema.prompt_templates + * 运行: npx tsx scripts/seed-ssa-intent-prompt.ts + */ + +import { PrismaClient, PromptStatus } from '@prisma/client'; + +const prisma = new PrismaClient(); + +const SSA_INTENT_PROMPT = `你是一个临床统计分析意图理解引擎。你的任务是根据用户的自然语言描述和数据画像,解析出结构化的分析意图。 + +## 输入信息 + +### 用户请求 +{{userQuery}} + +### 数据画像 +{{dataProfile}} + +### 可用统计工具 +{{availableTools}} + +## 你的任务 + +请分析用户的请求,输出一个 JSON 对象(不要输出任何其他内容,只输出 JSON): + +\`\`\`json +{ + "goal": "comparison | correlation | regression | descriptive | cohort_study", + "outcome_var": "结局变量名(Y),必须是数据画像中存在的列名,如果无法确定则为 null", + "outcome_type": "continuous | binary | categorical | ordinal | datetime | null", + "predictor_vars": ["自变量名列表(X),必须是数据画像中存在的列名"], + "predictor_types": ["对应每个自变量的类型"], + "grouping_var": "分组变量名,必须是数据画像中存在的列名,如果无法确定则为 null", + "design": "independent | paired | longitudinal | cross_sectional", + "confidence": 0.0到1.0之间的数字, + "reasoning": "你的推理过程,用1-2句话说明为什么这样解析" +} +\`\`\` + +## 关键规则 + +1. **变量名必须精确匹配数据画像中的列名**,不要翻译、缩写或改写。如果数据里是 "Blood_Pressure",你就输出 "Blood_Pressure",不要输出 "BP"。 +2. 如果用户没有明确指出变量,请根据数据画像中的变量类型合理推断,但 confidence 应相应降低。 +3. goal 为 "descriptive" 时,不需要 outcome_var 和 predictor_vars。 + +## Confidence 评分准则(严格按此打分) + +- **0.9 - 1.0**: 用户的原话中明确指定了结局变量(Y)和至少一个自变量(X),且这些变量在数据画像中存在。 +- **0.7 - 0.8**: 用户指出了 Y 变量,但 X 需要根据数据类型推断;或用户的意图清晰但有轻微歧义。 +- **0.5 - 0.6**: 用户意图大致清楚(如"帮我比较一下"),但没有具体指出任何变量名。 +- **< 0.5**: 用户只说了"帮我分析一下"这样的模糊表达,既没有明确 Y 也没有明确 X,必须追问。 + +## Few-Shot 示例 + +### 示例 1:明确的差异比较 +用户: "帮我比较 Treatment 组和 Control 组的 SBP 有没有差异" +数据画像中有: Group [categorical, 2个水平: Treatment/Control], SBP [numeric] +输出: +\`\`\`json +{"goal":"comparison","outcome_var":"SBP","outcome_type":"continuous","predictor_vars":["Group"],"predictor_types":["binary"],"grouping_var":"Group","design":"independent","confidence":0.95,"reasoning":"用户明确指定了分组变量Group和结局变量SBP,要求比较两组差异"} +\`\`\` + +### 示例 2:相关分析 +用户: "年龄和血压有关系吗?" +数据画像中有: Age [numeric], Blood_Pressure [numeric], Gender [categorical] +输出: +\`\`\`json +{"goal":"correlation","outcome_var":"Blood_Pressure","outcome_type":"continuous","predictor_vars":["Age"],"predictor_types":["continuous"],"grouping_var":null,"design":"independent","confidence":0.85,"reasoning":"用户想了解Age和Blood_Pressure的关系,两者都是连续变量,适合相关分析"} +\`\`\` + +### 示例 3:多因素回归 +用户: "什么因素影响患者的死亡率?" +数据画像中有: Death [categorical, 2个水平: 0/1], Age [numeric], BMI [numeric], Smoking [categorical, 2个水平: Yes/No], Stage [categorical, 4个水平] +输出: +\`\`\`json +{"goal":"regression","outcome_var":"Death","outcome_type":"binary","predictor_vars":["Age","BMI","Smoking","Stage"],"predictor_types":["continuous","continuous","binary","categorical"],"grouping_var":null,"design":"independent","confidence":0.8,"reasoning":"用户想分析影响死亡率的因素,Death是二分类结局,其余变量作为预测因素纳入logistic回归"} +\`\`\` + +### 示例 4:模糊表达 — 需要追问 +用户: "帮我分析一下这份数据" +数据画像中有: 10个变量 +输出: +\`\`\`json +{"goal":"descriptive","outcome_var":null,"outcome_type":null,"predictor_vars":[],"predictor_types":[],"grouping_var":null,"design":"independent","confidence":0.35,"reasoning":"用户没有指定任何分析目标和变量,只能先做描述性统计,建议追问具体分析目的"} +\`\`\` + +### 示例 5:队列研究 +用户: "我想做一个完整的队列研究分析,看看新药对预后的影响" +数据画像中有: Drug [categorical, 2个水平], Outcome [categorical, 2个水平: 0/1], Age [numeric], Gender [categorical], BMI [numeric], Comorbidity [categorical] +输出: +\`\`\`json +{"goal":"cohort_study","outcome_var":"Outcome","outcome_type":"binary","predictor_vars":["Drug","Age","Gender","BMI","Comorbidity"],"predictor_types":["binary","continuous","binary","continuous","categorical"],"grouping_var":"Drug","design":"independent","confidence":0.85,"reasoning":"用户明确要做队列研究分析,Drug是暴露因素/分组变量,Outcome是结局,其余为协变量"} +\`\`\` + +请只输出 JSON,不要输出其他内容。`; + +async function main() { + console.log('🚀 开始写入 SSA Intent Prompt...\n'); + + const existing = await prisma.prompt_templates.findUnique({ + where: { code: 'SSA_QUERY_INTENT' } + }); + + if (existing) { + console.log('⚠️ SSA_QUERY_INTENT 已存在 (id=%d),创建新版本...', existing.id); + + const latestVersion = await prisma.prompt_versions.findFirst({ + where: { template_id: existing.id }, + orderBy: { version: 'desc' } + }); + + const newVersion = (latestVersion?.version ?? 0) + 1; + + // 归档旧的 ACTIVE 版本 + await prisma.prompt_versions.updateMany({ + where: { template_id: existing.id, status: 'ACTIVE' }, + data: { status: 'ARCHIVED' } + }); + + await prisma.prompt_versions.create({ + data: { + template_id: existing.id, + version: newVersion, + content: SSA_INTENT_PROMPT, + model_config: { model: 'deepseek-v3', temperature: 0.3, maxTokens: 2048 }, + status: 'ACTIVE', + changelog: `Phase Q v1.0: 5 组 Few-Shot + Confidence Rubric 客观化`, + created_by: 'system-seed', + } + }); + + console.log(' ✅ 新版本 v%d 已创建并设为 ACTIVE', newVersion); + } else { + console.log('📝 创建 SSA_QUERY_INTENT 模板...'); + + const template = await prisma.prompt_templates.create({ + data: { + code: 'SSA_QUERY_INTENT', + name: 'SSA 意图理解 Prompt', + module: 'SSA', + description: 'Phase Q — 将用户自然语言转化为结构化的统计分析意图 (ParsedQuery)', + variables: ['userQuery', 'dataProfile', 'availableTools'], + } + }); + + await prisma.prompt_versions.create({ + data: { + template_id: template.id, + version: 1, + content: SSA_INTENT_PROMPT, + model_config: { model: 'deepseek-v3', temperature: 0.3, maxTokens: 2048 }, + status: 'ACTIVE', + changelog: 'Phase Q v1.0: 初始版本,5 组 Few-Shot + Confidence Rubric', + created_by: 'system-seed', + } + }); + + console.log(' ✅ 模板 id=%d + 版本 v1 已创建', template.id); + } + + console.log('\n✅ SSA Intent Prompt 写入完成!'); +} + +main() + .catch(e => { + console.error('❌ 写入失败:', e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/seed-ssa-reflection-prompt.ts b/backend/scripts/seed-ssa-reflection-prompt.ts new file mode 100644 index 00000000..54107752 --- /dev/null +++ b/backend/scripts/seed-ssa-reflection-prompt.ts @@ -0,0 +1,174 @@ +/** + * SSA Reflection Prompt Seed 脚本 + * + * 将 SSA_REFLECTION prompt 写入 capability_schema.prompt_templates + * 运行: npx tsx scripts/seed-ssa-reflection-prompt.ts + * + * Phase R — 论文级结论生成 Prompt + * 特性: + * - 统计量槽位注入(反幻觉) + * - 敏感性分析冲突处理准则 + * - 6 要素结构化 JSON 输出 + * - 基于 decision_trace 的方法学说明 + */ + +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +const SSA_REFLECTION_PROMPT = `你是一位高级生物统计师,请基于以下分析结果生成论文级结论。 + +## 分析背景 + +分析目标:{{goal}} +分析标题:{{title}} +采用方法:{{methodology}} +样本信息:{{sampleInfo}} + +## 方法选择的决策轨迹 + +请据此撰写方法学说明,不得臆造选择理由: + +匹配规则:{{decision_trace.matched_rule}} +主要工具:{{decision_trace.primary_tool}} +{{#if decision_trace.fallback_tool}}备选工具:{{decision_trace.fallback_tool}}(触发条件:{{decision_trace.switch_condition}}){{/if}} +决策推理:{{decision_trace.reasoning}} +{{#if decision_trace.epv_warning}}⚠️ EPV 警告:{{decision_trace.epv_warning}}{{/if}} + +## 各步骤统计结果 + +⚠️ 以下数值由系统自动注入,你必须原样引用,不得修改、四舍五入或重新表述任何数字。 + +{{#each findings}} +### 步骤 {{step_number}}:{{tool_name}}({{tool_code}}) +- 使用方法:{{method}} +{{#if statistic}}- 统计量({{statistic_name}}):{{statistic}}{{/if}} +{{#if p_value}}- P 值:{{p_value}}{{/if}} +{{#if effect_size}}- 效应量({{effect_size_name}}):{{effect_size}}{{/if}} +{{#if ci_lower}}- 95% 置信区间:{{ci_lower}} ~ {{ci_upper}}{{/if}} +- 显著性:{{#if is_significant}}显著(P < 0.05){{else}}不显著(P ≥ 0.05){{/if}} +{{#if group_stats}} +- 各组统计: +{{#each group_stats}} · {{group}} 组:n={{n}}{{#if mean}}, 均值={{mean}}, SD={{sd}}{{/if}}{{#if median}}, 中位数={{median}}{{/if}} +{{/each}} +{{/if}} + +{{/each}} + +## 冲突处理准则(强制执行) + +当主分析与敏感性分析的显著性结论不一致时: +1. 在 limitations 数组中必须包含:"敏感性分析未得到一致结论,结果的稳健性(Robustness)较弱,需谨慎解释临床意义" +2. 在 key_findings 中以主分析结果为基准报告,但需加注"(注:敏感性分析未验证此结论)" +3. 严禁选择性报告、强行拼凑显著性 +4. 当所有分析方向一致时,在 key_findings 中可强调"敏感性分析进一步验证了结论的稳健性" + +## 输出要求 + +请输出一个严格的 JSON 对象(不要输出任何其他内容,只输出 JSON),包含以下字段: + +\`\`\`json +{ + "executive_summary": "200-500字的综合摘要,涵盖研究目的、主要发现和临床意义。使用论文'结果'章节的行文风格,可直接复制到论文中。引用统计量时必须与上方数值完全一致。", + "key_findings": [ + "发现1:具体描述,含统计量和P值(必须与上方数值一致)", + "发现2:..." + ], + "statistical_summary": { + "total_tests": 3, + "significant_results": 2, + "methods_used": ["独立样本T检验", "Mann-Whitney U检验"] + }, + "methodology": "方法学说明段落:基于上方决策轨迹撰写,解释为什么选择此方法、是否发生降级、数据满足了哪些假设。使用论文'方法'章节的行文风格。", + "limitations": [ + "局限性1", + "局限性2" + ], + "recommendations": [ + "建议1:基于分析结果的后续研究建议", + "建议2:..." + ] +} +\`\`\` + +## 关键规则 + +1. **所有数值必须与上方统计结果完全一致**,不得修改、四舍五入、近似或重新表述 +2. executive_summary 必须是完整的论文段落,不是要点列表 +3. key_findings 每条必须包含具体的统计量和 P 值 +4. methodology 必须基于决策轨迹撰写,说明方法选择的理由 +5. limitations 至少包含 1 条,如果数据有局限则如实报告 +6. 请只输出 JSON,不要输出其他内容`; + +async function main() { + console.log('🚀 开始写入 SSA Reflection Prompt...\n'); + + const existing = await prisma.prompt_templates.findUnique({ + where: { code: 'SSA_REFLECTION' } + }); + + if (existing) { + console.log('⚠️ SSA_REFLECTION 已存在 (id=%d),创建新版本...', existing.id); + + const latestVersion = await prisma.prompt_versions.findFirst({ + where: { template_id: existing.id }, + orderBy: { version: 'desc' } + }); + + const newVersion = (latestVersion?.version ?? 0) + 1; + + await prisma.prompt_versions.updateMany({ + where: { template_id: existing.id, status: 'ACTIVE' }, + data: { status: 'ARCHIVED' } + }); + + await prisma.prompt_versions.create({ + data: { + template_id: existing.id, + version: newVersion, + content: SSA_REFLECTION_PROMPT, + model_config: { model: 'deepseek-v3', temperature: 0.3, maxTokens: 4096 }, + status: 'ACTIVE', + changelog: `Phase R v1.0: 6要素结构化JSON + 槽位注入 + 冲突处理准则`, + created_by: 'system-seed', + } + }); + + console.log(' ✅ 新版本 v%d 已创建并设为 ACTIVE', newVersion); + } else { + console.log('📝 创建 SSA_REFLECTION 模板...'); + + const template = await prisma.prompt_templates.create({ + data: { + code: 'SSA_REFLECTION', + name: 'SSA 论文级结论生成 Prompt', + module: 'SSA', + description: 'Phase R — 将 StepResult[] 转化为论文级结论(6要素结构化JSON,含槽位注入反幻觉 + 敏感性冲突准则)', + variables: ['goal', 'title', 'methodology', 'sampleInfo', 'decision_trace', 'findings'], + } + }); + + await prisma.prompt_versions.create({ + data: { + template_id: template.id, + version: 1, + content: SSA_REFLECTION_PROMPT, + model_config: { model: 'deepseek-v3', temperature: 0.3, maxTokens: 4096 }, + status: 'ACTIVE', + changelog: 'Phase R v1.0: 初始版本,6要素JSON + 槽位注入 + 冲突处理准则', + created_by: 'system-seed', + } + }); + + console.log(' ✅ 模板 id=%d + 版本 v1 已创建', template.id); + } + + console.log('\n✅ SSA Reflection Prompt 写入完成!'); +} + +main() + .catch(e => { + console.error('❌ 写入失败:', e); + process.exit(1); + }) + .finally(() => prisma.$disconnect()); diff --git a/backend/scripts/test-ssa-phase-q-e2e.ts b/backend/scripts/test-ssa-phase-q-e2e.ts new file mode 100644 index 00000000..b6aa45a8 --- /dev/null +++ b/backend/scripts/test-ssa-phase-q-e2e.ts @@ -0,0 +1,494 @@ +/** + * SSA Phase Q — 端到端集成测试 + * + * 完整链路:登录 → 创建会话+上传文件 → 数据画像 → LLM 意图解析 → 追问 → Q→P 规划 + * + * 依赖:Node.js 后端 + PostgreSQL + Python extraction_service + LLM 服务 + * 运行方式:npx tsx scripts/test-ssa-phase-q-e2e.ts + * + * 测试数据:docs/03-业务模块/SSA-智能统计分析/05-测试文档/test.csv + * 测试用户:13800000001 / 123456 + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const BASE_URL = 'http://localhost:3000'; +const TEST_PHONE = '13800000001'; +const TEST_PASSWORD = '123456'; +const TEST_CSV_PATH = join(__dirname, '../../docs/03-业务模块/SSA-智能统计分析/05-测试文档/test.csv'); + +// ──────────────────────────────────────────── +// 工具函数 +// ──────────────────────────────────────────── + +let passed = 0; +let failed = 0; +let skipped = 0; +let token = ''; +let sessionId = ''; + +function assert(condition: boolean, testName: string, detail?: string) { + if (condition) { + console.log(` ✅ ${testName}`); + passed++; + } else { + console.log(` ❌ ${testName}${detail ? ` — ${detail}` : ''}`); + failed++; + } +} + +function skip(testName: string, reason: string) { + console.log(` ⏭️ ${testName} — 跳过:${reason}`); + skipped++; +} + +function section(title: string) { + console.log(`\n${'─'.repeat(60)}`); + console.log(`📋 ${title}`); + console.log('─'.repeat(60)); +} + +function authHeaders(contentType?: string): Record { + const headers: Record = { + 'Authorization': `Bearer ${token}`, + }; + if (contentType) { + headers['Content-Type'] = contentType; + } + return headers; +} + +async function apiPost(path: string, body: any, headers?: Record): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + method: 'POST', + headers: headers || authHeaders('application/json'), + body: typeof body === 'string' ? body : JSON.stringify(body), + }); + const text = await res.text(); + try { + return { status: res.status, data: JSON.parse(text) }; + } catch { + return { status: res.status, data: text }; + } +} + +// ──────────────────────────────────────────── +// 测试 1: 登录获取 Token +// ──────────────────────────────────────────── + +async function testLogin() { + section('测试 1: 登录认证'); + + try { + const res = await apiPost('/api/v1/auth/login/password', { + phone: TEST_PHONE, + password: TEST_PASSWORD, + }, { 'Content-Type': 'application/json' }); + + assert(res.status === 200, `登录返回 200(实际 ${res.status})`, JSON.stringify(res.data).substring(0, 200)); + + if (res.status === 200 && res.data) { + token = res.data?.data?.tokens?.accessToken || res.data?.accessToken || res.data?.token || ''; + assert(token.length > 0, '获取到 JWT Token', `token 长度: ${token.length}`); + + if (res.data?.data?.user) { + const user = res.data.data.user; + console.log(` 用户信息: ${user.name || user.phone || 'N/A'}, 角色: ${user.role}`); + } + } + } catch (e: any) { + assert(false, '登录请求失败', e.message); + } + + if (!token) { + console.log('\n ⚠️ Token 获取失败,后续测试无法继续'); + return false; + } + return true; +} + +// ──────────────────────────────────────────── +// 测试 2: 创建会话 + 上传 test.csv +// ──────────────────────────────────────────── + +async function testCreateSessionWithUpload() { + section('测试 2: 创建会话 + 上传 test.csv'); + + try { + const csvBuffer = readFileSync(TEST_CSV_PATH); + assert(csvBuffer.length > 0, `test.csv 文件读取成功(${csvBuffer.length} bytes)`); + + // 构建 multipart/form-data + const formData = new FormData(); + const blob = new Blob([csvBuffer], { type: 'text/csv' }); + formData.append('file', blob, 'test.csv'); + + const res = await fetch(`${BASE_URL}/api/v1/ssa/sessions/`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: formData, + }); + + const data = await res.json(); + assert(res.status === 200, `创建会话返回 200(实际 ${res.status})`, JSON.stringify(data).substring(0, 300)); + + if (data.sessionId) { + sessionId = data.sessionId; + assert(true, `会话 ID: ${sessionId}`); + } else { + assert(false, '未返回 sessionId', JSON.stringify(data).substring(0, 200)); + } + + if (data.schema) { + const schema = data.schema; + assert(schema.columns?.length > 0, `数据 Schema 解析成功(${schema.columns?.length} 列)`); + assert(schema.rowCount > 0, `行数: ${schema.rowCount}`); + console.log(` 列名: ${schema.columns?.slice(0, 8).map((c: any) => c.name).join(', ')}...`); + } + } catch (e: any) { + assert(false, '创建会话失败', e.message); + } + + if (!sessionId) { + console.log('\n ⚠️ SessionId 获取失败,后续测试无法继续'); + return false; + } + return true; +} + +// ──────────────────────────────────────────── +// 测试 3: 数据画像(Python DataProfiler) +// ──────────────────────────────────────────── + +async function testDataProfile() { + section('测试 3: 数据画像(Python DataProfiler)'); + + try { + const res = await apiPost('/api/v1/ssa/workflow/profile', { sessionId }); + + assert(res.status === 200, `画像请求返回 200(实际 ${res.status})`); + + if (res.data?.success) { + const profile = res.data.profile; + assert(!!profile, '画像数据非空'); + + if (profile) { + assert(profile.row_count > 0 || profile.totalRows > 0, + `行数: ${profile.row_count || profile.totalRows}`); + assert(profile.column_count > 0 || profile.totalColumns > 0, + `列数: ${profile.column_count || profile.totalColumns}`); + + const cols = profile.columns || []; + if (cols.length > 0) { + console.log(` 前 5 列类型:`); + cols.slice(0, 5).forEach((c: any) => { + console.log(` ${c.name || c.column_name}: ${c.type || c.dtype} (missing: ${c.missing_ratio ?? c.missingPercent ?? 'N/A'})`); + }); + + // 检查 is_id_like 标记(Phase Q 防御性优化) + const idLikeCols = cols.filter((c: any) => c.is_id_like === true); + if (idLikeCols.length > 0) { + assert(true, `检测到 ${idLikeCols.length} 个 ID-like 列: ${idLikeCols.map((c: any) => c.name || c.column_name).join(', ')}`); + } else { + console.log(' ℹ️ 未检测到 ID-like 列(test.csv 无 ID 列,符合预期)'); + } + } + } + } else { + assert(false, '画像生成失败', res.data?.error || JSON.stringify(res.data).substring(0, 200)); + } + } catch (e: any) { + assert(false, '画像请求异常', e.message); + } +} + +// ──────────────────────────────────────────── +// 测试 4: LLM 意图解析(Phase Q 核心) +// ──────────────────────────────────────────── + +async function testIntentParsing() { + section('测试 4: LLM 意图理解(Phase Q 核心)'); + + const testQueries = [ + { + name: '场景 A — 明确的差异比较', + query: '比较 sex 不同组的 Yqol 有没有差别', + expectGoal: 'comparison', + expectHighConfidence: true, + }, + { + name: '场景 B — 相关分析', + query: '分析 age 和 bmi 的相关性', + expectGoal: 'correlation', + expectHighConfidence: true, + }, + { + name: '场景 C — 回归分析', + query: 'age、smoke、bmi 对 Yqol 的影响,做个多因素分析', + expectGoal: 'regression', + expectHighConfidence: true, + }, + { + name: '场景 D — 模糊意图(应触发追问)', + query: '帮我分析一下这个数据', + expectGoal: null, // 不确定 + expectHighConfidence: false, + }, + { + name: '场景 E — 描述统计', + query: '描述一下数据的基本情况', + expectGoal: 'descriptive', + expectHighConfidence: true, + }, + ]; + + for (const tc of testQueries) { + console.log(`\n 🔬 ${tc.name}`); + console.log(` Query: "${tc.query}"`); + + try { + const startTime = Date.now(); + const res = await apiPost('/api/v1/ssa/workflow/intent', { + sessionId, + userQuery: tc.query, + }); + const elapsed = Date.now() - startTime; + + assert(res.status === 200, ` 返回 200(实际 ${res.status})`, res.data?.error); + + if (res.data?.success) { + const intent = res.data.intent; + console.log(` 耗时: ${elapsed}ms`); + console.log(` Goal: ${intent?.goal}, Confidence: ${intent?.confidence}`); + console.log(` Y: ${intent?.outcome_var || 'null'}, X: ${JSON.stringify(intent?.predictor_vars || [])}`); + console.log(` Design: ${intent?.design}, needsClarification: ${intent?.needsClarification}`); + + if (intent) { + // 检查 goal 是否符合预期 + if (tc.expectGoal) { + assert(intent.goal === tc.expectGoal, + ` Goal = ${tc.expectGoal}(实际 ${intent.goal})`); + } + + // 检查置信度 + if (tc.expectHighConfidence) { + assert(intent.confidence >= 0.7, + ` 高置信度 >= 0.7(实际 ${intent.confidence})`); + assert(!intent.needsClarification, + ` 无需追问(实际 needsClarification=${intent.needsClarification})`); + } else { + // 模糊意图应该低置信度或触发追问 + const isLowConfOrClarify = intent.confidence < 0.7 || intent.needsClarification; + assert(isLowConfOrClarify, + ` 低置信度或需追问(confidence=${intent.confidence}, needsClarification=${intent.needsClarification})`); + } + + // 检查变量名是否来自真实数据(防幻觉校验) + const realColumns = ['sex', 'smoke', 'age', 'bmi', 'mouth_open', 'bucal_relax', + 'toot_morph', 'root_number', 'root_curve', 'lenspace', 'denseratio', + 'Pglevel', 'Pgverti', 'Winter', 'presyp', 'flap', 'operation', + 'time', 'surgage', 'Yqol', 'times']; + const realColumnsLower = realColumns.map(c => c.toLowerCase()); + + if (intent.outcome_var) { + const isReal = realColumnsLower.includes(intent.outcome_var.toLowerCase()); + assert(isReal, + ` Y 变量 "${intent.outcome_var}" 存在于数据中`, + `变量 "${intent.outcome_var}" 不在数据列名中(可能是 LLM 幻觉)`); + } + + if (intent.predictor_vars?.length > 0) { + const allReal = intent.predictor_vars.every( + (v: string) => realColumnsLower.includes(v.toLowerCase()) + ); + assert(allReal, + ` X 变量 ${JSON.stringify(intent.predictor_vars)} 全部存在于数据中`, + `部分变量可能为 LLM 幻觉`); + } + + // 检查追问卡片(模糊意图时) + if (intent.needsClarification && res.data.clarificationCards?.length > 0) { + const cards = res.data.clarificationCards; + console.log(` 追问卡片: ${cards.length} 张`); + cards.forEach((card: any, i: number) => { + console.log(` 卡片 ${i + 1}: ${card.question}`); + card.options?.slice(0, 3).forEach((opt: any) => { + console.log(` - ${opt.label}`); + }); + }); + assert(cards[0].options?.length >= 2, ` 追问卡片有 >= 2 个选项`); + } + } + } else { + assert(false, ` Intent 解析失败`, res.data?.error || JSON.stringify(res.data).substring(0, 200)); + } + } catch (e: any) { + assert(false, ` ${tc.name} 请求异常`, e.message); + } + } +} + +// ──────────────────────────────────────────── +// 测试 5: Q→P 全链路(Intent → Plan) +// ──────────────────────────────────────────── + +async function testQtoPPipeline() { + section('测试 5: Q→P 全链路(Intent → WorkflowPlan)'); + + const testCases = [ + { + name: '差异比较 → T 检验流程', + query: '比较 sex 不同组的 Yqol 有没有差别', + expectSteps: 2, // 描述统计 + 主分析(至少) + expectTool: 'ST_', + }, + { + name: '回归分析 → Logistic 流程', + query: 'age、smoke、bmi 对 Yqol 的预测作用,做个 Logistic 回归', + expectSteps: 2, + expectTool: 'ST_LOGISTIC', + }, + ]; + + for (const tc of testCases) { + console.log(`\n 🔬 ${tc.name}`); + console.log(` Query: "${tc.query}"`); + + try { + const startTime = Date.now(); + const res = await apiPost('/api/v1/ssa/workflow/plan', { + sessionId, + userQuery: tc.query, + }); + const elapsed = Date.now() - startTime; + + assert(res.status === 200, ` 返回 200(实际 ${res.status})`, res.data?.error); + + if (res.data?.success && res.data.plan) { + const plan = res.data.plan; + console.log(` 耗时: ${elapsed}ms`); + console.log(` 标题: ${plan.title}`); + console.log(` 步骤数: ${plan.total_steps}`); + + assert(plan.total_steps >= tc.expectSteps, + ` 步骤数 >= ${tc.expectSteps}(实际 ${plan.total_steps})`); + + // 打印每步信息 + plan.steps?.forEach((step: any, i: number) => { + const sensitivity = step.is_sensitivity ? ' [敏感性分析]' : ''; + const guardrail = step.switch_condition ? ` 🛡️${step.switch_condition}` : ''; + console.log(` 步骤 ${i + 1}: ${step.tool_name} (${step.tool_code})${sensitivity}${guardrail}`); + }); + + // 检查是否包含期望的工具 + const hasExpectedTool = plan.steps?.some( + (s: any) => s.tool_code?.startsWith(tc.expectTool) + ); + assert(hasExpectedTool, + ` 包含 ${tc.expectTool}* 工具`, + `工具列表: ${plan.steps?.map((s: any) => s.tool_code).join(', ')}`); + + // 检查 PlannedTrace + if (plan.planned_trace) { + const trace = plan.planned_trace; + console.log(` PlannedTrace:`); + console.log(` Primary: ${trace.primaryTool}`); + console.log(` Fallback: ${trace.fallbackTool || 'null'}`); + console.log(` SwitchCondition: ${trace.switchCondition || 'null'}`); + console.log(` Template: ${trace.templateUsed}`); + assert(!!trace.primaryTool, ` PlannedTrace 包含 primaryTool`); + assert(!!trace.templateUsed, ` PlannedTrace 包含 templateUsed`); + } else { + skip('PlannedTrace 检查', '计划中未返回 planned_trace'); + } + + // EPV 警告检查 + if (plan.epv_warning) { + console.log(` ⚠️ EPV Warning: ${plan.epv_warning}`); + } + + // 描述文字检查 + if (plan.description) { + assert(plan.description.length > 10, ` 规划描述非空(${plan.description.length} 字符)`); + console.log(` 描述: ${plan.description.substring(0, 100)}...`); + } + } else { + assert(false, ` 规划失败`, res.data?.error || JSON.stringify(res.data).substring(0, 200)); + } + } catch (e: any) { + assert(false, ` ${tc.name} 请求异常`, e.message); + } + } +} + +// ──────────────────────────────────────────── +// 运行所有测试 +// ──────────────────────────────────────────── + +async function main() { + console.log('\n🧪 SSA Phase Q+P — 端到端集成测试\n'); + console.log('测试链路:登录 → 上传 CSV → 数据画像 → LLM Intent → Q→P Plan'); + console.log(`测试用户:${TEST_PHONE}`); + console.log(`后端地址:${BASE_URL}`); + console.log(`测试文件:${TEST_CSV_PATH}\n`); + + // 前置检查 + try { + readFileSync(TEST_CSV_PATH); + } catch { + console.error('❌ test.csv 文件不存在,请检查路径'); + process.exit(1); + } + + try { + const healthCheck = await fetch(`${BASE_URL}/health`).catch(() => null); + if (!healthCheck || healthCheck.status !== 200) { + console.error('❌ 后端服务未启动或不可达'); + process.exit(1); + } + console.log('✅ 后端服务可达\n'); + } catch { + console.error('❌ 后端服务未启动或不可达'); + process.exit(1); + } + + // 顺序执行测试 + const loginOk = await testLogin(); + if (!loginOk) { + console.log('\n⛔ 登录失败,终止测试'); + process.exit(1); + } + + const sessionOk = await testCreateSessionWithUpload(); + if (!sessionOk) { + console.log('\n⛔ 会话创建失败,终止测试'); + process.exit(1); + } + + await testDataProfile(); + await testIntentParsing(); + await testQtoPPipeline(); + + // 汇总 + console.log(`\n${'═'.repeat(60)}`); + console.log(`📊 测试结果汇总:${passed} 通过 / ${failed} 失败 / ${skipped} 跳过 / ${passed + failed + skipped} 总计`); + if (failed === 0) { + console.log('🎉 全部通过!Phase Q+P 端到端验证成功。'); + } else { + console.log(`⚠️ 有 ${failed} 个测试失败,请检查上方输出。`); + } + console.log(`\n📝 测试会话 ID: ${sessionId}(可在数据库中查询详情)`); + console.log('═'.repeat(60)); + + process.exit(failed > 0 ? 1 : 0); +} + +main().catch(e => { + console.error('💥 测试脚本异常:', e); + process.exit(1); +}); diff --git a/backend/scripts/test-ssa-planner-pipeline.ts b/backend/scripts/test-ssa-planner-pipeline.ts new file mode 100644 index 00000000..aa7a6331 --- /dev/null +++ b/backend/scripts/test-ssa-planner-pipeline.ts @@ -0,0 +1,395 @@ +/** + * SSA Phase P — Tracer Bullet 测试脚本 + * + * 验证范围: + * 1. ConfigLoader 加载 + Zod 校验 3 个 JSON 配置文件 + * 2. DecisionTableService 四维匹配(6 种场景) + * 3. FlowTemplateService 模板填充 + EPV 截断 + * 4. Q→P 集成:mock ParsedQuery → WorkflowPlan + * + * 运行方式:npx tsx scripts/test-ssa-planner-pipeline.ts + * 不依赖:数据库、LLM、R 引擎 + */ + +import { toolsRegistryLoader, decisionTablesLoader, flowTemplatesLoader, reloadAllConfigs } from '../src/modules/ssa/config/index.js'; +import { decisionTableService } from '../src/modules/ssa/services/DecisionTableService.js'; +import { flowTemplateService } from '../src/modules/ssa/services/FlowTemplateService.js'; +import type { ParsedQuery } from '../src/modules/ssa/types/query.types.js'; +import type { DataProfile } from '../src/modules/ssa/services/DataProfileService.js'; + +// ──────────────────────────────────────────── +// 工具函数 +// ──────────────────────────────────────────── + +let passed = 0; +let failed = 0; + +function assert(condition: boolean, testName: string, detail?: string) { + if (condition) { + console.log(` ✅ ${testName}`); + passed++; + } else { + console.log(` ❌ ${testName}${detail ? ` — ${detail}` : ''}`); + failed++; + } +} + +function section(title: string) { + console.log(`\n${'─'.repeat(60)}`); + console.log(`📋 ${title}`); + console.log('─'.repeat(60)); +} + +// ──────────────────────────────────────────── +// Mock 数据 +// ──────────────────────────────────────────── + +function makeParsedQuery(overrides: Partial): ParsedQuery { + return { + goal: 'comparison', + outcome_var: 'BP', + outcome_type: 'continuous', + predictor_vars: ['Drug'], + predictor_types: ['binary'], + grouping_var: 'Drug', + design: 'independent', + confidence: 0.9, + reasoning: 'test', + needsClarification: false, + ...overrides, + }; +} + +function makeMockProfile(outcomeVar: string, minEventCount: number): DataProfile { + return { + totalRows: 200, + totalColumns: 10, + columns: [ + { + name: outcomeVar, + type: 'categorical', + missing: 0, + missingPercent: 0, + unique: 2, + topValues: [ + { value: '0', count: 200 - minEventCount }, + { value: '1', count: minEventCount }, + ], + }, + { name: 'Age', type: 'numeric', missing: 0, missingPercent: 0, unique: 50 }, + { name: 'Sex', type: 'categorical', missing: 0, missingPercent: 0, unique: 2 }, + { name: 'BMI', type: 'numeric', missing: 5, missingPercent: 2.5, unique: 80 }, + { name: 'Smoking', type: 'categorical', missing: 0, missingPercent: 0, unique: 2 }, + { name: 'SBP', type: 'numeric', missing: 0, missingPercent: 0, unique: 100 }, + ], + } as any; +} + +// ──────────────────────────────────────────── +// 测试 1: ConfigLoader + Zod 校验 +// ──────────────────────────────────────────── + +function testConfigLoading() { + section('测试 1: ConfigLoader 加载 + Zod 校验'); + + try { + const tools = toolsRegistryLoader.get(); + assert(!!tools, '工具注册表加载成功'); + assert(tools.tools.length >= 7, `工具数量 >= 7(实际 ${tools.tools.length})`); + assert(tools.tools.every(t => /^ST_[A-Z_]+$/.test(t.code)), '所有工具 code 格式正确 (ST_XXX)'); + + const toolCodes = tools.tools.map(t => t.code); + assert(toolCodes.includes('ST_DESCRIPTIVE'), '包含 ST_DESCRIPTIVE'); + assert(toolCodes.includes('ST_T_TEST_IND'), '包含 ST_T_TEST_IND'); + assert(toolCodes.includes('ST_LOGISTIC_BINARY'), '包含 ST_LOGISTIC_BINARY'); + } catch (e: any) { + assert(false, '工具注册表加载失败', e.message); + } + + try { + const rules = decisionTablesLoader.get(); + assert(!!rules, '决策表加载成功'); + assert(rules.length >= 9, `规则数量 >= 9(实际 ${rules.length})`); + assert(rules.every(r => r.id && r.goal && r.primaryTool), '所有规则含必填字段'); + + const ids = rules.map(r => r.id); + assert(ids.includes('DESC_ANY'), '包含 DESC_ANY 兜底规则'); + assert(ids.includes('COHORT_STUDY'), '包含 COHORT_STUDY 队列研究规则'); + } catch (e: any) { + assert(false, '决策表加载失败', e.message); + } + + try { + const templates = flowTemplatesLoader.get(); + assert(!!templates, '流程模板加载成功'); + assert(templates.templates.length >= 5, `模板数量 >= 5(实际 ${templates.templates.length})`); + + const ids = templates.templates.map(t => t.id); + assert(ids.includes('standard_analysis'), '包含 standard_analysis 模板'); + assert(ids.includes('cohort_study_standard'), '包含 cohort_study_standard 模板'); + assert(ids.includes('descriptive_only'), '包含 descriptive_only 模板'); + } catch (e: any) { + assert(false, '流程模板加载失败', e.message); + } + + // 热更新测试 + try { + const results = reloadAllConfigs(); + assert(results.every(r => r.success), `热更新全部成功(${results.length} 个文件)`); + } catch (e: any) { + assert(false, '热更新失败', e.message); + } +} + +// ──────────────────────────────────────────── +// 测试 2: DecisionTableService 四维匹配 +// ──────────────────────────────────────────── + +function testDecisionTableMatching() { + section('测试 2: DecisionTableService 四维匹配'); + + // 场景 A: 两组连续变量差异比较(独立样本)→ T 检验 + Mann-Whitney fallback + const queryA = makeParsedQuery({ + goal: 'comparison', + outcome_type: 'continuous', + predictor_types: ['binary'], + design: 'independent', + }); + const matchA = decisionTableService.match(queryA); + assert(matchA.primaryTool === 'ST_T_TEST_IND', `场景 A: Primary = ST_T_TEST_IND(实际 ${matchA.primaryTool})`); + assert(matchA.fallbackTool === 'ST_MANN_WHITNEY', `场景 A: Fallback = ST_MANN_WHITNEY(实际 ${matchA.fallbackTool})`); + assert(matchA.switchCondition !== null, '场景 A: 有 switchCondition(正态性检验)'); + assert(matchA.templateId === 'standard_analysis', `场景 A: Template = standard_analysis(实际 ${matchA.templateId})`); + + // 场景 B: 配对设计 + const queryB = makeParsedQuery({ + goal: 'comparison', + outcome_type: 'continuous', + predictor_types: ['binary'], + design: 'paired', + }); + const matchB = decisionTableService.match(queryB); + assert(matchB.primaryTool === 'ST_T_TEST_PAIRED', `场景 B: Primary = ST_T_TEST_PAIRED(实际 ${matchB.primaryTool})`); + assert(matchB.templateId === 'paired_analysis', `场景 B: Template = paired_analysis(实际 ${matchB.templateId})`); + + // 场景 C: 分类 vs 分类 → 卡方检验 + const queryC = makeParsedQuery({ + goal: 'comparison', + outcome_type: 'categorical', + predictor_types: ['categorical'], + design: 'independent', + }); + const matchC = decisionTableService.match(queryC); + assert(matchC.primaryTool === 'ST_CHI_SQUARE', `场景 C: Primary = ST_CHI_SQUARE(实际 ${matchC.primaryTool})`); + + // 场景 D: 相关分析(连续 vs 连续) + const queryD = makeParsedQuery({ + goal: 'correlation', + outcome_type: 'continuous', + predictor_types: ['continuous'], + design: 'independent', + }); + const matchD = decisionTableService.match(queryD); + assert(matchD.primaryTool === 'ST_CORRELATION', `场景 D: Primary = ST_CORRELATION(实际 ${matchD.primaryTool})`); + + // 场景 E: Logistic 回归 + const queryE = makeParsedQuery({ + goal: 'regression', + outcome_type: 'binary', + predictor_types: ['continuous'], + design: 'independent', + }); + const matchE = decisionTableService.match(queryE); + assert(matchE.primaryTool === 'ST_LOGISTIC_BINARY', `场景 E: Primary = ST_LOGISTIC_BINARY(实际 ${matchE.primaryTool})`); + + // 场景 F: 描述统计 fallback + const queryF = makeParsedQuery({ + goal: 'descriptive', + outcome_type: null, + predictor_types: [], + }); + const matchF = decisionTableService.match(queryF); + assert(matchF.primaryTool === 'ST_DESCRIPTIVE', `场景 F: Primary = ST_DESCRIPTIVE(实际 ${matchF.primaryTool})`); + + // 场景 G: 队列研究 + const queryG = makeParsedQuery({ + goal: 'cohort_study', + outcome_type: 'binary', + predictor_types: ['categorical'], + design: 'independent', + }); + const matchG = decisionTableService.match(queryG); + assert(matchG.templateId === 'cohort_study_standard', `场景 G: Template = cohort_study_standard(实际 ${matchG.templateId})`); + + // 场景 H: 未知 goal → 应该 fallback 到描述统计 + const queryH = makeParsedQuery({ + goal: 'descriptive' as any, // 模拟未匹配场景 + outcome_type: 'datetime' as any, + predictor_types: ['datetime' as any], + }); + const matchH = decisionTableService.match(queryH); + assert(matchH.primaryTool === 'ST_DESCRIPTIVE', `场景 H: 无精确匹配 → Fallback ST_DESCRIPTIVE(实际 ${matchH.primaryTool})`); +} + +// ──────────────────────────────────────────── +// 测试 3: FlowTemplateService 模板填充 +// ──────────────────────────────────────────── + +function testFlowTemplateFilling() { + section('测试 3: FlowTemplateService 模板填充'); + + // 场景 A: standard_analysis(有 fallback → 3 步) + const queryA = makeParsedQuery({ + goal: 'comparison', + outcome_type: 'continuous', + predictor_types: ['binary'], + outcome_var: 'BP', + predictor_vars: ['Drug'], + grouping_var: 'Drug', + }); + const matchA = decisionTableService.match(queryA); + const fillA = flowTemplateService.fill(matchA, queryA); + assert(fillA.steps.length === 3, `场景 A: 3 步流程(实际 ${fillA.steps.length})`); + assert(fillA.steps[0].toolCode === 'ST_DESCRIPTIVE', `场景 A 步骤 1: ST_DESCRIPTIVE(实际 ${fillA.steps[0].toolCode})`); + assert(fillA.steps[1].toolCode === 'ST_T_TEST_IND', `场景 A 步骤 2: ST_T_TEST_IND(实际 ${fillA.steps[1].toolCode})`); + assert(fillA.steps[2].toolCode === 'ST_MANN_WHITNEY', `场景 A 步骤 3: ST_MANN_WHITNEY(实际 ${fillA.steps[2].toolCode})`); + assert(fillA.steps[2].isSensitivity === true, '场景 A 步骤 3: isSensitivity = true'); + assert(fillA.epvWarning === null, '场景 A: 无 EPV 警告'); + + // 场景 B: descriptive_only(无 fallback → 1 步) + const queryB = makeParsedQuery({ + goal: 'descriptive', + outcome_type: null, + predictor_types: [], + }); + const matchB = decisionTableService.match(queryB); + const fillB = flowTemplateService.fill(matchB, queryB); + assert(fillB.steps.length === 1, `场景 B: 1 步流程(实际 ${fillB.steps.length})`); + assert(fillB.steps[0].toolCode === 'ST_DESCRIPTIVE', '场景 B: ST_DESCRIPTIVE'); + + // 场景 C: 队列研究 → 3 步 (Table 1/2/3) + const queryC = makeParsedQuery({ + goal: 'cohort_study', + outcome_var: 'Event', + outcome_type: 'binary', + predictor_vars: ['Age', 'Sex', 'BMI', 'Smoking', 'SBP'], + predictor_types: ['continuous', 'binary', 'continuous', 'binary', 'continuous'], + grouping_var: 'Drug', + design: 'independent', + }); + const matchC = decisionTableService.match(queryC); + const fillC = flowTemplateService.fill(matchC, queryC); + assert(fillC.steps.length === 3, `场景 C: 队列研究 3 步(实际 ${fillC.steps.length})`); + assert(fillC.steps.length > 0 && fillC.steps[0].name.includes('表1'), `场景 C 步骤 1: 表1(实际 "${fillC.steps[0]?.name ?? 'N/A'}")`); + assert(fillC.steps.length > 1 && fillC.steps[1].name.includes('表2'), `场景 C 步骤 2: 表2(实际 "${fillC.steps[1]?.name ?? 'N/A'}")`); + assert(fillC.steps.length > 2 && fillC.steps[2].name.includes('表3'), `场景 C 步骤 3: 表3(实际 "${fillC.steps[2]?.name ?? 'N/A'}")`); + + // 场景 D: EPV 截断 — 30 个事件 / 10 = 最多 3 个变量 + const queryD = makeParsedQuery({ + goal: 'cohort_study', + outcome_var: 'Event', + outcome_type: 'binary', + predictor_vars: ['Age', 'Sex', 'BMI', 'Smoking', 'SBP', 'HR', 'Chol', 'LDL'], + predictor_types: ['continuous', 'binary', 'continuous', 'binary', 'continuous', 'continuous', 'continuous', 'continuous'], + grouping_var: 'Drug', + design: 'independent', + }); + const profileD = makeMockProfile('Event', 30); // 只有 30 个 event → max 3 vars + const matchD = decisionTableService.match(queryD); + const fillD = flowTemplateService.fill(matchD, queryD, profileD); + + const table3Step = fillD.steps.find(s => s.name.includes('表3')); + if (table3Step) { + const predictors = table3Step.params.predictors as string[] | undefined; + if (predictors) { + assert(predictors.length <= 3, `场景 D EPV 截断: 自变量 <= 3(实际 ${predictors.length},原始 8)`); + } else { + assert(false, '场景 D EPV 截断: 未找到 predictors 参数'); + } + } else { + assert(false, '场景 D: 未找到表3 步骤'); + } + assert(fillD.epvWarning !== null, `场景 D: 有 EPV 警告(${fillD.epvWarning?.substring(0, 40)}...)`); + + // 场景 E: 配对分析 → 2 步(无 sensitivity) + const queryE = makeParsedQuery({ + goal: 'comparison', + outcome_type: 'continuous', + predictor_types: ['binary'], + design: 'paired', + outcome_var: 'BP_after', + predictor_vars: ['BP_before'], + }); + const matchE = decisionTableService.match(queryE); + const fillE = flowTemplateService.fill(matchE, queryE); + assert(fillE.steps.length === 2, `场景 E: 配对分析 2 步(实际 ${fillE.steps.length})`); + assert(fillE.steps.every(s => !s.isSensitivity), '场景 E: 无敏感性分析步骤'); +} + +// ──────────────────────────────────────────── +// 测试 4: PlannedTrace 完整性 +// ──────────────────────────────────────────── + +function testPlannedTrace() { + section('测试 4: PlannedTrace 数据完整性'); + + const query = makeParsedQuery({ + goal: 'comparison', + outcome_type: 'continuous', + predictor_types: ['binary'], + design: 'independent', + outcome_var: 'BP', + predictor_vars: ['Drug'], + grouping_var: 'Drug', + }); + + const match = decisionTableService.match(query); + const fill = flowTemplateService.fill(match, query); + + // PlannedTrace 应具备的信息 + assert(match.rule.id !== '', 'PlannedTrace: matchedRule 非空'); + assert(match.primaryTool === 'ST_T_TEST_IND', `PlannedTrace: primaryTool = ST_T_TEST_IND`); + assert(match.fallbackTool === 'ST_MANN_WHITNEY', `PlannedTrace: fallbackTool = ST_MANN_WHITNEY`); + assert(match.switchCondition !== null, 'PlannedTrace: switchCondition 非空'); + assert(fill.templateId === 'standard_analysis', 'PlannedTrace: templateUsed = standard_analysis'); + assert(match.matchScore > 0, `PlannedTrace: matchScore > 0(实际 ${match.matchScore})`); + + // 确认参数正确传递 + const primaryStep = fill.steps.find(s => s.role === 'primary_test'); + assert(!!primaryStep, 'Primary step 存在'); + if (primaryStep) { + assert(primaryStep.params.group_var === 'Drug' || primaryStep.params.value_var === 'BP', + `Primary step 参数包含正确变量`); + } +} + +// ──────────────────────────────────────────── +// 运行所有测试 +// ──────────────────────────────────────────── + +console.log('\n🧪 SSA Phase P — Tracer Bullet 测试\n'); +console.log('测试范围:ConfigLoader → DecisionTable → FlowTemplate → PlannedTrace'); +console.log('依赖项:无(不需要数据库、LLM、R 引擎)\n'); + +try { + testConfigLoading(); + testDecisionTableMatching(); + testFlowTemplateFilling(); + testPlannedTrace(); +} catch (e: any) { + console.error(`\n💥 测试过程中发生未捕获异常:${e.message}`); + console.error(e.stack); + failed++; +} + +// 汇总 +console.log(`\n${'═'.repeat(60)}`); +console.log(`📊 测试结果汇总:${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计`); +if (failed === 0) { + console.log('🎉 全部通过!P 层 Pipeline 验证成功。'); +} else { + console.log(`⚠️ 有 ${failed} 个测试失败,请检查上方输出。`); +} +console.log('═'.repeat(60)); + +process.exit(failed > 0 ? 1 : 0); diff --git a/backend/scripts/test-ssa-qper-e2e.ts b/backend/scripts/test-ssa-qper-e2e.ts new file mode 100644 index 00000000..802ef99f --- /dev/null +++ b/backend/scripts/test-ssa-qper-e2e.ts @@ -0,0 +1,663 @@ +/** + * SSA Q→P→E→R — 完整 QPER 链路端到端集成测试 + * + * 测试链路: + * 登录 → 创建会话+上传 CSV → 数据画像 + * → Q 层(LLM Intent)→ P 层(Plan) + * → E 层(R 引擎执行)→ R 层(LLM 结论生成) + * → 结论 API 缓存验证 + * + * 依赖:Node.js 后端 + PostgreSQL + Python extraction_service + R 引擎 + LLM 服务 + * 运行方式:npx tsx scripts/test-ssa-qper-e2e.ts + * + * 测试数据:docs/03-业务模块/SSA-智能统计分析/05-测试文档/test.csv + * 测试用户:13800000001 / 123456 + */ + +import { readFileSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +const BASE_URL = 'http://localhost:3000'; +const TEST_PHONE = '13800000001'; +const TEST_PASSWORD = '123456'; +const TEST_CSV_PATH = join(__dirname, '../../docs/03-业务模块/SSA-智能统计分析/05-测试文档/test.csv'); + +// ──────────────────────────────────────────── +// 工具函数 +// ──────────────────────────────────────────── + +let passed = 0; +let failed = 0; +let skipped = 0; +let token = ''; +let sessionId = ''; + +function assert(condition: boolean, testName: string, detail?: string) { + if (condition) { + console.log(` ✅ ${testName}`); + passed++; + } else { + console.log(` ❌ ${testName}${detail ? ` — ${detail}` : ''}`); + failed++; + } +} + +function skip(testName: string, reason: string) { + console.log(` ⏭️ ${testName} — 跳过:${reason}`); + skipped++; +} + +function section(title: string) { + console.log(`\n${'─'.repeat(60)}`); + console.log(`📋 ${title}`); + console.log('─'.repeat(60)); +} + +function authHeaders(contentType?: string): Record { + const headers: Record = { + 'Authorization': `Bearer ${token}`, + }; + if (contentType) { + headers['Content-Type'] = contentType; + } + return headers; +} + +async function apiPost(path: string, body: any, headers?: Record): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + method: 'POST', + headers: headers || authHeaders('application/json'), + body: typeof body === 'string' ? body : JSON.stringify(body), + }); + const text = await res.text(); + try { + return { status: res.status, data: JSON.parse(text) }; + } catch { + return { status: res.status, data: text }; + } +} + +async function apiGet(path: string): Promise { + const res = await fetch(`${BASE_URL}${path}`, { + method: 'GET', + headers: authHeaders(), + }); + const text = await res.text(); + try { + return { status: res.status, data: JSON.parse(text) }; + } catch { + return { status: res.status, data: text }; + } +} + +// ──────────────────────────────────────────── +// 测试 1: 登录获取 Token +// ──────────────────────────────────────────── + +async function testLogin(): Promise { + section('测试 1: 登录认证'); + + try { + const res = await apiPost('/api/v1/auth/login/password', { + phone: TEST_PHONE, + password: TEST_PASSWORD, + }, { 'Content-Type': 'application/json' }); + + assert(res.status === 200, `登录返回 200(实际 ${res.status})`); + + if (res.status === 200 && res.data) { + token = res.data?.data?.tokens?.accessToken || res.data?.accessToken || res.data?.token || ''; + assert(token.length > 0, '获取到 JWT Token', `token 长度: ${token.length}`); + } + } catch (e: any) { + assert(false, '登录请求失败', e.message); + } + + if (!token) { + console.log('\n ⚠️ Token 获取失败,后续测试无法继续'); + return false; + } + return true; +} + +// ──────────────────────────────────────────── +// 测试 2: 创建会话 + 上传 test.csv +// ──────────────────────────────────────────── + +async function testCreateSession(): Promise { + section('测试 2: 创建会话 + 上传 test.csv'); + + try { + const csvBuffer = readFileSync(TEST_CSV_PATH); + assert(csvBuffer.length > 0, `test.csv 读取成功(${csvBuffer.length} bytes)`); + + const formData = new FormData(); + const blob = new Blob([csvBuffer], { type: 'text/csv' }); + formData.append('file', blob, 'test.csv'); + + const res = await fetch(`${BASE_URL}/api/v1/ssa/sessions/`, { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}` }, + body: formData, + }); + + const data = await res.json(); + assert(res.status === 200, `创建会话返回 200(实际 ${res.status})`); + + if (data.sessionId) { + sessionId = data.sessionId; + assert(true, `会话 ID: ${sessionId}`); + } else { + assert(false, '未返回 sessionId'); + } + + if (data.schema) { + assert(data.schema.columns?.length > 0, `Schema 解析成功(${data.schema.columns?.length} 列, ${data.schema.rowCount} 行)`); + } + } catch (e: any) { + assert(false, '创建会话失败', e.message); + } + + return !!sessionId; +} + +// ──────────────────────────────────────────── +// 测试 3: 数据画像(Python DataProfiler) +// ──────────────────────────────────────────── + +async function testDataProfile() { + section('测试 3: 数据画像(Python DataProfiler)'); + + try { + const res = await apiPost('/api/v1/ssa/workflow/profile', { sessionId }); + assert(res.status === 200, `画像请求返回 200(实际 ${res.status})`); + + if (res.data?.success) { + const profile = res.data.profile; + assert(!!profile, '画像数据非空'); + if (profile) { + const rows = profile.row_count || profile.totalRows || 0; + const cols = profile.column_count || profile.totalColumns || 0; + assert(rows > 0, `行数: ${rows}`); + assert(cols > 0, `列数: ${cols}`); + } + } else { + assert(false, '画像生成失败', res.data?.error); + } + } catch (e: any) { + assert(false, '画像请求异常', e.message); + } +} + +// ──────────────────────────────────────────── +// 测试 4: Q 层 — LLM 意图解析 +// ──────────────────────────────────────────── + +async function testQLayer(): Promise { + section('测试 4: Q 层 — LLM 意图理解'); + + const query = '比较 sex 不同组的 Yqol 有没有差别'; + console.log(` Query: "${query}"`); + + try { + const start = Date.now(); + const res = await apiPost('/api/v1/ssa/workflow/intent', { + sessionId, + userQuery: query, + }); + const elapsed = Date.now() - start; + + assert(res.status === 200, `返回 200(实际 ${res.status})`); + + if (res.data?.success && res.data.intent) { + const intent = res.data.intent; + console.log(` 耗时: ${elapsed}ms`); + console.log(` Goal: ${intent.goal}, Confidence: ${intent.confidence}`); + console.log(` Y: ${intent.outcome_var}, X: ${JSON.stringify(intent.predictor_vars)}`); + console.log(` Design: ${intent.design}, needsClarification: ${intent.needsClarification}`); + + assert(intent.goal === 'comparison', `Goal = comparison(实际 ${intent.goal})`); + assert(intent.confidence >= 0.7, `高置信度 >= 0.7(实际 ${intent.confidence})`); + assert(!intent.needsClarification, '无需追问'); + + return intent.goal; + } else { + assert(false, 'Intent 解析失败', res.data?.error); + } + } catch (e: any) { + assert(false, 'Q 层请求异常', e.message); + } + + return null; +} + +// ──────────────────────────────────────────── +// 测试 5: P 层 — 工作流规划 +// ──────────────────────────────────────────── + +let workflowId = ''; + +async function testPLayer(): Promise { + section('测试 5: P 层 — 工作流规划'); + + const query = '比较 sex 不同组的 Yqol 有没有差别'; + console.log(` Query: "${query}"`); + + try { + const start = Date.now(); + const res = await apiPost('/api/v1/ssa/workflow/plan', { + sessionId, + userQuery: query, + }); + const elapsed = Date.now() - start; + + assert(res.status === 200, `返回 200(实际 ${res.status})`); + + if (res.data?.success && res.data.plan) { + const plan = res.data.plan; + console.log(` 耗时: ${elapsed}ms`); + console.log(` 标题: ${plan.title}`); + console.log(` 步骤数: ${plan.total_steps}`); + + workflowId = plan.workflow_id; + assert(!!workflowId, `Workflow ID: ${workflowId}`); + assert(plan.total_steps >= 2, `步骤数 >= 2(实际 ${plan.total_steps})`); + + plan.steps?.forEach((step: any, i: number) => { + const sensitivity = step.is_sensitivity ? ' [敏感性]' : ''; + const guardrail = step.switch_condition ? ` | 护栏:${step.switch_condition}` : ''; + console.log(` 步骤 ${i + 1}: ${step.tool_name} (${step.tool_code})${sensitivity}${guardrail}`); + }); + + if (plan.planned_trace) { + console.log(` PlannedTrace: Primary=${plan.planned_trace.primaryTool}, Fallback=${plan.planned_trace.fallbackTool || 'null'}`); + assert(!!plan.planned_trace.primaryTool, 'PlannedTrace 包含 primaryTool'); + } + + return true; + } else { + assert(false, '规划失败', res.data?.error); + } + } catch (e: any) { + assert(false, 'P 层请求异常', e.message); + } + + return false; +} + +// ──────────────────────────────────────────── +// 测试 6: E 层 — R 引擎执行 +// ──────────────────────────────────────────── + +async function testELayer(): Promise { + section('测试 6: E 层 — R 引擎执行(含 R 层结论生成)'); + + if (!workflowId) { + skip('E 层执行', '无 workflowId'); + return false; + } + + console.log(` Workflow ID: ${workflowId}`); + console.log(` Session ID: ${sessionId}`); + + try { + const start = Date.now(); + const res = await apiPost(`/api/v1/ssa/workflow/${workflowId}/execute`, { + sessionId, + }); + const elapsed = Date.now() - start; + + assert(res.status === 200, `返回 200(实际 ${res.status})`); + + if (res.data?.success && res.data.result) { + const result = res.data.result; + console.log(` 耗时: ${elapsed}ms`); + console.log(` 状态: ${result.status}`); + console.log(` 总步骤: ${result.totalSteps}, 成功: ${result.successSteps}, 完成: ${result.completedSteps}`); + + assert( + result.status === 'completed' || result.status === 'partial', + `执行状态正常(${result.status})`, + result.status === 'error' ? '全部步骤失败' : undefined, + ); + + assert(result.successSteps > 0, `至少 1 个步骤成功(实际 ${result.successSteps})`); + + // 逐步骤检查 + if (result.results && Array.isArray(result.results)) { + for (const step of result.results) { + const icon = step.status === 'success' || step.status === 'warning' ? '✅' : '❌'; + const pVal = step.result?.p_value != null ? `, P=${step.result.p_value_fmt || step.result.p_value}` : ''; + const blocks = step.reportBlocks?.length || 0; + const errMsg = step.error ? ` | 错误: ${step.error.userHint || step.error.message}` : ''; + console.log(` ${icon} 步骤 ${step.stepOrder}: ${step.toolName} [${step.status}] (${step.executionMs}ms${pVal}, ${blocks} blocks${errMsg})`); + } + } + + // 检查 report_blocks + if (result.reportBlocks && result.reportBlocks.length > 0) { + assert(true, `聚合 reportBlocks: ${result.reportBlocks.length} 个`); + const types = result.reportBlocks.map((b: any) => b.type); + const uniqueTypes = [...new Set(types)]; + console.log(` Block 类型分布: ${uniqueTypes.join(', ')}`); + } + + // 检查 R 层结论 + if (result.conclusion) { + console.log('\n ── R 层结论验证 ──'); + const c = result.conclusion; + + assert(!!c.executive_summary, `executive_summary 非空(${c.executive_summary?.length || 0} 字)`); + assert(Array.isArray(c.key_findings) && c.key_findings.length > 0, + `key_findings 非空(${c.key_findings?.length || 0} 条)`); + assert(!!c.statistical_summary, 'statistical_summary 存在'); + assert(Array.isArray(c.limitations) && c.limitations.length > 0, + `limitations 非空(${c.limitations?.length || 0} 条)`); + assert(!!c.generated_at, `generated_at: ${c.generated_at}`); + assert(!!c.source, `source: ${c.source}`); + + // 打印结论内容摘要 + console.log(` 结论来源: ${c.source === 'llm' ? 'AI 智能生成' : '规则引擎'}`); + console.log(` 摘要前 200 字: ${c.executive_summary?.substring(0, 200)}...`); + + if (c.key_findings?.length > 0) { + console.log(' 主要发现:'); + c.key_findings.slice(0, 3).forEach((f: string, i: number) => { + console.log(` ${i + 1}. ${f.substring(0, 120)}`); + }); + } + + if (c.statistical_summary) { + console.log(` 统计概览: ${c.statistical_summary.total_tests} 项检验, ${c.statistical_summary.significant_results} 项显著`); + console.log(` 使用方法: ${c.statistical_summary.methods_used?.join(', ')}`); + } + + if (c.step_summaries?.length > 0) { + console.log(' 步骤摘要:'); + c.step_summaries.forEach((s: any) => { + const sig = s.is_significant ? ' (显著*)' : ''; + console.log(` 步骤${s.step_number} ${s.tool_name}: ${s.summary?.substring(0, 100)}${sig}`); + }); + } + + if (c.limitations?.length > 0) { + console.log(' 局限性:'); + c.limitations.slice(0, 3).forEach((l: string, i: number) => { + console.log(` ${i + 1}. ${l.substring(0, 120)}`); + }); + } + + if (c.recommendations?.length > 0) { + console.log(' 建议:'); + c.recommendations.slice(0, 2).forEach((r: string, i: number) => { + console.log(` ${i + 1}. ${r.substring(0, 120)}`); + }); + } + + // 验证 workflow_id 一致 + if (c.workflow_id) { + assert(c.workflow_id === workflowId, `conclusion.workflow_id 与 workflowId 一致`); + } + } else { + assert(false, 'R 层未返回 conclusion'); + } + + return result.successSteps > 0; + } else { + assert(false, '执行失败', res.data?.error); + } + } catch (e: any) { + assert(false, 'E 层请求异常', e.message); + } + + return false; +} + +// ──────────────────────────────────────────── +// 测试 7: 结论 API 缓存验证 +// ──────────────────────────────────────────── + +async function testConclusionAPI() { + section('测试 7: 结论 API + 缓存验证'); + + if (!sessionId) { + skip('结论 API', '无 sessionId'); + return; + } + + try { + const start = Date.now(); + const res = await apiGet(`/api/v1/ssa/workflow/sessions/${sessionId}/conclusion`); + const elapsed = Date.now() - start; + + assert(res.status === 200, `返回 200(实际 ${res.status})`); + + if (res.data?.success && res.data.conclusion) { + const c = res.data.conclusion; + console.log(` 耗时: ${elapsed}ms`); + console.log(` 来源: ${res.data.source}`); + + assert(!!c.executive_summary, 'executive_summary 非空'); + assert(Array.isArray(c.key_findings), 'key_findings 是数组'); + assert(!!c.generated_at, `generated_at: ${c.generated_at}`); + + // 二次调用验证缓存 + console.log('\n ── 缓存验证(二次调用) ──'); + const start2 = Date.now(); + const res2 = await apiGet(`/api/v1/ssa/workflow/sessions/${sessionId}/conclusion`); + const elapsed2 = Date.now() - start2; + + assert(res2.status === 200, '二次调用返回 200'); + console.log(` 二次调用耗时: ${elapsed2}ms`); + + if (elapsed2 < elapsed && res.data.source === 'cache') { + assert(true, `缓存命中(${elapsed2}ms << ${elapsed}ms)`); + } else { + console.log(` ℹ️ 首次 ${elapsed}ms, 二次 ${elapsed2}ms(缓存效果取决于实现)`); + } + } else if (res.status === 404) { + skip('结论 API', '未找到已完成的 workflow(可能是 E 层全部失败)'); + } else { + assert(false, '获取结论失败', res.data?.error || JSON.stringify(res.data).substring(0, 200)); + } + } catch (e: any) { + assert(false, '结论 API 异常', e.message); + } +} + +// ──────────────────────────────────────────── +// 测试 8: 第二条链路(相关分析 Q→P→E→R) +// ──────────────────────────────────────────── + +async function testSecondScenario() { + section('测试 8: 第二条完整链路(相关分析 age vs bmi)'); + + const query = '分析 age 和 bmi 的相关性'; + console.log(` Query: "${query}"`); + + try { + // Q → P: Plan + const planRes = await apiPost('/api/v1/ssa/workflow/plan', { + sessionId, + userQuery: query, + }); + + assert(planRes.status === 200, 'Plan 返回 200'); + + if (!planRes.data?.success || !planRes.data.plan) { + assert(false, 'Plan 失败', planRes.data?.error); + return; + } + + const plan = planRes.data.plan; + const wfId = plan.workflow_id; + console.log(` Workflow: ${wfId}, 步骤数: ${plan.total_steps}`); + plan.steps?.forEach((s: any, i: number) => { + console.log(` 步骤 ${i + 1}: ${s.tool_name} (${s.tool_code})`); + }); + + // P → E → R: Execute + const start = Date.now(); + const execRes = await apiPost(`/api/v1/ssa/workflow/${wfId}/execute`, { sessionId }); + const elapsed = Date.now() - start; + + assert(execRes.status === 200, 'Execute 返回 200'); + + if (execRes.data?.success && execRes.data.result) { + const result = execRes.data.result; + console.log(` 执行耗时: ${elapsed}ms, 状态: ${result.status}, 成功步骤: ${result.successSteps}/${result.totalSteps}`); + + assert(result.successSteps > 0, `至少 1 步成功(实际 ${result.successSteps})`); + + for (const step of (result.results || [])) { + const icon = step.status === 'success' || step.status === 'warning' ? '✅' : '❌'; + const pVal = step.result?.p_value != null ? `, P=${step.result.p_value_fmt || step.result.p_value}` : ''; + console.log(` ${icon} 步骤 ${step.stepOrder}: ${step.toolName} [${step.status}] (${step.executionMs}ms${pVal})`); + } + + // 验证 R 层结论 + if (result.conclusion) { + const c = result.conclusion; + assert(!!c.executive_summary, `R 层结论存在(来源: ${c.source})`); + console.log(` 结论摘要: ${c.executive_summary?.substring(0, 150)}...`); + + // 相关分析应该提到相关系数 + const mentionsCorrelation = + c.executive_summary?.includes('相关') || + c.executive_summary?.includes('correlation') || + c.executive_summary?.includes('r =') || + c.executive_summary?.includes('r='); + if (mentionsCorrelation) { + assert(true, '结论中提到了相关性分析'); + } else { + console.log(' ℹ️ 结论未明确提到"相关"(可能是 fallback 结论)'); + } + } else { + skip('R 层结论', '未返回 conclusion'); + } + } else { + assert(false, '执行失败', execRes.data?.error); + } + } catch (e: any) { + assert(false, '第二条链路异常', e.message); + } +} + +// ──────────────────────────────────────────── +// 测试 9: 错误分类验证(E_COLUMN_NOT_FOUND 等) +// ──────────────────────────────────────────── + +async function testErrorClassification() { + section('测试 9: E 层错误分类验证(构造异常查询)'); + + const query = '比较 NONEXISTENT_GROUP 不同组的 FAKE_OUTCOME'; + console.log(` 构造异常 Query: "${query}"`); + console.log(' ℹ️ 此测试验证 LLM 面对不存在的变量名时的行为'); + + try { + const res = await apiPost('/api/v1/ssa/workflow/intent', { + sessionId, + userQuery: query, + }); + + if (res.data?.success && res.data.intent) { + const intent = res.data.intent; + console.log(` LLM 返回: goal=${intent.goal}, confidence=${intent.confidence}`); + console.log(` Y=${intent.outcome_var}, X=${JSON.stringify(intent.predictor_vars)}`); + + // Zod 动态校验应该拦截不存在的变量名 + // 或者 LLM 会给出低置信度 + if (intent.confidence < 0.7 || intent.needsClarification) { + assert(true, `LLM 识别到异常(confidence=${intent.confidence})或触发追问`); + } else { + console.log(' ℹ️ LLM 未识别到异常变量,可能猜测了现有变量作为替代'); + } + } else { + // Intent 解析失败也是可以接受的(Zod 拦截了幻觉变量) + console.log(` Intent 解析结果: ${res.data?.error || '失败/降级'}`); + assert(true, '异常输入被处理(未崩溃)'); + } + } catch (e: any) { + assert(false, '异常查询处理失败(不应崩溃)', e.message); + } +} + +// ──────────────────────────────────────────── +// 运行所有测试 +// ──────────────────────────────────────────── + +async function main() { + console.log('\n🧪 SSA QPER — 完整链路端到端集成测试(Q→P→E→R)\n'); + console.log('测试链路:登录 → 上传 CSV → 画像 → Q(Intent) → P(Plan) → E(Execute) → R(Conclusion)'); + console.log(`测试用户:${TEST_PHONE}`); + console.log(`后端地址:${BASE_URL}`); + console.log(`测试文件:${TEST_CSV_PATH}\n`); + + // 前置检查 + try { + readFileSync(TEST_CSV_PATH); + } catch { + console.error('❌ test.csv 文件不存在,请检查路径'); + process.exit(1); + } + + try { + const health = await fetch(`${BASE_URL}/health`).catch(() => null); + if (!health || health.status !== 200) { + console.error('❌ 后端服务未启动'); + process.exit(1); + } + console.log('✅ 后端服务可达'); + } catch { + console.error('❌ 后端服务不可达'); + process.exit(1); + } + + // 顺序执行 + const loginOk = await testLogin(); + if (!loginOk) { console.log('\n⛔ 登录失败,终止'); process.exit(1); } + + const sessionOk = await testCreateSession(); + if (!sessionOk) { console.log('\n⛔ 会话创建失败,终止'); process.exit(1); } + + await testDataProfile(); + + const goal = await testQLayer(); + if (!goal) { console.log('\n⚠️ Q 层失败,继续后续测试...'); } + + const planOk = await testPLayer(); + if (!planOk) { console.log('\n⚠️ P 层失败,E/R 层将跳过'); } + + const execOk = planOk ? await testELayer() : false; + + if (execOk) { + await testConclusionAPI(); + } else if (planOk) { + console.log('\n⚠️ E 层失败,跳过结论 API 测试'); + } + + await testSecondScenario(); + await testErrorClassification(); + + // 汇总 + console.log(`\n${'═'.repeat(60)}`); + console.log(`📊 测试结果汇总:${passed} 通过 / ${failed} 失败 / ${skipped} 跳过 / ${passed + failed + skipped} 总计`); + if (failed === 0) { + console.log('🎉 全部通过!QPER 四层端到端验证成功。'); + } else { + console.log(`⚠️ 有 ${failed} 个测试失败,请检查上方输出。`); + } + console.log(`\n📝 测试会话 ID: ${sessionId}`); + console.log('═'.repeat(60)); + + process.exit(failed > 0 ? 1 : 0); +} + +main().catch(e => { + console.error('💥 测试脚本异常:', e); + process.exit(1); +}); diff --git a/backend/src/modules/ssa/config/ConfigLoader.ts b/backend/src/modules/ssa/config/ConfigLoader.ts new file mode 100644 index 00000000..56ae6b85 --- /dev/null +++ b/backend/src/modules/ssa/config/ConfigLoader.ts @@ -0,0 +1,85 @@ +/** + * SSA ConfigLoader — 配置化基础设施 + * + * 通用基类:读 JSON 文件 → Zod Schema 校验 → 内存缓存 + * 支持热更新(reload 时重新读盘 + 重新校验,失败保留旧配置) + * + * 核心原则第 6 条:一切业务逻辑靠读 JSON 驱动,不写死在代码中。 + */ + +import { readFileSync } from 'fs'; +import { join } from 'path'; +import { fileURLToPath } from 'url'; +import { dirname } from 'path'; +import type { ZodType } from 'zod'; +import { logger } from '../../../common/logging/index.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +export interface ReloadResult { + success: boolean; + file: string; + error?: string; +} + +export class ConfigLoader { + private cache: T | null = null; + private readonly filePath: string; + private readonly schema: ZodType; + private readonly label: string; + + constructor(fileName: string, schema: ZodType, label: string) { + this.filePath = join(__dirname, fileName); + this.schema = schema; + this.label = label; + } + + /** + * 获取配置(带内存缓存,首次自动加载) + */ + get(): T { + if (!this.cache) { + this.loadFromDisk(); + } + return this.cache!; + } + + /** + * 热更新 — 从磁盘重新读取 + Zod 校验 + * 校验失败时保留旧配置,返回错误详情 + */ + reload(): ReloadResult { + try { + this.loadFromDisk(); + logger.info(`[SSA:Config] ${this.label} reloaded successfully`); + return { success: true, file: this.label }; + } catch (err: any) { + logger.error(`[SSA:Config] ${this.label} reload failed, keeping old config`, { + error: err.message, + }); + return { success: false, file: this.label, error: err.message }; + } + } + + private loadFromDisk(): void { + const raw = readFileSync(this.filePath, 'utf-8'); + let parsed: unknown; + + try { + parsed = JSON.parse(raw); + } catch (e: any) { + throw new Error(`${this.label}: JSON 语法错误 — ${e.message}`); + } + + const result = this.schema.safeParse(parsed); + if (!result.success) { + const issues = result.error.issues + .map(i => ` - ${i.path.join('.')}: ${i.message}`) + .join('\n'); + throw new Error(`${this.label}: Schema 校验失败\n${issues}`); + } + + this.cache = result.data; + } +} diff --git a/backend/src/modules/ssa/config/decision_tables.json b/backend/src/modules/ssa/config/decision_tables.json new file mode 100644 index 00000000..69f08e62 --- /dev/null +++ b/backend/src/modules/ssa/config/decision_tables.json @@ -0,0 +1,132 @@ +[ + { + "id": "DIFF_CONT_BIN_IND", + "goal": "comparison", + "outcomeType": "continuous", + "predictorType": "binary", + "design": "independent", + "primaryTool": "ST_T_TEST_IND", + "fallbackTool": "ST_MANN_WHITNEY", + "switchCondition": "normality_fail: Shapiro-Wilk P<0.05", + "templateId": "standard_analysis", + "priority": 10, + "description": "两组连续变量比较(独立样本)" + }, + { + "id": "DIFF_CONT_BIN_PAIRED", + "goal": "comparison", + "outcomeType": "continuous", + "predictorType": "binary", + "design": "paired", + "primaryTool": "ST_T_TEST_PAIRED", + "fallbackTool": null, + "switchCondition": null, + "templateId": "paired_analysis", + "priority": 10, + "description": "配对设计前后对比" + }, + { + "id": "DIFF_CONT_MULTI_IND", + "goal": "comparison", + "outcomeType": "continuous", + "predictorType": "categorical", + "design": "independent", + "primaryTool": "ST_T_TEST_IND", + "fallbackTool": "ST_MANN_WHITNEY", + "switchCondition": "normality_fail: Shapiro-Wilk P<0.05", + "templateId": "standard_analysis", + "priority": 5, + "description": "多组连续变量比较(暂用 T 检验处理两组场景,ANOVA 待扩展)" + }, + { + "id": "DIFF_CAT_CAT_IND", + "goal": "comparison", + "outcomeType": "categorical", + "predictorType": "categorical", + "design": "independent", + "primaryTool": "ST_CHI_SQUARE", + "fallbackTool": "ST_CHI_SQUARE", + "switchCondition": "expected_freq_low: 期望频数<5 时 R 内部自动切换 Fisher", + "templateId": "standard_analysis", + "priority": 10, + "description": "两个分类变量的独立性检验" + }, + { + "id": "ASSOC_CONT_CONT", + "goal": "correlation", + "outcomeType": "continuous", + "predictorType": "continuous", + "design": "*", + "primaryTool": "ST_CORRELATION", + "fallbackTool": null, + "switchCondition": null, + "templateId": "standard_analysis", + "priority": 10, + "description": "两个连续变量的相关分析(Pearson/Spearman 自动选择)" + }, + { + "id": "ASSOC_CAT_ANY", + "goal": "correlation", + "outcomeType": "categorical", + "predictorType": "*", + "design": "*", + "primaryTool": "ST_CHI_SQUARE", + "fallbackTool": "ST_CHI_SQUARE", + "switchCondition": "expected_freq_low: 期望频数<5 时 R 内部自动切换 Fisher", + "templateId": "standard_analysis", + "priority": 5, + "description": "分类变量关联分析" + }, + { + "id": "PRED_BIN_ANY", + "goal": "regression", + "outcomeType": "binary", + "predictorType": "*", + "design": "*", + "primaryTool": "ST_LOGISTIC_BINARY", + "fallbackTool": null, + "switchCondition": null, + "templateId": "regression_analysis", + "priority": 10, + "description": "二分类结局的多因素 Logistic 回归" + }, + { + "id": "PRED_CONT_ANY", + "goal": "regression", + "outcomeType": "continuous", + "predictorType": "*", + "design": "*", + "primaryTool": "ST_CORRELATION", + "fallbackTool": null, + "switchCondition": null, + "templateId": "regression_analysis", + "priority": 5, + "description": "连续结局的回归分析(线性回归待扩展,暂用相关分析)" + }, + { + "id": "DESC_ANY", + "goal": "descriptive", + "outcomeType": "*", + "predictorType": "*", + "design": "*", + "primaryTool": "ST_DESCRIPTIVE", + "fallbackTool": null, + "switchCondition": null, + "templateId": "descriptive_only", + "priority": 1, + "description": "纯描述性统计" + }, + { + "id": "COHORT_STUDY", + "goal": "cohort_study", + "outcomeType": "binary", + "predictorType": "*", + "design": "*", + "primaryTool": "ST_DESCRIPTIVE", + "fallbackTool": null, + "switchCondition": null, + "templateId": "cohort_study_standard", + "priority": 20, + "description": "队列研究全套分析(Table 1→2→3)" + } +] diff --git a/backend/src/modules/ssa/config/flow_templates.json b/backend/src/modules/ssa/config/flow_templates.json new file mode 100644 index 00000000..293bd9d7 --- /dev/null +++ b/backend/src/modules/ssa/config/flow_templates.json @@ -0,0 +1,69 @@ +{ + "version": "1.0.0", + "templates": [ + { + "id": "standard_analysis", + "name": "标准分析流程", + "description": "适用于差异比较、相关分析等场景的通用三步模板", + "steps": [ + { "order": 1, "role": "descriptive", "tool": "ST_DESCRIPTIVE", "name": "描述性统计" }, + { "order": 2, "role": "primary_test", "tool": "{{primaryTool}}", "name": "主分析" }, + { "order": 3, "role": "sensitivity", "tool": "{{fallbackTool}}", "name": "敏感性分析", "condition": "fallback_exists" } + ] + }, + { + "id": "paired_analysis", + "name": "配对设计分析", + "description": "配对设计的前后对比分析", + "steps": [ + { "order": 1, "role": "descriptive", "tool": "ST_DESCRIPTIVE", "name": "描述性统计" }, + { "order": 2, "role": "primary_test", "tool": "{{primaryTool}}", "name": "配对检验" } + ] + }, + { + "id": "regression_analysis", + "name": "回归建模", + "description": "描述统计 + 多因素回归分析", + "steps": [ + { "order": 1, "role": "descriptive", "tool": "ST_DESCRIPTIVE", "name": "描述性统计" }, + { "order": 2, "role": "primary_test", "tool": "{{primaryTool}}", "name": "多因素回归" } + ] + }, + { + "id": "descriptive_only", + "name": "描述性统计", + "description": "仅做数据概况分析", + "steps": [ + { "order": 1, "role": "descriptive", "tool": "ST_DESCRIPTIVE", "name": "描述性统计" } + ] + }, + { + "id": "cohort_study_standard", + "name": "经典队列研究全套分析", + "description": "覆盖 Table 1(基线比较)→ Table 2(单因素筛选)→ Table 3(多因素回归)", + "steps": [ + { + "order": 1, + "role": "baseline_table", + "tool": "ST_DESCRIPTIVE", + "name": "表1: 组间基线特征比较", + "paramsMapping": { "group_var": "{{grouping_var}}", "variables": "{{all_predictors}}" } + }, + { + "order": 2, + "role": "univariate_screen", + "tool": "ST_DESCRIPTIVE", + "name": "表2: 结局指标单因素分析", + "paramsMapping": { "group_var": "{{outcome_var}}", "variables": "{{all_predictors}}" } + }, + { + "order": 3, + "role": "multivariate_reg", + "tool": "ST_LOGISTIC_BINARY", + "name": "表3: 多因素 Logistic 回归", + "paramsMapping": { "outcome_var": "{{outcome_var}}", "predictors": "{{epv_capped_predictors}}" } + } + ] + } + ] +} diff --git a/backend/src/modules/ssa/config/index.ts b/backend/src/modules/ssa/config/index.ts new file mode 100644 index 00000000..ffe9c38f --- /dev/null +++ b/backend/src/modules/ssa/config/index.ts @@ -0,0 +1,48 @@ +/** + * SSA 配置中心 — 统一管理所有领域 JSON 配置 + * + * 每个 ConfigLoader 实例对应一个 JSON 文件 + Zod Schema。 + * 提供 reloadAll() 供热更新 API 调用。 + */ + +import { ConfigLoader, type ReloadResult } from './ConfigLoader.js'; +import { + ToolsRegistrySchema, + DecisionTablesSchema, + FlowTemplatesSchema, + type ToolsRegistry, + type DecisionTable, + type FlowTemplatesConfig, +} from './schemas.js'; + +export const toolsRegistryLoader = new ConfigLoader( + 'tools_registry.json', + ToolsRegistrySchema, + 'tools_registry' +); + +export const decisionTablesLoader = new ConfigLoader( + 'decision_tables.json', + DecisionTablesSchema, + 'decision_tables' +); + +export const flowTemplatesLoader = new ConfigLoader( + 'flow_templates.json', + FlowTemplatesSchema, + 'flow_templates' +); + +/** + * 热更新所有配置文件 + * 每个文件独立校验 — 一个失败不影响其他 + */ +export function reloadAllConfigs(): ReloadResult[] { + return [ + toolsRegistryLoader.reload(), + decisionTablesLoader.reload(), + flowTemplatesLoader.reload(), + ]; +} + +export type { ReloadResult } from './ConfigLoader.js'; diff --git a/backend/src/modules/ssa/config/schemas.ts b/backend/src/modules/ssa/config/schemas.ts new file mode 100644 index 00000000..a78470c1 --- /dev/null +++ b/backend/src/modules/ssa/config/schemas.ts @@ -0,0 +1,91 @@ +/** + * SSA 领域配置 Zod Schema + * + * 方法学团队编辑 JSON 时的拼写/结构错误在加载时立即拦截。 + * 每个 Schema 对应一个 JSON 领域文件。 + */ + +import { z } from 'zod'; + +// ──────────────────────────────────────────── +// 1. tools_registry.json — E 层工具注册表 +// ──────────────────────────────────────────── + +const ToolParamSchema = z.object({ + name: z.string(), + type: z.enum(['string', 'number', 'boolean', 'string[]', 'number[]']), + required: z.boolean().default(true), + description: z.string().optional(), + default: z.unknown().optional(), +}); + +const ToolDefinitionSchema = z.object({ + code: z.string().regex(/^ST_[A-Z_]+$/, 'tool code must match ST_XXX pattern'), + name: z.string().min(1), + category: z.string(), + description: z.string(), + inputParams: z.array(ToolParamSchema), + outputType: z.string(), + prerequisite: z.string().optional(), + fallback: z.string().optional(), +}); + +export const ToolsRegistrySchema = z.object({ + version: z.string().optional(), + tools: z.array(ToolDefinitionSchema).min(1), +}); + +export type ToolDefinition = z.infer; +export type ToolsRegistry = z.infer; + +// ──────────────────────────────────────────── +// 2. decision_tables.json — P 层决策表 +// ──────────────────────────────────────────── + +const DecisionRuleSchema = z.object({ + id: z.string(), + goal: z.string(), + outcomeType: z.string(), + predictorType: z.string(), + design: z.string(), + primaryTool: z.string(), + fallbackTool: z.string().nullable().default(null), + switchCondition: z.string().nullable().default(null), + templateId: z.string(), + priority: z.number().default(0), + description: z.string().optional(), +}); + +export const DecisionTablesSchema = z.array(DecisionRuleSchema).min(1); + +export type DecisionRule = z.infer; +export type DecisionTable = DecisionRule; + +// ──────────────────────────────────────────── +// 3. flow_templates.json — P 层流程模板 +// ──────────────────────────────────────────── + +const TemplateStepSchema = z.object({ + order: z.number(), + role: z.string(), + tool: z.string(), + name: z.string().optional(), + condition: z.string().optional(), + paramsMapping: z.record(z.string(), z.string()).optional(), +}); + +const FlowTemplateSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().optional(), + steps: z.array(TemplateStepSchema).min(1), +}); + +export const FlowTemplatesSchema = z.object({ + version: z.string().optional(), + templates: z.array(FlowTemplateSchema).min(1), +}); + +export type TemplateStep = z.infer; +export type FlowTemplate = z.infer; +export type FlowTemplatesConfig = z.infer; diff --git a/backend/src/modules/ssa/config/tools_registry.json b/backend/src/modules/ssa/config/tools_registry.json new file mode 100644 index 00000000..936e19c3 --- /dev/null +++ b/backend/src/modules/ssa/config/tools_registry.json @@ -0,0 +1,87 @@ +{ + "version": "1.0.0", + "tools": [ + { + "code": "ST_DESCRIPTIVE", + "name": "描述性统计", + "category": "basic", + "description": "数据概况、基线特征表", + "inputParams": [ + { "name": "variables", "type": "string[]", "required": true, "description": "分析变量列表" }, + { "name": "group_var", "type": "string", "required": false, "description": "分组变量" } + ], + "outputType": "summary" + }, + { + "code": "ST_T_TEST_IND", + "name": "独立样本T检验", + "category": "parametric", + "description": "两组连续变量比较(参数方法)", + "inputParams": [ + { "name": "group_var", "type": "string", "required": true, "description": "分组变量(二分类)" }, + { "name": "value_var", "type": "string", "required": true, "description": "连续型结局变量" } + ], + "outputType": "comparison", + "prerequisite": "正态分布", + "fallback": "ST_MANN_WHITNEY" + }, + { + "code": "ST_MANN_WHITNEY", + "name": "Mann-Whitney U检验", + "category": "nonparametric", + "description": "两组连续/等级变量比较(非参数方法)", + "inputParams": [ + { "name": "group_var", "type": "string", "required": true, "description": "分组变量(二分类)" }, + { "name": "value_var", "type": "string", "required": true, "description": "连续型结局变量" } + ], + "outputType": "comparison" + }, + { + "code": "ST_T_TEST_PAIRED", + "name": "配对T检验", + "category": "parametric", + "description": "配对设计的前后对比", + "inputParams": [ + { "name": "before_var", "type": "string", "required": true, "description": "前测变量" }, + { "name": "after_var", "type": "string", "required": true, "description": "后测变量" } + ], + "outputType": "comparison" + }, + { + "code": "ST_CHI_SQUARE", + "name": "卡方检验", + "category": "categorical", + "description": "两个分类变量的独立性检验", + "inputParams": [ + { "name": "var1", "type": "string", "required": true, "description": "分类变量1" }, + { "name": "var2", "type": "string", "required": true, "description": "分类变量2" } + ], + "outputType": "association", + "fallback": "ST_FISHER" + }, + { + "code": "ST_CORRELATION", + "name": "相关分析", + "category": "correlation", + "description": "Pearson/Spearman相关系数", + "inputParams": [ + { "name": "var_x", "type": "string", "required": true, "description": "自变量" }, + { "name": "var_y", "type": "string", "required": true, "description": "因变量" }, + { "name": "method", "type": "string", "required": false, "description": "auto/pearson/spearman", "default": "auto" } + ], + "outputType": "correlation" + }, + { + "code": "ST_LOGISTIC_BINARY", + "name": "二元Logistic回归", + "category": "regression", + "description": "二分类结局的多因素分析", + "inputParams": [ + { "name": "outcome_var", "type": "string", "required": true, "description": "二分类结局变量" }, + { "name": "predictors", "type": "string[]", "required": true, "description": "预测变量列表" }, + { "name": "confounders", "type": "string[]", "required": false, "description": "混杂因素列表" } + ], + "outputType": "regression" + } + ] +} diff --git a/backend/src/modules/ssa/routes/config.routes.ts b/backend/src/modules/ssa/routes/config.routes.ts index 8d47c16c..3b4944c9 100644 --- a/backend/src/modules/ssa/routes/config.routes.ts +++ b/backend/src/modules/ssa/routes/config.routes.ts @@ -9,6 +9,7 @@ import { FastifyInstance, FastifyRequest } from 'fastify'; import { prisma } from '../../../config/database.js'; import { logger } from '../../../common/logging/index.js'; +import { reloadAllConfigs } from '../config/index.js'; function getUserId(request: FastifyRequest): string { const userId = (request as any).user?.userId; @@ -106,14 +107,29 @@ export default async function configRoutes(app: FastifyInstance) { return reply.send([]); }); - // 热加载配置 + // 热加载配置 — 重新读取所有领域 JSON 文件并 Zod 校验 app.post('/reload', async (req, reply) => { - // TODO: 重新加载所有配置到缓存 - - return reply.send({ - success: true, - timestamp: new Date().toISOString() - }); + logger.info('[SSA:Config] Reloading all config files...'); + + const results = reloadAllConfigs(); + const allSuccess = results.every(r => r.success); + const failures = results.filter(r => !r.success); + + if (allSuccess) { + logger.info('[SSA:Config] All configs reloaded successfully'); + return reply.send({ + success: true, + timestamp: new Date().toISOString(), + results, + }); + } else { + logger.warn('[SSA:Config] Some configs failed to reload', { failures }); + return reply.status(400).send({ + success: false, + message: `${failures.length} 个配置文件校验失败,已保留旧配置`, + results, + }); + } }); // 校验配置文件 diff --git a/backend/src/modules/ssa/routes/workflow.routes.ts b/backend/src/modules/ssa/routes/workflow.routes.ts index 8c49b532..42783204 100644 --- a/backend/src/modules/ssa/routes/workflow.routes.ts +++ b/backend/src/modules/ssa/routes/workflow.routes.ts @@ -13,6 +13,10 @@ import { logger } from '../../../common/logging/index.js'; import { workflowPlannerService } from '../services/WorkflowPlannerService.js'; import { workflowExecutorService } from '../services/WorkflowExecutorService.js'; import { dataProfileService } from '../services/DataProfileService.js'; +import { queryService } from '../services/QueryService.js'; +import { reflectionService } from '../services/ReflectionService.js'; +import { prisma } from '../../../config/database.js'; +import { cache } from '../../../common/cache/index.js'; // 请求类型定义 interface PlanWorkflowBody { @@ -74,6 +78,109 @@ export default async function workflowRoutes(app: FastifyInstance) { } ); + /** + * POST /workflow/intent + * Phase Q: LLM 意图理解 — 解析用户自然语言为结构化 ParsedQuery + */ + app.post<{ Body: PlanWorkflowBody }>( + '/intent', + async (request, reply) => { + const { sessionId, userQuery } = request.body; + + if (!sessionId || !userQuery) { + return reply.status(400).send({ + success: false, + error: 'sessionId and userQuery are required' + }); + } + + try { + logger.info('[SSA:API] Parsing intent', { sessionId, userQuery }); + + const parsed = await queryService.parseIntent(userQuery, sessionId); + + return reply.send({ + success: true, + intent: parsed, + needsClarification: parsed.needsClarification, + clarificationCards: parsed.clarificationCards || [], + }); + + } catch (error: any) { + logger.error('[SSA:API] Intent parsing failed', { + sessionId, + error: error.message + }); + + return reply.status(500).send({ + success: false, + error: error.message + }); + } + } + ); + + /** + * POST /workflow/clarify + * Phase Q: 用户回答追问卡片后,补全 ParsedQuery 并重新规划 + */ + app.post<{ Body: { sessionId: string; userQuery: string; selections: Record } }>( + '/clarify', + async (request, reply) => { + const { sessionId, userQuery, selections } = request.body; + + if (!sessionId) { + return reply.status(400).send({ + success: false, + error: 'sessionId is required' + }); + } + + try { + logger.info('[SSA:API] Processing clarification', { sessionId, selections }); + + // 将用户选择拼接到原始 query 中,重新走 intent 解析 + const selectionText = Object.entries(selections) + .map(([key, value]) => `${key}: ${value}`) + .join('; '); + const enrichedQuery = userQuery + ? `${userQuery}(补充说明:${selectionText})` + : selectionText; + + const parsed = await queryService.parseIntent(enrichedQuery, sessionId); + + // 如果这次置信度足够,直接生成工作流计划 + if (!parsed.needsClarification) { + const plan = await workflowPlannerService.planWorkflow(sessionId, enrichedQuery); + return reply.send({ + success: true, + intent: parsed, + plan, + needsClarification: false, + }); + } + + return reply.send({ + success: true, + intent: parsed, + needsClarification: true, + clarificationCards: parsed.clarificationCards || [], + }); + + } catch (error: any) { + logger.error('[SSA:API] Clarification failed', { + sessionId, + error: error.message + }); + + return reply.status(500).send({ + success: false, + error: error.message + }); + } + } + ); + /** * POST /workflow/:workflowId/execute * 执行工作流 @@ -329,6 +436,73 @@ export default async function workflowRoutes(app: FastifyInstance) { } } ); + + /** + * GET /workflow/sessions/:sessionId/conclusion + * 获取会话的分析结论(优先返回缓存,无缓存则从 workflow 结果重新生成) + */ + app.get<{ Params: { sessionId: string } }>( + '/sessions/:sessionId/conclusion', + async (request, reply) => { + const { sessionId } = request.params; + + try { + // 查找该 session 最新的 completed workflow + const workflow = await prisma.ssaWorkflow.findFirst({ + where: { sessionId: sessionId, status: { in: ['completed', 'partial'] } }, + orderBy: { createdAt: 'desc' }, + }); + + if (!workflow) { + return reply.status(404).send({ + success: false, + error: 'No completed workflow found for this session', + }); + } + + // 检查缓存 + const cacheKey = `ssa:conclusion:${workflow.id}`; + const cached = await cache.get(cacheKey); + if (cached) { + return reply.send({ success: true, conclusion: cached, source: 'cache' }); + } + + // 无缓存:获取 workflow steps 结果并重新生成 + const steps = await prisma.ssaWorkflowStep.findMany({ + where: { workflowId: workflow.id }, + orderBy: { stepOrder: 'asc' }, + }); + + const results = steps.map((s: any) => ({ + stepOrder: s.stepOrder, + toolCode: s.toolCode, + toolName: s.toolName, + status: s.status, + result: s.outputResult, + reportBlocks: s.reportBlocks, + executionMs: s.executionMs || 0, + })); + + const workflowPlan = workflow.workflowPlan as any; + const conclusion = await reflectionService.reflect( + { + workflowId: workflow.id, + goal: workflowPlan?.goal || '统计分析', + title: workflowPlan?.title, + methodology: workflowPlan?.methodology, + plannedTrace: workflowPlan?.planned_trace, + }, + results, + ); + + return reply.send({ success: true, conclusion, source: conclusion.source }); + + } catch (error: any) { + logger.error('[SSA:API] Get conclusion failed', { sessionId, error: error.message }); + return reply.status(500).send({ success: false, error: error.message }); + } + } + ); } /** diff --git a/backend/src/modules/ssa/services/ConclusionGeneratorService.ts b/backend/src/modules/ssa/services/ConclusionGeneratorService.ts index a0cd733b..252ff98a 100644 --- a/backend/src/modules/ssa/services/ConclusionGeneratorService.ts +++ b/backend/src/modules/ssa/services/ConclusionGeneratorService.ts @@ -11,9 +11,13 @@ import { logger } from '../../../common/logging/index.js'; import { StepResult } from './WorkflowExecutorService.js'; +import type { ConclusionReport } from '../types/reflection.types.js'; -// 结论报告结构 -export interface ConclusionReport { +// Re-export for backward compatibility +export type { ConclusionReport } from '../types/reflection.types.js'; + +// 旧版内部结论结构(ConclusionGeneratorService 内部使用) +export interface LegacyConclusionReport { title: string; summary: string; sections: ConclusionSection[]; @@ -40,8 +44,11 @@ export class ConclusionGeneratorService { * @param goal 分析目标 * @returns 结论报告 */ - generateConclusion(results: StepResult[], goal: string): ConclusionReport { - logger.info('[SSA:Conclusion] Generating conclusion', { + /** + * 生成新版 ConclusionReport(Phase R 统一格式) + */ + generateConclusion(results: StepResult[], goal: string, workflowId?: string): ConclusionReport { + logger.info('[SSA:Conclusion] Generating rule-based conclusion', { stepCount: results.length, goal }); @@ -60,17 +67,37 @@ export class ConclusionGeneratorService { const methodology = this.generateMethodology(results); const limitations = this.generateLimitations(results); + const significantCount = sections.filter(s => s.significance === 'significant').length; + const methodsUsed = [...new Set(successResults.map(r => r.toolName))]; + const report: ConclusionReport = { + workflow_id: workflowId || '', title: `统计分析报告:${goal}`, - summary, - sections, - methodology, - limitations + executive_summary: summary, + key_findings: sections + .filter(s => s.significance === 'significant' || s.significance === 'marginal') + .map(s => `${s.toolName}:${s.interpretation}`), + statistical_summary: { + total_tests: sections.length, + significant_results: significantCount, + methods_used: methodsUsed, + }, + step_summaries: sections.map(s => ({ + step_number: s.stepOrder, + tool_name: s.toolName, + summary: s.finding, + p_value: s.details?.pValue, + is_significant: s.significance === 'significant', + })), + recommendations: [], + limitations, + generated_at: new Date().toISOString(), + source: 'rule_based' as const, }; - logger.info('[SSA:Conclusion] Conclusion generated', { + logger.info('[SSA:Conclusion] Rule-based conclusion generated', { sectionCount: sections.length, - hasLimitations: limitations.length > 0 + significantCount, }); return report; diff --git a/backend/src/modules/ssa/services/DataProfileService.ts b/backend/src/modules/ssa/services/DataProfileService.ts index 59d7f656..8a71d8a3 100644 --- a/backend/src/modules/ssa/services/DataProfileService.ts +++ b/backend/src/modules/ssa/services/DataProfileService.ts @@ -46,6 +46,8 @@ export interface ColumnProfile { minDate?: string; maxDate?: string; dateRange?: string; + // Phase Q: 非分析列标记(由 Python DataProfiler 生成) + isIdLike?: boolean; } export interface DataSummary { diff --git a/backend/src/modules/ssa/services/DecisionTableService.ts b/backend/src/modules/ssa/services/DecisionTableService.ts new file mode 100644 index 00000000..ebb8eee9 --- /dev/null +++ b/backend/src/modules/ssa/services/DecisionTableService.ts @@ -0,0 +1,172 @@ +/** + * DecisionTableService — P 层决策表匹配 + * + * 四维匹配:Goal × OutcomeType × PredictorType × Design → Primary + Fallback + Template + * + * Repository 模式:通过 ConfigLoader 加载 JSON,后期可切 DB。 + * 参数检验优先原则:Primary 始终为参数检验,Fallback 为非参数安全网。 + */ + +import { logger } from '../../../common/logging/index.js'; +import { decisionTablesLoader } from '../config/index.js'; +import type { DecisionRule } from '../config/schemas.js'; +import type { ParsedQuery, AnalysisGoal, VariableType, StudyDesign } from '../types/query.types.js'; + +export interface MatchResult { + rule: DecisionRule; + primaryTool: string; + fallbackTool: string | null; + switchCondition: string | null; + templateId: string; + matchScore: number; +} + +export class DecisionTableService { + + /** + * 四维匹配 — 从决策表中找到最佳规则 + */ + match(query: ParsedQuery): MatchResult { + const rules = decisionTablesLoader.get(); + + const candidates = rules + .map(rule => ({ + rule, + score: this.scoreRule(rule, query), + })) + .filter(c => c.score > 0) + .sort((a, b) => { + if (b.score !== a.score) return b.score - a.score; + return b.rule.priority - a.rule.priority; + }); + + if (candidates.length === 0) { + logger.warn('[SSA:DecisionTable] No matching rule, falling back to descriptive', { + goal: query.goal, + outcomeType: query.outcome_type, + }); + return this.getDefaultMatch(); + } + + const best = candidates[0]; + + logger.info('[SSA:DecisionTable] Rule matched', { + ruleId: best.rule.id, + score: best.score, + primary: best.rule.primaryTool, + fallback: best.rule.fallbackTool, + template: best.rule.templateId, + }); + + return { + rule: best.rule, + primaryTool: best.rule.primaryTool, + fallbackTool: best.rule.fallbackTool, + switchCondition: best.rule.switchCondition, + templateId: best.rule.templateId, + matchScore: best.score, + }; + } + + /** + * 计算规则匹配分数 + * 精确匹配得分 > 通配符匹配 + */ + private scoreRule(rule: DecisionRule, query: ParsedQuery): number { + let score = 0; + + // Goal 匹配(必须) + if (rule.goal !== query.goal && rule.goal !== '*') return 0; + score += rule.goal === query.goal ? 4 : 1; + + // Outcome Type 匹配 + const outcomeType = this.normalizeVariableType(query.outcome_type); + if (rule.outcomeType !== '*') { + if (!this.typeMatches(rule.outcomeType, outcomeType)) return 0; + score += 3; + } else { + score += 1; + } + + // Predictor Type 匹配 + const predictorType = this.getPrimaryPredictorType(query); + if (rule.predictorType !== '*') { + if (!this.typeMatches(rule.predictorType, predictorType)) return 0; + score += 2; + } else { + score += 1; + } + + // Design 匹配 + if (rule.design !== '*') { + if (rule.design !== query.design) return 0; + score += 2; + } else { + score += 1; + } + + return score; + } + + /** + * 类型匹配(支持 binary ⊂ categorical 的包含关系) + */ + private typeMatches(ruleType: string, actualType: string | null): boolean { + if (!actualType) return true; + if (ruleType === actualType) return true; + if (ruleType === 'categorical' && actualType === 'binary') return true; + if (ruleType === 'binary' && actualType === 'categorical') return true; + return false; + } + + /** + * 归一化变量类型到决策表维度 + */ + private normalizeVariableType(type: VariableType | null): string | null { + if (!type) return null; + switch (type) { + case 'continuous': return 'continuous'; + case 'binary': return 'binary'; + case 'categorical': return 'categorical'; + case 'ordinal': return 'categorical'; + case 'datetime': return null; + default: return null; + } + } + + /** + * 获取主要预测变量类型 + */ + private getPrimaryPredictorType(query: ParsedQuery): string | null { + if (query.predictor_types.length === 0) return null; + return this.normalizeVariableType(query.predictor_types[0]); + } + + private getDefaultMatch(): MatchResult { + const rules = decisionTablesLoader.get(); + const descRule = rules.find(r => r.id === 'DESC_ANY'); + const fallback: DecisionRule = descRule || { + id: 'DESC_ANY', + goal: 'descriptive', + outcomeType: '*', + predictorType: '*', + design: '*', + primaryTool: 'ST_DESCRIPTIVE', + fallbackTool: null, + switchCondition: null, + templateId: 'descriptive_only', + priority: 1, + }; + + return { + rule: fallback, + primaryTool: fallback.primaryTool, + fallbackTool: null, + switchCondition: null, + templateId: fallback.templateId, + matchScore: 0, + }; + } +} + +export const decisionTableService = new DecisionTableService(); diff --git a/backend/src/modules/ssa/services/FlowTemplateService.ts b/backend/src/modules/ssa/services/FlowTemplateService.ts new file mode 100644 index 00000000..9a3102ea --- /dev/null +++ b/backend/src/modules/ssa/services/FlowTemplateService.ts @@ -0,0 +1,255 @@ +/** + * FlowTemplateService — P 层流程模板填充 + * + * 根据 DecisionTableService 的匹配结果,选择模板并填充参数。 + * 含 EPV 防护(队列研究 Table 3 自变量截断)。 + */ + +import { logger } from '../../../common/logging/index.js'; +import { flowTemplatesLoader, toolsRegistryLoader } from '../config/index.js'; +import type { FlowTemplate, TemplateStep, ToolDefinition } from '../config/schemas.js'; +import type { MatchResult } from './DecisionTableService.js'; +import type { ParsedQuery } from '../types/query.types.js'; +import type { DataProfile } from './DataProfileService.js'; + +export interface FilledStep { + order: number; + role: string; + toolCode: string; + toolName: string; + name: string; + params: Record; + isSensitivity: boolean; + switchCondition: string | null; +} + +export interface FillResult { + templateId: string; + templateName: string; + steps: FilledStep[]; + epvWarning: string | null; +} + +const DEFAULT_EPV_RATIO = 10; + +export class FlowTemplateService { + + /** + * 选择模板并填充参数 + */ + fill( + match: MatchResult, + query: ParsedQuery, + profile?: DataProfile | null + ): FillResult { + const config = flowTemplatesLoader.get(); + const template = config.templates.find(t => t.id === match.templateId); + + if (!template) { + logger.warn('[SSA:FlowTemplate] Template not found, using descriptive_only', { + templateId: match.templateId, + }); + const fallback = config.templates.find(t => t.id === 'descriptive_only')!; + return this.fillTemplate(fallback, match, query, profile); + } + + return this.fillTemplate(template, match, query, profile); + } + + private fillTemplate( + template: FlowTemplate, + match: MatchResult, + query: ParsedQuery, + profile?: DataProfile | null + ): FillResult { + const toolsConfig = toolsRegistryLoader.get(); + let epvWarning: string | null = null; + + const steps: FilledStep[] = []; + + for (const step of template.steps) { + // 条件步骤:fallback_exists — 如果没有 fallback 工具则跳过 + if (step.condition === 'fallback_exists' && !match.fallbackTool) { + continue; + } + + // 解析工具代码(支持 {{primaryTool}} / {{fallbackTool}} 占位符) + const toolCode = this.resolveToolCode(step.tool, match); + const toolDef = toolsConfig.tools.find(t => t.code === toolCode); + const toolName = toolDef?.name ?? toolCode; + + // 填充参数 + let params: Record; + if (step.paramsMapping) { + const result = this.resolveParams(step.paramsMapping, query, profile); + params = result.params; + if (result.epvWarning) epvWarning = result.epvWarning; + } else { + params = this.buildDefaultParams(toolCode, query); + } + + steps.push({ + order: step.order, + role: step.role, + toolCode, + toolName, + name: step.name ?? toolName, + params, + isSensitivity: step.role === 'sensitivity', + switchCondition: step.role === 'sensitivity' ? match.switchCondition : null, + }); + } + + return { + templateId: template.id, + templateName: template.name, + steps, + epvWarning, + }; + } + + private resolveToolCode(tool: string, match: MatchResult): string { + if (tool === '{{primaryTool}}') return match.primaryTool; + if (tool === '{{fallbackTool}}') return match.fallbackTool || match.primaryTool; + return tool; + } + + /** + * 解析参数映射中的占位符 + */ + private resolveParams( + mapping: Record, + query: ParsedQuery, + profile?: DataProfile | null + ): { params: Record; epvWarning: string | null } { + const params: Record = {}; + let epvWarning: string | null = null; + + for (const [key, template] of Object.entries(mapping)) { + switch (template) { + case '{{outcome_var}}': + params[key] = query.outcome_var; + break; + case '{{grouping_var}}': + params[key] = query.grouping_var; + break; + case '{{all_predictors}}': + params[key] = query.predictor_vars; + break; + case '{{epv_capped_predictors}}': { + const result = this.applyEpvCap(query, profile); + params[key] = result.predictors; + epvWarning = result.warning; + break; + } + default: + params[key] = template; + } + } + + return { params, epvWarning }; + } + + /** + * 构建默认参数(非 paramsMapping 模板步骤使用) + */ + private buildDefaultParams(toolCode: string, query: ParsedQuery): Record { + switch (toolCode) { + case 'ST_DESCRIPTIVE': + return { + variables: [ + ...(query.outcome_var ? [query.outcome_var] : []), + ...query.predictor_vars, + ].slice(0, 10), + group_var: query.grouping_var, + }; + + case 'ST_T_TEST_IND': + case 'ST_MANN_WHITNEY': + return { + group_var: query.grouping_var || query.predictor_vars[0], + value_var: query.outcome_var, + }; + + case 'ST_T_TEST_PAIRED': + return { + before_var: query.predictor_vars[0], + after_var: query.outcome_var, + }; + + case 'ST_CHI_SQUARE': + return { + var1: query.predictor_vars[0] || query.grouping_var, + var2: query.outcome_var, + }; + + case 'ST_CORRELATION': + return { + var_x: query.predictor_vars[0], + var_y: query.outcome_var, + method: 'auto', + }; + + case 'ST_LOGISTIC_BINARY': + return { + outcome_var: query.outcome_var, + predictors: query.predictor_vars, + }; + + default: + return {}; + } + } + + /** + * EPV 防护 — 队列研究 Table 3 自变量截断 + * EPV = Events Per Variable,每个自变量至少需要 10 个事件 + */ + private applyEpvCap( + query: ParsedQuery, + profile?: DataProfile | null + ): { predictors: string[]; warning: string | null } { + const allPredictors = query.predictor_vars; + + if (!profile || !query.outcome_var) { + return { predictors: allPredictors, warning: null }; + } + + const outcomeCol = profile.columns.find( + c => c.name.toLowerCase() === query.outcome_var!.toLowerCase() + ); + + if (!outcomeCol || outcomeCol.type !== 'categorical' || !outcomeCol.topValues) { + return { predictors: allPredictors, warning: null }; + } + + // 计算 EPV:min(outcome=0, outcome=1) / 10 + const counts = outcomeCol.topValues.map(v => v.count); + const minEvents = Math.min(...counts); + const maxVars = Math.floor(minEvents / DEFAULT_EPV_RATIO); + + if (maxVars <= 0) { + return { + predictors: allPredictors.slice(0, 1), + warning: `样本量不足(最少事件组仅 ${minEvents} 例),回归模型仅保留 1 个变量`, + }; + } + + if (allPredictors.length <= maxVars) { + return { predictors: allPredictors, warning: null }; + } + + const capped = allPredictors.slice(0, maxVars); + const warning = `受样本量限制(EPV=${DEFAULT_EPV_RATIO},最少事件组 ${minEvents} 例),回归模型从 ${allPredictors.length} 个变量截断至 ${maxVars} 个`; + + logger.info('[SSA:FlowTemplate] EPV cap applied', { + original: allPredictors.length, + capped: maxVars, + minEvents, + }); + + return { predictors: capped, warning }; + } +} + +export const flowTemplateService = new FlowTemplateService(); diff --git a/backend/src/modules/ssa/services/QueryService.ts b/backend/src/modules/ssa/services/QueryService.ts new file mode 100644 index 00000000..08db6159 --- /dev/null +++ b/backend/src/modules/ssa/services/QueryService.ts @@ -0,0 +1,457 @@ +/** + * SSA QueryService — Phase Q 核心服务 + * + * 职责:用户自然语言 → LLM 意图解析 → 结构化 ParsedQuery + * + * 三层防御: + * 1. LLM 调用 + jsonrepair 容错 + * 2. 动态 Zod Schema(验证列名真实性) + * 3. Confidence 二次验证(不信 LLM 自评) + * + * Fallback:LLM 失败 → 旧正则匹配(WorkflowPlannerService.parseUserIntent) + */ + +import { logger } from '../../../common/logging/index.js'; +import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js'; +import { getPromptService } from '../../../common/prompt/index.js'; +import { prisma } from '../../../config/database.js'; +import { cache } from '../../../common/cache/index.js'; +import { jsonrepair } from 'jsonrepair'; +import type { Message } from '../../../common/llm/adapters/types.js'; +import type { DataProfile, ColumnProfile } from './DataProfileService.js'; +import { dataProfileService } from './DataProfileService.js'; +import { + type ParsedQuery, + type LLMIntentOutput, + type ClarificationCard, + type ClarificationOption, + type PrunedProfile, + LLMIntentOutputSchema, + createDynamicIntentSchema, + validateConfidence, +} from '../types/query.types.js'; +import { AVAILABLE_TOOLS } from './WorkflowPlannerService.js'; + +const CONFIDENCE_THRESHOLD = 0.7; +const MAX_LLM_RETRIES = 1; + +export class QueryService { + + /** + * 解析用户意图(主入口) + * + * 流程:获取 profile(带缓存)→ LLM 解析 → Zod 校验 → confidence 验证 → 裁剪 + */ + async parseIntent( + userQuery: string, + sessionId: string, + profileOverride?: DataProfile | null + ): Promise { + logger.info('[SSA:Query] Parsing intent', { sessionId, queryLength: userQuery.length }); + + // Q5: 带缓存的 profile 获取 + const profile = profileOverride ?? await this.getProfileWithCache(sessionId); + + try { + const result = await this.llmParseIntent(userQuery, profile); + + // Q4: 附加裁剪后的 profile 给 P 层 + if (profile && !result.needsClarification) { + result.prunedProfile = this.pruneForPlanner(profile, result); + } + + logger.info('[SSA:Query] LLM intent parsed', { + sessionId, + goal: result.goal, + confidence: result.confidence, + needsClarification: result.needsClarification, + outcomeVar: result.outcome_var, + predictorCount: result.predictor_vars.length, + }); + return result; + } catch (error: any) { + logger.warn('[SSA:Query] LLM parsing failed, falling back to regex', { + sessionId, + error: error.message, + }); + return this.fallbackToRegex(userQuery, profile); + } + } + + /** + * LLM 意图解析(核心逻辑) + */ + private async llmParseIntent( + userQuery: string, + profile: DataProfile | null + ): Promise { + const promptService = getPromptService(prisma); + + // 1. 准备 Prompt 变量 + const profileSummary = profile + ? this.buildProfileSummaryForPrompt(profile) + : '(未上传数据文件)'; + + const toolList = Object.values(AVAILABLE_TOOLS) + .map(t => `- ${t.code}: ${t.name} — ${t.description}`) + .join('\n'); + + // 2. 获取渲染后的 Prompt + const rendered = await promptService.get('SSA_QUERY_INTENT', { + userQuery, + dataProfile: profileSummary, + availableTools: toolList, + }); + + // 3. 调用 LLM + const adapter = LLMFactory.getAdapter( + (rendered.modelConfig?.model as any) || 'deepseek-v3' + ); + + const messages: Message[] = [ + { role: 'system', content: rendered.content }, + { role: 'user', content: userQuery }, + ]; + + let llmOutput: LLMIntentOutput | null = null; + let lastError: Error | null = null; + + for (let attempt = 0; attempt <= MAX_LLM_RETRIES; attempt++) { + try { + const response = await adapter.chat(messages, { + temperature: rendered.modelConfig?.temperature ?? 0.3, + maxTokens: rendered.modelConfig?.maxTokens ?? 2048, + }); + + // 4. 三层 JSON 解析 + const raw = this.robustJsonParse(response.content); + + // 5. Zod 校验(动态防幻觉) + const validColumns = profile?.columns.map(c => c.name) ?? []; + const schema = validColumns.length > 0 + ? createDynamicIntentSchema(validColumns) + : LLMIntentOutputSchema; + + llmOutput = schema.parse(raw); + break; + + } catch (err: any) { + lastError = err; + logger.warn('[SSA:Query] LLM attempt failed', { + attempt, + error: err.message?.substring(0, 200), + }); + + // 重试时在 messages 中追加纠错提示 + if (attempt < MAX_LLM_RETRIES && profile) { + const cols = profile.columns.map(c => c.name).join(', '); + messages.push({ + role: 'user', + content: `你上次的输出有错误: ${err.message}。请注意:变量名必须是以下列名之一: ${cols}。请重新输出正确的 JSON。`, + }); + } + } + } + + if (!llmOutput) { + throw lastError || new Error('LLM intent parsing failed after retries'); + } + + // 6. Confidence 二次验证 + const correctedConfidence = validateConfidence(llmOutput); + + // 7. 构建 ParsedQuery + const parsed: ParsedQuery = { + ...llmOutput, + confidence: correctedConfidence, + needsClarification: correctedConfidence < CONFIDENCE_THRESHOLD, + }; + + // 8. 低置信度 → 生成追问卡片 + if (parsed.needsClarification && profile) { + parsed.clarificationCards = this.generateClarificationCards(parsed, profile); + } + + return parsed; + } + + /** + * 三层 JSON 解析(容错) + */ + private robustJsonParse(text: string): unknown { + // Layer 1: 直接解析 + try { + return JSON.parse(text); + } catch { /* continue */ } + + // Layer 2: 提取 JSON 代码块后解析 + const codeBlockMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/); + if (codeBlockMatch) { + try { + return JSON.parse(codeBlockMatch[1].trim()); + } catch { + try { + return JSON.parse(jsonrepair(codeBlockMatch[1].trim())); + } catch { /* continue */ } + } + } + + // Layer 3: jsonrepair 修复 + try { + return JSON.parse(jsonrepair(text)); + } catch { /* continue */ } + + // Layer 4: 尝试从文本中提取 JSON 对象 + const jsonMatch = text.match(/\{[\s\S]*\}/); + if (jsonMatch) { + try { + return JSON.parse(jsonrepair(jsonMatch[0])); + } catch { /* continue */ } + } + + throw new Error('Failed to parse LLM output as JSON'); + } + + /** + * 生成封闭式追问卡片 + * 基于 DataProfile 的真实数据,不靠 LLM 编造 + */ + private generateClarificationCards( + parsed: ParsedQuery, + profile: DataProfile + ): ClarificationCard[] { + const cards: ClarificationCard[] = []; + const numericCols = profile.columns.filter(c => c.type === 'numeric'); + const categoricalCols = profile.columns.filter(c => c.type === 'categorical'); + const binaryCols = categoricalCols.filter(c => c.totalLevels === 2); + + // 卡片 1:确认分析目标 + if (!parsed.goal || parsed.confidence < 0.5) { + const goalOptions: ClarificationOption[] = []; + + if (binaryCols.length > 0 && numericCols.length > 0) { + goalOptions.push({ + label: '比较组间差异', + value: 'comparison', + description: `如: 比较 ${binaryCols[0].name} 两组的 ${numericCols[0].name} 差异`, + }); + } + if (numericCols.length >= 2) { + goalOptions.push({ + label: '相关性分析', + value: 'correlation', + description: `如: 分析 ${numericCols[0].name} 和 ${numericCols[1].name} 的关系`, + }); + } + if (binaryCols.length > 0 && (numericCols.length + categoricalCols.length) >= 3) { + goalOptions.push({ + label: '多因素回归', + value: 'regression', + description: `如: 分析影响 ${binaryCols[0].name} 的独立因素`, + }); + } + goalOptions.push({ + label: '描述性统计', + value: 'descriptive', + description: '先看看数据的基本特征', + }); + + if (goalOptions.length > 1) { + cards.push({ + question: '您想进行哪种分析?', + options: goalOptions, + }); + } + } + + // 卡片 2:确认结局变量 + if (parsed.goal && parsed.goal !== 'descriptive' && !parsed.outcome_var) { + const candidates = parsed.goal === 'regression' + ? binaryCols + : [...numericCols, ...binaryCols]; + + if (candidates.length > 0) { + cards.push({ + question: '您想分析哪个结局指标?', + options: candidates.slice(0, 5).map(c => ({ + label: c.name, + value: c.name, + description: `${c.type}${c.totalLevels ? `, ${c.totalLevels}个水平` : ''}`, + })), + }); + } + } + + return cards; + } + + /** + * 为 LLM Prompt 构建数据画像摘要 + * 物理剔除 is_id_like 列(Phase Q 防御建议 2) + */ + private buildProfileSummaryForPrompt(profile: DataProfile): string { + const { summary, columns } = profile; + + const analysisColumns = columns.filter(c => !this.isIdLikeColumn(c, summary.totalRows)); + + const lines: string[] = [ + `## 数据概况`, + `- 样本量: ${summary.totalRows} 行`, + `- 变量数: ${analysisColumns.length} 列(已排除 ID/日期等非分析列)`, + `- 整体缺失率: ${summary.overallMissingRate}%`, + '', + `## 变量清单(仅分析变量)`, + ]; + + for (const col of analysisColumns) { + let desc = `- **${col.name}** [${col.type}]`; + + if (col.missingRate > 0) { + desc += ` (缺失 ${col.missingRate}%)`; + } + + if (col.type === 'numeric') { + desc += `: 均值=${col.mean}, SD=${col.std}, 范围=[${col.min}, ${col.max}]`; + } else if (col.type === 'categorical') { + const levels = col.topValues?.slice(0, 5).map(v => v.value).join(', '); + desc += `: ${col.totalLevels}个水平 (${levels}${col.totalLevels && col.totalLevels > 5 ? '...' : ''})`; + } + + lines.push(desc); + } + + return lines.join('\n'); + } + + /** + * 判断是否为非分析列(ID / 日期 / 高基数字符串) + * 优先使用 Python DataProfiler 的 isIdLike 标记,缺失时本地推断 + */ + private isIdLikeColumn(col: ColumnProfile, totalRows: number): boolean { + if (col.isIdLike !== undefined) return col.isIdLike; + + const name = col.name.toLowerCase(); + + if (/(_id|_no|编号|序号|id$|^id_)/.test(name)) { + return true; + } + if (col.type === 'datetime') { + return true; + } + if (col.type === 'text' || (col.type === 'categorical' && col.uniqueCount > 0)) { + if (totalRows > 0 && col.uniqueCount / totalRows > 0.95) { + return true; + } + } + + return false; + } + + // ──────────────────────────────────────────── + // Q4: Context Pruning — Q→P 层最小子集 + // ──────────────────────────────────────────── + + /** + * 裁剪 DataProfile 为 Planner 需要的最小 Hot Context + * 全量列的类型信息保留(轻量),只有 Y/X 变量保留详细统计 + */ + pruneForPlanner(fullProfile: DataProfile, parsed: ParsedQuery): PrunedProfile { + const relevantVars = new Set(); + if (parsed.outcome_var) relevantVars.add(parsed.outcome_var); + parsed.predictor_vars.forEach(v => relevantVars.add(v)); + if (parsed.grouping_var) relevantVars.add(parsed.grouping_var); + + return { + schema: fullProfile.columns.map(c => ({ name: c.name, type: c.type })), + details: fullProfile.columns.filter(c => relevantVars.has(c.name)), + sampleSize: fullProfile.summary.totalRows, + missingRateSummary: fullProfile.summary.overallMissingRate, + }; + } + + // ──────────────────────────────────────────── + // Q5: DataProfile 会话级缓存 + // ──────────────────────────────────────────── + + /** + * 获取 DataProfile,带会话级缓存 + * key: ssa:profile:{sessionId} + * 同一会话+同一文件的后续 Q 层循环直接读缓存 + */ + async getProfileWithCache(sessionId: string): Promise { + const cacheKey = `ssa:profile:${sessionId}`; + + // 1. 查内存缓存 + const cached = await cache.get(cacheKey); + if (cached) { + logger.debug('[SSA:Query] Profile cache hit', { sessionId }); + return cached; + } + + // 2. 查数据库(Prisma 存储) + const dbProfile = await dataProfileService.getCachedProfile(sessionId); + if (dbProfile) { + // 写入内存缓存(30 分钟 TTL) + await cache.set(cacheKey, dbProfile, 1800); + logger.debug('[SSA:Query] Profile loaded from DB, cached', { sessionId }); + return dbProfile; + } + + return null; + } + + /** + * 正则 Fallback — 复用 WorkflowPlannerService 的旧逻辑 + */ + private fallbackToRegex(userQuery: string, profile: DataProfile | null): ParsedQuery { + const query = userQuery.toLowerCase(); + + let goal: ParsedQuery['goal'] = 'descriptive'; + let design: ParsedQuery['design'] = 'independent'; + + if (query.includes('比较') || query.includes('差异') || query.includes('不同') || query.includes('有没有效')) { + goal = 'comparison'; + } else if (query.includes('相关') || query.includes('关系') || query.includes('关联')) { + goal = 'correlation'; + } else if (query.includes('影响') || query.includes('因素') || query.includes('预测') || query.includes('回归')) { + goal = 'regression'; + } else if (query.includes('队列') || query.includes('cohort')) { + goal = 'cohort_study'; + } + + if (query.includes('前后') || query.includes('配对') || query.includes('变化')) { + design = 'paired'; + } + + // 尝试从查询中匹配列名 + let outcomeVar: string | null = null; + const predictorVars: string[] = []; + + if (profile) { + for (const col of profile.columns) { + if (query.includes(col.name.toLowerCase())) { + if (!outcomeVar) { + outcomeVar = col.name; + } else { + predictorVars.push(col.name); + } + } + } + } + + return { + goal, + outcome_var: outcomeVar, + outcome_type: null, + predictor_vars: predictorVars, + predictor_types: [], + grouping_var: profile?.columns.find(c => c.type === 'categorical' && c.totalLevels === 2)?.name ?? null, + design, + confidence: 0.5, + reasoning: '(正则 fallback 模式)', + needsClarification: !outcomeVar && goal !== 'descriptive', + }; + } +} + +export const queryService = new QueryService(); diff --git a/backend/src/modules/ssa/services/ReflectionService.ts b/backend/src/modules/ssa/services/ReflectionService.ts new file mode 100644 index 00000000..03bc81b3 --- /dev/null +++ b/backend/src/modules/ssa/services/ReflectionService.ts @@ -0,0 +1,341 @@ +/** + * SSA ReflectionService — Phase R 核心服务 + * + * 职责:StepResult[] → LLM 论文级结论 → ConclusionReport + * + * 三层防御: + * 1. 统计量槽位注入(LLM 只生成叙述框架,数值从 R 输出渲染) + * 2. jsonrepair + Zod 强校验 LLM 输出结构 + * 3. 降级到 ConclusionGeneratorService(规则拼接) + * + * 交付策略:完整 JSON 收集 + Zod 校验 → 一次性 SSE 推送(不做字符流) + */ + +import { logger } from '../../../common/logging/index.js'; +import { LLMFactory } from '../../../common/llm/adapters/LLMFactory.js'; +import { getPromptService } from '../../../common/prompt/index.js'; +import { prisma } from '../../../config/database.js'; +import { cache } from '../../../common/cache/index.js'; +import { jsonrepair } from 'jsonrepair'; +import type { Message } from '../../../common/llm/adapters/types.js'; +import type { StepResult } from './WorkflowExecutorService.js'; +import { conclusionGeneratorService } from './ConclusionGeneratorService.js'; +import { + LLMConclusionSchema, + type ConclusionReport, + type StepFinding, +} from '../types/reflection.types.js'; + +const CACHE_TTL = 3600; // 1 hour +const LLM_MODEL = 'deepseek-v3'; +const LLM_TEMPERATURE = 0.3; +const LLM_MAX_TOKENS = 4096; + +interface PlannedTraceInput { + matchedRule?: string; + primaryTool?: string; + fallbackTool?: string | null; + switchCondition?: string | null; + reasoning?: string; + epvWarning?: string | null; +} + +interface ReflectInput { + workflowId: string; + goal: string; + title?: string; + methodology?: string; + sampleInfo?: string; + plannedTrace?: PlannedTraceInput; +} + +export class ReflectionService { + + /** + * 生成论文级结论(主入口) + * + * 流程:缓存检查 → 提取 keyFindings → 组装 Prompt → LLM 调用 → Zod 校验 → fallback + */ + async reflect( + input: ReflectInput, + results: StepResult[], + ): Promise { + const { workflowId, goal } = input; + + logger.info('[SSA:Reflection] Starting reflection', { + workflowId, + goal, + stepCount: results.length, + }); + + // 0. Cache hit check + const cacheKey = `ssa:conclusion:${workflowId}`; + try { + const cached = await cache.get(cacheKey); + if (cached) { + logger.info('[SSA:Reflection] Cache hit', { workflowId }); + return cached; + } + } catch { + // cache miss, continue + } + + // 1. Extract key findings from step results (slot injection) + const findings = this.extractKeyFindings(results); + + // 2. Build prompt via PromptService + const prompt = await this.buildPrompt(input, findings); + if (!prompt) { + logger.warn('[SSA:Reflection] Failed to build prompt, falling back to rule-based'); + return this.fallback(workflowId, results, goal); + } + + // 3. Call LLM (full collection, no streaming) + try { + const llm = LLMFactory.getAdapter(LLM_MODEL); + const messages: Message[] = [ + { role: 'system', content: 'You are a senior biostatistician. Output only valid JSON.' }, + { role: 'user', content: prompt }, + ]; + + logger.info('[SSA:Reflection] Calling LLM', { model: LLM_MODEL }); + const response = await llm.chat(messages, { + temperature: LLM_TEMPERATURE, + maxTokens: LLM_MAX_TOKENS, + }); + + const rawOutput = response.content; + logger.info('[SSA:Reflection] LLM response received', { + contentLength: rawOutput.length, + usage: response.usage, + }); + + // 4. jsonrepair + Zod validation + const report = this.parseAndValidate(rawOutput, workflowId, input, findings, results); + + // 5. Cache the result + try { + await cache.set(cacheKey, report, CACHE_TTL); + } catch (cacheErr) { + logger.warn('[SSA:Reflection] Cache set failed', { error: String(cacheErr) }); + } + + logger.info('[SSA:Reflection] LLM conclusion generated successfully', { + workflowId, + source: 'llm', + keyFindingsCount: report.key_findings.length, + }); + + return report; + + } catch (error: any) { + logger.warn('[SSA:Reflection] LLM call failed, falling back to rule-based', { + workflowId, + error: error.message, + }); + return this.fallback(workflowId, results, goal); + } + } + + /** + * 从 StepResult[] 中提取关键统计量(槽位注入数据源) + */ + extractKeyFindings(results: StepResult[]): StepFinding[] { + const findings: StepFinding[] = []; + + for (const r of results) { + if (r.status !== 'success' && r.status !== 'warning') continue; + + const data = r.result || {}; + const finding: StepFinding = { + step_number: r.stepOrder, + tool_name: r.toolName, + tool_code: r.toolCode, + method: data.method || r.toolName, + is_significant: data.p_value != null && data.p_value < 0.05, + raw_result: data, + }; + + // P value + if (data.p_value != null) { + finding.p_value_num = data.p_value; + finding.p_value = data.p_value_fmt || this.formatPValue(data.p_value); + } + + // Statistic + if (data.statistic != null) { + finding.statistic = String(Number(data.statistic).toFixed(3)); + finding.statistic_name = this.getStatisticName(r.toolCode); + } + if (data.statistic_U != null) { + finding.statistic = String(Number(data.statistic_U).toFixed(1)); + finding.statistic_name = 'U'; + } + + // Effect size + if (data.effect_size?.cohens_d != null) { + finding.effect_size = String(Number(data.effect_size.cohens_d).toFixed(3)); + finding.effect_size_name = "Cohen's d"; + } else if (data.effect_size?.cramers_v != null) { + finding.effect_size = String(Number(data.effect_size.cramers_v).toFixed(3)); + finding.effect_size_name = "Cramér's V"; + } else if (data.effect_size?.r_squared != null) { + finding.effect_size = String(Number(data.effect_size.r_squared).toFixed(3)); + finding.effect_size_name = 'R²'; + } + + // Confidence interval + if (data.conf_int && Array.isArray(data.conf_int) && data.conf_int.length >= 2) { + finding.ci_lower = String(Number(data.conf_int[0]).toFixed(3)); + finding.ci_upper = String(Number(data.conf_int[1]).toFixed(3)); + } + + // Group stats + if (data.group_stats && Array.isArray(data.group_stats)) { + finding.group_stats = data.group_stats.map((g: any) => ({ + group: g.group || g.level || 'unknown', + n: g.n || 0, + mean: g.mean != null ? Number(Number(g.mean).toFixed(2)) : undefined, + sd: g.sd != null ? Number(Number(g.sd).toFixed(2)) : undefined, + median: g.median != null ? Number(Number(g.median).toFixed(2)) : undefined, + })); + } + + findings.push(finding); + } + + return findings; + } + + /** + * 构建 Prompt(通过 PromptService 从数据库加载模板) + */ + private async buildPrompt( + input: ReflectInput, + findings: StepFinding[], + ): Promise { + try { + const promptService = getPromptService(prisma); + const rendered = await promptService.get('SSA_REFLECTION', { + goal: input.goal, + title: input.title || `统计分析:${input.goal}`, + methodology: input.methodology || '系统自动选择', + sampleInfo: input.sampleInfo || '见各步骤详情', + decision_trace: { + matched_rule: input.plannedTrace?.matchedRule || '默认规则', + primary_tool: input.plannedTrace?.primaryTool || '', + fallback_tool: input.plannedTrace?.fallbackTool || null, + switch_condition: input.plannedTrace?.switchCondition || null, + reasoning: input.plannedTrace?.reasoning || '', + epv_warning: input.plannedTrace?.epvWarning || null, + }, + findings: findings.map(f => ({ + ...f, + group_stats: f.group_stats || [], + })), + }); + + return rendered.content; + } catch (error: any) { + logger.error('[SSA:Reflection] Failed to build prompt', { error: error.message }); + return null; + } + } + + /** + * 解析 LLM 输出 → jsonrepair → Zod 校验 + */ + private parseAndValidate( + rawOutput: string, + workflowId: string, + input: ReflectInput, + findings: StepFinding[], + results: StepResult[], + ): ConclusionReport { + // Strip markdown code fences if present + let cleaned = rawOutput.trim(); + if (cleaned.startsWith('```')) { + cleaned = cleaned.replace(/^```(?:json)?\s*\n?/, '').replace(/\n?```\s*$/, ''); + } + + // Layer 1: jsonrepair + const repaired = jsonrepair(cleaned); + + // Layer 2: JSON.parse + const parsed = JSON.parse(repaired); + + // Layer 3: Zod validation + const validated = LLMConclusionSchema.parse(parsed); + + // Assemble full ConclusionReport + return { + workflow_id: workflowId, + title: input.title || `统计分析报告:${input.goal}`, + executive_summary: validated.executive_summary, + key_findings: validated.key_findings, + statistical_summary: validated.statistical_summary, + step_summaries: this.buildStepSummaries(findings), + recommendations: validated.recommendations || [], + limitations: validated.limitations, + generated_at: new Date().toISOString(), + source: 'llm', + }; + } + + /** + * 降级到规则拼接 + */ + private fallback( + workflowId: string, + results: StepResult[], + goal: string, + ): ConclusionReport { + logger.info('[SSA:Reflection] Using rule-based fallback', { workflowId }); + return conclusionGeneratorService.generateConclusion(results, goal, workflowId); + } + + /** + * 从 findings 构建 step_summaries + */ + private buildStepSummaries(findings: StepFinding[]): ConclusionReport['step_summaries'] { + return findings.map(f => ({ + step_number: f.step_number, + tool_name: f.tool_name, + summary: this.buildStepSummaryText(f), + p_value: f.p_value_num, + is_significant: f.is_significant, + })); + } + + private buildStepSummaryText(f: StepFinding): string { + const parts: string[] = []; + if (f.statistic) parts.push(`${f.statistic_name || '统计量'} = ${f.statistic}`); + if (f.p_value) parts.push(`P ${f.p_value}`); + if (f.effect_size) parts.push(`${f.effect_size_name || '效应量'} = ${f.effect_size}`); + return parts.length > 0 ? parts.join(', ') : `${f.tool_name} 分析完成`; + } + + private formatPValue(p: number): string { + if (p < 0.001) return '< 0.001'; + if (p < 0.01) return `= ${p.toFixed(3)}`; + return `= ${p.toFixed(3)}`; + } + + private getStatisticName(toolCode: string): string { + const map: Record = { + 'ST_T_TEST_IND': 't', + 'ST_T_TEST_PAIRED': 't', + 'ST_MANN_WHITNEY': 'U', + 'ST_WILCOXON': 'W', + 'ST_CHI_SQUARE': 'χ²', + 'ST_FISHER': 'OR', + 'ST_ANOVA_ONE': 'F', + 'ST_CORRELATION': 'r', + 'ST_LINEAR_REG': 'F', + 'ST_LOGISTIC_BINARY': 'χ²', + }; + return map[toolCode] || '统计量'; + } +} + +export const reflectionService = new ReflectionService(); diff --git a/backend/src/modules/ssa/services/WorkflowExecutorService.ts b/backend/src/modules/ssa/services/WorkflowExecutorService.ts index 13dfe09f..7daaa137 100644 --- a/backend/src/modules/ssa/services/WorkflowExecutorService.ts +++ b/backend/src/modules/ssa/services/WorkflowExecutorService.ts @@ -17,7 +17,10 @@ import { logger } from '../../../common/logging/index.js'; import { prisma } from '../../../config/database.js'; import { storage } from '../../../common/storage/index.js'; import { WorkflowStep, ToolCode, AVAILABLE_TOOLS } from './WorkflowPlannerService.js'; -import { conclusionGeneratorService, ConclusionReport } from './ConclusionGeneratorService.js'; +import { conclusionGeneratorService } from './ConclusionGeneratorService.js'; +import { reflectionService } from './ReflectionService.js'; +import type { ConclusionReport } from '../types/reflection.types.js'; +import { classifyRError } from '../types/reflection.types.js'; // 步骤执行结果 export interface StepResult { @@ -26,6 +29,7 @@ export interface StepResult { toolName: string; status: 'success' | 'warning' | 'error' | 'skipped'; result?: any; + reportBlocks?: ReportBlock[]; guardrailChecks?: GuardrailCheck[]; error?: { code: string; @@ -35,6 +39,19 @@ export interface StepResult { executionMs: number; } +// Block-based 输出协议(与 R 端 block_helpers.R 对应) +export interface ReportBlock { + type: 'markdown' | 'table' | 'image' | 'key_value'; + title?: string; + content?: string; // markdown + headers?: string[]; // table + rows?: any[][]; // table + footnote?: string; // table + data?: string; // image (base64 data URI) + alt?: string; // image + items?: { key: string; value: string }[]; // key_value +} + // 护栏检查结果 export interface GuardrailCheck { checkName: string; @@ -55,6 +72,7 @@ export interface SSEMessage { progress?: number; durationMs?: number; result?: any; + reportBlocks?: ReportBlock[]; error?: { code: string; message: string; @@ -71,6 +89,7 @@ export interface WorkflowExecutionResult { completedSteps: number; successSteps: number; results: StepResult[]; + reportBlocks?: ReportBlock[]; conclusion?: ConclusionReport; executionMs: number; } @@ -175,7 +194,7 @@ export class WorkflowExecutorService extends EventEmitter { previousResults = stepResult.result; } - // 发送 SSE 消息 + // 发送 SSE 消息(report_blocks 同时以顶层字段推送,方便前端直接消费) this.emitProgress({ type: stepResult.status === 'error' ? 'step_error' : 'step_complete', step: step.stepOrder, @@ -187,6 +206,7 @@ export class WorkflowExecutorService extends EventEmitter { ? `${step.toolName} 执行失败: ${stepResult.error?.message}` : `${step.toolName} 执行完成`, result: stepResult.result, + reportBlocks: stepResult.reportBlocks, durationMs: stepResult.executionMs, error: stepResult.error, timestamp: new Date().toISOString() @@ -234,14 +254,30 @@ export class WorkflowExecutorService extends EventEmitter { timestamp: new Date().toISOString() }); - // 生成综合结论 + // 生成综合结论(Phase R:优先 LLM,失败降级到规则拼接) let conclusion: ConclusionReport | undefined; if (successCount > 0) { const workflowPlan = workflow.workflowPlan as any; - conclusion = conclusionGeneratorService.generateConclusion( - results, - workflowPlan?.goal || '统计分析' - ); + const goal = workflowPlan?.goal || '统计分析'; + try { + conclusion = await reflectionService.reflect( + { + workflowId, + goal, + title: workflowPlan?.title, + methodology: workflowPlan?.methodology, + sampleInfo: workflowPlan?.sampleInfo, + plannedTrace: workflowPlan?.planned_trace, + }, + results, + ); + } catch (reflectErr: any) { + logger.warn('[SSA:Executor] ReflectionService failed, using rule-based fallback', { + workflowId, + error: reflectErr.message, + }); + conclusion = conclusionGeneratorService.generateConclusion(results, goal, workflowId); + } } logger.info('[SSA:Executor] Workflow execution finished', { @@ -253,6 +289,14 @@ export class WorkflowExecutorService extends EventEmitter { hasConclusion: !!conclusion }); + // 聚合所有步骤的 reportBlocks(按步骤顺序拼接) + const allReportBlocks = results.reduce((acc, r) => { + if (r.reportBlocks?.length) { + acc.push(...r.reportBlocks); + } + return acc; + }, []); + return { workflowId, status: finalStatus, @@ -260,6 +304,7 @@ export class WorkflowExecutorService extends EventEmitter { completedSteps: results.length, successSteps: successCount, results, + reportBlocks: allReportBlocks.length > 0 ? allReportBlocks : undefined, conclusion, executionMs }; @@ -363,6 +408,8 @@ export class WorkflowExecutorService extends EventEmitter { const executionMs = Date.now() - startTime; if (response.data.status === 'error' || response.data.status === 'blocked') { + const rMsg = response.data.message || '执行失败'; + const classified = classifyRError(rMsg); return { stepOrder: step.stepOrder, toolCode: step.toolCode, @@ -370,14 +417,18 @@ export class WorkflowExecutorService extends EventEmitter { status: 'error', guardrailChecks, error: { - code: response.data.error_code || 'E100', - message: response.data.message || '执行失败', - userHint: response.data.user_hint || '请检查数据和参数' + code: response.data.error_code || classified.code, + message: rMsg, + userHint: response.data.user_hint || classified.userHint, }, executionMs }; } + const reportBlocks: ReportBlock[] | undefined = response.data.report_blocks?.length > 0 + ? response.data.report_blocks + : undefined; + return { stepOrder: step.stepOrder, toolCode: step.toolCode, @@ -386,22 +437,26 @@ export class WorkflowExecutorService extends EventEmitter { result: { ...response.data.results, plots: response.data.plots, + report_blocks: response.data.report_blocks, result_table: response.data.result_table, reproducible_code: response.data.reproducible_code, trace_log: response.data.trace_log, warnings: response.data.warnings, }, + reportBlocks, guardrailChecks, executionMs }; } catch (error: any) { const executionMs = Date.now() - startTime; + const classified = classifyRError(error.message || ''); logger.error('[SSA:Executor] Step execution failed', { step: step.stepOrder, toolCode: step.toolCode, - error: error.message + error: error.message, + classifiedCode: classified.code, }); return { @@ -410,9 +465,9 @@ export class WorkflowExecutorService extends EventEmitter { toolName: step.toolName, status: 'error', error: { - code: 'E100', + code: classified.code, message: error.message, - userHint: '执行过程中发生错误,请重试' + userHint: classified.userHint, }, executionMs }; diff --git a/backend/src/modules/ssa/services/WorkflowPlannerService.ts b/backend/src/modules/ssa/services/WorkflowPlannerService.ts index 2f17237f..69d65d8e 100644 --- a/backend/src/modules/ssa/services/WorkflowPlannerService.ts +++ b/backend/src/modules/ssa/services/WorkflowPlannerService.ts @@ -12,6 +12,11 @@ import { logger } from '../../../common/logging/index.js'; import { prisma } from '../../../config/database.js'; import { DataProfile, dataProfileService } from './DataProfileService.js'; +import { queryService } from './QueryService.js'; +import { decisionTableService, type MatchResult } from './DecisionTableService.js'; +import { flowTemplateService, type FilledStep, type FillResult } from './FlowTemplateService.js'; +import { toolsRegistryLoader } from '../config/index.js'; +import type { ParsedQuery } from '../types/query.types.js'; // 可用工具定义 export const AVAILABLE_TOOLS = { @@ -77,6 +82,17 @@ export const AVAILABLE_TOOLS = { export type ToolCode = keyof typeof AVAILABLE_TOOLS; +/** P 层策略日志 — 记录规划决策,供 R 层合并 E 层事实后生成方法学说明 */ +export interface PlannedTrace { + matchedRule: string; + primaryTool: string; + fallbackTool: string | null; + switchCondition: string | null; + templateUsed: string; + reasoning: string; + epvWarning: string | null; +} + // 工作流步骤 export interface WorkflowStep { stepOrder: number; @@ -109,9 +125,13 @@ export interface WorkflowPlan { description: string; params: Record; depends_on?: number[]; + is_sensitivity?: boolean; + switch_condition?: string | null; }>; estimated_time_seconds?: number; created_at: string; + planned_trace?: PlannedTrace; + epv_warning?: string | null; } // 用户意图解析结果 @@ -151,53 +171,151 @@ export class WorkflowPlannerService { profile = await dataProfileService.getCachedProfile(sessionId) || undefined; } - // 解析用户意图 - const intent = this.parseUserIntent(userQuery, profile); - - // 根据意图生成工作流 - const steps = this.generateSteps(intent, profile); - + // Phase Q: LLM 意图理解 + let parsedQuery: ParsedQuery; + try { + parsedQuery = await queryService.parseIntent(userQuery, sessionId, profile || null); + logger.info('[SSA:Planner] LLM intent parsed', { goal: parsedQuery.goal, confidence: parsedQuery.confidence }); + } catch (error: any) { + logger.warn('[SSA:Planner] QueryService failed, using regex fallback', { error: error.message }); + parsedQuery = queryService['fallbackToRegex'](userQuery, profile || null); + } + + // Phase P: 决策表匹配 → 流程模板填充(配置驱动,不写 if-else) + const match = decisionTableService.match(parsedQuery); + const fillResult = flowTemplateService.fill(match, parsedQuery, profile); + + // 构建 PlannedTrace(策略日志) + const plannedTrace: PlannedTrace = { + matchedRule: `Goal=${parsedQuery.goal}, Y=${parsedQuery.outcome_type || '*'}, X=${parsedQuery.predictor_types[0] || '*'}, Design=${parsedQuery.design}`, + primaryTool: match.primaryTool, + fallbackTool: match.fallbackTool, + switchCondition: match.switchCondition, + templateUsed: fillResult.templateId, + reasoning: this.buildPlanReasoning(match, fillResult, parsedQuery), + epvWarning: fillResult.epvWarning, + }; + + // 转换为 WorkflowStep(兼容旧的 saveWorkflow 格式) + const workflowSteps: WorkflowStep[] = fillResult.steps.map((s, i) => ({ + stepOrder: s.order, + toolCode: s.toolCode as ToolCode, + toolName: s.toolName, + inputParams: s.params, + purpose: s.name, + dependsOn: i > 0 ? [fillResult.steps[i - 1].order] : undefined, + })); + // 构建内部计划 const internalPlan: WorkflowPlanInternal = { - goal: intent.goal, - reasoning: this.generateReasoning(intent, steps), - steps, - estimatedDuration: this.estimateDuration(steps) + goal: parsedQuery.goal, + reasoning: plannedTrace.reasoning, + steps: workflowSteps, + estimatedDuration: this.estimateDuration(workflowSteps), }; // 保存到数据库 const workflowId = await this.saveWorkflow(sessionId, internalPlan); - logger.info('[SSA:Planner] Workflow planned', { + logger.info('[SSA:Planner] Workflow planned (config-driven)', { sessionId, - stepCount: steps.length, - tools: steps.map(s => s.toolCode) + stepCount: workflowSteps.length, + tools: workflowSteps.map(s => s.toolCode), + template: fillResult.templateId, + rule: match.rule.id, }); // 转换为前端期望的格式 const plan: WorkflowPlan = { workflow_id: workflowId, session_id: sessionId, - title: intent.goal, - description: internalPlan.reasoning, - total_steps: steps.length, - steps: steps.map(s => ({ - step_number: s.stepOrder, + title: fillResult.templateName, + description: plannedTrace.reasoning, + total_steps: fillResult.steps.length, + steps: fillResult.steps.map((s, i) => ({ + step_number: s.order, tool_code: s.toolCode, tool_name: s.toolName, - description: s.purpose, - params: s.inputParams, - depends_on: s.dependsOn + description: s.name, + params: s.params, + depends_on: i > 0 ? [fillResult.steps[i - 1].order] : undefined, + is_sensitivity: s.isSensitivity, + switch_condition: s.switchCondition, })), - estimated_time_seconds: steps.length * 5, - created_at: new Date().toISOString() + estimated_time_seconds: fillResult.steps.length * 5, + created_at: new Date().toISOString(), + planned_trace: plannedTrace, + epv_warning: fillResult.epvWarning, }; return plan; } + /** + * 生成人类可读的规划理由 + */ + private buildPlanReasoning( + match: MatchResult, + fill: FillResult, + query: ParsedQuery + ): string { + const lines: string[] = []; + + lines.push(`根据您的分析目标,为您规划了「${fill.templateName}」流程(${fill.steps.length} 步):`); + + for (const step of fill.steps) { + let desc = `${step.order}. ${step.name}(${step.toolName})`; + if (step.isSensitivity && step.switchCondition) { + desc += ` — 🛡️护栏:${step.switchCondition}`; + } + lines.push(desc); + } + + if (match.switchCondition) { + lines.push(`\n说明:系统会自动检验统计前提假设。若 ${match.switchCondition},将自动降级为备选方法。`); + } + + if (fill.epvWarning) { + lines.push(`\n⚠️ ${fill.epvWarning}`); + } + + return lines.join('\n'); + } + + /** + * 将 Phase Q 的 ParsedQuery 转换为旧版 ParsedIntent(桥接层) + * 让下游 generateSteps 无需改动即可消费 LLM 解析结果 + */ + private convertParsedQueryToIntent(pq: ParsedQuery, profile?: DataProfile): ParsedIntent { + const goalMap: Record = { + comparison: 'comparison', + correlation: 'correlation', + regression: 'regression', + descriptive: 'descriptive', + cohort_study: 'mixed', + }; + + return { + goal: pq.reasoning || pq.goal, + analysisType: goalMap[pq.goal] || 'descriptive', + design: pq.design === 'paired' ? 'paired' : 'independent', + variables: { + mentioned: [ + ...(pq.outcome_var ? [pq.outcome_var] : []), + ...pq.predictor_vars, + ], + outcome: pq.outcome_var ?? undefined, + predictors: pq.predictor_vars, + grouping: pq.grouping_var ?? undefined, + continuous: profile?.columns.filter(c => c.type === 'numeric').map(c => c.name) ?? [], + categorical: profile?.columns.filter(c => c.type === 'categorical').map(c => c.name) ?? [], + }, + }; + } + /** * 解析用户意图(改进版:识别用户提到的变量并选择合适方法) + * @deprecated Phase Q 后由 QueryService.parseIntent 替代,此方法保留为 fallback */ private parseUserIntent(userQuery: string, profile?: DataProfile): ParsedIntent { const query = userQuery.toLowerCase(); diff --git a/backend/src/modules/ssa/types/query.types.ts b/backend/src/modules/ssa/types/query.types.ts new file mode 100644 index 00000000..0d97372f --- /dev/null +++ b/backend/src/modules/ssa/types/query.types.ts @@ -0,0 +1,161 @@ +/** + * Phase Q — Query Layer 类型定义 + * + * Q 层输出 → P 层输入的标准契约 + * LLM 意图解析结果 + Zod 动态校验 Schema + */ + +import { z } from 'zod'; + +// ──────────────────────────────────────────── +// 1. 核心类型定义 +// ──────────────────────────────────────────── + +/** 分析目标类型 */ +export type AnalysisGoal = 'comparison' | 'correlation' | 'regression' | 'descriptive' | 'cohort_study'; + +/** 变量类型 */ +export type VariableType = 'continuous' | 'binary' | 'categorical' | 'ordinal' | 'datetime'; + +/** 研究设计类型 */ +export type StudyDesign = 'independent' | 'paired' | 'longitudinal' | 'cross_sectional'; + +/** + * ParsedQuery — Q 层的标准输出 + * LLM 解析用户意图后生成,传递给 P 层(Planner) + */ +export interface ParsedQuery { + goal: AnalysisGoal; + outcome_var: string | null; + outcome_type: VariableType | null; + predictor_vars: string[]; + predictor_types: VariableType[]; + grouping_var: string | null; + design: StudyDesign; + confidence: number; + reasoning: string; + needsClarification: boolean; + clarificationCards?: ClarificationCard[]; + dataDiagnosis?: DataDiagnosis; + prunedProfile?: PrunedProfile; +} + +/** 追问卡片 — 封闭式数据驱动选项 */ +export interface ClarificationCard { + question: string; + options: ClarificationOption[]; +} + +export interface ClarificationOption { + label: string; + value: string; + description?: string; +} + +/** 数据诊断结果 */ +export interface DataDiagnosis { + sampleSizeAdequate: boolean; + sampleSize: number; + missingRateWarnings: string[]; + outlierWarnings: string[]; + groupBalanceWarning?: string; + recommendations: string[]; +} + +/** 裁剪后的数据画像(Hot Context — 仅传给 P 层) */ +export interface PrunedProfile { + schema: Array<{ name: string; type: string }>; + details: any[]; + sampleSize: number; + missingRateSummary: number; +} + +// ──────────────────────────────────────────── +// 2. LLM 原始输出的 Zod Schema(静态版本) +// ──────────────────────────────────────────── + +/** LLM 直接输出的 JSON 结构(Zod 校验用) */ +export const LLMIntentOutputSchema = z.object({ + goal: z.enum(['comparison', 'correlation', 'regression', 'descriptive', 'cohort_study']), + outcome_var: z.string().nullable().default(null), + outcome_type: z.enum(['continuous', 'binary', 'categorical', 'ordinal', 'datetime']).nullable().default(null), + predictor_vars: z.array(z.string()).default([]), + predictor_types: z.array(z.enum(['continuous', 'binary', 'categorical', 'ordinal', 'datetime'])).default([]), + grouping_var: z.string().nullable().default(null), + design: z.enum(['independent', 'paired', 'longitudinal', 'cross_sectional']).default('independent'), + confidence: z.number().min(0).max(1).default(0.5), + reasoning: z.string().default(''), +}); + +export type LLMIntentOutput = z.infer; + +// ──────────────────────────────────────────── +// 3. 动态防幻觉 Schema 工厂(核心防御机制) +// ──────────────────────────────────────────── + +/** + * 基于真实列名动态生成 Zod Schema + * 防止 LLM 捏造不存在的列名 + * + * @param validColumns 数据中实际存在的列名列表 + */ +export function createDynamicIntentSchema(validColumns: string[]) { + const colSet = new Set(validColumns.map(c => c.toLowerCase())); + + const validateColumnName = (val: string | null) => { + if (val === null || val === '') return true; + return colSet.has(val.toLowerCase()); + }; + + const validateColumnArray = (vals: string[]) => { + return vals.every(v => colSet.has(v.toLowerCase())); + }; + + return LLMIntentOutputSchema.extend({ + outcome_var: z.string().nullable().default(null).refine( + validateColumnName, + { message: `LLM 输出了不存在的结局变量。有效列名: ${validColumns.join(', ')}` } + ), + predictor_vars: z.array(z.string()).default([]).refine( + validateColumnArray, + { message: `LLM 输出了不存在的自变量。有效列名: ${validColumns.join(', ')}` } + ), + grouping_var: z.string().nullable().default(null).refine( + validateColumnName, + { message: `LLM 输出了不存在的分组变量。有效列名: ${validColumns.join(', ')}` } + ), + }); +} + +// ──────────────────────────────────────────── +// 4. Confidence 二次验证 +// ──────────────────────────────────────────── + +/** + * 对 LLM 自评的 confidence 做客观化二次校正 + * 规则:不信 LLM 的自评,用实际输出倒推 + */ +export function validateConfidence(parsed: LLMIntentOutput): number { + let confidence = parsed.confidence; + + // 规则 1:高 confidence 但缺少关键变量 → 强制降级 + if (confidence >= 0.9) { + if (!parsed.outcome_var && parsed.predictor_vars.length === 0) { + confidence = 0.4; + } else if (!parsed.outcome_var || parsed.predictor_vars.length === 0) { + confidence = Math.min(confidence, 0.75); + } + } + + // 规则 2:goal 是 descriptive 天然不需要 Y/X,允许高 confidence + if (parsed.goal === 'descriptive') { + confidence = Math.max(confidence, 0.7); + } + + // 规则 3:有完整的 Y + X + goal → 保底 0.7 + if (parsed.outcome_var && parsed.predictor_vars.length > 0 && parsed.goal !== 'descriptive') { + confidence = Math.max(confidence, 0.7); + } + + return Math.round(confidence * 100) / 100; +} diff --git a/backend/src/modules/ssa/types/reflection.types.ts b/backend/src/modules/ssa/types/reflection.types.ts new file mode 100644 index 00000000..9ec46d21 --- /dev/null +++ b/backend/src/modules/ssa/types/reflection.types.ts @@ -0,0 +1,153 @@ +/** + * SSA Reflection Layer 类型定义 (Phase R) + * + * 统一前后端 ConclusionReport 数据结构 + * 前端类型位于 frontend-v2/src/modules/ssa/types/index.ts + */ + +import { z } from 'zod'; + +// ============================================ +// Zod Schema — LLM 输出强校验 +// ============================================ + +export const LLMConclusionSchema = z.object({ + executive_summary: z.string().min(10), + key_findings: z.array(z.string()).min(1), + statistical_summary: z.object({ + total_tests: z.number(), + significant_results: z.number(), + methods_used: z.array(z.string()), + }), + methodology: z.string().min(10), + limitations: z.array(z.string()).min(1), + recommendations: z.array(z.string()).optional().default([]), +}); + +export type LLMConclusionOutput = z.infer; + +// ============================================ +// 统一的 ConclusionReport(前后端对齐) +// ============================================ + +export interface StepSummary { + step_number: number; + tool_name: string; + summary: string; + p_value?: number; + is_significant?: boolean; +} + +export interface ConclusionReport { + workflow_id: string; + title: string; + executive_summary: string; + key_findings: string[]; + statistical_summary: { + total_tests: number; + significant_results: number; + methods_used: string[]; + }; + step_summaries: StepSummary[]; + recommendations: string[]; + limitations: string[]; + generated_at: string; + source: 'llm' | 'rule_based'; +} + +// ============================================ +// 槽位注入:从 StepResult 中提取关键统计量 +// ============================================ + +export interface StepFinding { + step_number: number; + tool_name: string; + tool_code: string; + statistic?: string; + statistic_name?: string; + p_value?: string; + p_value_num?: number; + effect_size?: string; + effect_size_name?: string; + ci_lower?: string; + ci_upper?: string; + method?: string; + is_significant: boolean; + group_stats?: Array<{ + group: string; + n: number; + mean?: number; + sd?: number; + median?: number; + }>; + raw_result?: Record; +} + +// ============================================ +// E 层错误分类映射 +// ============================================ + +export interface ErrorClassification { + code: string; + userHint: string; + isRetryable: boolean; +} + +export const R_ERROR_PATTERNS: Array<{ + patterns: string[]; + code: string; + userHint: string; + isRetryable: boolean; +}> = [ + { + patterns: ['NA', 'missing values', 'incomplete cases', 'na.rm'], + code: 'E_MISSING_DATA', + userHint: '数据中存在缺失值,请检查数据清洗后重试', + isRetryable: false, + }, + { + patterns: ['column not found', 'undefined columns', 'not found', 'object .* not found'], + code: 'E_COLUMN_NOT_FOUND', + userHint: '运算引擎未找到指定变量列,请检查数据源列名是否正确', + isRetryable: false, + }, + { + patterns: ['system is computationally singular', 'collinear', 'singular'], + code: 'E_COLLINEARITY', + userHint: '数据存在严重共线性,建议排除冗余变量后重试', + isRetryable: false, + }, + { + patterns: ['not enough observations', 'sample size', 'too few observations'], + code: 'E_INSUFFICIENT_SAMPLE', + userHint: '样本量不足以执行该统计方法,建议增加样本或选用非参数方法', + isRetryable: false, + }, + { + patterns: ['contrasts can be applied only to factors with 2 or more levels', 'need at least 2'], + code: 'E_FACTOR_LEVELS', + userHint: '分组变量的水平数不足,请检查数据分组', + isRetryable: false, + }, +]; + +export const DEFAULT_ERROR: ErrorClassification = { + code: 'E_UNKNOWN', + userHint: '运算引擎遇到异常,请检查数据结构后重试', + isRetryable: false, +}; + +/** + * 根据 R 引擎错误消息匹配友好提示 + */ +export function classifyRError(errorMessage: string): ErrorClassification { + const lowerMsg = errorMessage.toLowerCase(); + for (const entry of R_ERROR_PATTERNS) { + for (const pattern of entry.patterns) { + if (lowerMsg.includes(pattern.toLowerCase())) { + return { code: entry.code, userHint: entry.userHint, isRetryable: entry.isRetryable }; + } + } + } + return DEFAULT_ERROR; +} diff --git a/docs/00-系统总体设计/00-系统当前状态与开发指南.md b/docs/00-系统总体设计/00-系统当前状态与开发指南.md index f4bf22ef..30729028 100644 --- a/docs/00-系统总体设计/00-系统当前状态与开发指南.md +++ b/docs/00-系统总体设计/00-系统当前状态与开发指南.md @@ -1,11 +1,12 @@ # AIclinicalresearch 系统当前状态与开发指南 -> **文档版本:** v5.6 +> **文档版本:** v5.7 > **创建日期:** 2025-11-28 > **维护者:** 开发团队 -> **最后更新:** 2026-02-20 +> **最后更新:** 2026-02-21 > **🎉 重大里程碑:** -> - **🆕 2026-02-20:SSA Phase 2A 前端集成完成!** 多步骤工作流端到端 + V11 UI联调 + Block-based 架构共识 +> - **🆕 2026-02-21:SSA QPER 智能化主线闭环完成!** Q→P→E→R 四层架构全部开发完成,端到端 40/40 测试通过 +> - **2026-02-20:SSA Phase 2A 前端集成完成!** 多步骤工作流端到端 + V11 UI联调 + Block-based 架构共识 > - **2026-02-19:SSA T 检验端到端测试通过!** 完整流程验证 + 9 个 Bug 修复 + Phase 1 核心完成 85% > - **2026-02-19:SSA Week 1 开发完成!** R Docker 镜像构建成功 + 后端/前端骨架 + 规范对齐 > - **2026-02-18:SSA MVP 开发计划 v1.5 完成!** Brain-Hand架构 + 统计护栏 + HITL + 专家配置体系 @@ -23,14 +24,13 @@ > - **2026-01-24:Protocol Agent 框架完成!** 可复用Agent框架+5阶段对话流程 > - **2026-01-22:OSS 存储集成完成!** 阿里云 OSS 正式接入平台基础层 > -> **🆕 最新进展(SSA Phase 2A 前端集成完成 2026-02-20):** -> - ✅ **🎉 多步骤工作流端到端**:数据上传 → 质量报告 → 多步骤规划 → SSE 实时执行 → 结果展示 → 报告/代码导出 -> - ✅ **V11 UI 前后端联调**:Gemini 风格对话界面 + 多任务支持 + 单页滚动布局 -> - ✅ **Python 数据质量服务集成**:CSV 直传 Python 解析,修复端口/环境变量问题 -> - ✅ **意图识别增强**:正则提取变量名 + 变量类型判断 → 智能选择统计方法 -> - ✅ **描述性统计完整支持**:专用 DescriptiveResultView 组件 + Word 导出 -> - ✅ **6 个前端 Bug 修复**:SAP 误显示、布局混乱、SSE 卡死、结果丢失等 -> - ✅ **Block-based 架构共识**:4 种 Block 类型,R → Node.js → 前端全链路标准化 +> **🆕 最新进展(SSA QPER 智能化主线闭环 2026-02-21):** +> - ✅ **🎉 QPER 四层架构主线闭环** — Phase E+/Q/P/R 全部完成(93.5h),端到端 40/40 测试通过 +> - ✅ **Q 层 LLM 意图理解** — 自然语言→四维信息提取 + Zod 动态防幻觉 + 追问卡片 +> - ✅ **P 层配置化决策表** — JSON 驱动四维匹配 + 流程模板 + PlannedTrace + 热更新 API +> - ✅ **R 层 LLM 论文级结论** — 6 要素结论 + 统计量槽位注入 + Zod 校验 + 敏感性冲突准则 +> - ✅ **四层降级体系** — Q→正则, P→硬编码, E→错误分类, R→规则引擎,LLM 不可用时系统不中断 +> - ✅ **Block-based 标准化** — 7 个 R 工具输出 report_blocks,前端动态渲染 + Word 导出 > > **部署状态:** ✅ 生产环境运行中 | 公网地址:http://8.140.53.236/ > **REDCap 状态:** ✅ 生产环境运行中 | 地址:https://redcap.xunzhengyixue.com/ @@ -71,7 +71,7 @@ | **ASL** | AI智能文献 | 文献筛选、Meta分析、证据图谱 | ⭐⭐⭐⭐⭐ | 🎉 **智能检索MVP完成(60%)** - DeepSearch集成 | **P0** | | **DC** | 数据清洗整理 | ETL + 医学NER(百万行级数据) | ⭐⭐⭐⭐⭐ | ✅ **Tool B完成 + Tool C 99%(异步架构+性能优化-99%+多指标转换+7大功能)** | **P0** | | **IIT** | IIT Manager Agent | AI驱动IIT研究助手 - 双脑架构+REDCap集成 | ⭐⭐⭐⭐⭐ | 🎉 **事件级质控V3.1完成(设计100%,代码60%)** | **P0** | -| **SSA** | 智能统计分析 | Brain-Hand架构 + 统计护栏 + HITL + 10工具MVP | ⭐⭐⭐⭐⭐ | 🎉 **Phase 2A完成(设计100%,开发95%)** - 多步骤工作流+V11 UI+Block-based架构共识 | **P1** | +| **SSA** | 智能统计分析 | **QPER架构** + LLM意图理解 + 配置化决策表 + LLM论文结论 | ⭐⭐⭐⭐⭐ | 🎉 **QPER主线闭环(E+/Q/P/R 100%)** - 40/40端到端通过,Phase Deploy待启动 | **P1** | | **ST** | 统计分析工具 | 100+轻量化统计工具 | ⭐⭐⭐⭐ | 📋 规划中 | P2 | | **RVW** | 稿件审查系统 | 方法学评估 + 🆕数据侦探(L1/L2/L2.5验证)+ Skills架构 + Word导出 | ⭐⭐⭐⭐ | 🚀 **V2.0 Week3完成(85%)** - 统计验证扩展+负号归一化+文件格式提示+用户体验优化 | P1 | | **ADMIN** | 运营管理端 | Prompt管理、租户管理、用户管理、运营监控、系统知识库 | ⭐⭐⭐⭐⭐ | 🎉 **Phase 4.6完成(88%)** - Prompt知识库集成+动态注入 | **P0** | @@ -156,23 +156,31 @@ --- -## 🚀 当前开发状态(2026-02-20) +## 🚀 当前开发状态(2026-02-21) -### 🎉 最新进展:SSA Phase 2A 前端集成完成 + Block-based 架构共识(2026-02-20) +### 🎉 最新进展:SSA QPER 智能化主线闭环完成(2026-02-21) -#### ✅ SSA Phase 2A 前端集成(2026-02-20) +#### ✅ SSA QPER 四层架构全部完成(2026-02-21) -**核心成就**: -- ✅ **多步骤工作流端到端**:数据上传 → 质量报告 → 多步骤规划 → SSE 实时执行 → 结果 → 导出 -- ✅ **V11 UI 前后端联调**:Gemini 风格 + 多任务 + 单页滚动 + Word/R代码导出 -- ✅ **Python 数据质量服务**:CSV 直传 Python 解析,双端点支持 -- ✅ **意图识别增强**:变量名提取 + 类型判断 → 智能选择统计方法 -- ✅ **描述性统计**:专用渲染组件 + Word 导出完整支持 -- ✅ **6 个 Bug 修复**:SAP 误显示、布局混乱、SSE 卡死、结果丢失等 +**重大里程碑:Q→P→E→R 完整链路闭环,端到端 40/40 测试通过!** -**架构决策**: -- ✅ **Block-based 动态渲染协议**:4 种 Block 类型(markdown/table/image/key_value) -- ✅ **开发计划已制定**:`08-Block-based动态结果渲染开发计划.md`(~2.5 天工时) +| Phase | 核心产出 | 状态 | +|-------|---------|------| +| E+ | 7 个 R 工具 Block-based 标准化输出 + DynamicReport + Word 导出 | ✅ | +| Q | QueryService + LLM Intent + Zod 动态防幻觉 + 追问卡片 | ✅ | +| P | ConfigLoader + DecisionTable + FlowTemplate + PlannedTrace + 热更新 | ✅ | +| R | ReflectionService + 槽位注入 + Zod 校验 + 敏感性冲突准则 + 结论缓存 | ✅ | + +**架构亮点**: +- ✅ **四层降级体系**:每层 LLM 失败时自动 fallback,系统不中断 +- ✅ **配置化驱动**:决策表/模板/工具注册表为 JSON,`POST /reload` 热更新 +- ✅ **统计量槽位注入**:LLM 被剥夺生成数值权限,所有数值来自 R 引擎 + +**下一步**:Phase Deploy(工具补齐 + 部署)→ Phase Q+(人机协同增强) + +**相关文档**: +- 开发计划:`docs/03-业务模块/SSA-智能统计分析/04-开发计划/10-QPER架构开发计划-智能化主线.md` +- 开发总结:`docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-QPER架构开发总结-2026-02-21.md` ### 🎉 SSA Week 1 开发完成 + R Docker 构建成功(2026-02-19) diff --git a/docs/03-业务模块/SSA-智能统计分析/00-模块当前状态与开发指南.md b/docs/03-业务模块/SSA-智能统计分析/00-模块当前状态与开发指南.md index 8042c02f..45e29b89 100644 --- a/docs/03-业务模块/SSA-智能统计分析/00-模块当前状态与开发指南.md +++ b/docs/03-业务模块/SSA-智能统计分析/00-模块当前状态与开发指南.md @@ -1,47 +1,19 @@ # SSA智能统计分析模块 - 当前状态与开发指南 -> **文档版本:** v1.7 +> **文档版本:** v2.0 > **创建日期:** 2026-02-18 -> **最后更新:** 2026-02-20 +> **最后更新:** 2026-02-21 > **维护者:** 开发团队 -> **当前状态:** 🎉 **Phase 2A 前端集成完成!多步骤工作流端到端通过 + Block-based 架构共识达成** +> **当前状态:** 🎉 **QPER 智能化主线闭环完成!Q→P→E→R 端到端 40/40 通过** > **文档目的:** 快速了解SSA模块状态,为新AI助手提供上下文 > -> **🎉 里程碑(2026-02-18):** -> - ✅ **PRD 完成**:SSA-Pro 严谨型智能统计分析模块需求定义 -> - ✅ **架构设计 V4 完成**:Brain-Hand 双层架构 + 统计护栏 + HITL 人机协同 -> - ✅ **MVP 开发计划 v1.5 完成**:通过 5 轮评审,纳入完整专家配置体系 -> - ✅ **5 份开发文档完成**:总览、任务清单、R服务指南、后端指南、前端指南 -> -> **🎉 T 检验端到端测试通过(2026-02-19):** -> - ✅ **🎉 完整流程验证**:数据上传 → 计划生成 → 分析执行 → 结果展示 → 代码下载 -> - ✅ **R 服务 Bug 修复**:缺失值自动过滤,解决分组变量 3 组问题 -> - ✅ **类型推断优化**:0/1 数字列正确识别为分类变量 -> - ✅ **错误处理增强**:R 服务错误信息正确传递给前端 -> - ✅ **文件名动态生成**:`{toolName}_{dataName}_{MMDD}_{HHmm}.R` -> - ✅ **前端模块激活**:智能统计分析入口可用 -> - ✅ **用户会话隔离**:不同用户数据正确隔离 -> -> **🎉 V11 UI 前后端联调测试通过(2026-02-20 上午):** -> - ✅ **V11 UI 像素级还原**:Gemini 风格对话界面,全屏沉浸式体验 -> - ✅ **多任务支持**:单会话内可执行多个分析任务,独立管理状态 -> - ✅ **单页滚动布局**:分析计划 → 执行日志 → 分析结果,步骤进度条导航 -> - ✅ **Word 报告导出**:完整统计报告,包含数据描述、方法、结果、图表、结论 -> -> **🆕 🎉 Phase 2A 前端集成完成(2026-02-20 下午):** -> - ✅ **多步骤工作流端到端**:数据上传 → 质量报告 → 多步骤规划 → SSE 实时执行 → 结果展示 → 报告/代码导出 -> - ✅ **Python 数据质量服务集成**:CSV 直传 Python 解析,修复端口/环境变量问题 -> - ✅ **意图识别增强**:正则提取变量名 + 变量类型判断 → 智能选择统计方法 -> - ✅ **描述性统计完整支持**:专用 DescriptiveResultView 组件 + Word 导出 -> - ✅ **6 个前端 Bug 修复**:SAP 误显示、布局混乱、SSE 卡死、结果丢失等 -> - ✅ **Block-based 架构共识达成**:4 种 Block 类型,R 输出标准化 → Node.js 零维护 → 前端动态渲染 -> -> **🆕 v1.5 新增特性(专家配置体系):** -> - 🆕 **统计决策表**:(Goal, Y, X, Design) 四维匹配精准选工具,替代简单 RAG -> - 🆕 **R 代码库**:支持上传 100+ 成熟 R 脚本,统一 `run_analysis()` 入口 -> - 🆕 **参数映射配置**:JSON Key → R 参数名可配置 -> - 🆕 **护栏规则链**:支持 Block / Warn / Switch 三种 Action -> - 🆕 **结果解读模板**:"填空题"式的论文级结论生成 +> **🎉 重大里程碑(2026-02-21):** +> - ✅ **QPER 四层架构主线闭环** — Phase E+ / Q / P / R 全部完成,93.5h 计划工时 +> - ✅ **端到端测试 40/40 通过** — 两条完整链路(差异比较 + 相关分析)全部跑通 +> - ✅ **LLM 智能意图理解** — 自然语言→四维信息提取,Confidence=0.95 +> - ✅ **配置化决策表驱动** — JSON 驱动方法选择,热更新 API,方法学团队可配置 +> - ✅ **LLM 论文级结论生成** — 6 要素结论 + 槽位注入反幻觉 + Zod 强校验 + 敏感性冲突准则 +> - ✅ **四层降级体系** — 每层 LLM 失败时自动 fallback,系统不中断 --- @@ -53,379 +25,186 @@ |------|------| | **模块名称** | SSA - 智能统计分析 (Smart Statistical Analysis) | | **模块定位** | AI驱动的"白盒"统计分析系统 | +| **架构模式** | **QPER — Query → Planner → Execute → Reflection** | | **商业价值** | ⭐⭐⭐⭐⭐ 极高 | -| **独立性** | ⭐⭐⭐⭐ 高(可独立使用,也可与其他模块协同) | | **目标用户** | 临床研究人员、生物统计师 | -| **开发状态** | 🎉 **Phase 2A 完成,多步骤工作流端到端通过(开发 ~95%)** | +| **开发状态** | 🎉 **QPER 主线闭环完成,Phase Deploy 待启动** | ### 核心目标 -> 打造一个 **"白盒"、"严谨"、"可交付"** 的智能统计分析系统。 +> 让**不懂统计的医生**完成**专业级的统计分析**。 > -> **核心差异化**: +> **三大特征**: > 1. **白盒**:用户完全理解 AI 做了什么,为什么这样做 > 2. **严谨**:统计护栏自动检测前提条件,违规时自动降级 -> 3. **可交付**:生成可在本地运行的 R 代码,支持审计复现 - -### 功能规格 - -#### 核心AI能力(规划中) - -1. **智能规划(Planner)** - - 🆕 **决策表匹配**:(Goal, Y, X, Design) 四维精准选工具 - - RAG 工具检索:作为决策表的兜底方案 - - 参数映射:将自然语言映射为统计参数(可配置) - - 统计分析计划(SAP)生成 - -2. **统计护栏(Guardrails)** - - 正态性检验(Shapiro-Wilk) - - 方差齐性检验(Levene) - - 样本量检验 - - 大样本优化(N > 5000 抽样检验) - - 🆕 **护栏 Action**:Block(阻止) / Warn(警告) / Switch(切换方法) - -3. **人机协同(HITL)** - - Plan Card:用户确认/修改分析计划 - - Execution Trace:实时展示执行路径 - - Result Card:结构化结果 + AI 解读 - -4. **代码交付** - - 生成可复现的 R 代码 - - 自动注入依赖安装脚本 - - APA 格式化输出(p_value_fmt) - -5. **🆕 咨询模式** - - 无数据对话:用户只描述研究设计 - - SAP 文档生成:结构化统计分析计划 - - 多格式导出:Word/Markdown - -6. **🆕 配置中台(专家知识库)** - - 🆕 **统计决策表**:Goal + Y + X + Design → Tool 映射 - - 🆕 **R 代码库**:100+ 成熟脚本上传,统一 `run_analysis()` 入口 - - 🆕 **参数映射**:JSON Key → R 参数名 + 校验规则 - - 🆕 **护栏规则链**:Check → Threshold → Action (Block/Warn/Switch) - - 🆕 **结果解读模板**:"填空题"式论文级结论 - - Excel 配置导入 + 热加载 + 配置校验 - -#### MVP 工具清单(10个) - -| 工具代码 | 工具名称 | 适用场景 | -|---------|---------|---------| -| ST_T_TEST_IND | 独立样本T检验 | 两组连续变量比较 | -| ST_T_TEST_PAIRED | 配对样本T检验 | 配对设计 | -| ST_WILCOXON | Wilcoxon秩和检验 | T检验的非参数替代 | -| ST_ANOVA_ONE | 单因素方差分析 | 多组连续变量比较 | -| ST_CHI_SQUARE | 卡方检验 | 分类变量关联 | -| ST_FISHER | Fisher精确检验 | 小样本分类变量 | -| ST_CORRELATION | Pearson/Spearman相关 | 连续变量相关性 | -| ST_REGRESSION_LINEAR | 线性回归 | 预测建模 | -| ST_REGRESSION_LOGISTIC | Logistic回归 | 二分类预测 | -| ST_DESCRIBE | 描述性统计 | 数据概览 | +> 3. **可交付**:生成论文级结论 + 可在本地运行的 R 代码,支持审计复现 --- -## 🏗️ 架构设计 - -### Brain-Hand 双层架构 + 配置中台 +## 🏗️ QPER 四层架构 ``` -┌─────────────────────────────────────────────────────────┐ -│ 用户界面 (Frontend) │ -│ 🆕 ModeSwitch | DataUploader | PlanCard | ResultCard │ -│ ↓ 智能分析 ↓ 咨询模式 │ -└─────────────────────────────────────────────────────────┘ - ↓ -┌─────────────────────────────────────────────────────────┐ -│ Planner (Brain/大脑) - Node.js │ -│ ┌─────────────────────────────────────────────────────┐│ -│ │ DataParserService: 数据解析 → Schema 提取 ││ -│ │ ToolRetrievalService: RAG 工具检索 ││ -│ │ PlannerService: LLM 规划(有数据) ││ -│ │ 🆕 ConsultService: 无数据咨询 ││ -│ │ 🆕 SAPGeneratorService: SAP 文档生成 ││ -│ │ CriticService: 结果解读(流式) ││ -│ └─────────────────────────────────────────────────────┘│ -│ 📌 只看 Schema,支持有数据/无数据两种模式 │ -└─────────────────────────────────────────────────────────┘ - ↓ (仅智能分析模式) -┌─────────────────────────────────────────────────────────┐ -│ Executor (Hand/四肢) - R Docker │ -│ ┌─────────────────────────────────────────────────────┐│ -│ │ data_loader.R: 混合数据协议(inline/OSS) ││ -│ │ guardrails.R: 统计护栏(正态/方差齐性/样本量) ││ -│ │ tools/*.R: 统计工具 Wrapper(glue 模板) ││ -│ │ result_formatter.R: 结果格式化(p_value_fmt) ││ -│ │ error_codes.R: 结构化错误码 ││ -│ └─────────────────────────────────────────────────────┘│ -│ 📌 操作真实数据 + 生成可复现代码 │ -└─────────────────────────────────────────────────────────┘ - ▲ -┌─────────────────────────────────────────────────────────┐ -│ 🆕 配置中台 (Config Center) │ -│ ┌─────────────────────────────────────────────────────┐│ -│ │ ConfigLoaderService: Excel 配置加载 ││ -│ │ ConfigValidatorService: 配置校验 ││ -│ │ ConfigCacheService: 配置缓存 + 热加载 ││ -│ └─────────────────────────────────────────────────────┘│ -│ 📌 统计专家可配置,系统动态加载 │ -└─────────────────────────────────────────────────────────┘ +用户:"比较两组血压有没有差别" + │ + ▼ +┌─ Q · Query ─────────────────────────────────────┐ +│ LLM 意图解析 + Zod 动态防幻觉 + 追问卡片 │ +│ 输出:ParsedQuery { goal, y, x, design } │ +└──────────────────────┬──────────────────────────┘ + ▼ +┌─ P · Planner ────────────────────────────────────┐ +│ 决策表四维匹配 + 流程模板填充 + EPV 防护 │ +│ 输出:WorkflowPlan + PlannedTrace │ +└──────────────────────┬──────────────────────────┘ + ▼ +┌─ E · Execute ────────────────────────────────────┐ +│ R 引擎执行 + SSE 实时进度 + Block-based 输出 │ +│ 输出:StepResult[] + ReportBlock[] │ +└──────────────────────┬──────────────────────────┘ + ▼ +┌─ R · Reflection ─────────────────────────────────┐ +│ LLM 论文级结论 + 槽位注入 + Zod 校验 │ +│ 输出:ConclusionReport(6 要素) │ +└──────────────────────────────────────────────────┘ ``` -### 关键技术决策 +### 降级体系 -| 决策点 | 方案 | 理由 | -|--------|------|------| -| **R 服务部署** | SAE Docker(固定 2 实例) | 避免冷启动延迟 | -| **数据传输** | 混合协议(<2MB inline, 2-20MB OSS) | 平衡性能与内存 | -| **代码生成** | glue 模板 | 可维护性好于 paste0 | -| **LLM 输出** | jsonrepair + Zod | 容错 JSON 解析 | -| **隐私保护** | Schema 脱敏 + 分类变量稀有值隐藏 | 防止 LLM 泄露数据 | -| **🆕 结果渲染** | Block-based 协议(4 种 Block) | R 端标准化输出 → Node.js 零维护 → 前端动态渲染 | +| 层 | 正常路径 | 降级路径 | 触发条件 | +|----|---------|---------|---------| +| Q | QueryService(LLM) | 正则匹配 fallback | LLM 超时/不可用 | +| P | DecisionTable + FlowTemplate | 硬编码 if/else | 决策表无匹配 | +| E | R 引擎 | 错误分类→友好提示 | R 运行时崩溃 | +| R | ReflectionService(LLM) | ConclusionGeneratorService(规则拼接) | LLM 失败/Zod 校验失败 | --- ## 📋 开发进度 -| Phase | 任务 | 状态 | 完成日期 | -|-------|------|------|---------| -| Phase 0 | 需求分析与架构设计 | ✅ 已完成 | 2026-02-18 | -| Phase 0 | MVP 开发计划 v1.0 → v1.6 | ✅ 已完成 | 2026-02-19 | -| Phase 1 | 骨架搭建 + 配置中台 | ✅ **核心完成 95%** | 2026-02-19 | -| Phase 2A | 智能核心(多步骤工作流 + 意图识别) | ✅ **前端集成完成** | 2026-02-20 | -| Phase 2B | Block-based 动态渲染重构 | 📋 计划已制定 | - | -| Phase 2 | 智能规划 + 咨询模式 | 📋 待开始 | - | -| Phase 3 | 完善与联调 | 📋 待开始 | - | +| Phase | 任务 | 工时 | 状态 | 完成日期 | +|-------|------|------|------|---------| +| Phase 0 | 需求分析与架构设计 | - | ✅ 已完成 | 2026-02-18 | +| Phase 1 | 骨架搭建(T 检验端到端) | - | ✅ 已完成 | 2026-02-19 | +| Phase 1.5 | V11 UI 前后端联调 | - | ✅ 已完成 | 2026-02-20 | +| Phase 2A | 多步骤工作流 + 前端集成 | - | ✅ 已完成 | 2026-02-20 | +| **Phase E+** | **Block-based 标准化** | **15.5h** | ✅ **已完成** | 2026-02-20 | +| **Phase Q** | **LLM 意图理解** | **33h** | ✅ **已完成** | 2026-02-21 | +| **Phase P** | **决策表 + 流程模板** | **23h** | ✅ **已完成** | 2026-02-21 | +| **Phase R** | **LLM 论文级结论** | **22h** | ✅ **已完成** | 2026-02-21 | +| Phase Deploy | 工具补齐 + 部署上线 | 37h | 📋 待开始 | - | +| Phase Q+ | 人机协同增强 | 20h | 📋 待开始 | - | -### 🎉 已完成核心功能 +### 已完成核心功能 | 组件 | 完成项 | 状态 | |------|--------|------| -| **R 服务** | T 检验、描述性统计、卡方检验、Logistic 回归、相关分析、错误码、护栏 | ✅ 100% | -| **后端** | 路由、RClientService、DataProfileService、WorkflowPlannerService、WorkflowExecutorService、SSE | ✅ 95% | -| **前端** | V11 UI、多步骤工作流、DescriptiveResultView、R 代码/Word 导出、SSE 实时进度 | ✅ 95% | -| **Python 服务** | 数据质量分析(JSON + CSV 双端点) | ✅ 100% | -| **配置中台** | 数据库表、热加载 API | 🔄 18% | - -### 开发计划文档 - -| 文档 | 路径 | 说明 | -|------|------|------| -| **MVP总览** | `04-开发计划/00-MVP开发计划总览.md` | 范围、架构、里程碑 | -| **任务清单** | `04-开发计划/01-任务清单与进度追踪.md` | 可追踪的 TODO 列表 | -| **R服务指南** | `04-开发计划/02-R服务开发指南.md` | R 统计工程师专用 | -| **后端指南** | `04-开发计划/03-后端开发指南.md` | Node.js 工程师专用 | -| **前端指南** | `04-开发计划/04-前端开发指南.md` | 前端工程师专用 | -| **🆕 Block-based 渲染** | `04-开发计划/08-Block-based动态结果渲染开发计划.md` | 动态结果渲染架构重构 | - -### 评审记录 - -| 版本 | 评审报告 | 结论 | 日期 | -|------|---------|------|------| -| v1.0 | `06-开发记录/SSA-Pro 方案深度审查与风险评估报告.md` | 需修订 | 2026-02-18 | -| v1.1 | `06-开发记录/SSA-Pro 方案深度审查与风险评估报告 V2.0.md` | 需修订 | 2026-02-18 | -| v1.2 | `06-开发记录/SSA-Pro V1.2 终极审查与发令报告V3.0.md` | 🟢 通过 | 2026-02-18 | -| v1.4 | 🆕 纳入双引擎 + 咨询模式 + 配置中台 | 🟢 通过 | 2026-02-18 | +| **R 服务** | 7 个 R 工具(T 检验、描述统计、卡方、Logistic、相关分析等)+ Block-based 输出 | ✅ | +| **Q 层** | QueryService + LLM Intent + Zod 动态防幻觉 + 追问卡片 + DataProfile 增强 | ✅ | +| **P 层** | ConfigLoader + DecisionTable + FlowTemplate + PlannedTrace + 热更新 API | ✅ | +| **E 层** | WorkflowExecutor + RClient + SSE 实时进度 + 错误分类映射 | ✅ | +| **R 层** | ReflectionService + 槽位注入 + Zod 校验 + 敏感性冲突准则 + 结论缓存 + Word 增强 | ✅ | +| **前端** | V11 UI + DynamicReport + ClarificationCard + ConclusionReport(渐入动画)+ Word/R 代码导出 | ✅ | +| **Python** | DataProfileService(is_id_like 标记)+ CSV 解析 | ✅ | +| **测试** | QPER 端到端测试 40/40 通过 | ✅ | --- -## 🔧 技术依赖 +## 📂 代码目录结构 -### 复用的平台能力 +``` +backend/src/modules/ssa/ +├── services/ +│ ├── QueryService.ts # Q 层:LLM 意图解析 +│ ├── DecisionTableService.ts # P 层:四维匹配 +│ ├── FlowTemplateService.ts # P 层:流程模板 +│ ├── WorkflowPlannerService.ts # P 层:核心规划入口 +│ ├── WorkflowExecutorService.ts # E 层:步骤编排 + SSE +│ ├── RClientService.ts # E 层:R 引擎调用 +│ ├── ReflectionService.ts # R 层:LLM 结论生成 +│ ├── ConclusionGeneratorService.ts # R 层 fallback +│ ├── DataProfileService.ts # 共享:Python 数据质量 +│ └── DataParserService.ts # 共享:文件解析 +├── config/ +│ ├── ConfigLoader.ts # 通用 JSON 加载 + Zod 校验 +│ ├── tools_registry.json # R 工具注册表 +│ ├── decision_tables.json # 四维匹配规则 +│ └── flow_templates.json # 流程模板 +├── types/ +│ ├── query.types.ts # Q 层接口 +│ └── reflection.types.ts # R 层接口 +├── routes/ +│ ├── workflow.routes.ts # 工作流 API(含结论缓存) +│ └── config.routes.ts # 热更新 API +└── ... -| 能力 | 位置 | 用途 | -|------|------|------| -| **LLM网关** | `@/common/llm/LLMFactory` | Planner + Critic | -| **RAG引擎** | `@/common/rag` | 工具检索 | -| **存储** | `@/common/storage` | OSS 数据传输 | -| **日志** | `@/common/logging` | 结构化日志 | -| **流式响应** | `@/common/streaming` | Critic 流式输出 | +backend/scripts/ +├── seed-ssa-intent-prompt.ts # Q 层 Prompt 种子 +├── seed-ssa-reflection-prompt.ts # R 层 Prompt 种子 +├── test-ssa-qper-e2e.ts # QPER 端到端测试 +└── ... +``` -### LLM模型 +--- -| 模型 | 用途 | 说明 | -|------|------|------| -| DeepSeek-V3 | Planner + Query Rewriter | 性价比高 | -| Qwen3-rerank | 工具重排序 | 中文理解好 | -| GPT-5-Pro | Critic 结果解读 | 深度推理 | +## 🔧 开发环境 -### R 依赖 +### 启动服务 -| 包 | 版本 | 用途 | -|-----|------|------| -| plumber | 1.2.1 | Web API 框架 | -| jsonlite | 1.8.8 | JSON 处理 | -| ggplot2 | 3.4.4 | 可视化 | -| glue | 1.7.0 | 模板代码生成 | -| car | 3.1-2 | Levene 检验 | -| httr | 1.4.7 | OSS 下载 | +```bash +# 1. 数据库(Docker) +docker start ai-clinical-postgres + +# 2. Python 服务 +cd extraction_service && python main.py + +# 3. R 服务 +cd r-statistics-service && Rscript plumber_api.R + +# 4. Node.js 后端 +cd backend && npm run dev + +# 5. 前端 +cd frontend-v2 && npm run dev +``` + +### 运行测试 + +```bash +cd backend +npx tsx scripts/test-ssa-qper-e2e.ts +``` + +### Prompt 种子(需数据库运行) + +```bash +cd backend +npx tsx scripts/seed-ssa-intent-prompt.ts +npx tsx scripts/seed-ssa-reflection-prompt.ts +``` --- ## 📚 相关文档 -### 需求文档 - -- [PRD SSA-Pro 严谨型智能统计分析模块](./00-系统设计/PRD%20SSA-Pro%20严谨型智能统计分析模块.md) - -### 设计文档 - -- [SSA-Pro 严谨型智能统计分析架构设计方案V4](./00-系统设计/SSA-Pro%20严谨型智能统计分析架构设计方案V4.md) ⬅️ **核心架构文档** -- [SSA-Pro (V4.1) 统计技能中心架构规范](./00-系统设计/SSA-Pro%20(V4.1)%20统计技能中心架构规范.md) - -### 原型文件 - -- [智能统计分析V2.html](./03-UI设计/智能统计分析V2.html) - 可直接浏览器打开 - -### 参考文档 - -- [云原生开发规范](../../04-开发规范/08-云原生开发规范.md) -- [系统架构分层设计](../../00-系统总体设计/01-系统架构分层设计.md) +| 文档 | 路径 | +|------|------| +| **QPER 开发计划(主线)** | `04-开发计划/10-QPER架构开发计划-智能化主线.md` | +| **QPER 开发总结** | `06-开发记录/SSA-QPER架构开发总结-2026-02-21.md` | +| **智能化愿景设计** | `00-系统设计/SSA-Pro 理想状态与智能化愿景设计.md` | +| **PRD** | `00-系统设计/PRD SSA-Pro 严谨型智能统计分析模块.md` | +| **架构设计 V4** | `00-系统设计/SSA-Pro 严谨型智能统计分析架构设计方案V4.md` | --- -## 🎯 快速开始 +## 🎯 下一步 -### 目录结构 - -``` -docs/03-业务模块/SSA-智能统计分析/ -├── 00-系统设计/ # PRD + 架构设计 -├── 01-需求分析/ # 用户故事、用例 -├── 02-技术设计/ # 详细技术方案 -├── 03-UI设计/ # 原型图 -├── 04-开发计划/ # MVP 开发文档(5份) -├── 05-测试用例/ # 测试方案 -└── 06-开发记录/ # 评审报告、开发日志 -``` - -### 开发环境准备 - -1. **R 服务环境** - ```bash - cd r-statistics-service - docker build -t ssa-r-service . - docker run -p 8080:8080 -e DEV_MODE=true ssa-r-service - ``` - -2. **后端开发** - ```bash - cd backend - npm run dev - ``` - -3. **前端开发** - ```bash - cd frontend-v2 - npm run dev - ``` - -### API 接口预览 - -#### 智能分析模式 - -```http -### 创建会话 -POST http://localhost:3001/api/v1/ssa/sessions -Content-Type: multipart/form-data -# file: 数据文件 - -### 生成分析计划 -POST http://localhost:3001/api/v1/ssa/sessions/{sessionId}/plan -Content-Type: application/json -{"query": "比较两组GLU是否有显著差异"} - -### 执行分析 -POST http://localhost:3001/api/v1/ssa/sessions/{sessionId}/execute -Content-Type: application/json -{"plan": {...}, "debug": false} - -### 获取结果 -GET http://localhost:3001/api/v1/ssa/sessions/{sessionId}/result -``` - -#### 🆕 咨询模式 - -```http -### 创建咨询会话(无数据) -POST http://localhost:3001/api/v1/ssa/consult - -### 咨询对话 -POST http://localhost:3001/api/v1/ssa/consult/{sessionId}/chat -Content-Type: application/json -{"message": "我有一个双臂 RCT 研究,想比较主要终点..."} - -### 生成 SAP 文档 -POST http://localhost:3001/api/v1/ssa/consult/{sessionId}/generate-sap - -### 下载 SAP -GET http://localhost:3001/api/v1/ssa/consult/{sessionId}/download-sap?format=word -``` - -#### 🆕 配置中台 - -```http -### 导入 Excel 配置 -POST http://localhost:3001/api/v1/ssa/config/import -Content-Type: multipart/form-data -# file: config.xlsx - -### 热加载配置 -POST http://localhost:3001/api/v1/ssa/config/reload - -### 获取工具列表 -GET http://localhost:3001/api/v1/ssa/config/tools -``` +1. **Phase Deploy(37h)** — 补齐 ANOVA / Fisher / Wilcoxon / 线性回归 + 复合工具 ST_BASELINE_TABLE + 部署上线 +2. **Phase Q+(20h)** — 变量数据字典(AI 先猜用户微调)+ 变量选择确认面板(AI 推荐医生确认) +3. **前端集成测试** — 用户手动测试 QPER 全链路的真实交互体验 --- -## ⚠️ 注意事项 - -### 对新AI助手 - -1. ✅ **设计文档已完成**:开发前请先阅读架构设计V4 -2. ✅ **开发计划v1.4已审批**:遵循5份开发文档进行开发 -3. ⚠️ **Planner/Executor 分离**:代码目录按 planner/executor 组织 -4. ⚠️ **Brain-Hand 隔离**:Node.js 只看 Schema,R 操作真实数据 -5. ⚠️ **支持无数据模式**:咨询模式下 Planner 可独立工作 -6. ⚠️ **配置外置**:工具定义从 Excel 加载,不硬编码 -7. ⚠️ **混合数据协议**:< 2MB inline,2-20MB OSS -8. ⚠️ **代码模板同步**:修改 Wrapper 逻辑时必须同步更新 templates/ - -### 风险与应对 - -| 风险 | 概率 | 应对策略 | -|------|------|---------| -| R 工具封装进度慢 | 高 | 先做 5 个核心工具,glue 模板化开发 | -| LLM 输出 JSON 格式错误 | 中 | jsonrepair + Zod 强校验 | -| R 服务并发阻塞 | 中 | SAE 固定 2 实例 | -| Node.js xlsx 内存刺客 | 中 | SAE 内存上限 2GB+ | -| R 服务 Segfault 崩溃 | 低 | Liveness Probe + 502/504 友好提示 | -| 本地开发 OSS 不通 | 中 | DEV_MODE 读取本地 fixtures | -| 用户代码缺依赖 | 高 | 模板头部自动安装脚本 | - ---- - -## 🚀 未来规划 - -### MVP 阶段(当前) - -- [x] Phase 1:骨架搭建(T检验端到端跑通)✅ 2026-02-19 -- [x] Phase 1.5:V11 UI 前后端联调 ✅ 2026-02-20 -- [x] Phase 2A:智能核心(多步骤工作流 + 前端集成)✅ 2026-02-20 -- [ ] Phase 2B:Block-based 动态渲染重构(~2.5 天) -- [ ] Phase 2:智能与交互(RAG + HITL + 10工具) -- [ ] Phase 3:打磨与调试(性能优化 + Bug修复) - -### V2.0 阶段(规划中) - -- [ ] 更多统计方法(Meta分析、生存分析、倾向性评分) -- [ ] 自定义工具上传 -- [ ] 批量分析 -- [ ] 报告导出(Word/PDF) - ---- - -**文档版本:** v1.7 -**最后更新:** 2026-02-20 -**当前状态:** 🎉 Phase 2A 前端集成完成,多步骤工作流端到端通过 -**下一步:** Phase 2B Block-based 动态渲染重构(4 种 Block 类型 → R/Node.js/前端全链路改造) +**文档版本:** v2.0 +**最后更新:** 2026-02-21 +**当前状态:** 🎉 QPER 主线闭环完成,端到端 40/40 通过 +**下一步:** Phase Deploy 工具补齐 + 部署上线 diff --git a/docs/03-业务模块/SSA-智能统计分析/04-开发计划/09-智能化差距分析与演进路线图.md b/docs/03-业务模块/SSA-智能统计分析/04-开发计划/09-智能化差距分析与演进路线图.md new file mode 100644 index 00000000..2cea29d9 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/04-开发计划/09-智能化差距分析与演进路线图.md @@ -0,0 +1,400 @@ +# SSA-Pro 智能化差距分析与演进路线图 + +> **文档版本:** v1.0 +> **创建日期:** 2026-02-20 +> **基准文档:** `00-系统设计/SSA-Pro 理想状态与智能化愿景设计.md` +> **当前状态:** Phase 2A 前端集成完成,多步骤工作流端到端通过 +> **文档目的:** 明确当前系统与理想智能化之间的差距,规划演进路径 + +--- + +## 1. 执行摘要 + +### 核心结论 + +**当前系统已搭好"四肢"(R 执行 + SSE 实时进度 + 前端展示 + Word/R 导出),但缺少"大脑"(LLM 意图理解 + 决策表规划 + 论文级结论生成)。** + +| 维度 | 当前状态 | 理想状态 | 差距 | +|------|---------|---------|------| +| 用户输入 | 用户需了解变量名和分析意图 | 用自然语言描述临床问题即可 | 🔴 大 | +| 方法选择 | 硬编码 if/else 规则 | 决策表四维匹配 + LLM 兜底 | 🔴 大 | +| 流程规划 | 从零拼装步骤列表 | 预定义流程模板 + 数据驱动调整 | 🔴 大 | +| 执行引擎 | 顺序执行,各步独立 | 结果串联 + 护栏降级 + 可中断 | 🟡 中 | +| 结果输出 | 数值表格 + 基础图表 | 论文级综合结论 + 方法学说明 | 🔴 大 | + +### 智能化等级评估 + +``` +L1 单方法执行 ████████████████████ 100% ✅ 已完成(2026-02-19) +L2 智能选方法 ███████░░░░░░░░░░░░░ 35% 🟡 有规则匹配,缺决策表和 LLM +L3 数据自适应 ████░░░░░░░░░░░░░░░░ 20% 🔴 护栏在 R 内部,未与规划联动 +L4 流程编排 ████████░░░░░░░░░░░░ 40% 🟡 多步骤能跑,缺模板和串联 +L5 论文级输出 ███░░░░░░░░░░░░░░░░░ 15% 🔴 缺 LLM 结论生成 +``` + +--- + +## 2. 五大核心组件差距详析 + +### 2.1 意图理解器 (Intent Parser) — 完成度 20% + +**理想状态:** +- LLM 从用户自然语言中提取四维信息:Goal / Y_Type / X_Type / Design +- 不确定时主动追问澄清(展示选择卡片) +- 支持模糊输入:"有没有效"、"帮我分析一下" + +**当前实现:** +- `WorkflowPlannerService.parseUserIntent()` — 正则匹配关键词 + - "相关" → correlation,"影响" → regression,"比较" → comparison +- 变量名通过正则从用户文本中提取,与 DataProfile 变量列表交叉匹配 +- 变量类型通过 DataProfile 判断(categorical / numeric) + +**差距明细:** + +| 能力 | 理想 | 当前 | 状态 | +|------|------|------|------| +| Goal 分类 | Difference / Association / Prediction / Description | 关键词硬编码 | 🔴 | +| Y/X 类型提取 | LLM 自动识别结局变量和自变量 | 正则提取变量名 | 🟡 | +| Design 识别 | 独立 / 配对 / 重复测量 | 不支持 | 🔴 | +| 模糊理解 | "有没有效" → 差异比较 | 需要用户说"比较" | 🔴 | +| 追问澄清 | 展示选择卡片让用户确认 | 不追问,直接猜测 | 🔴 | +| 多目标识别 | 识别多个分析目标并分拆 | 不支持 | 🔴 | + +**关键缺失文件/服务:** +- `IntentParserService`(LLM 驱动的意图解析,替代当前的 `parseUserIntent` 方法) +- 前端追问卡片组件(当 AI 不确定时展示选项) + +--- + +### 2.2 数据诊断器 (Data Diagnostician) — 完成度 30% + +**理想状态:** +- 独立的"数据体检报告"模块 +- 检测项:正态性、方差齐性、缺失比例、异常值、样本量、分组平衡性 +- 诊断结果直接传递给路径规划器,影响方法选择 + +**当前实现:** +- Python `DataProfileService`:变量类型推断、缺失率、基本统计量、唯一值 +- R `guardrails.R`:在方法执行时检查正态性(Shapiro-Wilk)和方差齐性(Levene) +- 前端 `DataProfileCard` + `DataProfileModal`:展示数据概况 + +**差距明细:** + +| 检测项 | 理想 | 当前 | 状态 | +|--------|------|------|------| +| 变量类型推断 | 自动识别 | Python DataProfile ✅ | ✅ | +| 缺失值统计 | 占比 + 建议处理策略 | 有占比,无处理建议 | 🟡 | +| 正态性检验 | 独立模块,结果传递给 Planner | R 内部执行,不回传 | 🔴 | +| 方差齐性 | 独立模块,结果传递给 Planner | R 内部执行,不回传 | 🔴 | +| 异常值检测 | IQR 方法 + 可视化标注 | 不检测 | 🔴 | +| 样本量评估 | 功效分析建议 | 不评估 | 🔴 | +| 分组平衡性 | 各组比例 + 提示不平衡 | 不检测 | 🔴 | +| 诊断→规划联动 | 自动影响方法选择 | 完全割裂 | 🔴 | + +**关键问题:** +- 数据诊断(Python DataProfile)和统计前提检查(R guardrails)是两个独立系统 +- R 护栏在执行时发现正态性不满足时自动降级(如 T→Wilcoxon),但不通知 Planner +- Planner 做规划时不知道数据特征,全凭变量类型硬编码选方法 + +--- + +### 2.3 路径规划器 (Pathway Planner) — 完成度 15% + +**理想状态:** +- 决策表匹配:(Goal, Y_Type, X_Type, Design) → 精准选工具 +- 流程模板:预定义标准分析流程(如"两组差异比较 = 清洗 → 描述 → 正态检验 → [T/U] → 效应量 → 可视化") +- 数据驱动调整:根据诊断结果动态修改流程 +- 生成完整的 SAP(统计分析计划)供用户确认 + +**当前实现:** +- `WorkflowPlannerService.generateSteps()` — 硬编码 if/else 链 + - 如果 `analysisType === 'comparison'` 且 Y 是连续 → 加 T 检验步骤 + - 总是先加描述性统计步骤 +- 输出 `WorkflowPlan { title, steps[] }` 结构 + +**差距明细:** + +| 能力 | 理想 | 当前 | 状态 | +|------|------|------|------| +| 决策表匹配 | 四维精准匹配 | 硬编码 if/else | 🔴 | +| 流程模板 | 5-8 个预定义模板 | 无模板概念 | 🔴 | +| 数据驱动调整 | 诊断结果影响规划 | 不参考诊断结果 | 🔴 | +| SAP 生成 | 结构化计划文档 | 简单步骤列表 | 🔴 | +| 敏感性分析 | 自动添加补充分析 | 不支持 | 🔴 | +| 效应量步骤 | 自动包含 | 不支持 | 🔴 | +| 可视化步骤 | 自动规划图表 | 不支持(R 内部生成) | 🟡 | + +**关键缺失文件/服务:** +- `DecisionTableService` — 决策表加载和四维匹配 +- `FlowTemplateService` — 流程模板管理 +- 决策表 Excel 配置文件 +- 流程模板配置文件 + +--- + +### 2.4 流程执行器 (Workflow Executor) — 完成度 40% + +**理想状态:** +- 按 SAP 定义的顺序执行多个方法 +- 上一步输出作为下一步输入(结果串联) +- 护栏检查失败时自动降级方法 +- 每步实时反馈中间结果 +- 用户可暂停/跳过/取消 + +**当前实现:** +- `WorkflowExecutorService.executeWorkflow()` — 顺序遍历 steps 数组 +- 每步独立调用 R 服务(通过 `RClientService`) +- SSE 实时推送 `step_start` / `step_complete` / `workflow_complete` +- 前端实时展示执行日志和结果(terminal-box + 结果卡片) + +**差距明细:** + +| 能力 | 理想 | 当前 | 状态 | +|------|------|------|------| +| 顺序执行 | ✅ | ✅ | ✅ | +| SSE 实时反馈 | ✅ | ✅ | ✅ | +| 步骤级结果展示 | ✅ | ✅ | ✅ | +| 结果串联 | 上步输出 → 下步输入 | 各步独立执行 | 🔴 | +| 护栏降级 | 失败时自动切换方法 | 不支持 | 🔴 | +| 暂停/跳过/取消 | 用户可控 | 一旦开始无法中断 | 🔴 | +| 错误恢复 | 某步失败提供重试选项 | 失败则标记 failed | 🟡 | +| 步骤间数据共享 | 共享清洗后数据集 | 每步重新加载原始数据 | 🔴 | + +**这是完成度最高的组件**,架构基础已就位,需要补充的是上层能力。 + +--- + +### 2.5 结论生成器 (Conclusion Generator) — 完成度 15% + +**理想状态:** +- 论文级综合结论,包含 6 个要素: + 1. 样本描述(纳入/排除) + 2. 主要结果(核心统计量 + P 值) + 3. 效应解读(效应量含义) + 4. 敏感性分析(结果稳健性) + 5. 方法学说明(为什么选这个方法) + 6. 局限性声明(数据处理说明) +- LLM 生成自然语言结论 + +**当前实现:** +- `ConclusionGeneratorService` 存在但功能有限 +- 前端展示:统计量数值、P 值、效应量(数字形式) +- Word 导出:executive_summary + 各步骤统计量表格 + +**差距明细:** + +| 结论要素 | 理想 | 当前 | 状态 | +|----------|------|------|------| +| 样本描述 | "共纳入 186 例,剔除 14 例..." | 无 | 🔴 | +| 主要结果 | LLM 自然语言描述 | 数字展示(P=0.015) | 🔴 | +| 效应解读 | "中等程度效应" | 数字展示(r=0.52) | 🔴 | +| 敏感性分析 | "T 检验得到一致结论" | 不支持 | 🔴 | +| 方法学说明 | "因不满足正态性..." | 无 | 🔴 | +| 局限性声明 | 自动生成 | 无 | 🔴 | +| 多步骤整合 | 综合所有步骤生成报告 | 各步骤独立展示 | 🟡 | +| 可发表质量 | 直接用于论文 | 需大量人工整理 | 🔴 | + +**关键缺失:** +- LLM 结论生成 Prompt(调用 GPT/DeepSeek 生成论文级文字) +- 结果解读模板("填空题"式结论模板) +- 多步骤结果整合逻辑 + +--- + +## 3. 已完成的基础设施(不需要重做) + +在规划演进路径之前,确认以下基础已就位,无需重复建设: + +| 基础设施 | 状态 | 说明 | +|----------|------|------| +| R 统计引擎 | ✅ | Docker 部署,7 个工具,plumber API | +| Node.js 后端框架 | ✅ | 路由、Service 架构、SSE | +| 前端 V11 UI | ✅ | 三栏布局、对话式交互、结果展示 | +| SSE 实时通信 | ✅ | 前后端消息格式已对齐 | +| 数据上传 + OSS | ✅ | 文件上传、预签名 URL | +| Python 数据质量 | ✅ | 变量类型推断、基础统计 | +| R 代码导出 | ✅ | 多步骤聚合导出 | +| Word 报告导出 | ✅ | docx 库,多步骤报告 | +| R 护栏系统 | ✅ | 正态性、方差齐性检查(R 内部) | +| Block-based 协议 | 📋 | 规范已制定,待实施(2.5 天) | + +--- + +## 4. 演进路线图 + +### 4.1 推荐路径(按投入产出比排序) + +``` +当前状态(Phase 2A 完成,L1-L2 之间) + │ + │ ① LLM 意图理解 + 决策表匹配 (~5天) + │ - IntentParserService: LLM 提取 Goal/Y/X/Design + │ - DecisionTableService: 四维 → 工具精准匹配 + │ - 前端追问卡片组件 + ▼ + L2 完成: 智能选方法 + │ 「用户不再需要知道 T 检验、卡方检验是什么」 + │ + │ ② 流程模板 + 结果串联 (~3天) + │ - FlowTemplateService: 5-8 个预定义流程 + │ - 步骤间数据共享机制 + │ - 敏感性分析自动添加 + ▼ + L4 完成: 流程编排 + │ 「从单方法跃迁到完整统计分析流程」 + │ + │ ③ LLM 结论生成 (~3天) + │ - ConclusionService: LLM 综合解读 + │ - 结论模板体系 + │ - 方法学说明自动生成 + ▼ + L5 完成: 论文级输出 + │ 「结果可直接复制到论文中」 + │ + │ ④ 数据诊断→规划联动 + 追问机制 (~5天) + │ - DataDiagnosticService: 异常值/样本量/平衡性 + │ - 诊断结果传递给 Planner + │ - 护栏结果回传机制 + ▼ + L3 完成: 数据自适应 + │ 「系统自动根据数据特征调整方法」 + │ + │ ⑤ 高级交互 (~3天) + │ - 执行暂停/跳过/取消 + │ - 步骤失败自动降级 + │ - 用户修改 SAP 后重新执行 + ▼ + 理想状态: 完全智能化统计分析系统 +``` + +### 4.2 各阶段详细任务 + +#### 阶段 ①:LLM 意图理解 + 决策表匹配(~5 天) + +| 任务 | 层级 | 预估 | 说明 | +|------|------|------|------| +| 设计 Intent Prompt | 后端 | 4h | 提取 Goal/Y/X/Design + 置信度 | +| 实现 IntentParserService | 后端 | 6h | LLM 调用 + 结构化输出 + Zod 校验 | +| 设计决策表 Excel 模板 | 配置 | 3h | 四维匹配规则 + 10 个工具映射 | +| 实现 DecisionTableService | 后端 | 6h | Excel 加载 + 四维匹配逻辑 | +| 重构 WorkflowPlannerService | 后端 | 4h | 集成 Intent + DecisionTable | +| 前端追问卡片组件 | 前端 | 4h | 当置信度低时展示选项 | +| 联调测试 | 全栈 | 4h | 多场景测试 | + +#### 阶段 ②:流程模板 + 结果串联(~3 天) + +| 任务 | 层级 | 预估 | 说明 | +|------|------|------|------| +| 定义 5 个流程模板 | 配置 | 3h | 差异/关联/描述/回归/基线表 | +| 实现 FlowTemplateService | 后端 | 4h | 模板加载 + 参数填充 | +| 步骤间数据共享 | 后端+R | 6h | 清洗后数据缓存 + 传递 | +| 自动添加敏感性分析 | 后端 | 3h | 主要分析 + 替代方法 | +| 自动添加效应量步骤 | 后端 | 2h | Cohen's d / r | +| 联调测试 | 全栈 | 4h | 完整流程测试 | + +#### 阶段 ③:LLM 结论生成(~3 天) + +| 任务 | 层级 | 预估 | 说明 | +|------|------|------|------| +| 设计结论生成 Prompt | 后端 | 4h | 6 要素结论模板 | +| 实现 ConclusionService | 后端 | 6h | LLM 调用 + 流式输出 | +| 设计解读模板配置 | 配置 | 3h | 每种工具的"填空题"模板 | +| 前端论文结论展示 | 前端 | 4h | Markdown 渲染 + 复制 | +| Word 报告增强 | 前端 | 3h | 纳入论文级结论 | +| 联调测试 | 全栈 | 3h | | + +#### 阶段 ④:数据诊断→规划联动(~5 天) + +| 任务 | 层级 | 预估 | 说明 | +|------|------|------|------| +| 实现异常值检测 | Python/R | 4h | IQR 方法 + 可视化标注 | +| 实现样本量评估 | Python/R | 3h | 功效分析建议 | +| 实现分组平衡性检测 | Python | 2h | 各组比例 + 提示 | +| 诊断结果传递给 Planner | 后端 | 4h | DataDiagnosis → PlannerInput | +| R 护栏结果回传机制 | 后端+R | 6h | 护栏结果影响后续步骤 | +| 前端诊断报告增强 | 前端 | 4h | 问题列表 + 建议 + 评分 | +| 前端追问机制 | 前端 | 4h | 不确定时展示选择卡片 | +| 联调测试 | 全栈 | 4h | | + +#### 阶段 ⑤:高级交互(~3 天) + +| 任务 | 层级 | 预估 | 说明 | +|------|------|------|------| +| 执行暂停/跳过/取消 | 后端+前端 | 6h | SSE 控制消息 | +| 步骤失败自动降级 | 后端 | 4h | T 检验失败 → Wilcoxon | +| SAP 修改后重新执行 | 前端+后端 | 6h | 用户编辑步骤 → 重新规划 | +| 联调测试 | 全栈 | 4h | | + +--- + +## 5. 工时与里程碑 + +| 阶段 | 核心目标 | 工时 | 智能等级 | +|------|---------|------|---------| +| Block-based 重构 | 结果渲染标准化 | 2.5天 | 基础设施 | +| ① LLM 意图 + 决策表 | 用户不需要知道方法名 | 5天 | → L2 | +| ② 流程模板 + 串联 | 从单方法到完整流程 | 3天 | → L4 | +| ③ LLM 结论生成 | 论文级输出 | 3天 | → L5 | +| ④ 数据诊断联动 | 全自动方法调整 | 5天 | → L3 | +| ⑤ 高级交互 | 可中断 + 降级 | 3天 | 完善 | +| **合计** | **理想状态** | **~21.5天** | **L5** | + +### 里程碑时间线(预估) + +``` +2026-02-21 ─── Block-based 重构开始 +2026-02-24 ─── Block-based 完成 ✓ 基础设施就绪 +2026-02-25 ─── 阶段① 开始:LLM 意图 + 决策表 +2026-03-01 ─── 阶段① 完成 ✓ L2 智能选方法 +2026-03-02 ─── 阶段② 开始:流程模板 + 串联 +2026-03-05 ─── 阶段② 完成 ✓ L4 流程编排 +2026-03-06 ─── 阶段③ 开始:LLM 结论生成 +2026-03-10 ─── 阶段③ 完成 ✓ L5 论文级输出 +2026-03-11 ─── 阶段④ 开始:数据诊断联动 +2026-03-17 ─── 阶段④ 完成 ✓ L3 数据自适应 +2026-03-18 ─── 阶段⑤ 开始:高级交互 +2026-03-21 ─── 阶段⑤ 完成 ✓ 理想状态 +``` + +--- + +## 6. 成功标准 + +### 用户体验验证 + +| 测试场景 | 当前表现 | 目标表现 | +|----------|---------|---------| +| 用户说"有没有效" | ❌ 无法理解 | ✅ AI 识别为差异比较 | +| 用户不知道该用什么方法 | ❌ 必须指定 | ✅ AI 自动选择 | +| 数据不满足正态性 | 🟡 R 内部降级,用户不知 | ✅ 规划时就选非参数方法 | +| 分析结果 | 🟡 P 值 + 数字 | ✅ "两组差异显著(P<0.001),中等效应" | +| 导出报告 | 🟡 需大量整理 | ✅ 可直接用于论文 | + +### 智能化评分卡 + +| 能力 | 权重 | 当前评分 | 目标评分 | +|------|------|---------|---------| +| 意图理解 | 25% | 2/10 | 8/10 | +| 方法选择 | 20% | 3/10 | 9/10 | +| 流程规划 | 20% | 2/10 | 8/10 | +| 执行引擎 | 15% | 6/10 | 9/10 | +| 结论生成 | 20% | 1/10 | 8/10 | +| **加权总分** | **100%** | **2.7/10** | **8.4/10** | + +--- + +## 7. 风险与依赖 + +| 风险 | 概率 | 影响 | 应对策略 | +|------|------|------|---------| +| LLM 意图提取准确率不足 | 中 | 高 | 低置信度时追问用户,而非猜测 | +| 决策表覆盖率不足 | 中 | 中 | RAG 工具检索作为兜底方案 | +| LLM 结论生成"幻觉" | 中 | 高 | 基于模板+真实数据填充,而非自由生成 | +| 步骤间数据串联复杂度 | 中 | 中 | 先实现缓存共享,不做复杂依赖图 | +| 流程模板无法覆盖长尾场景 | 低 | 中 | 预留"自定义流程"入口 | + +--- + +**文档维护者:** SSA 架构团队 +**最后更新:** 2026-02-20 +**下一步行动:** 确认优先级后,从 Block-based 重构 → 阶段 ① 开始执行 diff --git a/docs/03-业务模块/SSA-智能统计分析/04-开发计划/10-QPER架构开发计划-智能化主线.md b/docs/03-业务模块/SSA-智能统计分析/04-开发计划/10-QPER架构开发计划-智能化主线.md new file mode 100644 index 00000000..77d504b4 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/04-开发计划/10-QPER架构开发计划-智能化主线.md @@ -0,0 +1,1297 @@ +# SSA-Pro Q-P-E-R 架构开发计划 — 智能化主线 + +> **文档版本:** v7.1 +> **创建日期:** 2026-02-20 +> **最后更新:** 2026-02-21(v7.1 — 新增核心原则"领域知识可配置化" + Phase P 配置化基础设施:Zod 校验 / 热更新 API / 领域文件拆分) +> **架构模式:** Query → Planner → Execute → Reflection +> **目标:** 让不懂统计的医生完成专业级的统计分析 +> **本文档定位:** **替代旧 MVP 开发计划总览**,成为 SSA 开发的主线指南 + +--- + +## 1. 架构总览 + +### 1.1 Q-P-E-R 四层架构 + +``` +用户:"我有 200 个患者数据,想看看新药有没有效" + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ Q · Query Layer (理解层) │ +│ "用户到底想要什么?数据长什么样?" │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ IntentParser │→ │ DataProfiler │→ │ Clarifier │ │ +│ │ LLM 意图 │ │ 数据诊断 │ │ 追问澄清 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ 输出:ParsedQuery { goal, y, x, design, dataProfile } │ +└──────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ P · Planner Layer (规划层) │ +│ "该用什么方法?按什么顺序?" │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │DecisionTable │→ │FlowTemplate │→ │ SAP Builder │ │ +│ │ 四维匹配 │ │ 流程模板 │ │ 计划生成 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ 输出:AnalysisPlan { steps[], methodology, rationale } │ +└──────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ E · Execute Layer (执行层) ✅ 已完成 │ +│ "调用 R 引擎,跑出数字" │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Orchestrator│→ │ R Client │→ │ SSE Stream │ │ +│ │ 步骤编排 │ │ 调用 R 引擎 │ │ 实时进度 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ 输出:StepResult[] { stats, tables, plots, code } │ +└──────────────────────┬──────────────────────────────────┘ + │ + ▼ +┌─────────────────────────────────────────────────────────┐ +│ R · Reflection Layer (审视层) │ +│ "结果说明了什么?可以发论文吗?" │ +│ │ +│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ +│ │ Integrator │→ │ LLM Critic │→ │ReportBuilder │ │ +│ │ 结果整合 │ │ 论文级结论 │ │ 报告生成 │ │ +│ └──────────────┘ └──────────────┘ └──────────────┘ │ +│ │ +│ 输出:Report { conclusion, methodology, limitations } │ +└─────────────────────────────────────────────────────────┘ +``` + +### 1.2 与现有代码的映射 + +| Q-P-E-R 层 | 现有代码 | 现状 | 目标 | +|------------|---------|------|------| +| **Q · Query** | `WorkflowPlannerService.parseUserIntent()` | 正则匹配,无 LLM | LLM 四维提取 + 追问 | +| **Q · Query** | `DataProfileService` + `DataParserService` | 基础统计 | 增加诊断项 + 传递给 P 层 | +| **P · Planner** | `WorkflowPlannerService.generateSteps()` | 硬编码 if/else | 决策表 + 流程模板 | +| **E · Execute** | `WorkflowExecutorService` + `RClientService` | **✅ 完成** | 保持不变 | +| **E · Execute** | 7 个 R 工具 + SSE + 前端展示 | **✅ 完成** | Block-based 标准化输出 | +| **R · Reflection** | `ConclusionGeneratorService` | 规则拼接,无 LLM | LLM 论文级结论 | + +### 1.3 核心原则 + +1. **Execute 层不动** — 这是花了最多时间搭建的基础,完整保留 +2. **前后加层** — 在 Execute 前面加更好的 Q+P,后面加 R +3. **LLM 只在 Q 和 R 层使用** — P 层用确定性的决策表,E 层用确定性的 R 引擎 +4. **渐进式替换** — 每层完成后立即可用,不需要全部完成才能看到效果 +5. **复用平台通用能力层,禁止重复造轮** — + - LLM 调用 **必须** 通过 `common/llm/LLMFactory` + `common/streaming/StreamingService` + - Prompt 管理 **必须** 使用 `common/prompt/PromptService`(数据库版本化 `capability_schema.prompt_templates`),禁止在代码中硬编码 Prompt 字符串 + - 缓存使用 `common/cache/CacheService`(Postgres-Only),日志使用 `common/logging/` + - 参考 `docs/04-开发规范/08-云原生开发规范.md` 和 `docs/02-通用能力层/00-通用能力层清单.md` +6. **⭐ 领域知识可配置化 — QPER 四层的所有"领域知识"由统计学方法学团队配置,不写死在代码中** — + > IT 团队负责搭建引擎和管道,统计学方法学团队负责往管道里填充内容。"可配置"是贯穿所有 Phase 的架构约束。 + + **约束:一切业务逻辑靠读 JSON/数据库驱动,绝不写死在 TypeScript 的 if-else 和常量对象中。** + + | QPER 层 | 方法学团队可配置的内容 | 配置载体 | + |---------|----------------------|---------| + | **Q 层** | Intent Prompt(Few-Shot 示例、Confidence Rubric、Goal 类型枚举) | `PromptService` 数据库(✅ 已实现) | + | **P 层** | 决策表规则、流程模板、Switch Condition、EPV 阈值 | JSON 文件 → Repository 接口(Phase P) | + | **E 层** | R 工具注册表(100 个工具的参数定义、前置条件、护栏规则) | JSON 文件 `tools_registry.json`(Phase P 创建) | + | **R 层** | 结论模板、方法学说明模板、统计量阈值定义 | JSON 文件 `narrative_templates.json`(Phase R 创建) | + + **配置化三道防线(Phase P 落地):** + 1. **Zod Schema 严格校验** — 方法学团队编辑 JSON 时的拼写错误、结构错误在加载时立即拦截,不传播到运行时 + 2. **热更新 API** — 方法学团队修改 JSON 后无需 IT 团队重启服务,调用 `POST /reload-config` 即可生效(校验失败则保留旧配置) + 3. **领域文件物理拆分** — 按职责拆为独立 JSON 文件,避免版本冲突,每个文件有明确的所有者 + + ``` + backend/src/modules/ssa/config/ + ├── tools_registry.json # E 层:100 个工具的注册定义(方法学团队维护) + ├── decision_tables.json # P 层:四维匹配规则 + 降级触发条件(方法学团队维护) + ├── flow_templates.json # P 层:标准步骤流组合(方法学团队维护) + └── narrative_templates.json # R 层:结论生成和理由说明的文案模板(Phase R 创建) + ``` + +### 1.4 架构红线(不可为清单) + +> **来源:** 四份外部调研报告 + 架构审查共识。以下三条为强制约束,任何 Phase 均不可违反。 + +| # | 红线 | 理由 | +|---|------|------| +| 🚫 1 | **禁止引入 LangGraph / AutoGen 等编排框架** | QPER 是线性流水线 + 重试,Node.js 原生 `while` + `try-catch` 足够编排,框架只增加复杂度和调试成本 | +| 🚫 2 | **禁止 LLM 生成并执行 R 代码** | 只允许 LLM 修改**参数 JSON** 重新请求固定 R 脚本(`r-statistics-service/tools/`),杜绝远程代码执行风险。R 工具是参数化封装的固定脚本,不是 LLM 的画布 | +| 🚫 3 | **禁止多智能体辩论(Multi-Agent Debate)** | 统计学有硬性定理(正态性 P<0.05 就是不满足),用决策表规则判定即可,不需要两个 LLM 互相消耗 Token | + +--- + +## 2. 当前系统资产盘点 + +### 2.1 可直接复用(不改动) + +| 资产 | 文件 | 状态 | +|------|------|------| +| R 统计引擎 | `r-statistics-service/` (7 个工具) | ✅ | +| R 调用客户端 | `RClientService.ts` | ✅ | +| 工作流执行器 | `WorkflowExecutorService.ts` | ✅ | +| SSE 实时通信 | `workflow.routes.ts` | ✅ | +| 数据上传 + OSS | 会话管理 + 文件上传路由 | ✅ | +| 前端 V11 UI | SSAWorkspace 全套组件 | ✅ | +| 前端执行日志 | terminal-box + TraceLogItem | ✅ | +| 前端结果展示 | 统计量/表格/图表渲染 | ✅ | +| R 代码导出 | SSACodeModal (多步骤聚合) | ✅ | +| Word 报告导出 | useAnalysis.exportWorkflowReport | ✅ | +| Python 数据质量 | DataProfileService + extraction_service | ✅ | + +### 2.2 需要拆分重构 + +| 现有代码 | 拆分为 | 层级 | +|---------|--------|------| +| `WorkflowPlannerService.parseUserIntent()` | `QueryService.parseIntent()` | Q | +| `WorkflowPlannerService.generateSteps()` | `PlannerService.plan()` | P | +| `AVAILABLE_TOOLS` 硬编码对象 | `DecisionTable` 配置文件 | P | +| `ConclusionGeneratorService`(已有,规则拼接) | `ReflectionService`(LLM 升级) | R | + +> **迁移策略:** `WorkflowPlannerService` 不立即删除,而是在内部委托给新的 QueryService / PlannerService,保留旧入口作为兼容层,待 Q+P 层稳定后再移除。 + +### 2.3 需要新建 + +| 新建服务 | 层级 | 核心能力 | +|---------|------|---------| +| `QueryService` | Q | LLM 意图解析 + 追问(通过平台 `LLMFactory` + `PromptService`) | +| `DataDiagnosticService` | Q | 异常值/正态性/平衡性检测(扩展现有 `DataProfileService`) | +| `DecisionTableService` | P | 四维匹配 | +| `FlowTemplateService` | P | 流程模板管理 | +| `DynamicReport.tsx` | E(前端) | Block-based 动态渲染 | + +> **注意:** `ReflectionService` 不是新建,而是对已有 `ConclusionGeneratorService` 的 LLM 升级重构(见 2.2)。 + +### 2.4 已有数据库资产(Prisma schema `ssa_schema`) + +| 模型 | 状态 | 说明 | +|------|------|------| +| `SsaSession` / `SsaMessage` / `SsaFileUpload` | ✅ 使用中 | 会话管理 + 消息 + 文件上传 | +| `SsaDecisionTable` | ✅ 已建模 | 决策表记录,`DecisionTableService` 应优先使用此表 | +| `SsaRCodeLibrary` | ✅ 已建模 | R 代码库记录 | +| `SsaAnalysisResult` | ✅ 已建模 | 分析结果记录(含 `report_blocks` 字段)| + +> Phase P 的决策表配置应评估是否直接使用 `SsaDecisionTable` 数据库表,而非仅用静态 JSON 文件。 + +--- + +## 3. 开发计划(5 个 Phase) + +### Phase E+ : Execute 层标准化 — Block-based(2.5 天) + +**为什么先做这个:** 后续每新增一个 R 工具,前端都需要写自定义渲染代码。Block-based 协议让这个成本归零,是所有后续开发的前提。 + +``` +目标:R 输出 report_blocks[] → Node.js 透传 → 前端 DynamicReport 渲染 +``` + +| # | 任务 | 层级 | 工时 | 产出 | +|---|------|------|------|------| +| 1 | R 端辅助函数库 `block_helpers.R` | R | 1h | `make_table_block()`, `make_kv_block()`, `make_image_block()` | +| 2 | 改造 `t_test_ind.R` 输出 `report_blocks` | R | 1h | 第一个 Block-based 工具 | +| 3 | 改造其余 6 个 R 工具 | R | 6h | 全部输出 `report_blocks` | +| 4 | 后端透传 `report_blocks` 全链路 | 后端 | 1.5h | `WorkflowExecutorService` → SSE 事件 → 前端 store;需验证 `SsaAnalysisResult` 表 `report_blocks` 字段持久化 | +| 5 | 前端 `DynamicReport.tsx` 组件 | 前端 | 2h | 4 个 Block 渲染子组件 | +| 6 | 前端 `exportBlocksToWord.ts` | 前端 | 2h | Block 数组 → Word 文档 | +| 7 | `SSAWorkspacePane` 集成 + fallback | 前端 | 1h | 优先读 `report_blocks`,旧逻辑兜底 | +| 8 | 旧渲染代码保留为 fallback | 前端 | 1h | `DescriptiveResultView` 等组件(2026-02-20 新建)暂保留,当 `report_blocks` 为空时自动降级使用;待 Block-based 全面验证后再清理 | + +**验收标准:** 所有 7 个 R 工具的结果通过 `DynamicReport` 统一渲染,Word 导出正常。 + +--- + +### Phase Q : Query 层建设 — LLM 意图理解(4 天) + +**为什么第二做:** 这是用户体验提升最大的单一改进。从"用户必须知道 T 检验"变为"用户说'有没有效'就够了"。 + +``` +目标:用户自然语言 → LLM 提取四维信息 → 不确定时追问 +``` + +#### 3.1 后端任务 + +| # | 任务 | 工时 | 说明 | +|---|------|------|------| +| 1 | 通过 `PromptService` 注册 Intent Prompt | 5h | 编写 Prompt + seed 脚本写入 `capability_schema.prompt_templates`;输出:`{ goal, outcome_var, predictor_vars, design, confidence }`。**必须包含 3-5 组 Few-Shot 示例**(覆盖"哪个组更好"→差异比较、"血压和体重有关吗"→关联分析等典型口语表达),显著提升首次解析准确率。**Confidence Rubric 客观化**:Prompt 中必须内嵌打分准则(0.9+ = 用户原话明确了 Y 和至少一个 X;0.7-0.8 = 指出了 Y 但 X 需推断;< 0.7 = 只说了"帮我分析"需追问),防止 LLM 自评虚高 | +| 2 | 新建 `QueryService.ts` | 8h | 通过 `LLMFactory.create()` 调用 LLM + json-repair + Zod 校验 + 低置信度追问逻辑 + 正则兜底(保留现有 `parseUserIntent` 逻辑作为 fallback)。**Zod 动态防幻觉**:用 `createIntentSchema(validColumns)` 动态生成 Schema,通过 `.refine()` 校验 LLM 输出的 `outcome_var`/`predictor_vars` 是否存在于真实列名中,捏造列名立即触发重试或 fallback。**Confidence 二次验证**:若 LLM 打分 ≥0.9 但 outcome_var 或 predictor_vars 为空,强制降级到 0.5 | +| 3 | 增强 `DataProfileService` | 4h | 增加异常值检测(IQR)、分组平衡性、样本量评估;注意:DataParserService 的 0/1 分类规则已在 2026-02-19 优化完成,不重复。**非分析变量自动标记**:Python 端对高基数字符串列(uniqueValues/totalRows > 0.95)、日期列、列名匹配 `_id/_no/编号/序号` 模式的列打 `is_id_like: true` 标签;Q→P 上下文裁剪时物理剔除这些列,让 LLM 看不到 patient_id 等非分析列 | +| 4 | 定义 `ParsedQuery` 接口 | 1h | Q 层输出 → P 层输入的标准契约,放 `types/query.types.ts` | +| 5 | 新建追问 API | 2h | `POST /api/v1/ssa/sessions/:id/clarify`,在现有 `analysis.routes.ts` 中扩展 | +| 6 | 安装 `json-repair` + `zod` 依赖 | 0.5h | Phase 1 遗留任务,此处一并完成 | +| 7 | **Q→P 上下文裁剪(Context Pruning)** | 1h | Q 层识别出 Y/X 变量后,只提取这几列的 DataProfile 子集传给 P 层(Hot Context);其余列的详情留在数据库按需拉取(Cold Context)。防止 100 列 CSV 的 50KB DataProfile JSON 导致 Planner 的 Token 爆炸和"注意力涣散" | +| 8 | **DataProfile 会话级缓存** | 0.5h | 用户上传文件后 DataProfiler 只执行一次,结果通过 `CacheService` 缓存(key: `ssa:profile:{sessionId}:{fileHash}`)。同一会话+同一文件的后续 Q 层循环(如追问后重新解析)直接读缓存,实现毫秒级响应,避免重复调用 Python 解析 20MB CSV | + +**QueryService 核心逻辑:** + +```typescript +// 伪代码 — 使用平台通用能力层 +class QueryService { + constructor( + private promptService: PromptService, // 平台通用能力:Prompt 管理 + private llmFactory: LLMFactory, // 平台通用能力:LLM 网关 + ) {} + + async parseQuery(userText: string, dataProfile: DataProfile): Promise { + // 1. 从数据库加载 Prompt 模板(支持版本化 + A/B 测试) + const promptTemplate = await this.promptService.getPrompt('ssa_intent_parse', 'latest'); + const prompt = this.promptService.render(promptTemplate, { userText, variables: dataProfile.variables }); + + // 2. 通过 LLMFactory 调用 LLM(自动选模型 + 统一错误处理) + const llm = this.llmFactory.create({ model: 'deepseek-v3', temperature: 0.1 }); + const llmResult = await llm.chat(prompt); + + // 3. Zod 校验结构(json-repair 容错) + const parsed = IntentSchema.parse(jsonRepair(llmResult)); + + // 4. 低置信度 → 生成追问 + if (parsed.confidence < 0.7) { + return { needsClarification: true, questions: this.generateQuestions(parsed) }; + } + + // 5. 融合数据诊断结果 + const diagnosis = await this.diagnoseData(dataProfile, parsed); + + // 6. 上下文裁剪(Context Pruning)— 只保留 Y/X 变量的 DataProfile 子集 + // Planner 不需要看 100 列的完整统计特征,只需相关变量的 Hot Context + const prunedProfile = this.pruneForPlanner(dataProfile, parsed); + + return { ...parsed, dataDiagnosis: diagnosis, prunedProfile, needsClarification: false }; + } + + // 上下文裁剪:从全量 DataProfile 中提取 Planner 需要的最小子集 + private pruneForPlanner(fullProfile: DataProfile, parsed: ParsedQuery): PrunedProfile { + const relevantVars = [parsed.outcome_var, ...parsed.predictor_vars]; + return { + schema: fullProfile.variables.map(v => ({ name: v.name, type: v.type })), // 全部列的类型(轻量) + details: fullProfile.variables.filter(v => relevantVars.includes(v.name)), // 只有 Y/X 的详细统计 + sampleSize: fullProfile.sampleSize, + missingRateSummary: fullProfile.overallMissingRate, + }; + } + + // Fallback:LLM 调用失败时降级到现有正则匹配(WorkflowPlannerService.parseUserIntent) + private async fallbackToRegex(userText: string, variables: Variable[]): Promise { ... } +} +``` + +#### 3.2 前端任务 + +| # | 任务 | 工时 | 说明 | +|---|------|------|------| +| 6 | 追问卡片组件 `ClarificationCard.tsx` | 3h | **必须是封闭式数据驱动选择题**,禁止开放式提问。后端基于 DataProfile 变量列表预生成 2-3 个具有统计学意义的分析假设,前端渲染为可点击的快捷 Tag(如:"检测到 [Drug] 和 [BP],您是想:👉[比较差异] 👉[相关分析]")。错误示范:`"请问您想分析哪两列?"` | +| 7 | `SSAChatPane` 集成追问流程 | 2h | 收到 `needsClarification` 时展示卡片;用户点击选项后自动填充 ParsedQuery 缺失字段,重新提交 | +| 8 | 数据诊断增强展示 | 2h | `DataProfileCard` 增加异常值/平衡性信息 | + +#### 3.3 Prompt 设计要点 + +> **存储方式:** 以下 Prompt 通过 seed 脚本写入 `capability_schema.prompt_templates` 表,`template_key = 'ssa_intent_parse'`。运行时通过 `PromptService.getPrompt()` 加载,**禁止硬编码在 TypeScript 中**。 + +``` +你是一位临床研究统计顾问。请从用户的描述中提取以下信息: + +1. goal: 分析目的 + - "difference": 比较差异("有没有效"、"是否不同"、"比较") + - "association": 关联分析("相关"、"影响因素"、"关系") + - "prediction": 预测建模("预测"、"风险"、"预后") + - "description": 描述统计("描述"、"特征"、"分布") + - "cohort_study": 队列研究全套分析("出一套完整报告"、"基线+单因素+多因素"、"Table 1-3"、"队列研究") + +2. outcome_var: 结局变量(Y),从变量列表中识别 +3. predictor_vars: 自变量/分组变量(X),从变量列表中识别 +4. design: 实验设计 + - "independent": 独立样本(不同的人比较) + - "paired": 配对设计(同一患者前后比较) + +5. confidence: 0.0-1.0,你对以上解析的确信程度 + +**重要规则:** +- 请自动忽略明显的非分析变量(如 patient_id、姓名、病历号、病床号、录入日期等标识/管理类字段),不要将它们选为 outcome_var 或 predictor_vars。 +- 如果变量列表中附有用户标注的中文含义(variable_description),优先参考含义而非列名进行角色判断。 + +可用变量列表:{{variables}} +用户查询:{{query}} +``` + +**验收标准:** +- "分析 sex 与 Yqol 的相关性" → `{ goal: "association", outcome: "Yqol", predictors: ["sex"], confidence: 0.9 }` +- "有没有效" → `{ goal: "difference", confidence: 0.3, needsClarification: true, questions: [...] }` + - 追问**必须是选择题**:`"检测到数据包含 [Drug](分类) 和 [BP](连续),您是想:A. 比较用药组 vs 对照组的 BP 差异 B. 分析 Drug 与 BP 的相关性"` + - 禁止开放式提问如 `"请问您想比较哪个指标?"` +- `prunedProfile` 只包含 Y/X 变量的详细统计(非 100 列全量),schema 字段包含全部列名+类型 +- **探路测试(Tracer Bullet):** 后端需提供 `test_query_pipeline.ts` 脚本,用 mock DataProfile 验证 Q 层输出结构正确,无需前端和 R 服务 + +--- + +### Phase P : Planner 层重构 — 决策表 + 流程模板(4 天) + +**为什么第三做:** 让系统从"单方法执行"跃迁到"完整分析流程"。用户会看到一个包含 4-6 个步骤的专业分析计划。 + +``` +目标:ParsedQuery → 决策表匹配 → 流程模板填充 → 完整 SAP +``` + +#### 3.4 决策表设计 + +**核心思想:一张表解决"该用什么方法"的问题。** + +> **v7.0 增强:** ① 新增 `Switch Condition` 列 — 描述 Primary/Fallback 互换的触发条件,让方法切换从硬编码变为配置化;② **参数检验优先原则** — Primary 始终设为参数检验(统计效力更高),非参数方法作为 Fallback(安全网),最终裁决权交给 R 引擎的数据分布检验。 + +| Goal | Y_Type | X_Type | Design | Primary Tool | Fallback Tool | Switch Condition | +|------|--------|--------|--------|-------------|---------------|-----------------| +| difference | continuous | categorical_2 | independent | ST_T_TEST_IND | ST_MANN_WHITNEY | `normality_fail`: Shapiro-Wilk P<0.05 | +| difference | continuous | categorical_2 | paired | ST_T_TEST_PAIRED | — | — | +| difference | continuous | categorical_multi | independent | ST_ANOVA_ONE | ST_KRUSKAL | `normality_fail`: Shapiro-Wilk P<0.05 | +| difference | categorical | categorical | independent | ST_CHI_SQUARE | ST_FISHER | `expected_freq_low`: 期望频数<5 | +| association | continuous | continuous | — | ST_CORRELATION | — | — | +| association | categorical | any | — | ST_CHI_SQUARE | ST_FISHER | `expected_freq_low`: 期望频数<5 | +| prediction | categorical_binary | any | — | ST_LOGISTIC | — | — | +| prediction | continuous | any | — | ST_LINEAR_REG | — | — | +| description | any | any | — | ST_DESCRIPTIVE | — | — | +| **cohort_study** | **categorical_binary** | **categorical** | **independent** | **ST_BASELINE_TABLE** | — | — | + +> **`cohort_study` 场景说明:** 当 Q 层识别到用户意图为"队列研究全套分析"(触发词:"出一套完整报告"、"基线比较+单因素+多因素"、"Table 1-3"等)时,不走单工具匹配,而是直接选用 `cohort_study_standard` 流程模板。Q 层 `ParsedQuery.goal` 需扩展支持 `'cohort_study'` 值。 + +#### 3.4.1 决策表存储策略 — Repository 模式 + +> **v7.0 新增架构决策:** 决策表初期使用 JSON 文件(内容稳定、不需 A/B 测试),但通过 Repository 接口解耦,后期可无缝切换到数据库。 + +```typescript +// Repository 接口 — 核心业务逻辑只依赖此接口 +interface IDecisionTableRepo { + getRules(): Promise; +} + +// Phase P 实现:JSON 文件 +class JsonDecisionTableRepo implements IDecisionTableRepo { + async getRules() { return decisionTableJson as DecisionRule[]; } +} + +// 预留 Phase Deploy 实现:数据库(SsaDecisionTable 模型已建好) +class PgDecisionTableRepo implements IDecisionTableRepo { + async getRules() { return await prisma.ssaDecisionTable.findMany(); } +} +``` + +#### 3.4.2 Expected vs Actual 双层审计日志 + +> **v7.0 新增架构决策:** P 层规划时**不做正态性预检**(正态性检验只在 R 引擎中执行),而是生成"策略日志 (Planned Trace)"。E 层执行后生成"事实日志 (Actual Trace)"。R 层合并两者生成方法学说明。 + +``` +P 层(策略)──→ PlannedTrace: { primary: "T-Test", fallback: "Wilcoxon", switchCondition: "Shapiro P<0.05" } + ↓ + 前端 SAP 卡片: "将执行 T 检验。🛡️护栏:若正态性不满足,将自动降级为 Wilcoxon。" + ↓ +E 层(事实)──→ R trace_log: { check: "normality", result: "fail", action: "switched to Wilcoxon" } + ↓ +R 层(合并)──→ 结合 PlannedTrace + trace_log 生成论文级方法学说明 +``` + +**PlannedTrace 类型定义:** + +```typescript +interface PlannedTrace { + matchedRule: string; // "Goal=difference, Y=continuous, X=categorical_2, Design=independent" + primaryTool: string; // "ST_T_TEST_IND" + fallbackTool: string | null; // "ST_MANN_WHITNEY" + switchCondition: string | null; // "normality_fail: Shapiro-Wilk P<0.05" + templateUsed: string; // "standard_analysis" + reasoning: string; // 人类可读的方法选择理由 +} +``` + +> **原则:** P 层只描述"如果…则…"的策略,**绝不在 Node.js 中做正态性预检**。R 引擎已有完整的分布检验 + 自动降级逻辑,它的 `trace_log` 天然就是 Actual Trace。 + +#### 3.5 流程模板设计 + +**核心思想:每种分析目的对应一个标准流程。** + +> **v7.0 优化:** 将 `two_group_comparison` 和 `association_analysis` 合并为 `standard_analysis`(结构相同,区别仅在决策表填充的工具不同)。最终为 **4 个通用模板 + 1 个队列研究专用模板**。 + +```typescript +const FLOW_TEMPLATES = { + // 通用三步模板 — 适用于 comparison / correlation / association + "standard_analysis": { + name: "标准分析流程", + steps: [ + { order: 1, role: "descriptive", tool: "ST_DESCRIPTIVE" }, + { order: 2, role: "primary_test", tool: "{{matched_tool}}" }, + { order: 3, role: "sensitivity", tool: "{{fallback_tool}}", condition: "fallback_exists" }, + ] + }, + // 配对设计专用 + "paired_analysis": { + name: "配对设计分析", + steps: [ + { order: 1, role: "descriptive", tool: "ST_DESCRIPTIVE" }, + { order: 2, role: "primary_test", tool: "{{matched_tool}}" }, + ] + }, + // 回归建模 + "regression_analysis": { + name: "回归建模", + steps: [ + { order: 1, role: "descriptive", tool: "ST_DESCRIPTIVE" }, + { order: 2, role: "primary_test", tool: "{{matched_tool}}" }, + ] + }, + // 纯描述统计 + "descriptive_only": { + name: "描述性统计", + steps: [ + { order: 1, role: "descriptive", tool: "ST_DESCRIPTIVE" }, + ] + }, + // v6.0 新增:队列研究全套分析(覆盖经典 Table 1 → Table 2 → Table 3) + "cohort_study_standard": { + name: "经典队列研究全套分析", + steps: [ + { order: 1, role: "baseline_table", tool: "ST_BASELINE_TABLE", + name: "表1: 组间基线特征比较", + params_mapping: { group_var: "{{exposure_var}}", analyze_vars: "{{all_covariates}}" } + }, + { order: 2, role: "univariate_screen", tool: "ST_BASELINE_TABLE", + name: "表2: 结局指标单因素分析", + params_mapping: { group_var: "{{outcome_var}}", analyze_vars: "{{all_covariates}}" } + }, + { order: 3, role: "multivariate_reg", tool: "ST_LOGISTIC_BINARY", + name: "表3: 多因素 Logistic 回归", + params_mapping: { outcome_var: "{{outcome_var}}", predictors: "{{epv_capped_predictors}}" } + }, + ] + } +}; +``` + +#### 3.5.1 队列研究 Table 3 — "全量推入 + EPV 防护 + 免责声明"策略 + +> **v7.0 新增架构决策:** Phase Q+ 的变量选择面板尚未上线前,Table 3 的自变量列表采用**全量推入**策略,辅以 EPV 上限防护。 + +**EPV(Events Per Variable)防护逻辑:** + +``` +可纳入变量数 = floor( min(outcome=0的数量, outcome=1的数量) / 10 ) +``` + +- 如果 Q 层给出 20 个 predictor,但 EPV 只允许 5 个 → P 层自动截断到前 5 个 +- 截断优先级:按 DataProfile 中与 outcome 的关联强度排序(分类变量用卡方统计量,连续变量用 T/Mann-Whitney 统计量) +- 前端 SAP 卡片标注红色 Tag: **`[AI 自动探索模式]`** +- 免责文案:"当前多因素回归模型纳入了 AI 自动选择的变量(受样本量限制已自动筛选),在后续版本中您将可以手动调整变量。" + +> **Phase Q+ 上线后:** `{{epv_capped_predictors}}` 替换为 `{{user_confirmed_predictors}}`,EPV 防护仍保留作为兜底。 + +#### 3.6 后端任务 + +> **v7.1 调整:** 新增任务 0(配置化基础设施)和任务 1(工具注册表),原有任务顺延。所有领域知识从代码常量迁移到 JSON 文件,通过 Repository + Zod 校验加载。 + +| # | 任务 | 工时 | 说明 | +|---|------|------|------| +| 0 | **配置化基础设施:ConfigLoader + Zod Schema + 热更新 API** | 2h | ① 通用 `ConfigLoader` 基类(读 JSON + Zod 校验 + 内存缓存);② `POST /api/v1/system/reload-config`(管理员权限,校验失败保留旧配置,返回 400 + 错误详情);③ 四个领域 JSON 文件的 Zod Schema 定义 | +| 1 | **创建工具注册表 `tools_registry.json`** | 2h | 将 `AVAILABLE_TOOLS` 硬编码常量迁移为 JSON 文件。每个工具定义包含:`code`, `name`, `category`, `description`, `inputParams`(含 Zod 类型), `outputType`, `prerequisite`, `fallback`。通过 `ToolRegistryService` + Repository 加载 | +| 2 | 创建决策表配置 `decision_tables.json` | 2h | 四维匹配规则的 JSON 版本,含 `switchCondition` 字段 | +| 3 | 新建 `DecisionTableService.ts` | 4h | 通过 `IDecisionTableRepo` 接口加载(Phase P: JSON 实现),四维匹配逻辑 | +| 4 | 创建流程模板配置 `flow_templates.json` | 2h | 4+1 个标准模板 | +| 5 | 新建 `FlowTemplateService.ts` | 4h | 模板选择 + 参数填充(含 EPV 截断逻辑) | +| 6 | 重构 `PlannerService.ts` | 4h | 接收 ParsedQuery → 调用决策表 → 选模板 → 生成 SAP + PlannedTrace | +| 7 | 前端 SAP 确认卡片增强 | 3h | 显示每步方法、选择理由、护栏说明、`[AI 自动探索模式]` 标签 | +| 8 | 联调测试 | 3h | Q → P → E 全链路 + Tracer Bullet 脚本 | + +> **配置化校验流程:** +> ``` +> 方法学团队修改 JSON → 调用 POST /reload-config → Zod 校验 +> ├── 通过 → 刷新内存缓存 → 返回 200 ✅ 新规则生效 +> └── 失败 → 保留旧配置 → 返回 400 + 详细错误信息 ❌ 服务不受影响 +> ``` + +**PlannerService 核心逻辑(v7.0 更新):** + +```typescript +// 伪代码 — 体现 Repository 模式 + Expected/Actual 双层日志 +class PlannerService { + constructor( + private decisionTableRepo: IDecisionTableRepo, // Repository 模式:JSON 或 DB + private flowTemplateService: FlowTemplateService + ) {} + + async plan(query: ParsedQuery, profile?: DataProfile): Promise { + // 1. 决策表匹配 → 主方法 + 备选方法(通过 Repository 加载规则) + const rules = await this.decisionTableRepo.getRules(); + const match = this.matchRule(rules, query.goal, query.outcome_type, query.predictor_types, query.design); + + // 2. 选择流程模板 + const template = this.flowTemplateService.select(query.goal, query.design, match); + + // 3. 填充参数(含 EPV 防护) + const steps = this.flowTemplateService.fill(template, { + matched_tool: match.primaryTool, + fallback_tool: match.fallbackTool, + outcome_var: query.outcome_var, + predictor_vars: query.predictor_vars, + grouping_var: query.grouping_var, + profile, + }); + + // 4. 生成 PlannedTrace(策略日志 — P 层只描述"如果…则…") + const plannedTrace: PlannedTrace = { + matchedRule: `Goal=${query.goal}, Y=${query.outcome_type}, X=${query.predictor_types[0]}, Design=${query.design}`, + primaryTool: match.primaryTool, + fallbackTool: match.fallbackTool, + switchCondition: match.switchCondition, // "normality_fail: Shapiro-Wilk P<0.05" + templateUsed: template.id, + reasoning: this.explainMethodChoice(match, query), + }; + // 注意:Actual Trace 由 E 层的 R trace_log 提供,P 层不做正态性预检 + + // 5. 生成 SAP + return { steps, plannedTrace, methodology: plannedTrace.reasoning }; + } +} +``` + +**验收标准(v7.0 更新):** +- 输入 `{ goal: "comparison", outcome_type: "continuous", predictor_types: ["binary"], design: "independent" }` +- 输出 3 步流程:描述统计 → T 检验(Primary) → Mann-Whitney(Sensitivity,条件展示) +- `plannedTrace` 包含 `switchCondition: "normality_fail"` + `reasoning` 人类可读理由 +- 前端 SAP 卡片每步显示选择理由;有 fallback 的步骤显示护栏说明("若正态性不满足,将自动降级为…") +- **P 层不做正态性预检**,方法切换由 R 引擎在执行时根据数据分布自行决定 +- **队列研究场景**:输入 `{ goal: "cohort_study" }` → 输出 Table 1/2/3 三步流程,Table 3 自变量经 EPV 截断 + `[AI 自动探索模式]` 标签 +- **探路测试(Tracer Bullet):** 后端需提供 `test_planner_pipeline.ts` 脚本,用 mock ParsedQuery 验证 P 层输出结构正确(不依赖前端和 R 服务)。Q→P 串联测试也必须通过 + +--- + +### Phase R : Reflection 层建设 — LLM 论文级结论(3 天) + +**为什么第四做:** 这是让输出从"P=0.015"变成"可以直接用于论文"的关键。 + +``` +目标:StepResult[] → LLM 综合解读 → 论文级结论 + 方法学说明 +``` + +#### 3.7 后端任务 + +| # | 任务 | 工时 | 说明 | +|---|------|------|------| +| 1 | 通过 `PromptService` 注册 Reflection Prompt | 3h | seed 脚本写入 `capability_schema.prompt_templates`,`template_key = 'ssa_reflection'`;输出:6 要素结论。**统计量通过槽位注入,禁止 LLM 生成数值**。**含敏感性分析冲突处理准则(3.10 节)** | +| 2 | 重构 `ConclusionGeneratorService` → `ReflectionService` | 6h | 通过 `LLMFactory` 调用 LLM + **Zod 强校验 LLM 输出结构(3.11 节)** + 结果整合。接收 `decision_trace` 生成方法学说明。LLM 输出完整收集后校验,不做字符级流式推送(3.12 节) | +| 3 | **E 层运行时崩溃优雅处理**(重试机制推迟至 Phase Deploy) | 1h | 错误分类映射表(NA 值/列名缺失/R Fatal Error → 友好用户提示),不做跨层 Self-healing。**完整重试机制(MAX_RETRIES=2 + 参数级修复 + 方法级降级)推迟至 Phase Deploy**(见 3.8 节详述) | +| 4 | 结论缓存(`CacheService`)+ API | 2h | `GET /sessions/:id/conclusion`,使用平台 `CacheService` 缓存结论避免重复 LLM 调用 | +| 5 | 前端论文结论展示 | 4h | Markdown 渲染 + 一键复制 + 折叠/展开;**采用"完整 JSON + 逐 section 渐入动画"方案(3.12 节)**,不做字符级流式推送 | +| 6 | Word 报告增强 | 3h | 纳入 LLM 结论(替代当前的简单 summary) | +| 7 | 联调测试 | 3h | Q → P → E → R 完整链路 | + +> **重要:** `ReflectionService` 是对现有 `ConclusionGeneratorService` 的升级重构,不是从零新建。保留 `ConclusionGeneratorService` 作为 fallback 入口,LLM 调用失败**或 Zod 校验失败**时降级到规则拼接。 + +#### 3.8 E 层运行时错误处理策略 + +> **来源:** 《QPER架构审查与工程避坑指南》暗礁 2 + 架构委员会裁决。 +> +> **关键区分:** "统计降级"(如正态性不满足自动切非参数)由 R 脚本内部 if-else 处理,**不是**运行时错误。"运行时崩溃"(如 NA 值导致 R Fatal Error、列名隐形空格、数据结构异常)才需要 Node.js 层处理。 + +**Phase R 当前实施(优雅错误分类):** + +现有 `WorkflowExecutorService` 已具备步骤级 try-catch + `step_error` SSE 推送能力。Phase R 在此基础上增加**错误分类映射表**,将 R 引擎的原始错误转化为用户友好提示: + +| 错误模式(R 报错关键词) | 用户友好提示 | 错误码 | +|------------------------|-----------|--------| +| `NA`、`missing values`、`incomplete cases` | "数据中存在缺失值,请检查数据清洗后重试" | `E_MISSING_DATA` | +| `column not found`、`undefined columns`、`not found` | "运算引擎未找到指定变量列,请检查数据源列名是否正确" | `E_COLUMN_NOT_FOUND` | +| `system is computationally singular`、`collinear` | "数据存在严重共线性,建议排除冗余变量后重试" | `E_COLLINEARITY` | +| `not enough observations`、`sample size` | "样本量不足以执行该统计方法,建议增加样本或选用非参数方法" | `E_INSUFFICIENT_SAMPLE` | +| `contrasts can be applied only to factors with 2 or more levels` | "分组变量的水平数不足,请检查数据分组" | `E_FACTOR_LEVELS` | +| 其他未匹配的 R 错误 | "运算引擎遇到异常,请检查数据结构后重试" | `E_UNKNOWN` | + +**Phase Deploy 推迟实施(完整重试短路机制):** + +> 以下机制推迟到 Phase Deploy 阶段实施,Phase R 不做跨层 Self-healing。 + +**强制规则:** `MAX_RETRIES = 2`,超过后直接中断。 + +| 错误类型 | 典型报错关键词 | 处理策略 | +|---------|--------------|---------| +| **不可重试(Hard Abort)** | `system is computationally singular`、`not enough observations`、`contrasts can be applied only to factors with 2 or more levels` | 直接中断,跳过重试,生成诊断报告给用户 | +| **可重试 — 参数级修复** | `column not found`、`invalid factor level`、`missing values` | LLM 分析错误日志 → 修改参数 JSON → 重新请求同一 R 脚本(**禁止生成新 R 代码**) | +| **可重试 — 方法级降级** | `sample size too small for parametric test` | 回调 Planner 切换 Fallback Tool(如 T 检验 → Mann-Whitney) | + +#### 3.9 统计量"槽位注入"反幻觉机制 + +> **来源:** 《医疗AI统计助手架构研究》议题 3 — LLM 被剥夺生成数值 Token 的权限。 + +**核心规则:** 结论中的所有统计量(P 值、效应量、置信区间等)**必须**来自 R 引擎的实际输出,通过模板槽位渲染,**禁止 LLM 在自由文本中"编写"任何数值**。 + +``` +// Reflection Prompt 中的槽位引用示例 +各步骤结果(以下数值为系统自动注入,你不得修改或重新表述这些数字): + +步骤 1:独立样本 T 检验 +- 统计量:t = {{steps[1].statistic}} +- P 值:{{steps[1].p_value}} +- 效应量 Cohen's d:{{steps[1].effect_size}} +- 95% 置信区间:{{steps[1].ci_lower}} ~ {{steps[1].ci_upper}} + +请基于上述精确数值生成论文结论,结论中引用数值时必须与上方完全一致。 +``` + +**实现方式:** `ReflectionService.extractKeyFindings()` 将 R 返回的 JSON 中的数值提取为 `{{slot}}` 变量,在 Prompt 渲染时注入。LLM 只负责生成叙述性文字框架。 + +**ReflectionService 核心逻辑:** + +```typescript +// 伪代码 — 使用平台通用能力层(v8.0 更新:Zod 校验 + 完整 JSON 推送) +import { z } from 'zod'; +import { jsonrepair } from 'jsonrepair'; + +// Zod Schema — 强校验 LLM 输出结构(见 3.11 节) +const ConclusionReportSchema = z.object({ + executive_summary: z.string().min(10), + key_findings: z.array(z.string()).min(1), + statistical_summary: z.object({ + total_tests: z.number(), + significant_results: z.number(), + methods_used: z.array(z.string()), + }), + methodology: z.string().min(10), + limitations: z.array(z.string()).min(1), + recommendations: z.array(z.string()).optional(), +}); + +class ReflectionService { + constructor( + private promptService: PromptService, // 平台通用能力:Prompt 管理 + private llmFactory: LLMFactory, // 平台通用能力:LLM 网关 + private cacheService: CacheService, // 平台通用能力:缓存 + private conclusionGenerator: ConclusionGeneratorService, // Fallback:旧规则拼接 + ) {} + + async reflect(plan: AnalysisPlan, results: StepResult[], sseEmitter?: SSEEmitter): Promise { + // 0. 缓存命中检查 + const cacheKey = `ssa:conclusion:${plan.workflow_id}`; + const cached = await this.cacheService.get(cacheKey); + if (cached) return cached; + + // 1. 发送 QPER 状态:正在生成结论 + sseEmitter?.emit({ type: 'qper_status', status: 'reflecting', message: '正在生成论文级结论...' }); + + // 2. 整合所有步骤的关键指标(提取为槽位变量,用于反幻觉注入) + const keyFindings = this.extractKeyFindings(results); + + // 3. 从数据库加载 Prompt 模板 + const promptTemplate = await this.promptService.getPrompt('ssa_reflection', 'latest'); + const prompt = this.promptService.render(promptTemplate, { + goal: plan.goal, + methodology: plan.methodology, + decision_trace: plan.decision_trace, + findings: keyFindings, + sampleSize: plan.sampleInfo + }); + + // 4. 调用 LLM(完整收集,不做字符级流式推送) + const llm = this.llmFactory.create({ model: 'deepseek-v3', temperature: 0.3 }); + try { + const rawOutput = await llm.chat(prompt); + + // 5. jsonrepair + Zod 强校验(见 3.11 节) + const repaired = jsonrepair(rawOutput); + const parsed = JSON.parse(repaired); + const conclusion = ConclusionReportSchema.parse(parsed); + + // 6. 缓存 + 通过 SSE 推送完整结构化结论(见 3.12 节) + const report: ConclusionReport = { + workflow_id: plan.workflow_id, + ...conclusion, + step_summaries: this.buildStepSummaries(results), + generated_at: new Date().toISOString(), + }; + await this.cacheService.set(cacheKey, report, { ttl: 3600 }); + sseEmitter?.emit({ type: 'reflection_complete', conclusion: report }); + return report; + + } catch (error) { + // LLM 失败或 Zod 校验失败 → 降级到规则拼接 + logger.warn('[SSA:Reflection] LLM/Zod failed, falling back to rule-based', { error }); + const fallback = this.conclusionGenerator.generateConclusion(results, plan.goal); + const report = this.adaptLegacyToNew(plan.workflow_id, fallback); + sseEmitter?.emit({ type: 'reflection_complete', conclusion: report }); + return report; + } + } +} +``` + +**Reflection Prompt 设计要点:** + +> **存储方式:** 以下 Prompt 通过 seed 脚本写入 `capability_schema.prompt_templates` 表,`template_key = 'ssa_reflection'`。运行时通过 `PromptService.getPrompt()` 加载。 + +``` +你是一位高级生物统计师,请基于以下分析结果生成论文级结论。 + +分析目标:{{goal}} +采用方法:{{methodology}} + +方法选择的决策轨迹(请据此撰写方法学说明,不得臆造选择理由): +{{decision_trace.matched_rule}} +{{#each decision_trace.diagnosis_adjustments}} +- 调整:{{this}} +{{/each}} + +各步骤结果(⚠️ 以下数值由系统自动注入,你必须原样引用,不得修改、四舍五入或重新表述任何数字): +{{#each findings}} +步骤 {{step_number}}:{{tool_name}} +- 统计量:{{statistic}} +- P 值:{{p_value}} +- 效应量:{{effect_size}} +- 置信区间:{{ci_lower}} ~ {{ci_upper}} +{{/each}} + +请生成包含以下要素的结论: +1. 样本描述(纳入/排除) +2. 主要结果(含统计量和 P 值 — 必须与上方数值完全一致) +3. 效应量解读(临床意义) +4. 敏感性分析结论(如有) +5. 方法学说明(基于上方决策轨迹撰写,解释为什么选择此方法) +6. 局限性声明 + +要求:使用论文"结果"章节的行文风格,可直接复制到论文中。 +``` + +**验收标准:** +- 输入 T 检验结果 `{ t=2.45, p=0.015, d=0.52 }` +- 输出包含 6 个要素的论文级结论 +- Word 导出包含 LLM 结论(非简单数字罗列) + +#### 3.10 敏感性分析结论冲突 Prompt 策略 + +> **来源:** 架构委员会建议 1 — 当主分析与敏感性分析显著性不一致时,LLM 容易陷入逻辑混乱或强行拼凑显著性。 +> +> **业务痛点:** 在"描述→主分析→敏感性分析"的标准流程中,如果 T 检验(主)P=0.04(显著)但 Wilcoxon 检验(辅)P=0.06(不显著),LLM 可能选择性忽略不一致的结果。在临床研究中,这属于必须报告的结果稳健性问题。 + +**实施方式:** 在 `ssa_reflection` Prompt 模板中硬编码冲突处理准则: + +``` +## 冲突处理准则(强制执行) +当主分析与敏感性分析的显著性结论不一致时: +1. 在【局限性声明】中必须指出:"敏感性分析未得到一致结论,结果的稳健性(Robustness)较弱,需谨慎解释临床意义" +2. 在【主要发现】中以主分析结果为基准报告,但需加注"敏感性分析未验证此结论" +3. 严禁选择性报告、强行拼凑显著性 +4. 当所有分析方向一致时,在【主要发现】中强调"敏感性分析进一步验证了结论的稳健性" +``` + +**效果:** LLM 遇到矛盾数据时有明确的处理规程,既保证了学术诚信,又为临床研究者提供了稳健性判断依据。实现成本极低(仅在 Prompt 中增加 4 行准则)。 + +#### 3.11 Zod Schema 强校验 LLM 输出 + +> **来源:** 架构委员会建议 2 — LLM 偶尔漏掉 JSON Key 或将数组写成字符串,导致前端渲染崩溃。 +> +> **与 Phase Q 的模式一致性:** Phase Q 中已成功实践 `jsonrepair` + Zod 动态校验(`createDynamicIntentSchema`)。Phase R 延续同一防御范式。 + +**实施方式:** `ReflectionService` 中对 LLM 输出进行三层防御: + +``` +Layer 1: jsonrepair — 修复 LLM 输出的 JSON 格式错误(漏逗号、多余尾逗号等) +Layer 2: JSON.parse — 解析为 JS 对象 +Layer 3: Zod Schema — 强校验结构完整性(字段是否齐全、类型是否正确、数组最小长度等) +``` + +**Zod Schema 定义(与前端 `ConclusionReport` 接口对齐):** + +```typescript +const ConclusionReportSchema = z.object({ + executive_summary: z.string().min(10), // 不允许空摘要 + key_findings: z.array(z.string()).min(1), // 至少 1 条发现 + statistical_summary: z.object({ + total_tests: z.number(), + significant_results: z.number(), + methods_used: z.array(z.string()), + }), + methodology: z.string().min(10), // 不允许空方法学说明 + limitations: z.array(z.string()).min(1), // 至少 1 条局限性 + recommendations: z.array(z.string()).optional(), // 建议为可选 +}); +``` + +**降级触发:** Zod 校验失败时,立即切回旧的 `ConclusionGeneratorService`(规则拼接),确保前端始终收到有效的结论结构。日志记录 Zod 错误详情,用于后续 Prompt 迭代优化。 + +#### 3.12 Reflection 输出交付策略(完整 JSON + 逐 Section 渐入) + +> **来源:** 架构委员会建议 3 — 逐字推送 JSON 字符串会导致前端频繁抛出 `SyntaxError`。 +> +> **核心决策:** Phase R 的 Reflection 输出**不做字符级流式推送**,采用"后端完整收集 + Zod 校验 + 一次性推送 + 前端逐 section 渐入动画"方案。 + +**不采用字符级流式推送的理由:** + +1. **Zod 校验需要完整 JSON** — 无法对不完整的 JSON 片段执行结构校验 +2. **结构化数据需求** — Word 导出、数据库存储、API 返回都需要完整的 `ConclusionReport` JSON +3. **等待时间可接受** — LLM 生成结论通常 3-8 秒,期间通过 `qper_status` SSE 事件展示"正在生成论文级结论...",用户已看到所有数字结果(E 层 `report_blocks`),不会焦虑 +4. **避免 Markdown↔JSON 双向转换的脆弱性** — 如果 LLM 输出纯 Markdown 再解析为 JSON,逆向解析极易出错 + +**完整的交付流程:** + +``` +后端流程: + 1. 发送 SSE: { type: 'qper_status', status: 'reflecting', message: '正在生成论文级结论...' } + 2. LLM 完整生成结构化 JSON(非流式) + 3. jsonrepair → JSON.parse → Zod 校验 + 4. 校验通过 → SSE: { type: 'reflection_complete', conclusion: ConclusionReport } + 5. 校验失败 → ConclusionGeneratorService fallback → SSE: { type: 'reflection_complete', ... } + +前端渲染(逐 section 渐入动画): + 1. 收到 qper_status(reflecting) → 显示加载动画 "📝 正在生成论文级结论..." + 2. 收到 reflection_complete → 开始渐入动画: + - 0ms: executive_summary 淡入 + - 300ms: key_findings 逐条滑入 + - 600ms: methodology 淡入 + - 900ms: limitations 淡入 + - 1200ms: recommendations 淡入(如有) + 3. 动画完成 → 显示"一键复制"和"导出 Word"按钮 +``` + +**SSE 事件类型调整(见 5.5 节):** 原 `reflection_stream`(chunk-based)改为 `reflection_complete`(一次性完整推送)。 + +--- + +### Phase Deploy : 工具补齐 + 部署上线(4 天) + +**为什么最后做:** 智能化架构搭好后,补齐剩余工具和部署上线。 + +| # | 任务 | 工时 | 说明 | +|---|------|------|------| +| 1 | R 工具补齐:ANOVA | 3h | `ST_ANOVA_ONE` + Block-based 输出 | +| 2 | R 工具补齐:Fisher | 2h | `ST_FISHER` + Block-based 输出 | +| 3 | R 工具补齐:Wilcoxon | 2h | `ST_WILCOXON` + Block-based 输出 | +| 4 | R 工具补齐:线性回归 | 3h | `ST_LINEAR_REG` + Block-based 输出 | +| 5 | **复合工具 `ST_BASELINE_TABLE`(R 端)** | 4h | 基于 `gtsummary::tbl_summary(by=group_var) %>% add_p()` 封装;输入:`group_var` + `analyze_vars[]`;自动判断连续/分类、正态/非正态、选择 T 检验/Mann-Whitney/卡方/Fisher;R Docker 镜像需新增 `gtsummary`+`gt`+`broom` 依赖 | +| 6 | **`gtsummary` → `report_blocks` 转换层** | 4h | `gtsummary::as_tibble()` 提取结构化表格 → 转换为 `table` block(需处理合并行:分类变量名+各水平值;列头:分组名称);输出需同时包含渲染用 blocks 和结构化 `significant_vars[]`(P<0.05 的变量列表,供后续步骤消费) | +| 7 | **前端三线表渲染增强** | 4h | `DynamicReport.tsx` 的 `table` block 增强:支持 `rowspan`(分类变量合并行)、分组列头(group1 vs group2 vs P 值)、P<0.05 加粗/标星、横向滚动(20+ 变量场景) | +| 8 | 决策表补齐所有工具映射 | 2h | 11 工具全部纳入(含 `ST_BASELINE_TABLE`) | +| 9 | 流程模板补齐 | 2h | 覆盖所有 Goal 类型 + `cohort_study_standard` 模板(见下方) | +| 10 | R Docker 镜像推送 ACR | 1h | 新工具 + `gtsummary` 依赖包含在内 | +| 11 | SAE 部署 + 联调 | 4h | R 服务 + 后端 + 前端 | +| 12 | 端到端测试(12 场景) | 8h | 原 10 场景 + 队列研究全套流程(表1→表2→表3)×2 数据集 | + +--- + +### Phase Q+ : 人机协同增强 — 变量字典 + 变量选择面板(2.5 天) + +**为什么独立分期:** Phase Q 核心目标是证明 LLM 能从自然语言中提取 `[Goal, Y, X, Design]`。将重度前端交互(表格编辑、穿梭框、状态回传)与核心 AI 逻辑耦合,会导致单点阻塞。Phase Q+ 在 Q-P-E-R 主线闭环跑通后再启动,此时:① 已有 AI 基线准确率数据,可量化人机协同的提升价值;② 后端接口已稳定,前端可安全叠加。 + +> **核心哲学:AI 负责统计专业知识,医生负责临床领域知识。** + +``` +定位:Phase Q 的增强层,非阻塞主线交付 +前置依赖:Phase Q 验收通过(LLM 意图解析基线可用) +触发时机:Q-P-E-R 主线闭环跑通后 +``` + +#### 3.10 增强点一:变量数据字典(Data Dictionary) + +**解决什么问题:** 临床数据列名极不规范(`grp`、`Tx`、`SBP`),DataProfile 只有统计特征没有语义信息。LLM 靠猜列名含义,猜错即全链路错误。 + +**交互模式:"AI 先猜,用户微调"** +- 文件上传 + DataProfiler 完成后,后端静默调用 LLM(基于列名 + 前 5 行数据 + 数据类型)猜测每个变量的中文含义 +- 前端推送**变量字典编辑面板**,用户确认或修改 +- 经用户确认的字典成为**黄金上下文(Golden Context)**,后续 Q/P/R 全链路可消费 + +| # | 任务 | 层级 | 工时 | 说明 | +|---|------|------|------|------| +| 1 | `VariableDictService.ts` — LLM 变量含义猜测 | 后端 | 3h | 通过 `PromptService` + `LLMFactory` 调用 LLM,输入列名+前5行+类型,输出 `{ name, guessed_meaning, guessed_role }[]`;Zod 校验 | +| 2 | Prisma schema 扩展 | 后端 | 1h | `SsaFileUpload` 新增 `variable_dictionary: Json?` 字段(或新建 `SsaVariableDictionary` 表),存储用户确认后的字典 | +| 3 | 变量字典 API | 后端 | 1h | `GET /api/v1/ssa/sessions/:id/variable-dict`(获取 AI 猜测结果)+ `PUT`(保存用户修改) | +| 4 | `VariableDictEditor.tsx` 前端组件 | 前端 | 3h | 表格形式:`变量名 | 检测类型 | 前5个值预览 | AI猜测含义(可编辑) | 用户标注角色提示`;支持批量确认 | +| 5 | `QueryService` 集成字典 | 后端 | 1h | `parseQuery` 方法接受可选 `variableDict` 参数,有字典时将 `variable_description` 注入 Prompt 的 `{{variables}}` 中 | + +**验收标准:** +- 上传含 `grp(1,2)` 列的 CSV → AI 猜测"分组变量" → 用户标注"1=新药, 2=安慰剂" → 后续 Q 层正确识别 `grp` 为分组变量 +- 用户跳过字典编辑(直接点"确认")→ 系统正常运行(AI 猜测结果作为默认值) + +#### 3.11 增强点二:变量选择确认面板(Variable Selection) + +**解决什么问题:** LLM 从 100 列中猜 Y/X 变量,可能选错或遗漏关键协变量。医生最清楚哪些是核心指标、哪些必须作为调整变量纳入。 + +**交互模式:AI 推荐 + 医生确认/调整** +- Q 层 LLM 解析完意图后,在进入 P 层之前推送**变量选择确认面板** +- AI 预选的 Y/X 变量已标记,医生可增删、可添加协变量(Confounders) +- 医生确认后的变量集合替代 AI 原始选择,传入 P 层 + +| # | 任务 | 层级 | 工时 | 说明 | +|---|------|------|------|------| +| 6 | `QueryService` 输出扩展 | 后端 | 1h | `ParsedQuery` 增加 `suggested_confounders: string[]`(AI 建议的协变量)+ `all_candidate_vars` 列表供前端渲染 | +| 7 | `VariableSelectionPanel.tsx` 前端组件 | 前端 | 4h | 穿梭框/卡片交互:左侧"可用变量"(全部),右侧"已选变量"分三区(Y/X/协变量);AI 预选项高亮标记;支持拖拽或点击增删 | +| 8 | 确认 API + Context Pruning 衔接 | 后端 | 1h | `POST /api/v1/ssa/sessions/:id/confirm-variables`;用户确认的变量集合覆盖 AI 原始选择,`pruneForPlanner` 以医生确认集合为基准裁剪 | +| 9 | SSE 事件 + 状态回传 | 后端 | 1h | `DICT_EDITING` / `VARIABLE_CONFIRMING` 状态推送 + 前端状态管理集成 | +| 10 | `SSAChatPane` 集成两个面板 | 前端 | 2h | 接收 SSE 事件渲染字典编辑器/变量选择面板,用户操作后回传并恢复流程 | + +**验收标准:** +- AI 推荐 Y=BP, X=Drug → 医生增加协变量 Age, Sex → P 层收到完整的 `[Y=BP, X=Drug, Confounders=[Age, Sex]]` +- 医生不修改直接确认 → 使用 AI 原始选择(零额外成本) + +#### 3.12 Phase Q+ 默认放行策略 + +> **重要:** Phase Q+ 未上线前,系统自动跳过 `DICT_EDITING` 和 `VARIABLE_CONFIRMING` 状态,直接使用 AI 自动解析结果。后端接口契约保持不变。 + +```typescript +// Phase Q+ 开关(feature flag) +const PHASE_Q_PLUS_ENABLED = false; // Phase Q+ 上线后改为 true 或从配置读取 + +// 状态机跳过逻辑 +if (currentStatus === 'profiling_done') { + nextStatus = PHASE_Q_PLUS_ENABLED ? 'dict_editing' : 'pending_intent'; +} +if (currentStatus === 'intent_parsed') { + nextStatus = PHASE_Q_PLUS_ENABLED ? 'variable_confirming' : 'planning'; +} +``` + +**Phase Q+ 工时汇总:** + +| 类型 | 任务 | 工时 | +|------|------|------| +| 后端 | VariableDictService + Schema + API + QueryService集成 + 确认API + SSE | 8h | +| 前端 | VariableDictEditor + VariableSelectionPanel + SSAChatPane集成 | 9h | +| 测试 | 两个面板的端到端验证 | 3h | +| **合计** | | **20h(2.5天)** | + +--- + +## 4. 代码目录结构(渐进式,不做大规模重组) + +> **策略:** 所有新文件放入现有的 `services/` 和 `config/` 目录,而非创建 `query/` `planner/` `reflection/` 子目录。原因:现有 SSA 模块已有大量文件互相引用,子目录化会导致全量 import 路径变更、风险过高、产出为零。用 **文件命名前缀** 区分 QPER 层级即可。 + +``` +backend/src/modules/ssa/ +├── services/ ← 所有服务统一放此目录 +│ ├── QueryService.ts # Q 层:LLM 意图解析 + 追问逻辑(新建) +│ ├── VariableDictService.ts # Q+ 层:LLM 变量含义猜测 + 字典管理(Phase Q+ 新建) +│ ├── DataDiagnosticService.ts # Q 层:数据深度诊断(新建,扩展 DataProfileService) +│ ├── PlannerService.ts # P 层:核心规划逻辑(新建,接收 ParsedQuery) +│ ├── DecisionTableService.ts # P 层:决策表加载 + 四维匹配(新建) +│ ├── FlowTemplateService.ts # P 层:流程模板管理(新建) +│ ├── ReflectionService.ts # R 层:LLM 结论生成(重构自 ConclusionGeneratorService) +│ ├── WorkflowExecutorService.ts # E 层:步骤编排 + SSE(✅ 保持不变) +│ ├── RClientService.ts # E 层:R 引擎调用(✅ 保持不变) +│ ├── WorkflowPlannerService.ts # 兼容层:旧入口,内部委托 QueryService + PlannerService +│ ├── ConclusionGeneratorService.ts # 兼容层:旧入口,内部委托 ReflectionService +│ ├── DataProfileService.ts # 共享:Python 数据质量(✅ 保留) +│ └── DataParserService.ts # 共享:文件解析(✅ 保留) +│ +├── config/ ← 决策表 + 流程模板配置 +│ ├── decision_table.json # P 层配置 +│ └── flow_templates.json # P 层配置 +│ +├── types/ ← 新增 QPER 接口定义 +│ ├── query.types.ts # ParsedQuery 接口 +│ ├── planner.types.ts # AnalysisPlan / AnalysisStep 接口 +│ └── reflection.types.ts # Conclusion 接口 +│ +├── routes/ # API 路由(扩展,不重建) +│ ├── analysis.routes.ts # ✅ 保留 +│ ├── workflow.routes.ts # ✅ 保留 +│ └── config.routes.ts # ✅ 保留(已有决策表/代码库 CRUD) +└── ... +``` + +> **Prompt 不放在本地文件中**,而是通过平台 `PromptService` 存入 `capability_schema.prompt_templates` 数据库表,支持版本管理和 A/B 测试。开发阶段使用 seed 脚本初始化。 + +--- + +## 5. 数据流协议 + +### 5.1 Q → P 接口契约 + +```typescript +interface ParsedQuery { + // 用户意图 + goal: 'difference' | 'association' | 'prediction' | 'description'; + confidence: number; // 0.0 - 1.0 + + // 变量角色 + outcome_var: string; // 结局变量 (Y) + outcome_type: 'continuous' | 'categorical' | 'binary'; + predictor_vars: string[]; // 自变量 (X) + predictor_types: string[]; // 各自变量类型 + + // 实验设计 + design: 'independent' | 'paired'; + + // 数据诊断(Q 层产出) + dataDiagnosis: { + sampleSize: number; + missingRate: Record; + outlierCount: Record; + normalityTests?: Record; + groupBalance?: { groups: string[]; counts: number[] }; + }; + + // 追问状态 + needsClarification: boolean; + clarificationQuestions?: string[]; +} +``` + +### 5.2 P → E 接口契约 + +> **兼容策略:** 新 `AnalysisPlan` 接口必须兼容现有 `WorkflowPlan`(`WorkflowExecutorService` 已在使用),避免 Execute 层改动。`AnalysisStep` 扩展自现有 `WorkflowStep`,新增字段为可选。 + +```typescript +// 现有接口(WorkflowExecutorService 已使用,不可破坏) +interface WorkflowStep { + step_number: number; + tool_code: string; + tool_name: string; + description: string; + params: Record; +} + +// 新增接口(扩展,向后兼容) +interface AnalysisStep extends WorkflowStep { + role?: 'descriptive' | 'primary_test' | 'sensitivity' | 'effect_size'; // 可选,新增 +} + +interface AnalysisPlan { + workflow_id: string; + title: string; + methodology: string; // "因 Y 为连续变量且两组独立,采用独立样本 T 检验" + rationale: string; // "数据满足正态分布假设(Shapiro P=0.15)" + + steps: AnalysisStep[]; // 兼容 WorkflowStep[] + + // 元信息(新增,P 层产出) + primary_tool: string; + fallback_tool?: string; + flow_template_used: string; + + // 决策审计日志(P 层产出 → R 层消费,用于生成方法学说明) + decision_trace: { + matched_rule: string; // "Y=continuous, X=categorical_2, Design=independent → ST_T_TEST_IND" + diagnosis_adjustments: string[]; // ["Shapiro-Wilk P=0.03 < 0.05 → 切换非参数 Mann-Whitney"] + triggered_at: string; // ISO timestamp + }; +} + +// 兼容转换函数(PlannerService 内部使用) +function toWorkflowPlan(plan: AnalysisPlan): WorkflowPlan { + return { + workflow_id: plan.workflow_id, + steps: plan.steps, // AnalysisStep extends WorkflowStep,直接兼容 + // ... + }; +} +``` + +### 5.3 E → R 接口契约 + +```typescript +interface ExecutionResult { + steps: StepResult[]; // 各步骤结果(已有) + report_blocks: ReportBlock[]; // Block-based 输出(Phase E+ 新增) + total_duration_ms: number; +} +``` + +### 5.4 会话状态机(防面条代码) + +> **来源:** 《QPER V3.0 架构审查与工程护航报告》隐患 2 — QPER 流程存在 Clarifier(等用户回复)和 Reflection(错误重试)两处中断/恢复逻辑,用嵌套 if-else + while 会迅速劣化为面条代码。 + +```typescript +enum ExecutionStatus { + PENDING_UPLOAD = 'pending_upload', // 等待数据上传 + PROFILING = 'profiling', // 数据诊断中(Python Tool C) + DICT_EDITING = 'dict_editing', // 🆕 Phase Q+:变量字典编辑中 — ⏸️ 中断,等待用户确认 + PENDING_INTENT = 'pending_intent', // 等待意图解析 + CLARIFYING = 'clarifying', // 追问中 — ⏸️ 中断,等待用户回复 + VARIABLE_CONFIRMING = 'variable_confirming', // 🆕 Phase Q+:变量选择确认中 — ⏸️ 中断,等待用户确认 + PLANNING = 'planning', // 规划中 + PLAN_CONFIRMING = 'plan_confirming', // 等待用户确认 SAP — ⏸️ 中断 + EXECUTING = 'executing', // R 引擎执行中 + REFLECTING = 'reflecting', // 结论生成中 + RETRYING = 'retrying', // 错误修复后重试中(MAX_RETRIES=2) + COMPLETED = 'completed', // 完成 + FAILED = 'failed', // 失败(不可恢复) +} +``` + +**状态持久化:** `ExecutionStatus` 存入 `SsaSession` 表。当状态为 `DICT_EDITING`、`CLARIFYING`、`VARIABLE_CONFIRMING` 或 `PLAN_CONFIRMING` 时,后端立即中断执行并向前端发送交互面板,等待下一次 HTTP 请求(用户操作/确认)后从该状态恢复继续。 + +**状态流转图:** + +``` +PENDING_UPLOAD → PROFILING → [DICT_EDITING]* → PENDING_INTENT → CLARIFYING (可选,循环) + ↓ + [VARIABLE_CONFIRMING]* + ↓ + PLANNING → PLAN_CONFIRMING (可选) + ↓ + EXECUTING → REFLECTING → COMPLETED + ↑ ↓ + └── RETRYING (MAX 2次) + ↓ (超限) + FAILED + +* [方括号] 表示 Phase Q+ 新增状态,Phase Q+ 未上线时自动跳过(feature flag 控制) +``` + +### 5.5 QPER 级 SSE 事件类型(全链路状态推送) + +> **来源:** 《QPER V3.0 架构审查与工程护航报告》隐患 3 — R 层重试过程可能耗时 15-20 秒,用户只看到转圈会认为系统死了。"展示 AI 工作过程"是信任建立的关键。 + +当前 SSE 只覆盖 E 层步骤进度。扩展为 QPER 全链路状态推送: + +```typescript +// SSE 事件类型扩展(v8.0 更新:reflection_stream → reflection_complete) +type SSAEvent = + | { type: 'qper_status'; status: ExecutionStatus; message: string } // 状态跃迁 + | { type: 'step_progress'; step: number; total: number; ... } // E 层步骤进度(已有) + | { type: 'clarification'; questions: ClarificationOption[] } // 追问卡片 + | { type: 'plan_ready'; plan: AnalysisPlan } // SAP 确认 + | { type: 'reflection_complete'; conclusion: ConclusionReport } // R 层完整结论(一次性推送,非字符流) + | { type: 'error'; code: string; message: string } // 错误 + +// 前端渲染效果示例: +// [Query] 🧠 正在理解您的分析意图... +// [Query] ✅ 识别到:差异比较 | Y=血压 | X=用药组 +// [Planner] 📋 正在规划分析方案... +// [Planner] ✅ 已生成 3 步分析计划(描述统计 → T检验 → Mann-Whitney) +// [Executor] 🔄 步骤 1/3:描述统计... +// [Executor] ✅ 步骤 1/3 完成 [0.8s] +// [Executor] ❌ 步骤 2/3 失败:变量存在完全共线性 +// [Executor] ⚠️ 运算引擎遇到异常:数据存在严重共线性(E_COLLINEARITY) +// [Executor] 🔄 步骤 3/3:Mann-Whitney... +// [Executor] ✅ 步骤 3/3 完成 [0.6s] +// [Reflection] 📝 正在生成论文级结论...(qper_status: reflecting) +// [Reflection] ✅ 结论生成完成(reflection_complete → 前端逐 section 渐入动画) +``` + +--- + +## 6. 工时与里程碑 + +| Phase | 名称 | 工时 | 日历天 | 里程碑 | 变更说明 | +|-------|------|------|--------|--------|---------| +| **E+** | Block-based 标准化 | 15.5h | 2.5天 | R 输出标准化,前端动态渲染 | 后端透传需含 report_blocks 全链路验证 | +| **Q** | Query 层(LLM 意图) | 33h | 5天 | 用户说"有没有效"即可分析 | v4.0 +1.5h:DataProfile 会话级缓存(0.5h) + Intent Prompt Few-Shot(1h);v3.0 +1h:Context Pruning | +| **P** | Planner 层(决策表+模板) | 23h | 4天 | 完整分析流程(非单方法) | v3.0 +1h:decision_trace 决策审计日志输出 | +| **R** | Reflection 层(LLM 结论) | 22h | 3天 | 论文级结论,可直接用于论文 | v3.0 +2h:统计量槽位注入反幻觉;v8.0 -1h:完整重试推迟至 Deploy,Phase R 仅做错误分类映射(1h);+0h:敏感性冲突 Prompt + Zod 校验 + 完整 JSON 交付(融入现有任务工时) | +| **Deploy** | 工具补齐 + 部署 | 37h | 5.5天 | 11 工具上线(含复合工具),生产环境可用 | v6.0 +12h:ST_BASELINE_TABLE(4h) + gtsummary→blocks 转换(4h) + 前端三线表增强(4h);端到端测试 +2h | +| **主线合计** | | **130.5h** | **~20天** | **Q-P-E-R 主线闭环** | v6.0 Deploy +12h;v4.0 +1.5h;v3.0 +4h;v8.0 -1h | +| --- | --- | --- | --- | --- | --- | +| **Q+** | 人机协同增强 | 20h | 2.5天 | 变量字典 + 变量选择面板 | v5.0 新增:医生注入临床领域知识,AI→人机协同 | +| **总计** | | **150.5h** | **~22.5天** | **Q-P-E-R + 人机协同 + 队列研究全套** | 主线闭环后启动 Phase Q+ | + +### 里程碑时间线 + +``` +Week 1 ────────────────────────────────── + Day 1-2: Phase E+ (Block-based) + Day 3-5: Phase Q (Query 层前半:Prompt 注册 + QueryService) + +Week 2 ────────────────────────────────── + Day 6-7: Phase Q (Query 层后半:DataDiagnostic + 前端追问 + 联调) + Day 8-11: Phase P (Planner 层) + +Week 3 ────────────────────────────────── + Day 12-14: Phase R (Reflection 层) + Day 15-19: Phase Deploy (原子工具补齐 + ST_BASELINE_TABLE 复合工具 + 部署) + Day 20: 队列研究端到端验证(Table 1→2→3 全流程) + +Week 4 ────────────────────────────────── + ✅ Q-P-E-R 主线闭环上线(里程碑 1:含队列研究全套分析能力) + Day 20: 收集 AI 基线准确率数据(无人机协同下的意图识别准确率) + +Week 4-5 ──────────────────────────────── + Day 19-21: Phase Q+ (变量字典 + 变量选择面板) + ✅ 人机协同增强上线(里程碑 2) + 对比量化:人机协同 vs 纯 AI 的准确率提升 +``` + +--- + +## 7. 风险管理 + +| 风险 | 概率 | 影响 | 应对 | +|------|------|------|------| +| LLM 意图提取准确率不足 | 中 | 高 | 低置信度时追问,不猜测;保留正则兜底(`WorkflowPlannerService.parseUserIntent` 作为 fallback) | +| 决策表覆盖率不足 | 低 | 中 | 先覆盖 10 工具,后续 Excel 可热加载扩展;评估使用已有 `SsaDecisionTable` 数据库表 | +| LLM 结论"幻觉"虚构统计量 | 中 | 高 | **统计量槽位注入机制(3.9 节)**:所有数值通过 `{{slot}}` 从 R 输出渲染,LLM 被剥夺生成数值 Token 的权限;Prompt 中明确标注"不得修改、四舍五入或重新表述任何数字" | +| Reflection 延迟过高(LLM 调用) | 中 | 中 | 通过平台 `StreamingService` 流式输出,用户先看到数字结果,结论异步加载 | +| Q→P→E 串联复杂度 | 低 | 中 | 每层独立可测试,接口契约明确 | +| **新增:LLM 服务不可用** | 中 | 高 | Q 层降级到正则匹配,R 层降级到规则拼接(`ConclusionGeneratorService`),系统功能不中断 | +| **新增:Prisma 迁移遗漏** | 低 | 高 | 新增字段(如 `report_blocks` 存储)需通过 `npx prisma migrate dev` 正式迁移,禁止 `db push --force-reset`;每个 Phase 完成时检查 schema 变更 | +| **新增:AnalysisPlan 接口破坏 Execute 层** | 低 | 高 | `AnalysisStep extends WorkflowStep`,保持向后兼容;E 层不做任何接口改动 | +| **新增:旧组件过早删除** | 中 | 中 | `DescriptiveResultView` 等现有组件(2026-02-20 新建)在 Block-based 未完全验证前保留作为 fallback,不提前删除 | +| **新增:R 引擎运行时崩溃(Runtime Crash)** | 中 | 中 | 区分"统计降级"(R 内部 if-else 处理)与"运行时崩溃"(NA 值/列名错误/Fatal Error);后者通过错误分类映射表转化为友好提示,Phase R 不做跨层 Self-healing,完整重试推迟至 Phase Deploy | +| **新增:敏感性分析结论冲突** | 中 | 高 | 主分析与敏感性分析显著性不一致时,Reflection Prompt 硬编码冲突处理准则:必须在局限性中报告,严禁强行拼凑显著性(3.10 节) | +| **新增:LLM Reflection 输出结构残缺** | 中 | 中 | `jsonrepair` + Zod Schema 三层防御(3.11 节);校验失败自动降级到 `ConclusionGeneratorService` 规则拼接 | + +### 7.1 回退策略(Fallback Plan) + +| 层级 | 正常路径 | 降级路径 | 触发条件 | +|------|---------|---------|---------| +| **Q 层** | `QueryService`(LLM 意图解析) | `WorkflowPlannerService.parseUserIntent()`(正则匹配) | LLM 超时/不可用/解析失败 | +| **P 层** | `PlannerService`(决策表+模板) | `WorkflowPlannerService.generateSteps()`(硬编码 if/else) | 决策表无匹配 | +| **E 层** | `WorkflowExecutorService`(不变) | 错误分类映射 → 友好用户提示 | R 引擎运行时崩溃(NA/列名/Fatal Error) | +| **R 层** | `ReflectionService`(LLM 论文结论) | `ConclusionGeneratorService`(规则拼接) | LLM 超时/不可用/**Zod 校验失败** | +| **前端** | `DynamicReport`(Block-based 渲染) | 现有自定义渲染组件 | `report_blocks` 为空时 | + +> **原则:** 每个 QPER 层都有明确的降级路径,确保 LLM 不可用时系统仍然可用(退化为 Phase 2A 水平)。R 层新增 Zod 校验失败作为降级触发条件——LLM 输出结构不完整时,自动切回规则拼接,前端始终收到有效的 `ConclusionReport` 结构。 + +--- + +## 8. 验收场景 + +### 8.1 核心验收(5 个场景) + +| # | 用户输入 | 期望 Q 输出 | 期望 P 输出 | 期望 R 输出 | +|---|---------|------------|------------|------------| +| 1 | "比较两组血压" | goal=difference, design=independent | 描述统计 → T检验 → Mann-Whitney | "两组差异显著(P<0.001),中等效应(d=0.52)" | +| 2 | "分析 sex 与 Yqol 的关系" | goal=association, Y=Yqol(categorical), X=sex | 描述统计 → 卡方检验 | "性别与 Yqol 存在显著关联(χ²=8.3, P=0.004)" | +| 3 | "age、smoke 对 Yqol 的影响" | goal=prediction, Y=Yqol(binary) | 描述统计 → Logistic 回归 | "Logistic 回归显示 smoke 是显著预测因子(OR=2.1, P=0.03)" | +| 4 | "描述一下数据" | goal=description | 描述统计 | 各变量的集中趋势和离散程度汇总 | +| 5 | "有没有效" | confidence<0.7 → 追问 | 等待用户澄清后规划 | — | + +### 8.2 智能化对比(改造前 vs 改造后) + +| 场景 | 改造前 | 改造后 | +|------|--------|--------| +| 用户说"有没有效" | ❌ 无法理解 | ✅ 追问后正确规划 | +| 数据不正态 | 🟡 R 内部降级,用户不知 | ✅ 规划阶段就选非参数方法,告知理由 | +| 分析结果 | P=0.015 + 数字表格 | 论文级结论:"两组差异具有统计学意义..." | +| 分析流程 | 1 个方法 | 描述统计 → 主分析 → 敏感性分析 | +| Word 报告 | 表格 + 数字 | 完整论文段落 + 方法学说明 | + +--- + +## 9. 与旧计划的关系 + +| 旧计划内容 | 处理方式 | 理由 | +|-----------|---------|------| +| 配置中台(Excel 导入体系) | **延后** | 10 个工具用 JSON 配置足够,100+ 工具时再做 | +| 咨询模式(无数据 SAP) | **延后** | 独立功能,不影响核心智能分析 | +| 决策表概念 | **保留并简化** | 从 Excel 配置中台简化为内置 JSON | +| Brain-Hand 命名 | **升级为 Q-P-E-R** | 更精确地描述四层职责 | +| 10 工具目标 | **扩展为 11 工具** | 原 10 个原子工具 + 1 个复合工具 `ST_BASELINE_TABLE`(v6.0) | +| SAE 部署 | **保留** | Phase Deploy 中完成 | + +--- + +**文档维护者:** SSA 架构团队 +**创建日期:** 2026-02-20 +**最后更新:** 2026-02-21(v8.0 — Phase R 架构增强) +**下一步行动:** Phase E+/Q/P 已完成,按 R1→R7 顺序启动 Phase R 开发 + +### 变更日志 + +| 版本 | 日期 | 变更内容 | +|------|------|---------| +| v1.0 | 2026-02-20 | 初版:QPER 架构设计 + 5 Phase 开发计划 | +| v2.0 | 2026-02-21 | 代码审查后修订:① 核心原则新增"复用平台通用能力层";② ReflectionService 修正为重构;③ 目录结构改为渐进式平铺;④ 伪代码使用 LLMFactory + PromptService;⑤ AnalysisStep extends WorkflowStep;⑥ 新增已有数据库资产盘点;⑦ 新增全链路回退策略表;⑧ 工时 108.5h→114h | +| v3.0 | 2026-02-21 | 外部调研审查后修订(综合4份报告):① 架构红线(禁止 LangGraph/AutoGen/LLM 生成 R 代码/多智能体辩论);② Context Pruning 上下文裁剪;③ 封闭式追问;④ decision_trace 决策审计日志;⑤ 重试短路机制;⑥ 统计量槽位注入反幻觉;⑦ Tracer Bullet 探路测试;⑧ 工时 114h→118h | +| v4.0 | 2026-02-21 | 工程护航修订(依据《QPER V3.0 架构审查与工程护航报告》):① DataProfile 会话级缓存;② 显式状态机(ExecutionStatus 14 态);③ QPER 级 SSE 全链路状态推送;④ Intent Prompt Few-Shot 示例;⑤ 工时 118h→119.5h | +| v5.0 | 2026-02-21 | 人机协同增强修订(依据《架构与产品委员会综合评估报告》):① 新增 Phase Q+(变量字典 + 变量选择面板,20h/2.5天);② 状态机扩展 DICT_EDITING + VARIABLE_CONFIRMING(含 feature flag 自动跳过);③ Intent Prompt 增加忽略非分析变量指令;④ 总工时 119.5h→139.5h | +| v6.0 | 2026-02-21 | 复合工具扩展修订(依据队列研究 Table 1-3 终态验证 +《SSA-Pro 架构诊断与复合工具扩展方案》审查):① Phase Deploy 新增复合 R 工具 `ST_BASELINE_TABLE`(基于 `gtsummary` 封装,一次遍历所有变量 + 自动选方法 + 合并出表,同时覆盖表1基线比较和表2单因素筛选);② 新增 `gtsummary` → `report_blocks` 转换层(4h,含合并行/分组列头的结构化提取);③ 前端三线表渲染增强(4h,`DynamicReport.tsx` 支持 rowspan/colspan/P值标星/横向滚动);④ 新增 `cohort_study_standard` 流程模板(表1→表2→表3 三步流程,表3自变量由用户确认而非自动 P<0.05 筛选);⑤ 决策表新增 `cohort_study` 场景行;⑥ Q 层 Intent Prompt `goal` 扩展支持 `cohort_study` 值(识别"出一套完整报告"等触发词);⑦ 工具总数从 10 扩展为 11(10 原子 + 1 复合);⑧ Phase Deploy 工时 25h→37h(+12h),总工时 139.5h→151.5h | +| v7.0 | 2026-02-21 | Phase P 架构增强(进入开发前的设计审查):① **Expected/Actual 双层审计日志** — P 层生成 PlannedTrace(策略),E 层 R trace_log 提供 Actual Trace(事实),R 层合并两者生成方法学说明;P 层绝不做正态性预检;② **Repository 模式** — DecisionTableService 通过 `IDecisionTableRepo` 接口解耦,初期 JSON 实现,后期可切 DB,核心业务逻辑零改动;③ **参数检验优先原则** — 决策表 Primary 始终为参数检验(效力更高),Fallback 为非参数(安全网),新增 `switch_condition` 列描述触发条件;④ **EPV 变量上限防护** — 队列研究 Table 3 采用"全量推入 + EPV 截断 + 免责声明"策略,防止过拟合和 R 引擎崩溃;⑤ **流程模板合并** — `two_group_comparison` + `association_analysis` 合并为 `standard_analysis`,最终 4+1 模板 | +| v7.1 | 2026-02-21 | **新增核心原则"领域知识可配置化"** + Phase P 配置化基础设施:① **核心原则第 6 条** — "QPER 四层的所有领域知识由统计学方法学团队配置,不写死在代码中","可配置"作为贯穿所有 Phase 的架构约束,IT 团队搭引擎、方法学团队填内容;② **Zod Schema 严格校验** — 方法学团队编辑 JSON 时的拼写/结构错误在加载时立即拦截;③ **热更新 API** — `POST /reload-config` 无需重启服务即可生效,校验失败保留旧配置不影响线上;④ **领域文件物理拆分** — `tools_registry.json`(E 层)、`decision_tables.json`(P 层)、`flow_templates.json`(P 层)、`narrative_templates.json`(R 层),每文件有明确的方法学团队所有者;⑤ Phase P 后端任务新增任务 0(ConfigLoader 基础设施,2h)+ 任务 1(工具注册表,2h),总任务数 7→9 | +| v8.0 | 2026-02-21 | **Phase R 架构增强(进入开发前的设计审查,依据架构委员会建议)**:① **敏感性分析结论冲突 Prompt 策略(3.10 节)** — 主分析与敏感性分析显著性不一致时,Reflection Prompt 硬编码冲突处理准则,严禁强行拼凑显著性,必须在局限性中报告稳健性问题;② **Zod Schema 强校验 LLM 输出(3.11 节)** — 延续 Phase Q 的 `jsonrepair` + Zod 三层防御范式,校验失败自动降级到 `ConclusionGeneratorService` 规则拼接;③ **Reflection 输出交付策略调整(3.12 节)** — 不做字符级流式推送,采用"后端完整收集 + Zod 校验 + 一次性 SSE 推送 `reflection_complete` + 前端逐 section 渐入动画"方案,原 `reflection_stream` 事件改为 `reflection_complete`;④ **运行时崩溃优雅处理(3.8 节重构)** — 区分"统计降级"与"运行时崩溃",Phase R 仅做错误分类映射(R 报错关键词→友好用户提示),完整重试短路机制(MAX_RETRIES + 参数级修复 + 方法级降级)推迟至 Phase Deploy;⑤ **ReflectionService 伪代码更新** — 加入 Zod Schema 定义、`qper_status` 状态推送、`reflection_complete` SSE 事件、Zod 校验失败降级逻辑;⑥ **风险管理表新增 3 行** — R 运行时崩溃、敏感性分析冲突、LLM 输出结构残缺;⑦ **回退策略表更新** — R 层触发条件新增"Zod 校验失败",E 层新增运行时崩溃处理路径;⑧ Phase R 工时 23h→22h(-1h:重试机制简化) | diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER V3.0 架构审查与工程护航报告.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER V3.0 架构审查与工程护航报告.md new file mode 100644 index 00000000..29b18e93 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER V3.0 架构审查与工程护航报告.md @@ -0,0 +1,69 @@ +# **架构委员会独立审查报告:SSA-Pro QPER 智能化主线 (V3.0)** + +**审查对象:** 《10-QPER架构开发计划-智能化主线.md》(v3.0) + +**审查时间:** 2026-02-21 + +**总体评级:** 🌟 **S+ 级 (极度卓越,可作为创业公司 AI Agent 标杆)** + +**核心裁决:** 绿灯放行(Green Light)。该计划完美融合了学术界前沿理论与极简工程实践,准予立即进入编码攻坚阶段。 + +## **一、 为什么这份计划能拿 S+?(三大高光决策)** + +1. **“断舍离”的顶级理解 (延后配置中台)**: + 在只有 10 个工具的 MVP 阶段,强行开发一套 Excel 导入和专家配置后台是极其浪费资源的。直接在 Node.js 中用 10 个静态 JSON 对象来充当“决策表”,**把工时从 2 周压缩到了 2 天**,极大地加速了核心引擎的验证。 +2. **Q 层的结构化降维 (防幻觉槽位)**: + 让 IntentParser 只输出 goal, y, x, design 这四个核心维度,而不是直接输出统计方法。这彻底阻断了 LLM “瞎编统计方法”的可能,把智能体最容易出错的环节卡死了。 +3. **清晰的 5 大验收场景 (TDD 导向)**: + 文档最后给出的 5 个核心验收场景(差异、相关、预测、描述、追问)极其精准。这相当于给测试团队(QA)和 Prompt 工程师画好了靶子,开发不再是盲人摸象。 + +## **二、 必须预先防范的 3 个工程隐患 (Hidden Engineering Risks)** + +计划的宏观架构已经无懈可击,但在具体写 Node.js 代码时,请务必规避以下三个暗礁: + +### **🚨 隐患 1:DataProfiler 的“重复运算”与资源浪费** + +* **设计现状**:在 Q 层,IntentParser \-\> DataProfiler \-\> Clarifier。 +* **物理现实**:DataProfiler 需要调用 Python 的 Tool C 去解析 20MB 的 CSV。如果用户在同一个会话里,先问了“比较血压”(触发一次 Profiler),又接着问“那年龄呢?”(又触发一次 Profiler),会导致极大的延迟和服务器 CPU 浪费。 +* **架构强制要求 (Caching)**: + 必须在 Node.js 的 Session 级别实现 **DataProfile 缓存**。 + 用户上传文件后,DataProfiler **只执行一次**,并将结果(轻量级 Schema JSON)存入 Redis 或内存 Session 中。后续的 Q 层循环直接读取缓存,实现毫秒级响应。 + +### **🚨 隐患 2:QPER 的“异步状态机”面条代码风险 (Spaghetti Code)** + +* **设计现状**:系统存在 Clarifier (等用户回复) 和 Reflection (捕获异常重试) 两种中断/循环逻辑。 +* **物理现实**:如果纯用 if-else 和嵌套 while 循环来写这种带“人机交互中断(HITL)”的工作流,Node.js 代码会迅速劣化为难以维护的面条代码。 +* **架构强制要求 (State Machine)**: + 后端开发必须采用**显式状态机 (Explicit State Machine)** 来管理会话状态。定义一个标准的 ExecutionStatus 枚举: + PENDING\_INTENT \-\> CLARIFYING \-\> PLANNING \-\> EXECUTING \-\> REFLECTING \-\> COMPLETED。 + 当状态为 CLARIFYING 时,立刻中断执行并向前端发送卡片,等待下一次 HTTP 请求恢复状态。 + +### **🚨 隐患 3:UI 感知盲区 (The UX Blackhole during Reflection)** + +* **设计现状**:R 层(Reflection)在后台捕获错误、修改参数、重新执行。 +* **物理现实**:这个过程可能长达 15-20 秒。如果在双屏 V8 UI 上,用户只看到一个 Spinner 在转,他们会以为系统死机了。 +* **架构强制要求 (SSE Trace Streaming)**: + Node.js 的编排器必须将 Q-P-E-R 的每一次状态跃迁,通过 **SSE (Server-Sent Events)** 推送给前端。 + *前端必须能渲染出这样的日志*: + \[Executor\] 正在计算 T 检验... + \[Executor\] ❌ 失败:变量存在完全共线性。 + \[Reflection\] 🧠 AI 正在反思并修正分析计划... + \[Executor\] 🔄 重新执行:已剔除共线变量... + 这种“展示 AI 工作和纠错过程 (Show your work)”的体验,是智能体产品的核心爽点。 + +## **三、 对 Prompt 工程师的战术指导** + +由于你们取消了配置中台,所有的智能压力都来到了 Prompt 工程师肩上。请遵循以下最佳实践: + +1. **Prompt 文件化隔离**: + 千万不要把长达百行的 Prompt 用反引号 (\`\`) 直接硬编码在 .ts 业务代码里!请在项目中建立一个 src/prompts/ 文件夹,将 Prompt 写在 .md 文件中(如 intent\_parser.md),利用 fs.readFileSync 运行时加载。这方便以后无缝迁移到数据库配置中心。 +2. **Few-Shot 示例至高无上**: + 在 IntentParser 的 Prompt 中,不要花大量篇幅解释什么是“差异”,直接给它 10 个经典的医生问句和对应的 JSON 答案。**在医疗统计领域,10 个高质量的 Few-Shot,胜过 1000 字的逻辑说教。** + +## **四、 结语** + +你们的 V3.0 计划展现了极高的业务理解力和工程克制力。你们没有被“大厂花哨的架构”带偏,而是用最务实的手段构建了最核心的智能壁垒。 + +**请带着这份审查报告召开项目启动会(Kick-off Meeting)。** + +让前端准备对接 SSE 状态流,让后端搭好状态机框架,让 R 工程师专注跑通工具。**SSA-Pro 的第一场硬仗,可以开始了!** \ No newline at end of file diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER架构审查与工程避坑指南.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER架构审查与工程避坑指南.md new file mode 100644 index 00000000..c4ca3c37 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/QPER架构审查与工程避坑指南.md @@ -0,0 +1,90 @@ +# **架构委员会独立审查报告:QPER 智能化主线开发计划** + +**审查对象:** 《10-QPER架构开发计划-智能化主线.md》 + +**审查时间:** 2026-02-20 + +**总体评级:** 🌟 **S级 (战略与战术高度统一)** + +**核心裁决:** 完全同意将此文档作为 MVP 的绝对主线。该架构已达到当前医疗 AI Agent 的工业界最佳实践水平。 + +## **一、 值得全团队起立鼓掌的 3 大亮点 (What You Did Exceptionally Well)** + +### **1\. 极其克制的 MVP 范围管理 (The Power of 7\)** + +放弃 100 个工具,死磕 7 个最核心的工具(描述、T检验、秩和、卡方、相关、线回、Logistic)。 + +**架构师点评:** 这是最英明的决定。这 7 个工具覆盖了 80% 的临床医学基础论文需求。如果 QPER 架构连这 7 个都跑不顺,跑 100 个只会是灾难。 + +### **2\. P层(规划层)的“规则先行”策略 (Rule-First Strategy)** + +在 Planner 中设计了 规则匹配 (决策表) \-\> LLM 兜底 的机制。 + +**架构师点评:** 这是对抗大模型“幻觉”的杀手锏。医学统计是有硬性定理的(比如:Y是二分类,就必须用 Logistic 回归,容不得 AI 自由发挥)。用硬编码的决策表管住核心逻辑,用 LLM 处理模糊边缘,完美平衡了“严谨”与“智能”。 + +### **3\. Q层(理解层)引入 Tool C (DataProfiler)** + +**架构师点评:** 将原本纯后端的意图识别,与真实的物理数据探测(Python Tool C)结合。这意味着 AI 不再是“盲人摸象”,它在写计划前,就已经知道了数据的缺失率、极值和真实分布。 + +## **二、 必须警惕的 3 个工程暗礁 (Critical Issues to Address)** + +虽然架构图很完美,但在写代码时,以下三个地方极容易导致系统崩溃或用户体验翻车: + +### **🚨 暗礁 1:Q层到 P层的“Token 爆炸” (The Context Window Trap)** + +* **计划现状**:Query 层输出 dataProfile,并传给 Planner。 +* **致命隐患**:如果用户的 CSV 有 100 列,Tool C 跑出来的 dataProfile JSON 可能会长达 50KB。如果把这 100 列的详细信息(均值、方差、频数分布)全部塞进 Planner (DeepSeek) 的 System Prompt 里,不仅会导致响应极慢,还会让 LLM 发生“注意力涣散(Lost in the middle)”。 +* **修正建议**: + 在 DataProfiler 和 Planner 之间加一个 **“按需截取 (Lazy Fetch / Pruning)”** 逻辑。 + * Planner 只需要看 Schema(列名、数据类型)。 + * 只有当 IntentParser 明确识别出用户想分析 Age 和 BP\_Change 时,后端才从完整的 dataProfile 中提取这**两列**的详细统计特征,喂给 Planner。 + +### **🚨 暗礁 2:R层(反思层)的“无限死循环” (The Infinite Loop of Doom)** + +* **计划现状**:遇到 R 报错 \-\> AutoFixer 修改参数 \-\> 重新 Execute。 +* **致命隐患**:如果报错是因为“奇异矩阵(Singular Matrix,多重共线性导致)”,LLM 可能会盲目地反复调整 conf.level 或换个无关紧要的参数重试,导致系统在后台疯狂请求 API,最后超时。 +* **修正建议**: + 必须在 Node.js 的 orchestrator 中建立**严格的状态机与短路机制**: + 1. 强制设定 MAX\_RETRIES \= 2。 + 2. 如果 R 返回的错误包含 system is computationally singular 或 not enough observations,**直接阻断**,跳过 AutoFixer,直接交由 Critic 生成一封“诊断失败报告”给用户(如:“您的数据存在严重共线性,建议删除X变量”)。 + +### **🚨 暗礁 3:Clarifier (澄清器) 的“傻瓜式连问”体验** + +* **计划现状**:信心度 \<0.7 时,追问澄清。 +* **致命隐患**:如果用户输入“帮我看看”,AI 问“你的目标是?”;用户回“看血压”,AI 又问“你想用什么方法?”…… 这种填表式的追问会让医生崩溃。 +* **修正建议**: + Clarifier 必须是 **“带选项的封闭式提问”**,而不是开放式聊天。 + * **错误示范**:“请问您想分析哪两列?” + * **正确示范**(配合前端 UI):“我发现数据包含\[年龄\]和\[血压\],您是想做:👉\[差异比较\] 👉\[相关性分析\]?”(前端渲染为可点击的快捷 Tag)。 + +## **三、 架构师对开发顺序的微调建议 (Roadmap Tweaks)** + +你们的 3 个 Phase 划分很合理,但我建议在内部执行时,微调一下测试策略: + +### **🛠️ 建议:采用“硬编码探路法 (Hardcoded Tracer Bullet)”** + +在 **Phase 1** 开发 Q 和 P 时,**千万不要等前端界面和真实的 R 服务!** + +后端开发人员应该写一个简单的 test.js 脚本: + +// 模拟前端传入 +const userQuery \= "看看吃药对血压的影响"; +const mockDataProfile \= { columns: { "Drug": "categorical", "BP": "numeric" } }; + +// 测 Q 层 +const parsed \= await IntentParser.run(userQuery, mockDataProfile); +console.log(parsed); // 应该输出: { goal: "difference", Y: "BP", X: "Drug" } + +// 测 P 层 +const plan \= await Planner.run(parsed); +console.log(plan); // 应该命中决策表,输出: ST\_T\_TEST\_IND + +**为什么这样做?** 只要后端用纯 JSON 把 Q 和 P 串通了,你们的智能化就成功了 80%。剩下的 R 执行和 UI 渲染只是体力活。 + +## **四、 最终结论** + +这是一份可以**直接拿去向管理层汇报、向投资人路演**的技术蓝图。 + +QPER 不仅仅是一个架构,它是 AI Data Scientist 的标准灵魂。 + +请团队立刻以此文档为唯一灯塔,废弃之前所有零散的 MVP 规划,**全力冲刺 QPER 第一版!** 🚀 \ No newline at end of file diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro MVP 智能化增强指南.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro MVP 智能化增强指南.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro MVP 智能化增强指南.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro MVP 智能化增强指南.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro Prompt体系与专家配置边界梳理.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro Prompt体系与专家配置边界梳理.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro Prompt体系与专家配置边界梳理.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro Prompt体系与专家配置边界梳理.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro R 服务代码深度审查报告.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro R 服务代码深度审查报告.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro R 服务代码深度审查报告.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro R 服务代码深度审查报告.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro V1.2 终极审查与发令报告V3.0.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro V1.2 终极审查与发令报告V3.0.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro V1.2 终极审查与发令报告V3.0.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro V1.2 终极审查与发令报告V3.0.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 前端 UI 改进计划审查报告.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 前端 UI 改进计划审查报告.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 前端 UI 改进计划审查报告.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 前端 UI 改进计划审查报告.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 前端UI改进计划-审查回应.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 前端UI改进计划-审查回应.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 前端UI改进计划-审查回应.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 前端UI改进计划-审查回应.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 动态结果渲染与通信协议规范.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 动态结果渲染与通信协议规范.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 动态结果渲染与通信协议规范.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 动态结果渲染与通信协议规范.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 方案深度审查与风险评估报告 V2.0.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 方案深度审查与风险评估报告 V2.0.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 方案深度审查与风险评估报告 V2.0.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 方案深度审查与风险评估报告 V2.0.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 方案深度审查与风险评估报告.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 方案深度审查与风险评估报告.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 方案深度审查与风险评估报告.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 方案深度审查与风险评估报告.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 智能化演进路径评估报告.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 智能化演进路径评估报告.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 智能化演进路径评估报告.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 智能化演进路径评估报告.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 智能化演进阶梯.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 智能化演进阶梯.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 智能化演进阶梯.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 智能化演进阶梯.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 架构审查反馈与智能化路径讨论.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 架构审查反馈与智能化路径讨论.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro 架构审查反馈与智能化路径讨论.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 架构审查反馈与智能化路径讨论.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 架构诊断与复合工具扩展方案.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 架构诊断与复合工具扩展方案.md new file mode 100644 index 00000000..f1553752 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro 架构诊断与复合工具扩展方案.md @@ -0,0 +1,109 @@ +# **SSA-Pro 架构诊断与复合工具扩展方案** + +**文档性质:** 架构诊断总结与 Phase Deploy 扩充计划 + +**诊断对象:** 经典队列研究分析流程(Table 1, Table 2, Table 3)的实现缺口 + +**核心结论:** 🌟 **诊断完全正确!** 架构与 AI 均无问题,核心缺口在于缺少“多变量聚合”的**复合 R 工具 (Macro-Tool)**。采用 gtsummary 填补此缺口,是四两拨千斤的神来之笔。 + +## **1\. 团队诊断结果复盘与高度认可** + +团队提出的诊断击中了医疗 AI 统计分析的最核心痛点:**“原子工具”与“临床真实工作流”的错位。** + +* **我们当前的误区**:把 10 个工具全部做成了“验证单一假设”的原子工具(如:只查一个变量的 T 检验)。 +* **临床的真实诉求**:“一键出 Table 1”。一张表里包含了 20 个变量的描述、分布检验、T 检验、Wilcoxon、卡方检验,甚至 Fisher 检验。 +* **破局解法**:新建复合工具 ST\_BASELINE\_TABLE,把复杂的循环、判断、拼表工作,全部**下沉到 R 引擎内部去完成**,释放 LLM 的规划压力。 + +## **2\. 核心利器:ST\_BASELINE\_TABLE 的设计与实现** + +团队提出的用 R 语言的 gtsummary 包来实现表 1 和表 2,是极其专业的选择。gtsummary 是目前全球医学统计界公认的“顶流制表神器”。 + +### **2.1 R 端开发要求 (仅需 6-8 小时)** + +R 开发工程师只需做一层很薄的 Wrapper,gtsummary::tbl\_summary() 会自动完成绝大部分工作: + +\# ST\_BASELINE\_TABLE 伪代码示例 +library(gtsummary) + +run\_tool \<- function(input) { + df \<- load\_data(input$data\_source) + + \# 核心一行代码:按分组变量 (by) 统计所有传入的分析变量 + \# gtsummary 会自动判断是连续还是分类,自动选 T检验 或 卡方 + table\_obj \<- df %\>% + select(all\_of(c(input$params$group\_var, input$params$analyze\_vars))) %\>% + tbl\_summary(by \= input$params$group\_var, missing \= "ifany") %\>% + add\_p() \# 自动执行统计检验并添加 P 值 + + \# 将 gtsummary 对象转换为标准前端 report\_blocks + \# ... +} + +**能力覆盖**: + +* ✅ 自动处理连续与分类变量。 +* ✅ 自动处理正态与非正态的检验降级。 +* ✅ 自动计算期望频数并切换 Fisher。 +* ✅ 完美对应 **表1(基线比较,group\_var=暴露因素)** 和 **表2(单因素筛选,group\_var=结局指标)**。 + +## **3\. 规划层 (Planner) 的流程模板升级** + +基于这个强大的复合工具,我们立刻可以在 flow\_templates.json 中配置一个秒杀市面所有竞品的\*\*“队列研究全家桶”\*\*。 + +"cohort\_study\_standard": { + "name": "经典队列研究全套分析 (Table 1-3)", + "steps": \[ + { + "order": 1, + "role": "baseline\_table", + "tool": "ST\_BASELINE\_TABLE", + "name": "表1: 组间基线特征比较", + "params\_mapping": { + "group\_var": "{{exposure\_var}}", // 暴露/分组变量 + "analyze\_vars": "{{all\_covariates}}" + } + }, + { + "order": 2, + "role": "univariate\_screen", + "tool": "ST\_BASELINE\_TABLE", + "name": "表2: 结局指标单因素分析", + "params\_mapping": { + "group\_var": "{{outcome\_var}}", // 结局变量 + "analyze\_vars": "{{all\_covariates}}" + } + }, + { + "order": 3, + "role": "multivariate\_reg", + "tool": "ST\_LOGISTIC\_BINARY", + "name": "表3: 结局多因素 Logistic 回归", + "params\_mapping": { + "outcome\_var": "{{outcome\_var}}", + "predictors": "{{exposure\_var}} \+ {{significant\_covariates}}" // 纳入表2中P\<0.05的变量 + } + } + \] +} + +## **4\. 对系统价值的深远影响** + +1. **Planner (大模型) 压力骤降**:LLM 不再需要像写循环一样生成 20 个分析步骤。它只需要提取出【结局变量】、【分组变量】和【协变量列表】即可。这使得规划的准确率无限逼近 100%。 +2. **完美契合《统计分析报告.docx》**: + * **Table 1** \-\> 对应 Step 1 + * **Table 2** \-\> 对应 Step 2 + * **Table 3** \-\> 对应 Step 3 + 这标志着我们的 AI 从“答题机器”变成了真正掌握科研 SOP 的“高级数据分析师”。 +3. **用户体验魔法化**:用户只需一句话输入(“这批队列数据,结局是生存状态,主要看吸烟的影响,帮我出一套完整报告”),系统就会直接扔出 3 张可以直接放进 SCI 论文的超长三线表。 + +## **5\. 决议与后续行动** + +团队的这个诊断极其精辟,**强烈同意纳入 Phase Deploy 阶段执行**。 + +**Action Items:** + +1. **R 工程师**:将研究 gtsummary 包作为 P0 级任务,本周内用 6-8 小时将其封装为 ST\_BASELINE\_TABLE,并测试其提取 report\_blocks 表格结构的能力。 +2. **后端工程师**:在决策表中加入 cohort\_study\_standard 流程模板。 +3. **前端工程师**:准备好能够支持横向滚动、拥有复杂表头(Span Header)的高级三线表组件,以迎接 Table 1 这种包含大量数据的巨型表格。 + +**做得漂亮!这是让系统具备商业变现能力的决定性一步。** \ No newline at end of file diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro-V11-UI-Development-Summary-2026-02-20.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro-V11-UI-Development-Summary-2026-02-20.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-Pro-V11-UI-Development-Summary-2026-02-20.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/SSA-Pro-V11-UI-Development-Summary-2026-02-20.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/UI遮挡Bug终极修复指南.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/UI遮挡Bug终极修复指南.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/UI遮挡Bug终极修复指南.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/UI遮挡Bug终极修复指南.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/医疗AI统计助手架构研究.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/医疗AI统计助手架构研究.md new file mode 100644 index 00000000..3afc22ac --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/医疗AI统计助手架构研究.md @@ -0,0 +1,194 @@ +# **智能化医疗统计分析助手 (SSA-Pro) 底层架构与大模型智能体演进深度研究报告** + +在当今医疗信息学与人工智能交叉的前沿领域,大型语言模型(LLM)正经历从单纯的文本生成工具向具备自主规划、工具调用与推理能力的智能体(Agent)范式转移。针对医疗统计分析场景,系统不仅需要处理医生输入的极度非结构化、充满模糊性与领域特定黑话的自然语言诉求,更需要跨越概率性生成模型与确定性数理统计之间的巨大鸿沟。医疗数据的分析容不得丝毫“幻觉”,任何一个伪造的 P 值或误用的统计检验方法,都可能导致临床试验结论的南辕北辙,进而危及患者生命安全与医疗决策的科学性。 + +基于 Q-P-E-R(Query 理解层、Planner 规划层、Execute 执行层、Reflection 审视层)的四层架构,为解决这一痛点提供了坚实的系统论基础。该架构通过深度解耦自然语言理解、统计学逻辑推理、底层代码编译执行以及医学结论转译,试图在灵活性与严谨性之间找到最佳平衡点。然而,在系统向高阶智能化演进的过程中,每一层都面临着严峻的技术抉择。本报告将以首席 AI 架构师的视角,深入剖析这四个核心模块中的前沿理论、工业界最佳实践,并提出高度可落地的架构演进方案。 + +## **议题 1:Query 层(用户意图识别与澄清)的最佳实践** + +Query 层是整个智能化统计分析系统的“感知中枢”。临床医生在输入需求时,往往缺乏对统计学术语的精确掌握,其表述(如“这 200 个患者的数据,新药到底有没有效?”)在统计学语境下是高度欠定的。系统必须将这种自然语言精准映射为包含分析目的(Goal)、因变量(Y\_var)、自变量(X\_var)以及实验设计(Design)的四维结构化意图。 + +### **意图识别的技术路线深度对比与 ROI 分析** + +在垂直领域的结构化意图提取任务中,工业界目前主要徘徊在纯提示词工程(Prompt Engineering)、检索增强生成(RAG)、监督微调(SFT)以及自然语言转领域特定语言(NL2DSL)四条技术路线之间。 + +纯提示词工程(Zero-shot 或 Few-shot)依赖于调用诸如 GPT-4o 或 Claude 3.5 Sonnet 等前沿大模型,通过在其系统提示中注入大量的规则说明与少量示例来实现意图抽取。这种方法的优势在于启动成本极低,能够快速进行原型验证,并且前沿模型具备强大的泛化与常识推理能力 1。然而,在复杂的医疗统计场景下,其边界效应显而易见。随着临床元数据(如包含成百上千个变量的数据字典)的输入,提示词长度急剧膨胀,大模型极易陷入“中间迷失”(Lost in the middle)的困境,导致指令遗忘或抽取目标偏移 3。此外,长期依赖商业前沿模型的 API 会带来高昂的推理成本、不可控的延迟,以及更为致命的患者隐私数据泄露风险 4。 + +引入意图识别知识库(RAG)能够部分缓解上述问题。在医疗场景中,单纯的向量检索(基于嵌入的 RAG)往往因为缺乏实体关联而表现不佳。前沿实践表明,构建基于临床实体增强检索(CLEAR)或知识图谱的结构化 RAG 系统,可以显著降低大模型的幻觉率并减少 Token 消耗 5。例如,系统可以在预处理阶段建立一个“医疗同义词与统计变量映射”的知识图谱,当医生提到“新药有效性”时,RAG 模块首先将“有效性”关联到具体的临床终点(如血压下降值或生存率),再将这些增强后的上下文喂给 LLM。这种做法将外部领域知识与 LLM 的推理能力结合,提高了识别的准确性 6。 + +然而,从投资回报率(ROI)与生产环境稳定性的角度来看,监督微调(SFT)小型专有模型(如 14B 参数量级的开源模型)展现出了压倒性的优势。最近的实证研究表明,在特定领域的结构化信息提取(如将临床文本转化为 JSON 格式的元数据)任务中,经过 DPO(直接偏好优化)或 SFT 训练的百亿参数模型,其表现不仅能够匹敌甚至在特定结构化约束下超越通用大模型 7。SFT 使得模型内化了统计意图提取的特定分布,彻底消除了复杂提示词的需求。在医疗场景下,本地部署 SFT 模型不仅实现了数据的绝对物理隔离(满足 HIPAA 或 GDPR 合规要求),还使得单次推理成本呈指数级下降。 + +进一步地,结合语义解析(Semantic Parsing)的 NL2DSL 方案代表了意图识别的最终演进形态。在这种架构下,开发团队预先定义一套严格的统计分析计划(SAP)上下文无关文法(CFG),并通过“受限解码”(Constrained Decoding)技术强制 LLM 在生成 Token 时必须符合该 DSL 的语法规则 9。这意味着模型输出的将不再是概率性的自由文本,而是 100% 语法正确的抽象语法树(AST)或强类型的 JSON Schema。在 SSA-Pro 中,采用 SFT 本地模型辅以 NL2DSL 受限解码,是兼顾极高解析准确率、数据安全性与系统 ROI 的最佳工业实践。 + +| 技术路线 | 实施复杂度 | 推理成本与延迟 | 结构化输出稳定性 | 医疗数据合规性 | +| :---- | :---- | :---- | :---- | :---- | +| **纯提示词工程 (API)** | 低 | 高 | 中等(易受指令漂移影响) | 低(数据出境风险) | +| **知识检索增强 (RAG)** | 中等 | 中等 | 较高(依赖知识库质量) | 视模型部署方式而定 | +| **监督微调小型专有模型 (SFT)** | 高 | 低 | 高(深度契合特定任务) | 高(支持本地私有化部署) | +| **受限解码的 NL2DSL** | 极高 | 低 | 绝对稳定(100% 语法正确) | 高(本地运行且逻辑可审计) | + +### **主动追问(Clarification)的“人类在环”机制设计** + +当医生输入的描述过于简略,导致大模型提取的四维意图置信度低于预设阈值时,系统必须触发主动追问机制。传统的对话机器人在遇到不确定性时,往往会生成发散性的开放问题(如:“请问您具体想怎么定义有效性?”)。这种做法将思考的负担重新推回给用户,极易引发医生的认知疲劳与抵触情绪,并且用户后续的开放式回答可能引入新的歧义 11。 + +优雅的“人类在环”(Human-in-the-loop)澄清机制应当遵循“发散思考,收敛提问”的设计模式(Divergent Outline Clarification)11。具体而言,当 Query 层的意图评估器识别出信息缺失(例如,缺失具体的比较基准或分组变量)时,系统会隐式启动一个“澄清子智能体(Clarifier Sub-Agent)”。该智能体首先调取数据体检报告中的变量字典,进行蒙特卡洛树搜索(MCTS)式的路径模拟,预测出 2 到 3 种在当前数据结构下合法且具备统计学意义的分析假设 11。 + +随后,智能体将这些底层计算路径转化为通俗易懂的临床业务选项,生成一个收敛性的多选题返回给医生。例如,系统不再问“你想用什么指标”,而是输出:“我们检测到您的数据中包含多个可能的终点指标,为了评估‘新药是否有效’,请问您倾向于选择:A. 比较用药前后连续的血压下降绝对值(适用于 T 检验);B. 比较用药后达到正常血压标准的患者比例(适用于卡方检验)。” 这种将大模型的内部歧义转化为外部确定性选择题的机制,不仅严格约束了上下文状态空间,避免了多轮对话带来的逻辑发散,同时也起到了隐性教育用户的作用,极大提升了医疗 AI 系统的专业感与可信度 14。 + +## **议题 2:Planner 层(分析路径规划)的构建方案** + +Planner 层是智能统计助手的核心大脑。它的任务是将 Query 层提取出的意图结构与数据体检报告(缺失率、偏度、峰度、样本量等)进行对齐,并据此生成一份严格的、包含具体步骤的统计分析计划(SAP)。在这个环节,数理统计的严密性与大模型的“幻觉”本性发生了最直接的冲突。 + +### **静态规则与动态规划的架构博弈** + +在 LLM 智能体架构中,关于任务规划主要存在两种极端范式:基于硬编码与决策树的“静态规则引擎”,以及完全由 LLM 主导、边思考边执行的“动态规划”(如 ReAct 范式)16。 + +ReAct(Reasoning and Acting)模式通过在“思考、行动、观察”之间不断循环来推进任务。虽然它在开放域问题(如网络搜索或代码调试)中展现出强大的适应性,但在医疗统计中却是灾难性的 17。首先,ReAct 容易陷入局部最优解,导致分析流程缺乏全局一致性;其次,多轮循环会消耗海量 Token(即所谓的“ReAct 税”),显著降低系统响应速度并增加成本;最关键的是,频繁的自主决策极易引发“动作幻觉”,即模型在没有数理依据的情况下随意捏造统计转换或过滤条件 17。 + +相比之下,坚持使用硬编码的 JSON 决策表虽然安全,但过于僵化,无法应对临床数据中层出不穷的边缘情况(Edge Cases)。因此,工业界的最佳实践是采用“先规划,后执行”(Plan-and-Execute / Plan-and-Solve)的混合架构 17。在这一架构中,规划和执行被物理隔离。Planner 智能体首先作为一个全局调度者,在一个单独的推理周期内,综合所有约束条件生成一个完整的、包含多步骤的有向无环图(DAG)计划。这个计划一旦生成,便不再轻易更改。只有在底层执行层(Execute)明确返回不可恢复的运行时错误时,系统才会通过回调机制(Callback)触发局部的动态重规划(Dynamic Replanning)20。这种分离机制不仅将 API 调用成本降低了数倍,更重要的是它锁死了统计分析流程的确定性边界,使得每一步操作在执行前都是可审计和可预测的 18。 + +### **将“统计学先验知识”深度注入系统** + +要让 Planner 在 Plan-and-Execute 架构中生成完美的工作流,单纯依赖大模型预训练权重中蕴含的统计知识是极不靠谱的。模型可能因为语料分布的偏差,错误地为非正态小样本数据推荐参数检验。将专家先验知识(如“分类变量用卡方,连续变量正态用 T 检验”)高效注入系统的最佳形态,是构建一个独立的“统计学知识图谱(Statistical Knowledge Graph, SKG)”并结合规则引擎 6。 + +提示词约束(Prompt Constraint)虽然实现简单,但随着规则的增多会导致上下文臃肿,且 LLM 难以在复杂的逻辑长链中保持绝对的遵循率 3。而硬编码的规则引擎虽然绝对准确,但难以处理自然语言意图中的软性模糊条件。因此,采用知识图谱作为中间件是最优解。在这个 SKG 中,节点代表数据类型(连续、分类、有序)、统计假设(正态性、方差齐性)、分析方法(T 检验、Wilcoxon 检验、ANOVA)以及后续的可视化手段;边则代表了严密的逻辑条件与因果关系 25。 + +在实际运行中,系统通过图检索增强生成(GraphRAG)技术,将当前用户的数据特征与图谱中的节点进行匹配。系统提取出从“数据起点”到“统计方法终点”的一条或多条有效子图路径(Sub-graph)。然后,将这条结构化的路径知识作为“硬性指令”注入到 Planner 智能体的系统提示词中 23。这种图谱注入机制(Graph Injection)彻底剥夺了 LLM 在核心统计规则上的“自由裁量权”,大模型退化为一个高级的“编译器”,其唯一任务是将图谱输出的绝对正确规则翻译为具体的、适配当前数据集维度的操作代码,从而从根本上消除了统计方法选择上的幻觉 24。 + +### **规避上下文爆炸的精细化状态管理** + +在将 Data Profile(数据体检报告)、Metadata(变量字典)、User Goal(用户意图)喂给 LLM 以做出规划时,必须实施激进的上下文隔离与压缩策略,以防止“中间迷失”现象 28。 + +系统应建立“冷热状态分离(Hot and Cold Context Separation)”机制 29。对于 Planner 而言,它不需要看到全量数据的每一行内容,甚至不需要看到每一个变量的详细描述。热上下文(Hot Context)仅包含:高度浓缩的用户最终目标、只包含涉事变量(X 与 Y)的数据类型与关键统计特征(如“X:二分类;Y:连续,缺失率 2%,Shapiro-Wilk P\<0.05 拒绝正态”),以及从 SKG 中提取的方法学路径。 + +冷上下文(Cold Context)——包括全量数据框架、不相关变量的分布、以及冗长的建表语句——全部卸载到外部键值存储(KV Store)或向量数据库中。Planner 只生成高级指令(如 invoke\_non\_parametric\_test(var\_x, var\_y)),而具体的执行工具(Tools)在被调用时,才会去冷上下文中拉取具体的数据片段进行运算 6。同时,引入“观察结果掩码(Observation Masking)”技术,当底层工具返回长篇大论的数据摘要时,系统内部的记忆管理模块会将其压缩为简短的状态占位符,从而保持大模型规划窗口的绝对清洁与高效 28。 + +## **议题 3:Execute 与 Reflection 层的智能化深度** + +如果在 Planner 层解决了“做什么”的问题,那么 Execute 层解决的就是“如何跑通”,而 Reflection 层则是解决“如何解释”。这两个位于后端的层级,是直接决定最终临床输出物质量的关键防线。 + +### **Execute(执行层)的代码沙箱自愈能力** + +在真实的医疗数据分析中,底层 R 语言引擎的执行往往充满变数。数据中未预见的多重共线性导致设计矩阵不可逆(奇异矩阵错误),或者极大似然估计算法不收敛,这些都是难以通过静态规则彻底预测的运行时错误(Runtime Errors)30。为了实现高健壮性的自愈能力(Self-Correction),架构必须引入具备元认知(Metacognitive)能力的诊断闭环 13。 + +工业界的最佳实践是将 R 脚本执行置于强隔离的容器化沙箱中(如基于 Docker 或 WebAssembly 的环境),并全面捕获标准输出(stdout)、标准错误(stderr)以及运行时的堆栈轨迹(Traceback)33。一旦捕获到异常状态(如 Error in solve.default(X) : system is computationally singular),Execute 层会立即冻结执行,并将错误上下文连同原始 R 代码抛给一个专门的“检查者智能体(Inspector/Critic Agent)” 32。 + +这种自愈并非盲目地让大模型“再试一次”。先进的 Agent-R 范式引入了蒙特卡洛树搜索(MCTS)的思想,系统会在内存中展开一条错误恢复轨迹 13。检查者智能体会分析错误根因,例如针对“矩阵奇异”问题,它会自主决定在代码中临时插入一段计算方差膨胀因子(VIF)的诊断代码,找出引起共线性的冗余变量(如同时包含了“身高”、“体重”和“BMI”),修改原始特征选择逻辑,剔除高 VIF 变量,或改用岭回归(Ridge Regression)等正则化方法,重新生成 R 脚本并提交沙箱运行 30。这种基于“诊断-修复-验证”状态机的迭代机制,能够在无人类干预的情况下,自动跨越绝大多数数据科学的工程性陷阱,大幅提升了任务完成的成功率 36。 + +### **Reflection(审视层)的反幻觉输出机制** + +Reflection 层承担着将冰冷的 JSON 统计结果转化为具有人情味、符合医学论文规范的文字结论的任务。由于大语言模型本质上是基于概率的下一个 Token 预测器,其对数值的敏感度和事实一致性往往存在固有缺陷。如果模型在撰写结论时捏造了 P 值,或者将置信区间(CI)随意篡改以迎合“具有统计学显著差异”的文本倾向,后果是不堪设想的 38。 + +为了达成绝对的反幻觉保证,必须在 Reflection 层实施多重防线: + +1. **基于受限解码的严格映射**:执行层返回的结构化 JSON(包含 P 值、OR 值、CI 等)不得直接作为自由文本混入提示词中让模型续写。相反,应采用模板引擎和函数调用(Function Calling)强制模型进行数值映射。模型被剥夺了生成数值 Token 的权限,所有的统计量只允许通过指向 JSON 键值的引用槽位(如 {{result.p\_value}})进行渲染 10。 +2. **分对数(Logit)熵值监控与一致性检验**:模型在产生幻觉时,其输出 Token 的概率分布往往会从陡峭变为平缓(呈现出高不确定性的均匀分布特征)。系统可以通过截获生成关键医学声明(如“显著提高”、“呈正相关”)时的前 K 个 Token 概率,运行柯尔莫哥洛夫-斯米尔诺夫检验(K-S Test)。一旦检测到熵值异常升高,即触发警报,拒绝当前生成,并要求模型在更高的温度(Temperature=0)下重新推理 42。 +3. **引入验证链(Chain-of-Verification, CoVe)**:在初稿生成完毕后,再引入一个完全独立的小型纠错模型(Verifier)。该模型只被分配一个任务:逐字比对生成的医学文本中的每一个数值和趋势描述,是否与底层的 JSON 数据在数学逻辑上绝对一致。一旦发现哪怕小数点后两位的微小偏离,即判定校验失败,打回重写 44。 + +### **生成具有说服力的可解释性“方法学说明”** + +在医学统计中,医生对系统的信任往往不取决于最终结果有多么完美,而取决于系统能否自圆其说。Reflection 层不仅要输出“新药有效”,更要生成符合 APA 标准的“方法学说明”(Methodology Section),解释 Planner 为什么这么选。 + +实现这一点的关键在于建立一条端到端的“溯源轨迹(Traceability)”。在 Planner 层调用知识图谱(SKG)做出决策时,系统需要将触发规则的审计日志保存为结构化元数据(例如:Trigger\_Rule\_402: Type=Continuous, Shapiro-Wilk=0.03 (\<0.05), N=200 \-\> Path=Non\_Parametric\_Mann\_Whitney)。 + +在 Reflection 层生成方法学说明时,LLM 会将这条审计日志作为骨架,扩写为流畅的学术文本:“为了评估新药对收缩压的影响,本研究首先对数据进行了 Shapiro-Wilk 正态性检验。由于数据显著偏离正态分布(p \= 0.03),且样本分布满足两独立样本条件,故系统未采用独立样本 T 检验,而是选用了非参数的 Mann-Whitney U 检验来比较两组间的数值中位数差异。该方法的选择有效避免了偏态数据导致的统计效力下降……” 这种将内部状态机的决策树透明化、并用自然语言解释其统计学合理性的过程,是建立专家级信任的最有效手段 24。 + +## **议题 4:行业标杆与高级 Agent 架构模式参考** + +为了确保 SSA-Pro 在未来的长远生命力,架构底座的选择与演进路线必须与最先进的工业界标准对齐。 + +### **底层架构模式选择:LangGraph 是 Q-P-E-R 的最佳载体** + +在目前的智能体开发框架生态中,以 AutoGen 为代表的“多智能体对话协作(Multi-Agent Conversation)”范式和以 LangGraph 为代表的“状态机与工作流编排(State Graph)”范式代表了两种截然不同的架构哲学 47。 + +对于高度严谨、要求确定性执行的医疗统计分析流程而言,LangGraph 是毫无争议的最优选择。AutoGen 将任务推进的主导权交给了 LLM 之间的黑盒对话,依赖提示词来指引哪一个 Agent 下一步发言。这种模式容易导致控制流混乱、代理之间无休止的死循环,以及状态在多轮对话中的隐性丢失 47。这在不允许任何非预期发散的医疗分析中是致命的。 + +相反,LangGraph 强迫开发者用图论(Graph)的思维将系统抽象为“节点(Node)”和“边(Edge)”。在 SSA-Pro 中,Q-P-E-R 就是四个核心的大型节点(或子图)。数据作为全局的状态对象(State),顺着边在节点间有向流动。更重要的是,LangGraph 原生支持带有条件路由的环状图(Cyclic Graph),使得 Execute 层的“出错-诊断-重试”闭环得以用极其明确的工程代码(而非纯 Prompt)进行控制 47。同时,它内置的持久化检查点(Checkpointer)完美支撑了 Query 层的“人类在环”澄清机制,使得系统可以在任意节点挂起,等待医生输入选择题答案后,毫无状态损耗地恢复执行 48。 + +| 架构维度 | AutoGen (对话式编排) | LangGraph (状态图编排) | 在 SSA-Pro 中的适用性对比 | +| :---- | :---- | :---- | :---- | +| **控制流范式** | 基于 LLM 隐式路由的群聊 | 基于代码显式定义条件边 | LangGraph 提供了医疗系统必需的绝对掌控权。 | +| **状态共享** | 依赖对话历史窗口的积累 | 结构化 TypedDict 强制传递 | LangGraph 避免了对话堆积造成的上下文挤兑。 | +| **错误恢复** | Agent 互相指责和修正,易发散 | 显式的错误捕捉与环路重试 | LangGraph 的重试逻辑可控,避免陷入 token 消耗黑洞。 | +| **断点交互(HITL)** | 依赖提示词介入,控制较弱 | 原生支持节点级别挂起与恢复 | 完美契合医生的中途选择性介入澄清机制。 | + +### **业界顶尖 AI 统计产品的核心亮点借鉴** + +在通用数据分析领域,Julius AI 与 Energent.ai 代表了目前业界最高的智能化水平。SSA-Pro 可以从这些标杆中汲取宝贵的工程经验。 + +Julius AI 的巨大成功并非仅仅因为其背靠强大的基座模型,而在于其极其工程化的计算隔离与状态记忆能力。Julius 在处理复杂文件时,完全放弃了让 LLM 内部进行数值计算的尝试,而是构建了强大的 Python/R 隔离沙箱环境,保障了大基数数据的安全处理 51。此外,Julius 设计了一个特殊的“学习子智能体(Learning Sub Agent)”,在用户多次进行数据分析的过程中,它会默默在后台构建关于该用户数据库的 Schema 关系和偏好记忆,使得后续查询越来越精准 53。 + +Energent.ai 则展示了面向企业级的“无代码代理推理层(Agentic Reasoning Layers)”的威力。它不提供一个容易让人产生迷茫的开放式对话框,而是高度聚焦于将杂乱的表格转换为成品的分析图表与演示文稿(PPT)。它通过极端的专门化分工(清洗智能体、分析智能体、图表智能体各司其职),实现了高达 94.4% 的金融分析准确率 54。这印证了我们在 Planner 和 Execute 层中必须解耦与极度细化智能体分工的架构思路。 + +### **通向 L5 全自主数据科学家的技术演进路线图** + +参考自动驾驶的 SAE 标准,工业界正在构建数据智能体的 L0 到 L5 自主性演进分类 55。目前的 SSA-Pro QPER V1.0 架构,通过提供强大的执行辅助并依赖医生(人类在环)进行目标澄清与最终把关,正处于 **L3 级(条件自主:Orchestration 阶段)**。要最终实现能够彻底替代人类高级生物统计学家的 **L5 级(完全自主:Generative 阶段)**,系统需要遵循以下三阶段的技术演进路线: + +**第一阶段:攻克 L4 级高阶自主(主动型智能体架构)** 在这一阶段,系统必须打破“一问一答”的被动响应模式。SSA-Pro 需要接入医院的电子病历(EMR)或临床试验数据流,进化出“主动感知与异常诊断”能力。当后台新的患者数据持续汇入时,驻留智能体(Resident Agents)将自主监控数据漂移,自动识别出临床指标(如某类并发症发生率的突增),并主动触发 Q-P-E-R 流程。系统无需医生输入指令,便能自主规划分析路径、撰写诊断代码、生成可视化报告,并将最终的异常预警直接推送到医生的桌面端 56。此阶段的核心技术突破在于构建长期的情节记忆(Episodic Memory)与基于强化学习(RL)的环境交互能力 57。 + +**第二阶段:跨域大模型群体智能(Agent-to-Agent 协作)** 进入 L4 后期的系统将不再是单一的 Q-P-E-R 链条,而是裂变为多个具备深度专业角色的微型组织。例如,建立“数据工程智能体”(负责纵向缺失值插补理论)、“资深生物统计智能体”(负责贝叶斯后验概率推断)、“流行病学智能体”(负责控制混杂偏倚与因果推断)以及“审稿人智能体”(专门负责挑错与证伪)。系统内采用类似多智能体辩论(Multi-Agent Debate)的共识机制,面对一个复杂的临床研究目的,各方智能体通过自主的 Agent-to-Agent (A2A) 通信协商出最佳分析方案 58。 + +**第三阶段:达到 L5 级完全自主(驱动科学范式转移)** 在最终的 L5 形态,系统将具备独立完成端到端医学科研的能力。它能够自主抓取最新的 PubMed 论文网络(GraphRAG),发现目前肿瘤治疗中的统计学研究空白;随后,系统可以自主提出创新性的科研假设,从万级别规模的异构临床数据库中自主提取、关联病历与基因组数据,设计前瞻性的虚拟临床试验(In Silico Trials)。此时,智能体不仅能修正 R 语言的语法错误,更能识别出人类当前统计学方法本身的局限性,自主组合并编写出全新的机器学习算法或统计算子来解决问题,最终自动生成符合《Nature》、《Lancet》标准的高质量学术论文全文 55。在这个阶段,AI 不再是医生的“分析助手”,而是成为了推动生物医学边界拓展的“全天候数据科学家同行”。 + +#### **引用的著作** + +1. Prompt engineering for accurate statistical reasoning with large language models in medical research \- Frontiers, 访问时间为 二月 21, 2026, [https://www.frontiersin.org/journals/artificial-intelligence/articles/10.3389/frai.2025.1658316/full](https://www.frontiersin.org/journals/artificial-intelligence/articles/10.3389/frai.2025.1658316/full) +2. Harnessing Large‐Language Models for Efficient Data Extraction in Systematic Reviews: The Role of Prompt Engineering \- PMC, 访问时间为 二月 21, 2026, [https://pmc.ncbi.nlm.nih.gov/articles/PMC12559671/](https://pmc.ncbi.nlm.nih.gov/articles/PMC12559671/) +3. Effective context engineering for AI agents \- Anthropic, 访问时间为 二月 21, 2026, [https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents) +4. Clinical Information Extraction with Large Language Models: A Case Study on Organ Procurement \- PMC, 访问时间为 二月 21, 2026, [https://pmc.ncbi.nlm.nih.gov/articles/PMC12099322/](https://pmc.ncbi.nlm.nih.gov/articles/PMC12099322/) +5. Clinical entity augmented retrieval for clinical information extraction \- PMC \- NIH, 访问时间为 二月 21, 2026, [https://pmc.ncbi.nlm.nih.gov/articles/PMC11743751/](https://pmc.ncbi.nlm.nih.gov/articles/PMC11743751/) +6. Injecting Knowledge Graphs in different RAG stages | by Chia Jeng Yang \- Medium, 访问时间为 二月 21, 2026, [https://medium.com/enterprise-rag/injecting-knowledge-graphs-in-different-rag-stages-a3cd1221f57b](https://medium.com/enterprise-rag/injecting-knowledge-graphs-in-different-rag-stages-a3cd1221f57b) +7. Fine-Tuning Methods for Large Language Models in Clinical Medicine by Supervised Fine-Tuning and Direct Preference Optimization: Comparative Evaluation \- PMC, 访问时间为 二月 21, 2026, [https://pmc.ncbi.nlm.nih.gov/articles/PMC12457693/](https://pmc.ncbi.nlm.nih.gov/articles/PMC12457693/) +8. Leveraging open-source large language models for clinical information extraction in resource-constrained settings \- Oxford Academic, 访问时间为 二月 21, 2026, [https://academic.oup.com/jamiaopen/article/8/5/ooaf109/8270821](https://academic.oup.com/jamiaopen/article/8/5/ooaf109/8270821) +9. LLM-Hardened DSLs for Probabilistic Code Generation in High-Assurance Systems, 访问时间为 二月 21, 2026, [https://deanm.ai/blog/2025/5/24/toward-data-driven-multi-model-enterprise-ai-7e545-sw6c2](https://deanm.ai/blog/2025/5/24/toward-data-driven-multi-model-enterprise-ai-7e545-sw6c2) +10. Large Language Models for Domain-Specific Language Generation Part 2: How to Constrain Your Dragon | by Andreas Mülder | itemis | Medium, 访问时间为 二月 21, 2026, [https://medium.com/itemis/large-language-models-for-domain-specific-language-generation-part-2-how-to-constrain-your-dragon-e0e2439b6a53](https://medium.com/itemis/large-language-models-for-domain-specific-language-generation-part-2-how-to-constrain-your-dragon-e0e2439b6a53) +11. Divergent Outline Clarification in LLMs | by Charlie Koster \- Medium, 访问时间为 二月 21, 2026, [https://ckoster22.medium.com/divergent-outline-clarification-in-llms-6221dd6902fa](https://ckoster22.medium.com/divergent-outline-clarification-in-llms-6221dd6902fa) +12. LLM-based Agents Suffer from Hallucinations: A Survey of Taxonomy, Methods, and Directions \- arXiv.org, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2509.18970v1](https://arxiv.org/html/2509.18970v1) +13. Training AI Agents to Self-Correct: A Deep Dive into Agent-R's Theoretical Foundations, 访问时间为 二月 21, 2026, [https://medium.com/@avd.sjsu/training-ai-agents-to-self-correct-a-deep-dive-into-agent-rs-theoretical-foundations-1c6d00fecdf6](https://medium.com/@avd.sjsu/training-ai-agents-to-self-correct-a-deep-dive-into-agent-rs-theoretical-foundations-1c6d00fecdf6) +14. Bird-Interact: Re-imagining Text-to-SQL Evaluation for Large Language Models via Lens of Dynamic Interactions \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2510.05318v2](https://arxiv.org/html/2510.05318v2) +15. AmbiBench: Benchmarking Mobile GUI Agents Beyond One-Shot Instructions in the Wild, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2602.11750v1](https://arxiv.org/html/2602.11750v1) +16. Dynamic Planning vs Static Workflows: What Truly Defines an AI Agent | by Tao An \- Medium, 访问时间为 二月 21, 2026, [https://tao-hpu.medium.com/dynamic-planning-vs-static-workflows-what-truly-defines-an-ai-agent-b13ca5a2d110](https://tao-hpu.medium.com/dynamic-planning-vs-static-workflows-what-truly-defines-an-ai-agent-b13ca5a2d110) +17. ReAct vs Plan-and-Execute: A Practical Comparison of LLM Agent Patterns, 访问时间为 二月 21, 2026, [https://dev.to/jamesli/react-vs-plan-and-execute-a-practical-comparison-of-llm-agent-patterns-4gh9](https://dev.to/jamesli/react-vs-plan-and-execute-a-practical-comparison-of-llm-agent-patterns-4gh9) +18. Planning Pattern for AI Agents: Strategic Reasoning Before Action | Gian Paolo Santopaolo, 访问时间为 二月 21, 2026, [https://genmind.ch/posts/Planning-Pattern-for-AI-Agents-Strategic-Reasoning-Before-Action/](https://genmind.ch/posts/Planning-Pattern-for-AI-Agents-Strategic-Reasoning-Before-Action/) +19. ReAct\&Plan: Hybrid Reactive & Planning Strategy \- Emergent Mind, 访问时间为 二月 21, 2026, [https://www.emergentmind.com/topics/react-plan-strategy](https://www.emergentmind.com/topics/react-plan-strategy) +20. Plan-and-Act: Improving Planning of Agents for Long-Horizon Tasks \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2503.09572v2](https://arxiv.org/html/2503.09572v2) +21. ALAS: Transactional and Dynamic Multi-Agent LLM Planning \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2511.03094v1](https://arxiv.org/html/2511.03094v1) +22. How to Build a Plan-and-Execute AI Agent \- Ema, 访问时间为 二月 21, 2026, [https://www.ema.co/additional-blogs/addition-blogs/build-plan-execute-agents](https://www.ema.co/additional-blogs/addition-blogs/build-plan-execute-agents) +23. How to Improve Multi-Hop Reasoning With Knowledge Graphs and LLMs \- Neo4j, 访问时间为 二月 21, 2026, [https://neo4j.com/blog/genai/knowledge-graph-llm-multi-hop-reasoning/](https://neo4j.com/blog/genai/knowledge-graph-llm-multi-hop-reasoning/) +24. Synergistic Joint Model of Knowledge Graph and LLM for Enhancing XAI-Based Clinical Decision Support Systems \- MDPI, 访问时间为 二月 21, 2026, [https://www.mdpi.com/2227-7390/13/6/949](https://www.mdpi.com/2227-7390/13/6/949) +25. LLMs \+ Knowledge Graphs: A Practical Guide to Real-World Intelligence \- Medium, 访问时间为 二月 21, 2026, [https://medium.com/@visrow/llms-knowledge-graphs-a-practical-guide-to-real-world-intelligence-0081b9d79cb1](https://medium.com/@visrow/llms-knowledge-graphs-a-practical-guide-to-real-world-intelligence-0081b9d79cb1) +26. Injecting Knowledge Graphs into Large Language Models \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2505.07554v1](https://arxiv.org/html/2505.07554v1) +27. Injecting Knowledge Graphs into Large Language Models \- ResearchGate, 访问时间为 二月 21, 2026, [https://www.researchgate.net/publication/391676783\_Injecting\_Knowledge\_Graphs\_into\_Large\_Language\_Models](https://www.researchgate.net/publication/391676783_Injecting_Knowledge_Graphs_into_Large_Language_Models) +28. Cutting Through the Noise: Smarter Context Management for LLM-Powered Agents, 访问时间为 二月 21, 2026, [https://blog.jetbrains.com/research/2025/12/efficient-context-management/](https://blog.jetbrains.com/research/2025/12/efficient-context-management/) +29. Context Engineering in Google ADK: The Ultimate Guide to Building Scalable AI Agents, 访问时间为 二月 21, 2026, [https://medium.com/@juanc.olamendy/context-engineering-in-google-adk-the-ultimate-guide-to-building-scalable-ai-agents-f8d7683f9c60](https://medium.com/@juanc.olamendy/context-engineering-in-google-adk-the-ultimate-guide-to-building-scalable-ai-agents-f8d7683f9c60) +30. Help for package spaMM \- CRAN, 访问时间为 二月 21, 2026, [https://cran.r-project.org/web/packages/spaMM/refman/spaMM.html](https://cran.r-project.org/web/packages/spaMM/refman/spaMM.html) +31. LLM as Runtime Error Handler: A Promising Pathway to Adaptive Self-Healing of Software Systems \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2408.01055v1](https://arxiv.org/html/2408.01055v1) +32. Self-correcting Code Generation Using Multi-Step Agent \- deepsense.ai, 访问时间为 二月 21, 2026, [https://deepsense.ai/resource/self-correcting-code-generation-using-multi-step-agent/](https://deepsense.ai/resource/self-correcting-code-generation-using-multi-step-agent/) +33. AgentBay: A Hybrid Interaction Sandbox for Seamless Human-AI Intervention in Agentic Systems \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2512.04367v1](https://arxiv.org/html/2512.04367v1) +34. LAMBDA: A Large Model Based Data Agent arXiv ... \- Defeng Sun, 访问时间为 二月 21, 2026, [https://defengwebsite.github.io/files/2407.17535v2.pdf](https://defengwebsite.github.io/files/2407.17535v2.pdf) +35. Applied Numerical Methods \- (NAFTI \- Ir) | PDF | Polynomial \- Scribd, 访问时间为 二月 21, 2026, [https://www.scribd.com/document/586172726/Applied-Numerical-Methods-NAFTI-ir](https://www.scribd.com/document/586172726/Applied-Numerical-Methods-NAFTI-ir) +36. OR-LLM-Agent: Automating Modeling and Solving of Operations Research Optimization Problem with Reasoning Large Language Model \- arXiv.org, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2503.10009v1](https://arxiv.org/html/2503.10009v1) +37. Why can't LLMs self-correct bad code? : r/ChatGPTCoding \- Reddit, 访问时间为 二月 21, 2026, [https://www.reddit.com/r/ChatGPTCoding/comments/1cygnez/why\_cant\_llms\_selfcorrect\_bad\_code/](https://www.reddit.com/r/ChatGPTCoding/comments/1cygnez/why_cant_llms_selfcorrect_bad_code/) +38. A Comprehensive Survey of Hallucination in Large Language Models: Causes, Detection, and Mitigation \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2510.06265v1](https://arxiv.org/html/2510.06265v1) +39. Why language models hallucinate \- OpenAI, 访问时间为 二月 21, 2026, [https://openai.com/index/why-language-models-hallucinate/](https://openai.com/index/why-language-models-hallucinate/) +40. Consistency Is the Key: Detecting Hallucinations in LLM Generated Text By Checking Inconsistencies About Key Facts, 访问时间为 二月 21, 2026, [https://aclanthology.org/2025.findings-ijcnlp.129.pdf](https://aclanthology.org/2025.findings-ijcnlp.129.pdf) +41. White Paper: The State of Hallucinations in AI-Driven Insights \- Fuel Cycle, 访问时间为 二月 21, 2026, [https://fuelcycle.com/resources/white-paper-the-state-of-hallucinations-in-ai-driven-insights/](https://fuelcycle.com/resources/white-paper-the-state-of-hallucinations-in-ai-driven-insights/) +42. Consistency Is the Key: Detecting Hallucinations in LLM Generated Text By Checking Inconsistencies About Key Facts \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2511.12236v1](https://arxiv.org/html/2511.12236v1) +43. Hallucination Detection and Mitigation in Large Language Models \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2601.09929v1](https://arxiv.org/html/2601.09929v1) +44. From Illusion to Insight: A Taxonomic Survey of Hallucination Mitigation Techniques in LLMs, 访问时间为 二月 21, 2026, [https://www.mdpi.com/2673-2688/6/10/260](https://www.mdpi.com/2673-2688/6/10/260) +45. THaMES: An End-to-End Tool for Hallucination Mitigation and Evaluation in Large Language Models \- arXiv.org, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2409.11353v1](https://arxiv.org/html/2409.11353v1) +46. AI Agents Need an Inference-Bearing Knowledge Graph. Here's Why. \- Squirro, 访问时间为 二月 21, 2026, [https://squirro.com/squirro-blog/ai-agents-inference-knowledge-graphs](https://squirro.com/squirro-blog/ai-agents-inference-knowledge-graphs) +47. AutoGen vs LangGraph: Comparing Multi-Agent AI Frameworks \- TrueFoundry, 访问时间为 二月 21, 2026, [https://www.truefoundry.com/blog/autogen-vs-langgraph](https://www.truefoundry.com/blog/autogen-vs-langgraph) +48. Tested 5 agent frameworks in production \- here's when to use each one : r/AI\_Agents, 访问时间为 二月 21, 2026, [https://www.reddit.com/r/AI\_Agents/comments/1oukxzx/tested\_5\_agent\_frameworks\_in\_production\_heres/](https://www.reddit.com/r/AI_Agents/comments/1oukxzx/tested_5_agent_frameworks_in_production_heres/) +49. Autogen vs. LangGraph : r/LangChain \- Reddit, 访问时间为 二月 21, 2026, [https://www.reddit.com/r/LangChain/comments/1b7q44y/autogen\_vs\_langgraph/](https://www.reddit.com/r/LangChain/comments/1b7q44y/autogen_vs_langgraph/) +50. langchain-ai/langgraph: Build resilient language agents as graphs. \- GitHub, 访问时间为 二月 21, 2026, [https://github.com/langchain-ai/langgraph](https://github.com/langchain-ai/langgraph) +51. DataLab vs. Julius AI Comparison \- SourceForge, 访问时间为 二月 21, 2026, [https://sourceforge.net/software/compare/DataLab-vs-Julius.ai/](https://sourceforge.net/software/compare/DataLab-vs-Julius.ai/) +52. AI for Data Analysis | Julius vs. Claude: How do they compare?, 访问时间为 二月 21, 2026, [https://julius.ai/compare/julius-vs-claude](https://julius.ai/compare/julius-vs-claude) +53. 16 Best Data Analysis Tools: Features & How to Choose \[2026\] \- Julius AI, 访问时间为 二月 21, 2026, [https://julius.ai/articles/data-analysis-tools](https://julius.ai/articles/data-analysis-tools) +54. Best AI data agent architecture comparison 2026 | Energent.ai, 访问时间为 二月 21, 2026, [https://energent.ai/use-cases/en/compare/best-ai-data-agent-architecture-comparison](https://energent.ai/use-cases/en/compare/best-ai-data-agent-architecture-comparison) +55. Data Agents: Levels, State of the Art, and Open Problems \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2602.04261v1](https://arxiv.org/html/2602.04261v1) +56. The Six Maturity Levels of AI Agents | by Girish Kurup \- Medium, 访问时间为 二月 21, 2026, [https://girishkurup21.medium.com/the-six-maturity-levels-of-ai-agents-9720264a6c82](https://girishkurup21.medium.com/the-six-maturity-levels-of-ai-agents-9720264a6c82) +57. LLM-in-Sandbox-RL: Tool-Driven Reinforcement Learning \- Emergent Mind, 访问时间为 二月 21, 2026, [https://www.emergentmind.com/topics/llm-in-sandbox-reinforcement-learning-llm-in-sandbox-rl](https://www.emergentmind.com/topics/llm-in-sandbox-reinforcement-learning-llm-in-sandbox-rl) +58. Agent4S Framework: Autonomous Science Workflows \- Emergent Mind, 访问时间为 二月 21, 2026, [https://www.emergentmind.com/topics/agent4s-framework](https://www.emergentmind.com/topics/agent4s-framework) +59. Multi-Agent Collaboration Mechanisms: A Survey of LLMs \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2501.06322v1](https://arxiv.org/html/2501.06322v1) +60. HKUSTDial/awesome-data-agents \- GitHub, 访问时间为 二月 21, 2026, [https://github.com/HKUSTDial/awesome-data-agents](https://github.com/HKUSTDial/awesome-data-agents) \ No newline at end of file diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/外部架构调研总结与研发共识备忘录.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/外部架构调研总结与研发共识备忘录.md new file mode 100644 index 00000000..07b4a240 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/外部架构调研总结与研发共识备忘录.md @@ -0,0 +1,78 @@ +# **SSA-Pro 外部架构调研总结与研发共识备忘录** + +**文档定位:** 研发团队的统一思想大纲与架构护栏 + +**核心参考:** 外部调研报告《医疗AI统计助手架构研究》、《智能统计分析助手开发探讨》 + +**宣贯目标:** 明确 SSA-Pro 智能化演进的“可为”与“不可为”,确立 **Q-P-E-R 架构** 的极简落地规范。 + +## **1\. 行业前沿共识:我们面临的最大挑战是什么?** + +两份外部顶级调研报告一致指出,医疗 AI 统计分析面临一个根本性矛盾: + +**大模型(LLM)的“概率性生成(经常出现幻觉)” 与 医学统计学要求的“100% 确定性(绝对严谨)” 之间的冲突。** + +如果任由大模型自由发挥(如直接写代码计算 P 值),极易导致致命的学术错误。 + +### **💡 破局之道:LLM 的“哑铃型”选择性部署** + +报告为我们指明了业界最佳实践:**“两头用 AI,中间用规则”**。 + +* **两头(Q层 & R层)**:利用 LLM 强大的自然语言处理能力,听懂医生的“大白话”,写出漂亮的“学术结论”。 +* **中间(P层 & E层)**:利用传统软件工程的“决策表”和“固定代码库”死死管住执行逻辑,绝不容许 AI 产生幻觉! + +## **2\. Q-P-E-R 四层架构的具体落地规范 (Actionable Guide)** + +为了将高大上的理论转化为我们初创团队可以马上写的代码,请开发团队严格遵循以下落地规范: + +### **🟢 Q层 (Query) \- 意图理解与澄清** + +* **调研精髓**:医生缺乏统计学术语,输入充满模糊性。不要让 AI 瞎猜。 +* **开发规范**: + 1. **结构化槽位提取**:LLM 的任务是做“翻译官”,将自然语言翻译成 JSON 格式的统计特征(如 {"goal": "difference", "y\_type": "numeric"}),而不是直接选工具。 + 2. **动态意图澄清 (Human-in-the-loop)**:当 LLM 发现意图不明确时(信心度 \< 0.8),后端立即中断流程,返回前端一个带选项的卡片(如:👉\[比较差异\] 👉\[相关分析\]),让用户点击选择。 + +### **🟡 P层 (Planner) \- 方案规划** + +* **调研精髓**:应对海量工具的 Context 爆炸,以及防止 AI 选错统计方法。 +* **开发规范**: + 1. **Tool RAG 动态检索**:在组装 Prompt 前,先用 pgvector 检索出最相关的 Top-5 工具定义喂给大模型,节省 Token 并提高准度。 + 2. **硬编码决策树兜底 (Rule-based)**:**严禁 AI 自由决定统计方法**。后端代码必须引入基于 Excel 导出的决策表。拿到 Q 层的 JSON 后,用 if-else 规则精准匹配出唯一的 Tool\_Code(如 ST\_T\_TEST\_IND)。 + +### **🔵 E层 (Execute) \- 代码执行** + +* **调研精髓**:用户需要的是完整的分析流程,以及绝对安全的运行环境。 +* **开发规范**: + 1. **工作流模板 (Workflow Templates)**:不要指望 AI 去拼接多步操作。我们将底层的单个 R 脚本升级为“套餐”。例如 T 检验的 R 脚本内应包含 \[基线描述 \-\> 正态性护栏检查 \-\> 核心计算 \-\> 画图\] 完整动作,Node.js 只需发起一次调用。 + 2. **安全隔离**:继续使用当前的 R Docker \+ Plumber API 提供服务,通过内网 OSS 传递数据。 + +### **🟣 R层 (Reflection) \- 结果反思与自愈** + +* **调研精髓**:AI 执行必然会报错,必须具备遇到 Error 自动修复的能力。 +* **开发规范**: + 1. **参数级靶向自愈 (Self-healing)**:当 R 容器报错(如“找不到年龄列”)时,Node.js 捕获 500 错误,将报错信息抛给 LLM。**警告:只允许 LLM 修改参数 JSON 重新请求,绝对禁止 LLM 现场重新生成一坨 R 代码来跑。** + 2. **论文级严谨解读**:Critic Agent 根据 R 吐出的 P 值,利用预设的 Prompt 约束(严禁使用“证明了”等词),生成结构化的学术解读。 + +## **3\. 架构红线:我们要坚决摒弃的“大厂陷阱”** + +外部报告提到了很多高级名词,但作为追求敏捷交付的团队,我们**坚决不碰**以下过度工程化的方案: + +❌ **拒绝引入 LangGraph/AutoGen 等复杂编排框架**: + +我们现阶段的 QPER 流程是一个清晰的“带重试的线性流水线”。用 **Node.js 原生的 while 循环和 try-catch** 就能实现完美的编排,并且更容易控制前端的 SSE 状态流。 + +❌ **拒绝引入微虚拟机 (Firecracker) 沙箱**: + +因为我们坚持了“调用内部固定的 R 脚本”而不是“执行 LLM 现场写的随意代码”,我们天然避开了 RCE(远程代码执行)的最高风险。现有的 Docker 容器隔离已经足够安全。 + +❌ **拒绝多智能体辩论 (Multi-Agent Debate)**: + +医疗统计有明确的数学定理,正态性 P \< 0.05 就是不满足。我们用“硬编码的 R 语言护栏”来保证正确性,不需要浪费 Token 让两个 LLM 互相辩论。 + +## **4\. 总结寄语** + +这份调研报告是对我们目前技术路线的最高级别肯定! + +它证明了我们在坚持的 **“规则匹配 \+ LLM推理 \+ 标准化执行库”** 模式,正是解决医疗 AI 痛点的终极答案。 + +请大家不要有任何技术栈焦虑,用最熟悉的 Node.js 和 R,用最扎实的 if-else 配合优秀的 Prompt,去打造业界最严谨、体验最好的智能统计助手! \ No newline at end of file diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/智能统计分析助手开发探讨.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/智能统计分析助手开发探讨.md new file mode 100644 index 00000000..03ee4572 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/智能统计分析助手开发探讨.md @@ -0,0 +1,189 @@ +# **智能化统计分析助手架构设计与实践:基于 Q-P-E-R 范式的深度研究报告** + +## **引言与架构概述** + +在自动化数据科学与临床统计学领域,系统架构的演进已经从传统的静态、基于规则的执行脚本,正式跨入具备自主推理、规划与自我纠错能力的智能体(Agent)时代。对于旨在极大提高系统智能化能力的统计分析助手(SSA-Pro)而言,如何在赋予大型语言模型(LLM)对话灵活性的同时,严格保障统计学分析的严谨性、可重复性与合规性,是架构设计的核心挑战。为了解决生成式人工智能固有的“幻觉”问题与专业统计分析对确定性的绝对需求之间的矛盾,业内最前沿的解决方案是构建基于 Query(查询)、Planner(规划)、Execute(执行)与 Reflection(反思)的四层 Q-P-E-R 架构框架 1。 + +这一架构的核心原则在于“分层递进”与“LLM 的选择性使用” 1。在 Q-P-E-R 范式中,系统的认知边界被严格划分:语言模型仅被部署在首端的 Query 层用于自然语言理解与意图解析,以及尾端的 Reflection 层用于结果解释与逻辑反思 1。中间的 Planner 层则依赖于确定性的决策表(Decision Tables)与工作流模板,而 Execute 层则完全交由成熟的统计引擎(如 R 语言代码库)进行精确计算 1。本研究报告将针对意图识别、智能规划器的构建、庞大 R 代码库的动态调用与修改、以及自我审查机制等核心问题,进行极其详尽的理论剖析与最佳实践推演,旨在为系统性提升统计分析助手的智能化水平提供详实的架构蓝图。 + +## **第一章:Query 层(认知与理解)—— 多模态意图识别与数据诊断诊断** + +Query 层是整个智能统计分析系统的认知入口,其核心目标是将用户模糊的、非结构化的自然语言输入,精准转化为包含分析目标(Goal)、结果变量(Y)、预测变量(X)以及实验设计(Design)四个维度的结构化查询对象(ParsedQuery) 1。在构建这一层时,如何实现高精度的用户意图识别是首要解决的关键问题。 + +### **意图识别的技术路径:提示词工程与知识库的博弈** + +在当前的智能体开发实践中,用户意图识别主要存在三种技术路径,它们在响应延迟、可扩展性以及对复杂语境的理解能力上各有优劣 2。 + +第一种路径是纯基于提示词(Prompt)与函数调用(Function Calling)的意图提取。这种方式依赖于将预定义的统计目标分类(如“差异性分析”、“相关性分析”、“预测模型”、“描述性统计”)直接写入系统提示词中,要求 LLM 在阅读用户输入后,直接输出对应的 JSON 结构 1。这种方法的优势在于启动成本低且对自然语言的微小差异具有极高的包容度 2。然而,当系统规模扩大,特别是面对医学或复杂商业分析中成百上千种细分统计意图时,将所有规则塞入上下文窗口不仅会导致极高的 Token 消耗和延迟,还会成倍增加模型幻觉的风险 3。 + +第二种路径是建立大规模意图识别知识库,通过语义路由(Semantic Routing)和向量检索(Vector Embeddings)来实现。语义路由器会将用户的查询转化为高维向量,并与向量数据库中预先存储的成千上万条标准意图话术进行余弦相似度比对 2。一旦相似度超过特定阈值,系统将直接触发对应的底层工作流,而无需调用沉重的生成式 LLM 4。这种方法在处理极其庞大的意图分类时具有毫秒级的响应速度和绝对的确定性,但其劣势在于缺乏推理能力,难以处理包含多个子意图的复合问题 4。 + +第三种路径,也是目前复杂数据分析智能体(如医疗临床试验助手)普遍采用的最佳实践,是混合智能体路由(Hybrid Agentic Routing) 5。在这种模式下,系统首先使用一个轻量级的分类器(如基于提示词的小型快速模型)进行顶层意图拦截。一旦识别出用户的查询属于复杂的统计范畴,系统会触发检索增强生成(RAG)机制,连接到专有的统计学知识库或临床终点数字图书馆(Library of Digital Endpoints) 5。通过将用户输入与知识库中的本体标签进行匹配,LLM 能够基于检索到的专业上下文,精确填补结构化查询中的缺失字段 6。 + +对于 SSA-Pro 这类专业级统计分析系统,强烈建议采用这种混合路径。此外,必须在 Query 层引入置信度阈值机制。当 LLM 提取上述四个维度信息(目标、Y、X、设计)的置信度低于 0.7 时,系统绝不能强行向下游传递错误参数,而应通过 Clarifier(澄清模块)主动发起多轮对话,利用动态生成的澄清卡片(ClarificationCard)向用户追问缺失的关键变量 1。 + +### **数据诊断:意图识别的物理锚点** + +一个常被忽视的行业洞察是:纯粹的语义意图识别在统计学领域是不充分的。用户的意图不仅存在于文字中,还深刻绑定在其提供的数据几何形态中。例如,用户可能要求“比较两组患者的血压差异”,从语义上看,这指向了独立样本 T 检验。但如果血压数据存在严重的极端异常值且不符合正态分布,正确的意图解析必须被重定向为非参数的曼-惠特尼 U 检验。 + +因此,Query 层的智能化水平提升,必须依赖于 DataProfiler(数据诊断服务)的深度介入 1。在生成最终的意图对象之前,系统必须调用后台脚本对用户上传的数据进行一次全方位的自动化体检。这包括利用四分位距(IQR)进行异常值检测、评估各组样本量的平衡性、计算数据缺失率、以及准确识别每个变量的物理类型(连续性、分类、二元) 1。提取出的这些数据画像元数据(Metadata)随后会被注入到 LLM 的提示词模板中。通过这种被称为混合提示(Hybrid Prompting)的技术,结合明确的指令、推理脚手架(思维链)和严格的格式限制,系统能够基于真实的数据分布来校准用户的初始意图,从而在源头上杜绝无效的统计请求 8。 + +| 意图识别路径 | 核心机制 | 适用场景与优势 | 潜在劣势与挑战 | +| :---- | :---- | :---- | :---- | +| **基于提示词的函数调用** | LLM 解析文本语义,强制输出符合预定义 JSON 模式的目标与变量映射。 | 适用于早期开发或意图种类较少的系统。灵活性极高,能理解极其模糊的自然语言 2。 | 延迟高,Token 成本昂贵;当工具集扩大时,极其容易出现参数幻觉和分类错误 3。 | +| **基于知识库的语义路由** | 将查询向量化,与庞大语料库中的标准模板进行相似度计算匹配。 | 适用于拥有标准化 SOP 且意图数量庞大的成熟系统。响应速度极快,成本极低,具备确定性 2。 | 无法处理超出知识库范围的新型提问;对复合型意图(一句话包含多个诉求)的解析能力较差 4。 | +| **混合智能体与元数据注入** | 结合轻量级分类器、垂直领域 RAG 检索以及底层数据的自动化诊断画像。 | 业内最佳实践(如临床数据智能体)。能够结合数据真实分布与语义信息,实现极高精度的意图矫正 5。 | 架构复杂度最高;需要构建完善的异常处理与多轮澄清对话(Clarifier)机制 1。 | + +## **第二章:Planner 层(规划与决策)—— 神经符号规划与 SAP 自动生成** + +当 Query 层输出了包含数据画像与用户目标的 ParsedQuery 后,系统进入控制统计学严谨性的核心地带——Planner 层。这一层的主要职责是决定具体的分析方法论和执行顺序,并最终生成一份详尽的统计分析计划(Statistical Analysis Plan, SAP) 1。为了提升智能化水平并保证绝对的科学正确性,Planner 层必须摒弃纯粹依赖 LLM 生成逻辑的做法,转而采用行业内最前沿的神经符号整合(Neuro-Symbolic Integration)架构。 + +### **神经符号架构与决策表(Decision Table)的构建** + +传统的数据分析智能体往往采用思维链(Chain-of-Thought)提示,让 LLM 自己推理应该使用何种统计方法。然而,自然语言推理虽然灵活,但在严密的统计学法则面前却经常充满歧义和不精确性,极其容易忽略潜在的假设前提 10。神经符号架构则结合了两种范式的优势:利用 LLM 强大的语义解析与上下文管理能力(神经系统),结合硬编码的、基于专家经验的统计学逻辑规则库(符号系统) 12。 + +在具体实现中,这种符号逻辑体现为 DecisionTable(决策表)模块 1。要建立一个智能化的 Planner,绝不能让 LLM 自由发挥,而是必须将明确的约束信息输入到决策逻辑中。这些输入信息包括:用户定义的分析目标(如关联性、差异性)、Y 变量的属性(如连续性变量、二分类变量、多分类变量)、X 变量的属性及其数量、以及实验设计方式(如配对样本还是独立样本) 1。 + +通过建立一个高维度的映射矩阵,系统能够实现精确的方法匹配。例如,当系统识别到目标为“比较差异”,因变量为“连续性变量”,自变量为“包含两个类别的分类变量”,且实验设计为“独立样本”时,决策表会确定性地将“独立样本 T 检验”设为“首选工具”(Primary Tool)。同时,基于统计学的基本规则,决策表必须配备条件分支:如果数据诊断显示不满足正态分布或方差齐性,则指定“曼-惠特尼 U 检验”或“韦尔奇 T 检验”为“备选工具”(Fallback Tool) 1。通过这种将显式规则与大模型结合的手段,系统既能理解复杂的业务诉求,又能确保统计学路径的绝对合规。 + +### **工作流模板(Flow Templates)与分层图建模** + +真实的专业统计分析绝非单一算法的调用,而是一整套标准操作流程(SOP)。为了让 Planner 生成的 SAP 达到专业级水准,必须在架构中引入 FlowTemplateService 1。不同分析目标对应不同的标准化流程模板。以“两组连续性变量比较”这一模板为例,Planner 不仅要规划主分析(T 检验),还要强制在 SAP 中自动插入前置步骤,如描述性统计生成(均值、标准差)、假设检验(正态性的 Shapiro-Wilk 检验、方差齐性的 Levene 检验),以及后续的敏感性分析 1。这些流程被打包成可重用的工作流,极大地降低了 LLM 的规划难度,并确保了不同用户执行相同任务时结果的一致性 14。 + +在行业最佳应用中,诸如 MetaGPT 开发的 Data Interpreter 智能体,已经将这种线性流程升级为基于分层有向无环图(Hierarchical DAG)的动态图建模 15。面对复杂的数据科学问题,LLM 驱动的规划器会将庞大的分析目标拆解为多个子任务节点,并通过图结构表达它们的执行依赖关系 16。DAG 架构不仅允许系统识别可并行执行的任务(如同时进行异常值检测与相关性分析),还赋予了系统极强的动态适应能力。如果在分析中途由于数据形态的突然改变导致某个节点失效,基于图结构的 Planner 可以在局部重新规划路径,而无需从头重启整个分析链条 17。 + +## **第三章:Execute 层(编排与执行)—— 破解百级 R 代码库的动态修改与融合** + +Execute 层承担着将高维度的 SAP 翻译为底层机器可执行指令,并与 R 引擎进行交互以获取统计结果的重任 1。贵团队目前面临的核心痛点是:已拥有超过 100 个成熟的 R 语言脚本,希望通过 LLM 修改和调度这些代码以响应多样化的用户需求。在处理庞大代码库时,传统的代码生成方案会遭遇严重的瓶颈,而破解这一难题需要综合运用元数据 RAG 检索、抽象语法树(AST)解析以及结构化的工具调用范式。 + +### **元数据检索增强(Metadata RAG)解决上下文溢出** + +将 100 多个动辄数百行的 R 语言脚本全部塞入 LLM 的上下文窗口是完全不可行的。这不仅会导致高昂的算力成本,更会引发模型注意力机制的崩溃(即“迷失在中间”现象),导致生成的代码张冠李戴 19。业内最佳实践是利用检索增强生成(RAG)技术,但并非对源代码本身进行检索,而是对“代码元数据”进行检索 21。 + +该方案要求在离线阶段,利用 LLM 对现有的 R 代码库进行系统性扫描,为每一个脚本、每一个函数生成高度浓缩的文档摘要 21。这些摘要必须详细记录该函数的功能意图、所需的入参数据类型、期望的返回结果以及典型的应用示例 22。随后,这些摘要被转化为向量嵌入(Embeddings)并与原始 R 文件的抽象语法树(AST)节点建立严格的映射关系 21。当 Planner 层下达具体的分析指令时,执行层的控制智能体首先在元数据向量库中进行检索,精准定位到解决当前任务所需的 1 到 3 个核心 R 脚本,仅仅提取这些高度相关的代码片段作为上下文提供给 LLM 21。这种精准喂药的策略从根本上保障了 LLM 在修改复杂系统时的稳定性。 + +### **基于抽象语法树(AST)的代码动态融合与组装** + +当 LLM 接收到用户的个性化需求(如过滤特定人群、修改特定的回归参数)并需要对现有的 R 脚本进行修改时,最忌讳的做法是让 LLM 重写整个文件。由于传统对话界面生成的代码片段经常在合并时破坏源文件的结构,业内开始转向基于抽象语法树(AST)的代码融合技术 23。 + +AST 能够将 R 语言源码解析为由节点和分支组成的树状逻辑结构。当 LLM 基于用户需求生成了修改后的代码片段后,系统会同时对原始的 R 文件和 LLM 生成的片段进行 AST 解析 23。在树的层面上,系统可以像做外科手术一样,精准定位到需要替换的函数定义或需要更新的数据过滤逻辑,将 LLM 生成的新节点无缝嫁接到原始代码树上,并最终重新生成完整的 R 脚本 23。这种方法彻底规避了正则表达式匹配的脆弱性,确保插入的代码不仅语法合法,而且维持了原有企业级代码的稳定结构 23。 + +### **胶水代码(Glue Code)的动态生成与区块化输出** + +在实际运行中,Execute 层表现为一个智能的工具调用(Tool-Calling)框架 25。这 100 多个 R 脚本被封装为一个个具有严格输入输出 Schema 限制的独立工具。LLM 在这一层的主要角色不再是“从零编写算法”,而是扮演“编排者”的角色,编写轻量级的“胶水代码”(Glue Code),利用 R 语言中的 pal、ellmer 等集成包,将各种参数和数据框(Dataframes)与现有的工具库连接起来 27。 + +为了彻底解放 LLM 对 UI 渲染的负担,极大地提高智能化并降低出错率,Execute 层必须贯彻“区块化(Block-based)协议” 1。在修改和封装所有的 100+ R 工具时,应统一其输出标准,强制引擎返回 report\_blocks(如标准化的表格数据、键值对指标、序列化的图像对象),而不是让 LLM 去生成复杂的 HTML 或 Markdown 渲染代码 1。前端 UI 层接收到这些区块后进行动态渲染,这种计算与展示的深度解耦,是构建高性能统计智能体的黄金准则。 + +## **第四章:Reflection 层(反思与审查)—— 闭环系统中的自我纠错与长效记忆** + +传统的自动化脚本在遭遇报错时会立刻中断并抛出异常,等待人类工程师介入。而在 Q-P-E-R 架构中,Reflection 层的引入标志着系统从反应性的“系统 1”跃升为深思熟虑的“系统 2” 30。该层通过在系统内部建立闭环的评估与纠错机制,使得智能体能够像资深统计师一样,对刚刚产生的计算结果进行批判性质疑和自适应修复 14。 + +### **运行时错误捕获与基于 Reflexion 的代码修复** + +在 Execute 层动态组装和运行 R 代码的过程中,语法报错、数据维度不匹配或库依赖冲突是不可避免的。当 R 引擎抛出错误堆栈日志时,Reflection 层会捕获这些信息,并触发基于 Reflexion 框架的自我反思循环 30。 + +该机制结合了思维链(CoT)推理与口头强化(Verbal Reinforcement)技术 30。充当“LLM 批评家”(LLM Critic)的智能体不会盲目地要求重新运行代码,而是会深度分析错误日志,并用自然语言生成一份反思摘要(例如:“尝试对包含缺失值的向量进行相关性计算导致了 NA 错误,需要在调用 cor() 函数前加入 use \= 'complete.obs' 参数”),随后将包含明确改进建议的指令回传给 Execute 层进行二次尝试 1。这种无需外部新数据训练即可实现代码层面自我修复的能力,是保障系统稳健运行的核心 30。 + +### **自动化统计学审查与安全护栏(Guardrails)** + +相较于显性的代码报错,更隐蔽且危险的是由于违反统计学假设而得出的“数学上正确但科学上谬误”的结果。因此,Reflection 层必须配备一套自动化的统计学审查护栏 8。 + +当统计计算顺利完成并返回结果时,Reflection 层需要依据 Planner 层在决策表中设定的预定规则,对结果进行深度校验。例如,如果主分析执行了方差分析(ANOVA),系统必须优先检查并发执行的 Levene 检验的 p 值 8。如果发现 ![][image1](即方差不齐的假设被拒绝),Reflection 层必须主动阻断当前分析结果的输出,判定此次分析在统计学上是不成立的,并自动向 Planner 层发出回调指令,强制切换至预设的备选工具(Fallback Tool,如 Welch's ANOVA)重新执行 8。此外,可以通过引入基于微调或特定提示模板的“LLM 裁判”(LLM-as-a-judge),审查最终报告是否完整包含检验统计量、自由度、置信区间等必须元素,从而确保输出达到学术发表级别的质量 1。 + +### **长效记忆与经验积累的存储机制** + +为了让智能助手随着使用时间的推移变得越来越聪明,避免在同一数据集上重复犯错,系统必须建立完善的记忆与经验存储机制 14。这种记忆分为两类: + +1. **短期上下文记忆:** 在单一会话周期内保持多轮对话的完整状态,允许用户在中途改变分析方向或对图表配色提出修改意见,而无需重新阐述数据背景 14。 +2. **长期语义记忆库:** 作为一个专门的向量数据库(Vector Database),长期记忆库用于存储智能体在运行中总结出的宝贵经验 36。例如,当系统经过多次反思循环终于解决了一个复杂的 R 代码包冲突问题,或者识别出某个特定业务数据中潜藏的隐藏过滤逻辑时,它会将这一经验浓缩并打上语义标签存入向量数据库 14。在未来的分析任务中,如果面对相似的表结构或查询意图,系统会优先提取这些经验记录,直接跳过错误的推理路径,实现跨会话的系统性进化 37。 + +| 反思与审查维度 | 触发条件与输入 | 处理机制与技术手段 | 输出与下一步动作 | +| :---- | :---- | :---- | :---- | +| **执行期代码错误** | R 引擎抛出报错堆栈与日志 33。 | 利用 Reflexion 框架与 CoT 进行错误根因分析,生成口头强化摘要 30。 | 将附带修改建议的自然语言指令回传给 Execute 层重写胶水代码 33。 | +| **统计学假设冲突** | 前置检验(如正态性、方差齐性)返回不合规的统计量 8。 | 与决策表(Decision Table)中的理论约束进行比对,触发规则护栏 8。 | 阻断错误结果生成,回调 Planner 强制激活并执行备选方案 8。 | +| **逻辑优化与经验沉淀** | 复杂任务执行成功或经过多轮干预后得出正确结果 14。 | 对执行路径进行摘要提炼,并转化为高维向量存储 36。 | 将关键经验存入长期向量记忆库,在未来类似场景中进行语义预加载 37。 | + +## **第五章:针对数据分析 Agent 的最佳应用与行业实践** + +在当前人工智能与数据科学交叉的最前沿领域,许多顶级机构已经构建了基于上述逻辑的强大智能体。通过分析这些最佳应用案例,可以为我们系统性提升智能化水平提供直接的借鉴。 + +### **OpenAI 的内部高级数据智能体** + +OpenAI 开发的内部数据智能体(Advanced Data Analysis 工作流的前身)展示了端到端分析闭环的巨大潜力 14。该系统的显著特征是彻底将迭代和试错的负担从人类用户转移到了机器身上 14。在其实践中,智能体会自主管理从自然语言理解、SQL 数据库查询到最终图表绘制的全过程。更重要的是,它具备极强的自我检查能力:当一个查询由于错误的联合(Join)逻辑返回空数据时,它不会直接向用户报错,而是深入数据库的元数据层,重新分析表结构,调整查询逻辑进行重试 14。为了解决重复性劳动的效率问题,OpenAI 引入了“可复用工作流”(Reusable Workflows)机制,将高频的商业报表生成和验证逻辑打包成模块,确保了系统在面对日常统计分析时的高度一致性 14。 + +### **MetaGPT Data Interpreter 的图结构动态规划** + +在开源生态中,MetaGPT 团队推出的 Data Interpreter 堪称复杂数据科学规划的典范 15。在面对机器学习任务或多变量相关性分析时,传统的线性思维链往往会陷入死胡同。Data Interpreter 创新性地引入了基于分层有向无环图(DAG)的动态规划机制 16。在分析开始前,它将庞大的目标拆解为细粒度的任务节点图。最关键的是,在执行过程中,系统持续监控节点产生的数据流,如果因为上游工具的处理导致中间数据形态发生变化,Data Interpreter 能够在不破坏全局目标的前提下,动态调整下游的任务图结构 17。通过配合自动化的工具集成与基于经验的置信度验证,该系统在机器学习任务上的准确率从基线的 88% 大幅提升至 95%,在开放式数学推理问题上提升了 112% 16。 + +### **临床试验智能体的合规与团队协作机制** + +在合规要求极为严格的医药研发与临床试验领域,智能体的最佳应用集中在对绝对准确性和可追溯性的追求上。例如,在处理诸如统计分析计划生成和 TLFs(图表和列表)批量生成的工作流中,智能体受到严格的数字终点库(DiMe)本体和联邦监管指南的约束 6。 + +以 TeamMedAgents 等框架为例,它们采用了基于角色的多智能体协作模式,将人类医疗团队的审查逻辑映射到 AI 系统中 41。在这些系统中,负责编写 SAS 或 R 代码的“分析智能体”之上,必定叠加着一个独立的“医学监查智能体” 41。监查智能体专门负责在后台审查统计结果是否符合预先设定的决策树逻辑,并验证所有的数据流向是否满足 ALCOA+(可归因性、易读性、同时性、原始性和准确性)的数据完整性标准 42。这种强护栏设计不仅实现了 40% 的交付提速,更满足了 FDA 对防篡改决策日志的监管要求 42。 + +### **Julius AI 的持久化学习与无缝交互** + +作为商业化非常成功的数据分析应用,Julius AI 的核心竞争力在于其底层对于“持久化学习”(Persistent Learning)的巧妙运用以及对交互界面的极简重构 44。用户只需连接数据库或上传 CSV,系统会在后台自动构建数据库的 Schema 映射关系,并且随着用户的持续使用,系统能记住特定的字段含义(例如记忆“revenue”列应该与销售表关联) 44。其智能化的另一大亮点是能将复杂的分析逻辑直接转化为基于 Notebook 的可视化步骤,辅以自然语言的洞察解释,在底层使用 Python/R 代码沙盒保障运算精准度的同时,在表层给用户提供了高度拟人化的交流体验 45。 + +## **第六章:核心总结与智能化提升之系统性建议** + +综上所述,开发一款具有极致智能化能力的统计分析助手,绝不仅仅是对一个生成式语言模型进行简单的封装。它需要融合自然语言处理、符号逻辑学、抽象语法树解析以及深度强化学习机制,构建一个缜密且容错的生态工程。针对贵团队的需求,要系统性提升 SSA-Pro 的智能化水平,建议在开发路径上采取以下四个核心维度的落地策略: + +首先,在**理解维度**,坚决放弃粗放的全文 Prompt 分类,全面拥抱以元数据为驱动的混合路由架构。通过前置的数据诊断服务(DataProfiler)获取变量的真实物理特征,将其作为强约束条件注入意图识别流程,辅以语义检索库进行细分统计目标的精准锚定,并在低置信度时引入柔性的澄清卡片与用户互动。 + +其次,在**规划维度**,必须将统计学的灵魂——严密的数理逻辑与假设检验规则,固化为神经符号系统的决策表(Decision Table)。利用大模型强大的推理能力去理解业务场景,但将其最终落地的统计方法选择权,交由硬编码的逻辑树与标准化工作流模板(Flow Templates)来裁定,从而在根源上消除模型在方法论选择上的幻觉。 + +再次,在**执行维度**,面对百级以上的高价值 R 语言遗产代码库,应利用基于抽象语法树(AST)和代码元数据的检索增强(RAG)技术进行盘活。让 LLM 从“代码编写者”转型为“流程编排者”,通过生成轻量级的胶水代码,精准调度封装好的 R 函数工具。同时,全面实施计算与渲染解耦的“区块化”输出协议,保障前端展示的灵活性与底层执行的稳定性。 + +最后,在**反思维度**,要赋予系统“自我意识”。通过构建拦截运行错误的 Reflexion 循环框架,以及核对统计假设的自动审查护栏,实现结果交付前的高频内审。并辅以支持语义检索的长效记忆向量数据库,使智能体能够在使用中不断累积纠错经验,实现从单次自动化工具向可持续进化的智能分析专家的终极跨越。 + +#### **引用的著作** + +1. 10-QPER架构开发计划-智能化主线.md +2. Intent Recognition and Auto‑Routing in Multi-Agent Systems \- GitHub Gist, 访问时间为 二月 21, 2026, [https://gist.github.com/mkbctrl/a35764e99fe0c8e8c00b2358f55cd7fa](https://gist.github.com/mkbctrl/a35764e99fe0c8e8c00b2358f55cd7fa) +3. Manual intent detection vs Agent-based approach: what's better for dynamic AI workflows? : r/LangChain \- Reddit, 访问时间为 二月 21, 2026, [https://www.reddit.com/r/LangChain/comments/1l7p3qy/manual\_intent\_detection\_vs\_agentbased\_approach/](https://www.reddit.com/r/LangChain/comments/1l7p3qy/manual_intent_detection_vs_agentbased_approach/) +4. Mastering RAG Chatbots: Semantic Router — User Intents | by Tal Waitzenberg | Medium, 访问时间为 二月 21, 2026, [https://medium.com/@talon8080/mastering-rag-chabots-semantic-router-user-intents-ef3dea01afbc](https://medium.com/@talon8080/mastering-rag-chabots-semantic-router-user-intents-ef3dea01afbc) +5. AI Workflows vs. AI Agents \- Prompt Engineering Guide, 访问时间为 二月 21, 2026, [https://www.promptingguide.ai/agents/ai-workflows-vs-ai-agents](https://www.promptingguide.ai/agents/ai-workflows-vs-ai-agents) +6. AI Agent-Powered FDA-Compliant Clinical Trial Design Using the Library of Digital Endpoints | by Alex G. Lee | Medium, 访问时间为 二月 21, 2026, [https://medium.com/@alexglee/ai-agent-powered-fda-compliant-clinical-trial-design-using-the-library-of-digital-endpoints-c2c1d0be3248](https://medium.com/@alexglee/ai-agent-powered-fda-compliant-clinical-trial-design-using-the-library-of-digital-endpoints-c2c1d0be3248) +7. AI Agent Clinical Trial Optimization 2025 \- Rapid Innovation, 访问时间为 二月 21, 2026, [https://www.rapidinnovation.io/post/ai-agent-clinical-trial-optimization-assistant](https://www.rapidinnovation.io/post/ai-agent-clinical-trial-optimization-assistant) +8. Prompt engineering for accurate statistical reasoning with ... \- Frontiers, 访问时间为 二月 21, 2026, [https://www.frontiersin.org/journals/artificial-intelligence/articles/10.3389/frai.2025.1658316/full](https://www.frontiersin.org/journals/artificial-intelligence/articles/10.3389/frai.2025.1658316/full) +9. Prompt engineering for accurate statistical reasoning with large language models in medical research \- PubMed, 访问时间为 二月 21, 2026, [https://pubmed.ncbi.nlm.nih.gov/41159127/](https://pubmed.ncbi.nlm.nih.gov/41159127/) +10. HybridMind: Meta Selection of Natural Language and Symbolic Language for Enhanced LLM Reasoning \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2409.19381v5](https://arxiv.org/html/2409.19381v5) +11. Bridging Symbolic Logic and Neural Intelligence: Hybrid Architectures for Scalable, Explainable AI \- Preprints.org, 访问时间为 二月 21, 2026, [https://www.preprints.org/manuscript/202504.0887](https://www.preprints.org/manuscript/202504.0887) +12. Advancing Symbolic Integration in Large Language Models: Beyond Conventional Neurosymbolic AI \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2510.21425v1](https://arxiv.org/html/2510.21425v1) +13. Symbolic-to-LLM Integration in Hybrid AI \- Emergent Mind, 访问时间为 二月 21, 2026, [https://www.emergentmind.com/topics/symbolic-to-llm](https://www.emergentmind.com/topics/symbolic-to-llm) +14. Inside OpenAI's in-house data agent | OpenAI, 访问时间为 二月 21, 2026, [https://openai.com/index/inside-our-in-house-data-agent/](https://openai.com/index/inside-our-in-house-data-agent/) +15. Data Interpreter: An LLM Agent For Data Science \- ACL Anthology, 访问时间为 二月 21, 2026, [https://aclanthology.org/2025.findings-acl.1016.pdf](https://aclanthology.org/2025.findings-acl.1016.pdf) +16. arxiv.org, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2402.18679v1](https://arxiv.org/html/2402.18679v1) +17. Data Interpreter LLMagent Data Science | PDF | Formal Verification \- Scribd, 访问时间为 二月 21, 2026, [https://www.scribd.com/document/905799019/Data-Interpreter-LLMagent-Data-Science](https://www.scribd.com/document/905799019/Data-Interpreter-LLMagent-Data-Science) +18. Auto-DS (I): The Data Interpreter | by Haitham Bou Ammar | Medium, 访问时间为 二月 21, 2026, [https://medium.com/@haitham.bouammar71/auto-ds-i-the-data-interpreter-1cbecf2820ff](https://medium.com/@haitham.bouammar71/auto-ds-i-the-data-interpreter-1cbecf2820ff) +19. How to use existing production code to build new features : r/aipromptprogramming \- Reddit, 访问时间为 二月 21, 2026, [https://www.reddit.com/r/aipromptprogramming/comments/1hykq77/how\_to\_use\_existing\_production\_code\_to\_build\_new/](https://www.reddit.com/r/aipromptprogramming/comments/1hykq77/how_to_use_existing_production_code_to_build_new/) +20. From Snippets to Systems: Advanced Techniques for Repository-Aware Coding Assistants | by Colin Baird | Medium, 访问时间为 二月 21, 2026, [https://medium.com/@colinbaird\_51123/from-snippets-to-systems-advanced-techniques-for-repository-aware-coding-assistants-cf1a2086ab41](https://medium.com/@colinbaird_51123/from-snippets-to-systems-advanced-techniques-for-repository-aware-coding-assistants-cf1a2086ab41) +21. LLM Agents for Automated Dependency Upgrades \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2510.03480v1](https://arxiv.org/html/2510.03480v1) +22. ReadMe.LLM: A Framework to Help LLMs Understand Your Library \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2504.09798v1](https://arxiv.org/html/2504.09798v1) +23. LLM generated code snippet merging into existing using ASTs : r/theprimeagen \- Reddit, 访问时间为 二月 21, 2026, [https://www.reddit.com/r/theprimeagen/comments/1idtjp2/llm\_generated\_code\_snippet\_merging\_into\_existing/](https://www.reddit.com/r/theprimeagen/comments/1idtjp2/llm_generated_code_snippet_merging_into_existing/) +24. Many Hands Make Light Work: An LLM-based Multi-Agent System for Detecting Malicious PyPI Packages \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2601.12148v2](https://arxiv.org/html/2601.12148v2) +25. Creating and using a Code Agent \- Dataiku Developer Guide, 访问时间为 二月 21, 2026, [https://developer.dataiku.com/latest/tutorials/genai/agents-and-tools/code-agent/index.html](https://developer.dataiku.com/latest/tutorials/genai/agents-and-tools/code-agent/index.html) +26. Tool Based Agent Pattern \- Elumenotion, 访问时间为 二月 21, 2026, [https://www.elumenotion.com/journal/toolbasedagents/](https://www.elumenotion.com/journal/toolbasedagents/) +27. Three experiments in LLM code assist with RStudio and Positron, 访问时间为 二月 21, 2026, [https://tidyverse.org/blog/2025/01/experiments-llm/](https://tidyverse.org/blog/2025/01/experiments-llm/) +28. LLM-Powered, Expert-Refined Causal Loop Diagramming via Pipeline Algebra \- MDPI, 访问时间为 二月 21, 2026, [https://www.mdpi.com/2079-8954/13/9/784](https://www.mdpi.com/2079-8954/13/9/784) +29. Replace Python with Go for LLMs? : r/golang \- Reddit, 访问时间为 二月 21, 2026, [https://www.reddit.com/r/golang/comments/1lfr9hi/replace\_python\_with\_go\_for\_llms/](https://www.reddit.com/r/golang/comments/1lfr9hi/replace_python_with_go_for_llms/) +30. Building a Self-Correcting AI: A Deep Dive into the Reflexion Agent with LangChain and LangGraph | by Vi Q. Ha | Medium, 访问时间为 二月 21, 2026, [https://medium.com/@vi.ha.engr/building-a-self-correcting-ai-a-deep-dive-into-the-reflexion-agent-with-langchain-and-langgraph-ae2b1ddb8c3b](https://medium.com/@vi.ha.engr/building-a-self-correcting-ai-a-deep-dive-into-the-reflexion-agent-with-langchain-and-langgraph-ae2b1ddb8c3b) +31. A Survey of Self-Evolving Agents: On Path to Artificial Super Intelligence \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2507.21046v1](https://arxiv.org/html/2507.21046v1) +32. Self-Evaluation in AI Agents Through Chain of Thought and Reflection \- Galileo AI, 访问时间为 二月 21, 2026, [https://galileo.ai/blog/self-evaluation-ai-agents-performance-reasoning-reflection](https://galileo.ai/blog/self-evaluation-ai-agents-performance-reasoning-reflection) +33. Agent Feedback Loops: From OODA to Self-Reflection | by Tao An | Medium, 访问时间为 二月 21, 2026, [https://tao-hpu.medium.com/agent-feedback-loops-from-ooda-to-self-reflection-92eb9dd204f6](https://tao-hpu.medium.com/agent-feedback-loops-from-ooda-to-self-reflection-92eb9dd204f6) +34. Self-Reflection in LLM Agents: Effects on Problem-Solving Performance \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2405.06682v3](https://arxiv.org/html/2405.06682v3) +35. \#12: How Do Agents Learn from Their Own Mistakes? The Role of Reflection in AI, 访问时间为 二月 21, 2026, [https://huggingface.co/blog/Kseniase/reflection](https://huggingface.co/blog/Kseniase/reflection) +36. Best practices for managing long-term memory in chatbots (Bedrock Agents) | AWS re:Post, 访问时间为 二月 21, 2026, [https://repost.aws/questions/QUvmFZ\_RPoTEm8jQk0SddKNw/best-practices-for-managing-long-term-memory-in-chatbots-bedrock-agents](https://repost.aws/questions/QUvmFZ_RPoTEm8jQk0SddKNw/best-practices-for-managing-long-term-memory-in-chatbots-bedrock-agents) +37. Comparing File Systems and Databases for Effective AI Agent Memory Management | by Richmond Alake | Oracle Developers | Feb, 2026 | Medium, 访问时间为 二月 21, 2026, [https://medium.com/oracledevs/comparing-file-systems-and-databases-for-effective-ai-agent-memory-management-5322ac45f3b6](https://medium.com/oracledevs/comparing-file-systems-and-databases-for-effective-ai-agent-memory-management-5322ac45f3b6) +38. Building smarter AI agents: AgentCore long-term memory deep dive \- AWS, 访问时间为 二月 21, 2026, [https://aws.amazon.com/blogs/machine-learning/building-smarter-ai-agents-agentcore-long-term-memory-deep-dive/](https://aws.amazon.com/blogs/machine-learning/building-smarter-ai-agents-agentcore-long-term-memory-deep-dive/) +39. DATA INTERPRETER: AN LLM AGENT FOR DATA SCIENCE \- OpenReview, 访问时间为 二月 21, 2026, [https://openreview.net/pdf/6908a9386102602f5d4722c6ffbb3d740ead352a.pdf](https://openreview.net/pdf/6908a9386102602f5d4722c6ffbb3d740ead352a.pdf) +40. arXiv:2409.12046v2 \[cs.CL\] 19 Sep 2024, 访问时间为 二月 21, 2026, [https://arxiv.org/pdf/2409.12046](https://arxiv.org/pdf/2409.12046) +41. TeamMedAgents: Enhancing Medical Decision-Making of LLMs Through Structured Teamwork \- arXiv, 访问时间为 二月 21, 2026, [https://arxiv.org/html/2508.08115v1](https://arxiv.org/html/2508.08115v1) +42. Agentic AI in Clinical Trials: Enabling Scalable Solutions | EPAM, 访问时间为 二月 21, 2026, [https://www.epam.com/insights/blogs/agentic-ai-in-clinical-trials-enabling-scalable-solutions](https://www.epam.com/insights/blogs/agentic-ai-in-clinical-trials-enabling-scalable-solutions) +43. Generative AI in the pharmaceutical industry: Moving from hype to reality \- McKinsey, 访问时间为 二月 21, 2026, [https://www.mckinsey.com/industries/life-sciences/our-insights/generative-ai-in-the-pharmaceutical-industry-moving-from-hype-to-reality](https://www.mckinsey.com/industries/life-sciences/our-insights/generative-ai-in-the-pharmaceutical-industry-moving-from-hype-to-reality) +44. AI for Data Analysis | AI in Analytics: What It Is, How It Works, and a Top Example \- Julius AI, 访问时间为 二月 21, 2026, [https://julius.ai/articles/ai-in-analytics](https://julius.ai/articles/ai-in-analytics) +45. AI for Data Analysis | Workflows: AI-Driven Insights, 访问时间为 二月 21, 2026, [https://julius.ai/feature\_page/workflows](https://julius.ai/feature_page/workflows) +46. Top 10 AI Tools for Excel Data Analysis in 2026 | by Powerdrill AI \- Medium, 访问时间为 二月 21, 2026, [https://medium.com/@powerdrillai/top-10-ai-tools-for-excel-data-analysis-in-2026-8edd8eba3a70](https://medium.com/@powerdrillai/top-10-ai-tools-for-excel-data-analysis-in-2026-8edd8eba3a70) + +[image1]: \ No newline at end of file diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构与产品委员会综合评估报告.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构与产品委员会综合评估报告.md new file mode 100644 index 00000000..82191c55 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构与产品委员会综合评估报告.md @@ -0,0 +1,102 @@ +# **架构与产品委员会综合评估报告:SSA-Pro 人机协同 (HITL) 交互增强与 Phase Q+ 演进规划** + +**文档版本:** v2.0 (完整整合版) + +**创建日期:** 2026-02-20 + +**核心裁决:** 🌟 **极度赞同 (Highly Recommended)**。引入“变量字典”与“变量筛选”两大专家协同功能,不仅填补了 AI 临床背景知识的短板,且通过划分为独立的 Phase Q+ 子阶段,完美平衡了“追求极致体验”与“保障核心交付”的矛盾。 + +## **第一部分:业务需求评估 (为什么要引入人机协同?)** + +在纯自动化的 QPER 流程中,AI 缺乏临床先验知识。将选择权和定义权适度交还给医生,是对医学专业性的最大尊重。 + +### **💡 增强点一:用户主导的变量选择 (Variable Selection)** + +* **业务痛点**:医院导出的原始数据往往包含上百列。如果任由 AI 自由发挥,极易把无关变量(如 Patient\_ID、病床号)纳入模型,导致多重共线性或过拟合。 +* **协同价值**:医生最清楚哪些是核心指标,哪些是干扰项。 +* **UX 设计建议 (穿梭框/卡片交互)**: + 在 Q 层处理时,弹出“变量筛选控制台”。 + **🤖 AI:** "我已解析您的数据(共 56 个变量)。为了提高准确度,请确认您本次研究关注的核心变量:" + * **\[AI 智能预选\]** (AI 根据 Prompt 自动勾选最相关的 5 个变量) + * **\[展开全部列表微调\]** (用户可手动增删) + +### **💡 增强点二:建立变量说明与数据字典 (Data Dictionary)** + +* **业务痛点**:临床数据列名极不规范(如 grp 为 1 和 2,AI 根本不知道哪个是治疗组)。 +* **协同价值 (AI-Assisted Codebook)**: + 坚决避免让用户手动填写 100 列的表单。采用 **“AI 先猜,用户微调”** 模式: + 1. Python DataProfiler 读取数据后,后台静默调用 LLM 猜测变量含义。 + 2. 弹出 **“变量数据字典确认表”** 给用户审阅: + * age \-\> AI猜: 患者年龄 \-\> 用户确认 ✅ + * grp \-\> AI猜: 分组 (1, 2\) \-\> 用户补充 ✍️: 1=新药, 2=安慰剂 + 3. 这个经过用户确认的字典,将成为后续 Planner 和 Critic 的**黄金上下文 (Golden Context)**。 + +## **第二部分:架构演进决议 (为什么剥离为 Phase Q+ ?)** + +虽然上述想法极佳,但在项目实施初期,如果将重度前端交互(表格编辑、状态回传)与核心后端 AI 逻辑耦合,会导致严重的**单点阻塞**。 + +因此,委员会决议:**将这两个人机交互检查点作为独立增强任务,归入 Phase Q+ 阶段。** + +### **剥离的战略意义:** + +1. **解耦后端AI与前端UI的依赖**:让后端可以先行打磨 LLM 从自然语言中提取 \[Goal, X, Y, Design\] 的核心纯逻辑(Phase Q)。 +2. **确立 AI 的“智商基线 (Baseline)”**:只有先让 AI 在没有任何人类帮助的情况下硬跑,才能摸清意图识别的真实准确率;之后加上 Phase Q+ 的人工辅助,才能量化“人机协同的提升价值”。 + +## **第三部分:Phase Q+ 在状态机中的精确占位** + +在未来的 Phase Q+ 中,这两个人机检查点将像“拦截器(Interceptor)”一样,无缝插入现有的 ExecutionStatus 状态机中。 + +stateDiagram-v2 + \[\*\] \--\> UPLOADING: 上传文件 + UPLOADING \--\> PROFILING: Python Tool C 探查 + + %% Phase Q+ 新增节点 1 + PROFILING \--\> DICT\_EDITING: 🆕 (Phase Q+) 拦截 + note right of DICT\_EDITING + 展示数据字典表格 + 用户编辑含义/纠正类型 + 点击确认后放行 + end note + + DICT\_EDITING \--\> PENDING\_INTENT: 放行 + PENDING\_INTENT \--\> PARSING\_INTENT: 用户输入自然语言 + + %% Phase Q+ 新增节点 2 + PARSING\_INTENT \--\> VARIABLE\_CONFIRMING: 🆕 (Phase Q+) 拦截 + note right of VARIABLE\_CONFIRMING + AI 已预选 X/Y 变量 + 展示变量穿梭框面板 + 用户微调纳入排除 + end note + + VARIABLE\_CONFIRMING \--\> PLANNING: 放行 + PLANNING \--\> EXECUTING: E 层接管 + +* **架构向后兼容性**:在 Phase Q+ 开发完成前,系统状态将直接从 PROFILING \-\> PENDING\_INTENT,以及 PARSING\_INTENT \-\> PLANNING 自动流转,底层架构基建完全一致。 + +## **第四部分:研发实施路线图 (Revised Roadmap)** + +基于这个决议,QPER 计划被拆解得更加平滑、颗粒度更细: + +| 阶段 | 核心任务 | 性质 | 验证目标 | +| :---- | :---- | :---- | :---- | +| **Phase Q (主线)** | IntentParser (意图解析), DataProfiler (自动探查) | 后端 \+ AI 主导 | 证明 LLM 能盲猜出 Goal, X, Y 槽位。 | +| **Phase P (主线)** | Planner (决策表匹配) | 后端 \+ 规则主导 | 证明系统能基于槽位选对 100% 正确的统计工具。 | +| **Phase E (主线)** | Executor (R 服务执行) | 后端 \+ R 主导 | 证明 R 引擎能跑通、护栏能拦截。 | +| **Phase R (主线)** | Reflection (自动报错重试) | 后端 \+ AI 主导 | 证明系统具备遇到错误自动修改参数的能力。 | +| \--- | \--- | \--- | \--- | +| **Phase Q+ (增强)** | **变量字典面板、变量纳入确认卡片** | 前端体验主导 | **让 AI 从“可用”变为“好用”,注入临床背景知识。** | +| **Phase E+ (增强)** | Block-based 动态多区块渲染 | 前端体验主导 | 支持多图多表的完美富文本展示。 | + +## **第五部分:给开发团队的当前实操建议** + +为了在当前(无 Phase Q+ 的情况下)顺利推进核心 Phase Q 的开发,请后端团队采用以下\*\*“默认放行策略”\*\*: + +1. **DataProfiler 接口契约保持不变**: + DataProfiler 依然需要输出一个标准的 DataProfile JSON。在目前的 Phase Q 阶段,这个 JSON 直接由后端传给 IntentParser;在未来的 Phase Q+ 阶段,这个 JSON 只是中间先发给前端修改,修改完再发给 IntentParser。 +2. **IntentParser 的容错增强**: + 因为当前没有人类帮 AI 筛选无关变量,Prompt 中必须加强指令:*“请自动忽略如 ID、姓名、病床号等明显的非分析变量。”* + +### **结语** + +不要试图让 AI 彻底取代临床医生的判断。最好的系统是用 AI 去做繁琐的计算,而把关键的\*\*“定义权”**和**“特征选择权”**通过优雅的 UI 还给医生。 请团队立刻将精力砸向**纯逻辑的 Q-P-E-R 主线闭环\*\*。当核心链路通畅的那一天,就是我们从容增加 Phase Q+ 人机协同面板的完美时机! \ No newline at end of file diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/架构审查报告:Phase 2A 核心开发计划 .md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构审查报告:Phase 2A 核心开发计划 .md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/架构审查报告:Phase 2A 核心开发计划 .md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构审查报告:Phase 2A 核心开发计划 .md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/架构审查报告:SSA-Pro 愿景与落地策略.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构审查报告:SSA-Pro 愿景与落地策略.md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/架构审查报告:SSA-Pro 愿景与落地策略.md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/架构审查报告:SSA-Pro 愿景与落地策略.md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/终极架构共识与智能化演进备忘录 (1).md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/终极架构共识与智能化演进备忘录 (1).md similarity index 100% rename from docs/03-业务模块/SSA-智能统计分析/06-开发记录/终极架构共识与智能化演进备忘录 (1).md rename to docs/03-业务模块/SSA-智能统计分析/06-开发记录/J技术报告审核评估与建议/终极架构共识与智能化演进备忘录 (1).md diff --git a/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-QPER架构开发总结-2026-02-21.md b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-QPER架构开发总结-2026-02-21.md new file mode 100644 index 00000000..bf01ae44 --- /dev/null +++ b/docs/03-业务模块/SSA-智能统计分析/06-开发记录/SSA-QPER架构开发总结-2026-02-21.md @@ -0,0 +1,108 @@ +# SSA QPER 架构开发总结 + +> **日期:** 2026-02-21 +> **范围:** Phase E+ / Q / P / R 四阶段全部完成 +> **耗时:** ~93.5h(计划内),跨 2026-02-20 ~ 2026-02-21 +> **结果:** QPER 智能化主线闭环,40/40 端到端测试通过 + +--- + +## 1. 完成概览 + +| Phase | 名称 | 核心产出 | 状态 | +|-------|------|---------|------| +| **E+** | Block-based 标准化 | 7 个 R 工具输出 `report_blocks`,前端 `DynamicReport` 动态渲染,Word 导出 | ✅ 100% | +| **Q** | Query 层(LLM 意图理解) | `QueryService` + LLM Intent 解析 + Zod 动态防幻觉 + 追问卡片 + DataProfile 增强 | ✅ 100% | +| **P** | Planner 层(决策表+模板) | `ConfigLoader` + `DecisionTableService` + `FlowTemplateService` + `PlannedTrace` + 热更新 API | ✅ 100% | +| **R** | Reflection 层(LLM 结论) | `ReflectionService` + 槽位注入 + Zod 校验 + 敏感性冲突准则 + 结论缓存 API + 前端渐入动画 | ✅ 100% | + +--- + +## 2. 各阶段关键文件 + +### Phase E+ — Block-based 标准化 + +| 文件 | 说明 | +|------|------| +| `r-statistics-service/tools/*.R` | 7 个 R 工具全部输出 `report_blocks` | +| `frontend-v2/.../DynamicReport.tsx` | 4 种 Block 渲染(markdown/table/image/key_value) | +| `frontend-v2/.../exportBlocksToWord.ts` | Block → Word 导出 | + +### Phase Q — LLM 意图理解 + +| 文件 | 说明 | +|------|------| +| `backend/.../services/QueryService.ts` | LLM Intent 解析 + json-repair + Zod 动态校验 + 正则 fallback | +| `backend/.../types/query.types.ts` | `ParsedQuery` / `ClarificationCard` 接口 + `createDynamicIntentSchema` | +| `backend/scripts/seed-ssa-intent-prompt.ts` | `SSA_QUERY_INTENT` Prompt 种子脚本 | +| `extraction_service/operations/data_profile.py` | `is_id_like` 非分析变量自动标记 | +| `frontend-v2/.../ClarificationCard.tsx` | 封闭式追问卡片组件 | + +### Phase P — 决策表 + 流程模板 + +| 文件 | 说明 | +|------|------| +| `backend/.../config/ConfigLoader.ts` | 通用 JSON 加载 + Zod 校验 + 内存缓存 + 热更新 | +| `backend/.../config/decision_tables.json` | 四维匹配规则(Goal×Y×X×Design) | +| `backend/.../config/flow_templates.json` | 4+1 个标准流程模板 | +| `backend/.../config/tools_registry.json` | R 工具注册表 | +| `backend/.../services/DecisionTableService.ts` | 规则匹配引擎 | +| `backend/.../services/FlowTemplateService.ts` | 模板选择 + EPV 防护 | +| `backend/.../services/WorkflowPlannerService.ts` | 核心规划入口 + `PlannedTrace` 输出 | +| `backend/.../routes/config.routes.ts` | `POST /reload` 热更新 API | + +### Phase R — LLM 论文级结论 + +| 文件 | 说明 | +|------|------| +| `backend/.../types/reflection.types.ts` | `ConclusionReport` / `LLMConclusionSchema` / `classifyRError` | +| `backend/.../services/ReflectionService.ts` | LLM 结论生成 + 槽位注入 + Zod 校验 + fallback | +| `backend/scripts/seed-ssa-reflection-prompt.ts` | `SSA_REFLECTION` Prompt(含敏感性冲突准则) | +| `backend/.../routes/workflow.routes.ts` | `GET /sessions/:id/conclusion` 缓存 API | +| `frontend-v2/.../ConclusionReport.tsx` | 逐 section 渐入动画 + 一键复制 + source 标识 | +| `frontend-v2/.../exportBlocksToWord.ts` | Word 导出增强(纳入 LLM 结论) | + +--- + +## 3. 端到端测试结果 + +**测试脚本:** `backend/scripts/test-ssa-qper-e2e.ts` +**运行方式:** `npx tsx scripts/test-ssa-qper-e2e.ts` + +| 测试项 | 结果 | +|--------|------| +| 登录认证 | ✅ JWT Token 获取 | +| 创建会话 + 上传 CSV | ✅ 21 列 / 311 行 | +| 数据画像(Python) | ✅ 行列数正确 | +| **Q 层** — LLM Intent | ✅ Goal=comparison, Confidence=0.95, 变量名准确 | +| **P 层** — Plan | ✅ 3 步流程, PlannedTrace 完整 | +| **E 层** — R 引擎执行 | ✅ 3/3 步骤成功, 10 个 ReportBlocks | +| **R 层** — LLM 结论 | ✅ source=llm, 6 要素完整(摘要/发现/统计/方法/局限/建议) | +| 结论 API 缓存 | ✅ 14ms 缓存命中 | +| 第二条链路(相关分析) | ✅ 2/2 步骤成功, LLM 结论正确 | +| 错误分类验证 | ✅ 异常变量 confidence=0.4, 不崩溃 | +| **总计** | **40/40 通过, 0 失败** | + +--- + +## 4. 架构亮点 + +1. **四层降级体系** — 每层都有 fallback:Q→正则, P→硬编码, R→规则引擎, 前端→旧组件 +2. **LLM 三层防御** — `jsonrepair` → `JSON.parse` → `Zod Schema`,Q 层和 R 层共用范式 +3. **统计量槽位注入** — LLM 被剥夺生成数值的权限,所有 P 值/效应量来自 R 引擎实际输出 +4. **配置化驱动** — 决策表/流程模板/工具注册表均为 JSON,方法学团队可配置,`POST /reload` 热更新 +5. **PlannedTrace 审计** — P 层只生成策略("如果…则…"),E 层 R 引擎执行事实,R 层合并两者生成方法学说明 + +--- + +## 5. 下一步 + +| 阶段 | 内容 | 预计工时 | +|------|------|---------| +| **Phase Deploy** | 补齐 4 个原子 R 工具(ANOVA/Fisher/Wilcoxon/线性回归)+ 复合工具 ST_BASELINE_TABLE + 部署 | 37h | +| **Phase Q+** | 变量数据字典 + 变量选择确认面板(人机协同增强) | 20h | + +--- + +**文档维护者:** SSA 架构团队 +**关联文档:** `04-开发计划/10-QPER架构开发计划-智能化主线.md` diff --git a/extraction_service/operations/data_profile.py b/extraction_service/operations/data_profile.py index 2621a9b3..2fdd5297 100644 --- a/extraction_service/operations/data_profile.py +++ b/extraction_service/operations/data_profile.py @@ -99,9 +99,35 @@ def analyze_column(col: pd.Series, col_name: str, max_unique_values: int = 20) - elif col_type == 'datetime': profile.update(analyze_datetime_column(non_null)) + profile['isIdLike'] = _detect_id_like(col_name, col_type, unique_count, total_count) + return profile +import re + +_ID_PATTERNS = re.compile( + r'(_id|_no|_code|编号|序号|流水号|主键|record_date|visit_date|enroll_date)$|^(id|ID|Id)_|^(patient|subject|sample|record)_?id$', + re.IGNORECASE +) + + +def _detect_id_like(col_name: str, col_type: str, unique_count: int, total_count: int) -> bool: + """ + 判断列是否为非分析变量(ID / 高基数字符串 / 日期) + 标记为 True 后,Q 层 Context Pruning 会在注入 Prompt 前物理剔除这些列 + """ + if col_type == 'datetime': + return True + if _ID_PATTERNS.search(col_name): + return True + if col_type == 'text' and total_count > 0 and unique_count / total_count > 0.95: + return True + if col_type == 'categorical' and total_count > 0 and unique_count / total_count > 0.95: + return True + return False + + def infer_column_type(col: pd.Series, unique_count: int, total_count: int) -> str: """ 推断列的数据类型 diff --git a/frontend-v2/src/modules/aia/styles/chat-workspace.css b/frontend-v2/src/modules/aia/styles/chat-workspace.css index c4f1158e..33023890 100644 --- a/frontend-v2/src/modules/aia/styles/chat-workspace.css +++ b/frontend-v2/src/modules/aia/styles/chat-workspace.css @@ -983,9 +983,7 @@ .message-bubble .markdown-content h1 { font-size: 1.3em; -} - -.message-bubble .markdown-content h2 { +}.message-bubble .markdown-content h2 { font-size: 1.2em; }.message-bubble .markdown-content h3 { font-size: 1.1em; @@ -1022,4 +1020,4 @@ border-radius: 4px; font-family: 'Monaco', 'Consolas', 'Courier New', monospace; font-size: 0.9em; -} \ No newline at end of file +} diff --git a/frontend-v2/src/modules/ssa/components/ClarificationCard.tsx b/frontend-v2/src/modules/ssa/components/ClarificationCard.tsx new file mode 100644 index 00000000..f1d75967 --- /dev/null +++ b/frontend-v2/src/modules/ssa/components/ClarificationCard.tsx @@ -0,0 +1,69 @@ +/** + * ClarificationCard — Phase Q 追问卡片组件 + * + * 当 LLM 意图解析置信度低于阈值时,展示封闭式数据驱动选项卡。 + * 用户点击选项后自动补全 ParsedQuery 缺失字段,触发重新规划。 + */ +import React from 'react'; +import { HelpCircle } from 'lucide-react'; + +export interface ClarificationOption { + label: string; + value: string; + description?: string; +} + +export interface ClarificationCardData { + question: string; + options: ClarificationOption[]; +} + +interface ClarificationCardProps { + cards: ClarificationCardData[]; + onSelect: (selections: Record) => void; + disabled?: boolean; +} + +export const ClarificationCard: React.FC = ({ + cards, + onSelect, + disabled = false, +}) => { + const handleOptionClick = (question: string, value: string) => { + if (disabled) return; + onSelect({ [question]: value }); + }; + + if (!cards || cards.length === 0) return null; + + return ( +
+ {cards.map((card, idx) => ( +
+
+ + {card.question} +
+
+ {card.options.map((opt, oi) => ( + + ))} +
+
+ ))} +
+ ); +}; + +export default ClarificationCard; diff --git a/frontend-v2/src/modules/ssa/components/ConclusionReport.tsx b/frontend-v2/src/modules/ssa/components/ConclusionReport.tsx index 5f7aa67e..f8010cc7 100644 --- a/frontend-v2/src/modules/ssa/components/ConclusionReport.tsx +++ b/frontend-v2/src/modules/ssa/components/ConclusionReport.tsx @@ -1,14 +1,19 @@ /** - * 综合结论报告组件 - * - * Phase 2A: 多步骤工作流执行完成后的综合结论展示 + * 综合结论报告组件 (Phase R) + * + * 特性: + * - 逐 section 渐入动画(reflection_complete 到达后依次展现) + * - 一键复制全文到剪贴板 + * - 来源标识(LLM / 规则引擎) + * - 折叠/展开详细步骤结果 */ -import React, { useState } from 'react'; +import React, { useState, useEffect, useCallback, useRef } from 'react'; import type { ConclusionReport as ConclusionReportType, WorkflowStepResult } from '../types'; interface ConclusionReportProps { report: ConclusionReportType; stepResults?: WorkflowStepResult[]; + animate?: boolean; } interface StepResultDetailProps { @@ -26,9 +31,9 @@ const StepResultDetail: React.FC = ({ stepSummary, stepRe 步骤 {stepSummary.step_number} {stepSummary.tool_name} {stepSummary.p_value !== undefined && ( - = ({ stepSummary, stepRe - +
{stepSummary.summary}
- + {expanded && stepResult?.result && (
- {/* 结果表格 */} {stepResult.result.result_table && (
@@ -69,14 +73,13 @@ const StepResultDetail: React.FC = ({ stepSummary, stepRe )} - {/* 图表 */} {stepResult.result.plots && stepResult.result.plots.length > 0 && (
- {stepResult.result.plots.map((plot, idx) => ( + {(stepResult.result.plots as any[]).map((plot: any, idx: number) => (
{plot.title}
- {plot.title} @@ -85,11 +88,10 @@ const StepResultDetail: React.FC = ({ stepSummary, stepRe
)} - {/* 详细解释 */} {stepResult.result.interpretation && (
- 💡 解读: -

{stepResult.result.interpretation}

+ 解读: +

{String(stepResult.result.interpretation)}

)}
@@ -98,132 +100,246 @@ const StepResultDetail: React.FC = ({ stepSummary, stepRe ); }; -export const ConclusionReport: React.FC = ({ report, stepResults = [] }) => { +/** + * Build plain-text version of the report for clipboard + */ +function buildPlainText(report: ConclusionReportType): string { + const lines: string[] = []; + lines.push(report.title); + lines.push(''); + lines.push('【综合结论】'); + lines.push(report.executive_summary); + lines.push(''); + + if (report.key_findings.length > 0) { + lines.push('【主要发现】'); + report.key_findings.forEach((f, i) => lines.push(`${i + 1}. ${f}`)); + lines.push(''); + } + + if (report.step_summaries.length > 0) { + lines.push('【分析步骤】'); + report.step_summaries.forEach(s => { + const sig = s.is_significant ? '(显著)' : ''; + const pStr = s.p_value != null ? `, P=${s.p_value < 0.001 ? '<0.001' : s.p_value.toFixed(4)}` : ''; + lines.push(` 步骤${s.step_number} ${s.tool_name}: ${s.summary}${pStr} ${sig}`); + }); + lines.push(''); + } + + if (report.recommendations && report.recommendations.length > 0) { + lines.push('【建议】'); + report.recommendations.forEach((r, i) => lines.push(`${i + 1}. ${r}`)); + lines.push(''); + } + + if (report.limitations.length > 0) { + lines.push('【局限性】'); + report.limitations.forEach((l, i) => lines.push(`${i + 1}. ${l}`)); + lines.push(''); + } + + lines.push(`生成时间: ${new Date(report.generated_at).toLocaleString('zh-CN')}`); + if (report.source) { + lines.push(`生成方式: ${report.source === 'llm' ? 'AI 智能分析' : '规则引擎'}`); + } + + return lines.join('\n'); +} + +export const ConclusionReport: React.FC = ({ + report, + stepResults = [], + animate = false, +}) => { const [showFullReport, setShowFullReport] = useState(true); + const [copySuccess, setCopySuccess] = useState(false); + const [visibleSections, setVisibleSections] = useState>( + animate ? new Set() : new Set(['summary', 'findings', 'stats', 'steps', 'recommendations', 'limitations', 'methods']) + ); + const animationTimer = useRef[]>([]); + + useEffect(() => { + if (!animate) return; + + const sections = ['summary', 'findings', 'stats', 'steps', 'recommendations', 'limitations', 'methods']; + const delays = [0, 300, 600, 900, 1100, 1300, 1500]; + + sections.forEach((section, idx) => { + const timer = setTimeout(() => { + setVisibleSections(prev => new Set([...prev, section])); + }, delays[idx]); + animationTimer.current.push(timer); + }); + + return () => { + animationTimer.current.forEach(clearTimeout); + }; + }, [animate]); + + const handleCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(buildPlainText(report)); + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + } catch { + const textarea = document.createElement('textarea'); + textarea.value = buildPlainText(report); + document.body.appendChild(textarea); + textarea.select(); + document.execCommand('copy'); + document.body.removeChild(textarea); + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + } + }, [report]); const getStepResult = (stepNumber: number): WorkflowStepResult | undefined => { return stepResults.find(r => r.step_number === stepNumber); }; + const sectionClass = (name: string) => + `conclusion-section ${visibleSections.has(name) ? 'conclusion-section-visible' : 'conclusion-section-hidden'}`; + return (
- {/* 报告头部 */} + {/* Header */}
-

📋 {report.title}

- - {new Date(report.generated_at).toLocaleString('zh-CN')} - -
- - {/* AI 总结摘要 - 始终显示 */} -
-
- 🤖 - AI 综合结论 -
-
- {report.executive_summary} -
-
- - {/* 主要发现 */} - {report.key_findings.length > 0 && ( -
-
- 🎯 - 主要发现 +
+

{report.title}

+
+ + {new Date(report.generated_at).toLocaleString('zh-CN')} + + {report.source && ( + + {report.source === 'llm' ? 'AI 生成' : '规则生成'} + + )} +
+
+ +
+ + {/* Executive Summary */} +
+
+
+ 综合结论 +
+
{report.executive_summary}
+
+
+ + {/* Key Findings */} + {report.key_findings.length > 0 && ( +
+
+
+ 主要发现 +
+
    + {report.key_findings.map((finding, idx) => ( +
  • {finding}
  • + ))} +
-
    - {report.key_findings.map((finding, idx) => ( -
  • {finding}
  • - ))} -
)} - {/* 统计概览 */} -
-
- 📊 - {report.statistical_summary.total_tests} - 统计检验 -
-
- - {report.statistical_summary.significant_results} - 显著结果 -
-
- 🔬 - {report.statistical_summary.methods_used.length} - 分析方法 + {/* Stats Overview */} +
+
+
+ {report.statistical_summary.total_tests} + 统计检验 +
+
+ {report.statistical_summary.significant_results} + 显著结果 +
+
+ {report.statistical_summary.methods_used.length} + 分析方法 +
- {/* 展开/折叠按钮 */} - - {/* 详细步骤结果 */} + {/* Step Results */} {showFullReport && ( -
-
- 📝 - 详细分析结果 -
-
- {report.step_summaries.map((stepSummary) => ( - - ))} +
+
+
+ 详细分析结果 +
+
+ {report.step_summaries.map((stepSummary) => ( + + ))} +
)} - {/* 建议 */} - {report.recommendations.length > 0 && ( -
-
- 💡 - 建议 + {/* Recommendations */} + {report.recommendations && report.recommendations.length > 0 && ( +
+
+
+ 建议 +
+
    + {report.recommendations.map((rec, idx) => ( +
  • {rec}
  • + ))} +
-
    - {report.recommendations.map((rec, idx) => ( -
  • {rec}
  • - ))} -
)} - {/* 局限性 */} + {/* Limitations */} {report.limitations.length > 0 && ( -
-
- ⚠️ - 局限性 +
+
+
+ 局限性 +
+
    + {report.limitations.map((lim, idx) => ( +
  • {lim}
  • + ))} +
-
    - {report.limitations.map((lim, idx) => ( -
  • {lim}
  • - ))} -
)} - {/* 使用的方法列表 */} -
- 使用的分析方法: -
- {report.statistical_summary.methods_used.map((method, idx) => ( - {method} - ))} + {/* Methods */} +
+
+ 使用的分析方法: +
+ {report.statistical_summary.methods_used.map((method, idx) => ( + {method} + ))} +
diff --git a/frontend-v2/src/modules/ssa/components/DynamicReport.tsx b/frontend-v2/src/modules/ssa/components/DynamicReport.tsx new file mode 100644 index 00000000..36e2cf46 --- /dev/null +++ b/frontend-v2/src/modules/ssa/components/DynamicReport.tsx @@ -0,0 +1,183 @@ +/** + * DynamicReport — Block-based 动态报告渲染组件 + * + * 消费后端/R 端返回的 ReportBlock[] 数组,按序渲染: + * markdown → 富文本段落 + * table → 三线表 + * image → base64 图片 + * key_value → 键值对网格 + */ +import React, { useState } from 'react'; +import { ImageOff, Loader2 } from 'lucide-react'; +import type { ReportBlock } from '../types'; + +interface DynamicReportProps { + blocks: ReportBlock[]; + className?: string; +} + +export const DynamicReport: React.FC = ({ blocks, className }) => { + if (!blocks || blocks.length === 0) return null; + + return ( +
+ {blocks.map((block, idx) => ( + + ))} +
+ ); +}; + +const DynamicBlock: React.FC<{ block: ReportBlock; index: number }> = ({ block, index }) => { + switch (block.type) { + case 'key_value': + return ; + case 'table': + return ; + case 'image': + return ; + case 'markdown': + return ; + default: + return null; + } +}; + +/* ─── key_value ─── */ +const KVBlock: React.FC<{ block: ReportBlock }> = ({ block }) => { + const items = block.items ?? []; + if (items.length === 0) return null; + + return ( +
+ {block.title &&

{block.title}

} +
+ {items.map((item, i) => ( +
+
{item.key}
+
{item.value}
+
+ ))} +
+
+ ); +}; + +/* ─── table ─── */ +const TableBlock: React.FC<{ block: ReportBlock; index: number }> = ({ block, index }) => { + const headers = block.headers ?? []; + const rows = block.rows ?? []; + if (headers.length === 0 && rows.length === 0) return null; + + return ( +
+ {block.title && ( +

+ Table {index + 1}. {block.title} +

+ )} +
+
+ {headers.length > 0 && ( + + + {headers.map((h, i) => ( + + ))} + + + )} + + {rows.map((row, ri) => ( + + {row.map((cell, ci) => ( + + ))} + + ))} + +
{h}
+ {formatTableCell(cell)} +
+
+ {block.footnote &&

{block.footnote}

} +
+ ); +}; + +/* ─── image ─── */ +const ImageBlock: React.FC<{ block: ReportBlock; index: number }> = ({ block, index }) => { + const [hasError, setHasError] = useState(false); + const [isLoading, setIsLoading] = useState(true); + + const src = React.useMemo(() => { + if (!block.data) return ''; + if (block.data.startsWith('data:')) return block.data; + return `data:image/png;base64,${block.data}`; + }, [block.data]); + + if (hasError || !block.data) { + return ( +
+ + 图片加载失败 +
+ ); + } + + return ( +
+

+ Figure {index + 1}. {block.title ?? block.alt ?? '可视化'} +

+
+ {isLoading && ( +
+ +
+ )} + {block.alt setIsLoading(false)} + onError={() => { setHasError(true); setIsLoading(false); }} + /> +
+
+ ); +}; + +/* ─── markdown ─── */ +const MarkdownBlock: React.FC<{ block: ReportBlock }> = ({ block }) => { + if (!block.content) return null; + + return ( +
+ {block.title &&

{block.title}

} +
+ {block.content.split('\n').map((line, i) => ( +

{line}

+ ))} +
+
+ ); +}; + +/* ─── helpers ─── */ +const isPValueCell = (cell: string | number): boolean => { + if (typeof cell === 'string') { + return cell.includes('<') || cell.includes('*'); + } + return false; +}; + +const formatTableCell = (cell: string | number): string => { + if (cell === null || cell === undefined) return '-'; + if (typeof cell === 'number') { + return Number.isInteger(cell) ? cell.toString() : cell.toFixed(4); + } + return String(cell); +}; + +export default DynamicReport; diff --git a/frontend-v2/src/modules/ssa/components/SSAChatPane.tsx b/frontend-v2/src/modules/ssa/components/SSAChatPane.tsx index 9b548825..30ce15e6 100644 --- a/frontend-v2/src/modules/ssa/components/SSAChatPane.tsx +++ b/frontend-v2/src/modules/ssa/components/SSAChatPane.tsx @@ -29,6 +29,8 @@ import { useWorkflow } from '../hooks/useWorkflow'; import type { SSAMessage } from '../types'; import { TypeWriter } from './TypeWriter'; import { DataProfileCard } from './DataProfileCard'; +import { ClarificationCard } from './ClarificationCard'; +import type { ClarificationCardData, IntentResult } from '../types'; export const SSAChatPane: React.FC = () => { const navigate = useNavigate(); @@ -46,15 +48,21 @@ export const SSAChatPane: React.FC = () => { error, setError, addToast, + addMessage, selectAnalysisRecord, dataProfile, dataProfileLoading, } = useSSAStore(); const { uploadData, generatePlan, isUploading, uploadProgress } = useAnalysis(); - const { generateDataProfile, generateWorkflowPlan, isProfileLoading, isPlanLoading } = useWorkflow(); + const { generateDataProfile, generateWorkflowPlan, parseIntent, handleClarify, isProfileLoading, isPlanLoading } = useWorkflow(); const [inputValue, setInputValue] = useState(''); const [uploadStatus, setUploadStatus] = useState<'idle' | 'uploading' | 'parsing' | 'success' | 'error'>('idle'); + const [pendingClarification, setPendingClarification] = useState<{ + cards: ClarificationCardData[]; + originalQuery: string; + intent: IntentResult; + } | null>(null); const fileInputRef = useRef(null); const chatEndRef = useRef(null); const messagesContainerRef = useRef(null); @@ -143,20 +151,83 @@ export const SSAChatPane: React.FC = () => { const handleSend = async () => { if (!inputValue.trim()) return; + const query = inputValue; + setInputValue(''); + try { - // Phase 2A: 如果已有 session,使用多步骤工作流规划 if (currentSession?.id) { - await generateWorkflowPlan(currentSession.id, inputValue); + // Phase Q: 先做意图解析,低置信度时追问 + const intentResp = await parseIntent(currentSession.id, query); + + if (intentResp.needsClarification && intentResp.clarificationCards?.length > 0) { + addMessage({ + id: crypto.randomUUID(), + role: 'user', + content: query, + createdAt: new Date().toISOString(), + }); + addMessage({ + id: crypto.randomUUID(), + role: 'assistant', + content: `我大致理解了你的意图(${intentResp.intent.reasoning}),但为了生成更精确的分析方案,想确认几个细节:`, + createdAt: new Date().toISOString(), + }); + setPendingClarification({ + cards: intentResp.clarificationCards, + originalQuery: query, + intent: intentResp.intent, + }); + return; + } + + // 置信度足够 → 直接生成工作流计划 + await generateWorkflowPlan(currentSession.id, query); } else { - // 没有数据时,使用旧流程 - await generatePlan(inputValue); + await generatePlan(query); } - setInputValue(''); } catch (err: any) { addToast(err?.message || '生成计划失败', 'error'); } }; + const handleClarificationSelect = async (selections: Record) => { + if (!currentSession?.id || !pendingClarification) return; + + setPendingClarification(null); + + const selectedLabel = Object.values(selections).join(', '); + addMessage({ + id: crypto.randomUUID(), + role: 'user', + content: selectedLabel, + createdAt: new Date().toISOString(), + }); + + try { + const resp = await handleClarify( + currentSession.id, + pendingClarification.originalQuery, + selections + ); + + if (resp.needsClarification && resp.clarificationCards?.length > 0) { + addMessage({ + id: crypto.randomUUID(), + role: 'assistant', + content: '还需要确认一下:', + createdAt: new Date().toISOString(), + }); + setPendingClarification({ + cards: resp.clarificationCards, + originalQuery: pendingClarification.originalQuery, + intent: resp.intent, + }); + } + } catch (err: any) { + addToast(err?.message || '处理追问失败', 'error'); + } + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); @@ -308,6 +379,20 @@ export const SSAChatPane: React.FC = () => { 旧的"数据挂载成功消息"已移除,避免在没有用户输入时就显示 SAP 卡片 */} + {/* Phase Q: 追问卡片 */} + {pendingClarification && ( +
+
+
+ +
+
+ )} + {/* 自动滚动锚点与占位垫片 - 解决底部输入框遮挡的终极方案 */}
diff --git a/frontend-v2/src/modules/ssa/components/SSAWorkspacePane.tsx b/frontend-v2/src/modules/ssa/components/SSAWorkspacePane.tsx index 1e63d402..1d720be3 100644 --- a/frontend-v2/src/modules/ssa/components/SSAWorkspacePane.tsx +++ b/frontend-v2/src/modules/ssa/components/SSAWorkspacePane.tsx @@ -27,8 +27,10 @@ import { import { useSSAStore } from '../stores/ssaStore'; import { useAnalysis } from '../hooks/useAnalysis'; import { useWorkflow } from '../hooks/useWorkflow'; -import type { TraceStep } from '../types'; +import type { TraceStep, ReportBlock } from '../types'; import { WorkflowTimeline } from './WorkflowTimeline'; +import { DynamicReport } from './DynamicReport'; +import { exportBlocksToWord } from '../utils/exportBlocksToWord'; type ExecutionPhase = 'planning' | 'executing' | 'completed' | 'error'; @@ -137,7 +139,21 @@ export const SSAWorkspacePane: React.FC = () => { const handleExportReport = async () => { try { - await exportReport(); + // 优先使用 block-based 导出 + const allBlocks = workflowSteps + .filter(s => s.status === 'success') + .flatMap(s => { + const r = s.result as any; + return s.reportBlocks ?? r?.report_blocks ?? []; + }) as ReportBlock[]; + + if (allBlocks.length > 0) { + await exportBlocksToWord(allBlocks, { + title: workflowPlan?.title || currentSession?.title || '统计分析报告', + }); + } else { + await exportReport(); + } addToast('报告导出成功', 'success'); } catch (err: any) { addToast(err?.message || '导出失败', 'error'); @@ -521,13 +537,13 @@ export const SSAWorkspacePane: React.FC = () => { )} - {/* 各步骤结果汇总 - MVP 风格 */} + {/* 各步骤结果汇总 — 优先 Block-based,fallback 旧 MVP 风格 */} {workflowSteps.filter(s => s.status === 'success' && s.result).map((step, stepIdx) => { const r = step.result as any; - const pVal = r?.p_value ?? r?.pValue; - const pFmt = r?.p_value_fmt || (pVal !== undefined ? formatPValue(pVal) : undefined); - const isDescriptive = step.tool_code === 'ST_DESCRIPTIVE' || r?.method === '描述性统计'; - + const blocks: ReportBlock[] | undefined = + step.reportBlocks ?? r?.report_blocks; + const hasBlocks = blocks && blocks.length > 0; + return (

@@ -535,134 +551,11 @@ export const SSAWorkspacePane: React.FC = () => { {step.duration_ms && 耗时 {step.duration_ms}ms}

- {/* 描述性统计 - 专用渲染 */} - {isDescriptive ? ( - + {/* Block-based 渲染(优先) */} + {hasBlocks ? ( + ) : ( - <> - {/* 非描述性统计 - 统计量汇总 */} -
-
-
统计方法
-
{r?.method || step.tool_name}
-
- {r?.statistic !== undefined && ( -
-
统计量
-
{Number(r.statistic).toFixed(4)}
-
- )} - {pVal !== undefined && ( -
-
P 值
-
- {pFmt} -
-
- )} - {r?.effect_size !== undefined && ( -
-
效应量
-
- {typeof r.effect_size === 'object' - ? Object.entries(r.effect_size).map(([k, v]) => `${k}=${Number(v).toFixed(3)}`).join(', ') - : Number(r.effect_size).toFixed(3)} -
-
- )} - {r?.conf_int && ( -
-
95% CI
-
[{r.conf_int.map((v: number) => v.toFixed(3)).join(', ')}]
-
- )} - {r?.coefficients && !Array.isArray(r.coefficients) && ( -
-
系数数量
-
{Object.keys(r.coefficients).length}
-
- )} -
- - {/* 分组统计表 */} - {r?.group_stats?.length > 0 && ( -
-

分组统计

-
- - - - - - {r.group_stats.map((g: any, i: number) => ( - - - - - - - ))} - -
分组N均值标准差
{g.group}{g.n}{g.mean !== undefined ? Number(g.mean).toFixed(4) : '-'}{g.sd !== undefined ? Number(g.sd).toFixed(4) : '-'}
-
-
- )} - - {/* Logistic 回归系数表 */} - {r?.coefficients && Array.isArray(r.coefficients) && r.coefficients.length > 0 && ( -
-

回归系数

-
- - - - - - {r.coefficients.map((c: any, i: number) => ( - - - - - - - - ))} - -
变量估计值OR95% CIP 值
{c.variable || c.term}{Number(c.estimate || c.coef || 0).toFixed(4)}{c.OR !== undefined ? Number(c.OR).toFixed(4) : '-'}{c.ci_lower !== undefined ? `[${Number(c.ci_lower).toFixed(3)}, ${Number(c.ci_upper).toFixed(3)}]` : '-'}{c.p_value_fmt || formatPValue(c.p_value)}
-
-
- )} - - {/* 详细表格数据 result_table */} - {r?.result_table && ( -
-
- - {r.result_table.headers.map((h: string, i: number) => )} - {r.result_table.rows.map((row: any[], i: number) => ( - {row.map((cell, j) => ( - - ))} - ))} -
{h}
{formatCell(cell)}
-
-
- )} - - )} - - {/* 图表 - 所有类型通用 */} - {r?.plots?.length > 0 && ( -
-

Figure {stepIdx + 1}. 可视化

- {r.plots.map((plot: any, plotIdx: number) => ( - - ))} -
+ )}
); @@ -1116,4 +1009,145 @@ const DescriptiveResultView: React.FC<{ result: any }> = ({ result }) => { ); }; +/** + * LegacyStepResultView — 旧版(非 Block-based)步骤结果渲染 + * 当 R 工具未返回 report_blocks 时作为 fallback 使用 + */ +const LegacyStepResultView: React.FC<{ step: any; stepIdx: number }> = ({ step, stepIdx }) => { + const r = step.result as any; + if (!r) return null; + + const pVal = r?.p_value ?? r?.pValue; + const pFmt = r?.p_value_fmt || (pVal !== undefined ? formatPValue(pVal) : undefined); + const isDescriptive = step.tool_code === 'ST_DESCRIPTIVE' || r?.method === '描述性统计'; + + return ( + <> + {isDescriptive ? ( + + ) : ( + <> +
+
+
统计方法
+
{r?.method || step.tool_name}
+
+ {r?.statistic !== undefined && ( +
+
统计量
+
{Number(r.statistic).toFixed(4)}
+
+ )} + {pVal !== undefined && ( +
+
P 值
+
+ {pFmt} +
+
+ )} + {r?.effect_size !== undefined && ( +
+
效应量
+
+ {typeof r.effect_size === 'object' + ? Object.entries(r.effect_size).map(([k, v]) => `${k}=${Number(v).toFixed(3)}`).join(', ') + : Number(r.effect_size).toFixed(3)} +
+
+ )} + {r?.conf_int && ( +
+
95% CI
+
[{r.conf_int.map((v: number) => v.toFixed(3)).join(', ')}]
+
+ )} + {r?.coefficients && !Array.isArray(r.coefficients) && ( +
+
系数数量
+
{Object.keys(r.coefficients).length}
+
+ )} +
+ + {r?.group_stats?.length > 0 && ( +
+

分组统计

+
+ + + + + + {r.group_stats.map((g: any, i: number) => ( + + + + + + + ))} + +
分组N均值标准差
{g.group}{g.n}{g.mean !== undefined ? Number(g.mean).toFixed(4) : '-'}{g.sd !== undefined ? Number(g.sd).toFixed(4) : '-'}
+
+
+ )} + + {r?.coefficients && Array.isArray(r.coefficients) && r.coefficients.length > 0 && ( +
+

回归系数

+
+ + + + + + {r.coefficients.map((c: any, i: number) => ( + + + + + + + + ))} + +
变量估计值OR95% CIP 值
{c.variable || c.term}{Number(c.estimate || c.coef || 0).toFixed(4)}{c.OR !== undefined ? Number(c.OR).toFixed(4) : '-'}{c.ci_lower !== undefined ? `[${Number(c.ci_lower).toFixed(3)}, ${Number(c.ci_upper).toFixed(3)}]` : '-'}{c.p_value_fmt || formatPValue(c.p_value)}
+
+
+ )} + + {r?.result_table && ( +
+
+ + {r.result_table.headers.map((h: string, i: number) => )} + {r.result_table.rows.map((row: any[], i: number) => ( + {row.map((cell, j) => ( + + ))} + ))} +
{h}
{formatCell(cell)}
+
+
+ )} + + )} + + {r?.plots?.length > 0 && ( +
+

Figure {stepIdx + 1}. 可视化

+ {r.plots.map((plot: any, plotIdx: number) => ( + + ))} +
+ )} + + ); +}; + export default SSAWorkspacePane; diff --git a/frontend-v2/src/modules/ssa/components/WorkflowTimeline.tsx b/frontend-v2/src/modules/ssa/components/WorkflowTimeline.tsx index f4fcdac1..bf58d608 100644 --- a/frontend-v2/src/modules/ssa/components/WorkflowTimeline.tsx +++ b/frontend-v2/src/modules/ssa/components/WorkflowTimeline.tsx @@ -75,12 +75,21 @@ const StepItem: React.FC = ({ step, result, isLast, isCurrent }) 步骤 {step.step_number} {getToolIcon(step.tool_code)} {step.tool_name} + {step.is_sensitivity && ( + 敏感性分析 + )} {result?.duration_ms && ( {result.duration_ms}ms )}
{step.description}
+ + {step.switch_condition && ( +
+ 🛡️ 护栏:{step.switch_condition} +
+ )} {step.params && Object.keys(step.params).length > 0 && (
@@ -146,6 +155,23 @@ export const WorkflowTimeline: React.FC = ({
+ {plan.epv_warning && ( +
+ ⚠️ + {plan.epv_warning} +
+ )} + + {plan.planned_trace?.fallbackTool && ( +
+ 🛡️ + + 主方法:{plan.planned_trace.primaryTool} +  → 若{plan.planned_trace.switchCondition}则自动降级为 {plan.planned_trace.fallbackTool} + +
+ )} + {isExecuting && (
diff --git a/frontend-v2/src/modules/ssa/hooks/useWorkflow.ts b/frontend-v2/src/modules/ssa/hooks/useWorkflow.ts index 77b33025..94b001d1 100644 --- a/frontend-v2/src/modules/ssa/hooks/useWorkflow.ts +++ b/frontend-v2/src/modules/ssa/hooks/useWorkflow.ts @@ -13,13 +13,25 @@ import type { WorkflowStepResult, SSEMessage, SSAMessage, + IntentResult, + ClarificationCardData, } from '../types'; const API_BASE = '/api/v1/ssa'; +interface IntentResponse { + success: boolean; + intent: IntentResult; + needsClarification: boolean; + clarificationCards: ClarificationCardData[]; + plan?: WorkflowPlan; +} + interface UseWorkflowReturn { generateDataProfile: (sessionId: string) => Promise; generateWorkflowPlan: (sessionId: string, query: string) => Promise; + parseIntent: (sessionId: string, query: string) => Promise; + handleClarify: (sessionId: string, userQuery: string, selections: Record) => Promise; executeWorkflow: (sessionId: string, workflowId: string) => Promise; cancelWorkflow: () => void; isProfileLoading: boolean; @@ -147,8 +159,78 @@ export function useWorkflow(): UseWorkflowReturn { addToast ]); + /** + * Phase Q: 解析用户意图(不直接生成计划) + */ + const parseIntent = useCallback(async ( + sessionId: string, + query: string + ): Promise => { + setWorkflowPlanLoading(true); + setError(null); + + try { + const response = await apiClient.post(`${API_BASE}/workflow/intent`, { + sessionId, + userQuery: query, + }); + return response.data as IntentResponse; + } catch (error: any) { + const errorMsg = error.response?.data?.message || error.message || '意图解析失败'; + setError(errorMsg); + throw error; + } finally { + setWorkflowPlanLoading(false); + } + }, [setWorkflowPlanLoading, setError]); + + /** + * Phase Q: 处理用户追问回答 + */ + const handleClarify = useCallback(async ( + sessionId: string, + userQuery: string, + selections: Record + ): Promise => { + setWorkflowPlanLoading(true); + setError(null); + + try { + const response = await apiClient.post(`${API_BASE}/workflow/clarify`, { + sessionId, + userQuery, + selections, + }); + + const data = response.data as IntentResponse; + + if (data.plan) { + setWorkflowPlan(data.plan); + setActivePane('sap'); + setWorkspaceOpen(true); + setIsWorkflowMode(true); + + addMessage({ + id: crypto.randomUUID(), + role: 'assistant', + content: `已生成分析方案:${data.plan.title}\n共 ${data.plan.total_steps} 个分析步骤`, + artifactType: 'sap', + createdAt: new Date().toISOString(), + }); + } + + return data; + } catch (error: any) { + const errorMsg = error.response?.data?.message || error.message || '处理追问失败'; + setError(errorMsg); + throw error; + } finally { + setWorkflowPlanLoading(false); + } + }, [setWorkflowPlanLoading, setError, setWorkflowPlan, setActivePane, setWorkspaceOpen, setIsWorkflowMode, addMessage]); + const executeWorkflow = useCallback(async ( - sessionId: string, + _sessionId: string, workflowId: string ): Promise => { setExecuting(true); @@ -238,12 +320,16 @@ export function useWorkflow(): UseWorkflowReturn { if (stepNumber !== undefined) { const result = message.result || message.data?.result; const durationMs = message.duration_ms || message.durationMs || message.data?.duration_ms; + const reportBlocks = message.reportBlocks + || (result as any)?.report_blocks + || message.data?.reportBlocks; updateWorkflowStep(stepNumber, { status: message.status || message.data?.status || 'success', completed_at: new Date().toISOString(), duration_ms: durationMs, result: result, + reportBlocks: reportBlocks || undefined, }); const totalSteps = message.total_steps || message.totalSteps || 2; @@ -365,6 +451,8 @@ export function useWorkflow(): UseWorkflowReturn { return { generateDataProfile, generateWorkflowPlan, + parseIntent, + handleClarify, executeWorkflow, cancelWorkflow, isProfileLoading: dataProfileLoading, diff --git a/frontend-v2/src/modules/ssa/styles/ssa-workspace.css b/frontend-v2/src/modules/ssa/styles/ssa-workspace.css index 22edcb90..dac2a87c 100644 --- a/frontend-v2/src/modules/ssa/styles/ssa-workspace.css +++ b/frontend-v2/src/modules/ssa/styles/ssa-workspace.css @@ -2953,6 +2953,64 @@ text-align: center; } +.workflow-timeline .sensitivity-badge { + display: inline-block; + padding: 1px 8px; + font-size: 11px; + font-weight: 600; + color: #d97706; + background: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 10px; + margin-left: 6px; +} + +.workflow-timeline .step-guardrail { + margin-top: 4px; + padding: 4px 8px; + font-size: 12px; + color: #1d4ed8; + background: #eff6ff; + border-left: 3px solid #3b82f6; + border-radius: 4px; +} + +.workflow-timeline .epv-warning-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + margin-bottom: 12px; + font-size: 13px; + color: #92400e; + background: #fffbeb; + border: 1px solid #fcd34d; + border-radius: 8px; +} + +.workflow-timeline .epv-warning-banner .epv-icon { + font-size: 16px; + flex-shrink: 0; +} + +.workflow-timeline .guardrail-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + margin-bottom: 12px; + font-size: 13px; + color: #1e40af; + background: #eff6ff; + border: 1px solid #93c5fd; + border-radius: 8px; +} + +.workflow-timeline .guardrail-banner .guardrail-icon { + font-size: 16px; + flex-shrink: 0; +} + .workflow-timeline .ready-hint { font-size: 13px; color: #64748b; @@ -3671,3 +3729,222 @@ border-radius: 4px; margin-left: auto; } + +/* ========================================== */ +/* DynamicReport — Block-based 动态报告 */ +/* ========================================== */ + +.dynamic-report { + display: flex; + flex-direction: column; + gap: 20px; +} + +.dr-section-title { + font-size: 13px; + font-weight: 600; + color: #334155; + margin: 0 0 8px 0; +} + +.dr-markdown-section { + line-height: 1.7; +} + +.dr-markdown-content p { + margin: 0 0 4px 0; + font-size: 13px; + color: #475569; +} + +.dr-markdown-content p:empty { + display: none; +} + +.dr-table-footnote { + font-size: 11px; + color: #94a3b8; + margin: 4px 0 0 0; + font-style: italic; +} + +.dr-kv-section { + margin: 0; +} + +/* ========================================== */ +/* ClarificationCard — Phase Q 追问卡片 */ +/* ========================================== */ + +.clarification-cards { + display: flex; + flex-direction: column; + gap: 12px; + margin: 8px 0; +} + +.clarification-card { + background: #f0f7ff; + border: 1px solid #bfdbfe; + border-radius: 12px; + padding: 14px 16px; +} + +.clarification-question { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + color: #1e40af; + margin-bottom: 10px; +} + +.clarification-options { + display: flex; + flex-wrap: wrap; + gap: 8px; +} + +.clarification-option-btn { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 2px; + background: #ffffff; + border: 1px solid #93c5fd; + border-radius: 8px; + padding: 8px 14px; + cursor: pointer; + transition: all 0.15s ease; + text-align: left; +} + +.clarification-option-btn:hover:not(:disabled) { + background: #dbeafe; + border-color: #3b82f6; + transform: translateY(-1px); +} + +.clarification-option-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.clarification-option-btn .option-label { + font-size: 13px; + font-weight: 500; + color: #1e40af; +} + +.clarification-option-btn .option-desc { + font-size: 11px; + color: #64748b; + line-height: 1.3; +} + +/* ============================================ + * Phase R: ConclusionReport 渐入动画 + 增强样式 + * ============================================ */ + +.conclusion-section { + transition: opacity 0.4s ease, transform 0.4s ease; +} + +.conclusion-section-hidden { + opacity: 0; + transform: translateY(12px); +} + +.conclusion-section-visible { + opacity: 1; + transform: translateY(0); +} + +.report-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; +} + +.report-header-left { + flex: 1; +} + +.report-meta { + display: flex; + align-items: center; + gap: 8px; + margin-top: 4px; +} + +.source-badge { + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + font-weight: 500; +} + +.source-llm { + background: #ede9fe; + color: #7c3aed; +} + +.source-rule_based { + background: #f0fdf4; + color: #16a34a; +} + +.copy-btn { + padding: 6px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + background: #fff; + color: #374151; + font-size: 12px; + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.copy-btn:hover { + background: #f3f4f6; + border-color: #9ca3af; +} + +.copy-btn.copy-success { + background: #ecfdf5; + border-color: #86efac; + color: #16a34a; +} + +.reflecting-indicator { + display: flex; + align-items: center; + gap: 10px; + padding: 16px; + background: linear-gradient(135deg, #faf5ff, #ede9fe); + border: 1px solid #ddd6fe; + border-radius: 10px; + margin: 12px 0; +} + +.reflecting-indicator .reflecting-spinner { + width: 20px; + height: 20px; + border: 2px solid #c4b5fd; + border-top-color: #7c3aed; + border-radius: 50%; + animation: reflecting-spin 0.8s linear infinite; +} + +@keyframes reflecting-spin { + to { transform: rotate(360deg); } +} + +.reflecting-indicator .reflecting-text { + font-size: 13px; + color: #6d28d9; + font-weight: 500; +} diff --git a/frontend-v2/src/modules/ssa/types/index.ts b/frontend-v2/src/modules/ssa/types/index.ts index 314b0549..72c92936 100644 --- a/frontend-v2/src/modules/ssa/types/index.ts +++ b/frontend-v2/src/modules/ssa/types/index.ts @@ -233,6 +233,19 @@ export interface WorkflowStepDef { params: Record; depends_on?: number[]; fallback_tool?: string; + is_sensitivity?: boolean; + switch_condition?: string | null; +} + +/** P 层策略日志 */ +export interface PlannedTrace { + matchedRule: string; + primaryTool: string; + fallbackTool: string | null; + switchCondition: string | null; + templateUsed: string; + reasoning: string; + epvWarning: string | null; } /** 工作流计划 */ @@ -245,6 +258,8 @@ export interface WorkflowPlan { steps: WorkflowStepDef[]; estimated_time_seconds?: number; created_at: string; + planned_trace?: PlannedTrace; + epv_warning?: string | null; } /** 工作流步骤执行状态 */ @@ -269,14 +284,16 @@ export interface WorkflowStepResult { headers: string[]; rows: (string | number)[][]; }; + report_blocks?: ReportBlock[]; plots?: PlotData[]; [key: string]: unknown; }; + reportBlocks?: ReportBlock[]; error?: string; logs: string[]; } -/** 综合结论报告 */ +/** 综合结论报告(Phase R 统一格式,前后端对齐) */ export interface ConclusionReport { workflow_id: string; title: string; @@ -297,10 +314,59 @@ export interface ConclusionReport { recommendations: string[]; limitations: string[]; generated_at: string; + source?: 'llm' | 'rule_based'; } -/** SSE 消息类型 */ -export type SSEMessageType = 'connected' | 'step_start' | 'step_progress' | 'step_complete' | 'step_error' | 'workflow_complete' | 'workflow_error'; +// ============================================ +// Block-based 输出协议(与后端 ReportBlock 对应) +// ============================================ + +export interface ReportBlock { + type: 'markdown' | 'table' | 'image' | 'key_value'; + title?: string; + content?: string; // markdown + headers?: string[]; // table + rows?: (string | number)[][]; // table + footnote?: string; // table + data?: string; // image (base64 data URI) + alt?: string; // image + items?: { key: string; value: string }[]; // key_value +} + +// ============================================ +// Phase Q: Intent / Clarification 类型 +// ============================================ + +export interface IntentResult { + goal: string; + outcome_var: string | null; + outcome_type: string | null; + predictor_vars: string[]; + grouping_var: string | null; + design: string; + confidence: number; + reasoning: string; + needsClarification: boolean; + clarificationCards?: ClarificationCardData[]; +} + +export interface ClarificationCardData { + question: string; + options: ClarificationOptionData[]; +} + +export interface ClarificationOptionData { + label: string; + value: string; + description?: string; +} + +/** SSE 消息类型(Phase R 扩展:qper_status + reflection_complete) */ +export type SSEMessageType = + | 'connected' + | 'step_start' | 'step_progress' | 'step_complete' | 'step_error' + | 'workflow_complete' | 'workflow_error' + | 'qper_status' | 'reflection_complete'; /** SSE 消息 */ export interface SSEMessage { @@ -317,6 +383,7 @@ export interface SSEMessage { durationMs?: number; duration_ms?: number; result?: Record; + reportBlocks?: ReportBlock[]; // 兼容嵌套格式 data?: WorkflowStepResult & { tool_code?: string; diff --git a/frontend-v2/src/modules/ssa/utils/exportBlocksToWord.ts b/frontend-v2/src/modules/ssa/utils/exportBlocksToWord.ts new file mode 100644 index 00000000..5efe9334 --- /dev/null +++ b/frontend-v2/src/modules/ssa/utils/exportBlocksToWord.ts @@ -0,0 +1,284 @@ +/** + * exportBlocksToWord — 将 ReportBlock[] 导出为 Word (.docx) + * + * 与 DynamicReport.tsx 对应:将 block-based 报告序列化为 docx 格式。 + */ +import { + Document, + Packer, + Paragraph, + Table, + TableRow, + TableCell, + TextRun, + HeadingLevel, + WidthType, + BorderStyle, + AlignmentType, + ImageRun, +} from 'docx'; +import type { ReportBlock, ConclusionReport } from '../types'; + +interface ExportOptions { + title?: string; + subtitle?: string; + generatedAt?: string; + conclusion?: ConclusionReport; +} + +const TABLE_BORDERS = { + top: { style: BorderStyle.SINGLE, size: 1 }, + bottom: { style: BorderStyle.SINGLE, size: 1 }, + left: { style: BorderStyle.SINGLE, size: 1 }, + right: { style: BorderStyle.SINGLE, size: 1 }, + insideHorizontal: { style: BorderStyle.SINGLE, size: 1 }, + insideVertical: { style: BorderStyle.SINGLE, size: 1 }, +}; + +function makeRow(cells: string[], isHeader = false): TableRow { + return new TableRow({ + children: cells.map(text => + new TableCell({ + children: [ + new Paragraph({ + children: [new TextRun({ text: String(text ?? '-'), bold: isHeader })], + }), + ], + width: { size: Math.floor(100 / cells.length), type: WidthType.PERCENTAGE }, + }) + ), + }); +} + +function blockToDocxElements(block: ReportBlock, index: number): (Paragraph | Table)[] { + const elements: (Paragraph | Table)[] = []; + + switch (block.type) { + case 'key_value': { + if (block.title) { + elements.push(new Paragraph({ text: block.title, heading: HeadingLevel.HEADING_2 })); + } + const items = block.items ?? []; + if (items.length > 0) { + elements.push( + new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: TABLE_BORDERS, + rows: [ + makeRow(['指标', '值'], true), + ...items.map(it => makeRow([it.key, it.value])), + ], + }) + ); + elements.push(new Paragraph({ text: '' })); + } + break; + } + + case 'table': { + const headers = block.headers ?? []; + const rows = block.rows ?? []; + if (headers.length > 0 || rows.length > 0) { + if (block.title) { + elements.push( + new Paragraph({ + text: `Table ${index + 1}. ${block.title}`, + heading: HeadingLevel.HEADING_2, + }) + ); + } + const tableRows: TableRow[] = []; + if (headers.length > 0) { + tableRows.push(makeRow(headers.map(String), true)); + } + for (const row of rows) { + tableRows.push(makeRow(row.map(c => (c === null || c === undefined ? '-' : String(c))))); + } + elements.push( + new Table({ + width: { size: 100, type: WidthType.PERCENTAGE }, + borders: TABLE_BORDERS, + rows: tableRows, + }) + ); + if (block.footnote) { + elements.push( + new Paragraph({ + children: [new TextRun({ text: block.footnote, italics: true, color: '999999', size: 18 })], + }) + ); + } + elements.push(new Paragraph({ text: '' })); + } + break; + } + + case 'image': { + if (block.data) { + try { + const raw = block.data.replace(/^data:image\/\w+;base64,/, ''); + const imageBuffer = Uint8Array.from(atob(raw), c => c.charCodeAt(0)); + if (block.title) { + elements.push( + new Paragraph({ + text: `Figure ${index + 1}. ${block.title}`, + heading: HeadingLevel.HEADING_2, + }) + ); + } + elements.push( + new Paragraph({ + children: [ + new ImageRun({ data: imageBuffer, transformation: { width: 450, height: 300 }, type: 'png' }), + ], + alignment: AlignmentType.CENTER, + }) + ); + elements.push(new Paragraph({ text: '' })); + } catch { + /* skip broken image */ + } + } + break; + } + + case 'markdown': { + if (block.title) { + elements.push(new Paragraph({ text: block.title, heading: HeadingLevel.HEADING_2 })); + } + if (block.content) { + for (const line of block.content.split('\n')) { + if (line.trim()) { + elements.push(new Paragraph({ text: line })); + } + } + elements.push(new Paragraph({ text: '' })); + } + break; + } + } + + return elements; +} + +function conclusionToDocxElements(conclusion: ConclusionReport): (Paragraph | Table)[] { + const elements: (Paragraph | Table)[] = []; + + elements.push(new Paragraph({ text: '综合结论', heading: HeadingLevel.HEADING_1 })); + elements.push(new Paragraph({ text: '' })); + + // Executive Summary + elements.push(new Paragraph({ text: '摘要', heading: HeadingLevel.HEADING_2 })); + for (const line of conclusion.executive_summary.split('\n')) { + if (line.trim()) { + elements.push(new Paragraph({ text: line })); + } + } + elements.push(new Paragraph({ text: '' })); + + // Key Findings + if (conclusion.key_findings.length > 0) { + elements.push(new Paragraph({ text: '主要发现', heading: HeadingLevel.HEADING_2 })); + for (const finding of conclusion.key_findings) { + elements.push( + new Paragraph({ + children: [new TextRun({ text: `• ${finding}` })], + }) + ); + } + elements.push(new Paragraph({ text: '' })); + } + + // Recommendations + if (conclusion.recommendations && conclusion.recommendations.length > 0) { + elements.push(new Paragraph({ text: '建议', heading: HeadingLevel.HEADING_2 })); + for (const rec of conclusion.recommendations) { + elements.push( + new Paragraph({ + children: [new TextRun({ text: `• ${rec}` })], + }) + ); + } + elements.push(new Paragraph({ text: '' })); + } + + // Limitations + if (conclusion.limitations.length > 0) { + elements.push(new Paragraph({ text: '局限性', heading: HeadingLevel.HEADING_2 })); + for (const lim of conclusion.limitations) { + elements.push( + new Paragraph({ + children: [new TextRun({ text: `• ${lim}` })], + }) + ); + } + elements.push(new Paragraph({ text: '' })); + } + + return elements; +} + +export async function exportBlocksToWord( + blocks: ReportBlock[], + options: ExportOptions = {} +): Promise { + const now = new Date(); + const { + title = '统计分析报告', + subtitle, + generatedAt = now.toLocaleString('zh-CN'), + conclusion, + } = options; + + const children: (Paragraph | Table)[] = []; + + children.push( + new Paragraph({ text: title, heading: HeadingLevel.TITLE, alignment: AlignmentType.CENTER }) + ); + if (subtitle) { + children.push(new Paragraph({ text: subtitle, alignment: AlignmentType.CENTER })); + } + children.push( + new Paragraph({ + children: [new TextRun({ text: `生成时间:${generatedAt}`, italics: true, color: '666666' })], + }), + new Paragraph({ text: '' }) + ); + + // Phase R: Conclusion section at the top (if available) + if (conclusion) { + children.push(...conclusionToDocxElements(conclusion)); + children.push(new Paragraph({ text: '详细分析结果', heading: HeadingLevel.HEADING_1 })); + children.push(new Paragraph({ text: '' })); + } + + for (let i = 0; i < blocks.length; i++) { + children.push(...blockToDocxElements(blocks[i], i)); + } + + children.push( + new Paragraph({ text: '' }), + new Paragraph({ + children: [ + new TextRun({ + text: `本报告由智能统计分析系统自动生成${conclusion?.source === 'llm' ? '(结论由 AI 生成)' : ''}`, + italics: true, + color: '666666', + }), + ], + }) + ); + + const doc = new Document({ sections: [{ children }] }); + const blob = await Packer.toBlob(doc); + + const dateStr = `${now.getFullYear()}${String(now.getMonth() + 1).padStart(2, '0')}${String(now.getDate()).padStart(2, '0')}_${String(now.getHours()).padStart(2, '0')}${String(now.getMinutes()).padStart(2, '0')}`; + const safeTitle = title.replace(/[^a-zA-Z0-9\u4e00-\u9fa5_-]/g, '_'); + + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${safeTitle}_${dateStr}.docx`; + a.click(); + URL.revokeObjectURL(url); +} diff --git a/r-statistics-service/plumber.R b/r-statistics-service/plumber.R index ac65a04b..5589f8de 100644 --- a/r-statistics-service/plumber.R +++ b/r-statistics-service/plumber.R @@ -16,6 +16,7 @@ source("utils/error_codes.R") source("utils/data_loader.R") source("utils/guardrails.R") source("utils/result_formatter.R") +source("utils/block_helpers.R") # 工具目录 tools_dir <- "tools" diff --git a/r-statistics-service/tools/chi_square.R b/r-statistics-service/tools/chi_square.R index 39687032..ee144c8c 100644 --- a/r-statistics-service/tools/chi_square.R +++ b/r-statistics-service/tools/chi_square.R @@ -213,6 +213,60 @@ cat("Cramer V =", round(cramers_v, 3), "\\n") mosaicplot(contingency_table, main = "Mosaic Plot", color = TRUE) ') + # ===== 构建 report_blocks ===== + # Block 1: 列联表 + table_headers <- c(var1, as.character(colnames(contingency_table))) + table_rows <- lapply(seq_len(nrow(contingency_table)), function(i) { + c(as.character(rownames(contingency_table)[i]), as.character(contingency_table[i, ])) + }) + blocks <- list( + make_table_block(table_headers, table_rows, title = "列联表") + ) + + # Block 2: 检验结果键值对 + if (use_fisher) { + kv_items <- list( + "方法" = method_used, + "P 值" = output_results$p_value_fmt + ) + if (!is.null(output_results$odds_ratio)) { + kv_items[["比值比"]] <- as.character(round(as.numeric(output_results$odds_ratio), 4)) + } + if (!is.null(output_results$conf_int)) { + kv_items[["95% 置信区间"]] <- sprintf("[%.4f, %.4f]", output_results$conf_int[1], output_results$conf_int[2]) + } + } else { + kv_items <- list( + "方法" = method_used, + "χ² 统计量" = as.character(round(as.numeric(output_results$statistic), 4)), + "自由度" = as.character(output_results$df), + "P 值" = output_results$p_value_fmt, + "Cramér's V" = as.character(output_results$effect_size$cramers_v), + "效应量解释" = output_results$effect_size$interpretation + ) + } + blocks[[length(blocks) + 1]] <- make_kv_block(kv_items, title = "检验结果") + + # Block 3: 马赛克图(若有) + if (!is.null(plot_base64)) { + blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "马赛克图") + } + + # Block 4: 结论摘要 + p_val <- as.numeric(output_results$p_value) + conclusion <- if (p_val < 0.05) { + glue("在 α=0.05 水平下,{var1} 与 {var2} 之间存在显著关联(P {output_results$p_value_fmt})。") + } else { + glue("在 α=0.05 水平下,未发现 {var1} 与 {var2} 之间的显著关联(P {output_results$p_value_fmt})。") + } + if (!use_fisher) { + conclusion <- paste0(conclusion, " 效应量为", output_results$effect_size$interpretation, + "(Cramér's V = ", output_results$effect_size$cramers_v, ")。") + } else if (!is.null(output_results$odds_ratio)) { + conclusion <- paste0(conclusion, " 比值比 = ", round(as.numeric(output_results$odds_ratio), 4), "。") + } + blocks[[length(blocks) + 1]] <- make_markdown_block(conclusion, title = "结论摘要") + # ===== 返回结果 ===== log_add("分析完成") @@ -221,6 +275,7 @@ mosaicplot(contingency_table, main = "Mosaic Plot", color = TRUE) message = "分析完成", warnings = if (length(warnings_list) > 0) warnings_list else NULL, results = output_results, + report_blocks = blocks, plots = if (!is.null(plot_base64)) list(plot_base64) else list(), trace_log = logs, reproducible_code = as.character(reproducible_code) diff --git a/r-statistics-service/tools/correlation.R b/r-statistics-service/tools/correlation.R index 654fc32c..05ea0c09 100644 --- a/r-statistics-service/tools/correlation.R +++ b/r-statistics-service/tools/correlation.R @@ -195,12 +195,50 @@ ggplot(df, aes(x = .data[[var_x]], y = .data[[var_y]])) + if (final_method == "pearson" && !is.null(result$conf.int)) { output_results$conf_int <- as.numeric(result$conf.int) } - + + # ===== 构建 report_blocks ===== + blocks <- list() + + # Block 1: 分析概况 + blocks[[length(blocks) + 1]] <- make_kv_block(list( + "变量 X" = var_x, + "变量 Y" = var_y, + "样本量" = as.character(n), + "分析方法" = final_method + ), title = "分析概况") + + # Block 2: 相关分析结果表 + ci_str <- if (final_method == "pearson" && !is.null(result$conf.int)) { + sprintf("[%.3f, %.3f]", result$conf.int[1], result$conf.int[2]) + } else { + "-" + } + corr_headers <- c("r 值", "P 值", "95% CI", "相关强度") + corr_rows <- list(c( + as.character(round(r_value, 4)), + format_p_value(p_value), + ci_str, + r_interpretation + )) + blocks[[length(blocks) + 1]] <- make_table_block(corr_headers, corr_rows, title = "相关分析结果") + + # Block 3: 散点图 + if (!is.null(plot_base64)) { + blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "散点图", alt = paste(var_x, "vs", var_y)) + } + + # Block 4: 结论摘要 + conclusion_text <- glue( + "**{var_x}** 与 **{var_y}** 的 {final_method} 相关系数为 r = {round(r_value, 3)} (P {format_p_value(p_value)}),相关强度为 **{r_interpretation}**。" + ) + blocks[[length(blocks) + 1]] <- make_markdown_block(conclusion_text, title = "结论摘要") + return(list( status = "success", message = "分析完成", warnings = if (length(warnings_list) > 0) warnings_list else NULL, results = output_results, + report_blocks = blocks, plots = if (!is.null(plot_base64)) list(plot_base64) else list(), trace_log = logs, reproducible_code = as.character(reproducible_code) diff --git a/r-statistics-service/tools/descriptive.R b/r-statistics-service/tools/descriptive.R index af39c98a..20ec6548 100644 --- a/r-statistics-service/tools/descriptive.R +++ b/r-statistics-service/tools/descriptive.R @@ -199,7 +199,83 @@ if (any(categorical_vars)) {{ # ===== 返回结果 ===== log_add("分析完成") - + + # ===== 构建 report_blocks ===== + blocks <- list() + + # Block 1: 数据概况 + kv_items <- list( + "总样本量" = as.character(summary_stats$n_total), + "变量数" = as.character(summary_stats$n_variables), + "数值变量数" = as.character(summary_stats$n_numeric), + "分类变量数" = as.character(summary_stats$n_categorical) + ) + if (!is.null(groups)) { + kv_items$group_var <- group_var + kv_items$groups <- paste(sapply(summary_stats$groups, function(g) paste0(g$name, "(n=", g$n, ")")), collapse = ", ") + } + blocks[[length(blocks) + 1]] <- make_kv_block(kv_items, title = "数据概况") + + # Block 2: 数值变量汇总表 + numeric_vars <- names(results_list)[sapply(results_list, function(x) { + if (is.list(x) && !is.null(x$type)) x$type == "numeric" else FALSE + })] + if (length(numeric_vars) > 0) { + if (is.null(groups)) { + num_headers <- c("变量名", "n", "mean", "sd", "median", "Q1", "Q3", "min", "max") + num_rows <- lapply(numeric_vars, function(v) { + s <- results_list[[v]] + c(v, as.character(s$n), as.character(s$mean), as.character(s$sd), + as.character(s$median), as.character(s$q1), as.character(s$q3), + as.character(s$min), as.character(s$max)) + }) + } else { + num_headers <- c("变量名", as.character(groups)) + num_rows <- lapply(numeric_vars, function(v) { + s <- results_list[[v]] + row <- c(v) + for (g in groups) { + gs <- s$by_group[[as.character(g)]] + row <- c(row, if (!is.null(gs$formatted)) gs$formatted else "-") + } + row + }) + } + blocks[[length(blocks) + 1]] <- make_table_block(num_headers, num_rows, title = "数值变量汇总表") + } + + # Block 3: 分类变量汇总表 + cat_vars <- names(results_list)[sapply(results_list, function(x) { + if (is.list(x) && !is.null(x$type)) x$type == "categorical" else FALSE + })] + if (length(cat_vars) > 0) { + cat_headers <- c("变量名", "水平", "n", "百分比") + cat_rows <- list() + for (v in cat_vars) { + s <- results_list[[v]] + if (is.null(groups)) { + for (lev in s$levels) { + cat_rows[[length(cat_rows) + 1]] <- c(v, lev$level, as.character(lev$n), paste0(lev$pct, "%")) + } + } else { + for (g in groups) { + gs <- s$by_group[[as.character(g)]] + for (lev in gs$levels) { + cat_rows[[length(cat_rows) + 1]] <- c(paste0(v, " (", g, ")"), lev$level, as.character(lev$n), paste0(lev$pct, "%")) + } + } + } + } + if (length(cat_rows) > 0) { + blocks[[length(blocks) + 1]] <- make_table_block(cat_headers, cat_rows, title = "分类变量汇总表") + } + } + + # Block 4+: 各图表 + for (i in seq_along(plots)) { + blocks[[length(blocks) + 1]] <- make_image_block(plots[[i]], title = paste0("图表 ", i), alt = paste0("描述性统计图 ", i)) + } + return(list( status = "success", message = "分析完成", @@ -208,6 +284,7 @@ if (any(categorical_vars)) {{ summary = summary_stats, variables = results_list ), + report_blocks = blocks, plots = plots, trace_log = logs, reproducible_code = as.character(reproducible_code) diff --git a/r-statistics-service/tools/logistic_binary.R b/r-statistics-service/tools/logistic_binary.R index 31ac6a71..f39243fb 100644 --- a/r-statistics-service/tools/logistic_binary.R +++ b/r-statistics-service/tools/logistic_binary.R @@ -254,7 +254,59 @@ cat("AIC:", AIC(model), "\\n") # ===== 返回结果 ===== log_add("分析完成") - + + # ===== 构建 report_blocks ===== + blocks <- list() + + # Block 1: 模型概况 + blocks[[length(blocks) + 1]] <- make_kv_block(list( + "模型公式" = formula_str, + "观测数" = as.character(nrow(df)), + "预测变量数" = as.character(n_predictors), + "AIC" = as.character(round(aic, 2)), + "Nagelkerke R²" = as.character(round(r2_nagelkerke, 4)), + "EPV" = as.character(round(epv, 1)) + ), title = "模型概况") + + # Block 2: 回归系数表 + coef_headers <- c("变量", "OR", "95% CI", "P 值", "显著性") + coef_rows <- lapply(coefficients_list, function(row) { + ci_str <- sprintf("[%.3f, %.3f]", row$ci_lower, row$ci_upper) + sig <- if (row$significant) "*" else "" + c(row$variable, as.character(row$OR), ci_str, row$p_value_fmt, sig) + }) + blocks[[length(blocks) + 1]] <- make_table_block(coef_headers, coef_rows, title = "回归系数表", footnote = "* P < 0.05") + + # Block 3: VIF 表(如存在) + if (!is.null(vif_results) && length(vif_results) > 0) { + vif_headers <- c("变量", "VIF") + vif_rows <- lapply(vif_results, function(row) c(row$variable, as.character(row$vif))) + blocks[[length(blocks) + 1]] <- make_table_block(vif_headers, vif_rows, title = "方差膨胀因子 (VIF)") + } + + # Block 4: 森林图(如存在) + if (!is.null(plot_base64)) { + blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "森林图", alt = "Odds Ratios Forest Plot") + } + + # Block 5: 结论摘要 + sig_vars <- sapply(coefficients_list, function(r) if (r$variable != "(Intercept)" && r$significant) r$variable else NULL) + sig_vars <- unlist(sig_vars[!sapply(sig_vars, is.null)]) + conclusion_lines <- c( + glue("模型拟合指标:AIC = {round(aic, 2)},Nagelkerke R² = {round(r2_nagelkerke, 4)}。"), + "" + ) + if (length(sig_vars) > 0) { + conclusion_lines <- c(conclusion_lines, + glue("在 α = 0.05 水平下,以下变量具有统计学意义:**{paste(sig_vars, collapse = '**, **')}**。"), + "" + ) + } else { + conclusion_lines <- c(conclusion_lines, "在 α = 0.05 水平下,无预测变量达到统计学意义。", "") + } + conclusion_lines <- c(conclusion_lines, glue("EPV = {round(epv, 1)}(建议 ≥ 10)。")) + blocks[[length(blocks) + 1]] <- make_markdown_block(paste(conclusion_lines, collapse = "\n"), title = "结论摘要") + return(list( status = "success", message = "分析完成", @@ -274,6 +326,7 @@ cat("AIC:", AIC(model), "\\n") vif = vif_results, epv = jsonlite::unbox(round(epv, 1)) ), + report_blocks = blocks, plots = if (!is.null(plot_base64)) list(plot_base64) else list(), trace_log = logs, reproducible_code = as.character(reproducible_code) diff --git a/r-statistics-service/tools/mann_whitney.R b/r-statistics-service/tools/mann_whitney.R index 91e6ee7e..5cc1b066 100644 --- a/r-statistics-service/tools/mann_whitney.R +++ b/r-statistics-service/tools/mann_whitney.R @@ -173,6 +173,62 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) + labs(title = paste("Distribution of", value_var, "by", group_var)) ') + # ===== 构建 report_blocks ===== + log_add("构建 report_blocks") + blocks <- list() + + # Block 1: 样本概况(两组 n, median, IQR) + g1_label <- as.character(groups[1]) + g2_label <- as.character(groups[2]) + blocks[[length(blocks) + 1]] <- make_kv_block( + title = "样本概况", + items = list( + list(key = paste0(g1_label, " (n, Median, IQR)"), + value = paste0("n=", n1, ", ", round(median(g1_vals), 3), ", ", round(IQR(g1_vals), 3))), + list(key = paste0(g2_label, " (n, Median, IQR)"), + value = paste0("n=", n2, ", ", round(median(g2_vals), 3), ", ", round(IQR(g2_vals), 3))) + ) + ) + + # Block 2: 检验结果(U 统计量, Z 值, P 值, 效应量 r) + blocks[[length(blocks) + 1]] <- make_table_block( + title = "Mann-Whitney U 检验结果", + headers = c("U 统计量", "Z 值", "P 值", "效应量 r", "效应量解释"), + rows = list( + list( + round(as.numeric(U), 4), + round(z_value, 4), + format_p_value(result$p.value), + round(effect_r, 4), + effect_interpretation + ) + ), + footnote = "Wilcoxon rank sum test with continuity correction" + ) + + # Block 3: 箱线图(如果 plot_base64 不为 NULL) + if (!is.null(plot_base64)) { + blocks[[length(blocks) + 1]] <- make_image_block( + base64_data = plot_base64, + title = paste0(value_var, " by ", group_var), + alt = paste("箱线图:", value_var, "按", group_var, "分组") + ) + } + + # Block 4: 结论摘要 + sig <- if (result$p.value < 0.05) "存在统计学显著差异" else "差异无统计学意义" + blocks[[length(blocks) + 1]] <- make_markdown_block( + title = "结果摘要", + content = paste0( + "两组 **", value_var, "** 的比较(Mann-Whitney U 检验):", + "U = ", round(as.numeric(U), 2), + ",Z = ", round(z_value, 3), + ",P ", format_p_value(result$p.value), + ",效应量 r = ", round(effect_r, 3), "(", effect_interpretation, ")。", + "两组间", sig, "。" + ) + ) + # ===== 返回结果 ===== log_add("分析完成") @@ -209,6 +265,7 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) + ) ) ), + report_blocks = blocks, plots = if (!is.null(plot_base64)) list(plot_base64) else list(), trace_log = logs, reproducible_code = as.character(reproducible_code) diff --git a/r-statistics-service/tools/t_test_ind.R b/r-statistics-service/tools/t_test_ind.R index 5d3c67fb..eea525a7 100644 --- a/r-statistics-service/tools/t_test_ind.R +++ b/r-statistics-service/tools/t_test_ind.R @@ -180,6 +180,66 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) + labs(title = paste("Distribution of", value_var, "by", group_var)) ') + # ===== 构建 report_blocks ===== + log_add("构建 report_blocks") + + blocks <- list() + + # Block 1: 描述统计键值对 + g1_label <- as.character(groups[1]) + g2_label <- as.character(groups[2]) + + blocks[[length(blocks) + 1]] <- make_kv_block( + title = "样本概况", + items = list( + list(key = paste0(group_var, " = ", g1_label, " (n)"), value = as.character(length(g1_vals))), + list(key = paste0(group_var, " = ", g2_label, " (n)"), value = as.character(length(g2_vals))), + list(key = paste0(g1_label, " Mean ± SD"), + value = paste0(round(mean(g1_vals), 3), " \u00b1 ", round(sd(g1_vals), 3))), + list(key = paste0(g2_label, " Mean ± SD"), + value = paste0(round(mean(g2_vals), 3), " \u00b1 ", round(sd(g2_vals), 3))) + ) + ) + + # Block 2: 检验结果表格 + blocks[[length(blocks) + 1]] <- make_table_block( + title = "独立样本 T 检验结果", + headers = c("统计量", "自由度", "P 值", "95% CI 下限", "95% CI 上限", "均值差"), + rows = list( + list( + round(as.numeric(result$statistic), 4), + round(as.numeric(result$parameter), 2), + format_p_value(result$p.value), + round(result$conf.int[1], 4), + round(result$conf.int[2], 4), + round(diff(result$estimate), 4) + ) + ), + footnote = result$method + ) + + # Block 3: 箱线图 + if (!is.null(plot_base64)) { + blocks[[length(blocks) + 1]] <- make_image_block( + base64_data = plot_base64, + title = paste0(value_var, " by ", group_var), + alt = paste("箱线图:", value_var, "按", group_var, "分组") + ) + } + + # Block 4: 结论摘要 + sig <- if (result$p.value < 0.05) "存在统计学显著差异" else "差异无统计学意义" + blocks[[length(blocks) + 1]] <- make_markdown_block( + title = "结果摘要", + content = paste0( + "两组 **", value_var, "** 的比较(", result$method, "):", + "t = ", round(as.numeric(result$statistic), 3), + ",df = ", round(as.numeric(result$parameter), 1), + ",P ", format_p_value(result$p.value), + "。两组间", sig, "。" + ) + ) + # ===== 返回结果 ===== log_add("分析完成") @@ -196,10 +256,11 @@ ggplot(df, aes(x = .data[[group_var]], y = .data[[value_var]])) + conf_int = as.numeric(result$conf.int), estimate = as.numeric(result$estimate), group_stats = list( - list(group = as.character(groups[1]), n = length(g1_vals), mean = mean(g1_vals), sd = sd(g1_vals)), - list(group = as.character(groups[2]), n = length(g2_vals), mean = mean(g2_vals), sd = sd(g2_vals)) + list(group = g1_label, n = length(g1_vals), mean = mean(g1_vals), sd = sd(g1_vals)), + list(group = g2_label, n = length(g2_vals), mean = mean(g2_vals), sd = sd(g2_vals)) ) ), + report_blocks = blocks, plots = if (!is.null(plot_base64)) list(plot_base64) else list(), trace_log = logs, reproducible_code = as.character(reproducible_code) diff --git a/r-statistics-service/tools/t_test_paired.R b/r-statistics-service/tools/t_test_paired.R index 8edc65e3..2f181a72 100644 --- a/r-statistics-service/tools/t_test_paired.R +++ b/r-statistics-service/tools/t_test_paired.R @@ -226,7 +226,98 @@ ggplot(df_long, aes(x = time, y = value, group = id)) + theme_minimal() + labs(title = "Paired Comparison") ') - + + # ===== 构建 report_blocks ===== + d <- output_results$descriptive + blocks <- list() + + # Block 1: 样本概况 + blocks[[length(blocks) + 1]] <- make_kv_block( + title = "样本概况", + items = list( + list(key = paste0(before_var, " (n)"), value = as.character(d$before$n)), + list(key = paste0(before_var, " Mean"), value = as.character(d$before$mean)), + list(key = paste0(before_var, " SD"), value = as.character(d$before$sd)), + list(key = paste0(before_var, " Median"), value = as.character(d$before$median)), + list(key = paste0(after_var, " (n)"), value = as.character(d$after$n)), + list(key = paste0(after_var, " Mean"), value = as.character(d$after$mean)), + list(key = paste0(after_var, " SD"), value = as.character(d$after$sd)), + list(key = paste0(after_var, " Median"), value = as.character(d$after$median)), + list(key = "差值 Mean", value = as.character(d$difference$mean)), + list(key = "差值 SD", value = as.character(d$difference$sd)) + ) + ) + + # Block 2: 检验结果表格(根据 use_wilcoxon 区分) + if (use_wilcoxon) { + blocks[[length(blocks) + 1]] <- make_table_block( + title = "Wilcoxon 符号秩检验结果", + headers = c("统计量 V", "P 值", "效应量 r", "效应量解释"), + rows = list(list( + round(as.numeric(output_results$statistic), 4), + format_p_value(output_results$p_value), + round(output_results$effect_size$r, 4), + output_results$effect_size$interpretation + )), + footnote = method_used + ) + } else { + ci_str <- if (length(output_results$conf_int) >= 2) { + sprintf("[%.4f, %.4f]", output_results$conf_int[1], output_results$conf_int[2]) + } else { + "—" + } + blocks[[length(blocks) + 1]] <- make_table_block( + title = "配对 T 检验结果", + headers = c("t", "df", "P 值", "95% CI", "Cohen's d", "效应量解释"), + rows = list(list( + round(as.numeric(output_results$statistic), 4), + round(as.numeric(output_results$df), 2), + format_p_value(output_results$p_value), + ci_str, + round(output_results$effect_size$cohens_d, 4), + output_results$effect_size$interpretation + )), + footnote = method_used + ) + } + + # Block 3: 配对比较图 + if (!is.null(plot_base64)) { + blocks[[length(blocks) + 1]] <- make_image_block( + base64_data = plot_base64, + title = paste0("配对比较: ", before_var, " vs ", after_var), + alt = paste("配对比较图:", before_var, "与", after_var) + ) + } + + # Block 4: 结论摘要 + sig <- if (output_results$p_value < 0.05) "存在统计学显著差异" else "差异无统计学意义" + if (use_wilcoxon) { + concl <- paste0( + "配对样本 **", before_var, "** 与 **", after_var, "** 的比较(", method_used, "):", + "V = ", round(as.numeric(output_results$statistic), 3), + ",P ", format_p_value(output_results$p_value), + ",效应量 r = ", round(output_results$effect_size$r, 3), + "(", output_results$effect_size$interpretation, ")。", + sig, "。" + ) + } else { + concl <- paste0( + "配对样本 **", before_var, "** 与 **", after_var, "** 的比较(", method_used, "):", + "t = ", round(as.numeric(output_results$statistic), 3), + ",df = ", round(as.numeric(output_results$df), 1), + ",P ", format_p_value(output_results$p_value), + ",Cohen's d = ", round(output_results$effect_size$cohens_d, 3), + "(", output_results$effect_size$interpretation, ")。", + sig, "。" + ) + } + blocks[[length(blocks) + 1]] <- make_markdown_block( + title = "结果摘要", + content = concl + ) + # ===== 返回结果 ===== log_add("分析完成") @@ -235,6 +326,7 @@ ggplot(df_long, aes(x = time, y = value, group = id)) + message = "分析完成", warnings = if (length(warnings_list) > 0) warnings_list else NULL, results = output_results, + report_blocks = blocks, plots = if (!is.null(plot_base64)) list(plot_base64) else list(), trace_log = logs, reproducible_code = as.character(reproducible_code) diff --git a/r-statistics-service/utils/block_helpers.R b/r-statistics-service/utils/block_helpers.R new file mode 100644 index 00000000..b1e51aa2 --- /dev/null +++ b/r-statistics-service/utils/block_helpers.R @@ -0,0 +1,85 @@ +# utils/block_helpers.R +# Block-based 输出协议 — 构造函数 +# +# 所有 R 工具通过这些函数构建 report_blocks[], +# 前端 DynamicReport.tsx 根据 block.type 统一渲染。 +# 支持 4 种 Block 类型:markdown / table / image / key_value + +#' 构造 Markdown 文本块 +#' @param content Markdown 格式文本(支持标题、列表、加粗等) +#' @param title 可选标题(前端渲染为区块标题) +#' @return block list +make_markdown_block <- function(content, title = NULL) { + block <- list(type = "markdown", content = content) + if (!is.null(title)) block$title <- title + block +} + +#' 构造表格块 +#' @param headers 列名字符向量,如 c("组别", "均值", "标准差") +#' @param rows 行数据列表,每行为字符向量,如 list(c("A", "5.2", "1.3"), ...) +#' @param title 可选表格标题 +#' @param footnote 可选脚注(如方法说明) +#' @return block list +make_table_block <- function(headers, rows, title = NULL, footnote = NULL) { + block <- list( + type = "table", + headers = as.list(headers), + rows = lapply(rows, as.list) + ) + if (!is.null(title)) block$title <- title + if (!is.null(footnote)) block$footnote <- footnote + block +} + +#' 从 data.frame 构造表格块(便捷方法) +#' @param df data.frame +#' @param title 可选表格标题 +#' @param footnote 可选脚注 +#' @param digits 数值列保留小数位数,默认 3 +#' @return block list +make_table_block_from_df <- function(df, title = NULL, footnote = NULL, digits = 3) { + headers <- colnames(df) + + rows <- lapply(seq_len(nrow(df)), function(i) { + lapply(df[i, , drop = FALSE], function(val) { + if (is.numeric(val)) { + format(round(val, digits), nsmall = digits) + } else { + as.character(val) + } + }) + }) + + make_table_block(headers, rows, title = title, footnote = footnote) +} + +#' 构造图片块 +#' @param base64_data 完整的 data URI,如 "data:image/png;base64,..." +#' @param title 可选图片标题 +#' @param alt 可选 alt 文本(无障碍 + Word 导出用) +#' @return block list +make_image_block <- function(base64_data, title = NULL, alt = NULL) { + block <- list(type = "image", data = base64_data) + if (!is.null(title)) block$title <- title + if (!is.null(alt)) block$alt <- alt + block +} + +#' 构造键值对块 +#' @param items 命名列表或 list(list(key=..., value=...), ...) +#' @param title 可选标题 +#' @return block list +make_kv_block <- function(items, title = NULL) { + if (!is.null(names(items)) && length(names(items)) > 0 && names(items)[1] != "") { + kv_list <- lapply(names(items), function(k) { + list(key = k, value = as.character(items[[k]])) + }) + } else { + kv_list <- items + } + + block <- list(type = "key_value", items = kv_list) + if (!is.null(title)) block$title <- title + block +}