跳到正文

外呼结果与业务结果约定

本文说明 callflow-esl 业务结束后回写 callout-server 的结果结构。完整接口字段以 api-reference.mdPOST /api/call-results 为准;端到端业务示例见 outbound-business-example.md

/api/call-results 的用途

POST /api/call-results 是内部回写接口,用于把一次外呼尝试从 originate 提交状态推进到真实通话结果。 调用方通常是 callflow-esl 的业务处理函数,也可以是通话生命周期补偿逻辑。

一次回写建议同时覆盖三类信息:

信息推荐字段
定位信息attemptIdrequestId,或 campaignId + contactId
通话结果callStatushangupCauseansweredAtendedAtdurationSecondsrecordingUrl
业务结果payload.resulttranscript,以及必要的 metadata / NLU 摘要

通话结果摘要

通话层字段描述的是电话是否接通、何时接通、何时结束以及录音位置,不承载业务判断。

json
{
  "requestId": "callout-req-001",
  "callStatus": "completed",
  "hangupCause": "NORMAL_CLEARING",
  "answeredAt": "2026-06-03T09:00:00.000Z",
  "endedAt": "2026-06-03T09:01:20.000Z",
  "durationSeconds": 80,
  "recordingUrl": "20260603-090000-001.wav"
}

约定:

  • callStatus 表示真实通话状态,不等同于 originate 提交状态。
  • durationSeconds 建议由 answeredAtendedAt 推导;缺少接听时间时可不传。
  • recordingUrl 建议只传录音文件名或部署约定的短路径,完整播放地址由媒体配置拼接。

业务 metadata

联系人导入、活动配置或业务过程中的附加信息建议放在 payload.result.metadata 下,保持业务字段与通话字段分离。

json
{
  "payload": {
    "result": {
      "businessCode": "sales-followup",
      "outcomeCode": "interested",
      "outcomeCategory": "positive",
      "metadata": {
        "orderId": "A123",
        "customerLevel": "vip",
        "callbackTime": "15:00"
      }
    }
  }
}

推荐写法:

  • businessCode:回写产生该结果的业务编码。
  • outcomeCode:稳定的业务结果代码,方便筛选、统计和下游自动化。
  • outcomeCategory:粗粒度分类,如 positivenegativeneutraltransferunknown
  • metadata:只放与当前业务相关的扩展字段,避免把完整通话日志塞入这里。

transcript

transcript 用于保存分角色对话片段。带 recordingUrl 时,服务会把结构化片段落到 transcript_segments,供前端按时间线展示。

json
{
  "transcript": [
    {
      "speaker": "agent",
      "startMs": 0,
      "endMs": 2600,
      "text": "您好,这里是售后回访。",
      "metadata": { "phase": "opening", "source": "tts" }
    },
    {
      "speaker": "customer",
      "startMs": 3100,
      "endMs": 6200,
      "text": "可以,下午三点以后再联系我。",
      "metadata": { "source": "asr", "confidence": 0.86 }
    }
  ]
}

推荐写法:

  • speaker 使用稳定角色值,如 agentcustomersystem
  • startMs / endMs 使用相对本次通话或录音开始的毫秒数。
  • metadata.phase 可记录当前业务阶段,例如 openingqualificationclosing
  • ASR 置信度、打断标记、播报来源等辅助信息放入片段级 metadata

NLU 摘要

如果业务使用统一 NLU 服务做决策或字段抽取,建议把最终摘要放在 payload.result.nlu 下,而不是把每次模型请求全文写入结果。

json
{
  "payload": {
    "result": {
      "recognizedText": "可以,下午三点以后再联系我。",
      "nlu": {
        "decision": {
          "profile": "sales-intent",
          "key": "interested",
          "confidence": 0.82,
          "reason": "客户愿意继续沟通"
        },
        "extract": {
          "profile": "customer-base",
          "fields": {
            "callbackTime": "下午三点以后"
          },
          "confidence": 0.78,
          "reason": "客户明确说明方便时间"
        }
      }
    }
  }
}

推荐写法:

  • profile 记录实际使用的 NLU profile,便于后续排查策略差异。
  • key / fields 保存最终业务可用结果。
  • confidence 保存 0 到 1 的归一化置信度。
  • reason 保留简短中文原因即可,详细 prompt、模型配置和内部重试日志不写入业务结果。

最小完整示例

json
{
  "requestId": "callout-req-001",
  "callStatus": "completed",
  "hangupCause": "NORMAL_CLEARING",
  "answeredAt": "2026-06-03T09:00:00.000Z",
  "endedAt": "2026-06-03T09:01:20.000Z",
  "recordingUrl": "20260603-090000-001.wav",
  "transcript": [
    {
      "speaker": "customer",
      "startMs": 3100,
      "endMs": 6200,
      "text": "可以,下午三点以后再联系我。"
    }
  ],
  "payload": {
    "result": {
      "businessCode": "sales-followup",
      "recognizedText": "可以,下午三点以后再联系我。",
      "outcomeCode": "interested",
      "outcomeCategory": "positive",
      "metadata": {
        "callbackTime": "15:00"
      },
      "nlu": {
        "decision": {
          "profile": "sales-intent",
          "key": "interested",
          "confidence": 0.82,
          "reason": "客户愿意继续沟通"
        }
      }
    }
  }
}

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