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

644 lines
21 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.
# 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 核心抽象
```rust
/// 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 模式)
```rust
/// 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 模式)
```rust
/// 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
```rust
// === 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 字符串模式)
```rust
// 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 实体命名模式)
```rust
/// 权限码遵循 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 模式)
```toml
# 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"
```
```rust
// 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 流式依赖
```toml
# 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
- 确认审计日志完整记录