Commit f9a457d8 by luoqi

feat(ai-script): 注入程序算好的事实 + 补字段(病历号)+ base-system 4模块铁律

补字段(PAC 有的补上,没有的优雅降级):
- prompt.ts:user prompt 加「程序已算好的事实」块 — 称呼/智能日期/主漏诊项(转换层)/
  接诊医生/复查时长/风险要点/治疗优势/年龄适应性,全 TS 算好直接给,LLM 不再判断
- 病历号(patient.medicalRecordNumber,PAC patients 表有)补进 input + orchestrator
- PAC 没有的:客服岗位角色/姓名 → 开场用"我是{诊所}的客服"兜底(不编头衔);
  上次主诉(无结构化)→ 省略;联系人姓名(无)→ 儿童称呼用 患者姓+家长
- base-system.md 重写:旧 5 段 → 4 模块铁律(医疗关怀非销售 / 只讲单漏诊项 /
  用"程序算好的事实" / 时间用【占位】/ ≤18禁拍片 / 禁费用·方案·推销 / 短句互动 / 主动约)
- safety/任务footer 同步 4 段

typecheck 0 + build 通过;script-facts 单测 25 通过。
待续:population 成人/儿童 SKILL.md 正文重写(+年龄分档对齐 ≤12/≥13)、页面4段、live 验证。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent a1cd620d
......@@ -13,6 +13,8 @@ export interface DraftPlanScriptInput {
nameMasked: string;
gender: string | null;
age: number | null;
/** 病历号(host 病历主键,如 "FY0A000922")— 话术里供客服核对身份用,可空 */
medicalRecordNumber?: string | null;
};
/** 诊所名(给 LLM 用作"我是X诊所的客服顾问",避免编造"XX口腔") */
......
import type { DraftPlanScriptInput } from './input.types';
import {
resolveAgeBranch,
resolveAgeGroup,
resolveSalutation,
smartDateDisplay,
missedFromReason,
lookupKeyPoints,
lookupReviewDuration,
} from './script-facts';
/**
* Prompt 版本管理约定:
......@@ -198,15 +207,45 @@ export function buildDraftPlanScriptPrompt(input: DraftPlanScriptInput): string
? clinicalContext.pendingTreatments.map((t) => `- ${t}`).join('\n')
: '- (暂无明确待做治疗)';
// ⭐ 程序算好的确定性事实(渐进式组合:LLM 不做年龄分支/日期格式/优先级/查表/称呼)
const now = new Date();
const branch = resolveAgeBranch(patient.age);
const salutation = resolveSalutation({ nameMasked: patient.nameMasked, gender: patient.gender, branch });
const lastVisitDate =
clinicalContext.daysSinceLastVisit != null
? new Date(now.getTime() - clinicalContext.daysSinceLastVisit * 86400_000)
: null;
const dateDisplay = smartDateDisplay(lastVisitDate, now) ?? '上次';
const topReason = [...plan.reasons].sort((a, b) => b.priorityScore - a.priorityScore)[0];
const missed = topReason ? missedFromReason(topReason) : { label: plan.primaryScenarioLabel, key: null };
const kp = lookupKeyPoints(missed.key);
const reviewDuration = lookupReviewDuration(missed.key);
const doctor = topReason?.triggerDoctor ?? clinicalContext.primaryDoctorName ?? '您的主诊医生';
const ageGroup = resolveAgeGroup(patient.age);
const ageFit = kp?.ageFit && ageGroup ? kp.ageFit[ageGroup] : null;
const mrn = patient.medicalRecordNumber ?? null;
return `# 召回任务背景(以下字段是 100% 可信的事实源,只能用这些,不能编额外的)
## ⭐ 程序已算好的事实(直接用,**不要自己改 / 不要自己推断或重新格式化**)
- 智能称呼(开场用这个):${salutation}
- 智能日期(开场"自从X检查后"用这个):${dateDisplay}
- 主漏诊项(本次**只讲这一个**,严禁提其他):${missed.label}
- 接诊/主诊医生(以此人名义体现关怀):${doctor}
- 复查时长(复查建议·检查说明 直接用原文):${reviewDuration}
- 风险要点(告知漏诊·健康提醒 从中挑 1-2 条口语化,不堆砌):
${kp ? kp.risks.map((r) => ` - ${r}`).join('\n') : ' - (按常识温和提醒,不吓唬人)'}
- 治疗优势(告知漏诊·个人化关怀 挑 1 条,用"趁现在/早一点"口吻):
${kp ? kp.advantages.map((a) => ` - ${a}`).join('\n') : ' - 趁现在早点处理会更省心'}${ageFit ? `\n- 年龄适应性(可融入关怀,禁直接报年龄):${ageFit}` : ''}
## 诊所
- 名称:${clinicName}
## 患者
- 称呼:${patient.nameMasked}
- 姓名:${patient.nameMasked}
- 性别:${patient.gender ?? '未知'}
- 年龄:${patient.age ?? '未知'}
- 年龄:${patient.age ?? '未知'}${mrn ? `\n- 病历号:${mrn}` : ''}
- 岗位角色/客服姓名:PAC 暂无 开场自报家门用"我是${clinicName}的客服",不要编岗位头衔
## 召回元数据(⚠️ 内部用,严禁念给患者)
- 主场景代号:${plan.primaryScenarioLabel}
......@@ -232,5 +271,6 @@ ${pendingLines}
${clinicalContext.ongoingChains.length > 0 ? clinicalContext.ongoingChains.map((l) => ` - ${l}`).join('\n') : ' - (无正在进行的治疗链)'}
# 任务
schema 5 字段输出 1 JSON。所有事实必须来自上面字段,system prompt "反向约束"严格遵守。`;
schema 4 (opening 开场白 / informMissed 告知漏诊项目 / reviewAdvice 复查建议 / closing 结束回访语)+ tone 输出 1 JSON
**只讲上面"主漏诊项"那一个**,严禁提其他漏诊项。称呼/日期/复查时长直接用"程序已算好的事实",不要自己改。所有事实必须来自上面字段。`;
}
......@@ -369,6 +369,7 @@ export class PlanScriptOrchestrator {
nameMasked: nameSpokenForm(patient.name, patient.gender),
gender: patient.gender,
age: patient.birthDate ? calcAge(patient.birthDate) : null,
medicalRecordNumber: patient.medicalRecordNumber ?? null,
},
// 临时:hardcoded jvs-dw 诊所字典(TODO #56 接 host 字典或新建 clinics 表)
// ⚠️ 直接吐 UUID 进 prompt 会让 LLM 编造"XX 客服中心",必须翻译成中文名
......
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