跳到主要内容
基于 Rokid 眼镜的 AI 天气应用、GPS 定位与旅游规划实现 | 极客日志
Kotlin AI 大前端
基于 Rokid 眼镜的 AI 天气应用、GPS 定位与旅游规划实现 综述由AI生成 Rokid 眼镜 AI 天气应用结合 GPS 定位与 AI 旅游规划功能,通过 Kotlin 开发。核心实现包括 LocationHelper 自动获取位置并处理直辖市逆地理编码问题,ConversationContext 管理多轮对话上下文及续播意图识别,以及利用 Claude API 生成个性化旅游建议。解决了 TTS 播报重叠、LLM 延迟控制等工程问题,实现了从语音查询到行程规划的完整闭环。
Qiny01 发布于 2026/4/8 更新于 2026/5/23 15 浏览本文选用的技术包括
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 )
}
fun {
(!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)
}
}
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("那呢" , "那边" , "那里呢" , "那边呢" , "再查" , "继续" , "再来一次" , "重新查" )
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:同城市重查 「明天北京天气」 正常解析:北京(不走续播)
四、功能 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