Commit e7a88608 by luoqi

feat(ai/script): prompt v3 — §0 总则白名单 + (示例) 时间标记 + 上下文增强

prompt.ts (DRAFT_PLAN_SCRIPT_SYSTEM 重写):
  - 删 few-shot JSON 大段(LLM 把示例文本当模板照抄,如"工作日 19:00 后"
    伪事实就是这么漏的)
  - 加 §0 总则:白名单(诊所/患者/触发原因/画像/临床上下文)+ 自检方法,
    白名单之外具体表述视为虚构 → 失败
  - 加 §6 时间/排班规则:必含"待确认"短语;(示例) 显式标记或方向词代替具体点;
    严禁加粗具体时间("**周六上午10点**" 被读作已敲定)
  - 加 医生引用规则:followup 引诊断必须用 reason.triggerDoctor,不能拿
    全患者高频医生顶替(38 是姜医生发现,不能写李医生)
  promptVersion → draft_plan_script@2026-05-27-time-marker

schema.ts:
  close 段 describe() 明确 (示例) 标记规则 + 加粗禁令

call.ts safety rules 新增 3 条:
  - close_no_commit_phrasing(block):"已为您约好"/"约定"/"敲定" 等承诺词
  - close_no_bold_time(block):正则禁加粗具体时间词
  - close_has_tentative_phrasing(warn):未含"待确认"语义短语提示

input.types.ts + orchestrator.buildCallInput:
  reason 加 triggerDoctor + triggerDate;plan 加 goal;
  clinicalContext 加 ongoingChains + completedTreatmentCount(信任锚);
  loadPlanContext fact status filter 改 ['active','fulfilled'](漏 fulfilled 会让
  AI 看不到实际 treatment_record → primaryDoctor 偏移、pendingTreatments 漏算);
  extractPrimaryDoctor 改"doctor_id 频次 top 1"(对齐前端);
  visitFacts EMR 兜底(同 plan-aggregate);
  pendingTreatments 改从 plan.reasons 派生(SQL 权威集,旧 DX_TO_CAT 漏 K01/K03)。
parent b6147297
...@@ -55,6 +55,62 @@ const safetyRules: ReadonlyArray<SafetyRule<DraftPlanScriptOutput>> = [ ...@@ -55,6 +55,62 @@ const safetyRules: ReadonlyArray<SafetyRule<DraftPlanScriptOutput>> = [
return { pass: hasH3, message: hasH3 ? undefined : 'objection 段未按 ### A./B. 子标题切分' }; return { pass: hasH3, message: hasH3 ? undefined : 'objection 段未按 ### A./B. 子标题切分' };
}, },
}, },
{
name: 'close_no_commit_phrasing',
severity: 'block',
check(output) {
// close 段禁止"我已为您约好 X" / "已成功预约 X" / "约定 / 敲定 / 安排好" 这种确定承诺(PAC 无排班 API)
const COMMIT_PHRASES = [
'已为您约好', '已成功预约', '已为您预约', '已经为您约', '已替您预约',
'约定本', '敲定本', '安排好了', '已经预约'
];
const hit = COMMIT_PHRASES.filter((p) => output.close.includes(p));
return {
pass: hit.length === 0,
message: hit.length > 0 ? `close 段承诺式表述(无排班 API,不能定): ${hit.join(',')}` : undefined,
};
},
},
{
name: 'close_no_bold_time',
severity: 'block',
check(output) {
// close 段禁止 **本周X上午X点** 这种加粗具体时间 — 加粗 = "已确认重点",误导患者
// 正确做法:具体时间紧跟 (示例) 后缀,或用"周X上午这个方向"模糊词
// 匹配:**...含数字时间词的字符串...**
const boldTimeRegex = /\*\*[^*\n]*(?:[一二三四五六日天]|\d+\s*(?:点|:|:))[^*\n]*\*\*/;
const m = output.close.match(boldTimeRegex);
return {
pass: !m,
message: m ? `close 段加粗了具体时间"${m[0]}" — 应去加粗 + 加 (示例) 后缀或用方向词` : undefined,
};
},
},
{
name: 'close_has_tentative_phrasing',
severity: 'warn', // warn 不阻断,只记日志(soft 引导,严格的话改 block)
check(output) {
// close 段应含"待确认"语义短语,避免患者以为时间真定了
const TENTATIVE_PHRASES = [
'以诊所排班为准',
'排班为准',
'稍后跟前台确认',
'跟前台确认',
'稍后跟诊所确认',
'稍后短信确认',
'排班确认后告知',
'排班确认后短信',
'稍后短信通知您实际',
'具体时段以',
'具体时间以',
];
const hasTentative = TENTATIVE_PHRASES.some((p) => output.close.includes(p));
return {
pass: hasTentative,
message: hasTentative ? undefined : 'close 段未含"待确认/以排班为准"等弱化短语,可能误导患者以为时间已定',
};
},
},
]; ];
/** /**
......
...@@ -23,11 +23,20 @@ export interface DraftPlanScriptInput { ...@@ -23,11 +23,20 @@ export interface DraftPlanScriptInput {
/** 主场景 label(从 scenario 枚举翻译,如"治疗后复诊召回"/"漏治-缺失牙"等) */ /** 主场景 label(从 scenario 枚举翻译,如"治疗后复诊召回"/"漏治-缺失牙"等) */
primaryScenarioLabel: string; primaryScenarioLabel: string;
priorityScore: number; priorityScore: number;
/** ⭐ 本次召回的明确目的(plan.goal 原文,如"邀约做牙周基础治疗(SRP/翻瓣),控制炎症发展")
* 让 LLM followup 段对齐该目标,不再自己脑补"我们想约您来评估" */
goal: string | null;
/** 触发原因摘要(最多 3 条) */ /** 触发原因摘要(最多 3 条) */
reasons: Array<{ reasons: Array<{
scenarioLabel: string; scenarioLabel: string;
reason: string; reason: string;
priorityScore: number; priorityScore: number;
/** 触发该诊断/建议的医生(LLM 在 followup 段必须引用此人,不要用 primaryDoctorName)
* 来源:reason.evidence.factIds[0] → patient_facts.content.doctor_name
* null = 该 fact 无医生记录 → LLM fallback "您的主诊医生" 泛指 */
triggerDoctor: string | null;
/** 触发诊断/建议的日期(YYYY-MM-DD),给 LLM 在话术里用,如"上次姜医生 X 月 X 日给您检查时...") */
triggerDate: string | null;
}>; }>;
}; };
...@@ -45,6 +54,11 @@ export interface DraftPlanScriptInput { ...@@ -45,6 +54,11 @@ export interface DraftPlanScriptInput {
treatmentChainSummary: string | null; // 治疗链当前阶段一句话 treatmentChainSummary: string | null; // 治疗链当前阶段一句话
/** 主诊医生名(从最近 treatment/diagnosis fact 抽);LLM 必须用此名,不可编造 */ /** 主诊医生名(从最近 treatment/diagnosis fact 抽);LLM 必须用此名,不可编造 */
primaryDoctorName: string | null; primaryDoctorName: string | null;
/** ⭐ 正在进行的治疗链摘要(每条一句:"牙周治疗在管 · 上次龈上洁治 · 吴医生 · 2024.04.27")
* LLM 用于:① 不重复邀约已在管的治疗 ② 引用历史治疗显出"诊所记得 ta" */
ongoingChains: string[];
/** ⭐ 已做治疗总次数(信任锚);LLM 用于:老客可以更家常,新客需自报家门更详细 */
completedTreatmentCount: number;
}; };
} }
......
...@@ -67,9 +67,17 @@ export const DraftPlanScriptSchema = z.object({ ...@@ -67,9 +67,17 @@ export const DraftPlanScriptSchema = z.object({
.describe( .describe(
[ [
'【结束·信息确认段 markdown,必须严格按以下 2 部分格式】', '【结束·信息确认段 markdown,必须严格按以下 2 部分格式】',
'第 1 部分:`> "确认话术..."` blockquote — 复述敲定的安排:具体时间(周X晚上X点)+ 医生名 + 提醒方式(短信/企微)+ 礼貌结尾', '第 1 部分:`> "确认话术..."` blockquote — 复述敲定的安排:示例时间 + 医生名 + 提醒方式(短信/企微)+ 礼貌结尾',
' ⭐ 时间措辞要求(call 时段 PAC 无排班 API,LLM 给的具体时间只是 example,实际以诊所排班为准):',
' - 必须含"待确认"语义短语之一:"具体时段以诊所排班为准" / "稍后跟前台确认后短信通知您实际时间" / "实际时间稍后短信确认" / "我先按 X 登记,排班确认后告知"',
' - 出现具体时间时(如"周六上午10点"),**两种方式标记为示例,任选一**:',
' 方式 A: 不加粗,紧跟 `(示例)` 后缀 → "周六上午10点(示例)"',
' 方式 B: 用方向词代替具体点 → "周六上午这个方向"',
' - ⚠️ 严禁 `**周六上午10点**` 这种加粗具体时间 — 加粗读作"已确认重点",会让患者误以为时间真定了',
' - 严禁说"我已为您约好 X" / "已成功预约 X" / "约定 X" / "敲定 X" 这种确定承诺',
' - close 段只给 1 个时间示例 + 弱化短语,不要罗列多个备选(close 是收尾,不是商量)',
'第 2 部分:空行后 `**回写要点**` 标题 + 2-4 条 `- xxx → 提交结果选「xxx」` bullet — 列不同通话结果对应的客服回写动作', '第 2 部分:空行后 `**回写要点**` 标题 + 2-4 条 `- xxx → 提交结果选「xxx」` bullet — 列不同通话结果对应的客服回写动作',
'禁止:省略具体时间敲定、省略回写要点', '禁止:省略具体时间敲定、省略回写要点、加粗具体时间、用承诺式"已为您约好/约定/敲定"',
].join('\n'), ].join('\n'),
), ),
}); });
......
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