跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
KotlinAI大前端算法

基于 Rokid 眼镜的 AI 天气应用、GPS 定位与旅游规划实现

基于 Kotlin 在 Rokid 眼镜端实现 AI 天气查询与旅游规划。集成高德地图 GPS 自动定位,支持“这里天气”等意图识别。构建多轮对话上下文管理,处理续播语义如“那边呢”。接入 Claude API 生成个性化旅游建议,优化 Prompt 设计以减少延迟。解决直辖市逆地理编码城市名为空、播报重叠等常见问题。提供完整调用流程与代码示例。

雾岛听风发布于 2026/3/27更新于 2026/6/215 浏览
基于 Rokid 眼镜的 AI 天气应用、GPS 定位与旅游规划实现

本文选用的技术包括:

GPS 自动定位:说「这里天气」自动获取位置,不用报城市名 多轮对话:说「上海呢」「那边呢」「再查一次」接续上轮查询 AI 旅游规划:接入 Claude API,天气播报后自动生成个性化旅游建议和行程规划 可直接复制的 Kotlin 代码(LocationHelper、ConversationContext、AiTravelPlanHelper) 踩坑经验:直辖市 adcode、续播语义识别、LLM 延迟控制


一、主要流程

本篇在 AiWeatherActivity(AI 语音查天气)基础上扩展,整体数据流如下:

新增三个辅助类,原有文件做对应改造:
新建文件职责
LocationHelper.ktGPS + 高德逆地理编码
ConversationContext.kt多轮对话上下文(含 5 分钟 TTL)
AiTravelPlanHelper.ktClaude API 旅游规划
文件创新点
AiIntentParser.kt+ GPS 触发词 + 续播意图解析 + 城市库扩充
WeatherViewHelper.kt+ tv_travel_plan 控件 + generateTravelPlanUpdateJson()
AiWeatherActivity.kt串联 GPS / Context / TravelPlan 完整调用链

图片

二、功能 A:GPS 自动定位

2.1 实现路径

用户说完'这里的天气'不想等 5 秒。缓存位置最多偏差几公里,对天气查询完全够用。

2.2 核心代码:LocationHelper.kt
class LocationHelper(private val context: Context) {
    interface LocationCallback {
        fun onCityCode(adcode: String, cityName: String, districtName: String)
        fun onError(reason: String)
    }

    fun getCurrentCityCode(callback: LocationCallback) {
        if (!hasLocationPermission()) {
            callback.onError("缺少定位权限")
            return
        }
        val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
        val lastKnown = getLastKnownLocation(manager)
        if (lastKnown != null) {
            reverseGeocode(lastKnown.latitude, lastKnown.longitude, callback)
        } else {
            requestSingleUpdate(manager, callback)
        }
    }

    @SuppressLint("MissingPermission")
    private fun getLastKnownLocation(manager: LocationManager): Location? = listOf(GPS_PROVIDER, NETWORK_PROVIDER, PASSIVE_PROVIDER).mapNotNull { runCatching { manager.getLastKnownLocation(it) }.getOrNull() }.maxByOrNull { it.time }

    private fun reverseGeocode(lat: Double, lon: Double, callback: LocationCallback) {
        // 注意:高德 API 格式是 "经度,纬度"(lon 在前)
        val url = "$REGEO_URL?location=$lon,$lat&key=$API_KEY&extensions=base&output=JSON"
        // OkHttp 调用 ...
        // 解析响应
        val component = json.getJSONObject("regeocode").getJSONObject("addressComponent")
        val adcode = component.optString("adcode")
        // 坑:直辖市 city 字段为空,需取 province
        val city = component.optString("city").ifEmpty { component.optString("province") }
        val district = component.optString("district")
        callback.onCityCode(adcode, city, district)
    }
}
2.3 意图识别:我们添加 GPS 的关键词

在 AiIntentParser 里加一批触发词,识别「天气」类意图:

private val LOCATION_KEYWORDS = listOf("这里", "附近", "当前", "我这", "这边", "当前位置", "我在哪", "这里的")
// 返回特殊常量 INTENT_LOCATION,交给 Activity 分支处理
const val INTENT_LOCATION = "__LOCATION__"
fun isLocationIntent(text: String): Boolean {
    val hasLocation = LOCATION_KEYWORDS.any { text.contains(it) }
    val hasWeather = text.contains("天气") || WEATHER_KEYWORDS.any { text.contains(it) }
    return hasLocation && hasWeather
}

Activity 侧处理分支:

private fun processRecognizedText(text: String) {
    val intent = intentParser.parseWeatherIntent(text, conversationContext)
    when {
        intent == null -> {
            updateStatus("未识别到查询意图,请说「XXX 天气」或「这里天气」")
            notifyAiError()
        }
        intent == AiIntentParser.INTENT_LOCATION -> handleLocationIntent()
        else -> queryWeather(intent, intentParser.getCityNameByCode(intent))
    }
}

private fun handleLocationIntent() {
    checkLocationPermission {
        locationHelper.getCurrentCityCode(object : LocationHelper.LocationCallback {
            override fun onCityCode(adcode: String, cityName: String, districtName: String) {
                val name = if (districtName.isNotBlank()) "$cityName$districtName" else cityName
                queryWeather(adcode, name)
            }
            override fun onError(reason: String) {
                notifyAiError()
            }
        })
    }
}

三、功能 B:对话上下文工程

3.1 核心数据结构
data class ConversationContext(
    val lastCityCode: String? = null,
    val lastCityName: String? = null,
    val turnCount: Int = 0,
    val lastQueryTimeMs: Long = 0L
) {
    companion object {
        private const val CONTEXT_TTL_MS = 5 * 60 * 1000L // 5 分钟
    }
    fun isValid(): Boolean = lastCityCode != null && (System.currentTimeMillis() - lastQueryTimeMs) < CONTEXT_TTL_MS
    fun advance(cityCode: String, cityName: String): ConversationContext = copy(
        lastCityCode = cityCode,
        lastCityName = cityName,
        turnCount = turnCount + 1,
        lastQueryTimeMs = System.currentTimeMillis()
    )
}

为什么设 5 分钟 TTL?其实就是经验估计:5 分钟内的续问大概率是连续对话;超过 5 分钟放下手机再拿起来,基本是新话题,不应复用旧上下文。

3.2 续播意图的两种形态
private val CONTINUATION_KEYWORDS = listOf("那呢", "那边", "那里呢", "那边呢", "再查", "继续", "再来一次", "重新查")
fun parseContinuationIntent(text: String, ctx: ConversationContext): String? {
    // 形态 1:续播词 → 直接复用上次城市
    if (CONTINUATION_KEYWORDS.any { text.contains(it) }) return ctx.lastCityCode
    // 形态 2:只有城市名,没有天气关键词(「福州呢」)→ 切换城市
    val hasWeather = WEATHER_KEYWORDS.any { text.contains(it) }
    if (!hasWeather) {
        val cityCode = extractCityCode(text)
        if (cityCode != null) return cityCode
    }
    return null
}

三种典型场景对照:

用户说解析结果
「福州呢」形态 2:切换到福州
「那边呢」形态 1:复用上次城市
「再查一次」形态 1:同城市重查
「明天北京天气」正常解析:北京(不走续播)

Activity 侧每次成功查询后更新上下文:

// queryWeather 成功回调中:conversationContext = conversationContext.advance(cityCode, cityName)

四、功能 C:AI 旅游规划

4.1 为什么用 LLM,而不是规则

用规则也能生成简单建议:

if (temp < 10) "适合室内景点"
else if (weather.contains("雨")) "建议带伞,推荐室内博物馆"
else "户外景点和公园都适合"

问题在于这是死的。同样是 25 度、晴天:北京故宫需要建议避开人流高峰;杭州西湖需要推荐骑行路线;三亚应该提醒防晒。LLM 能感知城市的旅游特色、气候背景,给出有地域差异的个性化旅游建议,这是规则系统做不到的。

4.2 核心代码:AiTravelPlanHelper.kt
class AiTravelPlanHelper {
    companion object {
        private const val API_URL = "https://api.anthropic.com/v1/messages"
        private const val CLAUDE_API_KEY = "YOUR_CLAUDE_API_KEY"
        private const val MODEL = "claude-haiku-4-5-20251001"
    }

    interface TravelPlanCallback {
        fun onTravelPlan(plan: String)
        fun onError(reason: String)
    }

    fun getTravelPlan(
        city: String, temp: String, weather: String, wind: String, humidity: String,
        callback: TravelPlanCallback
    ) {
        val systemPrompt = "你是一个专业的旅游规划助手,根据天气数据为用户生成简洁的中文旅游建议。" +
                "要求:语气自然友好,不超过 80 字,直接给建议,包含 1-2 个当地特色景点推荐,不要重复天气数据。"
        val userMessage = "城市:$city,气温:${temp}°C,天气:$weather," +
                "风力:$wind,湿度:${humidity}%,请给出旅游建议。"
        val requestBody = JSONObject().apply {
            put("model", MODEL)
            put("max_tokens", 300)
            put("system", systemPrompt)
            put("messages", JSONArray().apply {
                put(JSONObject().apply {
                    put("role", "user")
                    put("content", userMessage)
                })
            })
        }.toString()
        val request = Request.Builder()
            .url(API_URL)
            .addHeader("x-api-key", CLAUDE_API_KEY)
            .addHeader("anthropic-version", "2023-06-01")
            .addHeader("content-type", "application/json")
            .post(requestBody.toRequestBody("application/json".toMediaType()))
            .build()
        client.newCall(request).enqueue(object : Callback {
            override fun onResponse(call: Call, response: Response) {
                val body = response.body?.string() ?: return
                val plan = JSONObject(body).getJSONArray("content").getJSONObject(0).optString("text")?.trim()
                if (plan != null) callback.onTravelPlan(plan)
                else callback.onError("解析失败")
            }
            override fun onFailure(call: Call, e: IOException) {
                callback.onError("网络请求失败:${e.message}")
            }
        })
    }
}
4.3 Prompt 设计要点

「不要重复天气数据」这条约束很关键——用户刚听完 TTS 播报了天气,建议里再说「当前北京 25 度晴天,推荐去故宫」是纯粹的信息冗余。选 claude4.5 而不是更强的模型,是因为这个场景对「聪明程度」要求不高,对延迟的要求更高:用户说完天气查询,天气 TTS 结束后 2 秒内最好就能听到旅游建议。

4.4 与天气查询的串联时序
private fun queryWeather(cityCode: String, cityName: String) {
    weatherApiHelper.getWeatherForecast(cityCode, object : WeatherApiHelper.WeatherCallback {
        override fun onSuccess(response: WeatherApiResponse) {
            val live = response.lives?.firstOrNull()
            val forecast = response.forecasts?.firstOrNull()
            // 1. 打开眼镜端 Custom View(旅游规划区初始显示「规划获取中...」)
            openGlassCustomView(weatherViewHelper.generateWeatherViewJson(live, forecast))
            // 2. TTS 播报天气摘要
            sendWeatherTts(weatherViewHelper.generateWeatherTtsText(live, forecast))
            // 3. 更新多轮上下文
            conversationContext = conversationContext.advance(cityCode, cityName)
            // 4. 异步获取 AI 旅游规划(不阻塞天气播报)
            if (live != null) fetchAiTravelPlan(live, cityName)
        }
        override fun onError(error: String) {
            notifyAiError()
        }
    })
}

private fun fetchAiTravelPlan(live: Live, cityName: String) {
    val wind = "${live.winddirection ?: ""}${live.windpower ?: ""}".trim()
    travelPlanHelper.getTravelPlan(
        city = cityName,
        temp = live.temperature ?: "--",
        weather = live.weather ?: "--",
        wind = wind,
        humidity = live.humidity ?: "--",
        callback = object : AiTravelPlanHelper.TravelPlanCallback {
            override fun onTravelPlan(plan: String) {
                // 更新眼镜端旅游规划控件
                updateGlassCustomView(weatherViewHelper.generateTravelPlanUpdateJson(plan))
                // 延迟 2 秒播报,避免与天气 TTS 重叠
                Handler(Looper.getMainLooper()).postDelayed({
                    sendGlobalTtsContent(plan)
                }, 2000L)
            }
            override fun onError(reason: String) {
                updateGlassCustomView(weatherViewHelper.generateTravelPlanUpdateJson("旅游规划暂时无法获取"))
            }
        }
    )
}
4.5 眼镜端 Custom View 新增旅游规划区

WeatherViewHelper 在原有天气卡片末尾追加分割线和旅游规划控件:

// 分割线
children.put(createTextView(id = "tv_divider", text = "─────────────────", textSize = "10sp", textColor = "#FF444444", marginTop = "12dp", marginBottom = "8dp"))
// AI 旅游规划占位(成功后 updateCustomView 更新)
children.put(createTextView(id = ViewIds.TV_TRAVEL_PLAN, text = "规划获取中...", textSize = "14sp", textColor = "#FFFFCC00"))

仅更新旅游规划的方法:

fun generateTravelPlanUpdateJson(plan: String): String {
    val updates = JSONArray()
    updates.put(createUpdateAction(ViewIds.TV_TRAVEL_PLAN, "text", plan))
    return updates.toString()
}

五、踩坑与排错速查

直辖市逆地理编码返回城市名为空

高德 regeo 接口,北京/上海/天津/重庆的 city 字段是空字符串,城市信息在 province 里:

// 错误写法:val city = component.optString("city") // 北京返回 ""
// 正确写法:val city = component.optString("city").ifEmpty { component.optString("province") }
续播语义识别错误

判断关键是「有没有天气关键词」:

  • 有天气关键词(「北京天气」)→ 走正常解析,不走续播
  • 无天气关键词(「北京呢」)+ 有城市名 → 走续播,切换城市
  • 续播词(「那边呢」)→ 复用上次城市
AI 旅游规划延迟太长/播报重叠

Claude Haiku 响应通常在 1-2 秒。fetchAiTravelPlan 在天气查询成功后立即异步发起,规划播报延迟 2 秒,基本不会与天气 TTS 重叠。如果网络慢可以加 OkHttp 超时:

OkHttpClient.Builder().readTimeout(10, TimeUnit.SECONDS).build()
requestSingleUpdate 废弃警告

LocationManager.requestSingleUpdate() 在 API 30+ 被标记废弃,但本项目 minSdk=28,功能完全正常,用 @Suppress("DEPRECATION") 压警告即可。


六、完整调用示意

用户:「这里天气」 → isLocationIntent → INTENT_LOCATION → checkLocationPermission → LocationHelper.getCurrentCityCode → 高德 regeo → adcode=110105(朝阳区) → queryWeather("110105", "北京市朝阳区") → openCustomView(天气卡片,旅游规划区显示「获取中...」) → sendTtsContent(「北京市朝阳区当前天气,温度 25 度,晴...」) → context.advance("110105", "北京市朝阳区") → AiTravelPlanHelper.getTravelPlan → Claude API → updateCustomView(「今天天气舒适,推荐去朝阳公园散步,傍晚可以去三里屯逛逛」) → 2 秒后 sendGlobalTtsContent(「今天天气舒适,推荐去朝阳公园散步,傍晚可以去三里屯逛逛」)
用户:「福州呢」 → parseContinuationIntent → 形态 2,切换到福州 → queryWeather("310101", "福州") ...(同上流程)
用户:「那边呢」 → parseContinuationIntent → 形态 1,复用福州 → queryWeather("310101", "福州") ...

七、其他功能

做完这篇,其实有一个更大的问题浮现:眼镜应该做什么?

手机是工具——你主动去用它。眼镜是助手——它在你需要的时候说一句话,然后闭上嘴。

天气 + 旅游是最安全的起点:不打扰、有明确答案、TTS 一句话说完。但如果你想继续探索,以下方向都在这套框架上可以直接延伸:

  • 景点导览:到达景点后自动识别位置,推送景点介绍和历史背景
  • 行程提醒:结合日历,提前推送目的地天气和出行建议
  • 实时路况:结合地图数据,提供出行路线和实时交通信息
  • 多日规划:「那明天呢」处理预报字段,生成多天旅游行程
  • 美食推荐:结合当地特色美食,根据天气推荐适合的餐厅

目录

  1. 本文选用的技术包括:
  2. 一、主要流程
  3. 新增三个辅助类,原有文件做对应改造:
  4. 二、功能 A:GPS 自动定位
  5. 2.1 实现路径
  6. 2.2 核心代码:LocationHelper.kt
  7. 2.3 意图识别:我们添加 GPS 的关键词
  8. 三、功能 B:对话上下文工程
  9. 3.1 核心数据结构
  10. 3.2 续播意图的两种形态
  11. 四、功能 C:AI 旅游规划
  12. 4.1 为什么用 LLM,而不是规则
  13. 4.2 核心代码:AiTravelPlanHelper.kt
  14. 4.3 Prompt 设计要点
  15. 4.4 与天气查询的串联时序
  16. 4.5 眼镜端 Custom View 新增旅游规划区
  17. 五、踩坑与排错速查
  18. 直辖市逆地理编码返回城市名为空
  19. 续播语义识别错误
  20. AI 旅游规划延迟太长/播报重叠
  21. requestSingleUpdate 废弃警告
  22. 六、完整调用示意
  23. 七、其他功能
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • AI 绘画提示词工程与结构化创意引擎解析
  • 反无人机技术原理与反制手段详解
  • 2026 年国家自然科学基金 AI 使用声明撰写指南
  • OpenClaw 国内网络本地部署与飞书集成实战
  • Spring AI MCP Server 集成指南与源码解析
  • YOLOv3 C++ DLL 调用与 CUDA 依赖配置
  • OpenClaw 与 ToClaw 对比:AI 代理网关产品化体验
  • Python 实现 MCP 客户端调用高德地图天气查询
  • VB.NET 视频教程总结(八至十五单元)
  • AI 不是机器人:它到底是什么?
  • C++ 入门指南:历史、首个程序与命名空间详解
  • 顺序表与链表详解:结构、实现与算法分析
  • ADAU1452 开发教程 10:逻辑算法模块
  • 云原生容器技术入门:Docker 与 K8s 基本原理及用途
  • Python 基础语法与核心知识点速查手册
  • Java 数据结构实战:二叉树与哈希表核心解析
  • 计算机视觉高级应用与前沿技术发展
  • ModelSim 仿真软件安装与使用指南
  • Linux du 命令详解:精准探查文件和目录的磁盘占用
  • C++ STL 常用容器详解:Vector、Pair 到 Map 实战

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online