跳到主要内容
基于 Rokid 眼镜的 AI 天气、GPS 定位与旅游规划实现 | 极客日志
Kotlin AI 大前端
基于 Rokid 眼镜的 AI 天气、GPS 定位与旅游规划实现 综述由AI生成 在 Rokid 智能眼镜上开发 AI 天气应用的技术方案。主要实现了三个核心功能:一是通过 GPS 和高德逆地理编码实现自动定位,支持“这里天气”等指令;二是构建多轮对话上下文工程,处理续播意图如“那边呢”;三是接入 Claude API 进行 AI 旅游规划,根据天气生成个性化建议。文中提供了完整的 Kotlin 代码示例,包括 LocationHelper、ConversationContext 和 AiTravelPlanHelper 类,并总结了直辖市地址解析、语义识别错误及 LLM 延迟控制等踩坑经验。
机器人 发布于 2026/4/6 更新于 2026/5/21 33 浏览本文选用的技术包括
GPS 自动定位 :说「这里天气」自动获取位置,不用报城市名
多轮对话 :说「上海呢」「那边呢」「再查一次」接续上轮查询
AI 旅游规划 :接入 Claude API,天气播报后自动生成个性化旅游建议和行程规划
可直接复制的 Kotlin 代码 (LocationHelper、ConversationContext、AiTravelPlanHelper)
踩坑经验 :直辖市 adcode、续播语义识别、LLM 延迟控制
一、主要流程
本篇在 AiWeatherActivity(AI 语音查天气)基础上扩展,整体数据流如下:
新增三个辅助类,原有文件做对应改造
新建文件 职责 LocationHelper.kt GPS + 高德逆地理编码 ConversationContext.kt 多轮对话上下文(含 5 分钟 TTL) AiTravelPlanHelper.kt Claude 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 )
}
{
(!hasLocationPermission()) {
callback.onError( )
}
manager = context.getSystemService(Context.LOCATION_SERVICE) LocationManager
lastKnown = getLastKnownLocation(manager)
(lastKnown != ) {
reverseGeocode(lastKnown.latitude, lastKnown.longitude, callback)
} {
requestSingleUpdate(manager, callback)
}
}
: Location? =
listOf(GPS_PROVIDER, NETWORK_PROVIDER, PASSIVE_PROVIDER).mapNotNull {
runCatching { manager.getLastKnownLocation(it) }.getOrNull()
}.maxByOrNull { it.time }
{
url =
component = json.getJSONObject( ).getJSONObject( )
adcode = component.optString( )
city = component.optString( ).ifEmpty { component.optString( ) }
district = component.optString( )
callback.onCityCode(adcode, city, district)
}
}
fun getCurrentCityCode (callback: LocationCallback )
if
"缺少定位权限"
return
val
as
val
if
null
else
@SuppressLint("MissingPermission" )
private
fun getLastKnownLocation (manager: LocationManager )
private
fun reverseGeocode (lat: Double , lon: Double , callback: LocationCallback )
val
"$REGEO_URL ?location=$lon ,$lat &key=$API_KEY &extensions=base&output=JSON"
val
"regeocode"
"addressComponent"
val
"adcode"
val
"city"
"province"
val
"district"
2.3 意图识别:我们添加 GPS 的关键词 在 AiIntentParser 里加一批触发词,识别「天气」类意图:
private val LOCATION_KEYWORDS = listOf("这里" , "附近" , "当前" , "我这" , "这边" , "当前位置" , "我在哪" , "这里的" )
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
}
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
}
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("那呢" , "那边" , "那里呢" , "那边呢" , "再查" , "继续" , "再来一次" , "重新查" )
private fun parseContinuationIntent (text: String , ctx: ConversationContext ) : String? {
if (CONTINUATION_KEYWORDS.any { text.contains(it) }) return ctx.lastCityCode
val hasWeather = WEATHER_KEYWORDS.any { text.contains(it) }
if (!hasWeather) {
val cityCode = extractCityCode(text)
if (cityCode != null ) return cityCode
}
return null
}
用户说 解析结果 「福州呢」 形态 2:切换到福州 「那边呢」 形态 1:复用上次城市 「再查一次」 形态 1:同城市重查 「明天北京天气」 正常解析:北京(不走续播)
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()
openGlassCustomView(weatherViewHelper.generateWeatherViewJson(live, forecast))
sendWeatherTts(weatherViewHelper.generateWeatherTtsText(live, forecast))
conversationContext = conversationContext.advance(cityCode, cityName)
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))
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"
))
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 里:
续播语义识别错误
有天气关键词(「北京天气」)→ 走正常解析,不走续播
无天气关键词(「北京呢」)+ 有城市名 → 走续播,切换城市
续播词(「那边呢」)→ 复用上次城市
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 一句话说完。但如果你想继续探索,以下方向都在这套框架上可以直接延伸:
景点导览 :到达景点后自动识别位置,推送景点介绍和历史背景
行程提醒 :结合日历,提前推送目的地天气和出行建议
实时路况 :结合地图数据,提供出行路线和实时交通信息
多日规划 :「那明天呢」处理预报字段,生成多天旅游行程
美食推荐 :结合当地特色美食,根据天气推荐适合的餐厅
相关免费在线工具 RSA密钥对生成器 生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
Mermaid 预览与可视化编辑 基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
随机西班牙地址生成器 随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online
Base64 字符串编码/解码 将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
Base64 文件转换器 将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
Markdown转HTML 将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online