Commit 7eb8dbed by luoqi

feat(plan): 三源召回信号统一标定 — 置信度/优先级/打分一致

诊断 1.0 / 建议 0.8 / 影像AI 0.5 三源置信度直用为打分因子(下限0.5),
取代旧 0.75/0.9/1.0 分档;triggers 按 诊断>建议>影像 排序定 lead,
reason 文案由 lead 来源驱动(建议为首不再套子场景诊断词)。
priority-hover 提示同步为「诊断1.0/建议0.8/影像0.5」。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent c0d27880
...@@ -92,16 +92,15 @@ export interface PriorityBreakdown { ...@@ -92,16 +92,15 @@ export interface PriorityBreakdown {
intentBehavior: number; // 意愿·主诉行为 intentBehavior: number; // 意愿·主诉行为
trustBase: number; // 意愿·信任基础 trustBase: number; // 意愿·信任基础
freshness: number; // 诊断新鲜度因子 0.4-1.0(老诊断衰减) freshness: number; // 诊断新鲜度因子 0.4-1.0(老诊断衰减)
confidenceFactor: number; // 置信度因子 0.75-1.0(影像AI/建议 降权) confidenceFactor: number; // 置信度因子 = 来源置信度(诊断1.0/建议0.8/影像0.5)
base: number; // 三维加权(未乘因子) base: number; // 三维加权(未乘因子)
raw: number; // 综合 0-10(× 新鲜度 × 置信度后) raw: number; // 综合 0-10(× 新鲜度 × 置信度后)
} }
/// 置信度因子:≥0.9→1.0(医生诊断)/ ≥0.7→0.9(影像AI/建议)/ <0.7→0.75。 /// 置信度因子 = 来源置信度直用(诊断1.0 / 建议0.8 / 影像AI0.5),下限 0.5 防过罚。
/// v3.1:三源标定后因子与来源置信度一致,显示与打分同一套值(见 sourceConfidence)。
export function computeConfidenceFactor(confidence: number): number { export function computeConfidenceFactor(confidence: number): number {
if (confidence >= 0.9) return 1.0; return Math.max(0.5, Math.min(1, confidence));
if (confidence >= 0.7) return 0.9;
return 0.75;
} }
/** /**
......
...@@ -445,12 +445,19 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin { ...@@ -445,12 +445,19 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
// 触发信号类型(原 enum,不语义化;前端用 triggerTypeLabelZh 翻译) // 触发信号类型(原 enum,不语义化;前端用 triggerTypeLabelZh 翻译)
// lead 单一类型 — 想精准列出所有 sig 的话需要把 triggers[] 改成 cluster 全量,目前 lead 代表 // lead 单一类型 — 想精准列出所有 sig 的话需要把 triggers[] 改成 cluster 全量,目前 lead 代表
const triggerType = triggerTypeOf(r); const triggerType = triggerTypeOf(r);
// 标签分流(同前端 ReasonLine):lead = 优先级最高来源(诊断>建议>影像)。
// lead=建议(即无诊断,仅医生建议)→ 用建议名(如"建议种植")替代子场景诊断词"缺失牙未启动修复"。
const leadTrig = r.cluster_triggers?.[0] ?? { type: triggerType, code: r.signal_code };
const isRecLead = leadTrig.type === 'recommendation';
const catsStr = expectedCats.map((c) => treatmentCategoryNameZhForTeeth(c, r.tooth)).join(' / ');
hits.push({ hits.push({
patientId, patientId,
patientExternalId: r.patient_external_id, patientExternalId: r.patient_external_id,
// reason 文本兜底(AI prompt / 调试用,前端不依赖此字段) // reason 文本兜底(AI prompt / 调试用,前端不依赖此字段)
// 乳牙 restorative 只显「充填」(去嵌体),跟前端 ReasonLine/目标 tag 同口径(treatmentCategoryNameZhForTeeth) // 乳牙 restorative 只显「充填」(去嵌体),跟前端 ReasonLine/目标 tag 同口径(treatmentCategoryNameZhForTeeth)
reason: `${cfg.label}${toothStr}${diagnosisCodeNameZh(r.signal_code)}${sourceStr} ${r.days_since} 天前,未启动${expectedCats.map((c) => treatmentCategoryNameZhForTeeth(c, r.tooth)).join(' / ')}`, reason: isRecLead
? `${diagnosisCodeNameZh(leadTrig.code)}${toothStr}${sourceStr} ${r.days_since} 天前,未启动${catsStr}`
: `${cfg.label}${toothStr}${diagnosisCodeNameZh(r.signal_code)}${sourceStr} ${r.days_since} 天前,未启动${catsStr}`,
priorityScore: score, priorityScore: score,
goal: cfg.goal, goal: cfg.goal,
recommendedRole: 'staff', recommendedRole: 'staff',
...@@ -565,18 +572,17 @@ interface HitRow { ...@@ -565,18 +572,17 @@ interface HitRow {
cluster_fact_ids?: string[]; // cluster 内所有 sig 的 fact_id(给 evidence.factIds) cluster_fact_ids?: string[]; // cluster 内所有 sig 的 fact_id(给 evidence.factIds)
cluster_has_diagnosis?: boolean; // cluster 含至少 1 个 diagnosis_record cluster_has_diagnosis?: boolean; // cluster 含至少 1 个 diagnosis_record
cluster_has_recommendation?: boolean; // cluster 含至少 1 个 recommendation_record cluster_has_recommendation?: boolean; // cluster 含至少 1 个 recommendation_record
cluster_confidence?: number; // cluster 最优来源置信度(医生1.0/影像0.85/建议0.8) cluster_confidence?: number; // cluster 最优来源置信度(诊断1.0/建议0.8/影像0.5,取 max)
cluster_triggers?: Array<{ type: string; code: string }>; // cluster 内 unique (type, code),给 signals.triggers cluster_triggers?: Array<{ type: string; code: string }>; // cluster 内 unique (type, code),给 signals.triggers
} }
/// 单 sig 来源置信度:医生诊断(std_code/name_map)1.0 / 影像AI 0.85 / 建议 0.8 / 未知诊断 0.9。 /// 单 sig 来源置信度(v3.1 三源标定):诊断 1.0 / 医生建议 0.8 / 影像AI 0.5。
/// v3.0 置信度梯度(病历100% > 影像70-90% > 建议)。cluster 取 max(最优来源胜出)。 /// 优先级即置信度高低:**诊断 > 建议 > 影像AI**。cluster 取 max → "有诊断以诊断为准"。
/// 影像AI = code_source='image_ai' 的诊断(AI 读片,非医生确诊 → 降到 0.5)。
function sourceConfidence(row: HitRow): number { function sourceConfidence(row: HitRow): number {
if (row.signal_type === 'recommendation_record') return 0.8; if (row.signal_type === 'recommendation_record') return 0.8;
const src = row.code_source ?? ''; if (row.code_source === 'image_ai') return 0.5;
if (src === 'std_code' || src === 'name_map') return 1.0; return 1.0; // 医生诊断(std_code / name_map / 未标来源)
if (src === 'image_ai') return 0.85;
return 0.9;
} }
/// 牙位字符串 → set,**直接委托共享 toothSet**(单一真理源)。 /// 牙位字符串 → set,**直接委托共享 toothSet**(单一真理源)。
...@@ -689,7 +695,10 @@ function uniqueTriggers(rows: HitRow[]): Array<{ type: string; code: string }> { ...@@ -689,7 +695,10 @@ function uniqueTriggers(rows: HitRow[]): Array<{ type: string; code: string }> {
out.push({ type, code: r.signal_code }); out.push({ type, code: r.signal_code });
} }
} }
// 诊断/影像AI(带 K 码)排前 → 前端 triggers[0] 兜底展示码;医生建议(rec 码)排后 // v3.1 优先级排序:**诊断(0) > 医生建议(1) > 影像AI(2)**。
out.sort((a, b) => (a.type === 'recommendation' ? 1 : 0) - (b.type === 'recommendation' ? 1 : 0)); // triggers[0] = 最高优先来源 → 驱动展示标签("有诊断以诊断为准,以此类推")。
// (原口径把影像AI 和诊断并列排前;现按置信度梯度把影像AI 降到最后。)
const order = (t: string) => (t === 'recommendation' ? 1 : t === 'image_finding' ? 2 : 0);
out.sort((a, b) => order(a.type) - order(b.type));
return out; return out;
} }
...@@ -21,7 +21,7 @@ export interface PriorityBreakdown { ...@@ -21,7 +21,7 @@ export interface PriorityBreakdown {
intentBehavior?: number; // 意愿·主诉行为 intentBehavior?: number; // 意愿·主诉行为
trustBase?: number; // 意愿·信任基础 trustBase?: number; // 意愿·信任基础
freshness?: number; // 新鲜度因子 0.4-1 freshness?: number; // 新鲜度因子 0.4-1
confidenceFactor?: number; // 置信度因子 0.75-1 confidenceFactor?: number; // 置信度因子(诊断1.0/建议0.8/影像0.5)
base?: number; // 三维加权(未乘因子) base?: number; // 三维加权(未乘因子)
raw?: number; // 综合 ×因子后 raw?: number; // 综合 ×因子后
} }
...@@ -99,7 +99,7 @@ function PriorityBreakdownTable({ ...@@ -99,7 +99,7 @@ function PriorityBreakdownTable({
<Row <Row
label="× 置信度" label="× 置信度"
value={conf.toFixed(2)} value={conf.toFixed(2)}
hint="医生 1.0 / 影像·建议 0.9" hint="诊断 1.0 / 建议 0.8 / 影像 0.5"
tone={conf < 1 ? 'amber' : undefined} tone={conf < 1 ? 'amber' : undefined}
/> />
<Subtotal label="= 总分" value={disp} bold /> <Subtotal label="= 总分" value={disp} bold />
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment