【征文计划】基于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

“现在的AI就像1880年的笨重工厂!”微软CSO斯坦福泼冷水:别急着造神

“现在的AI就像1880年的笨重工厂!”微软CSO斯坦福泼冷水:别急着造神

大模型仍未对上商业的齿轮? 编译 | 王启隆 来源 | youtu.be/aWqfH0aSGKI 出品丨AI 科技大本营(ID:rgznai100) 现在的硅谷,空气里都飘着一股“再不上车就晚了”的焦躁感。 最近 OpenClaw 风头正旺,强势登顶 GitHub,终结了 React 神话,许多人更是觉得“AI 自己干活赚钱”的日子就在明天了。 特别是在斯坦福商学院(GSB)这种地方,台下坐着的都是成天琢磨怎么用下一个技术风口搞个独角兽出来的狠人。 微软的首席科学官(CSO)Eric Horvitz 被请到了这个几乎全美最想用 AI 变现的礼堂里。作为从上世纪 80 年代就开始搞 AI 的绝对老炮、也是微软技术底座的“扫地僧”,这位老哥并没有顺着台下的胃口,去吹捧下个月大模型又要颠覆什么行业,而是兜头给大家浇了一盆带点学术味的冷水。 他讲了一个挺有画面感的比喻:大家都在聊

By Ne0inhk
Godot被AI代码“围攻”!维护者崩溃发声:“不知道还能坚持多久”

Godot被AI代码“围攻”!维护者崩溃发声:“不知道还能坚持多久”

整理 | 郑丽媛 出品 | ZEEKLOG(ID:ZEEKLOGnews) 当大模型能在几秒钟内生成一段“看起来像那么回事”的补丁时,开源社区却开始付出另一种代价。 最近,开源游戏引擎 Godot 的核心维护团队公开吐槽:他们正被大量“AI 生成的低质量代码”淹没。那些代码往往结构完整、注释齐全、描述洋洋洒洒,但真正的问题是——提交者可能并不理解自己交上来的内容。 这件事,并不是简单的“有人偷懒用 AI 写代码”。它正在触及开源协作最核心的东西:信任。 一场悄无声息的“AI 洪水” 事情的导火索来自一条 Bluesky 讨论帖。 Godot 主要维护者之一、同时也是 Godot 商业支持公司 W4 Games 联合创始人的 Rémi Verschelde 表示,所谓的“AI slop”

By Ne0inhk
诺奖得主辛顿最新访谈:1 万个 AI 可以瞬间共享同一份“灵魂”,这就是为什么人类注定被超越

诺奖得主辛顿最新访谈:1 万个 AI 可以瞬间共享同一份“灵魂”,这就是为什么人类注定被超越

当宇宙级的“嘴炮”遇到降维打击。 编译 | 王启隆 来源 | youtu.be/l6ZcFa8pybE 出品丨AI 科技大本营(ID:rgznai100) 打开最新一期知名播客 StarTalk 的 YouTube 评论区,最高赞的一条留言是这样写的: “我长这么大,第一次看到尼尔·德葛司·泰森(Neil deGrasse Tyson)在一档节目里几乎全程闭嘴,像个手足无措的小学生一样乖乖听讲。” 作为全美最知名的天体物理学家,泰森平时的画风是充满激情、喋喋不休、用宇宙的宏大来震撼嘉宾。但这一次,坐在他对面的那位满头银发、带着温和英音的英国老人,仅仅用最平淡的语气,就让整个演播室陷入了数次令人窒息的沉默。 这位老人是 Geoffrey Hinton。深度学习三巨头之一,2024 年诺贝尔物理学奖得主,被公认为“AI 教父”。 对经常阅读 Hinton 演讲的我来说,这也是比较新奇的一幕—

By Ne0inhk
48小时“烧光”56万!三人创业团队濒临破产,仅因Gemini API密钥被盗:“AI账单远超我们的银行余额”

48小时“烧光”56万!三人创业团队濒临破产,仅因Gemini API密钥被盗:“AI账单远超我们的银行余额”

整理 | 苏宓 出品 | ZEEKLOG(ID:ZEEKLOGnews) 「仅过了 48 小时,一笔 8.2 万美元的天价费用凭空出现,较这家小型初创公司的正常月费暴涨近 46000%。」 这不是假设的虚幻故事,而是一家墨西哥初创公司正在经历的真实危机。 近日,一位名为 RatonVaquero 的开发者在 Reddit 发帖求助称,由于他的 Gemini API 密钥被盗用,原本每月仅约 180 美元(约 1242 元)的费用,在短短 48 小时内暴涨到 82,314.44 美元(约 56.8 万元)。对于这家只有三名开发者的小型创业团队来说,这笔突如其来的账单,几乎等同于灭顶之灾。 “我现在整个人都处在震惊和恐慌之中。”RatonVaquero

By Ne0inhk