Commit d2a02f0f by luoqi

feat(ai-script): 单一聚焦的上下文也项目相关 — 日期/主诉用"那次诊断"非泛泛最近

聚焦一个漏诊项时,喂 LLM 的上下文应针对该项目,而非泛泛"最近一次就诊":
- orchestrator resolveReasonTrigger 扩展:除触发医生/日期,再取该诊断**那次接诊的主诉**
  (evidence.factIds[0]→诊断 fact→source_encounter→该 encounter 的 emr.illness_desc)
- reason 加 triggerChiefComplaint;prompt 注入:
  · 智能日期 = top.triggerDate(该漏诊项诊断日期)优先,非最近就诊
  · "那次就诊主诉" = top.triggerChiefComplaint(那次为什么来),非最近一次主诉
  · 医生 = top.triggerDoctor 优先
- 临床上下文"最近一次接触"降级为仅参考;fallback 同步用项目相关日期/医生

避免"缺牙2025-12诊断却说自从2026-03洁牙后"的错位。typecheck 0 + build + 25 测试通过。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent d09f4339
...@@ -90,16 +90,16 @@ function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput { ...@@ -90,16 +90,16 @@ function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
const { patient, clinicName, plan, clinicalContext } = input; const { patient, clinicName, plan, clinicalContext } = input;
const branch = resolveAgeBranch(patient.age); const branch = resolveAgeBranch(patient.age);
const salutation = resolveSalutation({ nameMasked: patient.nameMasked, gender: patient.gender, branch }); const salutation = resolveSalutation({ nameMasked: patient.nameMasked, gender: patient.gender, branch });
const doctor = clinicalContext.primaryDoctorName ?? '您的主诊医生';
const dateDisplay =
smartDateDisplay(
clinicalContext.daysSinceLastVisit != null
? new Date(Date.now() - clinicalContext.daysSinceLastVisit * 86400_000)
: null,
new Date(),
) ?? '上次';
// 漏诊项 = PAC 应治未治 reason(取 priorityScore 最高的一条)→ 转换层归一 // 漏诊项 = PAC 应治未治 reason(取 priorityScore 最高的一条)→ 转换层归一
const topReason = [...(plan.reasons ?? [])].sort((a, b) => b.priorityScore - a.priorityScore)[0]; const topReason = [...(plan.reasons ?? [])].sort((a, b) => b.priorityScore - a.priorityScore)[0];
const doctor = topReason?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? '您的主诊医生';
// 日期优先取"那次诊断"的(项目相关),否则退回最近一次就诊
const dateBasis = topReason?.triggerDate
? new Date(topReason.triggerDate)
: clinicalContext.daysSinceLastVisit != null
? new Date(Date.now() - clinicalContext.daysSinceLastVisit * 86400_000)
: null;
const dateDisplay = smartDateDisplay(dateBasis, new Date()) ?? '上次';
const missed = topReason const missed = topReason
? missedFromReason(topReason) ? missedFromReason(topReason)
: { label: plan.primaryScenarioLabel, key: null }; : { label: plan.primaryScenarioLabel, key: null };
......
...@@ -45,6 +45,9 @@ export interface DraftPlanScriptInput { ...@@ -45,6 +45,9 @@ export interface DraftPlanScriptInput {
triggerDoctor: string | null; triggerDoctor: string | null;
/** 触发诊断/建议的日期(YYYY-MM-DD),给 LLM 在话术里用,如"上次姜医生 X 月 X 日给您检查时...") */ /** 触发诊断/建议的日期(YYYY-MM-DD),给 LLM 在话术里用,如"上次姜医生 X 月 X 日给您检查时...") */
triggerDate: string | null; triggerDate: string | null;
/** ⭐ 项目相关:该诊断**那次接诊**的主诉(emr illness_desc 原文)。
* 单一聚焦时优于"最近一次主诉"—— 缺牙是哪次来发现的、那次为什么来,更贴合本漏诊项。可空 */
triggerChiefComplaint?: string | null;
}>; }>;
}; };
......
...@@ -212,12 +212,15 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string ...@@ -212,12 +212,15 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string
clinicalContext.daysSinceLastVisit != null clinicalContext.daysSinceLastVisit != null
? new Date(now.getTime() - clinicalContext.daysSinceLastVisit * 86400_000) ? new Date(now.getTime() - clinicalContext.daysSinceLastVisit * 86400_000)
: null; : null;
const dateDisplay = smartDateDisplay(lastVisitDate, now) ?? '上次'; // ⭐ 项目相关:日期/主诉/医生优先取"该漏诊项**那次诊断**"的,而非泛泛"最近一次就诊"
const topReason = [...plan.reasons].sort((a, b) => b.priorityScore - a.priorityScore)[0]; // (top = priorityScore 最高那条 reason,见上方单一聚焦块)
const missed = topReason ? missedFromReason(topReason) : { label: plan.primaryScenarioLabel, key: null }; const projectDate = top?.triggerDate ? new Date(top.triggerDate) : lastVisitDate;
const dateDisplay = smartDateDisplay(projectDate, now) ?? '上次';
const chiefComplaint = top?.triggerChiefComplaint ?? clinicalContext.lastChiefComplaint ?? null;
const missed = top ? missedFromReason(top) : { label: plan.primaryScenarioLabel, key: null };
const kp = lookupKeyPoints(missed.key); const kp = lookupKeyPoints(missed.key);
const reviewDuration = lookupReviewDuration(missed.key); const reviewDuration = lookupReviewDuration(missed.key);
const doctor = topReason?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? '您的主诊医生'; const doctor = top?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? '您的主诊医生';
const ageGroup = resolveAgeGroup(patient.age); const ageGroup = resolveAgeGroup(patient.age);
const ageFit = kp?.ageFit && ageGroup ? kp.ageFit[ageGroup] : null; const ageFit = kp?.ageFit && ageGroup ? kp.ageFit[ageGroup] : null;
const mrn = patient.medicalRecordNumber ?? null; const mrn = patient.medicalRecordNumber ?? null;
...@@ -226,7 +229,8 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string ...@@ -226,7 +229,8 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string
## ⭐ 程序已算好的事实(直接用,**不要自己改 / 不要自己推断或重新格式化**) ## ⭐ 程序已算好的事实(直接用,**不要自己改 / 不要自己推断或重新格式化**)
- 智能称呼(开场用这个):${salutation} - 智能称呼(开场用这个):${salutation}
- 智能日期(开场"自从X检查后"用这个):${dateDisplay} - 智能日期(开场"自从X检查后"用这个 — 是**该漏诊项那次诊断**的日期,非泛泛最近):${dateDisplay}
- 那次就诊主诉(该漏诊项那次来为什么看,可作开场关怀上下文,别照念医学词):${chiefComplaint ?? '无记录'}
- 主漏诊项(本次**只讲这一个**,严禁提其他):${missed.label} - 主漏诊项(本次**只讲这一个**,严禁提其他):${missed.label}
- 接诊/主诊医生(以此人名义体现关怀):${doctor} - 接诊/主诊医生(以此人名义体现关怀):${doctor}
- 复查时长(复查建议·检查说明 直接用原文):${reviewDuration} - 复查时长(复查建议·检查说明 直接用原文):${reviewDuration}
...@@ -259,8 +263,7 @@ ${personaLines} ...@@ -259,8 +263,7 @@ ${personaLines}
## 临床上下文 ## 临床上下文
- 距上次到店:${clinicalContext.daysSinceLastVisit ?? '未知'} - 距上次到店:${clinicalContext.daysSinceLastVisit ?? '未知'}
- 上次到店:${clinicalContext.lastVisitSummary ?? '无记录'} - 上次到店:${clinicalContext.lastVisitSummary ?? '无记录'}(最近一次接触,仅参考;本次聚焦的是上面"那次诊断"那次)
- 上次就诊主诉(患者原话,可作开场寒暄/关怀的上下文,别照念医学词):${clinicalContext.lastChiefComplaint ?? '无记录'}
- 该患者长期主诊医生:${clinicalContext.primaryDoctorName ?? '(未知)'} - 该患者长期主诊医生:${clinicalContext.primaryDoctorName ?? '(未知)'}
- 历史已做治疗:${clinicalContext.completedTreatmentCount} - 历史已做治疗:${clinicalContext.completedTreatmentCount}
- 正在进行的治疗链(已在管,**不要再次邀约**这些类目;可作为"诊所记得 ta"的引用素材): - 正在进行的治疗链(已在管,**不要再次邀约**这些类目;可作为"诊所记得 ta"的引用素材):
......
...@@ -355,18 +355,32 @@ export class PlanScriptOrchestrator { ...@@ -355,18 +355,32 @@ export class PlanScriptOrchestrator {
// evidence.factIds[0] = 主触发 fact;关联 patient_facts.content.doctor_name + occurredAt // evidence.factIds[0] = 主触发 fact;关联 patient_facts.content.doctor_name + occurredAt
// LLM 在 followup 段必须引用触发医生(姜医生发现智齿)而非全患者高频医生(李医生) // LLM 在 followup 段必须引用触发医生(姜医生发现智齿)而非全患者高频医生(李医生)
const factById = new Map(facts.map((f) => [f.id, f])); const factById = new Map(facts.map((f) => [f.id, f]));
// encounter_external_id → 该次 EMR(取主诉 illness_desc 用)
const emrByEncounter = new Map<string, FactRow>();
for (const f of facts) {
if (f.type !== 'emr_record') continue;
const enc = (f.content as Record<string, unknown> | null)?.encounter_external_id as string | undefined;
if (enc && !emrByEncounter.has(enc)) emrByEncounter.set(enc, f);
}
// ⭐ 单一聚焦:trigger 信息要"项目相关"——这个漏诊项**那次诊断**的医生/日期/主诉,
// 不是泛泛的最近一次就诊(否则"缺牙2025-12诊断"却说"自从2026-03洁牙后"就错位)
const resolveReasonTrigger = (r: PlanWithReasons['reasons'][number]) => { const resolveReasonTrigger = (r: PlanWithReasons['reasons'][number]) => {
const evidence = (r.evidence ?? {}) as { factIds?: string[] }; const evidence = (r.evidence ?? {}) as { factIds?: string[] };
const leadFactId = evidence.factIds?.[0]; const leadFactId = evidence.factIds?.[0];
if (!leadFactId) return { doctor: null, date: null }; if (!leadFactId) return { doctor: null, date: null, chiefComplaint: null };
const lead = factById.get(leadFactId); const lead = factById.get(leadFactId);
if (!lead) return { doctor: null, date: null }; if (!lead) return { doctor: null, date: null, chiefComplaint: null };
const c = lead.content as Record<string, unknown> | null; const c = lead.content as Record<string, unknown> | null;
const doctor = (c?.doctor_name as string | undefined)?.trim() || null; const doctor = (c?.doctor_name as string | undefined)?.trim() || null;
const date = lead.occurredAt const date = lead.occurredAt ? lead.occurredAt.toISOString().slice(0, 10) : null;
? lead.occurredAt.toISOString().slice(0, 10) // 该诊断所在接诊的主诉(项目相关)
: null; const encId =
return { doctor, date }; ((c?.source_encounter_external_id ?? c?.encounter_external_id) as string | undefined) || null;
const emr = encId ? emrByEncounter.get(encId) : undefined;
const chiefComplaint =
((emr?.content as Record<string, unknown> | undefined)?.illness_desc as string | undefined)?.trim() ||
null;
return { doctor, date, chiefComplaint };
}; };
return { return {
...@@ -401,6 +415,7 @@ export class PlanScriptOrchestrator { ...@@ -401,6 +415,7 @@ export class PlanScriptOrchestrator {
priorityScore: r.priorityScore, priorityScore: r.priorityScore,
triggerDoctor: trig.doctor, triggerDoctor: trig.doctor,
triggerDate: trig.date, triggerDate: trig.date,
triggerChiefComplaint: trig.chiefComplaint,
}; };
}), }),
}, },
......
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