Files
hms/crates/erp-ai/src/copilot/scoring.rs
iven a999ee0036 feat(ai): LLM 补充风险分析 + 降级策略
- scoring.rs 新增 llm_supplement 函数(调用 AI provider 生成补充洞察)
- risk_service 新增 compute_risk_with_llm 方法(LLM 失败静默降级)
- risk_handler 改用 compute_risk_with_llm
2026-05-12 22:10:05 +08:00

155 lines
4.5 KiB
Rust
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.
use crate::copilot::rules::MatchedRuleData;
/// 风险评分结果
#[derive(Debug, Clone, serde::Serialize)]
pub struct RiskScore {
pub score: i16,
pub level: String,
pub matched_rules: Vec<MatchedRule>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct MatchedRule {
pub rule_id: uuid::Uuid,
pub name: String,
pub score: i16,
pub severity: String,
pub suggestion: Option<String>,
}
/// 根据匹配规则计算风险评分
pub fn calculate_risk(matched: Vec<MatchedRuleData>) -> RiskScore {
let total: i16 = matched.iter().map(|(_, _, s, _, _)| *s).sum();
let clamped = total.clamp(0, 10);
let level = match clamped {
0..=2 => "low".to_string(),
3..=5 => "medium".to_string(),
6..=8 => "high".to_string(),
_ => "critical".to_string(),
};
let matched_rules = matched
.into_iter()
.map(|(id, name, score, severity, suggestion)| MatchedRule {
rule_id: id,
name,
score,
severity,
suggestion,
})
.collect();
RiskScore {
score: clamped,
level,
matched_rules,
}
}
/// LLM 补充分析:基于规则评分结果和患者数据,生成自然语言的补充洞察
/// 失败时返回 None降级为纯规则模式
pub async fn llm_supplement(
provider_registry: &crate::provider::registry::ProviderRegistry,
preferred_provider: &str,
risk_score: &RiskScore,
patient_data: &serde_json::Value,
) -> Option<String> {
let prompt = format!(
"基于以下患者风险评分和匹配规则,是否存在规则未覆盖的风险因素?\
风险评分:{}/10等级{}\n\
匹配规则:{}\n\
患者近期数据摘要:{}\n\
请给出简洁的补充分析100字以内如无补充请回复\"无补充\"",
risk_score.score,
risk_score.level,
risk_score
.matched_rules
.iter()
.map(|r| format!("- {}+{}分)", r.name, r.score))
.collect::<Vec<_>>()
.join("\n"),
serde_json::to_string(&serde_json::json!({
"latest_bp": patient_data.get("vital_signs"),
"latest_lab": patient_data.get("lab_reports"),
}))
.unwrap_or_default(),
);
let resolved: crate::error::AiResult<_> = provider_registry.resolve(preferred_provider).await;
let resolved = resolved.ok()?;
let req = crate::dto::GenerateRequest {
system_prompt: "你是健康管理AI助手负责对患者的风险评分进行补充分析。".into(),
user_prompt: prompt,
model: String::new(),
temperature: 0.3,
max_tokens: 256,
};
let resp: crate::error::AiResult<_> = resolved.provider().generate(req).await;
Some(resp.ok()?.content)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_calculate_risk_low() {
let matched = vec![];
let result = calculate_risk(matched);
assert_eq!(result.score, 0);
assert_eq!(result.level, "low");
}
#[test]
fn test_calculate_risk_high() {
let matched = vec![
(
uuid::Uuid::now_v7(),
"eGFR下降".into(),
3,
"warning".into(),
Some("建议调整".into()),
),
(
uuid::Uuid::now_v7(),
"血压偏高".into(),
2,
"warning".into(),
None,
),
(uuid::Uuid::now_v7(), "失约".into(), 1, "info".into(), None),
];
let result = calculate_risk(matched);
assert_eq!(result.score, 6);
assert_eq!(result.level, "high");
assert_eq!(result.matched_rules.len(), 3);
}
#[test]
fn test_calculate_risk_clamp_at_10() {
let matched = vec![
(
uuid::Uuid::now_v7(),
"危急".into(),
5,
"critical".into(),
None,
),
(
uuid::Uuid::now_v7(),
"严重".into(),
4,
"critical".into(),
None,
),
(
uuid::Uuid::now_v7(),
"异常".into(),
3,
"warning".into(),
None,
),
];
let result = calculate_risk(matched);
assert_eq!(result.score, 10);
assert_eq!(result.level, "critical");
}
}