跳到正文

流程脚本与 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-profiledefault / 通用策略extract包含姓名、手机号等字段抽取规则与提示词
场景模板 - 意向分流跟进route-intentdefault / 通用策略decision包含高意向、低意向、拒绝等语义分支定义
场景模板 - 预约回访确认collect-appointmentdefault / 通用策略extract包含预约时间、意向状态抽取规则
场景模板 - 售后满意度回访collect-feedbackdefault / 通用策略extract包含满意度评分、改进建议抽取规则
场景模板 - 售后满意度回访route-feedbackdefault / 通用策略decision包含满意、不满意等分支判断逻辑
场景模板 - 投诉安抚与转人工collect-complaintdefault / 通用策略extract包含投诉类别、诉求要点抽取规则
场景模板 - 投诉安抚与转人工route-complaintdefault / 通用策略decision包含继续安抚、升级人工等分支逻辑

新增与维护脚本流程 ​

  1. 流程规划:在流程图设计器中拖入节点,规划业务流程路径与 NLU 交互点。
  2. 节点内联配置:
    • 对每个 collect 节点配置提问 Prompt、补问 Prompt、允许抽取的字段白名单(fields)及类型校验。
    • 对每个 router 节点配置分支 key、分支描述(branches)、变量保存键(save_as)及系统提示词。
  3. 即时调优与拨测:
    • 节点弹窗调试:在流程设计器中双击打开节点配置弹窗,直接在调试区域输入用户测试语料,实时验证抽取结果或意图分类命中分支。
    • 策略工作台集中调优:进入「AI 策略中心 → 话术策略工作台」(/ai/strategies),通过左侧节点大纲树快速定位节点,支持「一句话生成/优化 Prompt」、全量 Prompt 预览、Raw/思考链折叠展示与右栏实时多轮拨测。
  4. 原子保存与发布:
    • 在策略工作台中调优后,直接点击保存,系统通过 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(可选,抽取出值后由运行时做确定性检查,只有与字段类型匹配的规则生效):

字段类型生效规则说明
numbermin / max含边界的数值区间;min 不得大于 max,保存与运行时拉取都会拒绝
textpattern正则匹配,必须是合法正则表达式
enumenum允许值子集,应落在 options 之内

与字段类型不匹配的规则(例如 text 字段上残留的 min / max)保存时不报错,运行时与流程演示一律忽略。 字段定义不支持 sensitive 键:保存校验、提示词渲染与运行时都忽略它,存量脚本中的残留值不会报错, 也不会产生脱敏等行为。

校验与澄清追问流程:

  1. 抽取结果只接受 fields 声明过的 key;空值不会覆盖上一轮已采到的值;配置了 validation 的字段, 值未通过校验时不写入 variables 并记入 invalidFields。
  2. 仍有必填字段缺失或有校验失败时进入下一轮追问,播报文本 = clarify_prompt(或回退话术)+ 逐字段的问题说明:
    • 必填缺失:请补充:姓名、电话。
    • 数值越界:年龄需在 18 到 120 之间。;只配下限时 年龄不能小于 18。;只配上限时 年龄不能大于 120。
    • 正则不匹配:手机号格式不正确,请重新说明。
    • 枚举不在允许值内:回访时间段请在上午、下午、晚上中选择。
  3. 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 产出的字段。

提交前检查清单 ​

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。