一、为什么做这个
平时开发时经常需要查资料、问问题,但掏手机打字太慢,语音助手又得低头看屏幕。开会时想快速查个技术细节,拿手机操作容易被误会不专心。想着能不能把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官方文档
- 📦 SDK下载地址
- 🎓 若琪学院视频教程

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

✅ SDK提供的能力:
- 监听AI按键事件(
AiEventListener) - 发送ASR内容到眼镜(
sendAsrContent) - 发送AI结果到眼镜(
sendTtsContent) - 获取眼镜相机图片(
takeGlassPhoto) - 错误状态通知(
notifyNoNetwork、notifyAiError等)
❌ 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仓库:
scss
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
maven { url = uri("https://maven.rokid.com/repository/maven-public/") }
google()
mavenCentral()
}
}
在模块的build.gradle.kts中添加依赖:
scss
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:
ini
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()通知眼镜,不然眼镜会一直等待:
scss
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()
}
})
}
完整流程代码:
scss
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字以内",因为眼镜显示区域有限,答案太长显示不全。实测下来,通义千问对这个限制遵守得还不错。
使用示例:
scss
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 实战案例:更多对话场景
scss
// 场景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的音频再发送:
ini
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()
解决:
scss
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权限
解决:
scss
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestPermissions(arrayOf(
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.RECORD_AUDIO
), 100)
}
4. 对话历史爆炸
错误现象: 对话几十轮后,AI响应越来越慢
原因: 历史消息太多,Token超了
解决: 限制历史条数,只保留最近10轮
arduino
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的业务逻辑,通过sendAsrContent、sendTtsContent等API协同工作。
几个关键点:
- 监听AI按键事件是流程入口
- ASR、AI、TTS都是异步操作,用协程处理
- 每个阶段完成后必须调用对应的通知方法
- 通义千问响应快、理解好,适合AR场景
- 对话历史要限制条数,避免Token爆炸
- 错误处理和重试机制必不可少
整体开发体验不错,SDK API清晰,文档详细。最大的收获是体验到了AR+AI的魅力:边走边问,答案直接显示在视野里。这可能就是未来交互的方向,AI不在手机上,而在眼镜里。