【征文计划】基于Rokid 眼镜 的AI天气应用+GPS定位+AI旅游规划

【征文计划】基于Rokid 眼镜 的AI天气应用+GPS定位+AI旅游规划

文章目录

本文我将解决这三件事,将天气应用升级为AI旅游规划助手,基于Rokid 眼镜 的AI天气应用+GPS定位+AI旅游规划的实现。🙏
在这里插入图片描述

本文选用的技术包括:

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

classLocationHelper(privateval context: Context){interface LocationCallback {funonCityCode(adcode: String, cityName: String, districtName: String)funonError(reason: String)}fungetCurrentCityCode(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")privatefungetLastKnownLocation(manager: LocationManager): Location?=listOf(GPS_PROVIDER, NETWORK_PROVIDER, PASSIVE_PROVIDER).mapNotNull{ runCatching { manager.getLastKnownLocation(it)}.getOrNull()}.maxByOrNull{ it.time }privatefunreverseGeocode(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 字段为空,需取 provinceval city = component.optString("city").ifEmpty{ component.optString("province")}val district = component.optString("district") callback.onCityCode(adcode, city, district)}}

2.3 意图识别:我们添加 GPS 的关键词

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

privateval LOCATION_KEYWORDS =listOf("这里","附近","当前","我这","这边","当前位置","我在哪","这里的")// 返回特殊常量 INTENT_LOCATION,交给 Activity 分支处理constval INTENT_LOCATION ="__LOCATION__"funisLocationIntent(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 侧处理分支:

privatefunprocessRecognizedText(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))}}privatefunhandleLocationIntent(){ checkLocationPermission { locationHelper.getCurrentCityCode(object: LocationHelper.LocationCallback{overridefunonCityCode(adcode: String, cityName: String, districtName: String){val name =if(districtName.isNotBlank())"$cityName$districtName"else cityName queryWeather(adcode, name)}overridefunonError(reason: String){notifyAiError()}})}}

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

3.1 核心数据结构

dataclassConversationContext(val lastCityCode: String?=null,val lastCityName: String?=null,val turnCount: Int =0,val lastQueryTimeMs: Long =0L){companionobject{privateconstval CONTEXT_TTL_MS =5*60*1000L// 5分钟}funisValid(): Boolean = lastCityCode !=null &&(System.currentTimeMillis()- lastQueryTimeMs) < CONTEXT_TTL_MS funadvance(cityCode: String, cityName: String): ConversationContext =copy( lastCityCode = cityCode, lastCityName = cityName, turnCount = turnCount +1, lastQueryTimeMs = System.currentTimeMillis())}

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

3.2 续播意图的两种形态

privateval CONTINUATION_KEYWORDS =listOf("那呢","那边","那里呢","那边呢","再查","继续","再来一次","重新查")privatefunparseContinuationIntent(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 }returnnull}

三种典型场景对照:

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

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

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

四、功能 C:AI 旅游规划

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

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

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

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

4.2 核心代码:AiTravelPlanHelper.kt

class AiTravelPlanHelper {companionobject{privateconstval API_URL ="https://api.anthropic.com/v1/messages"privateconstval CLAUDE_API_KEY ="YOUR_CLAUDE_API_KEY"privateconstval MODEL ="claude-haiku-4-5-20251001"}interface TravelPlanCallback {funonTravelPlan(plan: String)funonError(reason: String)}fungetTravelPlan( 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 {overridefunonResponse(call: Call, response: Response){val body = response.body?.string()?:returnval plan =JSONObject(body).getJSONArray("content").getJSONObject(0).optString("text")?.trim()if(plan !=null) callback.onTravelPlan(plan)else callback.onError("解析失败")}overridefunonFailure(call: Call, e: IOException){ callback.onError("网络请求失败: ${e.message}")}})}}

4.3 Prompt 设计要点

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

4.4 与天气查询的串联时序

privatefunqueryWeather(cityCode: String, cityName: String){ weatherApiHelper.getWeatherForecast(cityCode,object: WeatherApiHelper.WeatherCallback{overridefunonSuccess(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)}overridefunonError(error: String){notifyAiError()}})}privatefunfetchAiTravelPlan(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{overridefunonTravelPlan(plan: String){// 更新眼镜端旅游规划控件updateGlassCustomView(weatherViewHelper.generateTravelPlanUpdateJson(plan))// 延迟 2 秒播报,避免与天气 TTS 重叠Handler(Looper.getMainLooper()).postDelayed({sendGlobalTtsContent(plan)},2000L)}overridefunonError(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"// 金色,区别于普通信息))

仅更新旅游规划的方法:

fungenerateTravelPlanUpdateJson(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 一句话说完。但如果你想继续探索,以下方向都在这套框架上可以直接延伸:

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

Read more

idea中使用git

Git常用记录 * 一、Git基本配置 * 1.1 Git设置用户账号密码邮箱 * 1.2 Git生成秘钥 * 二、IDEA中使用Git * 1、配置idea * 2、克隆远程仓库 * 3、拉取代码、提交代码到本机仓库 * 4 、git项目回滚,回退版本 * 5、创建分支、合并分支 原文链接: IDEA 使用Git图文详解 一、Git基本配置 1.1 Git设置用户账号密码邮箱 查看信息: 查看用户名 :git config user.name 查看密码: git config user.password 查看邮箱:git config user.email 设置信息(–global

By Ne0inhk
Python实现开源AI模型引入及测试全过程

Python实现开源AI模型引入及测试全过程

文章目录 * 摘要 * 1. 引言:开源AI生态系统概述 * 1.1 开源AI的发展现状 * 1.2 技术栈选择 * 1.3 项目目标 * 2. 环境配置与项目初始化 * 2.1 系统要求 * 2.2 创建虚拟环境 * 2.3 依赖管理文件 * 2.4 安装依赖 * 2.5 项目结构 * 3. 模型原理与架构解析 * 3.1 BERT模型原理 * 3.1.1 Transformer编码器架构 * 3.2 Hugging Face Transformers架构 * 4. 数据准备与预处理 * 4.1 数据集选择与加载

By Ne0inhk
安装openclaw时出现npm error code ENOENT npm error syscall spawn git报错的解决方案

安装openclaw时出现npm error code ENOENT npm error syscall spawn git报错的解决方案

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为ZEEKLOG博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的知识进行总结与归纳,不仅形成深入且独到的理解,而且能够帮助新手快速入门。 本文主要介绍了安装openclaw时出现npm error code ENOENT npm error syscall spawn git报错的解决方案,希望能对使用openclaw的同学们有所帮助。 文章目录 * 1. 问题描述 * 2. 解决方案 1. 问题描述 今天在使用命令安装openclaw时,却出现了npm error code ENOENT和npm error syscall spawn git的错误提示,具体报错信息如下图所示: 在经过了亲身的实践后,终于找到了解决问题的方案,最终将逐步的操作过程总结如下。希望能对遇到同样bug的同学们有所帮助。

By Ne0inhk
【2026 最新】下载安装 Git 详细教程 (Windows)

【2026 最新】下载安装 Git 详细教程 (Windows)

一、下载Git 1.下载网址:Git - Downloads (git-scm.com) https://git-scm.com/downloads 网盘链接: 通过百度网盘分享的文件:Git-2.50.1-64-bit.exe 链接:https://pan.baidu.com/s/1lRrAifTBtCYXAA4qr31UkA?pwd=dy6bhttps://pan.baidu.com/s/1lRrAifTBtCYXAA4qr31UkA?pwd=dy6b提取码:dy6b 2.等下载完成,找到下载文件的位置,双击打开安装向导 二、安装Git 1.许可声明点击Next 2.选择安装位置 记住这个位置接下来要用到 3.选择组件 勾选添加在桌面上,

By Ne0inhk