Files
AIclinicalresearch/docs/02-通用能力层/06-R统计引擎/01-R统计引擎架构与部署指南.md
HaHafeng 3446909ff7 feat(ssa): Complete Phase I-IV intelligent dialogue and tool system development
Phase I - Session Blackboard + READ Layer:
- SessionBlackboardService with Postgres-Only cache
- DataProfileService for data overview generation
- PicoInferenceService for LLM-driven PICO extraction
- Frontend DataContextCard and VariableDictionaryPanel
- E2E tests: 31/31 passed

Phase II - Conversation Layer LLM + Intent Router:
- ConversationService with SSE streaming
- IntentRouterService (rule-first + LLM fallback, 6 intents)
- SystemPromptService with 6-segment dynamic assembly
- TokenTruncationService for context management
- ChatHandlerService as unified chat entry
- Frontend SSAChatPane and useSSAChat hook
- E2E tests: 38/38 passed

Phase III - Method Consultation + AskUser Standardization:
- ToolRegistryService with Repository Pattern
- MethodConsultService with DecisionTable + LLM enhancement
- AskUserService with global interrupt handling
- Frontend AskUserCard component
- E2E tests: 13/13 passed

Phase IV - Dialogue-Driven Analysis + QPER Integration:
- ToolOrchestratorService (plan/execute/report)
- analysis_plan SSE event for WorkflowPlan transmission
- Dual-channel confirmation (ask_user card + workspace button)
- PICO as optional hint for LLM parsing
- E2E tests: 25/25 passed

R Statistics Service:
- 5 new R tools: anova_one, baseline_table, fisher, linear_reg, wilcoxon
- Enhanced guardrails and block helpers
- Comprehensive test suite (run_all_tools_test.js)

Documentation:
- Updated system status document (v5.9)
- Updated SSA module status and development plan (v1.8)

Total E2E: 107/107 passed (Phase I: 31, Phase II: 38, Phase III: 13, Phase IV: 25)

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-22 18:53:39 +08:00

1245 lines
36 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# R 统计引擎架构与部署指南
> **版本:** v1.3
> **更新日期:** 2026-02-22
> **维护者:** SSA-Pro 开发团队
> **状态:** ✅ 生产就绪Phase Deploy 完成 — 12 工具 + Block-based 标准化输出)
---
## 📋 目录
1. [概述](#1-概述)
2. [架构设计](#2-架构设计)
3. [Docker 镜像构建](#3-docker-镜像构建)
4. [部署指南](#4-部署指南)
5. [API 参考](#5-api-参考)
6. [开发指南](#6-开发指南)
- 6.1 [添加新工具(含 Block-based 模板)](#61-添加新工具)
- 6.5 [各工具参数快速参考](#65-各工具参数快速参考)
- 6.6 [R 语言陷阱速查7 大坑)](#66-r-语言陷阱速查从实际-bug-中总结)
- 6.7 [开发环境新增 R 包](#67-开发环境新增-r-包)
7. [运维指南](#7-运维指南)
8. [常见问题](#8-常见问题)
9. [测试指南](#9-测试指南)
---
## 1. 概述
### 1.1 什么是 R 统计引擎
R 统计引擎是平台的**专用统计计算服务**,基于 Docker 容器化部署,提供:
- 🧮 **严谨的统计分析能力**T 检验、方差分析、回归等)
- 🛡️ **统计护栏**(正态性检验、方差齐性检验等)
- 📊 **可视化输出**Base64 编码的图表)
- 📝 **可复现代码生成**APA 格式的 R 脚本)
### 1.2 定位
```
┌─────────────────────────────────────────────────────────────┐
│ 业务模块层 │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ SSA-Pro │ │ 其他 │ │ 其他 │ │
│ │ 智能统计 │ │ 模块 │ │ 模块 │ │
│ └────┬────┘ └─────────┘ └─────────┘ │
├───────┼─────────────────────────────────────────────────────┤
│ ▼ 通用能力层 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ R 统计引擎 (Docker) │ │
│ │ • /health 健康检查 │ │
│ │ • /api/v1/tools 工具列表 │ │
│ │ • /api/v1/skills 技能执行 │ │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
### 1.3 技术栈
| 组件 | 版本 | 说明 |
|------|------|------|
| R | 4.3.3 | 统计计算核心 |
| plumber | 1.2.1 | REST API 框架 |
| ggplot2 | 最新 | 数据可视化 |
| car | 3.1-2 | 高级统计检验 |
| dplyr/tidyr | 最新 | 数据处理 |
| gtsummary | 最新 | 基线特征表生成Phase Deploy 新增) |
| gt/broom | 最新 | 表格渲染/模型整理Phase Deploy 新增) |
| scales/gridExtra | 最新 | 坐标轴格式化/多图排版Phase Deploy 新增) |
| Docker | 24+ | 容器化部署 |
---
## 2. 架构设计
### 2.1 Brain-Hand 模型
R 统计引擎采用 **Brain-Hand 分离架构**
```
┌──────────────────┐ ┌──────────────────┐
│ Node.js │ │ R Docker │
│ (Brain) │ │ (Hand) │
├──────────────────┤ ├──────────────────┤
│ • 业务逻辑 │ HTTP │ • 统计计算 │
│ • 认证鉴权 │ ───────> │ • 数据处理 │
│ • OSS 签名 │ │ • 图表生成 │
│ • 结果解释 │ <─────── │ • 代码生成 │
└──────────────────┘ JSON └──────────────────┘
```
### 2.2 数据传输协议
支持两种数据传输方式:
| 方式 | 条件 | 字段 |
|------|------|------|
| **inline** | 数据 < 2MB | `data_source.data` (JSON) |
| **oss** | 数据 >= 2MB | `data_source.oss_url` (预签名 URL) |
```json
// 方式 1: inline
{
"data_source": {
"type": "inline",
"data": [{"group": "A", "value": 10}, ...]
}
}
// 方式 2: oss (预签名 URL)
{
"data_source": {
"type": "oss",
"oss_url": "https://bucket.oss.com/data.csv?signature=xxx"
}
}
```
#### 2.2.1 inline 数据格式详解
R 数据加载器 (`utils/data_loader.R`) 支持两种 JSON 数据格式:
| 格式 | 说明 | 示例 |
|------|------|------|
| **行格式** | JSON 对象数组,每个对象是一行 | `[{"sex": 1, "age": 25}, {"sex": 2, "age": 30}]` |
| **列格式** | JSON 对象,每个属性是一列 | `{"sex": [1, 2], "age": [25, 30]}` |
> **推荐**:使用**行格式**,与 JavaScript/TypeScript 的数据处理习惯一致。
**Node.js 调用示例:**
```typescript
// 推荐行格式Array of Objects
const data = [
{ sex: 1, age: 25, bmi: 22.5 },
{ sex: 2, age: 30, bmi: 24.1 },
// ...
];
const response = await axios.post('http://localhost:8082/api/v1/skills/ST_T_TEST_IND', {
data_source: {
type: 'inline',
data: data // 直接传入数组
},
params: {
group_var: 'sex',
value_var: 'age'
}
});
```
### 2.3 安全设计
| 安全措施 | 实现方式 |
|----------|----------|
| 非特权用户 | `USER appuser` |
| 路径遍历防护 | `tool_code` 正则白名单 `^[A-Z][A-Z0-9_]*$` |
| OSS 密钥隔离 | Node.js 生成预签名 URLR 无需持有密钥 |
| 健康检查 | Docker HEALTHCHECK |
---
## 3. Docker 镜像构建
### 3.1 完整 Dockerfile
```dockerfile
FROM rocker/r-ver:4.3
LABEL maintainer="dev-team@aiclinicalresearch.com"
LABEL version="1.0.1"
LABEL description="SSA-Pro R Statistics Service"
# 安装系统依赖(包括 R 包编译所需的库)
RUN apt-get update && apt-get install -y \
libcurl4-openssl-dev \
libssl-dev \
libxml2-dev \
libsodium-dev \
zlib1g-dev \
libnlopt-dev \
liblapack-dev \
libblas-dev \
gfortran \
pkg-config \
cmake \
curl \
&& rm -rf /var/lib/apt/lists/*
# 直接安装 R 包(含 Phase Deploy 新增依赖)
RUN R -e "install.packages(c( \
'plumber', \
'jsonlite', \
'ggplot2', \
'glue', \
'dplyr', \
'tidyr', \
'base64enc', \
'yaml', \
'car', \
'httr', \
'scales', \
'gridExtra', \
'gtsummary', \
'gt', \
'broom' \
), repos='https://cloud.r-project.org/', Ncpus=2)"
# 安全加固:创建非特权用户
RUN useradd -m -s /bin/bash appuser
WORKDIR /app
# 复制应用代码
COPY plumber.R plumber.R
COPY utils/ utils/
COPY tools/ tools/
COPY tests/ tests/
# 设置目录权限
RUN chown -R appuser:appuser /app
# 切换到非特权用户
USER appuser
EXPOSE 8080
# 环境变量
ENV DEV_MODE="false"
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
# 启动服务
CMD ["R", "-e", "plumber::plumb('plumber.R')$run(host='0.0.0.0', port=8080)"]
```
### 3.2 系统依赖说明
| 依赖包 | 用途 |
|--------|------|
| `libcurl4-openssl-dev` | httr 包HTTP 请求) |
| `libssl-dev` | openssl 包(加密) |
| `libxml2-dev` | xml2 包XML 解析) |
| `libsodium-dev` | sodium 包(加密) |
| `zlib1g-dev` | httpuv 包Web 服务器) |
| `libnlopt-dev` | nloptr 包(优化算法) |
| `liblapack-dev` | 线性代数计算 |
| `libblas-dev` | 基础线性代数 |
| `gfortran` | Fortran 编译器(部分 R 包需要) |
| `cmake` | nloptr 包构建 |
| `curl` | 健康检查 |
### 3.3 构建命令
```bash
# 本地构建
cd r-statistics-service
docker build -t ssa-r-statistics:1.0.1 .
# 查看镜像
docker images ssa-r-statistics
# 预期输出
REPOSITORY TAG IMAGE ID CREATED SIZE
ssa-r-statistics 1.0.1 xxxxxxxxxxxx x minutes ago 1.81GB
```
### 3.4 构建时间参考
| 阶段 | 耗时 |
|------|------|
| 基础镜像下载 | ~2 分钟(首次) |
| 系统依赖安装 | ~1 分钟 |
| R 包安装15 个包含 gtsummary/gt | ~10 分钟 |
| **总计** | **~13 分钟** |
---
## 4. 部署指南
### 4.1 开发环境
使用 docker-compose
```yaml
# r-statistics-service/docker-compose.yml
services:
ssa-r-service:
build: .
container_name: ssa-r-statistics
ports:
- "8082:8080" # 主机8082 → 容器8080REDCap占用8080/8081
environment:
- DEV_MODE=true
volumes:
# 开发环境挂载:支持热重载
- ./plumber.R:/app/plumber.R # ⚠️ 重要API 入口也需要挂载
- ./tools:/app/tools
- ./utils:/app/utils
- ./tests:/app/tests
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
```
启动命令:
```bash
cd r-statistics-service
docker-compose up -d
```
#### 4.1.1 热重载机制详解
| 文件类型 | 热重载支持 | 说明 |
|----------|-----------|------|
| `tools/*.R` | ✅ 自动 | DEV_MODE=true 时每次请求重新加载 |
| `utils/*.R` | ⚠️ 需重启 | 服务启动时加载,修改后需 `docker-compose restart` |
| `plumber.R` | ⚠️ 需重启 | API 路由定义,修改后需 `docker-compose restart` |
**最佳实践:**
- 开发新工具时,只需修改 `tools/` 目录,无需重启
- 修改 `utils/``plumber.R` 后,执行 `docker-compose restart`
- 添加新的 API 端点后,需要 `docker-compose up -d --force-recreate`
### 4.2 生产环境 (SAE)
```yaml
# SAE 配置
容器镜像: registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.1
实例规格: 2 vCPU, 4 GB
最小实例: 1
最大实例: 5
端口: 8080
环境变量:
DEV_MODE: "false"
```
### 4.3 环境变量
| 变量 | 默认值 | 说明 |
|------|--------|------|
| `DEV_MODE` | `false` | 开发模式(启用热重载,每次请求重新加载工具脚本) |
> **说明**:开发环境和生产环境都使用真实 OSS无需 Mock 数据。
> - 开发环境:`ai-clinical-data-dev` bucket
> - 生产环境:`ai-clinical-data` bucket
### 4.4 端口配置
| 环境 | 主机端口 | 容器端口 | 说明 |
|------|----------|----------|------|
| **开发环境** | 8082 | 8080 | 避免与 REDCap 8080/8081 冲突 |
| **生产环境 (SAE)** | 8080 | 8080 | 云端无端口冲突 |
> **注意**Node.js 后端通过 `R_SERVICE_URL` 环境变量配置 R 服务地址,默认值为 `http://localhost:8082`。
---
## 5. API 参考
### 5.1 健康检查
```http
GET /health
```
**响应:**
```json
{
"status": "ok",
"timestamp": "2026-02-19 08:00:00",
"version": "1.0.1",
"dev_mode": true,
"tools_loaded": 1
}
```
### 5.2 工具列表
```http
GET /api/v1/tools
```
**响应:**
```json
{
"status": "ok",
"tools": [
"anova_one",
"baseline_table",
"chi_square",
"correlation",
"descriptive",
"fisher",
"linear_reg",
"logistic_binary",
"mann_whitney",
"t_test_ind",
"t_test_paired",
"wilcoxon"
],
"count": 12
}
```
#### 已实现的统计工具12 个)
**Phase 2A 基础工具7 个)**
| tool_code | 名称 | 场景 |
|-----------|------|------|
| `ST_T_TEST_IND` | 独立样本 T 检验 | 两组连续变量比较(正态) |
| `ST_MANN_WHITNEY` | Mann-Whitney U | 两组连续变量比较(非参数) |
| `ST_T_TEST_PAIRED` | 配对 T 检验 | 前后对比 |
| `ST_CHI_SQUARE` | 卡方检验 | 分类变量关联 |
| `ST_CORRELATION` | 相关分析 | Pearson/Spearman 相关 |
| `ST_LOGISTIC_BINARY` | 二元 Logistic 回归 | 多因素分析(二分类结局) |
| `ST_DESCRIPTIVE` | 描述性统计 | 基线表、数据概况 |
**Phase Deploy 新增工具5 个)**
| tool_code | 名称 | 场景 |
|-----------|------|------|
| `ST_FISHER` | Fisher 精确检验 | 小样本/稀疏列联表(卡方替代) |
| `ST_ANOVA_ONE` | 单因素方差分析 | 三组及以上均值比较(含 Kruskal-Wallis 降级) |
| `ST_WILCOXON` | Wilcoxon 符号秩检验 | 配对非参数检验(配对 T 替代) |
| `ST_LINEAR_REG` | 线性回归 | 连续结局多因素分析 |
| `ST_BASELINE_TABLE` | 基线特征表(复合工具) | 基于 gtsummary 的一键式基线表生成 |
### 5.3 执行技能
```http
POST /api/v1/skills/{tool_code}
Content-Type: application/json
```
**请求体:**
```json
{
"data_source": {
"type": "inline",
"data": [...]
},
"params": {
"group_var": "group",
"value_var": "value"
},
"guardrails": {
"check_normality": true
}
}
```
**成功响应:**
```json
{
"status": "success",
"message": "分析完成",
"warnings": null,
"results": {
"method": "Welch Two Sample t-test",
"statistic": -5.196,
"df": 5.98,
"p_value": 0.002,
"p_value_fmt": "p = .002"
},
"plots": ["data:image/png;base64,..."],
"trace_log": [...],
"reproducible_code": "..."
}
```
**错误响应:**
```json
{
"status": "error",
"error_code": "E001",
"error_type": "business",
"message": "列名 'xxx' 在数据中不存在",
"user_hint": "请检查变量名是否拼写正确"
}
```
### 5.4 JIT 护栏检查Phase 2A 新增)
在执行核心统计工具前,调用此端点检验统计假设(正态性、方差齐性等)。
```http
POST /api/v1/guardrails/jit
Content-Type: application/json
```
**请求体:**
```json
{
"data_source": {
"type": "inline",
"data": [...]
},
"tool_code": "ST_T_TEST_IND",
"params": {
"group_var": "sex",
"value_var": "age"
}
}
```
**响应:**
```json
{
"status": "success",
"checks": [
{
"check_name": "正态性检验 (组: 1)",
"passed": true,
"p_value": 0.234,
"recommendation": "满足正态性"
},
{
"check_name": "方差齐性检验 (Levene)",
"passed": false,
"p_value": 0.012,
"recommendation": "建议使用 Welch 校正"
}
],
"suggested_tool": "ST_MANN_WHITNEY",
"can_proceed": true,
"all_checks_passed": false
}
```
**使用场景:**
- 工作流执行器在调用核心统计方法前,先调用 JIT 护栏
- 根据 `suggested_tool` 自动切换到更合适的方法
-`checks` 结果展示给用户
### 5.5 复合工具示例基线特征表Phase Deploy 新增)
```http
POST /api/v1/skills/ST_BASELINE_TABLE
Content-Type: application/json
```
**请求体:**
```json
{
"data_source": {
"type": "inline",
"data": [
{"group": "Drug", "age": 45, "sex": "M", "sbp": 130, "bmi": 24.5, "smoking": "Yes"},
{"group": "Placebo", "age": 47, "sex": "F", "sbp": 128, "bmi": 23.8, "smoking": "No"}
]
},
"params": {
"group_var": "group",
"analyze_vars": ["age", "sex", "sbp", "bmi", "smoking"]
}
}
```
**成功响应(核心字段):**
```json
{
"status": "success",
"results": {
"n_total": 30,
"n_groups": 2,
"n_variables": 5,
"significant_vars": ["sbp"],
"method_info": [
{"variable": "age", "method": "Wilcoxon rank sum test"},
{"variable": "sex", "method": "Fisher's exact test"}
]
},
"report_blocks": [
{
"type": "table",
"headers": ["Characteristic", "Drug, N = 15", "Placebo, N = 15", "p-value"],
"rows": [["age", "49 (42, 55)", "47 (41, 54)", "0.6"]],
"title": "基线特征表 (按 group 分组)",
"metadata": { "is_baseline_table": true, "group_var": "group", "has_p_values": true }
}
]
}
```
> **特点:** `ST_BASELINE_TABLE` 是复合工具,基于 `gtsummary::tbl_summary()` 自动判断变量类型(连续/分类、选择统计方法T 检验/Mann-Whitney/卡方/Fisher输出标准三线表。`report_blocks[0].metadata.is_baseline_table = true` 触发前端特殊渲染P 值标星、rowspan 合并行)。
---
## 6. 开发指南
### 6.1 添加新工具
1.`tools/` 目录创建 R 脚本:
```r
# tools/my_analysis.R
#' @tool_code ST_MY_ANALYSIS
#' @name 我的分析工具
#' @version 1.0.0
#' @description 工具描述
#' @author SSA-Pro Team
library(glue)
library(ggplot2)
library(base64enc)
run_analysis <- function(input) {
# ===== 初始化日志 =====
logs <- c()
log_add <- function(msg) { logs <<- c(logs, paste0("[", Sys.time(), "] ", msg)) }
# ===== 数据加载 =====
log_add("开始加载输入数据")
df <- tryCatch(
load_input_data(input),
error = function(e) {
log_add(paste("数据加载失败:", e$message))
return(NULL)
}
)
if (is.null(df)) {
return(make_error(ERROR_CODES$E100_INTERNAL_ERROR, details = "数据加载失败"))
}
log_add(glue("数据加载成功: {nrow(df)} 行, {ncol(df)} 列"))
# ===== 参数提取 =====
p <- input$params
my_var <- p$my_var
# ===== 参数校验 =====
if (!(my_var %in% names(df))) {
return(make_error(ERROR_CODES$E001_COLUMN_NOT_FOUND, col = my_var))
}
# ===== 护栏检查 =====
guardrail_results <- list()
warnings_list <- c()
sample_check <- check_sample_size(nrow(df), min_required = 10, action = ACTION_WARN)
guardrail_results <- c(guardrail_results, list(sample_check))
guardrail_status <- run_guardrail_chain(guardrail_results)
if (guardrail_status$status == "blocked") {
return(list(status = "blocked", message = guardrail_status$reason, trace_log = logs))
}
# ===== 核心计算 =====
log_add("执行分析...")
# result <- your_analysis_function(df, ...)
# ===== 构建 report_blocks必须 =====
blocks <- list()
# Block: 检验结果key_value
blocks[[length(blocks) + 1]] <- make_kv_block(
list("方法" = "Your Method", "统计量" = "1.234", "P 值" = format_p_value(0.05)),
title = "检验结果"
)
# Block: 图表image
plot_base64 <- tryCatch({
p <- ggplot(df, aes(x = df[[my_var]])) + geom_histogram() + theme_minimal()
tmp_file <- tempfile(fileext = ".png")
ggsave(tmp_file, p, width = 7, height = 5, dpi = 100)
base64_str <- base64encode(tmp_file)
unlink(tmp_file)
paste0("data:image/png;base64,", base64_str)
}, error = function(e) NULL)
if (!is.null(plot_base64)) {
blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "分析图表")
}
# Block: 结论markdown
blocks[[length(blocks) + 1]] <- make_markdown_block("分析结论...", title = "结论摘要")
# ===== 生成可复现代码 =====
reproducible_code <- glue('
# SSA-Pro 自动生成代码
# 工具: 我的分析工具
# 时间: {Sys.time()}
# ================================
df <- read.csv("data.csv")
# 你的分析代码...
')
# ===== 返回结果 =====
log_add("分析完成")
return(list(
status = "success",
message = "分析完成",
warnings = if (length(warnings_list) > 0) warnings_list else NULL,
results = list(
# 统计结果(使用 jsonlite::unbox 保证单值不被包装成数组)
statistic = jsonlite::unbox(1.234),
p_value = jsonlite::unbox(0.05),
p_value_fmt = format_p_value(0.05)
),
report_blocks = blocks, # ⚠️ 必须!前端 DynamicReport 依赖此字段渲染
plots = if (!is.null(plot_base64)) list(plot_base64) else list(),
trace_log = logs,
reproducible_code = as.character(reproducible_code)
))
}
```
2. **开发模式**:修改 `tools/` 下的文件后,无需重启,下次请求自动加载
3. 测试:
```bash
curl -X POST http://localhost:8082/api/v1/skills/ST_MY_ANALYSIS \
-H "Content-Type: application/json" \
-d '{"data_source": {"type": "inline", "data": [{"x": 1}, {"x": 2}]}, "params": {"my_var": "x"}}'
```
### 6.2 工具命名规范
| 项目 | 规范 |
|------|------|
| 文件名 | 小写下划线:`t_test_ind.R` |
| tool_code | 大写下划线:`ST_T_TEST_IND` |
| 入口函数 | 固定名称:`run_analysis` |
### 6.3 结果格式规范
```r
return(list(
status = "success" | "error" | "blocked",
message = "...",
warnings = c("...") | NULL,
results = list(
# 统计结果(使用 jsonlite::unbox() 保证单值不被包装成数组)
),
report_blocks = list(
# Block-based 标准化输出Phase E+ 协议),前端 DynamicReport.tsx 统一渲染
# 支持 4 种 Block 类型markdown / table / image / key_value
# 通过 utils/block_helpers.R 的辅助函数构建
),
plots = list(
"data:image/png;base64,..."
),
trace_log = c("..."),
reproducible_code = "..."
))
```
### 6.4 Block-based 输出协议Phase E+ 标准)
所有工具**必须**通过 `utils/block_helpers.R` 构建 `report_blocks[]`,前端 `DynamicReport.tsx` 根据 `block.type` 统一渲染。
| 辅助函数 | Block 类型 | 用途 |
|----------|-----------|------|
| `make_markdown_block(content, title)` | `markdown` | 文本结论、方法说明 |
| `make_table_block(headers, rows, title, footnote, metadata)` | `table` | 统计结果表、系数表、事后比较表 |
| `make_table_block_from_df(df, title, footnote, digits)` | `table` | 从 data.frame 快速构建表格 |
| `make_image_block(base64_data, title, alt)` | `image` | 图表base64 编码 PNG |
| `make_kv_block(items, title)` | `key_value` | 检验统计量、模型拟合指标 |
**示例:**
```r
blocks <- list()
blocks[[length(blocks) + 1]] <- make_kv_block(
list("检验方法" = "Welch t-test", "统计量" = "t = -2.35", "P 值" = "p = .021"),
title = "检验结果"
)
blocks[[length(blocks) + 1]] <- make_image_block(plot_base64, title = "组间比较箱线图")
blocks[[length(blocks) + 1]] <- make_markdown_block("两组差异具有统计学意义...", title = "结论")
```
### 6.5 各工具参数快速参考
> 调用 `POST /api/v1/skills/{tool_code}` 时,`params` 对象需要的字段速查。
| tool_code | 必需参数 | 可选参数 |
|-----------|---------|---------|
| `ST_T_TEST_IND` | `group_var`, `value_var` | `guardrails.check_normality` |
| `ST_MANN_WHITNEY` | `group_var`, `value_var` | — |
| `ST_T_TEST_PAIRED` | `before_var`, `after_var` | `guardrails.check_normality` |
| `ST_CHI_SQUARE` | `var1`, `var2` | — |
| `ST_CORRELATION` | `var_x`, `var_y` | `method` (`"auto"` / `"pearson"` / `"spearman"`) |
| `ST_LOGISTIC_BINARY` | `outcome_var`, `predictors` (数组) | — |
| `ST_DESCRIPTIVE` | `variables` (数组) | `group_var` |
| `ST_FISHER` | `var1`, `var2` | — |
| `ST_ANOVA_ONE` | `group_var`, `value_var` | `guardrails.check_normality` |
| `ST_WILCOXON` | `before_var`, `after_var` | — |
| `ST_LINEAR_REG` | `outcome_var`, `predictors` (数组) | `confounders` (数组) |
| `ST_BASELINE_TABLE` | `group_var` | `analyze_vars` (数组,不传则自动全选) |
### 6.6 R 语言陷阱速查(从实际 Bug 中总结)
> **Phase Deploy 开发中实际踩过的坑**,后续开发者必读。每条附真实错误信息和修复方法。
#### 陷阱 1JSON 数组参数在 R 中是 `list`,不是 `character` 向量
**错误信息:** `invalid subscript type 'list'`
**原因:** plumber 解析 JSON `["age", "sex", "bmi"]`R 拿到的是 `list("age", "sex", "bmi")`,不是 `c("age", "sex", "bmi")`。对 list 做 `%in%``[` 等操作都会报错。
```r
# ❌ 错误:直接使用 JSON 传入的数组参数
analyze_vars <- p$analyze_vars
missing <- analyze_vars[!(analyze_vars %in% names(df))] # 报错!
# ✅ 正确:先转换为字符向量
analyze_vars <- as.character(unlist(p$analyze_vars))
missing <- analyze_vars[!(analyze_vars %in% names(df))] # 正常
```
**影响范围:** 所有接收数组参数的工具(`predictors``variables``analyze_vars``confounders`)。
#### 陷阱 2`list()` 中不能用表达式做键名
**错误信息:** `unexpected '='`
**原因:** R 的 `list()` 构造器只接受**字面量**作为名称,不接受 `paste0()``glue()` 等函数调用。
```r
# ❌ 错误:用表达式做键名
items <- list(
paste0(var_name, " Median") = "5.2" # 语法错误!
)
# ✅ 正确:先创建 list 再用 [[ 赋值
items <- list()
items[[paste0(var_name, " Median")]] <- "5.2"
```
#### 陷阱 3`tryCatch` 会吞掉 warning 导致结果丢失
**错误信息:** 无明确错误,但返回 NULL 或非预期结果
**原因:** `tryCatch(expr, warning = function(w) {...})` 捕获第一个 warning 后**中断 expr 执行**,返回 warning handler 的返回值。gtsummary、car 等包常发 warning导致主计算被中断。
```r
# ❌ 错误tryCatch 捕获 warning 会中断执行
tbl <- tryCatch({
tbl_summary(df) %>% add_p() # 如果 add_p() 发 warningtbl 变成 NULL
}, warning = function(w) {
invokeRestart("muffleWarning") # 在 tryCatch 中无效!
})
# ✅ 正确withCallingHandlers 处理 warning不中断执行tryCatch 只捕获 error
tbl <- tryCatch(
withCallingHandlers(
{ tbl_summary(df) %>% add_p() },
warning = function(w) {
warnings_list <<- c(warnings_list, w$message)
invokeRestart("muffleWarning")
}
),
error = function(e) { return(NULL) }
)
```
#### 陷阱 4gtsummary `table_body` 的 p.value 是 list 列
**错误信息:** `invalid subscript type 'list'`
**原因:** `gtsummary` 的内部数据结构 `tbl$table_body$p.value` 是 list 列(每个元素可能是 NULL 或 numeric不能直接用 `<` 比较。
```r
# ❌ 错误:直接对 list 列做比较
p_rows <- body[body$p.value < 0.05, ] # 报错!
# ✅ 正确:先 unlist + as.numeric
p_vals <- as.numeric(unlist(body$p.value))
sig_idx <- which(!is.na(p_vals) & p_vals < 0.05)
```
#### 陷阱 5浮点数比较不能用 `==`
**错误信息:** 无明确错误,但条件判断逻辑错误
```r
# ❌ 错误:直接比较浮点数
if (sd(values) == 0) { ... } # 可能因精度问题漏判
# ✅ 正确:使用容差比较
if (isTRUE(sd(values) < .Machine$double.eps^0.5)) { ... }
```
#### 陷阱 6变量可能为 NULL 导致 glue/round 崩溃
**错误信息:** `non-numeric argument to mathematical function``subscript out of bounds`
**原因:** 某些统计结果字段(如 `fstatistic`)在边界条件下为 NULL。
```r
# ❌ 错误:直接使用可能为 NULL 的值
log_add(glue("F = {round(f_stat[1], 2)}")) # f_stat 为 NULL 时崩溃
# ✅ 正确:先检查再使用
if (!is.null(f_stat)) {
log_add(glue("F = {round(f_stat[1], 2)}"))
} else {
log_add("F = NA")
}
```
#### 陷阱 7新增 R 包后 `utils/` 修改需要重启容器
**现象:** `make_table_block()` 新增了 `metadata` 参数,但调用时报 `unused argument`
**原因:** `utils/*.R` 在服务启动时一次性加载,不像 `tools/*.R` 有热重载。修改后必须:
```bash
cd r-statistics-service
docker-compose restart
```
### 6.7 开发环境新增 R 包
当新工具依赖尚未安装的 R 包时,有两种方式:
**方式 1临时安装到运行中的容器开发测试用**
```bash
# 容器以 appuser 运行,无写权限,需用 root
docker exec -u root ssa-r-statistics R -e "install.packages('新包名', repos='https://cloud.r-project.org/', quiet=TRUE)"
```
> 注意:容器重启后丢失,仅用于开发验证。
**方式 2更新 Dockerfile 并重建镜像(正式方案)**
1.`Dockerfile``install.packages()` 中添加新包名
2. 重建:`docker-compose up -d --build`
---
## 7. 运维指南
### 7.1 日志查看
```bash
# 实时日志
docker logs -f ssa-r-statistics
# 最近 100 行
docker logs --tail 100 ssa-r-statistics
```
### 7.2 性能监控
```bash
# 容器资源使用
docker stats ssa-r-statistics
```
### 7.3 重启服务
```bash
# 开发环境
docker-compose restart
# 生产环境 (SAE)
通过 SAE 控制台重启实例
```
### 7.4 镜像更新
```bash
# 1. 构建新镜像
docker build -t ssa-r-statistics:1.0.2 .
# 2. 推送到镜像仓库
docker tag ssa-r-statistics:1.0.2 registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.2
docker push registry.cn-beijing.aliyuncs.com/aiclinical/ssa-r-statistics:1.0.2
# 3. 更新 SAE 部署
```
---
## 8. 常见问题
### Q1: 构建时 httpuv 安装失败
**错误:** `fatal error: zlib.h: No such file or directory`
**解决:** 添加 `zlib1g-dev` 到系统依赖
### Q2: 构建时 nloptr 安装失败
**错误:** `CMAKE NOT FOUND`
**解决:** 添加 `cmake` 到系统依赖
### Q3: /tmp 权限问题
**错误:** `cannot open file '/tmp/Rtmpxxx': No such file or directory`
**解决:** 不要在启动命令中清理 /tmp
### Q4: DEV_MODE 热重载不生效
**原因:** 没有挂载 volumes
**解决:**
```yaml
volumes:
- ./tools:/app/tools
```
### Q5: 容器启动后无法访问
**检查:**
1. 端口映射是否正确
2. 健康检查是否通过
3. 查看容器日志
### Q6: 数据加载失败inline 模式)
**错误:** `内部错误: 数据加载失败`
**原因:** 数据格式不正确,或数据为空
**解决:**
1. 确保 `data_source.data` 是有效的 JSON 数组
2. 行格式:`[{"col1": val1}, {"col1": val2}]`
3. 检查是否有空数据或全 NA 列
### Q7: R 脚本语法错误
**错误:** `unexpected symbol``lexical error`
**常见原因:**
1. `glue()` 字符串中使用 `\'` 转义(应直接使用 `'`
2. 中文注释编码问题
3. 代码块中的花括号不匹配
**解决:**
```r
# 错误glue 中的转义
glue("# Cramer\'s V = ...") # ❌
# 正确:直接使用单引号或避免
glue("# Cramer V = ...") # ✅
```
### Q8: JSON 序列化失败
**错误:** `No method asJSON S3 class: table`
**原因:** R 的 `table` 对象无法直接序列化为 JSON
**解决:**
```r
# 错误
observed = as.matrix(contingency_table) # ❌ 可能保留 table 属性
# 正确:显式转换为纯数值矩阵
observed = matrix(
as.numeric(contingency_table),
nrow = nrow(contingency_table),
ncol = ncol(contingency_table)
) # ✅
```
### Q9: 新端点返回 404
**原因:** 修改 `plumber.R` 后未重启服务
**解决:**
```bash
# 修改 plumber.R 后必须重启
docker-compose restart
# 如果修改了 docker-compose.yml如添加新 volume
docker-compose up -d --force-recreate
```
### Q10: 变量类型判断错误missing value where TRUE/FALSE needed
**原因:** 对包含 NA 的数据进行布尔比较
**解决:**
```r
# 错误
if (var_type == "numeric") { ... } # var_type 可能是 NA
# 正确
if (identical(var_type, "numeric")) { ... } # ✅ 处理 NA
```
### Q11: 修改 utils/ 后新参数报 `unused argument`
**原因:** `utils/*.R`(如 `block_helpers.R`)在服务启动时加载进内存,不支持热重载(与 `tools/*.R` 不同)。
**解决:**
```bash
docker-compose restart
```
### Q12: Docker 已 build 但包仍不存在(`there is no package called 'xxx'`
**原因:** `docker-compose.yml` 中的 `volumes` 挂载会覆盖镜像中的文件,但**不影响已安装的 R 包**。常见场景是更新了 Dockerfile 却只用了 `docker-compose up -d` 而没有加 `--build`
**解决:**
```bash
# 确保 rebuild
docker-compose up -d --build
# 或临时装包(开发验证)
docker exec -u root ssa-r-statistics R -e "install.packages('xxx', repos='https://cloud.r-project.org/', quiet=TRUE)"
```
### Q13: 工具返回成功但 report_blocks 为空
**原因:** 返回结构中没有 `report_blocks` 字段或 blocks 列表为空。
**检查清单:**
1. 确认使用了 `utils/block_helpers.R` 的辅助函数构建 blocks
2. 确认 return 中包含 `report_blocks = blocks`
3. 确认每个 block 至少包含 `type` 字段
4. 用测试脚本验证:`node r-statistics-service/tests/run_all_tools_test.js`
---
## 9. 测试指南
### 9.1 单工具测试
```bash
# 测试 T 检验
curl -s -X POST "http://localhost:8082/api/v1/skills/ST_T_TEST_IND" \
-H "Content-Type: application/json" \
-d '{
"data_source": {
"type": "inline",
"data": [
{"group": "A", "value": 23}, {"group": "A", "value": 25},
{"group": "B", "value": 30}, {"group": "B", "value": 32}
]
},
"params": {"group_var": "group", "value_var": "value"}
}'
```
### 9.2 健康检查
```bash
curl -s http://localhost:8082/health | jq
```
### 9.3 R 工具集中测试脚本12 工具 + JIT
项目提供了 R 统计引擎的全工具测试脚本:
```bash
# 仅测试 R 服务层12 工具 + JIT 护栏 + report_blocks 校验)
node r-statistics-service/tests/run_all_tools_test.js
```
测试覆盖:
- 12 个统计工具Phase 2A × 7 + Phase Deploy × 5
- JIT 护栏检查ST_T_TEST_IND / ST_ANOVA_ONE / ST_FISHER / ST_LINEAR_REG
- `report_blocks` 协议校验类型、必填字段、metadata
### 9.4 端到端测试脚本(三层联调)
三层联调测试覆盖 R → Python → Node.js
```bash
cd docs/03-业务模块/SSA-智能统计分析/05-测试文档
node run_e2e_test.js
```
测试覆盖:
- Layer 1: R 服务12 个统计工具 + JIT 护栏)
- Layer 2: Python DataProfile API
- Layer 3: Node.js 后端 API登录 → 会话 → 规划 → 执行)
---
## 附录:文件结构
```
r-statistics-service/
├── Dockerfile # 生产镜像定义(含 gtsummary/gt/broom/scales/gridExtra
├── docker-compose.yml # 开发环境编排(含 volume 挂载)
├── renv.lock # R 包版本锁定(备用)
├── .Rprofile # R 启动配置(备用)
├── plumber.R # API 入口(含 JIT 护栏端点,自动发现 tools/ 目录)
├── utils/
│ ├── data_loader.R # 数据加载(支持行格式/列格式)
│ ├── guardrails.R # 统计护栏 + JIT 检查12 工具全覆盖)
│ ├── error_codes.R # 错误映射
│ ├── result_formatter.R # 结果格式化
│ └── block_helpers.R # Block-based 输出辅助函数Phase E+ 协议)
├── tools/ # 统计工具12 个)
│ ├── t_test_ind.R # 独立样本 T 检验
│ ├── t_test_paired.R # 配对 T 检验
│ ├── mann_whitney.R # Mann-Whitney U 检验
│ ├── chi_square.R # 卡方检验
│ ├── correlation.R # 相关分析
│ ├── logistic_binary.R # 二元 Logistic 回归
│ ├── descriptive.R # 描述性统计
│ ├── fisher.R # 🆕 Fisher 精确检验Phase Deploy
│ ├── anova_one.R # 🆕 单因素方差分析Phase Deploy
│ ├── wilcoxon.R # 🆕 Wilcoxon 符号秩检验Phase Deploy
│ ├── linear_reg.R # 🆕 线性回归Phase Deploy
│ └── baseline_table.R # 🆕 基线特征表 — 复合工具Phase Deploy
├── tests/
│ ├── run_all_tools_test.js # 🆕 全工具自动化测试12 工具 + JIT + blocks 校验)
│ ├── test_t_test.json # T 检验测试数据
│ ├── test_fisher.json # Fisher 测试数据
│ ├── test_anova_one.json # ANOVA 测试数据
│ ├── test_wilcoxon.json # Wilcoxon 测试数据
│ ├── test_linear_reg.json # 线性回归测试数据
│ ├── test_baseline_table.json # 基线表测试数据
│ └── fixtures/
│ └── normal_data.csv # 测试数据
├── metadata/ # 工具元数据(预留)
└── templates/ # 解释模板(预留)
```
---
## 更新日志
| 版本 | 日期 | 更新内容 |
|------|------|----------|
| v1.3 | 2026-02-22 | 开发者体验增强:新工具模板补全 report_blocks§6.1)、各工具 params 速查表§6.5、R 语言 7 大陷阱实录§6.6)、新增 R 包操作指南§6.7)、新增 Q11-Q13 常见问题 |
| v1.2 | 2026-02-22 | Phase Deploy 完成:工具 7→12+Fisher/ANOVA/Wilcoxon/线性回归/基线表、Dockerfile 新增 gtsummary 等 5 包、Block-based 输出协议文档化§6.4)、全工具测试脚本 |
| v1.1 | 2026-02-20 | Phase 2A 完成7 个统计工具、JIT 护栏、热重载说明、常见问题补充 |
| v1.0 | 2026-02-19 | 初始版本架构设计、部署指南、T 检验工具 |
---
**文档结束**