基于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不在手机上,而在眼镜里。

相关推荐
风象南25 分钟前
Claude Code这个隐藏技能,让我告别PPT焦虑
人工智能·后端
Mintopia1 小时前
OpenClaw 对软件行业产生的影响
人工智能
陈广亮2 小时前
构建具有长期记忆的 AI Agent:从设计模式到生产实践
人工智能
会写代码的柯基犬2 小时前
DeepSeek vs Kimi vs Qwen —— AI 生成俄罗斯方块代码效果横评
人工智能·llm
Mintopia2 小时前
OpenClaw 是什么?为什么节后热度如此之高?
人工智能
爱可生开源社区2 小时前
DBA 的未来?八位行业先锋的年度圆桌讨论
人工智能·dba
叁两5 小时前
用opencode打造全自动公众号写作流水线,AI 代笔太香了!
前端·人工智能·agent
前端付豪5 小时前
LangChain记忆:通过Memory记住上次的对话细节
人工智能·python·langchain