外观
外呼结果与业务结果约定
本文说明 callflow-esl 业务结束后回写 callout-server 的结果结构。完整接口字段以 api-reference.md 的 POST /api/call-results 为准;端到端业务示例见 outbound-business-example.md。
/api/call-results 的用途
POST /api/call-results 是内部回写接口,用于把一次外呼尝试从 originate 提交状态推进到真实通话结果。 调用方通常是 callflow-esl 的业务处理函数,也可以是通话生命周期补偿逻辑。
一次回写建议同时覆盖三类信息:
| 信息 | 推荐字段 |
|---|---|
| 定位信息 | attemptId、requestId,或 campaignId + contactId |
| 通话结果 | callStatus、hangupCause、answeredAt、endedAt、durationSeconds、recordingUrl |
| 业务结果 | payload.result、transcript,以及必要的 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建议由answeredAt到endedAt推导;缺少接听时间时可不传。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:粗粒度分类,如positive、negative、neutral、transfer、unknown。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使用稳定角色值,如agent、customer、system。startMs/endMs使用相对本次通话或录音开始的毫秒数。metadata.phase可记录当前业务阶段,例如opening、qualification、closing。- 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": "客户愿意继续沟通"
}
}
}
}
}