fix(ai): AI 分析预校验 + prompt 非对话化
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

- 四个 SSE 端点增加数据完整性校验:items/sections 为空时返回 400
- 迁移 000123 更新全部 prompt system_prompt:明确非对话、输出结构化结果
- 前端用户看到的是分析结论,不再收到"请补充数据"的对话式回复
This commit is contained in:
iven
2026-05-05 19:53:04 +08:00
parent 1f91dcc5cc
commit a62332f1c4
3 changed files with 135 additions and 0 deletions

View File

@@ -42,6 +42,13 @@ where
.health_provider
.get_lab_report(ctx.tenant_id, report_id)
.await?;
if lab_dto.items.is_empty() {
return Err(erp_core::error::AppError::Validation(
"化验报告缺少检查项目数据,无法进行 AI 分析。请先录入完整的化验指标。".into(),
));
}
let sanitized_data = state.analysis.sanitizer.sanitize_lab_report(&lab_dto)?;
let prompt = state
@@ -112,6 +119,13 @@ where
.health_provider
.get_trend_analysis_data(ctx.tenant_id, patient_id, &metrics, &range)
.await?;
if trend_data.metrics.is_empty() {
return Err(erp_core::error::AppError::Validation(
"患者在选定时间段内无体征监测数据,无法进行趋势分析。".into(),
));
}
let sanitized_data = state.analysis.sanitizer.sanitize_trend_analysis(&trend_data)?;
let prompt = state
@@ -223,6 +237,13 @@ where
.health_provider
.get_full_report(ctx.tenant_id, report_id)
.await?;
if report_dto.sections.is_empty() {
return Err(erp_core::error::AppError::Validation(
"健康报告缺少内容数据,无法生成摘要。请先完善报告内容。".into(),
));
}
let sanitized_data = state
.analysis
.sanitizer

View File

@@ -122,6 +122,7 @@ mod m20260505_000119_enable_pgvector;
mod m20260505_000120_create_ai_knowledge_rules;
mod m20260505_000121_create_ai_knowledge_references;
mod m20260505_000122_create_ai_knowledge_guides;
mod m20260505_000123_update_ai_prompts_system_instruction;
pub struct Migrator;
@@ -251,6 +252,7 @@ impl MigratorTrait for Migrator {
Box::new(m20260505_000120_create_ai_knowledge_rules::Migration),
Box::new(m20260505_000121_create_ai_knowledge_references::Migration),
Box::new(m20260505_000122_create_ai_knowledge_guides::Migration),
Box::new(m20260505_000123_update_ai_prompts_system_instruction::Migration),
]
}
}

View File

@@ -0,0 +1,112 @@
//! 更新所有 AI 分析 prompt 的 system_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 sys_lab = esc(r#"你是一名专业的医学检验解读助手。这是由健康管理系统自动触发的分析任务,不是对话。
请直接输出结构化的分析结果,格式如下:
## 指标分析
逐项列出每个指标的数值、是否在正常范围、简要说明。
## 异常指标
列出所有异常指标,说明可能的原因(不作为诊断)。
## 健康建议
给出 2-3 条切实可行的后续行动建议。
## 总体评估
用一句话总结健康状况。
要求:
1. 直接输出结果,不要寒暄或询问
2. 使用通俗易懂的语言
3. 异常指标要重点标注"#);
// 趋势分析 — 保持已有的 v2 格式,只加非对话指令前缀
let sys_trend = esc(r#"你是一名健康数据分析专家。你将收到经过预处理的结构化统计摘要数据,包括线性回归趋势、异常检测结果等。
这是由健康管理系统自动触发的分析任务,不是对话。请直接输出结构化的分析结果。
要求:
1. **趋势判断** — 基于回归斜率(slope)和R²判断指标趋势上升/下降/稳定注意R²较低时趋势不确定性大
2. **异常预警** — 重点分析被检测为异常的数据点,说明偏离程度和可能的原因
3. **综合分析** — 考虑各指标间的关联性(如血压和体重、血糖和心率)
4. **临床建议** — 给出切实可行的健康管理建议,不替代医生诊断
5. **风险评级** — 对整体健康风险给出低/中/高评估并说明理由
6. **关注重点** — 用简洁的语言总结最需要关注的 2-3 个问题"#);
// 体检方案 — 加非对话指令
let sys_checkup = esc(r#"你是一名健康管理顾问。这是由健康管理系统自动触发的分析任务,不是对话。
请直接输出个性化的体检方案,格式如下:
## 推荐检查项目
按优先级列出,每项说明目的、建议频率。
## 重点关注的健康风险
基于患者情况列出 2-3 个需要特别关注的健康风险。
## 生活方式建议
给出 3-5 条切实可行的日常健康管理建议。
要求:
1. 直接输出结果,不要寒暄或询问
2. 基于患者年龄、性别、既往病史推荐
3. 按优先级排序"#);
// 报告摘要 — 加非对话指令
let sys_summary = esc(r#"你是一名医疗报告摘要撰写专家。这是由健康管理系统自动触发的分析任务,不是对话。
请直接输出结构化的报告摘要,格式如下:
## 关键发现
列出报告中的主要发现。
## 异常项目
列出所有异常项目及其严重程度。
## 结论
简明的总体结论。
## 行动建议
具体的后续步骤建议。
要求:
1. 直接输出结果,不要寒暄或询问
2. 控制在 500 字以内
3. 语言简洁专业"#);
for (name, sys) in [
("lab_report_interpretation", sys_lab),
("health_trend_analysis", sys_trend),
("personalized_checkup_plan", sys_checkup),
("report_summary_generation", sys_summary),
] {
db.execute(sea_orm::Statement::from_string(
sea_orm::DatabaseBackend::Postgres,
format!(
"UPDATE ai_prompt SET system_prompt = '{sys}', version = version + 1, updated_at = NOW() WHERE name = '{name}' AND is_active = true"
),
))
.await?;
}
Ok(())
}
async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
// prompt 更新不做回滚,保持最新版本
Ok(())
}
}
fn esc(s: &str) -> String {
s.replace('\'', "''")
}