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

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

文章目录

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

本文应用基于Rokid灵珠智能体/CXR SDK开发,开发指南https://forum.rokid.com/index

在这里插入图片描述

本文选用的技术包括:

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

openclaw使用llama.cpp 本地大模型部署教程

openclaw使用llama.cpp 本地大模型部署教程

openclaw使用llama.cpp 本地大模型部署教程 本教程基于实际操作整理,适用于 Windows WSL2 环境 全程使用 openclaw 帮我搭建大模型 一、环境准备 1. 硬件要求 显卡推荐模型显存占用GTX 1050 Ti (4GB)Qwen2.5-3B Q4~2.5GBRTX 4060 (8GB)Qwen2.5-7B Q4~5GBRTX 4090 (24GB)Qwen2.5-32B Q4~20GB 2. 安装编译工具(WSL Ubuntu) sudoapt update sudoaptinstall -y cmake build-essential 二、下载和编译 llama.cpp

Llama-Factory使用指南:从入门到实战

Llama-Factory使用指南:从入门到实战 在大模型落地越来越快的今天,越来越多开发者和企业开始尝试让通用模型“学会”自己的业务逻辑——比如客服对话、法律文书生成、医疗问答等。但一提到微调,很多人立刻想到复杂的训练脚本、满屏报错的日志、动辄几十GB的显存占用……最终只能望而却步。 有没有一种方式,能让我们不用写一行代码,点点鼠标就能完成专业级的大模型定制?答案是:有。而且它已经开源了,名字叫 Llama-Factory。 这个项目最近在 GitHub 上持续走热,不是因为它又搞了个新算法,而是它真的把“大模型微调”这件事做成了普通人也能上手的工具。你可以把它理解为一个“AI工厂流水线”:你只需要准备好原料(数据)、选好机器(模型),剩下的清洗、加工、质检、打包,全由系统自动完成。 更关键的是,它支持 WebUI 图形界面操作,全程可视化配置,连参数含义都有提示。哪怕你是第一次接触 LoRA 或 QLoRA,

全球AI生图新王诞生:腾讯混元Image-3.0登顶国际榜单,800亿参数重构AIGC格局

全球AI生图新王诞生:腾讯混元Image-3.0登顶国际榜单,800亿参数重构AIGC格局 2025年10月,国际权威AI模型竞技场LMArena发布最新文生图榜单,腾讯混元Image-3.0以89.7分超越DALL-E 3(87.2分)和Midjourney v6(86.5分),成为首个登顶该榜单的中国模型。这款参数规模达800亿的原生多模态模型,不仅刷新了开源领域纪录,更标志着国产大模型实现从"跟跑"到"领跑"的战略转折。 行业现状:多模态竞争进入深水区 2025年全球文生图API调用量突破240亿次,但商业闭源模型长期占据72%市场份额。IDC最新报告显示,多模态模型正推动AI应用从单一文本生成向图像、视频、语音等复合场景扩展,非文本模态使用占比已提升至20%。在此背景下,HunyuanImage-3.0的开源具有里程碑意义——其在LMArena盲测中以89.7分超越DALL-E 3(87.2分),成为首个登顶该榜单的中国模型。 如上图所示,LMArena榜单显示腾讯混元图像3.0(

知网AIGC检测多少钱?各渠道价格对比和省钱攻略

知网AIGC检测多少钱?各渠道价格对比和省钱攻略

知网AIGC检测多少钱?各渠道价格对比和省钱攻略 毕业季一到,论文查重和查AI就成了绕不开的两座山。查重大家都比较熟悉了,但知网AIGC检测是这两年才铺开的新项目,很多同学对它的价格体系还不太清楚。 我去年帮几个学弟学妹查过知网AIGC,踩了不少坑,也摸索出了一些省钱的路子。今天把这些经验整理出来,希望能帮大家少花点冤枉钱。 知网AIGC检测的官方定价是多少? 先说结论:知网AIGC检测并没有一个面向个人用户的统一公开售价。 这和知网查重类似——知网的检测服务主要是面向机构(高校、期刊社)提供的,个人用户想用知网检测,通常需要通过第三方渠道。不同渠道的定价差异不小,这也是很多同学踩坑的原因。 目前市面上能查到的知网AIGC检测价格,大致分布在这个区间: 渠道类型价格区间(单篇)可靠性备注学校图书馆免费(限次)最高部分学校提供1-2次免费机会知网官方合作渠道80-150元高需确认是否为正规授权淘宝/拼多多店铺30-80元参差不齐低价店铺有报告造假风险第三方检测网站50-120元中等需辨别是否真的调用知网接口 学校免费次数用完了怎么办? 很多学校会给毕业生提供