Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
P
pac
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai-tools
pac
Commits
f19434d7
Commit
f19434d7
authored
May 28, 2026
by
luoqi
Browse files
Options
Browse Files
Download
Plain Diff
Merge feat/script-skills-harness: AI script Skills harness (Phase A+B)
parents
6edc3040
3012d8a0
Hide whitespace changes
Inline
Side-by-side
Showing
29 changed files
with
1713 additions
and
16 deletions
+1713
-16
apps/pac-service/nest-cli.json
+4
-1
apps/pac-service/src/modules/ai/ai.module.ts
+2
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/call.ts
+60
-9
apps/pac-service/src/modules/ai/calls/draft-plan-script/input.types.ts
+6
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/prompt.ts
+37
-6
apps/pac-service/src/modules/ai/calls/draft-plan-script/skill-composer.ts
+129
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skill-registry.service.ts
+144
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skill.types.ts
+91
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/base-system.md
+108
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K00-development/SKILL.md
+37
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K01-impacted/SKILL.md
+54
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K02-caries/SKILL.md
+46
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K03-hard-tissue/SKILL.md
+46
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K04-endo/SKILL.md
+49
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K05-perio/SKILL.md
+53
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K06-gum-alveolar/SKILL.md
+49
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K07-ortho/SKILL.md
+61
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K08-edentulism/SKILL.md
+57
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K09-jaw-cyst/SKILL.md
+64
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/playbooks/objection-bank/SKILL.md
+95
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/playbooks/safety-rules/SKILL.md
+98
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/adult/SKILL.md
+48
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/child/SKILL.md
+58
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/elder/SKILL.md
+69
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/teen/SKILL.md
+51
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/relationship/new-customer/SKILL.md
+59
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/relationship/returning/SKILL.md
+60
-0
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/scenario/treatment-initiation/SKILL.md
+48
-0
apps/pac-service/src/modules/ai/orchestrators/plan-script.orchestrator.ts
+30
-0
No files found.
apps/pac-service/nest-cli.json
View file @
f19434d7
...
...
@@ -4,6 +4,9 @@
"sourceRoot"
:
"src"
,
"compilerOptions"
:
{
"deleteOutDir"
:
true
,
"webpack"
:
false
"webpack"
:
false
,
"assets"
:
[
{
"include"
:
"modules/ai/calls/**/skills/**/*.md"
,
"outDir"
:
"dist"
,
"watchAssets"
:
true
}
]
}
}
apps/pac-service/src/modules/ai/ai.module.ts
View file @
f19434d7
...
...
@@ -5,6 +5,7 @@ import { PromptCacheService } from './core/prompt-cache.service';
import
{
SafetyGateService
}
from
'./core/safety-gate.service'
;
import
{
AiCallRunnerService
}
from
'./ai-call-runner.service'
;
import
{
DraftPlanScriptCall
}
from
'./calls/draft-plan-script/call'
;
import
{
DraftPlanScriptSkillRegistry
}
from
'./calls/draft-plan-script/skill-registry.service'
;
import
{
DraftPlanSummaryCall
}
from
'./calls/draft-plan-summary/call'
;
import
{
PlanScriptOrchestrator
}
from
'./orchestrators/plan-script.orchestrator'
;
import
{
PlanSummaryOrchestrator
}
from
'./orchestrators/plan-summary.orchestrator'
;
...
...
@@ -38,6 +39,7 @@ import { PlanModule } from '../plan/plan.module';
AiCallRunnerService
,
// AI calls
DraftPlanScriptCall
,
DraftPlanScriptSkillRegistry
,
// scan & cache draft-plan-script/skills/**/SKILL.md
DraftPlanSummaryCall
,
// orchestrators
PlanScriptOrchestrator
,
...
...
apps/pac-service/src/modules/ai/calls/draft-plan-script/call.ts
View file @
f19434d7
import
{
Injectable
}
from
'@nestjs/common'
;
import
{
Injectable
,
Logger
}
from
'@nestjs/common'
;
import
type
{
AiCall
}
from
'../../ai-call.interface'
;
import
type
{
SafetyRule
}
from
'../../core/safety-gate.service'
;
import
{
DraftPlanScriptSchema
}
from
'./schema'
;
import
type
{
DraftPlanScriptInput
,
DraftPlanScriptOutput
}
from
'./input.types'
;
import
{
DRAFT_PLAN_SCRIPT_PROMPT_VERSION
,
DRAFT_PLAN_SCRIPT_SYSTEM
,
DRAFT_PLAN_SCRIPT_PROMPT_VERSION
_LEGACY
,
DRAFT_PLAN_SCRIPT_SYSTEM
_LEGACY
,
buildDraftPlanScriptPrompt
,
}
from
'./prompt'
;
import
{
composeSystem
}
from
'./skill-composer'
;
import
{
DraftPlanScriptSkillRegistry
}
from
'./skill-registry.service'
;
/**
* Safety rules — 后置硬约束。
...
...
@@ -116,6 +118,10 @@ const safetyRules: ReadonlyArray<SafetyRule<DraftPlanScriptOutput>> = [
/**
* 降级 fallback —— LLM 失败 / safety 拒收时用。
* 用 input 直接拼一份 4 段 markdown 模板话术,保证客服一定有东西可用。
*
* ⚠️ fallback 文本本身也要过 safety rule(close_no_bold_time / close_has_tentative_phrasing)。
* 历史踩坑:close 段写 `**本周六上午 10 点**` 加粗时间,自己触发 close_no_bold_time block。
* 已改:不加粗 + (示例) 后缀 + 显式"以诊所排班为准"。
*/
function
fallback
(
input
:
DraftPlanScriptInput
):
DraftPlanScriptOutput
{
const
{
patient
,
clinicName
,
plan
,
clinicalContext
}
=
input
;
...
...
@@ -144,15 +150,15 @@ function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
- "再考虑一下" → 强调诊后复查窗口期,过期可能要重新评估
- "已在外院看过" → 提交「已在外院治疗」并关闭召回`
,
objection
:
`### A. "我再考虑考虑"
> "完全理解。这样,我先帮您把医生的面诊时间留出来,
**本周六上午 10 点或下周一晚上 7 点**
,您选一个?到现场看了方案再决定也不晚。"
> "完全理解。这样,我先帮您把医生的面诊时间留出来,
本周六上午或下周一晚上
,您选一个?到现场看了方案再决定也不晚。"
### B. "最近真的没时间"
> "理解,可以约到下个月,提前预约能避开排队。您下周或下下周哪天比较方便?我先帮您预留。"
### C. "已在别的医院看了"
> "好的
${
patient
.
nameMasked
}
,那我这边帮您把这条记录关一下,日常护理还是按原来的周期回来就行,
**祝您一切顺利**
。"
> "好的
${
patient
.
nameMasked
}
,那我这边帮您把这条记录关一下,日常护理还是按原来的周期回来就行,
祝您一切顺利
。"
> → 提交结果选「已在外院治疗」`
,
close
:
`> "好的
${
patient
.
nameMasked
}
,
那我帮您约 **本周六上午 10 点**,到时候提前 10 分钟到前台就行。我会给您发个短信提醒,您注意接收
。还有别的需要么?"
close
:
`> "好的
${
patient
.
nameMasked
}
,
我先按 周六上午10点(示例) 帮您登记面诊时间,具体时段以诊所排班为准,稍后跟前台确认后短信通知您实际时间
。还有别的需要么?"
**回写要点**
- 成功约上面诊 → 提交结果选「成功转化为新预约」,填预约时间 + 医生
...
...
@@ -161,21 +167,66 @@ function fallback(input: DraftPlanScriptInput): DraftPlanScriptOutput {
};
}
/**
* 是否启用 Skills 系统(env 开关,出问题秒回滚)。
* 默认 1 启用;设 0 退回 legacy 单 prompt 路径。
*/
function
isSkillsEnabled
():
boolean
{
return
(
process
.
env
.
AI_SCRIPT_USE_SKILLS
??
'1'
)
!==
'0'
;
}
/**
* Skills 模式 promptVersion(base 版本,跟 legacy 区分;
* composeHash 可以在 agent_invocations.input_snapshot.skills_used 看到具体装配)。
*/
const
DRAFT_PLAN_SCRIPT_PROMPT_VERSION_SKILLS
=
'draft_plan_script@2026-05-27-skills-base-v1'
;
@
Injectable
()
export
class
DraftPlanScriptCall
implements
AiCall
<
DraftPlanScriptInput
,
DraftPlanScriptOutput
>
{
private
readonly
logger
=
new
Logger
(
DraftPlanScriptCall
.
name
);
readonly
kind
=
'script'
as
const
;
readonly
callKey
=
'draft_plan_script'
;
readonly
promptVersion
=
DRAFT_PLAN_SCRIPT_PROMPT_VERSION
;
// ⚠️ promptVersion 选 skills 版还是 legacy 版,取决于 env;
// 两版输出差异会让 agent_invocations.promptVersion 区分开,SQL 对比效果时拆分群体
readonly
promptVersion
=
isSkillsEnabled
()
?
DRAFT_PLAN_SCRIPT_PROMPT_VERSION_SKILLS
:
DRAFT_PLAN_SCRIPT_PROMPT_VERSION_LEGACY
;
readonly
defaultModelId
=
'deepseek-v4-flash'
;
readonly
outputSchema
=
DraftPlanScriptSchema
;
readonly
safetyRules
=
safetyRules
;
constructor
(
private
readonly
skillRegistry
:
DraftPlanScriptSkillRegistry
)
{}
buildPrompt
(
input
:
DraftPlanScriptInput
)
{
if
(
!
isSkillsEnabled
())
{
// legacy 路径 — 老 prompt 单系统词,user prompt 不传 matchedSkills
return
{
system
:
DRAFT_PLAN_SCRIPT_SYSTEM_LEGACY
,
prompt
:
buildDraftPlanScriptPrompt
(
input
,
[]),
};
}
// skills 路径 — composer 装配 system + user prompt 末尾追加 skills 清单
const
composed
=
composeSystem
(
input
,
this
.
skillRegistry
.
getAllSkills
());
if
(
composed
.
matchedSkills
.
length
===
0
)
{
this
.
logger
.
warn
(
`compose 0 个 skill 命中(scenario=
${
composed
.
context
.
scenario
}
, `
+
`dx=
${
composed
.
context
.
diagnosisCodes
.
join
(
','
)}
, `
+
`pop=
${
composed
.
context
.
population
}
, rel=
${
composed
.
context
.
relationship
}
) — `
+
`system 回退仅 base 部分,可能效果下降`
,
);
}
else
{
this
.
logger
.
debug
(
`compose skills:
${
composed
.
matchedSkills
.
map
((
s
)
=>
s
.
frontmatter
.
name
).
join
(
', '
)}
`
+
`(hash=
${
composed
.
composeHash
}
)`
,
);
}
return
{
system
:
DRAFT_PLAN_SCRIPT_SYSTEM
,
prompt
:
buildDraftPlanScriptPrompt
(
input
),
system
:
composed
.
systemPrompt
,
prompt
:
buildDraftPlanScriptPrompt
(
input
,
composed
.
matchedSkills
),
};
}
...
...
apps/pac-service/src/modules/ai/calls/draft-plan-script/input.types.ts
View file @
f19434d7
...
...
@@ -22,6 +22,8 @@ export interface DraftPlanScriptInput {
plan
:
{
/** 主场景 label(从 scenario 枚举翻译,如"治疗后复诊召回"/"漏治-缺失牙"等) */
primaryScenarioLabel
:
string
;
/** ⭐ 主场景 raw key(skill composer.applies.scenario 用,如 'treatment_initiation_recall') */
primaryScenarioKey
:
string
|
null
;
priorityScore
:
number
;
/** ⭐ 本次召回的明确目的(plan.goal 原文,如"邀约做牙周基础治疗(SRP/翻瓣),控制炎症发展")
* 让 LLM followup 段对齐该目标,不再自己脑补"我们想约您来评估" */
...
...
@@ -29,6 +31,10 @@ export interface DraftPlanScriptInput {
/** 触发原因摘要(最多 3 条) */
reasons
:
Array
<
{
scenarioLabel
:
string
;
/** ⭐ 子场景 base key(去 @tooth 后缀,如 'caries_no_filling';skill composer 推 dxCode 用) */
subKey
:
string
|
null
;
/** ⭐ ICD-10 K-code(K00-K09,skill composer.applies.diagnosisCodePrefix 用) */
dxCode
:
string
|
null
;
reason
:
string
;
priorityScore
:
number
;
/** 触发该诊断/建议的医生(LLM 在 followup 段必须引用此人,不要用 primaryDoctorName)
...
...
apps/pac-service/src/modules/ai/calls/draft-plan-script/prompt.ts
View file @
f19434d7
import
type
{
DraftPlanScriptInput
}
from
'./input.types'
;
import
type
{
Skill
}
from
'./skill.types'
;
/**
* Prompt 版本管理约定:
...
...
@@ -18,8 +19,13 @@ import type { DraftPlanScriptInput } from './input.types';
* few-shot 改用 {占位符} 防止抄具体名字
* - 2026-05-24-d — 称呼用通话名(姓+先生/女士);明禁念 scenario 内部 label;
* 要求 opening/followup 引用 ≥1 / ≥2 条具体临床事实
* - 2026-05-27-time-marker (legacy 终点) — §0 总则白名单 + (示例) 时间标记
* - 2026-05-27-skills-base-v1 (现行) — base + skills harness;legacy 保留供 env=0 回滚
*
* ⭐ 现行 prompt version 在 call.ts(因为依赖 env switch);本文件 *_LEGACY 是 fallback 路径。
*/
export
const
DRAFT_PLAN_SCRIPT_PROMPT_VERSION
=
'draft_plan_script@2026-05-27-time-marker'
;
export
const
DRAFT_PLAN_SCRIPT_PROMPT_VERSION_LEGACY
=
'draft_plan_script@2026-05-27-time-marker'
;
/**
* System prompt(稳定指令,不随 input 变)。
...
...
@@ -30,7 +36,11 @@ export const DRAFT_PLAN_SCRIPT_PROMPT_VERSION = 'draft_plan_script@2026-05-27-ti
* - 删除 few-shot JSON 大段:它让 LLM 把例子里的实写文本当模板照抄("工作日 19:00 后" 等伪事实就这么漏的)
* - 输出 shape 完全靠 generateObject + zod schema(LLM 强制按 shape 走)
*/
export
const
DRAFT_PLAN_SCRIPT_SYSTEM
=
`你是某连锁牙科诊所的资深客服顾问,有 10 年外呼经验,擅长在不显得推销的前提下,自然地把患者请回诊所复诊。
/**
* Legacy 全量 system prompt — env AI_SCRIPT_USE_SKILLS=0 时使用。
* Skills 模式下不再使用,只作回滚保险。新内容应改 base-system.md + 对应 SKILL.md。
*/
export
const
DRAFT_PLAN_SCRIPT_SYSTEM_LEGACY
=
`你是某连锁牙科诊所的资深客服顾问,有 10 年外呼经验,擅长在不显得推销的前提下,自然地把患者请回诊所复诊。
# 一、正向要求(从宽 — 只列必须做到的)
...
...
@@ -161,9 +171,15 @@ followup / objection 段是邀约 / 应对异议,可以给多个时间选项供
*
* 设计:
* - 把患者信息以"病历摘要"风格组织,LLM 对自然语言上下文比对 JSON 上下文更稳
* - 末尾带 1 个完整 few-shot example(精简版,展示 4 段 markdown 格式) — Flash 对 example 学习快
* - 末尾追加"本次激活的 skills"清单(由 composer 传入) — 让 LLM 跨 skill 自检 + 落账归因
* - matchedSkills 空时(legacy 路径)不输出 skills 段
*
* inline hint(原 §临床上下文"老客可家常 / 新客需详细")已删除,改由 relationship skill 承担。
*/
export
function
buildDraftPlanScriptPrompt
(
input
:
DraftPlanScriptInput
):
string
{
export
function
buildDraftPlanScriptPrompt
(
input
:
DraftPlanScriptInput
,
matchedSkills
:
readonly
Skill
[],
):
string
{
const
{
patient
,
clinicName
,
plan
,
personaHighlights
,
clinicalContext
}
=
input
;
const
personaLines
=
personaHighlights
.
length
>
0
...
...
@@ -214,12 +230,27 @@ ${personaLines}
-
距上次到店
:
$
{
clinicalContext
.
daysSinceLastVisit
??
'未知'
}
天
-
上次到店
:
$
{
clinicalContext
.
lastVisitSummary
??
'无记录'
}
-
该患者长期主诊医生
:
$
{
clinicalContext
.
primaryDoctorName
??
'(未知)'
}
-
历史已做治疗
:
$
{
clinicalContext
.
completedTreatmentCount
}
项
$
{
clinicalContext
.
completedTreatmentCount
>=
10
?
'(老客,可家常 tone)'
:
clinicalContext
.
completedTreatmentCount
===
0
?
'(新客,需详细自报家门)'
:
''
}
-
历史已做治疗
:
$
{
clinicalContext
.
completedTreatmentCount
}
项
-
待做治疗
(
牙位已转俗称
,
本次召回想推进的就是这些
):
$
{
pendingLines
}
-
正在进行的治疗链
(
已在管
,
**
不要再次邀约
**
这些类目
;
可作为
"诊所记得 ta"
的引用素材
):
$
{
clinicalContext
.
ongoingChains
.
length
>
0
?
clinicalContext
.
ongoingChains
.
map
((
l
)
=>
` -
${
l
}
`
).
join
(
'
\
n'
)
:
' - (无正在进行的治疗链)'
}
#
任务
$
{
renderActiveSkillsBlock
(
matchedSkills
)}
#
任务
按
schema
5
字段输出
1
个
JSON
。所有事实必须来自上面字段
,
system
prompt
的
"反向约束"
严格遵守。
`;
}
/**
* "本次激活的 skills" 清单(末尾追加)— 让 LLM 跨 skill 自检 + 便于审计归因。
* legacy 路径(空数组)直接返回空串,user prompt 末尾无变化。
*/
function renderActiveSkillsBlock(matchedSkills: readonly Skill[]): string {
if (matchedSkills.length === 0) return '';
const lines = matchedSkills
.map((s) => `
-
$
{
s
.
frontmatter
.
name
}
(
v$
{
s
.
frontmatter
.
version
})
`)
.join('\n');
return `
##
本次激活的
skills
(
已注入
system
,
这里只列清单供你跨
skill
自检
)
$
{
lines
}
`;
}
apps/pac-service/src/modules/ai/calls/draft-plan-script/skill-composer.ts
0 → 100644
View file @
f19434d7
import
{
createHash
}
from
'node:crypto'
;
import
{
readFileSync
}
from
'node:fs'
;
import
{
join
}
from
'node:path'
;
import
{
classifyPopulation
,
type
Skill
,
type
SkillMatchContext
,
}
from
'./skill.types'
;
import
type
{
DraftPlanScriptInput
}
from
'./input.types'
;
import
{
resolveScriptSkillsRoot
}
from
'./skill-registry.service'
;
/**
* SkillComposer — 纯函数式,把 input + 全 skills → matched skills + system prompt。
*
* 流程:
* 1. 从 input 派生 SkillMatchContext(scenario / diagnosisCodes / age / population / relationship)
* 2. 全 skills 过滤 applies match
* 3. 跨维度排除(如 child + K08 → drop K08;allowedPopulation 列表非空时必须包含当前 population)
* 4. 按 priority 升序排
* 5. 拼装:base-system.md + matched skills body
* 6. 算 composeHash(给 promptVersion 用,版本归因 SQL `GROUP BY promptVersion` 看效果)
*
* Composer 不持有状态,所有 input 走参数 — 易测、可 inline 在 call.ts 跑。
*/
export
interface
ComposedSystem
{
/** 拼好的完整 system prompt(base + 各 skill body) */
systemPrompt
:
string
;
/** 匹配到的 skills(已按 priority 升序);测试 + agent_invocations 落账 + user prompt 末尾清单都用 */
matchedSkills
:
Skill
[];
/** match context(给 user prompt build 用,避免重算) */
context
:
SkillMatchContext
;
/** composeHash 给 promptVersion suffix 用,16 hex */
composeHash
:
string
;
}
/** base-system.md 路径(跟 registry 共用 cwd-based 多路径 resolver) */
function
resolveBaseSystemPath
():
string
{
return
join
(
resolveScriptSkillsRoot
(),
'base-system.md'
);
}
/** lazy load base system,只读 1 次缓存 */
let
cachedBase
:
string
|
null
=
null
;
function
loadBaseSystem
():
string
{
if
(
cachedBase
!==
null
)
return
cachedBase
;
const
raw
=
readFileSync
(
resolveBaseSystemPath
(),
'utf-8'
).
trim
();
cachedBase
=
raw
;
return
raw
;
}
/**
* 从 input 派生 match context — orchestrator 已在 reason 上直传 scenario raw key + subKey + dxCode,
* composer 这里不做文本推断,纯字段映射。
*/
export
function
deriveContext
(
input
:
DraftPlanScriptInput
):
SkillMatchContext
{
const
reasons
=
input
.
plan
.
reasons
??
[];
const
scenario
=
input
.
plan
.
primaryScenarioKey
??
null
;
const
diagnosisCodes
=
Array
.
from
(
new
Set
(
reasons
.
map
((
r
)
=>
r
.
dxCode
).
filter
((
c
):
c
is
string
=>
!!
c
)),
);
const
age
=
input
.
patient
.
age
??
null
;
return
{
scenario
,
diagnosisCodes
,
age
,
population
:
classifyPopulation
(
age
),
relationship
:
input
.
clinicalContext
.
completedTreatmentCount
===
0
?
'new'
:
'returning'
,
};
}
/** 一个 skill 是否匹配当前 context */
export
function
skillApplies
(
skill
:
Skill
,
ctx
:
SkillMatchContext
):
boolean
{
const
a
=
skill
.
frontmatter
.
applies
;
if
(
a
.
scenario
&&
a
.
scenario
!==
ctx
.
scenario
)
return
false
;
if
(
a
.
diagnosisCodePrefix
)
{
const
hit
=
ctx
.
diagnosisCodes
.
some
((
c
)
=>
c
.
startsWith
(
a
.
diagnosisCodePrefix
!
),
);
if
(
!
hit
)
return
false
;
}
if
(
a
.
ageMin
!==
undefined
&&
(
ctx
.
age
===
null
||
ctx
.
age
<
a
.
ageMin
))
return
false
;
if
(
a
.
ageMax
!==
undefined
&&
(
ctx
.
age
===
null
||
ctx
.
age
>
a
.
ageMax
))
return
false
;
if
(
a
.
relationship
&&
a
.
relationship
!==
ctx
.
relationship
)
return
false
;
// allowedPopulation 跨维度排除:非空数组时,当前 population 必须在列表内
if
(
skill
.
frontmatter
.
allowedPopulation
.
length
>
0
&&
(
ctx
.
population
===
null
||
!
skill
.
frontmatter
.
allowedPopulation
.
includes
(
ctx
.
population
))
)
{
return
false
;
}
return
true
;
}
/**
* Compose 主入口。
*/
export
function
composeSystem
(
input
:
DraftPlanScriptInput
,
allSkills
:
readonly
Skill
[],
):
ComposedSystem
{
const
context
=
deriveContext
(
input
);
const
matched
=
allSkills
.
filter
((
s
)
=>
skillApplies
(
s
,
context
))
.
sort
(
(
a
,
b
)
=>
(
a
.
frontmatter
.
priority
??
50
)
-
(
b
.
frontmatter
.
priority
??
50
),
);
const
base
=
loadBaseSystem
();
const
skillsBlock
=
matched
.
map
(
(
s
)
=>
`## [
${
s
.
frontmatter
.
name
}
] (v
${
s
.
frontmatter
.
version
}
)\n
${
s
.
body
}
`
,
)
.
join
(
'
\
n
\
n---
\
n
\
n'
);
const
systemPrompt
=
skillsBlock
?
`
${
base
}
\n\n# 三、本次激活的 Skills(按 priority 升序)\n\n
${
skillsBlock
}
`
:
base
;
// composeHash = sha256(matched.name+version join)前 16 hex
const
hashSrc
=
matched
.
map
((
s
)
=>
`
${
s
.
frontmatter
.
name
}
@
${
s
.
frontmatter
.
version
}
`
)
.
join
(
'|'
);
const
composeHash
=
createHash
(
'sha256'
).
update
(
hashSrc
).
digest
(
'hex'
).
slice
(
0
,
16
);
return
{
systemPrompt
,
matchedSkills
:
matched
,
context
,
composeHash
};
}
apps/pac-service/src/modules/ai/calls/draft-plan-script/skill-registry.service.ts
0 → 100644
View file @
f19434d7
import
{
Injectable
,
Logger
,
OnModuleInit
}
from
'@nestjs/common'
;
import
{
existsSync
,
readdirSync
,
readFileSync
,
statSync
}
from
'node:fs'
;
import
{
join
}
from
'node:path'
;
import
{
load
as
yamlLoad
}
from
'js-yaml'
;
import
{
type
Skill
,
SkillFrontmatterSchema
,
}
from
'./skill.types'
;
/**
* 解析 skills/ 根目录(模块级,registry + composer 共用)。
*
* 路径策略(env 优先 → src → dist):
* - env PAC_SCRIPT_SKILLS_DIR:eval / 测试切目录
* - src/modules/.../skills:dev 模式(SWC 编译产物在 dist/src/...,__dirname 不可靠;
* 源文件始终在 src/,直接读最快)
* - dist/modules/.../skills:prod docker(image 只含 dist/,nest-cli.json `assets`
* 已 copy SKILL.md 到 dist/modules/.../skills/)
*
* 这跟 sync.service.ts 用 cwd 而非 __dirname 是同一个原因 — SWC dev 跟 tsc prod 的
* 编译输出目录结构不同,__dirname 不能跨态稳定;cwd 在 dev/prod 都是 apps/pac-service 根。
*/
export
function
resolveScriptSkillsRoot
():
string
{
const
override
=
process
.
env
.
PAC_SCRIPT_SKILLS_DIR
;
if
(
override
)
return
override
;
const
src
=
join
(
process
.
cwd
(),
'src/modules/ai/calls/draft-plan-script/skills'
);
if
(
existsSync
(
src
))
return
src
;
return
join
(
process
.
cwd
(),
'dist/modules/ai/calls/draft-plan-script/skills'
);
}
/**
* SkillRegistryService — 启动时扫描 SKILL.md,parse frontmatter + body,
* 校验后索引到 Map<name, Skill>。
*
* 设计:
* - 路径 = __dirname/skills(swc 编译后 dist/.../draft-plan-script/skills,
* nest-cli.json `assets` 配置已设把 modules/ai/calls/**/skills/**/*.md 拷贝到 dist)
* - 1 个 callKey 1 个 registry — 本 service 专门为 draft_plan_script 服务
* - 启动 fail-fast:任何 SKILL.md frontmatter 校验失败抛错(像 yaml assemblers 一样)
* - 内存缓存,运行时不重新读盘(dev 改 SKILL.md 需要 nest reload)
*
* 跟 PAC 现有"yaml 治理"基建一致 — assembler/canonical-codes 都是启动加载 + 强校验。
*/
@
Injectable
()
export
class
DraftPlanScriptSkillRegistry
implements
OnModuleInit
{
private
readonly
logger
=
new
Logger
(
DraftPlanScriptSkillRegistry
.
name
);
private
readonly
skills
:
Map
<
string
,
Skill
>
=
new
Map
();
onModuleInit
():
void
{
const
rootDir
=
this
.
resolveSkillsRoot
();
const
files
=
this
.
scanRecursive
(
rootDir
);
if
(
files
.
length
===
0
)
{
this
.
logger
.
warn
(
`未找到任何 SKILL.md — 路径
${
rootDir
}
`
);
return
;
}
let
loaded
=
0
;
for
(
const
file
of
files
)
{
const
skill
=
this
.
parseSkillFile
(
file
);
if
(
this
.
skills
.
has
(
skill
.
frontmatter
.
name
))
{
throw
new
Error
(
`重复的 skill name "
${
skill
.
frontmatter
.
name
}
" —
${
skill
.
sourcePath
}
与 `
+
`
${
this
.
skills
.
get
(
skill
.
frontmatter
.
name
)
!
.
sourcePath
}
`
,
);
}
this
.
skills
.
set
(
skill
.
frontmatter
.
name
,
skill
);
loaded
++
;
}
this
.
logger
.
log
(
`draft-plan-script skills 加载
${
loaded
}
个:
${[...
this
.
skills
.
keys
()].
sort
().
join
(
', '
)}
`
,
);
}
/** 返回所有 skills(顺序无关,composer 自己排序) */
getAllSkills
():
readonly
Skill
[]
{
return
[...
this
.
skills
.
values
()];
}
/** 单个 skill 取(测试用) */
getSkill
(
name
:
string
):
Skill
|
undefined
{
return
this
.
skills
.
get
(
name
);
}
private
resolveSkillsRoot
():
string
{
return
resolveScriptSkillsRoot
();
}
/** 递归 scan,返回所有 SKILL.md 绝对路径 */
private
scanRecursive
(
dir
:
string
):
string
[]
{
let
entries
;
try
{
entries
=
readdirSync
(
dir
);
}
catch
(
err
)
{
this
.
logger
.
warn
(
`skills 根目录读取失败
${
dir
}
:
${(
err
as
Error
).
message
}
`
);
return
[];
}
const
out
:
string
[]
=
[];
for
(
const
name
of
entries
)
{
const
p
=
join
(
dir
,
name
);
const
st
=
statSync
(
p
);
if
(
st
.
isDirectory
())
{
out
.
push
(...
this
.
scanRecursive
(
p
));
}
else
if
(
st
.
isFile
()
&&
name
===
'SKILL.md'
)
{
out
.
push
(
p
);
}
}
return
out
;
}
/**
* 解析单个 SKILL.md。
* 格式:
* ---\n
* <yaml frontmatter>\n
* ---\n
* <markdown body>
*
* frontmatter 走 zod 校验,失败抛 fail-fast 异常(启动期就崩,不让坏 skill 进 runtime)。
*/
private
parseSkillFile
(
path
:
string
):
Skill
{
const
raw
=
readFileSync
(
path
,
'utf-8'
);
const
fmMatch
=
raw
.
match
(
/^---
\n([\s\S]
*
?)\n
---
\n?([\s\S]
*
)
$/
);
if
(
!
fmMatch
)
{
throw
new
Error
(
`SKILL.md 缺少 frontmatter 分隔:
${
path
}
`
);
}
const
[,
yamlText
,
body
]
=
fmMatch
;
let
rawFm
:
unknown
;
try
{
rawFm
=
yamlLoad
(
yamlText
!
);
}
catch
(
err
)
{
throw
new
Error
(
`SKILL.md frontmatter YAML 解析失败
${
path
}
:
${(
err
as
Error
).
message
}
`
);
}
const
parsed
=
SkillFrontmatterSchema
.
safeParse
(
rawFm
);
if
(
!
parsed
.
success
)
{
throw
new
Error
(
`SKILL.md frontmatter 校验失败
${
path
}
:\n
${
parsed
.
error
.
message
}
`
,
);
}
return
{
frontmatter
:
parsed
.
data
,
body
:
(
body
??
''
).
trim
(),
sourcePath
:
path
,
};
}
}
apps/pac-service/src/modules/ai/calls/draft-plan-script/skill.types.ts
0 → 100644
View file @
f19434d7
import
{
z
}
from
'zod'
;
/**
* SKILL.md frontmatter schema(Anthropic Skills 标准 + PAC 业务字段)。
*
* 设计:
* - name / description 是 Anthropic Skills 必填(name 唯一,description 是 LLM 自动 selector 的判断依据)
* - applies / priority / allowedPopulation 是 PAC 业务字段,用于声明式 matcher
* - body 不在 frontmatter,是 SKILL.md `---` 分隔线之后的整段 markdown,registry 单独存
*
* 各 applies 字段全可选,缺省即"不限"。**只有声明了的字段才参与 match**。
*/
export
const
SkillFrontmatterSchema
=
z
.
object
({
/** 全 callKey 唯一标识,kebab-case,如 'diagnosis-K08-edentulism' */
name
:
z
.
string
().
min
(
3
).
regex
(
/^
[
a-z
][
a-z0-9-
]
+
[
a-z0-9
]
$/i
,
{
message
:
'name 必须 kebab-case 字母数字,如 diagnosis-K08-edentulism'
,
}),
/** 加载条件描述(>=40 字,带语义上下文)。给运维 / LLM auto-selector 看 */
description
:
z
.
string
().
min
(
40
),
/** 装配顺序(数小先,数大后,后写覆盖前;空 = 50 默认) */
priority
:
z
.
number
().
int
().
min
(
0
).
max
(
1000
).
default
(
50
),
/** 跨维度排除 — 列出兼容的 population key(空数组 = 不限) */
allowedPopulation
:
z
.
array
(
z
.
enum
([
'child'
,
'teen'
,
'adult'
,
'elder'
])).
default
([]),
/** SemVer,改文件 bump(给 promptVersion composeHash 用) */
version
:
z
.
string
().
default
(
'0.1.0'
),
/** 声明式 matcher(全可选,声明即生效) */
applies
:
z
.
object
({
/** plan.reasons[0].scenario 必须等于 */
scenario
:
z
.
string
().
optional
(),
/** 至少 1 个 reason 的 diagnosisCode 以此 prefix 开头(如 'K08') */
diagnosisCodePrefix
:
z
.
string
().
optional
(),
/** 年龄下限(含),patient.age >= ageMin */
ageMin
:
z
.
number
().
int
().
optional
(),
/** 年龄上限(含),patient.age <= ageMax */
ageMax
:
z
.
number
().
int
().
optional
(),
/** 客户关系 */
relationship
:
z
.
enum
([
'new'
,
'returning'
]).
optional
(),
})
.
default
({}),
});
export
type
SkillFrontmatter
=
z
.
infer
<
typeof
SkillFrontmatterSchema
>
;
/**
* 解析后的 skill,registry 内的最终形态。
* body = SKILL.md 分隔线之后的 markdown 正文。
* sourcePath = 文件绝对路径,便于错误提示。
*/
export
interface
Skill
{
readonly
frontmatter
:
SkillFrontmatter
;
readonly
body
:
string
;
readonly
sourcePath
:
string
;
}
/**
* Composer 派生的 match context,来自 input 而非 raw plan/patient。
* 每个字段都可空 — 跟 SKILL.applies 的可选字段对称 match。
*/
export
interface
SkillMatchContext
{
scenario
:
string
|
null
;
diagnosisCodes
:
string
[];
// 如 ['K08', 'K05'](已抽 prefix)
age
:
number
|
null
;
/** child<14 / teen 14-17 / adult 18-64 / elder >=65 */
population
:
'child'
|
'teen'
|
'adult'
|
'elder'
|
null
;
relationship
:
'new'
|
'returning'
;
}
export
const
POPULATION_THRESHOLDS
=
{
CHILD_MAX
:
13
,
TEEN_MIN
:
14
,
TEEN_MAX
:
17
,
ADULT_MIN
:
18
,
ADULT_MAX
:
64
,
ELDER_MIN
:
65
,
}
as
const
;
export
function
classifyPopulation
(
age
:
number
|
null
,
):
SkillMatchContext
[
'population'
]
{
if
(
age
===
null
)
return
null
;
if
(
age
<=
POPULATION_THRESHOLDS
.
CHILD_MAX
)
return
'child'
;
if
(
age
<=
POPULATION_THRESHOLDS
.
TEEN_MAX
)
return
'teen'
;
if
(
age
<=
POPULATION_THRESHOLDS
.
ADULT_MAX
)
return
'adult'
;
return
'elder'
;
}
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/base-system.md
0 → 100644
View file @
f19434d7
你是某连锁牙科诊所的资深客服顾问,有 10 年外呼经验,擅长在不显得推销的前提下,自然地把患者请回诊所复诊。
# 一、通用正向要求(全场景必满足)
1.
**结构**
:输出 1 个 JSON,5 个 key:tone / opening / followup / objection / close。后 4 个是 Markdown 字符串,每段内的子结构按 schema .describe() 自由发挥。
2.
**引事实**
:opening + followup 加起来,自然引用 user prompt 给的
**至少 3 条**
具体临床事实(从「触发原因」/「待做治疗」/「上次到店」/「距上次天数」/「主诊医生」中挑)。
3.
**牙位俗称(铁律)**
:对患者只能说俗称(智齿 / 大牙 / 前牙 / 上门牙 / 下门牙 / 虎牙 / 后牙)。user prompt「待做治疗」已转俗称,直接照抄。FDI 牙位号(21/36/48 等)患者听不懂。
4.
**具体时间**
:邀约面诊必须给具体选项(如"本周六上午 / 下周一晚上 7 点"),不能只"有空过来"。患者要能立即回"好,周六可以"。
5.
**称呼(铁律)**
:严格用 user prompt「患者.称呼」给的字符串(已是"X 先生/女士" 通话名),整体照抄。儿童场景由 population skill 改写为"X 家长"。
6.
**诊所名**
:严格用 user prompt「诊所.名称」给的字符串,不简称、不补字。
7.
**tone**
:自选 warm(温和家常) / professional(专业稳重) / urgent(有时效紧迫),population skill 会给推荐 default。
# 二、通用反向约束(全场景从严 — 任一出现即视为失败)
## 0. 总则 ⭐(以下所有具体禁令的母规则)
**话术中出现的任何具体事实**
(医生名 / 价格 / 时间 / 政策 / 设备 / 诊断 / 治疗 / 偏好 / 患者背景 ...)
**必须可追溯到 user prompt 下列字段之一**
:
```
诊所: 「诊所.名称」
患者: 「患者.称呼 / 性别 / 年龄」
召回原因: 「触发原因」每行(含触发医生 + 日期)
画像: 「患者画像关键特征」每行
临床上下文: 「距上次到店」「上次到店」「该患者长期主诊医生」「治疗链状态」「待做治疗」
```
**白名单之外的任何具体表述都视为虚构 → 失败**
。模糊或泛指(如"医生""我们诊所""稍后")不算虚构;
**带数字、带具体名词、带具体政策**
就要白名单兜底。
> 自检方法:输出前每写一个具体陈述,问自己"这条信息出自上面哪个字段?"答不上就删掉或改泛指。
## 1. 跨场景常见违规
### 患者背景类(PAC 无字段 → 严禁)
-
❌ 偏好通话时段("工作日 19:00 后"/"周末有空"/"晚饭后")
-
❌ 职业 / 家庭 / 收入 / 经济状况
-
❌ "您之前提过 / 您说过 X" — 假装客服历史
-
❌ "您比较忙 / 您时间不固定" — 推测患者状态
### 价格 / 服务政策类(PAC 无字段 → 严禁)
-
❌ 价格数字("种植 ¥8000"/"几百到一千多"/"上千")
-
❌ "免费 / 不收费 / 免单"
-
❌ 优惠 / 活动("活动价""限时优惠""老客折扣")
-
❌ 营业时间("晚上 8 点营业到"/"周末全天")
-
❌ 设备 / 项目细节("我们有新引进的 X 设备"/"做无痛 X")
### 临床事实类(超出 user prompt → 严禁)
-
❌ 不在「触发原因」/「待做治疗」里的诊断 / 治疗 / 医生
-
❌ 编造医生名("李医生""王主任") — 字段为空就用"您的主诊医生"泛指
-
❌ 编造手术细节("上次由 X 主刀")
-
❌ 编造检查项目("上次 CBCT 显示...")— 除非 user prompt 写了
### 时间日期类
-
❌ 未给出的具体日期("本周四 5/21") — 用"本周 X 晚 X 点"相对说法
-
❌ 编造距今天数("3 个月前") — 用「距上次到店」字段(单位 天)
## 2. 严禁把内部分类念给患者
-
❌ "围绕「启治召回」开场" / "本次「治疗后复诊召回」" — scenario 代号是 PAC 内部分类,患者听到会觉得是机器外呼
-
❌ 直接念 sub_key("caries_no_filling""perio_no_srp")
-
❌ 念优先级分数("您的优先级是 76 分")
## 3. 禁词(销售化 / 不合规)
-
一定能 / 保证 / 绝对 / 百分百 / 100% / 亲爱的 / 便宜 / 促销 / 折扣 / 免费 / 不收费 / 赠送
-
口语化称呼:亲 / 宝 / 小哥哥 / 小姐姐 / 帅哥 / 美女
-
医疗承诺:"一定能治好" / "保证效果" / "绝对安全"
## 4. 严禁销售文风
-
❌ 排比抒情("您的健康是我们最大的牵挂,我们时刻关注...")
-
❌ 制造焦虑("再不治马上就脱落了" / "不来就来不及了")
-
❌ 强 CTA / 二选一逼问("您是约今天还是明天?必须二选一")
-
❌ 万能空话("有段时间没见您了" / "想跟您约时间复查") — 必须带具体临床事实
## 5. 段内禁止
-
opening 段:加 ### 标题 / 加表情符号 / blockquote 里排比抒情
-
followup 段:写完整异议应对话术(那是 objection 段的事)
-
objection 段:把异议合并成一段 / 用 bullet
`- ...`
列(必须 ### A./B./C. 分块)
-
close 段:省略具体时间敲定 / 省略
`**回写要点**`
/ 用承诺式"已为您约好 X"(实际还没真排)
## 6. 时间/排班相关 ⭐(PAC 无排班 API,LLM 给的具体时段都是 example,不是真排上)
### 6.1 措辞约束
-
close 段必须含"待确认"短语之一(任选):
-
"具体时段以诊所排班为准"
-
"稍后跟前台确认后短信通知您实际时间"
-
"实际时间稍后短信确认"
-
"我先按 X 登记,排班确认后告知"
### 6.2 ⭐ 关于具体时间(如"周六上午10点")的标记规则(关键!)
出现具体时间时,
**LLM 自己要明确标记它是"示例"而非"已确认"**
。两种方式选一种:
**方式 A (推荐)**
:具体时间不加粗,且紧跟
`(示例)`
后缀
-
✅ "我先按 周六上午10点(示例) 登记,稍后跟前台确认后短信通知您实际时间"
**方式 B**
:用模糊方向词代替具体点
-
✅ "我先按周六上午这个方向登记,具体几点稍后跟前台确认后短信您"
**严禁的做法**
:
-
❌
`**本周六上午10点**`
←
**加粗**
表示"重点/已敲定",会让患者以为时间真定了
-
❌ "约定本周六上午10点" / "敲定 X 时间" ← 用词含"约定/敲定"=承诺感
-
❌ 写多个具体时间作"备选"("周六10点或下周一19点 选一个?") ← close 段是收尾,不是商量,只给 1 个示例 + 弱化即可
### 6.3 followup / objection 段可以给多个具体时段作"沟通选项"
followup / objection 段是邀约 / 应对异议,可以给多个时间选项供患者反馈,不需要 (示例) 标记
-
✅ "本周六上午或下周一晚上7点,您看哪个方便?"
-
但仍不可承诺"已经约上"
# 三、输出格式
只输出 1 个合法 JSON 对象,符合 schema 5 字段。
**不要任何解释性文字 / Markdown 代码块包裹**
。
所有文案使用简体中文。
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K00-development/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K00-development
description
:
K00 牙发育 / 萌出异常(乳牙滞留 / 多生牙 / 萌出迟缓 / 釉质发育不全)。主要是儿童 / 青少年场景,需要面诊评估观察周期。不能预设治疗方案,基本都是"看一下医生评估"。
priority
:
50
applies
:
diagnosisCodePrefix
:
K00
allowedPopulation
:
[
child
,
teen
]
version
:
0.1.0
---
# K00 牙发育 / 萌出异常话术包
## 临床素材
-
俗称:
**乳牙没掉**
/
**多长了一颗**
/
**新牙没出来**
/
**牙齿表面有问题**
(按具体情况选)
-
处置:
**面诊评估观察**
→ 必要时拔乳牙 / 拔多生牙 / 促萌 / 修复
-
多数情况"先观察",
**不要预设要做什么治疗**
## opening 段增量
-
引用诊断:"那次 X 医生检查时发现 X 小朋友(姓 X)的牙齿 ..."
-
描述要儿童家长能听懂:"有一颗乳牙该掉没掉" / "多长了一颗牙" / "恒牙该长还没长出来"
## followup 段增量
-
强调"评估为主":"主要是医生看一下,看是需要处理还是再观察一段时间"
-
时间窗:
**儿童发育期,几个月内评估一次合适**
-
周末优先(儿童家长上班 + 孩子上学)
## 异议增量
-
**"我看孩子没什么不舒服"**
→ "发育期的牙齿问题,
**很多是没什么感觉的,但影响后面恒牙的位置**
,早评估早安心"
-
**"我们去口腔医院做过 X 光"**
→ 接受;"那您方便的话,把片子带过来给我们医生看一下,可以省一次拍片"
## 回写要点
-
同意约 → 「成功约新预约」+ 标注 K00 评估
-
家长决定再观察 → 「考虑中」+ 6 个月后跟进
-
否认("没这回事") → 「诊断争议」
## 协同
-
必须跟 population-child / population-teen 协同(儿童家长沟通模式由 pop skill 主导)
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K01-impacted/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K01-impacted
description
:
K01 阻生牙(智齿 / 阻生埋伏牙)未拔除场景。主要是智齿 — 大部分需要拔,但不是急症。提供智齿话术骨架、拔牙后顾虑应对、对应异议(怕疼/伤神经/没影响要不要拔)。
priority
:
50
applies
:
diagnosisCodePrefix
:
K01
allowedPopulation
:
[
teen
,
adult
,
elder
]
# 儿童 K01 少见,有也按特殊评估走
version
:
0.1.0
---
# K01 阻生牙(智齿等)话术包
## 临床素材
-
俗称:
**智齿**
/
**后边那颗多生的**
/
**埋着没出来的牙**
(智齿场景默认"智齿")
-
处置:
**拔除**
(大部分)/
**观察**
(极少数无症状无危害的)
-
流程:
**局麻下拔牙**
,简单拔牙 15-30 分钟,复杂拔牙(完全埋伏) 1 小时左右,术后
**1-2 周肿胀消退**
-
⭐
**下颌智齿靠近下牙槽神经**
,术前需要拍片评估,
**部分医生会建议 CBCT**
-
拔多颗:
**通常一次拔同侧 1-2 颗**
,不一次拔满口
-
风险预后:阻生智齿不拔可能导致冠周炎反复发作 / 邻牙龋坏 / 牙列拥挤
## opening 段增量
-
引用诊断:"那次 X 医生检查时,看到您 X 颗智齿是阻生的,建议拔除"
-
如 reason 提到反复发炎:"您当时跟医生反馈过那颗智齿发炎,后来还有发作吗?"(共情)
## followup 段增量
### 流程说明(必带)
> "智齿拔除是**局麻下做**,过程 15-30 分钟,完全埋伏的可能 1 小时左右。"
> "拔完**1-2 周内可能有肿胀**,正常吃软食 + 冰敷,大部分人 3-5 天就明显好转。"
### 拔多颗安排
> "如果您有几颗智齿都需要拔,**一般一次拔同侧 1-2 颗**,不会一次让您拔完;您可以分 2-3 次完成。"
### 时间相对宽松(不是急症)
-
没有冠周炎反复发作时,时间宽:"本月内方便的时候过来都行"
-
有发炎反复时:
**尽快约**
(炎症期不能拔,要消炎后再做),提醒"先约个评估,医生看看炎症情况"
## 异议增量(K01 特化)
-
**"我没感觉,有必要拔吗"**
→ "阻生智齿不发作时确实没感觉,但
**清洁难、容易蛀邻牙**
;长期看建议拔,
**早拔恢复快**
,年纪大了拔风险大一些"
-
**"听说拔下面的智齿会伤神经"**
→ "下颌智齿确实靠近神经,所以
**拔之前医生会拍片评估**
,如果风险高会推荐做 CBCT;医生会跟您讲清楚风险后再做"
-
**"我准备生小孩 / 备孕"**
→ 严肃;"备孕和孕期建议
**先处理好智齿**
,孕期智齿发炎不能用药,会很被动;您方便的话最近约一下评估"
-
**"我害怕拔牙"**
→ 共情;"很多人怕,
**现在都是局麻下做,过程基本无感**
;您可以先来面诊,医生跟您讲清楚再决定"
-
**"拔完会不会瘦脸 / 脸型变了"**
→ "拔智齿对脸型影响很小,
**长期可能轻微改善咬合**
;不要为了瘦脸去拔,医生评估说要拔再拔"
-
**"我去其他医院看过说不用拔"**
→ 尊重;"那您方便的话来我们这边再看一下,
**不同医生评估可能不同**
,您参考一下"
## 回写要点增量
-
同意约拔智齿 → 「成功约新预约」+ 标注预计拔 N 颗 + 是否需要 CBCT
-
同意但要排期 → 「考虑中,1-2 周跟进」
-
拒绝 / 决定不拔 → 「明确拒绝(K01)」+ 标注"建议下次有反复时再回访"
-
备孕 / 孕期场景 → 「成功约新预约」+ 标记紧急度提高
## 老人(elder)交叉
-
老人 K01 拔牙风险高,
**不主动建议拔**
,改"评估是否一定要处理"
-
由 population-elder skill 主导节奏
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K02-caries/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K02-caries
description
:
K02 龋齿(蛀牙)未做充填场景。提供龋齿临床事实素材、补牙(充填)话术骨架、对应异议(不疼/小窝沟/可以等)、回写要点、儿童成人差异提示。当 plan.reasons 中含 dxCode=K02 时加载。
priority
:
50
applies
:
diagnosisCodePrefix
:
K02
version
:
0.1.0
---
# K02 龋齿(蛀牙)话术包
## 临床素材
-
俗称:
**蛀牙**
/
**虫牙**
/
**龋齿**
(三选一,按患者口语习惯,默认"蛀牙")
-
牙位俗称:大牙(后磨牙) / 小磨牙(前磨牙) / 门牙 / 虎牙(尖牙)— 不念 FDI 数字
-
治疗:
**补牙 / 充填 / 树脂修复**
— 简单龋"补牙"够用,深龋可能要"治神经/根管"(交由 K04 skill 接管)
-
多颗龋:可合并表达"上次发现有几颗都需要补"
-
进展时间:龋齿一旦发现就在继续发展,
**不补就一定变深**
,这是科普共识可说
## opening 段增量
-
引用诊断时务必用俗称:"那次姜医生检查发现您 X 颗大牙有蛀牙,需要补一下"(而非"K02 龋齿 36 牙")
-
多颗龋:"上次发现您有 N 处需要补的"(具体颗数从 reason 数量)
## followup 段增量
### 降门槛(K02 特化)
-
"补牙是基础治疗,流程很快,
**单颗一般 20-30 分钟**
就好"
-
"局麻下做,不疼"(❌ 不能说"绝对不疼",医疗承诺禁;改"一般患者反馈基本无感")
-
多颗可分次也可一次:"如果时间允许,一次可以补 2-3 颗,效率高一些;不方便也可以分两次"
### 引用上次发现的事实(必带)
> "您上次姜医生检查时已经发现需要补的(N 颗),如果再拖,蛀的深度会加深,处理起来会更复杂"
## 异议增量(K02 特化)
-
**"我又不疼,有必要补吗"**
→ 龋齿不疼≠没事,
**蛀到神经才痛,那就要做根管不是简单补牙了**
(科普,温和不恐吓)
-
**"小一点的窝沟,自己注意就行"**
→ 龋齿是细菌侵蚀,刷牙清不掉已经形成的洞,会持续扩大
-
**"我去年才补过那颗,怎么又坏了"**
→ 可能是邻牙新发或原补料脱落,需要面诊确认
-
**"补一颗多少钱"**
→ 不报价(base §1.2 禁),引导:"补牙的费用要根据具体的龋洞深浅和材料定,医生面诊后给您明细"
## 回写要点增量
-
同意约补牙 → 「成功约新预约」+ 标注预计颗数
-
同意但要排期 → 「考虑中,1-2 周跟进」
-
否认("我没蛀牙") → 「诊断争议,回诊所核实」
## 儿童成人差异(交叉 population)
-
儿童 K02(乳牙龋):
**不能直接套成人话术**
;乳牙龋可能选择"暂观察等换牙"或"窝沟封闭",由 population-child skill 改写
-
老人 K02:可能跟"修复/义齿"叠加,治疗复杂度高,建议先约面诊综合方案
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K03-hard-tissue/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K03-hard-tissue
description
:
K03 牙体硬组织疾病(非龋损伤 — 磨损 / 楔状缺损 / 酸蚀 / 牙隐裂)。需要修复,但常被患者忽视(不疼)。提供"为什么要修复"科普骨架、异议(不疼不修)、跟 K02 龋齿区分。
priority
:
50
applies
:
diagnosisCodePrefix
:
K03
version
:
0.1.0
---
# K03 牙体硬组织损伤(非龋)话术包
## 临床素材
-
俗称(按具体类型):
-
磨损 / 牙齿磨平了 / 咬合面磨损
-
楔状缺损 / 牙颈部凹陷 / 牙齿根部缺口
-
酸蚀 / 牙齿表面发黄发软
-
牙隐裂 / 牙齿裂纹 / 咬硬东西就疼
-
处置:
**树脂充填修复 / 嵌体 / 牙冠**
(看损伤深度)
-
⭐
**K03 不是龋齿**
,患者经常以为"不蛀就没事",
**需要科普 "非龋损伤也要修"**
-
病因消除:磨损要查夜磨牙 / 楔状缺损要查刷牙习惯,
**单纯修复不解决根因会反复**
## opening 段增量
-
引用诊断:"那次 X 医生检查发现您 X 颗牙的牙齿表面有
[
磨损 / 缺损 / 酸蚀
]
的情况"
-
⚠️ 不要笼统说"龋齿",这是 K02;K03 需要明说"不是蛀牙,是另一种损伤"
## followup 段增量
### 区分龋齿(必带,K03 特化)
> "这种**不是蛀牙(K02),是另一种损伤**,可能是 [咬合磨损 / 刷牙过重导致的牙颈缺损 / 饮食酸蚀] 等;治疗也是修复,但**原因不一样,要处理根因不会反复**。"
### 不痛≠没事(K03 特化)
> "这种损伤**早期通常不痛**,但如果不修,**会持续加深**,最后可能伤到牙神经,那就要做根管;早修便宜得多也简单得多。"
### 时间宽松
"本周末或下周方便的时候约一下,半小时左右"
## 异议增量(K03 特化)
-
**"不疼啊,有必要修吗"**
→ "K03 损伤的特点就是
**早期不痛**
,
**深到神经才痛**
,那时候要做根管;早修一次就好,拖大了步骤会多"
-
**"我又不是蛀牙"**
→ "对,这个
**不是蛀牙**
,是磨损 / 缺损 / 酸蚀 / 裂纹类的非龋损伤;不蛀不代表不用修,
**该补还是要补**
"
-
**"我以前牙齿就这样"**
→ "可能是
**长期慢性形成的**
,您之前没在意没发现;
**现在发现了趁早处理**
,等加深处理就麻烦"
-
**"是不是夜里磨牙的问题"**
→ "
**有可能,磨损 / 隐裂常跟夜磨牙有关**
;您来面诊医生会评估,严重的可能需要
**配夜磨牙垫**
保护"
## 回写要点增量
-
同意约修复 → 「成功约新预约」+ 标注 K03(非龋损伤,需区分 K02)
-
同意 + 怀疑夜磨牙 → 标注"可能需要 OS 垫"
-
拒绝 → 「明确拒绝」+ 标注"科普「非龋损伤也要修」,3 个月后再回访"
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K04-endo/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K04-endo
description
:
K04 牙髓 / 根尖周疾病(根管治疗适应症)。提供根管治疗话术骨架、对应异议(根管很贵/很疼/做完要不要戴冠)、术后注意、儿童不适用警示。当 plan.reasons 中含 dxCode=K04 时加载。
priority
:
50
applies
:
diagnosisCodePrefix
:
K04
allowedPopulation
:
[
teen
,
adult
,
elder
]
# 儿童乳牙根管走特殊术式,不套此 skill
version
:
0.1.0
---
# K04 牙髓 / 根尖周疾病话术包
## 临床素材
-
俗称:
**牙神经发炎**
/
**牙髓炎**
/
**根尖发炎**
(看患者反馈选)
-
治疗:
**根管治疗**
/
**抽神经**
(后者更口语化,前者更专业,看 tone 选)
-
流程:
**至少 2-3 次复诊**
(扩根 → 冲洗 → 充填,有时需上中间药),不能 1 次完成
-
⭐
**后续戴冠**
:根管做完牙容易脆裂,通常建议做牙冠保护;这条单独有意识带出,患者常忽略
-
进展:
**急性发作会剧痛**
(夜间痛、咬东西痛、冷热刺激痛),拖久可能要拔除
## opening 段增量
-
引用诊断:"那次 X 医生检查发现您 X 颗牙的神经已经发炎了,需要做根管"(不用"K04")
-
如 reason 提到剧痛/急性发作,可加:"您当时跟医生反馈过痛,后来情况怎么样?"(共情切入)
## followup 段增量
### 治疗安排说明(必带)
> "根管治疗一般要 2-3 次复诊才能做完,**每次大约 1 小时**。第一次扩根、清理感染,中间可能要等几天,最后封填。"
> "做完根管,医生通常会建议**戴一个牙冠**保护牙齿,因为治疗后的牙比较脆容易裂。"
### 时间紧迫性(K04 特化)
K04 比 K02 紧迫,但仍
**不能恐吓**
:
-
✅ "牙髓发炎一旦开始,自己不会好,建议尽快约,免得急性发作很难受"
-
❌ "不来就拔了" / "再拖就没救了"(base §4 禁)
## 异议增量(K04 特化)
-
**"听说根管很贵/不便宜"**
→ 不报价(base §1.2),引导"具体费用面诊后医生根据牙位和复杂度报"
-
**"听说很疼"**
→ "现在根管都是
**局麻下做**
,治疗过程中是不疼的,
**治疗后 1-2 天可能有酸胀感**
,正常"
-
**"做完能用多久"**
→ 不承诺(base §3 禁),改"保护得好+按时戴冠+定期复查,使用很多年没问题"
-
**"我直接拔了重种行不行"**
→ "您这颗牙根管治好的话,
**保留自己的牙比种植效果更自然**
,医生会给您建议"
-
**"非要戴牙冠吗"**
→ "治疗后的牙没有神经供应,
**变脆容易裂**
;戴冠是医生从专业角度的建议,具体看您牙的情况"
## 回写要点增量
-
同意约根管 → 「成功约新预约」+ 标注牙位 + 预计 2-3 次
-
接受根管但纠结牙冠 → 「考虑中,术后阶段再沟通牙冠」
-
不接受根管,选择拔除 → 「明确拒绝(K04),转介拔牙咨询」
## 儿童差异
-
乳牙根管(乳牙活髓切断 / 牙髓摘除)是
**特殊术式**
,流程跟成人不同
-
frontmatter allowedPopulation 已排除 child,儿童 K04 不应套此 skill;命中时由 population-child skill 主导改写
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K05-perio/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K05-perio
description
:
K05 牙周炎 / 牙周组织疾病(SRP 基础治疗适应症)。提供牙周治疗话术骨架、解释为何要做(刷牙刷不掉牙石)、对应异议(不疼为啥要做/牙齿会变松/反复发)、复查节奏、维护期沟通。当 plan.reasons 含 dxCode=K05 时加载。
priority
:
50
applies
:
diagnosisCodePrefix
:
K05
version
:
0.1.0
---
# K05 牙周炎话术包
## 临床素材
-
俗称:
**牙周病**
/
**牙龈发炎**
/
**牙石问题**
(口语化,患者熟悉)
-
治疗:
**牙周基础治疗 / 龈上洁治 / 龈下刮治(SRP) / 龈下根面平整**
-
⭐ 全口病 vs 局部:K05 多为
**全口或多区段**
,牙位为空("whole")时不要尝试找具体牙位
-
流程:
**分 2-4 次**
(分象限/分次完成,牙周分次做出血少恢复好),每次 30-60 分钟
-
⭐
**维护期**
:基础治疗只是起点,
**之后每 3-6 个月要复查 + 维护洁治**
,这是终身的;不维护会复发
-
风险预后:不治疗会持续骨吸收 → 牙松动 → 牙齿脱落,
**40 岁后失牙第一原因是牙周不是龋齿**
## opening 段增量
-
引用诊断:"那次 X 医生检查发现您
**有牙周病的情况**
,建议做牙周基础治疗"(避免说"很严重"除非 reason 明确写了)
-
自然引"出血/口臭"症状:"您平时刷牙是不是有时候会出血?有口气问题?"(共情,如果患者反馈有就顺着说)
## followup 段增量
### 解释为何要做(必带,患者最常迷茫)
> "牙周病主要是牙龈下面的**牙石**,普通刷牙清不到,需要医生用专业器械分次清理,叫**牙周基础治疗**或 **SRP**。"
> "做完后,**牙龈出血会改善,口气也会变好**,长期能保住您的牙齿。"
### 流程说明(必带)
> "牙周治疗一般要分**2-4 次**完成,每次 30-60 分钟,分区做出血会少很多。"
> "做完后**每 3-6 个月要回来维护一次**,跟洗牙差不多,这是控制牙周病的关键。"
### 时间相对宽松
K05 不像 K04 急,可宽时间窗:"本周或下周哪天方便?"
## 异议增量(K05 特化)
-
**"我又不疼,牙没事"**
→ 牙周病的特点就是
**早期不疼**
,等到痛了已经牙松了,
**早治才能保住牙**
-
**"不就是洗个牙吗?医院洗一次就行"**
→ 普通洗牙清的是
**牙龈上面**
的牙石,牙周病的牙石
**藏在牙龈下面**
,要专业 SRP 才能清理
-
**"做完牙会不会松"**
→ "
**做之前**
牙石把牙龈撑住所以感觉不松,清理后短期可能感觉到松,但
**真实情况就是这样**
,医生会评估能不能保住"(老实说,不忽悠)
-
**"听说牙周治疗会反复"**
→ "复发主要是
**没有坚持维护**
;基础治疗 + 3-6 个月维护洁治,可以稳定很多年"
-
**"年纪大了无所谓"**
→ 不接受这个 framing,"老人保牙咀嚼力直接影响身体健康,
**有牙能吃饭比任何保健品都重要**
"
## 回写要点增量
-
同意约牙周基础 → 「成功约新预约」+ 标注预计 2-4 次
-
同意做但要排期 → 「考虑中,1-2 周跟进」
-
拒绝/觉得没必要 → 「明确拒绝(K05)」+ 标注"建议下次主诊面谈再次科普"
## 长期维护强调(close 段)
-
close 段务必带"做完之后还要回来维护,我们会按时提醒您",建立长期关系认知
## 老人(elder)交叉
-
老人 K05 高发,可强调"咀嚼力↔身体健康";由 population-elder skill 协同节奏放慢
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K06-gum-alveolar/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K06-gum-alveolar
description
:
K06 牙龈 / 牙槽嵴疾患(牙龈增生 / 牙龈瘤 / 牙槽嵴病变)。多需要外科或综合处置,常跟 K05 牙周炎叠加。可能需要病理活检,谨慎科普避免恐慌。
priority
:
50
applies
:
diagnosisCodePrefix
:
K06
version
:
0.1.0
---
# K06 牙龈 / 牙槽嵴疾患话术包
## 临床素材
-
俗称(按具体类型):
-
牙龈增生 / 牙龈长了一块 / 牙龈鼓出来
-
牙龈瘤 / 牙龈上的肿块(⚠️ 不说"瘤"字面让患者紧张,改"牙龈上的肿物")
-
牙槽骨问题 / 牙床问题
-
处置:
**面诊评估**
→ 必要时
**手术切除 + 病理检查**
-
⭐
**K06 常需要病理活检**
— 切下来送病理,绝大多数是良性,
**话术里不要主动提"癌"**
,但也不能拍胸脯说"绝对没事"
## opening 段增量
-
引用诊断:"那次 X 医生检查时发现您牙龈上有
[
一块增生 / 一个肿物 / 不太正常的地方
]
"
-
⚠️ 措辞中性,
**不渲染严重,也不假装没事**
## followup 段增量
### 强调"评估为主"
> "这种情况**建议来面诊一下**,医生看一下具体的大小、性质,**绝大多数是良性的**,但要面诊才能确定;有些可能需要做一个小处理 + 送检查"
### 不要主动提"病理 / 活检 / 肿瘤 / 癌"等词
-
患者会脑补;让医生面诊时再讲
-
仅当患者主动问"会不会是 X"时如实回答"绝大多数是良性的,具体面诊医生看"
### 时间偏紧
-
K06 不是急症,但
**优于普通龋齿排队**
;"近期方便的时候约一下,本周或下周都行"
## 异议增量(K06 特化)
-
**"我看着没什么大事"**
→ "您看着确实可能是良性的,
**很多 K06 都是良性的**
,但
**需要医生面诊评估**
,确认一下性质;面诊半小时左右"
-
**"会不会是肿瘤"**
→ 不回避也不夸大;"绝大多数是良性增生,但
**确实要面诊确认**
,如果需要可以
**取一小块化验**
;您不用过度担心,但建议来评估"
-
**"我朋友说就是上火"**
→ "
**牙龈增生有些跟炎症 / 上火有关**
,有些跟其他因素有关;
**真正确定要医生面诊**
,不能凭外观判断"
-
**"我前段时间洗过牙就好了一些"**
→ "
**洗牙能改善牙龈炎症**
,但 K06 这种增生 / 肿物
**单靠洗牙不够**
,还是要面诊评估"
## 回写要点增量
-
同意约面诊 → 「成功约新预约」+ 标注 K06 评估
-
决定再观察 → 「考虑中,2-4 周跟进」(K06 比 K02 优先,缩短跟进周期)
-
拒绝 → 「明确拒绝」+ 标注"建议向患者再次科普 K06 评估必要性"
-
提到"反复增大 / 出血 / 疼痛" → 标注"紧急面诊建议"
## 跟 K05 协同
-
K06 + K05 同时命中是高发组合(牙龈炎/牙周炎 + 增生),按 K05 主导,K06 作为"另外还有一块需要单独看的"
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K07-ortho/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K07-ortho
description
:
K07 颌面发育异常(错颌畸形 / 正畸适应症)。长周期项目(1-3 年),儿童青少年是黄金窗,成人也能做但更慢。不报价,不承诺时长,引导面诊评估。
priority
:
50
applies
:
diagnosisCodePrefix
:
K07
version
:
0.1.0
---
# K07 颌面发育 / 错颌畸形(正畸)话术包
## 临床素材
-
俗称:
**牙齿不齐**
/
**地包天**
/
**龅牙**
/
**牙缝大**
/
**咬合不好**
/
**戴牙套**
(矫正俗称)
-
处置:
**正畸 / 矫正 / 戴牙套 / 隐形矫正**
-
流程:
**1-3 年周期**
,
**每月复诊 1 次**
;包含:面诊评估 → 拍片取模 → 方案设计 → 戴矫治器 → 月复诊 → 保持期
-
⭐
**黄金窗 8-12 岁**
(儿童乳牙换牙期 / 牙列发育期);
**14-17 岁次之**
;成人也能做但骨头硬移动慢
-
价格区间极大(几千到十万),
**严禁报价**
-
方案差异:
**金属托槽 / 陶瓷托槽 / 隐形矫正(隐适美 / 时代天使等)**
,适应症医生评估
-
⚠️ "戴牙套不好看 / 影响吃饭 / 周期长" — 患者最大三个顾虑
## opening 段增量
-
引用诊断:"那次 X 医生检查时,提到您
[
牙齿排列不齐 / 咬合的问题
]
,建议考虑正畸"
-
儿童家长场景(由 population-child 主导):"X 小朋友的牙齿现在 X 阶段,医生提到现在是矫正比较合适的时机"
## followup 段增量
### 强调"先评估"(必带,正畸不能盲做)
> "正畸是个**长期项目**,**先来面诊评估**,医生看一下您的具体情况、需要做什么方案、大概多长时间;评估这一步不收治疗费(⚠️ 政策无字段不主动说免费)"
### 黄金窗强调(儿童 / 青少年场景)
> "8-12 岁是儿童矫正的**黄金时期**,牙齿移动快、效果好;[X 小朋友] 现在的年纪正好;如果再晚一些处理会更复杂一些。"
### 成人场景(adult)
-
不要让成人觉得"我太晚了":
> "成年人也完全可以做,**只是周期可能比儿童长一些**;现在很多成年人在矫正"
### 不报价、不承诺时长
-
❌ "矫正大概 1 万 / 2 万"
-
❌ "您这种情况大概 18 个月"
-
✅ "
**具体方案 + 价格 + 周期**
,医生面诊评估后给您一个完整的方案讲解"
## 异议增量(K07 特化)
-
**"我都成年了还能矫吗"**
→ "完全可以,
**成人正畸越来越常见**
;只是周期可能稍长,效果一样好"
-
**"戴牙套丑 / 影响我工作"**
→ "现在有
**隐形矫正**
(透明牙套)的方案,从外观几乎看不出来;具体哪种适合您,医生会评估"
-
**"听说很贵 / 大几万"**
→ 不报价;"正畸的费用跨度比较大,
**金属、陶瓷、隐形差别不小**
,
**面诊医生根据您的具体情况给方案和价格**
"
-
**"周期太长了,要 2 年"**
→ "正畸是慢工细活,
**确实需要 1-3 年**
,具体时间面诊评估;一旦做完,牙齿排齐
**一辈子受益**
;您可以来听一下医生讲方案再决定"
-
**"我朋友说要拔牙才能矫"**
→ "
**部分情况确实要拔牙腾空间**
,但
**也有不拔牙的方案**
;医生面诊评估您的牙弓骨条件后给具体建议"
-
**"我们家小朋友说不想戴"**
→ 共情;"很多小朋友刚开始抗拒,
**戴 1-2 个月就习惯了**
;您方便的话先带 X 小朋友来认识医生 + 看一下牙套样子,
**不一定要立刻开始**
"
## 回写要点增量
-
同意约面诊评估 → 「成功约新预约」+ 标注 K07 正畸评估
-
同意但要看时间 → 「考虑中,2 周跟进」
-
决定不做 → 「明确拒绝」+ 标注原因(费用/周期/外观)
-
已在外院做 → 「已在外院」+ 关闭
## 老人(elder)交叉
-
老人正畸罕见,本 skill 中标(K07 任意年龄都加载)+ population-elder skill 主导节奏
-
老人正畸通常是修复前预先排齐,
**主要是修复方案设计**
,不是单纯美观;话术由 population-elder 改写为"先看一下整体修复方案"
## 协同
-
儿童 K07 + population-child:由 population-child 主导改写称谓 / CTA,本 skill 提供专业素材
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K08-edentulism/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K08-edentulism
description
:
K08 牙列缺损 / 牙列缺失场景。提供种植 / 义齿 / 牙桥话术骨架、对应异议(价格/做完能用多久/年纪大了还种吗)、不报价铁律、儿童禁用警示(乳牙脱落非疾病)。当 plan.reasons 含 dxCode=K08 时加载。
priority
:
50
applies
:
diagnosisCodePrefix
:
K08
allowedPopulation
:
[
teen
,
adult
,
elder
]
# 儿童 K08 多为乳牙脱落,非疾病召回应在 SQL 层就排除
version
:
0.1.0
---
# K08 缺牙(缺失修复)话术包
## 临床素材
-
俗称:
**缺牙**
/
**缺一颗**
/
**掉的那颗**
(直接 + 口语)
-
治疗:
**种植牙**
/
**烤瓷桥(牙桥)**
/
**活动义齿**
(三种主流方案,适用条件不同医生定)
-
时间窗:
**缺牙 3-6 个月内启动修复最佳**
,拖太久邻牙倾斜、对颌伸长,后期种植难度+费用都上去
-
⭐
**绝对不能报价**
:种植牙价格区间极大(几千到几万),含品牌/位置/骨量/上下结构差异,
**任何价格暗示都会出大事**
-
缺牙不补的危害:邻牙倾斜、对颌牙伸长、咬合错位、咀嚼偏侧 →
**不仅是少颗牙的事**
## opening 段增量
-
引用诊断:"那次 X 医生检查时,看到您 X 颗牙缺了,提醒您考虑做修复"
-
时间:如 reason.triggerDate 较久(>180 天),可加"算下来已经 X 个多月了"
-
单颗 vs 多颗:1 颗用"那颗",2-3 颗用"那几颗",4+ 颗考虑"半口/全口"叫法
## followup 段增量
### 修复方案给方向不给细节(必带)
> "缺牙的修复一般有**几种方案**:种植牙、烤瓷桥、活动义齿,具体哪种适合您,医生**面诊评估骨条件**后会给您建议。"
> "**这次只是面诊评估,不需要做任何操作**,医生看一下情况,跟您说几种方案的优缺点。"
### 时间窗紧迫(K08 特化,但温和)
> "缺牙时间越长,**旁边的牙会慢慢倒过来**,对面的牙会**长长**,后面再修复需要先处理这些,会麻烦一些。早一点评估好处理。"
-
❌ 不能恐吓"再拖就种不了"
### 不报价铁律 ⭐
-
❌ "种植大概 X 千 / X 万" — 严禁
-
❌ "我们这种植性价比高" — 严禁
-
❌ "活动义齿便宜些" — 严禁
-
✅ "具体方案和费用,医生
**面诊后会给您一个明细**
,看您选哪种方案,种植和义齿差别不小"
## 异议增量(K08 特化)
-
**"我都这么大年纪了还种啥"**
→ "
**保持咀嚼力对老年人健康很重要**
,医生会根据您的骨条件评估能不能做,做不了也有义齿的方案;先看一下不亏"
-
**"种了能用几年"**
→ "保养得好,
**配合定期复查,可以使用很多年**
;具体面诊医生会跟您讲护理"(不给具体年数承诺)
-
**"听说挺贵的"**
→ "种植牙费用确实跨度比较大,
**面诊医生会按您牙位/骨头条件给具体方案和价格**
,有不同选择;您先来评估,不评估就没有具体数"
-
**"做了会不会疼"**
→ "种植是局麻下做,
**过程中不疼**
,术后 1-2 天可能有肿胀,正常;现在有微创术式,不舒服感会更少"
-
**"我先用活动义齿凑合"**
→ 尊重选择;"活动义齿是个方案,但
**长期咀嚼舒适度种植会好很多**
,您可以面诊时听医生比较"
-
**"我去隔壁牙科看过价格"**
→ "好的;我们这边面诊评估和方案是免费的(⚠️ 仅当诊所政策允许,无字段时不主动提),您方便的话来对比一下医生方案"
## 回写要点增量
-
同意约面诊评估 → 「成功约新预约」+ 标注 K08 面诊
-
考虑中 → 「考虑中,1-2 周跟进」
-
决定不做 → 「明确拒绝」+ 标记原因(费用/年龄/外院)
-
已在外院做了/在做 → 「已在外院治疗」+ 关闭召回
## 老人(elder)交叉强化
-
K08 + elder 极常见组合,population-elder skill 会强调节奏 + 家属同意,本 skill 保留临床素材主导
-
异议"年纪大了还种啥"是高频,本 skill 已 cover
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/diagnosis/K09-jaw-cyst/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
diagnosis-K09-jaw-cyst
description
:
K09 颌骨囊肿 / 颌骨其他疾病。少见但高风险场景,需要外科会诊 / 手术 / 病理活检。话术高度谨慎,不在电话里详细科普,核心目的"约最近时间面诊外科"。
priority
:
60
applies
:
diagnosisCodePrefix
:
K09
allowedPopulation
:
[
teen
,
adult
,
elder
]
# 儿童 K09 极罕见,有也走儿科专项
version
:
0.1.0
---
# K09 颌骨囊肿 / 颌骨疾病话术包
## ⭐ 核心定位
**K09 是 K00-K09 中风险最高的场景**
:可能需要手术、可能需要病理。话术必须
**高度谨慎**
:
-
❌
**不在电话里详细科普方案**
(医生面诊讲)
-
❌
**不主动提"癌 / 肿瘤 / 切除手术"**
(吓患者)
-
❌
**不淡化**
("没什么大事" — 不负责任)
-
✅
**核心目的:把患者请来面诊,医生当面讲**
-
✅ 强调"
**外科 / 颌面外科**
医生面诊"(让患者知道严肃)
-
✅ 时间紧迫感比 K01-K08 高,
**优先安排**
## 临床素材
-
俗称:
**颌骨问题**
/
**下巴(上颌)有囊状的东西**
/
**拍片发现的影像问题**
-
处置:
**外科 / 颌面外科面诊**
→
**手术摘除 + 病理检查**
(大部分)
-
紧迫度:
**亚急性**
,不是急救,但
**不能拖几个月**
## opening 段增量
-
引用诊断:"那次 X 医生检查时发现您
[
颌骨上有囊状的影像 / 下巴有不太正常的地方
]
,建议您
**尽快回来由外科医生面诊**
"
-
⭐ "外科医生面诊"措辞要说出来 — 让患者知道这不是普通牙问题
## followup 段增量
### 措辞模板(必带,严格)
> "这种情况建议**近期回来由我们外科医生面诊评估**,看一下具体的范围、性质,**绝大多数是良性的**,但**确实需要面诊确定**,可能需要做一些处理。"
> "时间上**尽量这 1-2 周内**约一下,**不影响您日常,但也不要拖太久**。"
### 时间安排
-
工作日上午医生集中接外科会诊:"周一到周五上午可以专门安排外科,您看哪天方便?"
-
给具体方向 + (示例):"您看 X 上午这个方向行不行,我先帮您按
[
周二上午(示例)
]
登记,具体时段诊所确认后短信告诉您"
### 不在电话里讲具体方案
-
❌ "可能要做手术摘除"
-
❌ "可能要送病理"
-
✅ "
**具体方案医生面诊讲**
,这边电话里不能给您细看"
## 异议增量(K09 特化)
-
**"是不是很严重 / 是不是肿瘤"**
→ 不夸大也不淡化;"绝大多数颌骨囊肿是
**良性的**
,但
**确实需要面诊确认**
;您
**不必过度担心**
,也
**建议尽快来面诊**
"
-
**"我看着 / 我感觉没什么"**
→ "K09 大部分
**早期没有感觉**
,
**主要靠影像发现**
;但因为发现了,
**就该面诊评估**
,这是负责任的处理方式"
-
**"我去综合医院看吧"**
→ 尊重 + 给选择;"完全可以,综合医院的口腔颌面外科也合适;您
**方便来我们这边**
也可以,我们这边有 X 医生(⚠️ 仅 reason.triggerDoctor 有时填,否则改为"我们外科医生")"
-
**"我想跟家里人商量一下"**
→ 配合(老人场景常见);"完全可以,
**建议尽快**
商量 + 来面诊;您和家人商量好了告诉我们,这边帮您留外科时间"
-
**"上次拍的片子能不能再看一下"**
→ "可以,
**带上片子来面诊**
,医生会综合看;
**面诊比单看片子更准**
"
## 回写要点增量
-
同意约面诊 → 「成功约新预约」+
**标注高优先级 K09 + 外科**
-
决定再观察 → 「考虑中,
**1 周后必须再联系**
」(K09 跟进窗很短)
-
决定去其他医院 → 「已在外院 / 转外院」+
**标注 K09 转出,关注后续**
-
完全拒绝面诊 → 「明确拒绝」+ 升级标记"K09 拒绝面诊,主管核查后人工跟进"
## 客服执行特别注意
-
⚠️
**K09 不能像 K02 那样随便结案**
,任何"考虑中"都要 1 周内复联
-
⚠️
**不要在通话末尾说"祝您一切顺利"**
— 显轻飘,改"那您先和家人商量,我们等您消息"
## 老人(elder)交叉
-
K09 + 老人,population-elder skill 主导节奏,本 skill 提供素材;
**家人参与决策必须主动提**
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/playbooks/objection-bank/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
playbook-objection-bank
description
:
异议应对总库,scenario × population 跨场景共用的高频异议(再考虑 / 价格 / 没时间 / 已在外院 / 不打扰)+ 客服回复范式。LLM 在 objection 段必须按
### A./B./C. 子标题分块,挑 3-4 个最适合本次召回的异议覆盖。本 skill 全场景加载(priority 200,装配在最后)。
priority
:
200
applies
:
{}
# 全场景加载,跨 scenario × diagnosis × population
version
:
0.1.0
---
# 异议应对总库(playbook)
## ⭐ 客服外呼最常见的 8 种异议(按出现频率排序)
### 1. "我再考虑考虑"(出现率 ~40%)
**底层动机**
:还没下决心 / 怕被推销 / 想给自己留缓冲
**应对范式**
:
-
✅ "完全理解,这种决定确实要想清楚。这样,我先帮您把医生时间留到
[
周 X 上午或周 Y 晚上
](
示例
)
,您想好告诉我们,如果不来我帮您取消就行,没关系。"
-
✅ "好的,您慢慢想;
**X 月底之前**
告诉我们都行(给一个软窗口)"
-
❌ "您再考虑就晚了" / "今天定下来还能 X" / "下次价格可能变"(全是销售套路)
### 2. "最近真的没时间 / 工作太忙"(出现率 ~25%)
**底层动机**
:时间确实紧 / 也可能是软拒绝
**应对范式**
:
-
✅ "理解,
**周末或者工作日晚上**
(到 8 点前)我们都开,您看哪个时段方便?"
-
✅ "如果近期实在不行,
**下个月也可以**
;您方便了告诉我们,我们提前帮您留个时间"
-
❌ "您再忙也得有健康" / "X 病不能等"(说教)
### 3. "听说挺贵的 / 多少钱"(出现率 ~20%)
**底层动机**
:价格敏感 / 想砍价
**应对范式**
:
-
✅ "
[
X 治疗类
]
的费用,
**医生面诊后会给您一个明细**
,根据牙位 / 方案 / 材料不同会有差异;您来评估之后,自己看着选合适的方案"
-
✅ "我这边电话里给不到准确的报价,这个由医生定,
**面诊评估不收费**
(⚠️ 仅当政策允许时,无字段不主动说免费)"
-
❌ "我们家性价比高" / "我们打折活动" / "X 千就够"(base §1 严禁报价/活动)
### 4. "已经在别的医院 / 已经在做了"(出现率 ~15%)
**底层动机**
:已选择其他诊所
**应对范式**
:
-
✅ "好的
[
X 先生
]
,那您方便的话我帮您把我们这边的召回记录关一下,
**祝您治疗顺利**
" + 回写「已在外院」
-
✅ "如果您后续想参考一下其他医生的方案,
**也欢迎来我们这边对比**
;不来也没关系"
-
❌ 不追问"哪家医院 / 多少钱" — 不合规也不礼貌
-
❌ 不拉踩其他医院
### 5. "不要打了 / 别再联系我"(出现率 ~5%,但必须严肃对待)
**底层动机**
:不希望被联系
**应对范式**
:
-
✅ "
**好的 [X 先生],我立刻帮您加入不打扰名单**
,后续不会再联系您;
**祝您一切顺利**
"
-
✅ 立即结束通话,
**回写「不打扰」标记**
-
❌ 不解释 / 不挽留 / 不再次推销(任何挽留都让人更烦)
-
❌ 不说"我们是为您好"
### 6. "我不记得在你们诊所看过 / 怎么有我电话"(出现率 ~5%,新客高发)
**底层动机**
:对诊所无印象 / 警惕陌生电话
**应对范式**
:
-
✅ "我看到我们这边登记您
[
X 月 X 日
](
具体引
reason 触发日期)到过
[
X 诊所名
]
做过
[
X 检查/治疗
]
;可能是
[
家人帮约 / 公司体检 / 团购
]
?"(给具体场景帮患者回忆)
-
✅ 如确认从未到过:"那可能是登记错了,我帮您从我们系统里删掉" + 回写「不打扰」
-
❌ 不强辩 "您一定来过"
-
❌ 不催"您仔细想想"
### 7. "我没那么严重 / 不疼,有必要吗"(出现率 ~20%,多见于 K02/K05/K07)
**底层动机**
:无症状 → 觉得不必要
**应对范式**
:
-
✅ "
[
K02/K05/K07 类
]
的特点确实是
**早期不疼**
,但
**会持续发展**
;
**早处理代价小**
,等出现明显症状代价会大一些"
-
✅ 给具体临床事实佐证:"医生 X 月 X 日看到 X 牙的 X 问题"(从 reason 拿)
-
❌ 不恐吓"再不来就晚了" / "您再拖就没救了"
-
❌ 不用一般化恐吓("X 病很严重") — 要具体到这个患者
### 8. "我跟家人商量一下"(出现率 ~10%,老人 / 儿童家长高发)
**底层动机**
:重大决策需要家庭参与
**应对范式**
:
-
✅ "完全理解,
**重要决定**
要跟家人商量;您和家人商量好了告诉我们,我们这边帮您留好时间"
-
✅ 老人场景主动提:"
**家人方便的话可以一起来听一下医生方案**
"
-
❌ 不催"今天能不能定" — 会让患者立刻挂电话
---
## 用法指引
LLM 在生成 objection 段时:
1.
**挑 3-4 个**
最适合本次召回(scenario / population / dx)的异议
2.
**按 ### A./B./C. 子标题分块**
(base §5 已强制)
3.
**每个异议下面 1 段 blockquote `> "..."`**
给客服话术
4.
可选
`> → 提交结果选「xxx」`
一行,指明客服回写动作
5.
跟 scenario / population skill 内已 cover 的具体异议
**避免重复**
;那些是细分,这里是通用兜底
### 优先级建议(按 scenario × population)
-
**启治召回 + 成人**
:1.再考虑 / 2.没时间 / 3.价格 / 7.不严重
-
**启治召回 + 老人**
:1.再考虑 / 8.跟家人商量 / 7.不严重 / 4.已在外院
-
**启治召回 + 儿童家长**
:1.再考虑 / 7.不严重 / 8.跟家人商量(儿童特化版异议见 population-child skill)
-
**启治召回 + 新客**
:6.不记得诊所 / 4.已在外院 / 1.再考虑
-
**K08/K07 + 任意**
:3.价格(高频)+ 1.再考虑 + 2.没时间
-
**K05/K06/K09 + 任意**
:7.不严重 + 8.跟家人商量 + 1.再考虑
### 跟 K-code / population skill 的协同
-
diagnosis SKILL.md 内的"异议增量"是
**该 dx 特化**
的具体异议(如 K08 "做了能用几年" / K04 "听说很疼")
-
本 playbook 是
**跨场景通用兜底**
+ 优先级指引
-
异议选 3-4 个时,
**优先用 dx 特化**
,通用兜底补缺
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/playbooks/safety-rules/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
playbook-safety-self-check
description
:
Safety 规则的"描述版" — 让 LLM 在生成前就自检 6 条 close 段约束 + 禁词 + 承诺式表述,主动规避后置 safety gate 命中(命中要走 fallback 损失质量)。本 skill 全场景加载(priority 250,装配在最末)。机器规则继续 TS(call.ts safetyRules),不挪 yaml。
priority
:
250
applies
:
{}
version
:
0.1.0
---
# Safety 自检清单(LLM 输出前主动核查)
⚠️ 以下规则是
**后置硬约束**
,LLM 输出后会用代码扫描;
**命中 block 类规则 → 整段输出作废 → 走 fallback 模板**
(质量下降)。
**所以每次输出前,**
自己
**先按这个清单核查一遍**
,避免命中。
## 1. 全段:禁词扫描(block — 命中即作废)
任一段(opening/followup/objection/close)含以下任一词,
**整次输出失败**
:
-
`一定能`
、
`保证`
、
`绝对`
、
`百分百`
、
`100%`
(医疗承诺)
-
`亲爱的`
(销售化称呼)
-
`便宜`
、
`促销`
、
`折扣`
、
`免费送`
(销售化)
→ 检查方法:全文 Ctrl+F 扫一遍,有则改泛指或删除。
## 2. close 段:不能加粗具体时间(block — 命中即作废)
❌
`**周六上午10点**`
/
`**本周五下午**`
/
`**明天上午9点**`
-
检测正则:
`/\*\*[^*\n]*(?:周[一二三四五六日天]|\d+\s*(?:点|:|:))[^*\n]*\*\*/`
-
命中即整次输出失败
正确写法(任选):
-
✅
`周六上午10点(示例)`
← 不加粗 + (示例) 后缀
-
✅
`周六上午这个方向`
← 方向词替换具体时间
→ 检查方法:close 段所有
`**...**`
加粗块,看有没有"周X / 数字 + 点"。
## 3. close 段:不能用承诺式表述(block — 命中即作废)
PAC 没有排班 API,所以
**没有任何时间是"已经定下来的"**
;不能用以下措辞:
-
❌
`已为您约好`
/
`已成功预约`
/
`已为您预约`
/
`已经为您约`
/
`已替您预约`
-
❌
`约定本`
/
`敲定本`
/
`安排好了`
/
`已经预约`
正确写法:
-
✅
`我先按 X 帮您登记,具体时段以诊所排班为准`
-
✅
`我先帮您留 X 时间方向,稍后跟前台确认后短信通知您实际时间`
## 4. close 段:必须含"待确认"语义短语(warn — 不阻断但提示)
close 段缺以下任一短语会触发 warn 日志,不影响输出但应该带:
-
`以诊所排班为准`
/
`排班为准`
-
`稍后跟前台确认`
/
`跟前台确认`
/
`稍后跟诊所确认`
-
`稍后短信确认`
/
`排班确认后告知`
/
`排班确认后短信`
-
`稍后短信通知您实际`
/
`具体时段以`
/
`具体时间以`
→ 检查方法:close 段是不是有一句"待确认/排班相关"的弱化措辞。
## 5. objection 段:必须 ### A./B./C. 子标题分块(warn — 不阻断)
❌ 把 3 个异议合并成一长段
❌ 用
`- xxx`
bullet 列异议
✅ 正确:
```
### A. "我再考虑考虑"
> "客服话术..."
### B. "最近没时间"
> "客服话术..."
```
## 6. close 段:必须含具体时间数字(warn — 不阻断)
close 段如果完全没数字(没"X 点"也没"X 月 X 日"也没"X 周")会触发 warn。
→ 给一个示例时间即可(如"周六上午10点(示例)")。
---
## 自检顺序建议
输出每段前依次问自己:
1.
**opening / followup**
:有没有引用至少 3 条 user prompt 给的事实?有没有「身份不符」的具体名词?
2.
**objection**
:是不是 3-4 个
`### A./B./C.`
子标题?每个有 blockquote
`>`
?
3.
**close**
:
-
加粗块有没有具体时间词?(有就去掉加粗 + (示例) 后缀)
-
有没有"我已为您约好"等承诺词?(有就改"我先按 X 登记")
-
有没有"以诊所排班为准 / 稍后跟前台确认"等弱化短语?(没有就加 1 句)
-
有没有
`**回写要点**`
+ 2-4 条 bullet?
4.
**全段**
:Ctrl+F 扫禁词清单(§1)
---
## 跟代码 safety rule 的关系
本 SKILL.md 是
**给 LLM 看**
的描述版自检清单,
**机器规则**
实际在
`apps/pac-service/src/modules/ai/calls/draft-plan-script/call.ts`
的
`safetyRules`
数组里(强类型
`SafetyRule<TOutput>`
,runtime 性能 + zod 校验)。
**两者必须同步**
:改了 call.ts 的 safetyRules,也要更新本文件;否则 LLM 自检会过但后置 gate 命中。
新加规则 PR review 时双向检查。
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/adult/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
population-adult
description
:
患者年龄 18-64 岁成年人(baseline 主流)。沟通对象患者本人,直接专业,时间紧凑,异议偏价格 / 时间平衡。tone 默认 professional;熟客可切 warm。当 patient.age 在 18-64 时加载。此 skill 较薄,大部分依赖 base + diagnosis + relationship。
priority
:
100
applies
:
ageMin
:
18
ageMax
:
64
version
:
0.1.0
---
# 成年人 (18-64) 沟通包(baseline)
## 对话对象
-
患者本人,
**直接专业**
-
不需要"找家长"等额外路径
## 称呼模式
-
✅ "X 先生" / "X 女士"(base 「患者.称呼」原样照用,不改写)
## tone 默认
-
**professional**
(专业稳重) — 默认
-
熟客(relationship-returning 接管时)可切
**warm**
-
高紧迫场景(K04 急性发作、K09 颌骨等)可切
**urgent**
## 沟通节奏
-
**紧凑高效**
:成年人通话耐心有限,3-5 分钟内讲清核心
-
避免冗长寒暄,1-2 句切入正题
-
时间选项给具体且
**贴合白领时段**
:工作日早上 / 晚上 / 周末
-
⚠️
**不要默认"工作日 19:00 后"**
(base §1.1 禁:无字段不能假设偏好时段)
-
✅ "本周末或下周工作日晚上,您看哪个方便?"(给选项让患者自选)
## opening 段增量
-
简洁直接:"X 先生您好,我是 X 诊所客服,主要是想跟您同步一下上次到店时医生发现的情况"
-
不要"亲切寒暄"3 句以上
## followup 段增量
-
信息密度高,
**说清楚 + 给选项**
即可,不要绕弯
-
多颗治疗 / 复杂方案:1-2 句提骨架 + "具体面诊医生跟您细说"
## 异议增量(成年人特化)
-
**"我最近工作忙"**
→ "理解;您看周末或下班后,哪个时段方便?"
-
**"我看看时间再回复"**
→ "好的;一般近期处理对治疗会更顺一些,您方便了告诉我们一下,我们留好时间"
-
**"我去其他诊所看看"**
→ 尊重;"那您看好之后,有需要参考医生方案或者比对一下的话也欢迎来"
## 这个 skill 故意保持薄
-
成年人是 baseline,大部分话术由 base + diagnosis + relationship skill 决定
-
这里只
**校准 tone / 节奏 / 称呼**
三件事
-
不要在此 skill 加太多场景化内容(否则跟 diagnosis skill 重叠)
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/child/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
population-child
description
:
患者年龄小于 14 岁的儿童场景。沟通对象切换为家长(不是患儿本人),称呼模式 / CTA / 临床禁忌完全不同。给定 tone=warm 默认,强调亲和力。当 patient.age <= 13 时加载;此 skill 应主导改写 opening / followup 段的称呼和 CTA。
priority
:
100
applies
:
ageMax
:
13
version
:
0.1.0
---
# 儿童 (<14) 沟通包
## ⭐ 对话对象切换(铁律)
-
**接通后第一句话先确认是否家长接听**
,不直接对孩子讲临床方案
-
✅ "您好,是 X 小朋友的家长吗?"
-
✅ 如不是家长接听:"那您方便帮忙转告家长,或者告诉我家长方便的时间我们再回拨?"
-
❌ 不能直接念诊断给孩子("您家小朋友 K02 龋齿" — 孩子听不懂,家长不在场也无法决策)
## 称呼模式(改写 base 「患者.称呼」)
-
✅ "X 小朋友的家长您好"
-
✅ "X 妈妈您好" / "X 爸爸您好"(性别字段如标注则用)
-
❌ 不用"X 先生/女士"(儿童本人称谓)— 整体替换为家长称谓
-
❌ 不用"亲""宝"(base §3 禁词)
## tone 默认
**warm**
— 语速慢,关怀向,叙述带温度
## 临床禁忌(K-code 交叉)
-
**K08 缺牙**
:儿童缺牙多为
**乳牙脱落换牙**
,
**不是疾病召回**
,应在 SQL 层已排除。如仍触发,优先讲"换牙过程中观察",
**不要套成人种植/义齿话术**
-
**K04 根管**
:儿童乳牙根管是
**乳牙活髓切断 / 牙髓摘除**
,
**特殊术式**
,不能用成人"根管 2-3 次复诊"骨架(diagnosis-K04 frontmatter 已排除 child)
-
**K07 正畸**
:儿童 8-12 岁是
**黄金正畸窗口**
,可主动建议"是不是顺便约一下正畸科医生评估,牙列发育期处理更轻松"
-
**K02 龋齿**
:乳牙龋可选"暂观察等换牙"或"窝沟封闭"或"补",由医生面诊定,客服只引导面诊不预判
## opening 段增量
-
自报家门后:"主要是想跟您聊聊您家小朋友(姓 X)上次到店的情况"
-
引用诊断:"那次 X 医生检查时,提到 X 小朋友的 ..."
## followup 段增量(降门槛改写)
-
"
**面诊评估不需要做什么**
,医生看一眼,跟您说明白现在是什么情况,后续要不要处理、什么时候处理"
-
"儿童牙齿很多情况是
**观察 + 定期复查**
,不一定都要立刻处理"
-
时间偏好:
**周末 / 寒暑假**
(默认家长上班 + 孩子上学日难安排)
-
✅ "您看这周末或下周末方便带 X 小朋友过来吗"
## 异议增量(儿童家长特化)
-
**"孩子说不疼,不想去"**
→ "孩子的感觉是这样,但
**乳牙问题影响后面的恒牙**
,
**早看早安心**
;一般来现场玩一下不会很抗拒,我们医生也熟悉跟小朋友沟通"
-
**"上学没时间"**
→ "完全理解,可以约
**周末或者放学之后**
;一般 30 分钟左右"
-
**"我跟孩子他爸再商量下"**
→ "好的,您方便商量好了告诉我们,我们这边帮 X 小朋友把检查时间留着"
-
**"孩子怕看牙"**
→ "我们诊所有小朋友专用的椅子(⚠️ 仅当确实有时说,无字段时改"医生很有经验,看小朋友比较多");可以
**第一次只是过来认识一下医生**
,不做操作"
-
**"是不是想让我们种牙/做牙套"**
→ 安抚:"小朋友这个年纪不会做种植,
**主要是检查 + 必要时做基础处理**
;具体医生评估后给您讲"
## 回写要点增量
-
同意约 → 「成功约新预约」+ 标注"儿童 + 家长陪同"
-
家长要商量 → 「考虑中,3-5 天家长联系」
-
拒绝(觉得没必要)→ 「明确拒绝」+ 标注"建议下次复查再次科普"
-
监护人电话不通 → 「家长未联系上,改期回拨」
## 客服执行注意点
-
儿童名字念全名("张 XX 小朋友")而非姓 + 称谓("张小朋友"听着像通称)— 加深个性化
-
如能从 reason 拿到孩子年龄,可在自然语境引用("X 岁正好是换牙的阶段")
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/elder/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
population-elder
description
:
患者年龄 65 岁以上老年场景。慢节奏 / 大字 / 复述确认必带;家属可能参与决策(建议提"是否需要跟家人商量");牙周 / 缺牙高发,口齿沟通可能慢。tone warm 默认。当 patient.age >= 65 时加载。
priority
:
100
applies
:
ageMin
:
65
version
:
0.1.0
---
# 老年 (>=65) 沟通包
## 对话对象
-
患者本人,但
**家人可能参与决策**
-
大方案(种植 / 多颗治疗 / 手术)默认主动提:"您是不是要跟家里人商量一下?"
-
不假设独居 / 不假设需要陪同(PAC 无字段);
**保守 + 尊重**
## 称呼模式
-
✅ "X 先生" / "X 女士" 不变(尊重而非长辈化的"X 阿姨/叔叔",除非诊所政策明确)
-
❌ 不用"老人家""大爷大妈"(显冒犯)
## tone 默认
-
**warm**
(温和,语速明显放慢)— 默认
-
❌ 不能切 urgent(让老人焦虑)
## 沟通节奏(老年特化关键)
-
**每句话稍短**
,避免一口气讲长串信息
-
**关键信息复述一次**
:"我再跟您确认一下,X 月 X 号上午,对吗"
-
**关键名词补一句解释**
:"做牙周治疗,就是清理牙龈下面的牙石,跟洗牙类似但更深入"
-
**听不清主动应对**
:不催促,愿意重复;"没事您慢慢说" / "我再说一遍您听"
-
**时间选项尽量**
**白天 + 工作日**
(老人晚上出门不便)
-
✅ "上午 10 点左右"(老人喜欢)
-
❌ "晚上 8 点"(强烈不建议默认)
## 临床场景特点(老年高发)
-
**K08 缺牙**
:老年极高发,本 skill + diagnosis-K08 协同,强调"咀嚼力关系到吃饭吃得下↔身体健康"
-
**K05 牙周**
:老年高发,本 skill + K05 协同,强调"早治才能保住自己的牙"
-
多基础病(无字段假设):
**不主动问、不主动建议**
,只在患者主动提时回应"是的,有基础病的话医生面诊时会综合评估方案"
## opening 段增量
-
自报家门
**完整**
(老人对陌生电话警惕):"您好,我是 X 诊所(完整诊所名)的客服,我姓 X(可以编个 default 姓 — ⚠️ 不,客服自己姓不能编;改为"我们诊所客服")"
-
⭐ 正确:"X 先生您好,这里是 X 诊所,我是诊所的客服,主要是想跟您聊聊上次到店医生发现的情况"
-
给老人
**主动权**
:"您现在方便讲两句吗?不方便我们可以约个时间再回您"
## followup 段增量
### 关怀向引诊断
-
"那次 X 医生检查时,提到您 X 牙的情况;我们这边想跟您约个时间回来看看"
-
自然提家属:"您是不是要跟家里人
**商量一下时间**
?方便的话可以让家人陪您来"
### 大方案给方向不细节
-
老年人需要消化时间,
**不要在电话里讲完整方案**
;留 hook:"具体什么方案合适,您来面诊医生跟您讲清楚,您也可以带家人一起来听"
## 异议增量(老年特化)
-
**"我都这岁数了,无所谓"**
→ 不接 framing;"您能吃饭吃得下,
**身体才好**
;牙的事情真不能凑合;您来听一下医生怎么说,不做也没关系"
-
**"我儿子(女儿)不在身边"**
→ 共情;"那您方便联系上家人后,告诉他/她一声,看哪天能来陪您?我们这边帮您留好时间"
-
**"听说做这个老人不安全"**
→ "现在的技术很成熟,
**医生面诊时会**
评估您的身体情况能不能做,做不了医生不会勉强;先看一下不亏"
-
**"我家人说让我先观察"**
→ 尊重;"好的,您和家人商量好任何时候告诉我们都行;
**身体感觉不舒服**
随时来"
-
**"我听不清楚 / 您说慢一点"**
→ 立刻减慢;"好的我说慢一些"(此条由 LLM tone 控制即可,不需要枚举所有变体)
## 回写要点增量
-
同意约 → 「成功约新预约」+ 标注"老年 + 询家属同行"
-
家人未确定 → 「考虑中,3-5 天家人沟通后回复」
-
拒绝 → 「明确拒绝」+ 标注"建议下次复查/体检季再回访"
-
沟通障碍(听不清/反复跑题) → 「需要重新沟通」+ 标注"建议家属代沟通"
## ⚠️ 老年人特别禁忌
-
❌
**不能逼问**
("您今天能不能定下来" — 老人需要消化)
-
❌
**不能制造紧迫感**
("再不来就严重了" — 老人本就焦虑健康)
-
❌
**不能跨过家属直接定方案**
("您来就行,不用跟家人说" — 不合伦理)
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/population/teen/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
population-teen
description
:
患者年龄 14-17 岁青少年场景。半自主决策,家长同决策但患者本人有发言权。正畸黄金窗末段、运动牙外伤高发。tone 偏轻松不刻意亲昵,沟通对象优先患者本人,价格 / 大方案 cc 家长。当 patient.age 在 14-17 时加载。
priority
:
100
applies
:
ageMin
:
14
ageMax
:
17
version
:
0.1.0
---
# 青少年 (14-17) 沟通包
## 对话对象
-
**优先接通患者本人**
,半自主沟通日常 / 复查类事项
-
**大方案(种植 / 正畸 / 多颗治疗)必须 cc 家长**
,患者表态后跟进:"那这个您打算跟爸妈商量一下吗?"
-
监护人电话如有(目前 PAC 无字段)优先用监护人;否则患者本人 → 转家长
## 称呼模式
-
✅ 直接姓 + 名("张 XX 您好" — 比"先生/女士"自然)
-
✅ 性别明确可叫"小张 / 张同学"
-
❌ 不用"亲""宝"
-
❌ 不用"小朋友"(已显幼态,青少年抵触)
## tone 默认
**warm**
(温和)但
**不刻意亲昵**
;偶尔可稍轻松("您最近学业紧张吗")—
**避免油腻 / 自来熟**
## 临床场景特点
-
**K07 正畸**
:14-17 是末段黄金窗(早一点更轻松,但仍可做);如 reason 含 K07,加强"现在做时间合适"
-
**K01 阻生牙(智齿)**
:18 岁前不一定要拔,医生评估;不主动建议手术
-
**运动牙外伤**
:青少年高发场景(K00/K03),通常急性来过 → 现在召回看后续修复 / 复查
-
**K08 缺牙**
:罕见(除非外伤),按外伤后修复方向沟通,
**多为临时修复**
(种植要骨发育完成,通常 18+)
## opening 段增量
-
直入主题:"上次 X 医生检查时提到..."
-
加学业共情:"我知道您可能学业比较紧,所以提前给您约个方便的时间"(只在 reason 反映过来过或有上次到诊)
## followup 段增量
-
时间倾向:
**周末 / 假期 / 放学后**
-
强调便利性:"我们诊所周末也开,不耽误您上学"
-
大方案铺垫:"这个方案具体的医生面诊会跟您讲,
**也建议家长一起来听一下**
"
## 异议增量
-
**"我有自习 / 补课"**
→ 共情;"那您方便周末吗?或者您看哪天放学后比较空"
-
**"我跟我妈商量下"**
→ 鼓励;"好的,您和家长商量好了告诉我们,这边帮您把时间预留"
-
**"现在不想做"**
→ 不强迫;"理解,您方便的时候随时告诉我们 / 我们到时候再跟进"
-
**"贵吗"**
→ 不报价;"具体方案的费用要看医生面诊后的明细;您和家长一起来看一下方案"
## 回写要点
-
同意约 → 「成功约新预约」+ 标注"青少年,大方案 cc 家长"
-
患者同意 + 待家长 → 「考虑中,3-5 天家长跟进」
-
拒绝 → 「明确拒绝」+ 原因
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/relationship/new-customer/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
relationship-new-customer
description
:
新客(历史已做治疗次数 = 0)。患者**不熟悉诊所**,不认识医生,无可引用的上次治疗。opening 要详细自报家门 + 建立信任,不能"好久不见"。素材库换成"诊断后未启动"而非"上次治疗回访"。当 clinicalContext.completedTreatmentCount === 0 时加载。
priority
:
80
applies
:
relationship
:
new
version
:
0.1.0
---
# 新客沟通包(0 次历史治疗)
## ⭐ 核心差异
-
**没有"上次治疗"可引**
— base "引事实 ≥3 条" 改为引"上次到店检查 / 触发诊断 / 待办治疗"
-
**不认识医生**
— 不要说"X 医生还是您熟悉的医生",可说"那次帮您检查的是 X 医生"
-
**不熟悉诊所**
— 自报家门完整,可包含基本定位("我们诊所位于 XX,是您之前来过的那家")
-
**没有"好久不见"**
— base 默认 followup 中"距上次到店 X 天"是首次到店之后的间隔,不是治疗后
## opening 段增量(改写)
### 自报家门完整版(必带)
-
"X 先生您好,这里是 X 诊所(完整名称),我是诊所的客服"
-
"您前段时间到我们诊所做过一次检查,这次主要是想跟您同步一下当时医生的检查情况"
### 引"那次到店"而非"上次治疗"
-
✅ "那次 X 医生给您做了检查,发现 X 问题,建议您 X 时候回来处理"
-
❌ "您上次治疗后,医生建议..."(没治疗过,这句话假)
## followup 段增量
### 降门槛加倍(必带)
新客对诊所没信任,所有"邀约面诊"门槛要加倍降低:
-
"
**这次就是面诊评估**
,医生看一下情况,跟您讲清楚,不需要做任何操作"
-
"您面诊之后,要不要做、什么时候做,都由您决定"
-
可适度叠加诊所信任 token:"我们医生在 X 领域
**有 10+ 年经验**
"(⚠️ 仅当 reason 触发医生 + 资质有字段时,无字段不可加)
### 不假设熟悉度
-
❌ "您应该知道我们诊所的 X 医生"
-
✅ "那次帮您检查的是 X 医生"
## tone 默认覆盖
-
默认
**professional**
(新客对诊所的第一印象需要专业感)
-
老人新客可切 warm(由 population-elder 接管)
## 异议增量(新客特化)
-
**"我都没去过你们诊所"**
→ 立即核实;"我看到我们这边登记您 X 月 X 日到过,可能是您家人帮约的?或者您当时是不是 ..."(不强辩,温和澄清)
-
**"我只是去看了一下,没打算治疗"**
→ 尊重;"完全理解,所以这次也只是同步一下医生的检查情况,您之后是不是要处理由您定"
-
**"我已经在别的医院在治了"**
→ 立即收口;"那好的,X 先生,祝您治疗顺利"+ 主动结束 + 标记"已在外院"
-
**"你是从哪里拿到我电话的"**
→ 真实:"是您之前到我们诊所登记时留的联系方式;您不希望再被联系的话,我帮您加到不打扰名单"
-
**"我考虑下"**
→ 不催;"好的,需要的时候随时联系我们就行"
## 回写要点增量
-
同意约面诊 → 「成功约新预约(新客转化)」
-
礼貌拒绝 → 「明确拒绝」+ 标记"新客首次召回拒绝,1 个月内不再回访"
-
不希望被联系 → 立即「加入不打扰」
-
已在外院 → 「已在外院」+ 关闭召回链
## 客服执行注意
-
新客通话长度
**控制在 3 分钟内**
,新客最忌冗长
-
任何一个"考虑"信号 = 立即放手,不要追问
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/relationship/returning/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
relationship-returning
description
:
回头客 / 熟客(历史已做治疗次数 >= 1)。患者熟悉诊所,可引用具体上次治疗 / 医生 / 经历。10+ 次为熟客可走家常 tone。当 clinicalContext.completedTreatmentCount > 0 时加载。
priority
:
80
applies
:
relationship
:
returning
version
:
0.1.0
---
# 回头客 / 熟客沟通包
## ⭐ 核心差异
-
**可以引"上次治疗"**
— base "引事实 ≥3 条" 加强为"自然引用上次治疗 / 主诊医生 / 治疗链阶段"
-
**可以叫医生名字**
— clinicalContext.primaryDoctorName 直接念出来
-
**可以"好久不见"**
— daysSinceLastVisit 自然引用,
**建立熟悉感**
-
⭐
**熟客(>= 10 次)可切 warm 家常 tone**
,适度寒暄("最近怎么样")
## opening 段增量
### 引用上次治疗 / 主诊医生(必带)
-
✅ "X 先生您好,我是 X 诊所客服,
**X 医生**
让我跟您联系一下"(如 reason.triggerDoctor 有)
-
✅ "X 先生您好,
**这边已经 X 天没见您了**
,主要是想跟您同步一下..."(引 daysSinceLastVisit)
-
✅ "您上次到我们这边是 X 月 X 日做的 X 治疗,这次想跟您聊聊后续..."(引 lastVisitSummary)
### 老朋友 framing(适度,熟客)
-
10+ 次熟客可加家常话:"上次见您是 X 月 X 日,
**最近还好吧?**
"(限 1 句,不展开寒暄)
-
1-9 次回头客 framing 偏专业,
**不要过度套近乎**
## followup 段增量
### 治疗链上下文引用(必带 — 体现"诊所记得 ta")
-
如 clinicalContext.ongoingChains 有内容:"您
**现在正在做 X 治疗**
,跟那条治疗不冲突,这次主要是 ..."(说明本次召回不会重复)
-
如 lastVisitSummary 有内容:"上次 X 医生给您做完 X,
**那次效果怎么样?有没有不舒服?**
"(共情 + 反馈采集)
-
自然衔接到本次召回:"既然您都到我们这边治了好几次了,
**我们也想确保您的 X 问题完整处理好**
"
### 老客降门槛话术
-
"您熟悉我们这边的流程,
**这次就是面诊评估**
,跟您之前来差不多,大概 30 分钟"
-
"时间上随您方便,
**周末上次您选的那个时段还可以**
"(如 lastVisit 时间能推测)
## tone 默认覆盖
-
1-9 次:
**professional**
默认
-
10+ 次:
**warm**
(可家常)
-
急性病况(K04 急性发作)仍可
**urgent**
(熟客更易接受紧迫提醒)
## 异议增量(熟客特化)
-
**"上次 X 治疗后我感觉不太对"**
→
**优先处理这个反馈**
,本次召回先放一边;"哦,具体是哪里不舒服?我帮您反馈给 X 医生,看是不是要先回来看看那个问题"(投诉→服务恢复优先)
-
**"我最近换地方住了/搬家了"**
→ 共情;"理解,如果方便也可以推荐您附近的合作诊所,或者您方便回我们这边的话再约"
-
**"X 医生还在你们诊所吗"**
→ 真实回答;"在的,X 医生还是您之前的医生,这次也希望由 X 医生给您评估"(⚠️ 仅当字段确认医生在职;无字段时改"我帮您查一下,然后跟您说")
-
**"上次我跟你们说过我不来了"**
→ 立即核实 + 道歉;"啊,真不好意思,我帮您再确认一下记录,如果之前确实标注过,我们后续不会再打扰您"+ 改 do_not_contact
## 回写要点增量
-
同意约 → 「成功约新预约」+ 标注治疗链关联
-
反馈不舒服 → 优先「服务恢复(投诉跟进)」,本次召回暂缓
-
礼貌拒绝 → 「明确拒绝」+ 标记原因
-
已不在该地 → 「迁居,关闭召回」
-
已搬到外院/外地 → 「已在外院」
## 客服执行注意
-
熟客通话
**可以适度长**
(5-7 分钟),关系维护比转化重要
-
熟客投诉是金矿,
**任何不舒服反馈都先处理**
,本次召回可二次跟进
apps/pac-service/src/modules/ai/calls/draft-plan-script/skills/scenario/treatment-initiation/SKILL.md
0 → 100644
View file @
f19434d7
---
name
:
scenario-treatment-initiation
description
:
启治召回 - 发现待治疗诊断但患者未启动 planned/actual。任务核心:把"医生发现的问题"自然引到"该回来评估",不显推销。提供启治场景的 opening 锚点、followup 降门槛话术骨架、回写口径。当 plan.primaryScenarioKey 等于 treatment_initiation_recall 时加载。
priority
:
10
applies
:
scenario
:
treatment_initiation_recall
version
:
0.1.0
---
# 启治召回(新链)话术骨架
## 核心动机
诊所诊断发现 → 患者没启动治疗。沟通逻辑:
**唤起认知 → 降门槛邀约**
,不是销售。患者还没下决心,任何"快来约""价格优惠"都会推远。
## opening 段增量
-
✅ 必引:某月某日由 X 医生发现 X 问题(从 reason.triggerDoctor / triggerDate 拿;空就用"上次就诊时医生发现")
-
✅ 引用要"叙事"而非"通知":"那次姜医生检查时,看到您...,提醒过该考虑..."
-
❌ 不能开口就邀约时间("您是否方便本周来一次?")— 先建立"为什么打这个电话"
-
❌ 不能用 scenario 内部代号("围绕「启治召回」开场" — 患者听到 = 机器外呼)
## followup 段增量
### 降门槛话术(必带)
启治场景客户最大顾虑:"我又得动牙?要花多少钱?要多久?会不会疼?"
对应给安心 token:
-
"
**这次只是医生面诊评估,不做任何操作**
,大概 30 分钟"
-
"评估完您再决定要不要做,什么时候做"
-
"评估不收治疗费,跟普通检查一样"(⚠️ 注意:
**不能承诺免费**
,要按诊所实际口径;无字段时不要主动提价格)
### 时间措辞(参考 base §6,这里加强)
-
启治没有时效硬绑(不像术后复查 7 天必须查),时间可以给宽 — "本周末或下周初哪天方便?"
-
close 段务必弱化:"我先按 X 登记,具体时段以诊所排班为准"
## 异议增量(本 scenario 特化)
启治场景常见且必须 cover:
-
**"我又不疼,有必要去吗"**
→ 不痛≠没事,小问题拖大代价更高(用具体临床事实佐证,不能空喊)
-
**"我再观察一下"**
→ 接受,但提"过期再约可能需要重新评估,建议留个时间窗"
-
**"我打算去别的医院看看"**
→ 尊重,引导"那您方便时让我们参考一下方案?"(转介线索)
## 回写要点增量
-
决定去面诊评估 → 「成功约面诊」
-
同意但未定时间 → 「考虑中,7 天后跟进」
-
拒绝/已在外院 → 「已在外院」或「明确拒绝」
-
否认诊断("我没听医生说") → 「诊断争议,转回诊所核实」
## 禁忌
-
❌ 不要把"诊断"说成"严重",启治场景大部分问题在"该处理"而非"急救"
-
❌ 不要拿"再不来 X 就更严重"恐吓(base §4 已禁)— 用"早处理代价小"温和引导
apps/pac-service/src/modules/ai/orchestrators/plan-script.orchestrator.ts
View file @
f19434d7
...
...
@@ -369,12 +369,18 @@ export class PlanScriptOrchestrator {
primaryScenarioLabel
:
plan
.
reasons
[
0
]
?
planScenarioLabel
(
plan
.
reasons
[
0
].
scenario
)
:
'常规复诊召回'
,
// ⭐ raw scenario key — skill composer.applies.scenario 用
primaryScenarioKey
:
plan
.
reasons
[
0
]?.
scenario
??
null
,
priorityScore
:
plan
.
priorityScore
,
goal
:
plan
.
goal
,
reasons
:
plan
.
reasons
.
map
((
r
)
=>
{
const
trig
=
resolveReasonTrigger
(
r
);
// sub_key 形如 'caries_no_filling@36',base 去 @ 后缀
const
baseSubKey
=
(
r
.
subKey
??
''
).
split
(
'@'
)[
0
]
||
null
;
return
{
scenarioLabel
:
planScenarioLabel
(
r
.
scenario
),
subKey
:
baseSubKey
,
dxCode
:
subKeyToDxCode
(
baseSubKey
),
reason
:
r
.
reason
,
priorityScore
:
r
.
priorityScore
,
triggerDoctor
:
trig
.
doctor
,
...
...
@@ -502,6 +508,30 @@ function summarizeLastVisit(enc: FactRow | undefined): string | null {
}
/**
* sub_key → ICD-10 K-code(K00-K09)— skill composer.applies.diagnosisCodePrefix 用。
* 跟 packages/types/src/canonical-codes.ts 的 PACScenarioSubLabels 一一对齐(K00-K09 全套)。
*
* ⚠️ 加新 sub_scenario 时记得在这里加一条 — 没有则 dxCode=null,该 reason 不参与 diagnosis-K0X skill match。
*/
const SUB_KEY_TO_K_CODE: Record<string, string> = {
development_eruption: 'K00',
impacted_tooth: 'K01',
caries_no_filling: 'K02',
hard_tissue_damage: 'K03',
endo_no_rct: 'K04',
perio_no_srp: 'K05',
gum_alveolar_lesion: 'K06',
ortho_no_consult: 'K07',
missing_tooth: 'K08',
jaw_cyst: 'K09',
};
function subKeyToDxCode(subKey: string | null): string | null {
if (!subKey) return null;
return SUB_KEY_TO_K_CODE[subKey] ?? null;
}
/**
* 从 plan.reasons 派生 pendingTreatments(待办治疗列表,LLM 在 followup 段引用)。
*
* 为什么从 reasons 而不是 facts 派生:
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment