流程脚本与 NLU 开发规范
本文约定 callout 流程脚本中的 NLU 节点如何声明、实现和验证。目标是明确流程脚本节点内联 Prompt 架构、配置约束与调试规范。
核心约定
流程脚本采用「以流程为中心(Flow-Centric)」的节点内联 Prompt 架构;真正的 NLU 行为由 callout-server 原生内嵌的 AI 策略执行引擎驱动。
collect节点使用data.extract_profile(可选;保存与读取时缺省注入default),调用统一 NLU 的action=extract。抽取所需的目标字段集合(fields)与节点提示词(system_prompt/prompt)直接内联于节点数据中。router节点使用data.decision_profile(可选;保存与读取时缺省注入default),调用统一 NLU 的action=decision。决策分支(branches)与路由指示词直接内联于节点数据中。chat节点使用data.chat_profile(可选,无缺省 profile;未配置时回退内置 general-assistant 基底),走独立的流式 Chat 服务(action=chat),节点内联会话 Prompt 与转人工/打断配置,行为规范见 flow-script-chat-development.md。var_router节点不调用任何模型,根据运行时流程变量进行确定性条件分流。- 节点内联 Prompt 支持通过
POST /api/scripts/node-prompt-update实现无损原子局部更新,避免画布全图保存引发的并发覆盖。 - 调试与评估直接在流程节点弹窗抽屉或「三栏话术策略工作台」(
/ai/strategies)中进行即时多轮拨测。
流程图保存契约
- 节点类型白名单:
start、send、collect、router、var_router、chat、transfer、end。 - 必须且只能有一个
start;节点 ID 唯一,所有连线端点必须存在。 - 每类节点的出口要求:
start/send:必须且只能有一个默认出口(无sourceHandle)。collect:必须有success;fail可选 0/1 条,最多两个出口,不接受其它 handle。router/var_router:出口与branches[*].key一一对应,不允许隐式默认分支。chat:出口是全部语义分支加上单一的fallback(兜底/未决出口);存量timeout、error连线自动兼容。transfer:默认出口与timeout(坐席振铃超时降级)出口均必填,且各只能有一条。end:不允许出边。
- 同一节点上出口不能重复(按
source+sourceHandle判定)。 - 所有节点必须从
start可达。流程允许形成循环,运行时以最多 100 步防止无限执行。 - collect 字段 key、router/transfer
save_as必须合法且不能冲突;模板只能引用已声明变量。 collect.data.extract_profile与router.data.decision_profile可选,显式提供时必须格式合法;未显式指定时由normalizeFlowProfiles在保存与读取时缺省注入default。- 数据库中的旧脚本若不满足上述契约,运行时接口返回
422,不会替换成空流程继续执行。
节点内联 Prompt 与策略机制
在现行架构中,业务话术、字段定义与意图分支直接随流程脚本(callout.scripts.flow_json)统一版本化管理。
profile仅作为底层执行策略算法模板的标识(例如系统内置的default,或特定算法策略)。- 节点的实际决策逻辑完全由节点内联的
system_prompt、branches(针对 router)和fields(针对 collect)决定。 - 引擎在执行
action=extract或action=decision时,会将节点内联的 Prompt 与结构化字段/分支定义合并,生成针对特定业务场景的推理请求。
下表为标准示例脚本在各节点中使用的策略与内联配置要点:
| 脚本 | 节点 | profile (算法策略) | action | 内联配置要点 |
|---|---|---|---|---|
| 场景模板 - 客户资料采集 | collect-profile | default / 通用策略 | extract | 包含姓名、手机号等字段抽取规则与提示词 |
| 场景模板 - 意向分流跟进 | route-intent | default / 通用策略 | decision | 包含高意向、低意向、拒绝等语义分支定义 |
| 场景模板 - 预约回访确认 | collect-appointment | default / 通用策略 | extract | 包含预约时间、意向状态抽取规则 |
| 场景模板 - 售后满意度回访 | collect-feedback | default / 通用策略 | extract | 包含满意度评分、改进建议抽取规则 |
| 场景模板 - 售后满意度回访 | route-feedback | default / 通用策略 | decision | 包含满意、不满意等分支判断逻辑 |
| 场景模板 - 投诉安抚与转人工 | collect-complaint | default / 通用策略 | extract | 包含投诉类别、诉求要点抽取规则 |
| 场景模板 - 投诉安抚与转人工 | route-complaint | default / 通用策略 | decision | 包含继续安抚、升级人工等分支逻辑 |
新增与维护脚本流程
- 流程规划:在流程图设计器中拖入节点,规划业务流程路径与 NLU 交互点。
- 节点内联配置:
- 对每个
collect节点配置提问 Prompt、补问 Prompt、允许抽取的字段白名单(fields)及类型校验。 - 对每个
router节点配置分支 key、分支描述(branches)、变量保存键(save_as)及系统提示词。
- 对每个
- 即时调优与拨测:
- 节点弹窗调试:在流程设计器中双击打开节点配置弹窗,直接在调试区域输入用户测试语料,实时验证抽取结果或意图分类命中分支。
- 策略工作台集中调优:进入「AI 策略中心 → 话术策略工作台」(
/ai/strategies),通过左侧节点大纲树快速定位节点,支持「一句话生成/优化 Prompt」、全量 Prompt 预览、Raw/思考链折叠展示与右栏实时多轮拨测。
- 原子保存与发布:
- 在策略工作台中调优后,直接点击保存,系统通过
POST /api/scripts/node-prompt-update无损原子更新节点,不破坏画布其他节点状态。 - 在流程设计器中全量保存脚本,系统将完整校验全图拓扑、连线端点与变量引用合法性。
- 在策略工作台中调优后,直接点击保存,系统通过
collect 节点规范
collect 节点必须包含:
prompt:向客户提问的话术。error_prompt:第一次抽取缺必填字段后的补问话术。fields:允许抽取并写入variables的字段白名单。extract_profile:字段抽取策略 key,可选;缺省时保存与读取注入default,显式提供时只能从extract.profiles选择。
可选配置:
clarify_prompt:校验失败或必填缺失时的澄清追问底稿;留空时回退error_prompt,再回退prompt。max_retries:追问轮数上限,1 到 5 的整数,默认 3。interruptible:是否允许客户在播报提问时插话打断,默认true。
字段类型:
| type | 返回值 | 说明 |
|---|---|---|
text | 字符串或 null | 自然语言文本,支持 pattern 正则校验 |
number | 数字或 null | 数值字段,支持 min / max 区间校验 |
enum | 必须来自 options | 枚举选项,支持 enum 校验 |
字段校验规则 validation(可选,抽取出值后由运行时做确定性检查,只有与字段类型匹配的规则生效):
| 字段类型 | 生效规则 | 说明 |
|---|---|---|
number | min / max | 含边界的数值区间;min 不得大于 max,保存与运行时拉取都会拒绝 |
text | pattern | 正则匹配,必须是合法正则表达式 |
enum | enum | 允许值子集,应落在 options 之内 |
与字段类型不匹配的规则(例如 text 字段上残留的 min / max)保存时不报错,运行时与流程演示一律忽略。 字段定义不支持 sensitive 键:保存校验、提示词渲染与运行时都忽略它,存量脚本中的残留值不会报错, 也不会产生脱敏等行为。
校验与澄清追问流程:
- 抽取结果只接受
fields声明过的 key;空值不会覆盖上一轮已采到的值;配置了validation的字段, 值未通过校验时不写入variables并记入invalidFields。 - 仍有必填字段缺失或有校验失败时进入下一轮追问,播报文本 =
clarify_prompt(或回退话术)+ 逐字段的问题说明:- 必填缺失:
请补充:姓名、电话。 - 数值越界:
年龄需在 18 到 120 之间。;只配下限时年龄不能小于 18。;只配上限时年龄不能大于 120。 - 正则不匹配:
手机号格式不正确,请重新说明。 - 枚举不在允许值内:
回访时间段请在上午、下午、晚上中选择。
- 必填缺失:
max_retries轮后仍未完成则走fail出口,routeTrace中该节点记录outcome: "fail"、failReason: "exhausted"、missingRequiredFields(字段标签)与invalidFields(字段 key)。
示例:
json
{
"id": "collect-appointment",
"type": "collect",
"data": {
"prompt": "请问您希望哪天、哪个时间段回访?回访主题是什么,是否需要短信提醒?",
"error_prompt": "没有听清,请再说一下回访时间段、到场人数以及备注。",
"extract_profile": "appointment-confirm",
"fields": [
{
"key": "appointment.note",
"label": "备注事项",
"type": "text",
"required": false,
"description": "客户提出的特殊备注"
},
{
"key": "appointment.slot",
"label": "回访时间段",
"type": "enum",
"required": true,
"options": ["上午", "下午", "晚上"]
},
{
"key": "appointment.guests",
"label": "到场人数",
"type": "number",
"validation": { "min": 1, "max": 10 }
}
]
}
}对应 NLU 请求示例:
json
{
"action": "extract",
"profile": "appointment-confirm",
"text": "下周二下午回访,我们一共3个人来",
"fields": [
{
"key": "appointment.note",
"label": "备注事项",
"type": "text"
},
{
"key": "appointment.slot",
"label": "回访时间段",
"type": "enum",
"required": true,
"options": ["上午", "下午", "晚上"]
},
{
"key": "appointment.guests",
"label": "到场人数",
"type": "number",
"validation": { "min": 1, "max": 10 }
}
]
}validation 原样透传给 NLU 接口;服务端只把与字段类型匹配的规则渲染进模型提示词 (如 取值范围:1~10、允许值:上午/下午),其余规则不进入模型输入。
期望响应:
json
{
"code": "0000",
"data": {
"fields": {
"appointment.date": "下周二",
"appointment.slot": "下午",
"appointment.sms_reminder": true,
"appointment.guests": null
},
"confidence": 0.86,
"reason": "客户明确说明回访日期、时间段和短信提醒诉求"
},
"msg": "success"
}router 节点规范
router 节点必须包含:
prompt:用于获取客户意图的问题。error_prompt:意图不清晰时的补问。branches:候选分支,key必须与对应出边sourceHandle一致。decision_profile:路由决策策略 key,可选;缺省时保存与读取注入default,显式提供时只能从decision.profiles选择。save_as:可选,把本轮决策对象写入variables[save_as],便于结果回写或后续模板引用。
示例:
json
{
"id": "route-complaint",
"type": "router",
"data": {
"prompt": "请问这个问题需要我们马上加急处理,还是安排普通跟进?如果您要人工,也可以直接说转人工。",
"error_prompt": "请问是普通跟进、加急处理,还是转人工?",
"decision_profile": "complaint-routing",
"save_as": "complaint.route",
"branches": [
{ "key": "普通跟进", "desc": "客户接受正常时效回访处理" },
{ "key": "加急处理", "desc": "客户明确要求尽快、马上、今天内处理" },
{ "key": "转人工", "desc": "客户明确要求人工坐席或主管介入" }
]
}
}对应连线示例:
json
[
{
"source": "route-complaint",
"target": "send-normal",
"sourceHandle": "普通跟进"
},
{
"source": "route-complaint",
"target": "send-urgent",
"sourceHandle": "加急处理"
},
{
"source": "route-complaint",
"target": "transfer-agent",
"sourceHandle": "转人工"
},
{ "source": "transfer-agent", "target": "end" }
]var_router 节点规范
var_router 是不调 NLU、不播报的确定性分支节点:直接按已收集变量决定走哪条出边, 适合把 collect / chat 的结果转成流程走向(例如"抽到手机号才发短信")。它没有 profile, 也不产生对话内容和 transcript,因此不占用模型超时预算。
必填与可选字段:
branches:至少一项,每项key(与出边sourceHandle一致)+desc,key 不能重复。branches[*].match:可选条件;缺省即无条件兜底分支,通常放在最后一项。save_as:可选,把决策结果写入变量,注册.key、.branchDesc、.dispatch、.reason四个后缀(确定性分发固定为dispatch="condition"、reason="deterministic_condition")。
match 形如 { "variable": "...", "operator": "...", "value": ... },variable 只能包含 字母、数字、下划线、点和横线(与 collect 字段 key 同规则):
| operator | 语义 | value |
|---|---|---|
eq / ne | 相等 / 不等;数字对数字、布尔对布尔,其余按去空格字符串精确比较 | 必填,字符串/数字/布尔 |
gt / gte / lt / lte | 数值比较,任一侧不是有限数即不命中 | 必填,数字 |
contains / not_contains | 字符串子串包含;数组则按元素全等判断 | 必填,字符串 |
empty / not_empty | 空判定:undefined/null/空白字符串/空数组算空 | 不需要 |
求值规则按声明顺序取第一个命中的分支:
- 未收集或为
null的变量只对empty/not_empty生效,对其它运算符一律不命中,不报错。 - 条件写法非法(缺
variable、运算符不认识、该给值却没给)时,保存接口直接 400 拒绝; 运行时读到这种历史配置会把该分支降级为无条件兜底,而不是让通话崩掉。 - 全部分支都没命中且没有兜底分支时,运行时抛错并按错误路径收尾——多分支条件务必留一条兜底。
json
{
"id": "route-has-mobile",
"type": "var_router",
"data": {
"save_as": "route.mobile",
"branches": [
{
"key": "有手机号",
"desc": "已抽到手机号,可直接发短信",
"match": { "variable": "customer.mobile", "operator": "not_empty" }
},
{ "key": "无手机号", "desc": "兜底:转人工补录" }
]
}
}transfer 节点规范
transfer 节点用于真实 AI 转人工,而不是只播报“稍后联系”。运行时会调用 callflow-esl 中的 requestHumanAgentTransfer,进一步请求 callout-server 的 POST /api/transfers/route 预占空闲坐席,并把客户腿 ctx.conference.join 到 transfer-<uuid> 会议室。
推荐配置:
json
{
"id": "transfer-agent",
"type": "transfer",
"data": {
"prompt": "好的,正在为您转接人工坐席,请稍候。",
"unavailable_prompt": "抱歉,当前没有可用的人工坐席,本次投诉需求已记录,我们会安排专人尽快联系您。",
"ring_timeout_seconds": 30,
"save_as": "complaint.transfer"
}
}save_as(缺省 transfer)会注册 .routed、.reason、.conferenceId、.agent.name、 .agent.extension、.softphoneCallId、.ringTimeoutSeconds 模板变量。 ring_timeout_seconds 缺省 30。
执行语义:
routed=true只代表坐席腿的后台 originate 已被接受,不代表坐席已经在会议里。客户腿入会后 由看门狗盯ring_timeout_seconds:坐席真的入会(或客户先挂机)则 AI 流程就地结束,不再执行后续节点。- 坐席始终没进来时,结果变量被改写为
routed=false+reason="agent_join_timeout",与路由失败 走同一条兜底路径,客户不会被独留在静音会议里。 routed=false(无空闲坐席,或上面的超时降级):播报unavailable_prompt,然后优先走timeout出边;存量脚本若没有timeout出边则沿默认出边继续,两者都没有才直接结束通话。transfer的默认出口与timeout出口均必填且各一条,保存时缺少任一都会被拒绝。
对应 NLU 请求示例:
json
{
"action": "decision",
"profile": "complaint-routing",
"text": "这个问题今天必须解决,不行就帮我转人工",
"branches": [
{ "key": "普通跟进", "desc": "客户接受正常时效回访处理" },
{ "key": "加急处理", "desc": "客户明确要求尽快、马上、今天内处理" },
{ "key": "转人工", "desc": "客户明确要求人工坐席或主管介入" }
]
}期望响应:
json
{
"code": "0000",
"data": {
"key": "加急处理",
"confidence": 0.84,
"reason": "客户明确要求今天必须解决,优先命中加急诉求"
},
"msg": "success"
}策略配置格式
在 callout-server 中,策略通过数据库与「AI 策略中心」管理台维护,支持结构化 JSON 提示词与回退配置:
json
{
"key": "complaint-routing",
"labelZh": "投诉处理路由",
"descriptionZh": "面向投诉安抚场景,识别普通跟进、加急处理或转人工。",
"systemPrompt": "你是投诉处理外呼路由分类器。只能返回 JSON:{\"key\":\"分支key\",\"confidence\":0到1,\"reason\":\"简短原因\"}。必须从给定 branches 的 key 中选择一个。",
"openaiConfig": { "timeoutMs": 42000 },
"fallbackKeywords": ["加急", "人工", "投诉"]
}json
{
"key": "complaint-summary",
"labelZh": "投诉摘要抽取",
"descriptionZh": "抽取投诉核心问题、处理诉求和紧急程度。",
"systemPrompt": "你是投诉摘要字段抽取器。只能返回 JSON:{\"fields\":{\"字段key\":值},\"confidence\":0到1,\"reason\":\"简短原因\"}。只允许返回请求 fields/field 中指定的字段 key。",
"openaiConfig": { "timeoutMs": 45000 },
"fallbackKeywords": []
}验证命令
bash
bun test apps/callout-server
bun run --cwd apps/callout-server typecheck
curl http://127.0.0.1:9920/api/ai/nlu/capabilities如果通过前端页面验证,打开 http://localhost:19920/flow-scripts,选择目标脚本后检查:
- 节点属性里的 profile key 与注册表一致。
router分支 key 与画布出边sourceHandle一致。collect字段 key 只包含字母、数字、下划线、点和横线。- 变量面板能看到 collect 产出的字段。