背景
在老年关怀机器人应用中,语音交互是核心功能。我们接入了讯飞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_open 的 rc=0(命中),所以走第二个分支,cbmSemanticHit = true,aiuiParseResult 被设为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 没有任何地方引用它——这是一段死代码!它本该处理这个场景,却从未被调用。
解决方案
在 SemanticHandler 的 coolkit_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设备控制。