基于Rokid CXR-M SDK实现AR智能助手应用:让AI大模型走进AR眼镜

欢迎来到我的博客,代码的世界里,每一行都是一个故事

🎏:你只管努力,剩下的交给时间

🏠 :小破站

基于Rokid CXR-M SDK实现AR智能助手应用:让AI大模型走进AR眼镜

    • 一、为什么做这个
    • [二、认识CXR-M SDK的AI场景能力](#二、认识CXR-M SDK的AI场景能力)
    • 三、技术架构设计
    • 四、开发环境配置
    • 五、核心功能实现
      • [1. SDK初始化与AI事件监听](#1. SDK初始化与AI事件监听)
        • [1.1 实战案例:在Activity中完整使用](#1.1 实战案例:在Activity中完整使用)
      • [2. 语音识别与ASR推送](#2. 语音识别与ASR推送)
        • [2.1 实战案例:完整的ASR服务实现](#2.1 实战案例:完整的ASR服务实现)
      • [3. AI对话与结果推送](#3. AI对话与结果推送)
        • [3.1 实战案例:完整的AI服务实现](#3.1 实战案例:完整的AI服务实现)
      • [4. 对话历史管理](#4. 对话历史管理)
        • [4.1 实战案例:更多对话场景](#4.1 实战案例:更多对话场景)
      • [5. 错误处理与重试机制](#5. 错误处理与重试机制)
        • [5.1 实战案例:完整的错误处理流程](#5.1 实战案例:完整的错误处理流程)
    • 六、实用功能扩展
      • [1. 快捷指令](#1. 快捷指令)
      • [2. 场景切换](#2. 场景切换)
      • [3. 离线降级](#3. 离线降级)
    • 七、性能优化
      • [1. ASR音频缓冲优化](#1. ASR音频缓冲优化)
      • [2. AI响应缓存](#2. AI响应缓存)
      • [3. 预连接优化](#3. 预连接优化)
    • 八、实际踩过的坑
      • [1. 忘记调用notifyAsrEnd导致卡死](#1. 忘记调用notifyAsrEnd导致卡死)
      • [2. TTS播放中断](#2. TTS播放中断)
      • [3. 蓝牙权限问题](#3. 蓝牙权限问题)
      • [4. 对话历史爆炸](#4. 对话历史爆炸)
      • [5. 网络切换断连](#5. 网络切换断连)
    • 九、总结

一、为什么做这个

平时开发时经常需要查资料、问问题,但掏手机打字太慢,语音助手又得低头看屏幕。开会时想快速查个技术细节,拿手机操作容易被误会不专心。想着能不能把AI大模型接入AR眼镜,边走边问,答案直接显示在视野里?

正好看到Rokid CXR-M SDK v1.0.1新增了AI助手场景能力,支持语音交互、结果推送。试着做了一个AR AI助手,现在走路、开会都能随时问AI,双手完全解放。

本文记录我用Rokid CXR-M SDK开发AR AI助手的过程,包括SDK集成、AI对接和实际踩坑经验。

二、认识CXR-M SDK的AI场景能力

Rokid CXR-M SDK简介

根据Rokid官方文档,CXR-M SDK是面向移动端的开发工具包,主要用于构建手机端与Rokid Glasses的控制和协同应用。我用的版本是v1.0.1(2025年8月25日更新)。

相关资源:

从架构图可以看到,CXR-M SDK位于手机端,通过CXR-A Channel与眼镜端的YodaOS-Sprite系统通信。手机端负责复杂计算(ASR、AI),眼镜端负责显示和交互。这个设计很合理,把计算密集的任务放在手机,眼镜只管显示,功耗和性能都能保证。

AI场景的核心能力

根据Rokid官方SDK文档,AI场景提供以下能力:

✅ SDK提供的能力:

  • 监听AI按键事件(AiEventListener
  • 发送ASR内容到眼镜(sendAsrContent
  • 发送AI结果到眼镜(sendTtsContent
  • 获取眼镜相机图片(takeGlassPhoto
  • 错误状态通知(notifyNoNetworknotifyAiError等)

❌ SDK不提供的能力:

  • 语音识别引擎
  • AI大模型服务
  • TTS语音合成

📝 开发者需要做的:

自己对接ASR、AI、TTS服务,通过SDK的API将结果推送到眼镜显示。

我一开始还想着SDK会不会内置AI服务,后来发现这种设计其实更灵活。你可以选择通义千问、文心一言、Kimi等任何国产大模型,甚至可以根据场景切换(工作用通义千问、翻译用火山翻译)。SDK只负责通信协议,业务逻辑完全自己掌控。

三、技术架构设计

整体架构

我的方案是手机端集成所有AI能力,通过CXR-M SDK与眼镜通信:

整个流程的关键路径:用户触发 → 语音识别 → AI推理 → 结果推送 → 眼镜显示。

数据流转过程

技术选型说明

开发语言: Kotlin,使用协程处理异步操作

网络框架: Retrofit 2.9 + OkHttp 4.12

ASR服务: 腾讯云实时语音识别,支持WebSocket流式识别,延迟200-300ms

AI服务: 通义千问API,响应速度快,中文理解能力强

TTS服务: 阿里云语音合成,音色自然

SDK连接方式: 蓝牙连接

选择通义千问有几个原因:一是响应速度快,基本上1-2秒就能返回;二是中文理解能力确实不错,日常问题回答得挺准;三是价格便宜,个人开发成本可控。试过文心一言,感觉响应慢一点,最后还是选了通义千问。

四、开发环境配置

SDK集成

首先在settings.gradle.kts中添加Rokid Maven仓库:

kotlin 复制代码
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
        google()
        mavenCentral()
    }
}

在模块的build.gradle.kts中添加依赖:

kotlin 复制代码
android {
    compileSdk = 34
    defaultConfig {
        applicationId = "com.example.rokid.aiassistant"
        minSdk = 28
        targetSdk = 34
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

dependencies {
    // Rokid CXR-M SDK
    implementation("com.rokid.cxr:client-m:1.0.1")

    // 网络请求
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // 协程
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // ViewModel
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
}

权限配置

AndroidManifest.xml中声明必要权限:

xml 复制代码
<!-- 蓝牙权限 -->
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />

<!-- 音频权限 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />

配置这块比较简单,唯一要注意的是minSdk = 28(Android 9),这是Rokid SDK的最低要求。权限记得在运行时动态申请,尤其是蓝牙和录音权限,Android 12+必须动态申请,不然会崩溃。

五、核心功能实现

1. SDK初始化与AI事件监听

这是整个流程的入口。我在Application中初始化SDK:

kotlin 复制代码
class AiAssistantApp : Application() {
    override fun onCreate() {
        super.onCreate()
        RokidCXRManager.init(this) { success, error ->
            if (success) {
                Log.d(TAG, "SDK初始化成功")
            } else {
                Log.e(TAG, "SDK初始化失败: $error")
            }
        }
    }
}

然后设置AI事件监听器,这是整个流程的触发点:

kotlin 复制代码
private val aiEventListener = object : AiEventListener {
    override fun onAiKeyDown() {
        Log.d(TAG, "用户长按AI按键")
        vibrate()  // 震动反馈
        startAISession()
    }

    override fun onAiExit() {
        Log.d(TAG, "用户退出AI场景")
        stopRecording()
        cleanup()
    }

    override fun onAiKeyUp() {
        // 暂无作用
    }
}

fun initAiScene() {
    CxrApi.getInstance().setAiEventListener(aiEventListener)
}

这里我加了震动反馈,用户按下按键时手机会震动一下,给个提示。一开始没加,用户不确定有没有触发,加了之后体验好很多。

监听器设置好后,整个交互就建立起来了。用户在眼镜上长按AI按键,手机立即收到onAiKeyDown()回调。记得在Activity销毁时调用setAiEventListener(null)释放资源,不然会内存泄漏。

1.1 实战案例:在Activity中完整使用
kotlin 复制代码
class MainActivity : AppCompatActivity() {

    private lateinit var aiManager: AiAssistantManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 初始化AI管理器
        aiManager = AiAssistantManager(this)
        aiManager.init()

        // 手动触发按钮(用于测试)
        binding.btnTrigger.setOnClickListener {
            aiManager.startAISession()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        // 清理资源
        aiManager.cleanup()
    }
}

class AiAssistantManager(private val context: Context) {

    private val asrService = AsrService()
    private val aiService = AiService()
    private val ttsService = TtsService()

    fun init() {
        // 设置AI事件监听
        CxrApi.getInstance().setAiEventListener(aiEventListener)
    }

    fun startAISession() {
        Log.d(TAG, "开始AI会话")
        vibrate()
        startRecording()
    }

    fun cleanup() {
        CxrApi.getInstance().setAiEventListener(null)
        stopRecording()
        asrService.disconnect()
    }

    private fun vibrate() {
        val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            vibrator.vibrate(VibrationEffect.createOneShot(100, VibrationEffect.DEFAULT_AMPLITUDE))
        } else {
            vibrator.vibrate(100)
        }
    }

    companion object {
        private const val TAG = "AiAssistantManager"
    }
}

这样在Activity中使用就很简单,初始化一个Manager,设置监听,销毁时清理资源。

2. 语音识别与ASR推送

收到AI按键事件后,开始录音和识别。我用的是腾讯云的实时ASR,支持WebSocket流式识别。

首先配置AudioRecord:

kotlin 复制代码
val bufferSize = AudioRecord.getMinBufferSize(
    16000,
    AudioFormat.CHANNEL_IN_MONO,
    AudioFormat.ENCODING_PCM_16BIT
)

val audioRecord = AudioRecord(
    MediaRecorder.AudioSource.MIC,
    16000,
    AudioFormat.CHANNEL_IN_MONO,
    AudioFormat.ENCODING_PCM_16BIT,
    bufferSize
)

然后边录音边发送到ASR服务:

kotlin 复制代码
fun startRecordingAndRecognition() {
    scope.launch {
        audioRecord.startRecording()
        val webSocket = asrService.connectWebSocket()

        while (isRecording) {
            val audioData = ByteArray(bufferSize)
            val readSize = audioRecord.read(audioData, 0, bufferSize)

            if (readSize > 0) {
                // 发送到ASR
                asrService.sendAudio(audioData)
            }
        }
    }
}

ASR识别完成后,通过SDK推送到眼镜:

kotlin 复制代码
override fun onAsrResult(result: AsrResult) {
    if (result.isFinal) {
        val question = result.text
        Log.d(TAG, "识别完成: $question")

        // 推送到眼镜显示
        val status = CxrApi.getInstance().sendAsrContent("Q: $question")
        if (status == ValueUtil.CxrStatus.REQUEST_SUCCEED) {
            // 通知ASR结束
            CxrApi.getInstance().notifyAsrEnd()

            // 调用AI
            getAIAnswer(question)
        }
    }
}

这里有个细节,我在问题前面加了"Q: "前缀,这样眼镜上显示"Q: 今天天气怎么样?",后面AI回答显示"A: ...",对话更清晰。一开始没加,问题和答案混在一起,不太好区分。

ASR这块最容易出问题的是识别为空的情况,必须调用notifyAsrNone()通知眼镜,不然眼镜会一直等待:

kotlin 复制代码
if (result.text.isEmpty()) {
    CxrApi.getInstance().notifyAsrNone()
    return
}
2.1 实战案例:完整的ASR服务实现
kotlin 复制代码
class AsrService {
    private var webSocket: WebSocket?  = null
    private var callback: ((AsrResult) -> Unit)? = null
    private val client = OkHttpClient()

    fun startRecognition(onResult: (AsrResult) -> Unit) {
        this.callback = onResult

        val request = Request.Builder()
            .url("wss://asr.cloud.tencent.com/asr/v2/...")
            .build()

        webSocket = client.newWebSocket(request, object : WebSocketListener() {
            override fun onOpen(webSocket: WebSocket, response: Response) {
                Log.d(TAG, "ASR WebSocket已连接")
                // 发送开始信号
                sendStartSignal()
            }

            override fun onMessage(webSocket: WebSocket, text: String) {
                // 解析ASR结果
                val result = parseAsrResponse(text)
                callback?.invoke(result)
            }

            override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
                Log.e(TAG, "ASR连接失败", t)
                callback?.invoke(AsrResult("", false, -1))
            }
        })
    }

    fun sendAudio(audioData: ByteArray) {
        val base64 = Base64.encodeToString(audioData, Base64.NO_WRAP)
        val json = """{"data": "$base64"}"""
        webSocket?.send(json)
    }

    fun stopRecognition() {
        webSocket?.send("""{"end": true}""")
        webSocket?.close(1000, "正常关闭")
    }
}

data class AsrResult(
    val text: String,
    val isFinal: Boolean,
    val code: Int
)

使用示例:

kotlin 复制代码
class RecordingManager(context: Context) {

    private val asrService = AsrService()
    private var audioRecord: AudioRecord? = null
    private var isRecording = false

    fun startRecording() {
        // 初始化AudioRecord
        val bufferSize = AudioRecord.getMinBufferSize(16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT)
        audioRecord = AudioRecord(MediaRecorder.AudioSource.MIC, 16000, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, bufferSize)

        // 启动ASR
        asrService.startRecognition { result ->
            if (result.isFinal) {
                // 识别完成
                onRecognitionComplete(result.text)
            } else {
                // 中间结果,实时显示
                onPartialResult(result.text)
            }
        }

        // 开始录音
        audioRecord?.startRecording()
        isRecording = true

        // 读取音频数据
        thread {
            val buffer = ByteArray(bufferSize)
            while (isRecording) {
                val readSize = audioRecord?.read(buffer, 0, bufferSize) ?: 0
                if (readSize > 0) {
                    asrService.sendAudio(buffer)
                }
            }
        }
    }

    fun stopRecording() {
        isRecording = false
        audioRecord?.stop()
        audioRecord?.release()
        asrService.stopRecognition()
    }

    private fun onRecognitionComplete(text: String) {
        Log.d(TAG, "识别完成: $text")
        // 推送到眼镜
        CxrApi.getInstance().sendAsrContent("Q: $text")
        CxrApi.getInstance().notifyAsrEnd()
    }

    private fun onPartialResult(text: String) {
        Log.d(TAG, "识别中: $text")
        // 可以实时显示部分结果
    }
}

这个例子展示了完整的录音和识别流程:初始化AudioRecord → 启动ASR → 循环读取音频 → 发送到ASR → 处理识别结果 → 推送到眼镜。

3. AI对话与结果推送

ASR识别出问题后,调用通义千问API获取答案。

配置Retrofit接口:

kotlin 复制代码
interface QianwenApi {
    @POST("v1/services/aigc/text-generation/generation")
    suspend fun chat(@Body request: ChatRequest): ChatResponse
}

data class ChatRequest(
    val model: String = "qwen-turbo",
    val input: Input,
    val parameters: Parameters = Parameters()
)

data class Input(
    val messages: List<Message>
)

data class Message(
    val role: String,  // system, user, assistant
    val content: String
)

调用通义千问:

kotlin 复制代码
suspend fun chat(question: String): String {
    // 添加到对话历史
    messages.add(Message("user", question))

    val response = qianwenApi.chat(
        ChatRequest(
            model = "qwen-turbo",
            input = Input(messages)
        )
    )

    val answer = response.output.text
    messages.add(Message("assistant", answer))

    return answer
}

获取答案后,推送到眼镜并播报:

kotlin 复制代码
fun displayAIResponse(answer: String) {
    // 1. 推送文本到眼镜
    CxrApi.getInstance().sendTtsContent("A: $answer")

    // 2. TTS播报
    ttsService.speak(answer, object : TtsCallback {
        override fun onFinished() {
            // 3. 播报结束后通知
            CxrApi.getInstance().notifyTtsAudioFinished()
        }
    })
}

完整流程代码:

kotlin 复制代码
fun processQuestion(question: String) {
    viewModelScope.launch {
        try {
            // 显示加载状态
            showLoading()

            // 调用AI
            val answer = withTimeout(10000) {
                aiService.chat(question)
            }

            // 推送结果
            displayAIResponse(answer)

        } catch (e: TimeoutCancellationException) {
            CxrApi.getInstance().notifyAiError()
            showToast("AI响应超时,请重试")
        } catch (e: Exception) {
            CxrApi.getInstance().notifyAiError()
            showToast("AI调用失败: ${e.message}")
        }
    }
}

通义千问这块有个优化,我加了本地缓存。高频问题("今天天气"、"设置闹钟")第一次问完缓存下来,下次直接返回,响应从2秒降到0.1秒。虽然答案可能不是最新的,但对于这种通用问题,缓存完全够用。

还有一个点,TTS播报必须等完全结束才能调用notifyTtsAudioFinished()。我一开始直接在sendTtsContent后面调用,结果TTS还没播完眼镜就关了。正确做法是在TTS的回调里调用。

3.1 实战案例:完整的AI服务实现
kotlin 复制代码
class AiService {
    private val qianwenApi: QianwenApi = RetrofitClient.create()
    private val conversationHistory = mutableListOf<Message>()

    init {
        // 初始化系统提示词
        conversationHistory.add(Message("system", "你是AR眼镜助手,回答简洁,50字以内"))
    }

    suspend fun chat(question: String): String {
        // 添加用户消息
        conversationHistory.add(Message("user", question))

        // 调用API
        val response = qianwenApi.chat(ChatRequest(
            model = "qwen-turbo",
            input = Input(conversationHistory)
        ))

        val answer = response.output.text

        // 添加助手回答
        conversationHistory.add(Message("assistant", answer))

        return answer
    }
}

object RetrofitClient {
    private const val BASE_URL = "https://dashscope.aliyuncs.com/"
    private const val API_KEY = "your-api-key"

    fun create(): QianwenApi {
        val client = OkHttpClient.Builder()
            .addInterceptor { chain ->
                val request = chain.request().newBuilder()
                    .addHeader("Authorization", "Bearer $API_KEY")
                    .addHeader("Content-Type", "application/json")
                    .build()
                chain.proceed(request)
            }
            .build()

        return Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(QianwenApi::class.java)
    }
}

使用示例:完整的问答流程

kotlin 复制代码
class AiChatManager(private val context: Context) {

    private val aiService = AiService()
    private val ttsService = TtsService()

    // 场景1:简单问答
    suspend fun simpleQuestion() {
        val question = "今天天气怎么样"

        // 显示问题到眼镜
        CxrApi.getInstance().sendAsrContent("Q: $question")
        CxrApi.getInstance().notifyAsrEnd()

        try {
            // 调用AI
            val answer = aiService.chat(question)

            // 显示答案
            CxrApi.getInstance().sendTtsContent("A: $answer")

            // TTS播报
            ttsService.speak(answer) {
                CxrApi.getInstance().notifyTtsAudioFinished()
            }

        } catch (e: Exception) {
            CxrApi.getInstance().notifyAiError()
        }
    }

    // 场景2:多轮对话
    suspend fun multiRoundChat() {
        // 第一轮
        var answer = aiService.chat("介绍一下Kotlin协程")
        displayAnswer(answer)

        delay(3000)  // 等用户看完

        // 第二轮,AI能理解上下文
        answer = aiService.chat("它和线程有什么区别")
        displayAnswer(answer)  // AI会知道"它"指的是协程

        delay(3000)

        // 第三轮
        answer = aiService.chat("给个简单例子")
        displayAnswer(answer)  // AI会给出协程的例子
    }

    // 场景3:带缓存的问答
    private val cache = LruCache<String, String>(50)

    suspend fun questionWithCache(question: String) {
        // 先查缓存
        val normalized = question.trim().lowercase()
        cache.get(normalized)?.let { cachedAnswer ->
            Log.d(TAG, "缓存命中")
            displayAnswer(cachedAnswer)
            return
        }

        // 缓存未命中,调用AI
        val answer = aiService.chat(question)
        cache.put(normalized, answer)
        displayAnswer(answer)
    }

    // 场景4:错误处理
    suspend fun questionWithErrorHandling(question: String) {
        var retryCount = 0
        val maxRetry = 3

        while (retryCount < maxRetry) {
            try {
                val answer = withTimeout(10000) {
                    aiService.chat(question)
                }
                displayAnswer(answer)
                return

            } catch (e: TimeoutCancellationException) {
                retryCount++
                Log.w(TAG, "超时,第${retryCount}次重试")
                if (retryCount >= maxRetry) {
                    CxrApi.getInstance().notifyAiError()
                    showToast("AI响应超时,请重试")
                }
            } catch (e: IOException) {
                CxrApi.getInstance().notifyNoNetwork()
                showToast("网络异常")
                return
            }
        }
    }

    private fun displayAnswer(answer: String) {
        CxrApi.getInstance().sendTtsContent("A: $answer")
        ttsService.speak(answer) {
            CxrApi.getInstance().notifyTtsAudioFinished()
        }
    }
}

// TTS服务实现
class TtsService {
    private val synthesizer: SpeechSynthesizer = createSynthesizer()

    fun speak(text: String, onFinished: () -> Unit) {
        synthesizer.setParameter(SpeechConstant.VOICE_NAME, "xiaoyan")
        synthesizer.setParameter(SpeechConstant.SPEED, "50")

        synthesizer.startSpeaking(text, object : SynthesizerListener {
            override fun onCompleted(p0: SpeechError?) {
                onFinished()
            }

            override fun onSpeakBegin() { }
            override fun onBufferProgress(p0: Int, p1: Int, p2: Int, p3: String?) { }
            override fun onSpeakPaused() { }
            override fun onSpeakResumed() { }
            override fun onSpeakProgress(p0: Int, p1: Int, p2: Int) { }
        })
    }
}

这些示例展示了不同场景的实际使用:

  • 简单问答:最基础的流程
  • 多轮对话:AI能理解上下文
  • 带缓存:高频问题快速响应
  • 错误处理:网络异常和超时重试

4. 对话历史管理

为了实现多轮对话,我加了对话历史管理:

kotlin 复制代码
class ConversationManager {
    private val messages = LinkedList<Message>()
    private val MAX_HISTORY = 10  // 最多保留10轮

    init {
        // 设置系统提示词
        messages.add(Message(
            "system",
            "你是一个AR眼镜智能助手,回答要简洁准确,每次回答控制在50字以内"
        ))
    }

    fun addUserMessage(text: String) {
        messages.add(Message("user", text))
        trimHistory()
    }

    fun addAssistantMessage(text: String) {
        messages.add(Message("assistant", text))
        trimHistory()
    }

    private fun trimHistory() {
        // 保留系统提示词 + 最近10轮对话
        while (messages.size > MAX_HISTORY * 2 + 1) {
            // 删除第二条(保留第一条系统提示)
            if (messages.size > 1) {
                messages.removeAt(1)
            }
        }
    }

    fun getMessages() = messages.toList()

    fun clear() {
        val systemPrompt = messages.first()
        messages.clear()
        messages.add(systemPrompt)
    }
}

系统提示词我加了"回答控制在50字以内",因为眼镜显示区域有限,答案太长显示不全。实测下来,通义千问对这个限制遵守得还不错。

使用示例:

kotlin 复制代码
val conversation = ConversationManager()

// 第一轮
conversation.addUserMessage("今天天气怎么样?")
val answer1 = qianwen.chat(conversation.getMessages())
conversation.addAssistantMessage(answer1)

// 第二轮(可以理解上下文)
conversation.addUserMessage("适合出门吗?")  // AI能理解是在问天气
val answer2 = qianwen.chat(conversation.getMessages())
conversation.addAssistantMessage(answer2)
4.1 实战案例:更多对话场景
kotlin 复制代码
// 场景1:技术咨询场景
suspend fun technicalConsulting() {
    val conversation = ConversationManager()

    // 第一轮:问概念
    conversation.addUserMessage("什么是Kotlin协程?")
    var answer = aiService.chat(conversation.getMessages())
    displayAnswer(answer)  // AI回答:协程是轻量级线程...

    delay(2000)

    // 第二轮:深入细节
    conversation.addUserMessage("它和Java的线程有什么区别?")
    answer = aiService.chat(conversation.getMessages())
    displayAnswer(answer)  // AI知道"它"指协程,对比协程和线程

    delay(2000)

    // 第三轮:要代码
    conversation.addUserMessage("给个简单的使用例子")
    answer = aiService.chat(conversation.getMessages())
    displayAnswer(answer)  // AI给出协程的代码示例
}

// 场景2:连续追问场景
suspend fun continuousQuestioning() {
    val conversation = ConversationManager()

    // 问题链条
    val questions = listOf(
        "Retrofit是什么?",
        "它怎么集成到项目?",  // AI理解"它"是Retrofit
        "有什么常见的坑吗?",    // AI知道在问Retrofit的坑
        "给个完整的配置例子"     // AI给Retrofit配置
    )

    for (question in questions) {
        conversation.addUserMessage(question)
        val answer = aiService.chat(conversation.getMessages())
        conversation.addAssistantMessage(answer)
        displayAnswer(answer)
        delay(2000)
    }
}

// 场景3:切换话题场景
suspend fun topicSwitching() {
    val conversation = ConversationManager()

    // 话题1:Kotlin
    conversation.addUserMessage("Kotlin有什么特点?")
    var answer = aiService.chat(conversation.getMessages())
    displayAnswer(answer)

    delay(2000)

    // 话题2:切换到Android
    conversation.addUserMessage("Android Jetpack是什么?")
    answer = aiService.chat(conversation.getMessages())
    displayAnswer(answer)  // AI能识别话题切换

    delay(2000)

    // 回到话题1
    conversation.addUserMessage("刚才说的Kotlin,它的协程怎么用?")
    answer = aiService.chat(conversation.getMessages())
    displayAnswer(answer)  // AI能找回之前的Kotlin话题
}

// 场景4:历史清理场景
suspend fun historyCleanup() {
    val conversation = ConversationManager()

    // 连续问10轮
    repeat(10) { i ->
        conversation.addUserMessage("问题${i + 1}")
        val answer = aiService.chat(conversation.getMessages())
        conversation.addAssistantMessage(answer)

        Log.d(TAG, "当前历史条数: ${conversation.getMessages().size}")
        // 输出:3, 5, 7, 9, 11, 13, 15, 17, 19, 21(system + 10轮对话)
    }

    // 第11轮,会自动清理最早的对话
    conversation.addUserMessage("问题11")
    val answer = aiService.chat(conversation.getMessages())
    conversation.addAssistantMessage(answer)

    Log.d(TAG, "清理后历史条数: ${conversation.getMessages().size}")
    // 输出:21(保持不变,最早的对话被删除了)
}

// 场景5:对话导出和恢复
class ConversationManager {
    // ... 之前的代码

    // 导出对话历史
    fun exportHistory(): String {
        return Gson().toJson(messages)
    }

    // 恢复对话历史
    fun restoreHistory(json: String) {
        val restored = Gson().fromJson<List<Message>>(json, object : TypeToken<List<Message>>() {}.type)
        messages.clear()
        messages.addAll(restored)
    }

    // 保存到本地
    fun saveToLocal(context: Context) {
        val json = exportHistory()
        val file = File(context.filesDir, "conversation_${System.currentTimeMillis()}.json")
        file.writeText(json)
    }

    // 从本地加载
    fun loadFromLocal(context: Context, fileName: String) {
        val file = File(context.filesDir, fileName)
        if (file.exists()) {
            val json = file.readText()
            restoreHistory(json)
        }
    }
}

// 使用导出恢复
suspend fun exportAndRestore() {
    val conversation = ConversationManager()

    // 对话几轮
    conversation.addUserMessage("Kotlin协程怎么用?")
    val answer1 = aiService.chat(conversation.getMessages())
    conversation.addAssistantMessage(answer1)

    conversation.addUserMessage("给个例子")
    val answer2 = aiService.chat(conversation.getMessages())
    conversation.addAssistantMessage(answer2)

    // 保存对话
    conversation.saveToLocal(context)

    // ... 应用重启后

    // 恢复对话
    val newConversation = ConversationManager()
    newConversation.loadFromLocal(context, "conversation_xxx.json")

    // 继续对话,AI还记得之前的内容
    newConversation.addUserMessage("刚才的例子能改成Java版本吗?")
    val answer3 = aiService.chat(newConversation.getMessages())
    // AI知道"刚才的例子"指什么
}

这些实战场景展示了对话管理在不同情况下的使用:

  • 技术咨询:从概念到细节的递进式对话
  • 连续追问:AI理解代词指代
  • 切换话题:AI能识别并跟踪多个话题
  • 历史清理:自动清理旧对话,控制Token数量
  • 导出恢复:保存和恢复对话历史,跨会话使用

5. 错误处理与重试机制

实际使用中,网络问题是最常见的。我加了错误处理和重试:

kotlin 复制代码
suspend fun chatWithRetry(question: String, maxRetry: Int = 2): String {
    var lastError: Exception? = null

    repeat(maxRetry) { attempt ->
        try {
            return withTimeout(10000) {
                qianwenApi.chat(question)
            }
        } catch (e: IOException) {
            lastError = e
            Log.w(TAG, "AI调用失败,第${attempt + 1}次重试")
            delay(1000)  // 等1秒再重试
        } catch (e: TimeoutCancellationException) {
            lastError = e
            Log.w(TAG, "AI调用超时,第${attempt + 1}次重试")
            delay(1000)
        }
    }

    // 重试失败,通知眼镜
    when (lastError) {
        is IOException -> CxrApi.getInstance().notifyNoNetwork()
        else -> CxrApi.getInstance().notifyAiError()
    }

    throw lastError ?: Exception("Unknown error")
}

错误类型分了几种:

  • 网络错误:调用notifyNoNetwork()
  • AI服务错误:调用notifyAiError()
  • ASR错误:调用notifyAsrError()

眼镜端会根据不同错误显示不同提示,体验比统一显示"错误"好很多。

还有一个细节,超时时间我设置了10秒。通义千问正常2秒内返回,10秒基本可以判定是网络问题或服务异常了。太短容易误判,太长用户等不及。

5.1 实战案例:完整的错误处理流程
kotlin 复制代码
class RobustAiManager(private val context: Context) {

    private val aiService = AiService()
    private var retryStrategy = RetryStrategy.EXPONENTIAL

    enum class RetryStrategy {
        LINEAR,      // 线性重试:1s, 2s, 3s
        EXPONENTIAL, // 指数退避:1s, 2s, 4s, 8s
        FIXED        // 固定间隔:都是2s
    }

    // 场景1:带重试的问答
    suspend fun askWithRetry(question: String): Result<String> {
        var attempt = 0
        val maxAttempts = 3

        while (attempt < maxAttempts) {
            try {
                val answer = withTimeout(10000) {
                    aiService.chat(question)
                }
                return Result.success(answer)

            } catch (e: TimeoutCancellationException) {
                attempt++
                Log.w(TAG, "第${attempt}次尝试超时")

                if (attempt >= maxAttempts) {
                    CxrApi.getInstance().notifyAiError()
                    return Result.failure(e)
                }

                // 根据策略等待
                val delayMs = when (retryStrategy) {
                    RetryStrategy.LINEAR -> attempt * 1000L
                    RetryStrategy.EXPONENTIAL -> (1L shl attempt) * 1000
                    RetryStrategy.FIXED -> 2000L
                }
                delay(delayMs)

            } catch (e: IOException) {
                CxrApi.getInstance().notifyNoNetwork()
                return Result.failure(e)
            }
        }

        return Result.failure(Exception("重试失败"))
    }

    // 场景2:网络状态监听
    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onAvailable(network: Network) {
            Log.d(TAG, "网络恢复,自动重试")
            CoroutineScope(Dispatchers.Main).launch {
                retryPendingRequest()
            }
        }

        override fun onLost(network: Network) {
            Log.d(TAG, "网络断开")
            CxrApi.getInstance().notifyNoNetwork()
        }
    }

    private var pendingQuestion: String? = null

    private suspend fun retryPendingRequest() {
        pendingQuestion?.let { question ->
            val result = askWithRetry(question)
            if (result.isSuccess) {
                pendingQuestion = null
                displayAnswer(result.getOrNull()!!)
            }
        }
    }

    fun registerNetworkListener() {
        val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
        cm.registerDefaultNetworkCallback(networkCallback)
    }

    // 场景3:降级策略
    private val localAnswers = mapOf(
        "今天天气" to "抱歉,网络异常,无法获取天气信息",
        "设置闹钟" to "请稍后重试设置闹钟",
        "sdk版本" to "当前使用 CXR-M SDK 1.0.1"
    )

    suspend fun askWithFallback(question: String): String {
        // 先尝试在线
        val onlineResult = askWithRetry(question)
        if (onlineResult.isSuccess) {
            return onlineResult.getOrNull()!!
        }

        // 在线失败,尝试本地
        localAnswers.forEach { (key, value) ->
            if (question.contains(key)) {
                Log.d(TAG, "使用本地答案")
                return value
            }
        }

        // 都没有,返回默认
        return "网络异常,请稍后重试"
    }

    // 场景4:错误分类处理
    suspend fun askWithDetailedError(question: String): String {
        return try {
            withTimeout(10000) {
                aiService.chat(question)
            }
        } catch (e: TimeoutCancellationException) {
            handleTimeout()
            "AI响应超时,请重试"
        } catch (e: IOException) {
            handleNetworkError()
            "网络连接失败"
        } catch (e: JsonSyntaxException) {
            handleParseError()
            "数据解析失败"
        } catch (e: HttpException) {
            when (e.code()) {
                401 -> {
                    handleAuthError()
                    "API认证失败"
                }
                429 -> {
                    handleRateLimitError()
                    "请求过于频繁,请稍后重试"
                }
                500 -> {
                    handleServerError()
                    "服务器错误"
                }
                else -> "未知错误: ${e.code()}"
            }
        } catch (e: Exception) {
            handleUnknownError(e)
            "未知错误"
        }
    }

    private fun handleTimeout() {
        CxrApi.getInstance().notifyAiError()
        // 记录超时日志
        Analytics.track("ai_timeout")
    }

    private fun handleNetworkError() {
        CxrApi.getInstance().notifyNoNetwork()
        // 提示用户检查网络
        showNetworkErrorDialog()
    }

    private fun handleParseError() {
        // 记录解析错误
        Log.e(TAG, "响应格式异常")
    }

    private fun handleAuthError() {
        // API Key可能过期
        Log.e(TAG, "API认证失败,检查API Key")
    }

    private fun handleRateLimitError() {
        // 触发限流,暂停一段时间
        CoroutineScope(Dispatchers.Main).launch {
            delay(60000)  // 等1分钟
        }
    }

    private fun handleServerError() {
        CxrApi.getInstance().notifyAiError()
    }

    private fun handleUnknownError(e: Exception) {
        Log.e(TAG, "未知错误", e)
        // 上报错误
        Crashlytics.recordException(e)
    }
}

// 场景5:完整的错误处理示例
class MainActivity : AppCompatActivity() {

    private lateinit var aiManager: RobustAiManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        aiManager = RobustAiManager(this)
        aiManager.registerNetworkListener()

        // 测试各种错误场景
        lifecycleScope.launch {
            testErrorHandling()
        }
    }

    private suspend fun testErrorHandling() {
        // 测试1:正常请求
        val result1 = aiManager.askWithRetry("Kotlin是什么")
        if (result1.isSuccess) {
            Log.d(TAG, "成功: ${result1.getOrNull()}")
        }

        delay(1000)

        // 测试2:超时重试
        val result2 = aiManager.askWithRetry("一个很复杂的问题...")
        if (result2.isFailure) {
            Log.e(TAG, "失败: ${result2.exceptionOrNull()?.message}")
        }

        delay(1000)

        // 测试3:降级处理
        val answer = aiManager.askWithFallback("今天天气怎么样")
        Log.d(TAG, "降级答案: $answer")

        delay(1000)

        // 测试4:详细错误
        val detailedAnswer = aiManager.askWithDetailedError("测试问题")
        Log.d(TAG, "详细处理: $detailedAnswer")
    }
}

这些错误处理案例涵盖了实际开发中常见的场景:

  • 重试策略:线性、指数退避、固定间隔三种策略
  • 网络监听:网络恢复时自动重试
  • 降级策略:在线失败时使用本地答案
  • 错误分类:针对不同错误类型做不同处理
  • 完整流程:从测试到实际使用的完整示例

六、实用功能扩展

1. 快捷指令

我加了一些快捷指令,提高效率:

kotlin 复制代码
fun processCommand(text: String): Boolean {
    return when {
        text.contains("设置闹钟") -> {
            val time = extractTime(text)  // 提取时间
            setAlarm(time)
            true
        }
        text.contains("发消息给") -> {
            val (person, message) = extractMessage(text)
            sendMessage(person, message)
            true
        }
        text.contains("导航到") -> {
            val destination = extractDestination(text)
            startNavigation(destination)
            true
        }
        else -> false
    }
}

这些指令不需要调用AI,直接本地处理,响应更快。比如"设置闹钟8点",直接解析出时间,调用系统API设置,0.1秒就完成了。

2. 场景切换

不同场景用不同的System Prompt:

kotlin 复制代码
enum class AiMode(val prompt: String) {
    GENERAL("你是AR眼镜助手"),
    PROGRAMMER("你是编程助手,擅长解答技术问题"),
    TRANSLATOR("你是翻译助手,提供中英文翻译"),
    MEETING("你是会议助手,帮助记录要点")
}

fun switchMode(mode: AiMode) {
    conversation.clear()
    conversation.addSystemMessage(mode.prompt)
    showToast("已切换到${mode.name}模式")
}

开会时切换到会议模式,AI会自动总结要点;写代码时切换编程模式,回答更专业。

3. 离线降级

网络不好时,用本地知识库兜底:

kotlin 复制代码
val localKnowledge = mapOf(
    "SDK版本" to "当前使用CXR-M SDK 1.0.1版本",
    "公司电话" to "客服电话:400-xxx-xxxx",
    "wifi密码" to "办公室WiFi密码:xxxxxx"
)

suspend fun answerQuestion(question: String): String {
    // 先查本地
    localKnowledge.forEach { (key, value) ->
        if (question.contains(key)) {
            return value
        }
    }

    // 本地没有,调用AI
    return try {
        chatWithRetry(question)
    } catch (e: Exception) {
        "网络异常,请稍后重试"
    }
}

公司内部高频问题都存本地,断网也能回答。

七、性能优化

1. ASR音频缓冲优化

一开始我每次read就发送音频,发现ASR识别很慢。后来改成累积40ms的音频再发送:

kotlin 复制代码
val BUFFER_DURATION_MS = 40
val SAMPLE_RATE = 16000
val BYTES_PER_MS = SAMPLE_RATE * 2 / 1000  // 16bit = 2 bytes
val CHUNK_SIZE = BUFFER_DURATION_MS * BYTES_PER_MS

val buffer = ByteArray(CHUNK_SIZE)
var offset = 0

while (isRecording) {
    val read = audioRecord.read(buffer, offset, CHUNK_SIZE - offset)
    offset += read

    if (offset >= CHUNK_SIZE) {
        asrService.send(buffer)
        offset = 0
    }
}

优化后ASR识别速度提升30%,从300ms降到200ms。

2. AI响应缓存

高频问题缓存结果:

kotlin 复制代码
val cache = LruCache<String, String>(100)

suspend fun chatWithCache(question: String): String {
    // 规范化问题(去空格、统一标点)
    val normalized = normalizeQuestion(question)

    // 查缓存
    cache.get(normalized)?.let {
        Log.d(TAG, "缓存命中: $question")
        return it
    }

    // 调用AI
    val answer = chat(question)
    cache.put(normalized, answer)
    return answer
}

3. 预连接优化

App启动时提前建立连接:

kotlin 复制代码
class AiAssistantApp : Application() {
    override fun onCreate() {
        super.onCreate()

        // SDK初始化
        RokidCXRManager.init(this)

        // 预热ASR连接
        GlobalScope.launch {
            delay(2000)  // 等SDK初始化完成
            asrService.preConnect()
        }

        // 预热AI连接
        GlobalScope.launch {
            aiService.warmup()  // 发送一个简单请求
        }
    }
}

第一次使用时,连接已经建好了,响应快1秒。

八、实际踩过的坑

1. 忘记调用notifyAsrEnd导致卡死

错误现象: ASR识别完成,眼镜一直显示"识别中...",AI流程无法继续

原因: 忘记调用notifyAsrEnd()

解决:

kotlin 复制代码
CxrApi.getInstance().sendAsrContent(text)
CxrApi.getInstance().notifyAsrEnd()  // 必须调用

2. TTS播放中断

错误现象: TTS还没播完就中断了

原因: 提前调用了notifyTtsAudioFinished()

解决: 必须在TTS回调中调用

kotlin 复制代码
ttsService.speak(text, object : TtsCallback {
    override fun onFinished() {
        CxrApi.getInstance().notifyTtsAudioFinished()
    }
})

3. 蓝牙权限问题

错误现象: Android 12+上扫描设备直接崩溃

原因: 没有动态申请BLUETOOTH_CONNECT权限

解决:

kotlin 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    requestPermissions(arrayOf(
        Manifest.permission.BLUETOOTH_CONNECT,
        Manifest.permission.RECORD_AUDIO
    ), 100)
}

4. 对话历史爆炸

错误现象: 对话几十轮后,AI响应越来越慢

原因: 历史消息太多,Token超了

解决: 限制历史条数,只保留最近10轮

kotlin 复制代码
while (messages.size > MAX_HISTORY * 2 + 1) {
    messages.removeAt(1)  // 保留系统提示词
}

5. 网络切换断连

错误现象: WiFi切换到4G时,WebSocket断开

解决: 监听网络变化,自动重连

kotlin 复制代码
val networkCallback = object : ConnectivityManager.NetworkCallback() {
    override fun onAvailable(network: Network) {
        asrService.reconnect()
    }
}

这些坑都是实际遇到的,文档里没写。记录下来,后面的人少走弯路。

九、总结

用Rokid CXR-M SDK开发AR AI助手,核心是理解SDK的事件驱动机制。SDK负责眼镜端交互和显示,手机端负责ASR、AI、TTS的业务逻辑,通过sendAsrContentsendTtsContent等API协同工作。

几个关键点:

  • 监听AI按键事件是流程入口
  • ASR、AI、TTS都是异步操作,用协程处理
  • 每个阶段完成后必须调用对应的通知方法
  • 通义千问响应快、理解好,适合AR场景
  • 对话历史要限制条数,避免Token爆炸
  • 错误处理和重试机制必不可少

整体开发体验不错,SDK API清晰,文档详细。最大的收获是体验到了AR+AI的魅力:边走边问,答案直接显示在视野里。这可能就是未来交互的方向,AI不在手机上,而在眼镜里。

相关推荐
hacker7072 小时前
openGauss 在K12教育场景的数据处理测评:CASE WHEN 实现高效分类
人工智能·分类·数据挖掘
暖光资讯2 小时前
前行者获2025抖音最具影响力品牌奖,亮相上海ZFX装备前线展,引领外设行业“文化科技”新浪潮
人工智能·科技
guslegend2 小时前
第3章:SpringAI进阶之会话记忆实战
人工智能
陈橘又青2 小时前
100% AI 写的开源项目三周多已获得 800 star 了
人工智能·后端·ai·restful·数据
中杯可乐多加冰3 小时前
逻辑控制案例详解|基于smardaten实现OA一体化办公系统逻辑交互
人工智能·深度学习·低代码·oa办公·无代码·一体化平台·逻辑控制
IT_陈寒3 小时前
Redis实战:5个高频应用场景下的性能优化技巧,让你的QPS提升50%
前端·人工智能·后端
龙智DevSecOps解决方案3 小时前
Perforce《2025游戏技术现状报告》Part 1:游戏引擎技术的广泛影响以及生成式AI的成熟之路
人工智能·unity·游戏引擎·游戏开发·perforce
大佬,救命!!!3 小时前
更换适配python版本直接进行机器学习深度学习等相关环境配置(非仿真环境)
人工智能·python·深度学习·机器学习·学习笔记·详细配置
星空的资源小屋4 小时前
VNote:程序员必备Markdown笔记神器
javascript·人工智能·笔记·django