- scoring.rs 新增 llm_supplement 函数(调用 AI provider 生成补充洞察) - risk_service 新增 compute_risk_with_llm 方法(LLM 失败静默降级) - risk_handler 改用 compute_risk_with_llm
155 lines
4.5 KiB
Rust
155 lines
4.5 KiB
Rust
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");
|
||
}
|
||
}
|