Commit a19bbf3e by luoqi

fix(ai-script): 话术接入真实画像特征(修死 key)+ 收口 time/special/敏感/禁忌

#1 话术对画像全暗的 bug:fact-block 旧版只 filter key==='value'||'recall_risk'(早被 rfm 取代删除)
  → 深度档话术拿不到任何 persona 信号。
- 新增 buildPersonaGuide:从 16 特征里按用途挑 + 排序渲染指引块:
  注意(禁忌/治疗敏感/特别关注)→ 定语气(rfm/生命周期)→ 切入点(治疗史/权益/折扣/转介/家庭/时间偏好)。
- orchestrator 不再 slice(0,5) 任意截断,全量传给 buildPersonaGuide 按白名单挑。

#3 特征接入(原 recommendedAt/Channel 目标已 moot:前端移除了推荐时间展示、免打扰已被池 SQL 硬排除)
  → 改由更合适的通道收口:time_preference/special_attention 已进①画像标签卡(展示)②话术指引(本次)
  ③ summary(orchestrator 早已全量传)。不再臆造 moot 的 plan 字段。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent 7f459b48
...@@ -94,6 +94,53 @@ ${others.length ? `\n## 其他可一并关心的问题(以本次聚焦为主,自 ...@@ -94,6 +94,53 @@ ${others.length ? `\n## 其他可一并关心的问题(以本次聚焦为主,自
} }
/** /**
* 客户画像 → 话术指引块。从 16 个画像特征里挑对话术有用的,按用途排序:
* ⚠注意(禁忌/治疗敏感/特别关注)在前 → 定语气(rfm/lifecycle)→ 切入点(治疗史/权益/折扣/转介/家庭)。
* 只渲染命中的特征(persona 不产空标签),description 自包含 + 每 key 附一句话术指引。
* (修:旧版只读已删的 value/recall_risk key → 画像对话术全暗;现消费真实 16 特征。)
*/
const SCRIPT_PERSONA_GUIDE: Record<string, { warn?: boolean; guide: string }> = {
contraindication: { warn: true, guide: '安全红线:不得推荐该禁忌项目' },
treatment_sensitivity: { warn: true, guide: '沟通安抚:强调无痛/麻醉/慢节奏,先共情再谈治疗' },
special_attention: { warn: true, guide: '服务注意:不可等候→提前确认时间;免打扰→克制频次' },
rfm: { guide: '据分群定语气:重要/高价值→更上心维系、可适度尊享;低活跃→先唤醒别硬推' },
lifecycle_stage: { guide: '久未到诊(流失/沉睡)→先重新拉近、别急着推治疗' },
treatment_history: { guide: '可借"上次同类治疗体验"建立信任、跨科室迁移' },
entitlement_status: { guide: '可切入:商保直付 / 储值权益,降低决策门槛' },
discount_anchor: { guide: '谈优惠以此历史折扣为底线,优先换推增值权益而非直接降价' },
referral_champion: { guide: '转介达人→先致谢,体验好可再邀转介' },
family_structure: { guide: '可切入家庭套餐 / 家人同诊' },
time_preference: { guide: '约时间时优先客户习惯时段(更易接通/到诊)' },
};
const GUIDE_ORDER = [
'contraindication',
'treatment_sensitivity',
'special_attention',
'rfm',
'lifecycle_stage',
'treatment_history',
'entitlement_status',
'discount_anchor',
'referral_champion',
'family_structure',
'time_preference',
];
export function buildPersonaGuide(
highlights: Array<{ key: string; label: string; description: string }>,
): string {
const byKey = new Map(highlights.map((h) => [h.key, h]));
const lines: string[] = [];
for (const key of GUIDE_ORDER) {
const h = byKey.get(key);
const g = SCRIPT_PERSONA_GUIDE[key];
if (!h || !g) continue;
lines.push(`- ${g.warn ? '⚠ ' : ''}${h.label}${h.description} ${g.guide}`);
}
return lines.length ? `## 客户画像(内部参考:⚠注意事项 + 定语气 + 切入点)\n${lines.join('\n')}` : '';
}
/**
* 深度档专属扩展块(base++)—— 叠在 buildRichFactBlock 之后,给"分析(plan)"步更全的信息。 * 深度档专属扩展块(base++)—— 叠在 buildRichFactBlock 之后,给"分析(plan)"步更全的信息。
* ① 价值 / 流失风险(persona)→ 定语气;② 历史联系(诊所最近回访,≤5)→ 避免重复、接住上下文。 * ① 价值 / 流失风险(persona)→ 定语气;② 历史联系(诊所最近回访,≤5)→ 避免重复、接住上下文。
* 标准档不用(标准 = base+)。返回空串时调用方不追加。 * 标准档不用(标准 = base+)。返回空串时调用方不追加。
...@@ -101,14 +148,8 @@ ${others.length ? `\n## 其他可一并关心的问题(以本次聚焦为主,自 ...@@ -101,14 +148,8 @@ ${others.length ? `\n## 其他可一并关心的问题(以本次聚焦为主,自
export function buildDeepExtensions(input: DraftPlanScriptInput): string { export function buildDeepExtensions(input: DraftPlanScriptInput): string {
const blocks: string[] = []; const blocks: string[] = [];
const ph = (input.personaHighlights ?? []).filter((p) => p.key === 'value' || p.key === 'recall_risk'); const personaBlock = buildPersonaGuide(input.personaHighlights ?? []);
if (ph.length) { if (personaBlock) blocks.push(personaBlock);
blocks.push(
`## 价值 / 流失风险(内部参考,定语气:高价值→更上心维系;久未到店/高流失→先重新拉近、别急着推)\n${ph
.map((p) => `- ${p.label}:${p.description}`)
.join('\n')}`,
);
}
const ch = input.clinicalContext.contactHistory ?? []; const ch = input.clinicalContext.contactHistory ?? [];
if (ch.length) { if (ch.length) {
......
...@@ -611,7 +611,8 @@ export class PlanScriptOrchestrator { ...@@ -611,7 +611,8 @@ export class PlanScriptOrchestrator {
}; };
}), }),
}, },
personaHighlights: (persona?.features ?? []).slice(0, 5).map((f) => ({ // 全量传(buildPersonaGuide 按白名单 + 用途排序挑,不在此截断)
personaHighlights: (persona?.features ?? []).map((f) => ({
key: f.key, key: f.key,
label: personaFeatureMeta(f.key).label, label: personaFeatureMeta(f.key).label,
description: f.description, description: f.description,
......
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