Files
AIclinicalresearch/docs/03-业务模块/SSA-智能统计分析/04-开发计划/03-后端开发指南.md
HaHafeng 428a22adf2 feat(ssa): Complete Phase 2A frontend integration - multi-step workflow end-to-end
Phase 2A: WorkflowPlannerService, WorkflowExecutorService, Python data quality, 6 bug fixes, DescriptiveResultView, multi-step R code/Word export, MVP UI reuse. V11 UI: Gemini-style, multi-task, single-page scroll, Word export. Architecture: Block-based rendering consensus (4 block types). New R tools: chi_square, correlation, descriptive, logistic_binary, mann_whitney, t_test_paired. Docs: dev summary, block-based plan, status updates, task list v2.0.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-20 23:09:27 +08:00

74 KiB
Raw Blame History

SSA-Pro 后端开发指南

文档版本: v1.7
创建日期: 2026-02-18
最后更新: 2026-02-20纳入 Prompt 体系 + 多工具流程规划 + 数据质量核查报告)
目标读者: Node.js 后端工程师


1. 模块目录结构(概念显性化)

backend/src/modules/ssa/
├── index.ts                    # 模块入口,注册路由
├── routes/
│   ├── session.routes.ts       # 会话管理路由
│   ├── analysis.routes.ts      # 分析执行路由
│   └── consult.routes.ts       # 🆕 咨询模式路由
│   └── config.routes.ts        # 🆕 配置中台路由
│
├── planner/                    # 🆕 Planner 职责(大脑)
│   ├── DataParserService.ts    # 数据解析 + Schema 提取
│   ├── DecisionTableService.ts # 🆕 决策表匹配 (Goal,Y,X,Design)
│   ├── ToolRetrievalService.ts # RAG 工具检索(辅助)
│   ├── PlannerService.ts       # AI 规划(有数据)
│   ├── ConsultService.ts       # 无数据咨询
│   ├── SAPGeneratorService.ts  # SAP 文档生成
│   └── CriticService.ts        # 结果解读(流式)
│
├── executor/                   # 🆕 Executor 职责(四肢)
│   ├── RClientService.ts       # R 服务调用
│   └── InterpretationService.ts # 🆕 结果解读(配置模板)
│
├── config/                     # 🆕 配置中台
│   ├── DecisionTableLoader.ts  # 🆕 统计决策表加载
│   ├── RCodeLibraryService.ts  # 🆕 R 代码库管理
│   ├── ParamMappingService.ts  # 🆕 参数映射配置
│   ├── GuardrailConfigService.ts # 🆕 护栏规则链
│   ├── ConfigValidatorService.ts # 配置校验
│   └── ConfigCacheService.ts   # 配置缓存
│
├── validators/
│   └── planSchema.ts           # 📌 Zod Schema 定义
├── dto/
│   ├── CreateSessionDto.ts
│   ├── UploadDataDto.ts
│   └── ExecuteAnalysisDto.ts
└── types/
    └── index.ts                # 类型定义

1.1 🆕 设计原则(五条核心原则)

原则 说明
Planner/Executor 分离 不要把规划逻辑和执行逻辑混在一个 Class 里
支持无数据模式 Planner 可以在没有数据的情况下独立工作
决策表优先 🆕 工具选择优先用决策表匹配RAG 作为兜底
配置外置 工具定义、护栏规则从 Excel/数据库 加载,不硬编码
统一入口函数 🆕 所有 R 脚本统一 run_analysis() 入口

1.2 🆕 Prompt 体系与专家配置边界

核心理念:骨架与血肉的分离

详细设计参考:06-开发记录/SSA-Pro Prompt体系与专家配置边界梳理.md

1.2.1 动态 Prompt 注入模式

┌─────────────────────────────────────────────────────────────────┐
│                    Prompt 动态注入架构                           │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  AI 工程师维护             统计专家维护                          │
│  ┌─────────────┐          ┌─────────────────────┐              │
│  │ Prompt 模板 │          │ 配置中台 (Excel)     │              │
│  │  (骨架)     │          │  - 决策表            │              │
│  │             │          │  - 使用规则          │              │
│  │ {{占位符}}  │ ◀─────── │  - 解读模板          │              │
│  │             │   注入    │  - 禁用词列表        │              │
│  └─────────────┘          └─────────────────────┘              │
│         │                                                        │
│         ▼                                                        │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ 完整 Prompt = 骨架 + 血肉 → 发送给 LLM                      ││
│  └─────────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────────┘

1.2.2 三个核心 Prompt

环节 Prompt 名称 作用 专家注入内容
意图重写 SSA_QUERY_REWRITER 将医生口语翻译为统计术语 同义词字典
智能规划 SSA_PLANNER 选择工具 + 生成参数映射 工具定义 + 使用规则
结果解读 SSA_CRITIC 生成论文级结论 解读模板 + 禁用词

1.2.3 职责边界表

资产类型 谁来写? 存在哪里? 被谁执行?
System Prompt 模板 (骨架) AI 工程师 prompt_templates 传给 LLM
工具适用条件/数据要求 统计专家 配置中台 Excel 注入 Prompt → LLM
统计护栏规则 统计专家 配置中台 Excel 传给 R 服务,由 R 强执行
R 代码模板 统计专家 配置中台 Excel 传给 R 服务
论文结论解释规范 统计专家 配置中台 Excel 注入 Critic Prompt → LLM

⚠️ 关键原则:统计护栏规则(如正态性检验 P<0.05 降级)绝对不要放到 Prompt 里让 LLM 判断。这些规则必须由 R 代码强逻辑执行。


1.3 🆕 多工具流程规划设计

愿景目标"不是执行方法,而是规划流程"

MVP 目标LLM 能够规划 2-7 个工具的串联执行流程

1.3.1 流程规划架构

用户:"比较两组患者的血压差异"
           │
           ▼
┌─────────────────────────────────────────────────────────────────┐
│                    多工具流程规划                                │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  Step 1: 意图解析                                                │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ 目的:差异比较 | 变量:连续 | 分组:二分类 | 设计:独立     ││
│  └─────────────────────────────────────────────────────────────┘│
│                              │                                   │
│                              ▼                                   │
│  Step 2: 流程规划LLM 输出)                                    │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ {                                                           ││
│  │   "workflow": [                                             ││
│  │     { "step": 1, "tool": "ST_DATA_CHECK", "name": "数据校验" },││
│  │     { "step": 2, "tool": "ST_QUALITY_REPORT", "name": "质量核查" },││
│  │     { "step": 3, "tool": "ST_NORMALITY_TEST", "name": "正态性检验" },││
│  │     { "step": 4, "tool": "ST_LEVENE_TEST", "name": "方差齐性" },││
│  │     { "step": 5, "tool": "ST_T_TEST_IND", "name": "T检验" },││
│  │     { "step": 6, "tool": "ST_EFFECT_SIZE", "name": "效应量" },││
│  │     { "step": 7, "tool": "ST_CONCLUSION", "name": "结论生成" }││
│  │   ],                                                        ││
│  │   "reasoning": "两组独立样本比较,需先检验前提条件..."     ││
│  │ }                                                           ││
│  └─────────────────────────────────────────────────────────────┘│
│                              │                                   │
│                              ▼                                   │
│  Step 3: 串联执行                                                │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ 工具1 → 工具2 → 工具3 → ... → 工具N                        ││
│  │   ↓        ↓        ↓              ↓                        ││
│  │ 结果1 → 结果2 → 结果3 → ... → 最终报告                     ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

1.3.2 WorkflowPlannerService 设计

// planner/WorkflowPlannerService.ts

interface WorkflowStep {
  step: number;
  toolCode: string;
  toolName: string;
  params?: Record<string, any>;
  dependsOn?: number[];  // 依赖的前置步骤
}

interface WorkflowPlan {
  workflow: WorkflowStep[];
  reasoning: string;
  estimatedTime: number;  // 预估耗时(秒)
}

export class WorkflowPlannerService {
  
  /**
   * 生成多工具执行流程
   */
  async generateWorkflow(
    sessionId: string, 
    userQuery: string, 
    dataSchema: object
  ): Promise<WorkflowPlan> {
    
    // 1. 获取候选工具
    const tools = await this.toolRetrieval.retrieveTools(userQuery, dataSchema, 10);
    
    // 2. 构造 Prompt包含流程规划指令
    const prompt = this.buildWorkflowPrompt(userQuery, dataSchema, tools);
    
    // 3. 调用 LLM 生成流程
    const llm = LLMFactory.getAdapter('deepseek-v3');
    const response = await llm.chat([
      { role: 'system', content: prompt },
      { role: 'user', content: userQuery }
    ]);
    
    // 4. 解析 + 校验
    return this.parseWorkflowPlan(response, tools);
  }
  
  /**
   * 构造流程规划 Prompt
   */
  private buildWorkflowPrompt(query: string, schema: object, tools: any[]): string {
    return `
你是一位顶尖的临床数据科学家。你拥有一个包含 ${tools.length} 个专家级统计工具的代码库。

用户数据结构:
${JSON.stringify(schema, null, 2)}

可用工具库:
${tools.map(t => `- ${t.toolCode}: ${t.name} - ${t.description}`).join('\n')}

请根据用户需求,规划一个完整的统计分析流程。流程应包含:
1. 数据质量核查(必须)
2. 前提条件检验(如适用)
3. 核心统计分析
4. 效应量计算(如适用)
5. 结论生成

输出 JSON 格式:
{
  "workflow": [
    { "step": 1, "toolCode": "工具代码", "toolName": "工具名称" },
    ...
  ],
  "reasoning": "规划理由"
}

只输出 JSON不要其他内容。
    `.trim();
  }
}

1.3.3 WorkflowExecutorService 设计

// executor/WorkflowExecutorService.ts

interface StepResult {
  step: number;
  toolCode: string;
  status: 'success' | 'warning' | 'error';
  result?: any;
  error?: string;
  executionMs: number;
}

export class WorkflowExecutorService {
  
  /**
   * 串联执行多个工具
   */
  async executeWorkflow(
    sessionId: string,
    workflow: WorkflowStep[],
    onStepComplete?: (stepResult: StepResult) => void  // 实时回调
  ): Promise<StepResult[]> {
    
    const results: StepResult[] = [];
    let previousResult: any = null;
    
    for (const step of workflow) {
      const startTime = Date.now();
      
      try {
        // 构造本步骤的输入(可能依赖前置步骤的输出)
        const input = this.buildStepInput(step, previousResult, sessionId);
        
        // 调用 R 服务执行
        const result = await this.rClient.execute(sessionId, {
          tool_code: step.toolCode,
          params: input
        });
        
        const stepResult: StepResult = {
          step: step.step,
          toolCode: step.toolCode,
          status: result.status === 'success' ? 'success' : 'warning',
          result: result,
          executionMs: Date.now() - startTime
        };
        
        results.push(stepResult);
        previousResult = result;
        
        // 实时通知前端
        onStepComplete?.(stepResult);
        
      } catch (error: any) {
        const stepResult: StepResult = {
          step: step.step,
          toolCode: step.toolCode,
          status: 'error',
          error: error.message,
          executionMs: Date.now() - startTime
        };
        
        results.push(stepResult);
        onStepComplete?.(stepResult);
        
        // 决定是否继续执行后续步骤
        if (this.isCriticalError(error)) {
          break;  // 关键错误,中断流程
        }
        // 非关键错误,继续执行
      }
    }
    
    return results;
  }
}

1.4 🆕 数据质量核查报告设计

愿景目标:自动生成"数据体检报告",主动告诉用户数据有什么问题

MVP 目标:在执行分析前,先生成数据质量核查报告

1.4.1 核查报告结构

// types/DataQualityReport.ts

interface DataQualityReport {
  // 基础统计
  summary: {
    totalRows: number;
    totalColumns: number;
    numericColumns: number;
    categoricalColumns: number;
  };
  
  // 缺失值分析
  missingAnalysis: {
    totalMissing: number;
    missingRate: number;  // 总体缺失率
    columns: Array<{
      name: string;
      missingCount: number;
      missingRate: number;
      suggestion: string;  // 处理建议
    }>;
  };
  
  // 异常值检测
  outlierAnalysis: {
    columns: Array<{
      name: string;
      outlierCount: number;
      outlierValues: any[];
      method: 'IQR' | 'ZScore';
      suggestion: string;
    }>;
  };
  
  // 分布检验(数值变量)
  distributionAnalysis: {
    columns: Array<{
      name: string;
      shapiroP: number;
      isNormal: boolean;
      skewness: number;
      kurtosis: number;
      suggestion: string;
    }>;
  };
  
  // 分组平衡性(如有分组变量)
  groupBalance?: {
    groupColumn: string;
    groups: Array<{
      name: string;
      count: number;
      percentage: number;
    }>;
    isBalanced: boolean;
    suggestion: string;
  };
  
  // 整体评估
  overallAssessment: {
    qualityScore: number;  // 0-100
    level: 'good' | 'acceptable' | 'poor';
    warnings: string[];
    recommendations: string[];
  };
}

1.4.2 DataQualityService 设计

// planner/DataQualityService.ts

export class DataQualityService {
  
  /**
   * 生成数据质量核查报告
   * 这是 R 服务调用,不是 LLM
   */
  async generateReport(sessionId: string): Promise<DataQualityReport> {
    
    // 调用 R 服务的数据质量核查工具
    const result = await this.rClient.execute(sessionId, {
      tool_code: 'ST_QUALITY_REPORT',
      params: {
        check_missing: true,
        check_outliers: true,
        check_distribution: true,
        check_balance: true
      }
    });
    
    return this.transformToReport(result);
  }
  
  /**
   * 生成用户友好的摘要(可选用 LLM 增强)
   */
  async generateSummary(report: DataQualityReport): Promise<string> {
    const llm = LLMFactory.getAdapter('deepseek-v3');
    
    const prompt = `
你是一位数据分析专家。请根据以下数据质量核查结果生成一段简洁的中文摘要3-5句话
告诉用户数据的整体质量如何,主要问题是什么,是否可以继续分析。

核查结果:
${JSON.stringify(report, null, 2)}

请直接输出摘要文本。
    `;
    
    return await llm.chat([{ role: 'user', content: prompt }]);
  }
}

1.4.3 R 服务端实现ST_QUALITY_REPORT

# tools/quality_report.R

#' @tool_code ST_QUALITY_REPORT
#' @name 数据质量核查报告
#' @description 生成全面的数据质量评估报告

run_analysis <- function(input) {
  # 加载数据
  df <- load_data(input)
  
  report <- list()
  
  # 1. 基础统计
  report$summary <- list(
    totalRows = nrow(df),
    totalColumns = ncol(df),
    numericColumns = sum(sapply(df, is.numeric)),
    categoricalColumns = sum(sapply(df, is.character) | sapply(df, is.factor))
  )
  
  # 2. 缺失值分析
  report$missingAnalysis <- analyze_missing(df)
  
  # 3. 异常值检测IQR 方法)
  report$outlierAnalysis <- analyze_outliers(df)
  
  # 4. 分布检验
  report$distributionAnalysis <- analyze_distribution(df)
  
  # 5. 分组平衡性
  if (!is.null(input$group_var)) {
    report$groupBalance <- analyze_balance(df, input$group_var)
  }
  
  # 6. 整体评估
  report$overallAssessment <- calculate_quality_score(report)
  
  return(list(
    status = "success",
    report = report
  ))
}

# 计算整体质量评分
calculate_quality_score <- function(report) {
  score <- 100
  warnings <- c()
  recommendations <- c()
  
  # 缺失值扣分
  if (report$missingAnalysis$missingRate > 0.1) {
    score <- score - 20
    warnings <- c(warnings, "缺失值比例超过10%")
    recommendations <- c(recommendations, "建议处理缺失值后再进行分析")
  } else if (report$missingAnalysis$missingRate > 0.05) {
    score <- score - 10
    warnings <- c(warnings, "存在一定比例的缺失值")
  }
  
  # 异常值扣分
  outlier_cols <- sum(sapply(report$outlierAnalysis$columns, function(x) x$outlierCount > 0))
  if (outlier_cols > 0) {
    score <- score - 5 * outlier_cols
    warnings <- c(warnings, paste0(outlier_cols, "个变量存在异常值"))
  }
  
  # 非正态扣分(提示,不强制扣分)
  non_normal <- sum(!sapply(report$distributionAnalysis$columns, function(x) x$isNormal))
  if (non_normal > 0) {
    recommendations <- c(recommendations, 
      paste0(non_normal, "个变量不满足正态分布,系统将自动选择非参数方法"))
  }
  
  # 确定等级
  level <- if (score >= 80) "good" else if (score >= 60) "acceptable" else "poor"
  
  return(list(
    qualityScore = max(0, score),
    level = level,
    warnings = warnings,
    recommendations = recommendations
  ))
}

1.4.4 前端展示(核查报告卡片)

┌─────────────────────────────────────────────────────────────────┐
│  📊 数据质量核查报告                                            │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  📈 数据概况                                                    │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ 总样本量200 行 × 15 列                                    ││
│  │ 数值变量8 个 | 分类变量7 个                             ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  ⚠️ 发现的问题                                                  │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ • 缺失值:血压字段有 12 例缺失 (6%)                        ││
│  │ • 异常值2 例血压 > 300 mmHg疑似记录错误              ││
│  │ • 正态性:治疗组血压不满足正态分布                         ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  💡 系统建议                                                    │
│  ┌─────────────────────────────────────────────────────────────┐│
│  │ 1. 建议处理 2 例异常值后再分析                             ││
│  │ 2. 由于正态性不满足,系统将自动选择非参数方法              ││
│  └─────────────────────────────────────────────────────────────┘│
│                                                                  │
│  🎯 整体评估:良好 (82/100)                                     │
│                                                                  │
│  [继续分析]  [下载报告]                                         │
└─────────────────────────────────────────────────────────────────┘

2. 数据库 SchemaPrisma

// schema.prisma - SSA 模块部分

// 分析会话
model SsaSession {
  id            String   @id @default(uuid())
  userId        String   @map("user_id")
  title         String?
  dataSchema    Json?    @map("data_schema")    // 数据结构LLM可见
  dataPayload   Json?    @map("data_payload")   // 真实数据仅R可见
  status        String   @default("active")
  createdAt     DateTime @default(now()) @map("created_at")
  updatedAt     DateTime @updatedAt @map("updated_at")
  
  messages      SsaMessage[]
  
  @@map("ssa_sessions")
  @@schema("ssa_schema")
}

// 消息记录
model SsaMessage {
  id            String   @id @default(uuid())
  sessionId     String   @map("session_id")
  role          String   // user | assistant | system
  contentType   String   @map("content_type")  // text | plan | result
  content       Json
  createdAt     DateTime @default(now()) @map("created_at")
  
  session       SsaSession @relation(fields: [sessionId], references: [id])
  
  @@map("ssa_messages")
  @@schema("ssa_schema")
}

// 工具库
model SsaTool {
  id            String   @id @default(uuid())
  toolCode      String   @unique @map("tool_code")
  name          String
  version       String   @default("1.0.0")
  description   String
  usageContext  String?  @map("usage_context")
  paramsSchema  Json     @map("params_schema")
  guardrails    Json?
  searchText    String   @map("search_text")
  embedding     Unsupported("vector(1024)")?
  isActive      Boolean  @default(true) @map("is_active")
  createdAt     DateTime @default(now()) @map("created_at")
  updatedAt     DateTime @updatedAt @map("updated_at")
  
  @@map("tools_library")
  @@schema("ssa_schema")
}

// 执行日志
model SsaExecutionLog {
  id            String   @id @default(uuid())
  sessionId     String   @map("session_id")
  messageId     String?  @map("message_id")
  toolCode      String   @map("tool_code")
  inputParams   Json     @map("input_params")
  outputStatus  String   @map("output_status")
  outputResult  Json?    @map("output_result")
  traceLog      String[] @map("trace_log")
  executionMs   Int?     @map("execution_ms")
  createdAt     DateTime @default(now()) @map("created_at")
  
  @@map("execution_logs")
  @@schema("ssa_schema")
}

// 🆕 统计决策表
model SsaDecisionTable {
  id            String   @id @default(uuid())
  goalType      String   @map("goal_type")      // 分析目标:组间差异、相关性、分布描述
  yType         String   @map("y_type")          // 因变量类型:连续、分类、计数
  xType         String?  @map("x_type")          // 自变量类型:可选
  designType    String   @map("design_type")     // 设计类型:独立、配对、重复测量
  toolCode      String   @map("tool_code")       // 推荐工具
  altToolCode   String?  @map("alt_tool_code")   // 备选工具(降级)
  priority      Int      @default(0)             // 优先级
  isActive      Boolean  @default(true) @map("is_active")
  createdAt     DateTime @default(now()) @map("created_at")
  
  @@unique([goalType, yType, xType, designType])
  @@map("decision_table")
  @@schema("ssa_schema")
}

// 🆕 R 代码库
model SsaRCodeLibrary {
  id            String   @id @default(uuid())
  toolCode      String   @map("tool_code")       // 关联工具代码
  version       String   @default("1.0.0")
  fileName      String   @map("file_name")       // R 脚本文件名
  codeContent   String   @map("code_content")    // R 代码内容
  entryFunc     String   @default("run_analysis") @map("entry_func")  // 入口函数
  description   String?
  dependencies  String[] @default([])            // 依赖包列表
  isActive      Boolean  @default(true) @map("is_active")
  createdAt     DateTime @default(now()) @map("created_at")
  updatedAt     DateTime @updatedAt @map("updated_at")
  
  @@map("r_code_library")
  @@schema("ssa_schema")
}

// 🆕 参数映射配置
model SsaParamMapping {
  id            String   @id @default(uuid())
  toolCode      String   @map("tool_code")
  jsonKey       String   @map("json_key")        // 前端传入的 JSON Key
  rParamName    String   @map("r_param_name")    // R 函数参数名
  dataType      String   @map("data_type")       // string | number | boolean
  isRequired    Boolean  @default(false) @map("is_required")
  defaultValue  String?  @map("default_value")
  validationRule String? @map("validation_rule") // 校验规则
  description   String?
  
  @@unique([toolCode, jsonKey])
  @@map("param_mapping")
  @@schema("ssa_schema")
}

// 🆕 护栏规则配置
model SsaGuardrailConfig {
  id            String   @id @default(uuid())
  toolCode      String   @map("tool_code")
  checkName     String   @map("check_name")      // 检查名称:正态性、方差齐性
  checkOrder    Int      @default(0) @map("check_order")  // 执行顺序
  checkCode     String   @map("check_code")      // R 函数名
  threshold     String?                          // 阈值条件p < 0.05
  actionType    String   @map("action_type")     // Block | Warn | Switch
  actionTarget  String?  @map("action_target")   // Switch 时的目标工具
  isEnabled     Boolean  @default(true) @map("is_enabled")
  
  @@map("guardrail_config")
  @@schema("ssa_schema")
}

// 🆕 结果解读模板
model SsaInterpretation {
  id            String   @id @default(uuid())
  toolCode      String   @map("tool_code")
  scenarioKey   String   @map("scenario_key")    // 场景significant | not_significant
  template      String                           // 解读模板(含占位符)
  placeholders  String[] @default([])            // 占位符列表
  
  @@unique([toolCode, scenarioKey])
  @@map("interpretation_templates")
  @@schema("ssa_schema")
}

3. API 路由设计

3.1 路由注册

// index.ts
import { FastifyInstance } from 'fastify';
import sessionRoutes from './routes/session.routes';
import analysisRoutes from './routes/analysis.routes';
import consultRoutes from './routes/consult.routes';  // 🆕
import configRoutes from './routes/config.routes';    // 🆕

export default async function ssaModule(app: FastifyInstance) {
  // 注册认证中间件
  app.addHook('preHandler', app.authenticate);
  
  // 注册子路由
  app.register(sessionRoutes, { prefix: '/sessions' });
  app.register(analysisRoutes, { prefix: '/sessions' });
  app.register(consultRoutes, { prefix: '/consult' });  // 🆕 咨询模式
  app.register(configRoutes, { prefix: '/config' });    // 🆕 配置中台
}

3.2 会话路由

// routes/session.routes.ts
import { FastifyInstance } from 'fastify';
import { SessionService } from '../services/SessionService';

export default async function sessionRoutes(app: FastifyInstance) {
  const sessionService = new SessionService();

  // 创建会话
  app.post('/', async (req, reply) => {
    const userId = req.user.id;
    const session = await sessionService.create(userId);
    return reply.send(session);
  });

  // 获取会话列表
  app.get('/', async (req, reply) => {
    const userId = req.user.id;
    const sessions = await sessionService.listByUser(userId);
    return reply.send(sessions);
  });

  // 获取单个会话(含消息历史)
  app.get('/:id', async (req, reply) => {
    const { id } = req.params as { id: string };
    const session = await sessionService.getById(id, req.user.id);
    return reply.send(session);
  });

  // 上传数据
  app.post('/:id/upload', async (req, reply) => {
    const { id } = req.params as { id: string };
    // 解析 Excel/CSV提取 Schema 和 Data
    const result = await sessionService.uploadData(id, req);
    return reply.send(result);
  });
}

3.3 分析路由

// routes/analysis.routes.ts
import { FastifyInstance } from 'fastify';
import { PlannerService } from '../services/PlannerService';
import { RClientService } from '../services/RClientService';
import { CriticService } from '../services/CriticService';

export default async function analysisRoutes(app: FastifyInstance) {
  const plannerService = new PlannerService();
  const rClientService = new RClientService();
  const criticService = new CriticService();

  // 生成分析计划(不执行)
  app.post('/:id/plan', async (req, reply) => {
    const { id } = req.params as { id: string };
    const { query } = req.body as { query: string };
    
    // 1. RAG 检索工具
    // 2. LLM 生成计划
    const plan = await plannerService.generatePlan(id, query);
    
    return reply.send({
      type: 'plan',
      plan
    });
  });

  // 确认执行
  app.post('/:id/execute', async (req, reply) => {
    const { id } = req.params as { id: string };
    const { plan } = req.body as { plan: object };
    
    // 1. 调用 R 服务执行
    const result = await rClientService.execute(id, plan);
    
    // 2. 保存执行日志
    // 3. 保存结果到消息
    
    return reply.send({
      type: 'result',
      result
    });
  });

  // 获取结果解读(流式)
  app.get('/:id/interpret/:messageId', async (req, reply) => {
    const { id, messageId } = req.params as { id: string; messageId: string };
    
    // 流式返回 Critic 解读
    reply.raw.setHeader('Content-Type', 'text/event-stream');
    
    await criticService.streamInterpret(id, messageId, reply.raw);
  });

  // 下载代码
  app.get('/:id/download-code/:messageId', async (req, reply) => {
    const { id, messageId } = req.params as { id: string; messageId: string };
    
    const code = await sessionService.getReproducibleCode(messageId);
    
    reply.header('Content-Type', 'text/plain');
    reply.header('Content-Disposition', 'attachment; filename="analysis.R"');
    return reply.send(code);
  });
}

4. 核心服务实现

4.1 RClientService调用 R 服务)

// services/RClientService.ts
import axios, { AxiosInstance } from 'axios';
import { prisma } from '@/common/db';
import { logger } from '@/common/logging';

export class RClientService {
  private client: AxiosInstance;
  
  constructor() {
    this.client = axios.create({
      baseURL: process.env.R_SERVICE_URL || 'http://localhost:8080',
      timeout: 120000,  // 📌 120s 超时(应对复杂计算)
      headers: { 'Content-Type': 'application/json' }
    });
  }

  async execute(sessionId: string, plan: {
    tool_code: string;
    params: Record<string, any>;
    guardrails: Record<string, boolean>;
  }) {
    const startTime = Date.now();
    
    // 1. 获取会话的真实数据
    const session = await prisma.ssaSession.findUniqueOrThrow({
      where: { id: sessionId }
    });
    
    // 🆕 2. 构造 R 服务请求(混合数据协议)
    const dataSource = this.buildDataSource(session);
    const requestBody = {
      data_source: dataSource,  // 🆕 统一数据源字段
      params: plan.params,
      guardrails: plan.guardrails
    };
  
  /**
   * 🆕 根据数据大小选择传输方式
   * - < 2MB: inline JSON
   * - >= 2MB: OSS key
   */
  private buildDataSource(session: any): { type: string; data?: any; oss_key?: string } {
    const payload = session.dataPayload;
    const payloadSize = JSON.stringify(payload).length;
    
    const SIZE_THRESHOLD = 2 * 1024 * 1024;  // 2MB
    
    if (payloadSize < SIZE_THRESHOLD) {
      // 小数据:直接内联
      return {
        type: 'inline',
        data: payload
      };
    } else {
      // 大数据:上传 OSS传递 key
      // 注意:此处假设 session 创建时已上传 OSS
      const ossKey = session.dataOssKey || `sessions/${session.id}/data.json`;
      return {
        type: 'oss',
        oss_key: ossKey
      };
    }
  }
    
    // 3. 调用 R 服务
    try {
      const response = await this.client.post(
        `/api/v1/skills/${plan.tool_code}`,
        requestBody
      );
      
      const executionMs = Date.now() - startTime;
      
      // 4. 记录执行日志(不含真实数据)
      await prisma.ssaExecutionLog.create({
        data: {
          sessionId,
          toolCode: plan.tool_code,
          inputParams: plan.params,  // 只记录参数,不记录数据
          outputStatus: response.data.status,
          outputResult: response.data.results,
          traceLog: response.data.trace_log || [],
          executionMs
        }
      });
      
      return response.data;
      
    } catch (error: any) {
      logger.error('R service call failed', { sessionId, toolCode: plan.tool_code, error });
      
      // 🆕 502/504 特殊处理R 服务崩溃或超时)
      const statusCode = error.response?.status;
      if (statusCode === 502 || statusCode === 504) {
        throw new Error('统计服务繁忙或数据异常,请稍后重试');
      }
      
      // 🆕 提取 R 服务返回的用户友好提示
      const userHint = error.response?.data?.user_hint;
      if (userHint) {
        throw new Error(userHint);
      }
      
      throw new Error(`R service error: ${error.message}`);
    }
  }
  
  async healthCheck(): Promise<boolean> {
    try {
      const res = await this.client.get('/health');
      return res.data.status === 'ok';
    } catch {
      return false;
    }
  }
}

4.2 🆕 DecisionTableService决策表匹配 - 优先)

// planner/DecisionTableService.ts
import { prisma } from '@/common/db';
import { logger } from '@/common/logging';

interface AnalysisIntent {
  goalType: string;     // 组间差异 | 相关性 | 分布描述 | 预测建模
  yType: string;        // 连续 | 分类 | 计数
  xType?: string;       // 连续 | 分类 | 无
  designType: string;   // 独立 | 配对 | 重复测量
}

export class DecisionTableService {
  
  /**
   * 🆕 根据分析意图从决策表精准匹配工具
   * 优先级: 决策表匹配 > RAG 检索
   */
  async matchTool(intent: AnalysisIntent): Promise<string | null> {
    const result = await prisma.ssaDecisionTable.findFirst({
      where: {
        goalType: intent.goalType,
        yType: intent.yType,
        xType: intent.xType || null,
        designType: intent.designType,
        isActive: true
      },
      orderBy: { priority: 'desc' }
    });
    
    if (result) {
      logger.info('Decision table matched', { 
        intent, 
        toolCode: result.toolCode 
      });
      return result.toolCode;
    }
    
    return null;
  }
  
  /**
   * 🆕 获取降级工具(护栏触发时使用)
   */
  async getAlternativeTool(toolCode: string): Promise<string | null> {
    const entry = await prisma.ssaDecisionTable.findFirst({
      where: { toolCode, isActive: true }
    });
    return entry?.altToolCode || null;
  }
  
  /**
   * 🆕 从 LLM 提取分析意图(结构化)
   */
  async extractIntent(userQuery: string, dataSchema: object): Promise<AnalysisIntent> {
    const llm = LLMFactory.getAdapter('deepseek-v3');
    
    const prompt = `
分析用户的统计需求,提取以下四个维度:

用户需求: ${userQuery}
数据结构: ${JSON.stringify(dataSchema, null, 2)}

请返回 JSON 格式:
{
  "goalType": "组间差异 | 相关性 | 分布描述 | 预测建模",
  "yType": "连续 | 分类 | 计数",
  "xType": "连续 | 分类 | 无",
  "designType": "独立 | 配对 | 重复测量"
}

只返回 JSON不要其他内容。
    `.trim();
    
    const response = await llm.chat([{ role: 'user', content: prompt }]);
    
    try {
      return JSON.parse(jsonrepair(response));
    } catch {
      // 兜底默认值
      return {
        goalType: '组间差异',
        yType: '连续',
        xType: '分类',
        designType: '独立'
      };
    }
  }
}

4.3 ToolRetrievalServiceRAG 检索 - 兜底)

// planner/ToolRetrievalService.ts
import { VectorSearchService } from '@/common/rag';
import { LLMFactory } from '@/common/llm/adapters/LLMFactory';
import { prisma } from '@/common/db';
import { DecisionTableService } from './DecisionTableService';

export class ToolRetrievalService {
  private vectorSearch: VectorSearchService;
  private decisionTable: DecisionTableService;
  
  constructor() {
    this.vectorSearch = new VectorSearchService({
      schema: 'ssa_schema',
      table: 'tools_library',
      embeddingColumn: 'embedding',
      textColumn: 'search_text'
    });
    this.decisionTable = new DecisionTableService();
  }
  
  /**
   * 🆕 工具选择策略决策表优先RAG 兜底
   */
  async selectTool(query: string, dataSchema: object): Promise<any> {
    // 1. 尝试决策表精准匹配
    const intent = await this.decisionTable.extractIntent(query, dataSchema);
    const matchedCode = await this.decisionTable.matchTool(intent);
    
    if (matchedCode) {
      const tool = await prisma.ssaTool.findUnique({
        where: { toolCode: matchedCode }
      });
      if (tool) {
        return { ...tool, matchMethod: 'decision_table' };
      }
    }
    
    // 2. 决策表未命中,使用 RAG 检索
    const ragResults = await this.retrieveTools(query, dataSchema, 1);
    if (ragResults.length > 0) {
      return { ...ragResults[0], matchMethod: 'rag' };
    }
    
    return null;
  }

  async retrieveTools(query: string, dataSchema: object, topK = 5) {
    // 1. Query Rewrite可选提升召回
    const rewriter = LLMFactory.getAdapter('deepseek-v3');
    const rewritePrompt = `
将用户的统计分析需求改写为更适合检索统计工具的查询:
用户需求: ${query}
数据结构: ${JSON.stringify(dataSchema)}

输出改写后的查询(一句话):
    `.trim();
    
    const rewrittenQuery = await rewriter.chat([
      { role: 'user', content: rewritePrompt }
    ]);
    
    // 2. 向量检索
    const vectorResults = await this.vectorSearch.search(rewrittenQuery, topK);
    
    // 3. 关键词检索 (pg_bigm)
    const keywordResults = await prisma.$queryRaw`
      SELECT id, tool_code, name, description, params_schema, guardrails
      FROM ssa_schema.tools_library
      WHERE search_text LIKE '%' || ${query} || '%'
      AND is_active = true
      LIMIT 5
    `;
    
    // 4. RRF 融合
    const merged = this.rrfMerge(vectorResults, keywordResults);
    
    // 5. Rerank可选
    // const reranked = await this.rerank(merged, query);
    
    return merged.slice(0, topK);
  }

  private rrfMerge(vectorResults: any[], keywordResults: any[], k = 60) {
    const scores = new Map<string, number>();
    
    vectorResults.forEach((item, idx) => {
      const rrf = 1 / (k + idx + 1);
      scores.set(item.id, (scores.get(item.id) || 0) + rrf);
    });
    
    keywordResults.forEach((item, idx) => {
      const rrf = 1 / (k + idx + 1);
      scores.set(item.id, (scores.get(item.id) || 0) + rrf);
    });
    
    // 合并并排序
    const allItems = [...vectorResults, ...keywordResults];
    const unique = [...new Map(allItems.map(i => [i.id, i])).values()];
    
    return unique.sort((a, b) => 
      (scores.get(b.id) || 0) - (scores.get(a.id) || 0)
    );
  }
}

4.3 PlannerServiceAI 规划 + JSON 容错)

🆕 Prompt 世界观设计(智能化演进关键)

重要Prompt 的"世界观"设计直接影响 LLM 的推理质量和未来演进能力。

详细背景参考:04-开发计划/06-智能化演进共识与MVP执行计划.md

错误的世界观(接线员思维):

你是一个工具选择器,从以下列表中选择合适的工具...

正确的世界观(数据科学家思维):

你是一位顶尖的临床数据科学家,拥有以下能力:

1. 深刻理解医学研究的统计学需求
2. 精通各类统计方法的适用场景和前提条件
3. 能够诊断数据特征并选择最优分析路径

你现在拥有一个包含 100+ 专家级统计算法的代码库。
每个算法都经过统计学专家的严格验证,确保结果的权威性。

请理解医生的研究意图,诊断数据特征,从代码库中选择最合适的工具组合,
并制定完整的分析计划。你的目标是帮助医生产出可以直接用于 SCI 论文的统计结果。

为什么这很重要?

维度 接线员思维 数据科学家思维
LLM 自我认知 被动的工具选择器 主动的分析规划师
推理深度 简单匹配 深度分析数据特征
输出质量 可能选错工具 更准确的工具选择
Phase 3 演进 难以扩展到代码修改 自然过渡到代码理解和修改

MVP 阶段行动:更新 SSA_PLANNER Prompt 模板,使用"数据科学家"世界观。


// services/PlannerService.ts
import { LLMFactory } from '@/common/llm/adapters/LLMFactory';
import { PromptService } from '@/common/prompts';
import { ToolRetrievalService } from './ToolRetrievalService';
import { prisma } from '@/common/db';
import { jsonrepair } from 'jsonrepair';  // 📌 JSON 修复库
import { planSchema } from '../validators/planSchema';  // 📌 Zod Schema

export class PlannerService {
  private retrieval: ToolRetrievalService;
  
  constructor() {
    this.retrieval = new ToolRetrievalService();
  }

  async generatePlan(sessionId: string, userQuery: string) {
    // 1. 获取会话的数据 Schema不含真实数据
    const session = await prisma.ssaSession.findUniqueOrThrow({
      where: { id: sessionId },
      select: { dataSchema: true }
    });
    
    // 2. RAG 检索候选工具
    const candidateTools = await this.retrieval.retrieveTools(
      userQuery,
      session.dataSchema,
      5
    );
    
    // 3. 获取 Planner Prompt
    const promptTemplate = await PromptService.get('SSA_PLANNER');
    
    // 4. 构造 Prompt
    const systemPrompt = promptTemplate
      .replace('{{data_schema_json}}', JSON.stringify(session.dataSchema, null, 2))
      .replace('{{candidate_tools_json}}', JSON.stringify(candidateTools, null, 2));
    
    // 5. 调用 LLM
    const llm = LLMFactory.getAdapter('deepseek-v3');
    const response = await llm.chat([
      { role: 'system', content: systemPrompt },
      { role: 'user', content: userQuery }
    ]);
    
    // 6. 📌 解析 + 修复 + 校验 JSON
    const plan = this.parseAndValidateJson(response, candidateTools);
    
    // 7. 保存用户消息和计划消息
    await prisma.ssaMessage.createMany({
      data: [
        {
          sessionId,
          role: 'user',
          contentType: 'text',
          content: { text: userQuery }
        },
        {
          sessionId,
          role: 'assistant',
          contentType: 'plan',
          content: plan
        }
      ]
    });
    
    return plan;
  }

  // 📌 增强的 JSON 解析(含修复和校验)
  private parseAndValidateJson(text: string, candidateTools: any[]): object {
    // Step 1: 提取 JSON 块
    const jsonMatch = text.match(/```json\n?([\s\S]*?)\n?```/) || 
                      text.match(/\{[\s\S]*\}/);
    
    if (!jsonMatch) {
      throw new Error('LLM response does not contain valid JSON');
    }
    
    let jsonStr = jsonMatch[1] || jsonMatch[0];
    
    // Step 2: 使用 jsonrepair 修复常见问题(末尾逗号、缺少引号等)
    try {
      jsonStr = jsonrepair(jsonStr);
    } catch (repairError) {
      // 修复失败,继续尝试原始解析
    }
    
    // Step 3: 解析 JSON
    let parsed: any;
    try {
      parsed = JSON.parse(jsonStr);
    } catch (parseError) {
      throw new Error(`JSON parse failed: ${parseError.message}`);
    }
    
    // Step 4: 使用 Zod 校验结构
    const validatedPlan = planSchema.safeParse(parsed);
    
    if (!validatedPlan.success) {
      throw new Error(`Plan validation failed: ${validatedPlan.error.message}`);
    }
    
    // Step 5: 校验 tool_code 是否在候选列表中
    const validToolCodes = candidateTools.map(t => t.tool_code);
    if (!validToolCodes.includes(validatedPlan.data.tool_code)) {
      throw new Error(`Invalid tool_code: ${validatedPlan.data.tool_code}`);
    }
    
    return validatedPlan.data;
  }
}

4.4 Zod Schema 定义

// validators/planSchema.ts
import { z } from 'zod';

export const planSchema = z.object({
  tool_code: z.string().min(1),
  reasoning: z.string().optional(),
  params: z.record(z.any()),
  guardrails: z.object({
    check_normality: z.boolean().optional(),
    check_homogeneity: z.boolean().optional(),
    auto_fix: z.boolean().optional()
  }).optional()
});

export type PlanType = z.infer<typeof planSchema>;

4.5 🆕 ConsultService无数据咨询

// planner/ConsultService.ts
import { LLMFactory } from '@/common/llm/adapters/LLMFactory';
import { PromptService } from '@/common/prompts';
import { ToolRetrievalService } from './ToolRetrievalService';
import { prisma } from '@/common/db';

export class ConsultService {
  private retrieval: ToolRetrievalService;
  
  constructor() {
    this.retrieval = new ToolRetrievalService();
  }

  /**
   * 🆕 无数据咨询对话
   * - 用户只描述研究设计、变量类型等
   * - 系统推理适合的统计方法
   */
  async chat(sessionId: string, userMessage: string) {
    // 1. 获取会话历史
    const history = await prisma.ssaMessage.findMany({
      where: { sessionId },
      orderBy: { createdAt: 'asc' }
    });
    
    // 2. 获取咨询专用 Prompt
    const systemPrompt = await PromptService.get('SSA_CONSULT');
    
    // 3. 构造消息列表
    const messages = [
      { role: 'system' as const, content: systemPrompt },
      ...history.map(m => ({
        role: m.role as 'user' | 'assistant',
        content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content)
      })),
      { role: 'user' as const, content: userMessage }
    ];
    
    // 4. 调用 LLM
    const llm = LLMFactory.getAdapter('deepseek-v3');
    const response = await llm.chat(messages);
    
    // 5. 保存消息
    await prisma.ssaMessage.createMany({
      data: [
        { sessionId, role: 'user', contentType: 'text', content: { text: userMessage } },
        { sessionId, role: 'assistant', contentType: 'text', content: { text: response } }
      ]
    });
    
    return response;
  }

  /**
   * 🆕 生成 SAP 文档
   * - 基于对话历史生成结构化的统计分析计划
   */
  async generateSAP(sessionId: string): Promise<{
    title: string;
    sections: Array<{
      heading: string;
      content: string;
    }>;
    recommendedTools: string[];
  }> {
    const history = await prisma.ssaMessage.findMany({
      where: { sessionId },
      orderBy: { createdAt: 'asc' }
    });
    
    const sapPrompt = await PromptService.get('SSA_SAP_GENERATOR');
    
    const llm = LLMFactory.getAdapter('deepseek-v3');
    const response = await llm.chat([
      { role: 'system', content: sapPrompt },
      { role: 'user', content: `基于以下对话生成统计分析计划:\n${JSON.stringify(history)}` }
    ]);
    
    // 解析 JSON 响应
    const sap = JSON.parse(response);
    
    return sap;
  }
}

4.6 🆕 SAPGeneratorServiceSAP 文档导出)

// planner/SAPGeneratorService.ts
import { Document, Packer, Paragraph, HeadingLevel, Table, TableRow, TableCell } from 'docx';

interface SAPDocument {
  title: string;
  sections: Array<{
    heading: string;
    content: string;
  }>;
  recommendedTools: string[];
}

export class SAPGeneratorService {
  
  /**
   * 🆕 生成 Word 文档
   */
  async generateWord(sap: SAPDocument): Promise<Buffer> {
    const doc = new Document({
      sections: [{
        children: [
          new Paragraph({
            text: sap.title,
            heading: HeadingLevel.TITLE
          }),
          ...sap.sections.flatMap(section => [
            new Paragraph({
              text: section.heading,
              heading: HeadingLevel.HEADING_1
            }),
            new Paragraph({ text: section.content })
          ]),
          new Paragraph({
            text: '推荐统计方法',
            heading: HeadingLevel.HEADING_1
          }),
          ...sap.recommendedTools.map(tool => 
            new Paragraph({ text: `• ${tool}` })
          )
        ]
      }]
    });
    
    return await Packer.toBuffer(doc);
  }

  /**
   * 🆕 生成 Markdown
   */
  generateMarkdown(sap: SAPDocument): string {
    let md = `# ${sap.title}\n\n`;
    
    for (const section of sap.sections) {
      md += `## ${section.heading}\n\n${section.content}\n\n`;
    }
    
    md += `## 推荐统计方法\n\n`;
    for (const tool of sap.recommendedTools) {
      md += `- ${tool}\n`;
    }
    
    return md;
  }
}

4.7 🆕 ConfigLoaderService配置中台

// config/ConfigLoaderService.ts
import * as XLSX from 'xlsx';
import { ConfigValidatorService } from './ConfigValidatorService';
import { logger } from '@/common/logging';

interface ToolConfig {
  tool_code: string;
  name: string;
  description: string;
  params_schema: Record<string, any>;
  guardrails: Record<string, any>;
  search_text: string;
}

interface GuardrailConfig {
  guardrail_code: string;
  description: string;
  threshold: number;
  auto_fix_action: string;
}

export class ConfigLoaderService {
  private static instance: ConfigLoaderService;
  private toolsCache: Map<string, ToolConfig> = new Map();
  private guardrailsCache: Map<string, GuardrailConfig> = new Map();
  private lastLoadTime: Date | null = null;

  static getInstance() {
    if (!this.instance) {
      this.instance = new ConfigLoaderService();
    }
    return this.instance;
  }

  /**
   * 🆕 从 Excel 加载配置
   */
  async loadFromExcel(buffer: Buffer): Promise<{
    tools: number;
    guardrails: number;
    errors: string[];
  }> {
    const workbook = XLSX.read(buffer, { type: 'buffer' });
    const errors: string[] = [];
    
    // Sheet 1: 工具定义
    const toolsSheet = workbook.Sheets['Tools'];
    if (toolsSheet) {
      const toolsData = XLSX.utils.sheet_to_json<ToolConfig>(toolsSheet);
      
      for (const tool of toolsData) {
        // 校验
        const validation = ConfigValidatorService.validateTool(tool);
        if (validation.valid) {
          this.toolsCache.set(tool.tool_code, tool);
        } else {
          errors.push(`Tool ${tool.tool_code}: ${validation.error}`);
        }
      }
    }
    
    // Sheet 2: 护栏规则
    const guardrailsSheet = workbook.Sheets['Guardrails'];
    if (guardrailsSheet) {
      const guardrailsData = XLSX.utils.sheet_to_json<GuardrailConfig>(guardrailsSheet);
      
      for (const gr of guardrailsData) {
        const validation = ConfigValidatorService.validateGuardrail(gr);
        if (validation.valid) {
          this.guardrailsCache.set(gr.guardrail_code, gr);
        } else {
          errors.push(`Guardrail ${gr.guardrail_code}: ${validation.error}`);
        }
      }
    }
    
    this.lastLoadTime = new Date();
    logger.info('Config loaded', { 
      tools: this.toolsCache.size, 
      guardrails: this.guardrailsCache.size 
    });
    
    return {
      tools: this.toolsCache.size,
      guardrails: this.guardrailsCache.size,
      errors
    };
  }

  /**
   * 🆕 热加载(清空缓存并重新加载)
   */
  async reload(): Promise<void> {
    // 从数据库或默认 Excel 重新加载
    this.toolsCache.clear();
    this.guardrailsCache.clear();
    // ... 重新加载逻辑
    logger.info('Config reloaded');
  }

  getTool(toolCode: string): ToolConfig | undefined {
    return this.toolsCache.get(toolCode);
  }

  getAllTools(): ToolConfig[] {
    return Array.from(this.toolsCache.values());
  }

  getGuardrail(code: string): GuardrailConfig | undefined {
    return this.guardrailsCache.get(code);
  }
}

4.8 🆕 ConfigValidatorService配置校验

// config/ConfigValidatorService.ts

interface ValidationResult {
  valid: boolean;
  error?: string;
}

export class ConfigValidatorService {
  
  /**
   * 🆕 校验工具配置
   */
  static validateTool(tool: any): ValidationResult {
    // 必填校验
    if (!tool.tool_code) {
      return { valid: false, error: 'tool_code is required' };
    }
    if (!tool.name) {
      return { valid: false, error: 'name is required' };
    }
    
    // 格式校验
    if (!/^ST_[A-Z_]+$/.test(tool.tool_code)) {
      return { valid: false, error: 'tool_code must match ST_XXX pattern' };
    }
    
    // params_schema 校验
    if (tool.params_schema) {
      try {
        if (typeof tool.params_schema === 'string') {
          JSON.parse(tool.params_schema);
        }
      } catch {
        return { valid: false, error: 'params_schema is not valid JSON' };
      }
    }
    
    return { valid: true };
  }

  /**
   * 🆕 校验护栏配置
   */
  static validateGuardrail(gr: any): ValidationResult {
    if (!gr.guardrail_code) {
      return { valid: false, error: 'guardrail_code is required' };
    }
    
    if (typeof gr.threshold !== 'number') {
      return { valid: false, error: 'threshold must be a number' };
    }
    
    if (gr.threshold < 0 || gr.threshold > 1) {
      return { valid: false, error: 'threshold must be between 0 and 1' };
    }
    
    return { valid: true };
  }
}

4.9 🆕 配置中台路由

// routes/config.routes.ts
import { FastifyInstance } from 'fastify';
import { ConfigLoaderService } from '../config/ConfigLoaderService';

export default async function configRoutes(app: FastifyInstance) {
  const configService = ConfigLoaderService.getInstance();

  // 导入 Excel 配置
  app.post('/import', async (req, reply) => {
    const data = await req.file();
    if (!data) {
      return reply.status(400).send({ error: 'No file uploaded' });
    }
    
    const buffer = await data.toBuffer();
    const result = await configService.loadFromExcel(buffer);
    
    return reply.send(result);
  });

  // 🆕 热加载配置Admin API
  app.post('/reload', async (req, reply) => {
    await configService.reload();
    return reply.send({ success: true, timestamp: new Date().toISOString() });
  });

  // 获取工具列表
  app.get('/tools', async (req, reply) => {
    const tools = configService.getAllTools();
    return reply.send(tools);
  });

  // 校验配置文件(不导入)
  app.post('/validate', async (req, reply) => {
    const data = await req.file();
    if (!data) {
      return reply.status(400).send({ error: 'No file uploaded' });
    }
    
    // 仅校验,不加载到缓存
    // ...
    return reply.send({ valid: true });
  });
}

4.10 🆕 咨询模式路由

// routes/consult.routes.ts
import { FastifyInstance } from 'fastify';
import { ConsultService } from '../planner/ConsultService';
import { SAPGeneratorService } from '../planner/SAPGeneratorService';
import { prisma } from '@/common/db';

export default async function consultRoutes(app: FastifyInstance) {
  const consultService = new ConsultService();
  const sapGenerator = new SAPGeneratorService();

  // 创建咨询会话(无数据)
  app.post('/', async (req, reply) => {
    const userId = req.user.id;
    
    const session = await prisma.ssaSession.create({
      data: {
        userId,
        title: '统计咨询',
        status: 'consult'  // 🆕 标记为咨询模式
      }
    });
    
    return reply.send(session);
  });

  // 咨询对话
  app.post('/:id/chat', async (req, reply) => {
    const { id } = req.params as { id: string };
    const { message } = req.body as { message: string };
    
    const response = await consultService.chat(id, message);
    
    return reply.send({ response });
  });

  // 生成 SAP 文档
  app.post('/:id/generate-sap', async (req, reply) => {
    const { id } = req.params as { id: string };
    
    const sap = await consultService.generateSAP(id);
    
    // 保存到会话
    await prisma.ssaMessage.create({
      data: {
        sessionId: id,
        role: 'assistant',
        contentType: 'sap',
        content: sap
      }
    });
    
    return reply.send(sap);
  });

  // 下载 SAPWord/Markdown
  app.get('/:id/download-sap', async (req, reply) => {
    const { id } = req.params as { id: string };
    const { format = 'word' } = req.query as { format?: 'word' | 'markdown' };
    
    // 获取最新的 SAP
    const sapMessage = await prisma.ssaMessage.findFirst({
      where: { sessionId: id, contentType: 'sap' },
      orderBy: { createdAt: 'desc' }
    });
    
    if (!sapMessage) {
      return reply.status(404).send({ error: 'SAP not found' });
    }
    
    const sap = sapMessage.content as any;
    
    if (format === 'markdown') {
      const md = sapGenerator.generateMarkdown(sap);
      reply.header('Content-Type', 'text/markdown');
      reply.header('Content-Disposition', 'attachment; filename="SAP.md"');
      return reply.send(md);
    } else {
      const buffer = await sapGenerator.generateWord(sap);
      reply.header('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
      reply.header('Content-Disposition', 'attachment; filename="SAP.docx"');
      return reply.send(buffer);
    }
  });
}

5. Brain-Hand 数据隔离

核心原则LLM 只看 SchemaR 服务处理真实数据

┌─────────────────────────────────────────────────────────────┐
│                     数据上传流程                              │
│                                                             │
│  Excel/CSV  ──────┬────────────────────────────────────────│
│                   │                                         │
│            ┌──────▼──────┐                                 │
│            │  数据解析器  │                                 │
│            └──────┬──────┘                                 │
│                   │                                         │
│         ┌─────────┴─────────┐                              │
│         │                   │                              │
│    dataSchema          dataPayload                         │
│   (结构/类型/统计)       (真实数据)                          │
│         │                   │                              │
│         ▼                   ▼                              │
│   LLM (Planner)        R (Executor)                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘

5.1 数据解析实现

// services/DataParserService.ts
import * as XLSX from 'xlsx';

export class DataParserService {
  
  static parse(buffer: Buffer, filename: string) {
    const workbook = XLSX.read(buffer, { type: 'buffer' });
    const sheetName = workbook.SheetNames[0];
    const sheet = workbook.Sheets[sheetName];
    
    // 转为 JSON 数组
    const data = XLSX.utils.sheet_to_json(sheet);
    
    // 提取 Schema
    const schema = this.extractSchema(data);
    
    return {
      dataSchema: schema,    // 给 LLM
      dataPayload: data      // 给 R
    };
  }

  private static extractSchema(data: any[]) {
    if (data.length === 0) return { columns: [], rowCount: 0 };
    
    const columns = Object.keys(data[0]).map(colName => {
      const values = data.map(row => row[colName]).filter(v => v != null);
      const type = this.inferType(values);
      
      return {
        name: colName,
        type,
        ...this.computeStats(values, type, data.length)  // 📌 传入行数用于隐私保护
      };
    });
    
    return {
      rowCount: data.length,
      columns
    };
  }

  private static inferType(values: any[]): 'numeric' | 'categorical' | 'datetime' {
    const sample = values.slice(0, 100);
    const numericCount = sample.filter(v => typeof v === 'number' || !isNaN(Number(v))).length;
    
    if (numericCount / sample.length > 0.9) return 'numeric';
    return 'categorical';
  }

  private static computeStats(values: any[], type: string, rowCount: number) {
    if (type === 'numeric') {
      const nums = values.map(Number).filter(n => !isNaN(n));
      let min = Math.min(...nums);
      let max = Math.max(...nums);
      
      // 📌 小样本隐私保护N < 10 时模糊化极值
      if (rowCount < 10) {
        min = Math.floor(min / 10) * 10;  // 向下取整到十位
        max = Math.ceil(max / 10) * 10;   // 向上取整到十位
      }
      
      return {
        min,
        max,
        mean: nums.reduce((a, b) => a + b, 0) / nums.length,
        missing: values.length - nums.length,
        privacyProtected: rowCount < 10  // 📌 标记是否已模糊化
      };
    }
    
    // categorical
    const counts = new Map<string, number>();
    values.forEach(v => {
      const key = String(v);
      counts.set(key, (counts.get(key) || 0) + 1);
    });
    
    // 🆕 分类变量隐私保护:
    // 如果某个取值的计数 < 5 且总行数 > 10则隐藏具体值
    const uniqueValues: string[] = [];
    let maskedCount = 0;
    
    for (const [value, count] of counts.entries()) {
      if (count < 5 && rowCount > 10) {
        maskedCount++;
      } else {
        uniqueValues.push(value);
      }
    }
    
    // 最多展示 10 个非敏感值
    const safeValues = uniqueValues.slice(0, 10);
    if (maskedCount > 0) {
      safeValues.push(`[${maskedCount} 个稀有值已隐藏]`);
    }
    
    return {
      uniqueValues: safeValues,
      uniqueCount: counts.size,
      missing: values.filter(v => v == null || v === '').length,
      privacyProtected: maskedCount > 0  // 🆕 标记
    };
  }
}

6. Prompt 注册

-- 注册 Planner Prompt
INSERT INTO capability_schema.prompt_templates (code, name, content, model, temperature)
VALUES (
  'SSA_PLANNER',
  'SSA 统计规划器',
  '你是一名资深的生物统计学家。你面前有一份数据摘要Metadata和一组可用的统计工具箱。
请根据用户的需求选择最合适的一个工具并生成详细的执行计划SAP
### 数据摘要
{{data_schema_json}}

### 可用工具箱 (Candidates)
{{candidate_tools_json}}

### 决策规则 (Guardrails)
1. **类型匹配**:严格检查变量类型。不要把分类变量填入要求数值型的参数中。
2. **工具匹配**:如果用户要做 "预测",优先选 "回归" 类工具;如果做 "差异",选 "检验" 类工具。
3. **护栏配置**:对于 T 检验、ANOVA 等参数检验,必须开启 check_normality。

### 输出要求
请先在 <thinking> 标签中进行推理,分析变量类型和工具适用性。
然后输出纯 JSON格式如下
{
  "tool_code": "选中工具的CODE",
  "reasoning": "一句话解释为什么选这个工具",
  "params": { ...根据工具定义的 params_schema 填写... },
  "guardrails": { "check_normality": true, "auto_fix": true }
}',
  'deepseek-v3',
  0.3
);

7. 与主应用集成

// backend/src/index.ts
import ssaModule from './modules/ssa';

// 在 Fastify 注册
app.register(ssaModule, { prefix: '/api/v1/ssa' });

8. 环境变量

# .env

# R 服务配置
R_SERVICE_URL=http://ssa-r-service:8080  # SAE VPC 内网地址
R_SERVICE_TIMEOUT=120000                  # 📌 超时 120s

# 📌 OSS 配置(必须使用 VPC 内网 Endpoint
OSS_ENDPOINT=oss-cn-beijing-internal.aliyuncs.com  # 内网地址
OSS_BUCKET=ssa-data-bucket
OSS_ACCESS_KEY_ID=your-access-key
OSS_ACCESS_KEY_SECRET=your-secret

# LLM 配置
LLM_DEFAULT_MODEL=deepseek-v3

重要OSS Endpoint 必须使用 -internal 后缀的 VPC 内网地址,否则 R 服务的网络隔离策略会导致文件下载失败。


9. 测试检查清单

测试场景 预期结果
POST /sessions 创建会话 返回 sessionId
POST /sessions/:id/upload (CSV) 返回 dataSchema
POST /sessions/:id/upload (N<10) dataSchema.privacyProtected = true
POST /sessions/:id/plan (T检验意图) 返回包含 tool_code 的 plan
POST /sessions/:id/plan (LLM 返回格式错误 JSON) json-repair 修复成功
POST /sessions/:id/plan (参数不合法) Zod 校验失败,返回错误
POST /sessions/:id/execute R 服务返回 success
POST /sessions/:id/execute (超过 60s) 不超时,等待 120s
GET /sessions/:id/download-code 下载 .R 文件
R 服务宕机时 execute 返回友好错误

10. 依赖包清单

{
  "dependencies": {
    "jsonrepair": "^3.6.0",
    "zod": "^3.22.4",
    "xlsx": "^0.18.5",
    "axios": "^1.6.0",
    "docx": "^8.5.0"      // 🆕 Word 文档生成SAP 导出)
  }
}

11. 🆕 配置中台 Excel 模板规范

核心理念:统计学专家通过 Excel + R 脚本配置系统行为,无需修改代码。

11.0 🆕 决策表 Excel (decision_table.xlsx)

用于 Planner 工具选择,四维精准匹配

列名 类型 必填 说明
goal_type string 分析目标:组间差异 / 相关性 / 分布描述 / 预测建模
y_type string 因变量类型:连续 / 分类 / 计数
x_type string 自变量类型:连续 / 分类 / 无
design_type string 设计类型:独立 / 配对 / 重复测量
tool_code string 推荐工具代码
alt_tool_code string 备选工具(护栏降级)
priority number 优先级(数字越大越优先)

示例数据:

goal_type y_type x_type design_type tool_code alt_tool_code
组间差异 连续 分类 独立 ST_T_TEST_IND ST_MANN_WHITNEY
组间差异 连续 分类 配对 ST_T_TEST_PAIRED ST_WILCOXON
相关性 连续 连续 独立 ST_CORRELATION ST_CORRELATION

11.1 Sheet 1: Metadata工具元数据

列名 类型 必填 说明
tool_code string 工具代码,格式 ST_XXX
name string 工具名称
version string 版本号,默认 1.0.0
r_script_file string 🆕 R 脚本文件名(如 t_test_ind.R
description string 工具描述
usage_context string 适用场景
search_text string RAG 搜索关键词

11.2 Sheet 2: ParamMapping参数映射

🆕 JSON Key → R 参数名映射

列名 类型 必填 说明
tool_code string 工具代码
json_key string 前端传入的 JSON 字段名
r_param_name string R 函数参数名
data_type string 数据类型string / number / boolean
is_required boolean 是否必填
default_value string 默认值
validation_rule string 校验规则(正则或条件)
description string 参数说明

示例数据:

tool_code json_key r_param_name data_type is_required
ST_T_TEST_IND group_variable group_var string TRUE
ST_T_TEST_IND value_variable value_var string TRUE
ST_T_TEST_IND confidence_level conf_level number FALSE

11.3 Sheet 3: Guardrails护栏规则链

🆕 支持 Block / Warn / Switch 三种 Action

列名 类型 必填 说明
tool_code string 工具代码
check_name string 检查名称:正态性检验 / 方差齐性 / 样本量
check_order number 执行顺序(数字越小越先)
check_code string R 函数名(如 check_normality
threshold string 阈值条件p < 0.05
action_type string 🆕 Block / Warn / Switch
action_target string Switch 时的目标工具代码
is_enabled boolean 是否启用

Action 类型说明:

  • Block: 阻止执行,返回错误
  • Warn: 警告但继续执行
  • Switch: 🆕 自动切换到备选方法

示例数据:

tool_code check_name check_code threshold action_type action_target
ST_T_TEST_IND 正态性检验 check_normality p < 0.05 Switch ST_MANN_WHITNEY
ST_T_TEST_IND 样本量检查 check_sample_size n < 3 Block
ST_ANOVA_ONE 方差齐性 check_homogeneity p < 0.05 Warn

11.4 Sheet 4: OutputDef输出字段定义

列名 类型 必填 说明
tool_code string 工具代码
field_name string 字段名
display_name string 展示名称
data_type string 数据类型
format_rule string 格式化规则(如 %.3f

11.5 Sheet 5: Interpretation结果解读模板

🆕 "填空题"式的论文级结论生成

列名 类型 必填 说明
tool_code string 工具代码
scenario_key string 场景significant / not_significant / warning
template text 解读模板(含占位符)
placeholders text 占位符列表JSON 数组)

示例模板:

场景: significant
模板: "采用 {method} 进行分析结果表明两组之间存在统计学显著差异t = {statistic}, p {p_value_fmt}, 95% CI [{ci_lower}, {ci_upper}])。{group1} 组均值为 {mean1} ± {sd1}{group2} 组均值为 {mean2} ± {sd2}。"

场景: not_significant
模板: "采用 {method} 进行分析结果表明两组之间差异无统计学意义t = {statistic}, p = {p_value_fmt})。"

11.6 🆕 R 脚本规范

专家上传的 R 脚本必须遵循以下规范

# 文件名: t_test_ind.R
# 工具代码: ST_T_TEST_IND
# 版本: 1.0.0

#' @title 独立样本 T 检验
#' @description 比较两组独立样本的均值差异
#' @param input List 包含 data_source, params, guardrails
#' @return List 包含 status, results, plots, trace_log, reproducible_code

# 📌 所有脚本必须使用统一入口函数
run_analysis <- function(input) {
  # 1. 数据加载
  df <- load_input_data(input)
  
  # 2. 参数提取(根据 ParamMapping 配置)
  group_var <- input$params$group_var
  value_var <- input$params$value_var
  
  # 3. 护栏检查(根据 Guardrails 配置)
  # ... 护栏检查代码 ...
  
  # 4. 核心计算
  result <- t.test(...)
  
  # 5. 返回标准格式
  return(list(
    status = "success",
    results = list(...),
    plots = list(...),
    trace_log = logs,
    reproducible_code = code
  ))
}

12. 🆕 SAP 文档规范

12.1 SAP 结构定义

interface SAPDocument {
  title: string;                  // 统计分析计划标题
  sections: Array<{
    heading: string;              // 章节标题
    content: string;              // 章节内容
  }>;
  recommendedTools: string[];     // 推荐的统计方法列表
  metadata: {
    generatedAt: string;          // 生成时间
    sessionId: string;            // 关联会话
    version: string;              // 版本号
  };
}

12.2 标准章节

  1. 研究背景 - 研究目的、设计类型
  2. 数据描述 - 样本量、变量类型、缺失情况
  3. 统计假设 - 原假设、备择假设
  4. 分析方法 - 具体统计方法及选择理由
  5. 结果解读指南 - 如何解读统计结果
  6. 注意事项 - 方法局限性、前提条件