讯飞AIUI语音意图误识别的兜底策略:从"打开今日日程"无响应说起

背景

在老年关怀机器人应用中,语音交互是核心功能。我们接入了讯飞AIUI SDK,用户说出指令后,SDK返回语义识别结果(intent + slots),应用层通过 SemanticHandler 分发到对应的处理逻辑。

某天收到反馈:用户说"打开今日日程",页面既没有语音回复,也没有跳转到日程页面。但说"打开卧室台灯"却能正常回复。两个指令都是"打开XX"的句式,为什么表现不同?本文记录了从日志分析到根因定位再到兜底修复的完整过程。

日志分析

正常场景:"打开卧室台灯"

14:55:31.553  resultInfo -> 打开卧室台灯。
14:55:31.758  cbm_semantic intent=ztd_open, slots=[start="打开", dev_name="卧室台", ztd="灯"]
14:55:32.705  全部接受完毕 -> 抱歉,我没办法帮你打开卧室台灯呢。但我随时都在...

讯飞把"打开卧室台灯"识别为 ztd_open(IoT控制类意图),虽然识别"错误"(用户家里没有这个设备),但大模型NLP返回了一段兜底回复"抱歉,我没办法帮你打开卧室台灯呢",用户至少听到了反馈。

异常场景:"打开今日日程"

14:55:38.112  resultInfo -> 打开今日日程。
14:55:38.169  cbm_semantic intent=coolkit_single_open, slots=[start="打开", dev_name="今日日程"]
14:55:38.959  ttsMsg -> {"category":"OS16576376313.iot_control","intent":"coolkit_single_open",...}
(日志到此结束,没有任何回复或跳转)

讯飞把"打开今日日程"识别成了 coolkit_single_open(IoT单设备打开意图),dev_name="今日日程" 被当成了设备名。之后既没有"全部接受完毕"的大模型回复,也没有跳转动作。

根因定位

第一层:SemanticHandler的空实现

打开 SemanticHandler.kt,找到 coolkit_single_open 分支:

"coolkit_single_open" -> {
    return false
}

空实现! 直接 return false 什么都不做。这就是没有跳转的原因——根本没走到 schedule_fun 分支(那个分支才会调用 openSchedulePage()):

"schedule_fun" -> { //今日日程
    finishVoiceInteraction()
    openSchedulePage()
}

第二层:为什么也没有语音回复?

return false 只是不跳转,按理还有大模型NLP兜底回复。但日志显示连回复都没有。我追踪了完整的代码链路:

SemanticUtil的解析流程:

val parseResult = SemanticHandler.parseSemantic(rawMessage, semanticObj)
if (parseResult) {
    // 命中技能,不再构造兜底结果
    cbmSemanticHit = true
    aiuiParseResult = null
} else if (semanticObj.rc != 4) {
    // rc!=4 表示语义命中了技能
    cbmSemanticHit = true
    aiuiParseResult = AIUIParseResult(ParseResultType.NLP, rawMessage)
} else {
    // rc=4 表示语义未命中
    cbmSemanticHit = false
    // 构造空answer等待大模型回复
}

关键点:coolkit_single_openrc=0(命中),所以走第二个分支,cbmSemanticHit = trueaiuiParseResult 被设为cbm_semantic的rawMessage。

nlp2TTS的处理:

当nlp结果到达时,因为 cbmSemanticHit = true,不会用nlp文本替换 answer.text。最终 nlp2TTS 收到的msg数据是cbm_semantic的JSON:

private fun nlp2TTS(msg: RawMessage) {
    val ttsMsg = JSONObject(String(msg.msgData!!))
    if (ttsMsg.has("text")) {
        val text = ttsMsg.getString("text")
        if (text.trim().isNotEmpty()) {
            ttsText = text
        }
    }
    if (ttsMsg.has("answer")) {
        val answer = ttsMsg.getJSONObject("answer")
        val text = answer.getString("text")
        if (text.trim().isNotEmpty()) {
            ttsText = text
        }
    }
    if (ttsText.isBlank()) {
        // 检查是否是roll_camara指令...
        return  // ← 直接返回,不调用chatRepo.addMessage!
    }
    chatRepo.addMessage(msg)  // 这行被跳过了
}

cbm_semantic的JSON中,text 被 SemanticUtil 主动清空了(semanticResult.put("text", "")),也没有 answer 字段。所以 ttsText 一直为空,直接return,不调用 chatRepo.addMessage,聊天页面收不到Left消息,用户看不到任何回复。

第三层:死代码DevicesCoolkitManager

排查过程中还发现了一个意外情况:项目里有一个 DevicesCoolkitManager 类,它的 checkContent 方法中有针对 coolkit_single_open 的兜底逻辑——根据 dev_name 是否包含"日程"等关键字来跳转对应页面:

private fun checkContent(type: DeviceType, valueStr: String?, ...): Boolean {
    if (type == DeviceType.singleOpen) {
        if (valueStr!!.contains(IotUtils.schedule_fun1) || valueStr.contains(IotUtils.schedule_fun2)) {
            finishVoiceInteraction()
            openSchedulePage()
            isNormal = false
        }
        // ... 电话本、通话记录、设置等
    }
}

但搜索全项目,DevicesCoolkitManager 没有任何地方引用它——这是一段死代码!它本该处理这个场景,却从未被调用。

解决方案

SemanticHandlercoolkit_single_open 分支增加 dev_name 二次匹配,将误识别的指令兜底到正确的应用功能:

"coolkit_single_open" -> {
    // 讯飞可能把"打开今日日程/电话本/通话记录/设置/抖音/喜马拉雅"等
    // 误识别为 IoT 单设备打开意图,根据 dev_name 做应用功能兜底
    val devName = semanticObj.semantic?.optJSONArray("slots")?.let { slots ->
        (0 until slots.length())
            .map { slots.optJSONObject(it) }
            .firstOrNull { it?.optString("name") == "dev_name" }
            ?.optString("value") ?: ""
    } ?: ""
    when {
        devName.contains(IotUtils.schedule_fun1) || devName.contains(IotUtils.schedule_fun2) -> {
            finishVoiceInteraction()
            openSchedulePage()
        }
        devName.contains(IotUtils.call_book_fun1) || devName.contains(IotUtils.call_book_fun2) -> {
            finishVoiceInteraction()
            openPhoneBook()
        }
        devName.contains(IotUtils.open_call_records1) || devName.contains(IotUtils.open_call_records2) ||
        devName.contains(IotUtils.open_call_records3) || devName.contains(IotUtils.open_call_records4) -> {
            waitFinishVoiceInteraction2Next(object : NextAIUIWakeUp {
                override fun next() { openCallRecord() }
            })
        }
        devName.contains(IotUtils.app_setting_fun1) -> {
            finishVoiceInteraction()
            openSetting()
        }
        // ... 抖音、喜马拉雅等
    }
    return false
}

其中 IotUtils 定义了各功能的关键字:

object IotUtils {
    const val schedule_fun1 = "日程"
    const val schedule_fun2 = "提醒"
    const val call_book_fun1 = "电话本"
    const val call_book_fun2 = "通讯录"
    // ...
}

收获与思考

1. NLP意图识别不是100%准确的

讯飞AIUI的语义识别基于模型概率,"打开今日日程"在IoT技能的模板 {start}{dev_name} 下也能匹配,且置信度0.87够高,就被分到了 coolkit_single_open。我们不能假设NLP总能给出正确意图,必须在应用层做兜底。

2. 理解完整数据流才能定位"无响应"问题

"没有回复"这种症状最让人头疼——没有报错,没有异常,就是没反应。这次排查的关键是追踪了完整的数据流:cbm_semantic → SemanticHandler.parseSemantic → SemanticUtil(cbmSemanticHit=true) → nlp到达但不替换answer → nlp2TTS(ttsText为空) → return → 不addMessage。每一步都"正确"地执行了,但最终结果是用户什么都看不到。只有理解了每一步的状态转换,才能找到断点在哪。

3. 死代码是技术债的信号

DevicesCoolkitManager 有完整的兜底逻辑却从未被调用,说明这段代码是在某次重构中被遗弃的。如果当时删除了它,至少能在 coolkit_single_open 分支留个注释提示需要处理。死代码不仅占用空间,还会误导排查方向——我看到它时差点以为"已经处理了为什么没用",浪费了时间。

4. 兜底策略的设计原则

对于NLP误识别的兜底,核心原则是:不改变正常路径,只在误识别分支增加二次匹配coolkit_single_open 本意是控制IoT设备,我们只在 dev_name 包含已知应用关键字时才拦截,其他情况仍然 return false 走原逻辑。这样既修复了误识别问题,又不影响真正的IoT设备控制。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容