Files
hms/docs/superpowers/specs/2026-04-25-erp-ai-module-design.md
iven db626d27b8
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
docs(ai): 修复 spec 审查问题 — 路由模式/HealthDataProvider/AiState/缓存/权限
修复 2 CRITICAL + 4 HIGH + 3 MEDIUM:
- CRITICAL: 路由注册改为 public_routes/protected_routes 静态方法
- CRITICAL: 补全 HealthDataProvider trait + DTO 定义 + 注入机制
- HIGH: 修复 async_trait + impl Stream 不兼容 (改用 Pin<Box<dyn Stream>>)
- HIGH: 新增 AiConfig 配置段 + config.toml 定义
- HIGH: sanitized_input 改为 AES-256 加密 + 90 天自动清理
- HIGH: 新增 futures/tokio-stream/async-stream 依赖说明
- MEDIUM: 事件改为 DomainEvent 字符串模式
- MEDIUM: 权限码改为 .list/.manage 实体命名模式
- MEDIUM: 补全 AiState + FromRef 注入模式
- MEDIUM: 补全缓存设计 (Redis/键格式/TTL/Hash算法)
2026-04-25 12:34:34 +08:00

21 KiB
Raw Blame History

erp-ai 模块设计规格

日期: 2026-04-25 | 状态: 设计审批通过,待实施 技术架构演进方向: 实时能力层 → AI 智能分析流


1. 背景与目标

HMS 健康管理平台已完成全部基础模块和健康业务模块的开发。为提升患者端体验、降低医患沟通成本,引入 AI 智能分析能力,让患者在小程序端即可获得化验单解读、健康趋势分析、个性化体检方案和报告摘要等 AI 驱动的自助服务。

核心目标:

  • 患者按需获取 AI 流式解读,无需等待医生电话
  • 模型无关的 AI Gateway支持多提供商切换和 A/B 测试
  • 严格数据脱敏PII 不离开服务器
  • Prompt 模板数据库化管理,支持版本控制和回滚
  • 架构预留异步分析管道,平滑演进到混合模式

2. 模块定位

2.1 在 HMS 架构中的位置

erp-core (ErpModule trait + EventBus + AppState)
    ↑
erp-health ──publish──→ {patient.lab-report.created, health.vital-signs.updated, ...}
    ↑
erp-ai (NEW) ──subscribe──→ health 事件 (Phase 3 预留)
              ──publish──→ {ai.analysis.completed, ai.analysis.failed}
    ↑
erp-server (组装入口)

2.2 为什么是独立模块而非内嵌

  • AI 是跨横切平台能力,非健康专属(未来消息、工作流、客服都可能用)
  • 遵循 CLAUDE.md 铁律:业务 crate 之间禁止直接依赖
  • 通过 EventBus + HealthDataProvider trait 获取数据,模块边界清晰
  • 提供商切换、成本管理、脱敏策略全部内聚在 erp-ai 内部

3. 架构设计

3.1 Crate 结构

crates/erp-ai/
├── Cargo.toml
└── src/
    ├── lib.rs              # 模块入口, ErpModule impl
    ├── error.rs            # AiError → AppError
    ├── provider/           # AI 提供商抽象
    │   ├── mod.rs          # AiProvider trait 定义
    │   ├── claude.rs       # Claude API 实现
    │   ├── openai.rs       # OpenAI API 实现 (Phase 2)
    │   └── config.rs       # 提供商配置 & 路由策略
    ├── pipeline/           # 分析管道
    │   ├── mod.rs          # AnalysisPipeline trait
    │   ├── request.rs      # 请求驱动管道 (Phase 1, SSE)
    │   └── async.rs        # 异步管道 (Phase 3, stub)
    ├── sanitization/       # 数据脱敏层
    │   ├── mod.rs          # DeIdentificationService
    │   └── rules.rs        # 脱敏规则
    ├── prompt/             # Prompt 管理
    │   ├── mod.rs          # PromptManager trait
    │   └── template.rs     # 模板渲染引擎
    ├── entity/             # SeaORM Entity
    │   ├── ai_prompt.rs
    │   ├── ai_analysis.rs
    │   └── ai_usage.rs
    ├── service/            # 业务逻辑
    │   ├── analysis.rs     # AnalysisService (核心编排)
    │   ├── prompt.rs       # PromptService (CRUD + 版本)
    │   └── usage.rs        # UsageService (用量追踪)
    └── handler/            # Axum 路由
        ├── mod.rs
        ├── analysis.rs     # SSE 流式分析端点
        ├── prompt.rs       # Prompt 管理 CRUD
        └── usage.rs        # 用量统计端点

3.2 核心抽象

/// AI 提供商 trait
/// 使用 Pin<Box<dyn Stream>> 避免 async_trait + impl Trait 不兼容问题
#[async_trait]
pub trait AiProvider: Send + Sync {
    async fn stream_generate(
        &self,
        req: GenerateRequest,
    ) -> Result<Pin<Box<dyn Stream<Item = Result<Chunk>> + Send>>>;
    async fn generate(&self, req: GenerateRequest) -> Result<GenerateResponse>;
    fn name(&self) -> &str;
    async fn health_check(&self) -> Result<bool>;
}

/// 分析管道 trait (Phase 1: RequestPipeline; Phase 3: + AsyncPipeline)
pub trait AnalysisPipeline: Send + Sync {
    fn analyze(
        &self,
        ctx: AnalysisContext,
    ) -> Pin<Box<dyn Stream<Item = AnalysisChunk> + Send>>;
}

/// 数据脱敏 trait — 输入为 HealthDataProvider 返回的 DTO非原始 SeaORM Entity
pub trait DataSanitizer: Send + Sync {
    fn sanitize(&self, data: &HealthAnalysisInput) -> Result<SanitizedData>;
}

/// 分析上下文
pub struct AnalysisContext {
    pub trigger: AnalysisTrigger,
    pub tenant_id: Uuid,
    pub user_id: Uuid,
    pub analysis_type: AnalysisType,
    pub source_ref: String,
}

pub enum AnalysisTrigger {
    Request,  // Phase 1: 患者主动请求
    Event,    // Phase 3: 异步事件触发 (预留)
}

3.3 路由注册(遵循现有 public_routes / protected_routes 模式)

/// erp-ai 模块注册(参照 erp-health 的注册方式)
pub struct AiModule;

impl AiModule {
    pub fn public_routes<S>() -> Router<S>
    where
        S: Clone + Send + Sync + 'static,
        AiState: FromRef<S>,
    {
        Router::new()
            .nest("/api/v1/ai/analyze", analyze::public_routes::<S>())
    }

    pub fn protected_routes<S>() -> Router<S>
    where
        S: Clone + Send + Sync + 'static,
        AiState: FromRef<S>,
    {
        Router::new()
            .nest("/api/v1/ai/analyze", analyze::protected_routes::<S>())
            .nest("/api/v1/ai/analysis", analysis::routes::<S>())
            .nest("/api/v1/ai/prompts", prompt::routes::<S>())
            .nest("/api/v1/ai/usage", usage::routes::<S>())
            .nest("/api/v1/ai/admin", admin::routes::<S>())
    }
}

// erp-server/src/main.rs 中合并路由:
// let app = Router::new()
//     .merge(AuthModule::public_routes())
//     // ...
//     .merge(AiModule::public_routes())
//     .merge(AuthModule::protected_routes())
//     // ...
//     .merge(AiModule::protected_routes())

3.4 AiState 注入(遵循现有 FromRef 模式)

/// erp-ai 的 Axum State
pub struct AiState {
    pub db: DatabaseConnection,
    pub event_bus: EventBus,
    pub provider_registry: ProviderRegistry,
    pub sanitizer: Arc<dyn DataSanitizer>,
    pub prompt_manager: Arc<dyn PromptManager>,
    pub redis: deadpool_redis::Pool,
    pub config: AiConfig,
}

// erp-server/src/state.rs 中新增:
// impl FromRef<AppState> for AiState { ... }

3.5 HealthDataProvider trait 与数据 DTO

// === erp-core 中新增 ===

/// 健康数据提供者 trait由 erp-health 实现
/// 通过 AppState 中的 Arc<dyn HealthDataProvider> 注入到 erp-ai
#[async_trait]
pub trait HealthDataProvider: Send + Sync {
    /// 获取化验报告(指标列表)
    async fn get_lab_report(
        &self,
        tenant_id: Uuid,
        report_id: Uuid,
    ) -> AppResult<LabReportDto>;

    /// 获取生命体征趋势数据
    async fn get_vital_signs(
        &self,
        tenant_id: Uuid,
        patient_id: Uuid,
        metrics: &[String],
        range: TimeRange,
    ) -> AppResult<Vec<VitalSignDto>>;

    /// 获取患者摘要(用于个性化方案,不含 PII 的 DTO
    async fn get_patient_summary(
        &self,
        tenant_id: Uuid,
        patient_id: Uuid,
    ) -> AppResult<PatientSummaryDto>;

    /// 获取完整健康报告(用于摘要生成)
    async fn get_full_report(
        &self,
        tenant_id: Uuid,
        report_id: Uuid,
    ) -> AppResult<HealthReportDto>;
}

// === DTO 定义erp-core 中,已脱去 PII 的数据传输对象)===
// 注意: erp-health 实现此 trait 时,返回的 DTO 不包含患者姓名、身份证号等 PII
// 而是只包含 age_group / sex / 科室 / 检验指标 / 诊断编码 等医疗数据

pub struct LabReportDto {
    pub age_group: String,       // "40-50"
    pub sex: String,             // "male" / "female"
    pub department: String,      // 科室
    pub report_date: String,     // 报告日期
    pub items: Vec<LabItemDto>,  // 检验项目列表
}

pub struct LabItemDto {
    pub name: String,            // 指标名称
    pub value: f64,              // 检测值
    pub unit: String,            // 单位
    pub reference_range: String, // 参考范围
    pub is_abnormal: bool,       // 是否异常
}

pub struct VitalSignDto {
    pub metric: String,          // 指标名
    pub values: Vec<(String, f64)>, // (日期, 值) 时间序列
    pub unit: String,
}

pub struct PatientSummaryDto {
    pub age_group: String,
    pub sex: String,
    pub chronic_conditions: Vec<String>,  // 慢性病编码
    pub medications: Vec<String>,         // 当前用药
    pub family_history: Vec<String>,      // 家族史
    pub last_checkup_date: String,
}

pub struct HealthReportDto {
    pub age_group: String,
    pub sex: String,
    pub department: String,
    pub report_date: String,
    pub sections: Vec<ReportSectionDto>,
}

pub struct ReportSectionDto {
    pub title: String,
    pub findings: Vec<String>,
    pub abnormal_items: Vec<String>,
}

3.6 事件定义(遵循现有 DomainEvent 字符串模式)

// AI 事件使用现有 DomainEvent 结构event_type 为字符串
// 而非独立的 AiEvent enum

// ai.analysis.completed 事件 payload:
// {
//   "analysis_id": "uuid",
//   "patient_id": "uuid",
//   "analysis_type": "lab_report",
//   "status": "completed",
//   "model_used": "claude-sonnet-4-6"
// }

// ai.analysis.failed 事件 payload:
// {
//   "analysis_id": "uuid",
//   "patient_id": "uuid",
//   "analysis_type": "lab_report",
//   "error": "provider timeout"
// }

3.7 权限码(遵循 .list / .manage 实体命名模式)

/// 权限码遵循 CLAUDE.md 规范: 每个实体声明 .list + .manage
pub const AI_PERMISSIONS: &[&str] = &[
    "ai.analysis.list",       // 查看分析历史列表
    "ai.analysis.manage",     // 请求新分析 / 管理分析结果
    "ai.prompt.list",         // 查看 Prompt 列表
    "ai.prompt.manage",       // 创建/编辑/激活/回滚 Prompt
    "ai.usage.list",          // 查看用量统计
    "ai.provider.manage",     // 管理提供商配置
];

3.8 配置结构(遵循现有 AppConfig 模式)

# config/default.toml 中新增 [ai] 段

[ai]
default_provider = "claude"
cache_ttl_seconds = 604800        # 7 天
rate_limit_patient_daily = 10
rate_limit_admin_hourly = 100
sanitized_input_retention_days = 90  # 脱敏输入保留 90 天

[ai.providers.claude]
api_key_env = "ANTHROPIC_API_KEY"  # 从环境变量读取
model = "claude-sonnet-4-6"
max_tokens = 2048
temperature = 0.3

[ai.providers.openai]
enabled = false
api_key_env = "OPENAI_API_KEY"
model = "gpt-4o"
// erp-server/src/config.rs 新增:
#[derive(Debug, Deserialize)]
pub struct AiConfig {
    pub default_provider: String,
    pub cache_ttl_seconds: u64,
    pub rate_limit_patient_daily: u32,
    pub rate_limit_admin_hourly: u32,
    pub sanitized_input_retention_days: u32,
    pub providers: HashMap<String, ProviderConfig>,
}

3.9 流式依赖

# erp-ai/Cargo.toml 新增依赖
[dependencies]
futures = "0.3"
tokio-stream = "0.1"
async-stream = "0.3"
# axum 已内置 SSE 支持: axum::response::sse::{Event, Sse}

4. 数据流设计

4.1 请求驱动管道 (Phase 1)

患者点击"AI解读" → POST /api/v1/ai/analyze/lab-report
  → 鉴权 (JWT → tenant_id + user_id)
  → 权限检查 (ai.analysis.request)
  → 加载数据 (via HealthDataProvider trait)
  → DeIdentificationService.sanitize()
  → PromptManager.render(template, sanitized_data)
  → AiProvider.stream_generate(prompt)
  → SSE 流式返回给前端
  → 存储完整结果到 ai_analysis_results
  → 记录用量到 ai_usage_logs
  → 发布 ai.analysis.completed 事件

4.2 SSE 事件格式

event: chunk
data: {"content": "您的血常规检查中,", "index": 1}

event: metadata
data: {"model": "claude-sonnet-4-6", "tokens": {"input": 856, "output": 423}, "duration_ms": 3200}

event: done
data: {"analysis_id": "uuid-xxx", "status": "completed"}

4.3 四种分析场景

场景 端点 输入 Prompt 模板
化验单解读 POST /ai/analyze/lab-report report_id lab_report_interpretation
趋势分析 POST /ai/analyze/trends patient_id + metrics + 时间范围 health_trend_analysis
个性化方案 POST /ai/analyze/checkup-plan patient_id personalized_checkup_plan
报告摘要 POST /ai/analyze/report-summary report_id report_summary_generation

5. 数据库设计

5.1 ai_prompts

类型 说明
id UUID v7 主键
tenant_id UUID 租户
name VARCHAR(100) 模板标识
description TEXT 用途描述
system_prompt TEXT 系统角色 Prompt
user_prompt_template TEXT 用户 Prompt 模板 ({{variable}} 占位符)
variables_schema JSONB 模板变量 JSON Schema
model_config JSONB {provider, model, temperature, max_tokens}
version INT 语义版本(自增)
is_active BOOLEAN 当前激活版本
category VARCHAR(50) analysis / summary / suggestion
tags JSONB 标签数组
+ 标准审计字段 tenant_id, created_at, updated_at, created_by, updated_by, deleted_at, version

5.2 ai_analysis_results

类型 说明
id UUID v7 主键
tenant_id UUID 租户
patient_id UUID 患者
analysis_type VARCHAR(50) lab_report / trend / checkup_plan / report_summary
source_ref VARCHAR(200) 来源引用
prompt_id UUID 关联 Prompt
prompt_version INT Prompt 版本号
model_used VARCHAR(100) 实际模型
input_data_hash VARCHAR(64) SHA-256缓存键
sanitized_input JSONB 脱敏输入快照AES-256 加密存储90天自动清理
result_content TEXT AI 完整输出
result_metadata JSONB tokens/耗时/模型信息
status VARCHAR(20) pending / streaming / completed / failed
error_message TEXT 失败原因
+ 标准审计字段

5.3 ai_usage_logs

类型 说明
id UUID v7 主键
tenant_id UUID 租户
provider VARCHAR(50) claude / openai / local
model VARCHAR(100) 具体模型
analysis_type VARCHAR(50) 分析类型
input_tokens INT
output_tokens INT
duration_ms INT
cost_cents INT 费用(分)
is_cache_hit BOOLEAN
created_at TIMESTAMP

6. 数据脱敏与安全

6.1 脱敏规则

  • 移除: 姓名、身份证号、手机号、地址、出生日期、医生姓名
  • 泛化: 精确年龄 → 年龄段 (18-30, 30-40, 40-50, ...)
  • 保留: 科室、检验指标、诊断编码、用药记录、家族史

6.2 安全防护层

  1. 速率限制: 患者端 10次/天/人,管理端 100次/小时/租户 (Redis Token Bucket)
  2. 输入验证: schema 校验,租户隔离校验
  3. Prompt 注入防护: JSON 序列化注入,不做字符串拼接
  4. 输出过滤: 正则扫描处方药推荐/诊断结论,强制追加免责声明
  5. 审计日志: sanitized_input AES-256 加密存储90 天自动清理

6.3 缓存设计

缓存位置: Redis 缓存键: ai:cache:{tenant_id}:{analysis_type}:{input_data_hash}:{prompt_version} TTL: 7 天(可配置 ai.cache_ttl_seconds Hash 计算: 对 HealthDataProvider 返回的 DTO 做 canonical JSON 序列化后 SHA-256 失效策略: TTL 自然过期Prompt 版本切换后旧缓存键自然不同

缓存命中 (Redis GET) → 直接返回完整 result_content (非流式,秒级)
缓存未命中 + AI 可用 → 正常 SSE 流式分析 → 完成后写入 Redis
缓存未命中 + AI 不可用 + 有旧版本 → 返回旧结果 + 标注"基于历史版本"
缓存未命中 + AI 不可用 + 无历史 → 本地规则引擎50 条常见指标)

6.4 sanitized_input 数据生命周期

  • 写入时: AES-256-GCM 加密后存储为 JSONB密钥从环境变量加载
  • 查询时: 仅管理端有权限解密查看,且需 ai.analysis.manage 权限
  • 清理: 后台定时任务每日扫描,超过 sanitized_input_retention_days(默认 90 天)的记录自动清空此字段
  • 审计: 清理操作本身记录审计日志

7. API 设计

/api/v1/ai/
├── analyze/
│   ├── POST /lab-report         SSE  化验单解读
│   ├── POST /trends             SSE  健康趋势分析
│   ├── POST /checkup-plan       SSE  个性化体检方案
│   └── POST /report-summary     SSE  报告摘要生成
├── analysis/
│   ├── GET  /history            JSON 分析历史列表
│   └── GET  /:id                JSON 分析详情
├── prompts/                     (需 ai.prompt.manage)
│   ├── GET  /                   JSON Prompt 列表
│   ├── POST /                   JSON 新建 Prompt
│   ├── GET  /:id                JSON Prompt 详情
│   ├── PUT  /:id                JSON 编辑 (自动+1版本)
│   ├── POST /:id/activate       JSON 激活指定版本
│   ├── GET  /:id/versions        JSON 版本历史
│   └── POST /:id/rollback        JSON 回滚
├── usage/                       (需 ai.usage.view)
│   ├── GET  /stats              JSON 用量统计
│   ├── GET  /costs              JSON 费用明细
│   └── GET  /models             JSON 模型分布
└── admin/
    ├── GET  /providers          JSON 提供商状态
    ├── PUT  /providers/:name/config  JSON 更新配置
    └── POST /providers/:name/test    JSON 连通性测试

8. 前端设计

8.1 小程序 (Phase 1)

  • 报告详情页新增「AI 智能解读」卡片,引导点击
  • 新增 AI 解读展示页SSE 逐字流式渲染
  • 历史解读列表(点击查看过往分析)

8.2 Web 管理后台 (Phase 2)

  • AI Prompt 管理页面:列表/编辑/版本历史/diff 对比/回滚
  • AI 用量统计仪表盘:按天/模型/场景维度
  • 提供商状态与配置管理

9. 扩展路线图

Phase 1 — MVP (约 2-3 周)

  • erp-ai crate 骨架 + ErpModule 实现
  • AiProvider trait + Claude SSE 实现
  • DeIdentificationService
  • PromptManager + 3 张表 + migration
  • 4 个分析端点
  • 缓存 + 降级规则引擎
  • 小程序 AI 解读功能
  • 速率限制 + 审计日志

Phase 2 — 运营强化 (约 2 周)

  • Web Prompt 管理完整 UI
  • 版本 diff 对比 + 回滚
  • 用量统计仪表盘
  • OpenAI 提供商实现
  • 成本告警
  • 分享功能

Phase 3 — 混合管道 (约 2-3 周)

  • AsyncAnalysisPipeline 实现
  • EventBus 订阅 health 事件
  • 后台预分析队列
  • 消息中心通知集成
  • WebSocket StreamingProvider 实现
  • 本地模型对接 (Ollama/vLLM)

Phase 4 — 智能化 (持续)

  • Prompt A/B 测试
  • 分析效果评分
  • 多轮对话式解读
  • 多模态输入 (OCR)
  • 知识库 RAG

Phase 1 架构预留点

预留点 位置 做法
AnalysisPipeline trait pipeline/mod.rs RequestPipeline 唯一实现Phase 3 加 AsyncPipeline
StreamingProvider trait provider/mod.rs SSE 唯一实现Phase 3 加 WebSocket
AnalysisContext.trigger pipeline/mod.rs 枚举 Request | EventPhase 3 加 Event
HealthDataProvider trait erp-core erp-health 实现erp-ai 注入使用
事件发布 service/analysis.rs 完成后发布事件Phase 1 无消费者也发布
model_config 字段 ai_prompts 表 每模板独立配置 provider/model

10. 关键文件清单

区域 文件 操作
Cargo.toml (workspace root) [workspace].members + [workspace.dependencies] 添加 erp-ai
erp-core src/health_provider.rs 新增 HealthDataProvider trait + DTO 定义
erp-core src/events.rs 新增 ai.analysis.* 事件类型常量
erp-core src/permission.rs 新增 ai.* 权限码
erp-health src/health_provider_impl.rs 新增 HealthDataProvider trait impl
erp-ai 整个 crate 新建
erp-server src/main.rs 合并 AiModule::public/protected_routes
erp-server src/state.rs 新增 impl FromRef<AppState> for AiState
erp-server src/config.rs 新增 AiConfig 结构
erp-server config/default.toml 新增 [ai] 配置段
erp-server/migration 新增 migration 3 张表 (ai_prompts, ai_analysis_results, ai_usage_logs)
erp-server/Cargo.toml dependencies 添加 erp-ai 依赖
apps/miniprogram 报告详情页 + 解读页 新增/修改
apps/web Prompt 管理页面 Phase 2 新增

11. 验证方案

后端验证

  • cargo check — 全 workspace 编译通过
  • cargo test -p erp-ai — 单元测试覆盖 provider/sanitization/prompt/service
  • 启动后端服务,通过 curl 测试 SSE 端点流式输出
  • 通过 Swagger UI 测试 Prompt CRUD 和用量统计端点

小程序验证

  • 打开报告详情页确认「AI 解读」卡片展示
  • 点击触发 SSE 流式渲染,确认逐字输出效果
  • 测试缓存命中(第二次请求秒返回)
  • 测试降级(停止 AI 服务后请求,确认降级提示)

安全验证

  • 确认发送给 AI 的数据不包含 PII
  • 确认速率限制生效(超过 10 次返回 429
  • 确认审计日志完整记录