diff --git a/crates/erp-ai/src/handler/mod.rs b/crates/erp-ai/src/handler/mod.rs index 00d3597..7f55b59 100644 --- a/crates/erp-ai/src/handler/mod.rs +++ b/crates/erp-ai/src/handler/mod.rs @@ -104,11 +104,11 @@ where end: chrono::Utc::now(), }; - let vital_dtos = state + let trend_data = state .health_provider - .get_vital_signs(ctx.tenant_id, patient_id, &metrics, &range) + .get_trend_analysis_data(ctx.tenant_id, patient_id, &metrics, &range) .await?; - let sanitized_data = state.analysis.sanitizer.sanitize_vital_signs(&vital_dtos)?; + let sanitized_data = state.analysis.sanitizer.sanitize_trend_analysis(&trend_data)?; let prompt = state .prompt diff --git a/crates/erp-ai/src/sanitization/mod.rs b/crates/erp-ai/src/sanitization/mod.rs index afb4c3f..e355f69 100644 --- a/crates/erp-ai/src/sanitization/mod.rs +++ b/crates/erp-ai/src/sanitization/mod.rs @@ -1,5 +1,5 @@ use erp_core::health_provider::{ - HealthReportDto, LabReportDto, PatientSummaryDto, VitalSignDto, + HealthReportDto, LabReportDto, PatientSummaryDto, TrendAnalysisDto, VitalSignDto, }; use serde_json::Value; @@ -43,6 +43,13 @@ impl SanitizationService { Ok(sanitized) } + pub fn sanitize_trend_analysis(&self, data: &TrendAnalysisDto) -> AiResult { + let sanitized = serde_json::to_value(data) + .map_err(|e| AiError::SanitizationError(format!("序列化失败: {e}")))?; + self.verify_no_pii(&sanitized)?; + Ok(sanitized) + } + /// 二次验证: 确保没有意外泄漏的 PII fn verify_no_pii(&self, value: &Value) -> AiResult<()> { let pii_keys = [ diff --git a/crates/erp-server/migration/src/lib.rs b/crates/erp-server/migration/src/lib.rs index 8ed0077..8dc102c 100644 --- a/crates/erp-server/migration/src/lib.rs +++ b/crates/erp-server/migration/src/lib.rs @@ -92,6 +92,7 @@ mod m20260428_000089_blind_indexes; mod m20260428_000090_critical_alerts; mod m20260428_000091_dead_letter_events; mod m20260429_000092_device_readings_metric; +mod m20260429_000093_trend_analysis_prompt_v2; pub struct Migrator; @@ -191,6 +192,7 @@ impl MigratorTrait for Migrator { Box::new(m20260428_000090_critical_alerts::Migration), Box::new(m20260428_000091_dead_letter_events::Migration), Box::new(m20260429_000092_device_readings_metric::Migration), + Box::new(m20260429_000093_trend_analysis_prompt_v2::Migration), ] } } diff --git a/crates/erp-server/migration/src/m20260429_000093_trend_analysis_prompt_v2.rs b/crates/erp-server/migration/src/m20260429_000093_trend_analysis_prompt_v2.rs new file mode 100644 index 0000000..17a7314 --- /dev/null +++ b/crates/erp-server/migration/src/m20260429_000093_trend_analysis_prompt_v2.rs @@ -0,0 +1,94 @@ +//! 更新 health_trend_analysis prompt 模板为结构化统计摘要版本 + +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let db = manager.get_connection(); + + // 更新系统提示词 — 强调使用统计摘要而非原始数据 + let system_prompt = r#"你是一名健康数据分析专家。你将收到经过预处理的结构化统计摘要数据,包括线性回归趋势、异常检测结果等。 + +要求: +1. **趋势判断** — 基于回归斜率(slope)和R²判断指标趋势(上升/下降/稳定),注意R²较低时趋势不确定性大 +2. **异常预警** — 重点分析被检测为异常的数据点,说明偏离程度和可能的原因 +3. **综合分析** — 考虑各指标间的关联性(如血压和体重、血糖和心率) +4. **临床建议** — 给出切实可行的健康管理建议,不替代医生诊断 +5. **风险评级** — 对整体健康风险给出低/中/高评估并说明理由 +6. **关注重点** — 用简洁的语言总结最需要关注的 2-3 个问题"#; + + // 更新用户提示词模板 — 使用结构化统计字段 + let user_template = r#"请分析以下患者的健康趋势数据: + +分析周期:{{period_start}} 至 {{period_end}} + +## 各指标趋势分析 + +{{#each metrics}} +### {{metric}}({{unit}}) +- 数据点数:{{data_point_count}} +{{#if regression}} +- **趋势方向**:{{regression.direction}}(斜率:{{regression.slope}},R²:{{regression.r_squared}}) +- **日变化量**:{{regression.daily_change}} {{unit}}/天 +- **周期变化**:{{regression.period_change}} {{unit}}({{regression.daily_change}} × 天数) +{{else}} +- 数据不足,无法计算回归趋势 +{{/if}} +{{#if anomalies.length}} +- **异常数据点**: +{{#each anomalies}} + - {{date}}:值 {{value}} {{../unit}}(均值 {{mean}},偏离 {{deviation}} 个标准差) +{{/each}} +{{else}} +- 未检测到显著异常 +{{/if}} + +{{/each}} + +请基于以上统计摘要,给出详细的趋势分析报告。"#; + + let esc_sys = esc(system_prompt); + let esc_tpl = esc(user_template); + + // 更新已有的 health_trend_analysis prompt(version 升级到 2) + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + format!( + "UPDATE ai_prompt SET system_prompt = '{esc_sys}', user_prompt_template = '{esc_tpl}', version = version + 1, updated_at = NOW() WHERE name = 'health_trend_analysis' AND is_active = true" + ), + )) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + // 回滚到原始 prompt(保持与 000082 seed 一致) + let db = manager.get_connection(); + + let system_prompt = "你是一名健康数据分析专家。请根据提供的生命体征趋势数据,分析患者的健康变化趋势。\n\n要求:\n1. 识别数据中的关键趋势\n2. 对异常趋势提出预警\n3. 结合各指标间的关联性进行综合分析\n4. 给出健康管理建议"; + + let user_template = "以下是患者的生命体征趋势数据:\n\n患者年龄段:{{age_group}},性别:{{sex}}\n\n监测指标:\n{{#each metrics}}\n### {{name}}({{unit}})\n数据点:{{#each values}}{{this.[0]}}: {{this.[1]}}{{#unless @last}}, {{/unless}}{{/each}}\n{{/each}}\n\n请分析以上健康趋势数据。"; + + let esc_sys = esc(system_prompt); + let esc_tpl = esc(user_template); + + db.execute(sea_orm::Statement::from_string( + sea_orm::DatabaseBackend::Postgres, + format!( + "UPDATE ai_prompt SET system_prompt = '{esc_sys}', user_prompt_template = '{esc_tpl}', version = version + 1, updated_at = NOW() WHERE name = 'health_trend_analysis' AND is_active = true" + ), + )) + .await?; + + Ok(()) + } +} + +fn esc(s: &str) -> String { + s.replace('\'', "''") +}