国庆那天我一直泡在一座热门历史文化的古城博物院里,从早上开门排到下午闭馆,别人肩上扛着导游旗、手里捏着讲解器,我却只拿着一部装着鸿蒙系统开发板的手机。我说了一句“帮我规划一条人少又好看的路线”,屏幕几乎在同一时间给出三条建议,耳机里紧跟着就念了出来。周围几个游客以为我开了什么魔法,其实这是我放假前赶工做出的一套“AI 导游”:鸿蒙端做交互,云端接元生代 MaaS 平台的大模型接口,语音识别、定位、知识问答全都在这个闭环里跑通。这篇文章就把这次实战从头到尾拆给你看:方案怎么选、接口怎么接、现场怎么调、翻车怎么救,全部是这次实景测试攒下来的记录。
1. 整体设计:AI 导游不是套壳聊天框
先把话放在前头:做一个能让人在景区里真正愿意用的 AI 导游,难度不在“能不能对话”,而在“对话以外的一圈东西”。我在动手前把需求拆了一整天,最后确定方案结构,才动手写代码。
1.1 需求拆解:AI 导游到底要解决什么事
传统景区讲解器是“刷编号听录音”,内容固定,位置错了就尴尬。AI 导游要解决的其实是三件完全不同的事:
- 实时讲解:游客走到哪个展区,系统就得自动知道位置,并且把这件文物的背景、看点、故事讲出来。注意,这个讲解不是把所有百科词条念一遍,而是跟着人物当前位置、观看角度、停留时间动态组织。
- 自由问答:“这件青铜器上的铭文是什么意思”“旁边那个建筑是什么时候重建的”,这类问题是高频场景。讲解器完全没法接住,但大模型很擅长,问题在于怎么把回答控制得简短且口语化,而不是一段八百字论文。
- 路线决策:“我现在在南门,只有两个小时,想先看最经典的,怎么走?”游客真正需要的不是千篇一律的推荐,而是结合当前人流量、步行距离和个人偏好的推荐。
这三件事如果全塞进一个普通聊天页面,用户得一边走路一边低头打字,体验会很差。所以我定的产品原则是:默认自动触发讲解,语音为第一交互方式,文字只做辅助回看。所有界面都要适合“边走边用”的场景,按钮尽量大,信息尽量少。
1.2 选型逻辑:为什么用 MaaS 而不是自建模型
我也认真考虑过在本地部署一个开源模型,RAM、GPU、功耗、网络延迟,这几个问题摆出来后,直接放弃。MaaS(Model as a Service,模型即服务)平台的好处是它把“模型部署和运维”这件事整体接走,我只需要管业务逻辑。
- 成本控制:自建模型意味着要长期养显卡和电费,而我这次场景是“节假日高峰期一天内几千次请求”,属于典型的突发流量。MaaS 按 token 计费,流量小的时候花的钱很少,高峰期最多也就是多花点调用费,不需要我准备任何物理硬件。
- 模型迭代:MaaS 提供的模型版本是平台维护的,我自己的代码几乎不用动。节前我还在用旧版模型,节后平台升级成新版,同一个 API 接口直接生效,不需要重新训练,也不需要换权重文件。
- 多模扩展:后边如果要做图像识别文物、语音克隆导游音色,MaaS 平台大多已经把对应接口也准备好了,我在鸿蒙端只需要调用一个新的 API 即可。
当然,MaaS 也有缺点,最核心的是依赖网络。景区里人多、信号差、运营商基站拥塞,这是我行前最担心的点,后面专门做了本地兜底缓存来解决。架构上,我把它定位成“云端大脑 + 本地记忆”,云端负责理解和生成,本地负责定位缓存和离线内容。
1.3 为什么端侧选鸿蒙而不是其他方案
鸿蒙系统最让我看重的不是某个单独能力,而是它把几个原本需要额外集成的服务放在了系统层面。调起麦克风、定位、后台语音播放、桌面卡片,这些在传统移动开发中各有各的适配成本,在鸿蒙上相对统一。
另外,鸿蒙的原子化服务非常适合景区这种“用完即走”的场景:游客不需要专门下载一个 App,扫一下服务卡片就能进入 AI 导游界面,还可以把讲解卡片直接钉在桌面。我这次的实际目标是先跑通主流程,但这些系统级能力的接口在开发时已经预留了。一次开发,后续还能顺手适配到平板和车机屏幕,这在后期做车载自驾讲解时很有价值。
1.4 整体架构:云端大脑加本地手脚
我的架构图如果拿文字描述,大概是这个走向:
- 端侧:鸿蒙应用,负责采集用户语音、定位坐标、展示生成结果、播放语音。
- 云侧:元生代 MaaS 的对话接口,负责理解意图、生成讲解词、回答提问。
- 中间层:我自己写的一个请求网关,负责统一鉴权、格式化提示词、管理上下文、拦截异常。
鸿蒙端(ArkTS) │ ├── 语音输入:语音识别 → 文本 ├── 定位:GPS/网络定位 → 坐标 └── 操作:路线选择、问答提交 │ ▼ 请求网关(鸿蒙本地模块) ├── 组装 system prompt(导游人设 + 当前展区背景) ├── 追加历史消息(多轮上下文) └── 发送 HTTP/HTTPS 流式请求 │ ▼ 元生代 MaaS 平台 ├── 大模型推理 └── 返回流式文本 │ ▼ 鸿蒙端 ├── 文本实时渲染 ├── 语音合成播报 └── 本地缓存热点讲解我在标题里写的“蓝耘元生代 MaaS”是一类开放模型服务平台的统称,下文简称元生代 MaaS。实际开发时接入方式和主流平台兼容,你换成自己常用的任何 MaaS 也都适用,只是模型名和鉴权参数略有差异。
2. 核心细节:鸿蒙端工程与 MaaS 接口接入
方案定完之后,我花了不到一天时间就把第一个版本跑通了。很多人看到“大模型应用”就发怵,其实核心工作量集中在几个不算复杂但很关键的模块上,逐个拆开说。
2.1 鸿蒙工程初始化与权限配置
我用的 IDE 是 DevEco Studio,创建工程时选择“Application”模板,开发框架选择 Stage 模型,语言用 ArkTS/ArkUI。这些基本创建的流程不再多说,重点讲权限配置。
AI 导游要能正常工作,必须提前在module.json5里声明下面几个权限:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" }, { "name": "ohos.permission.GET_NETWORK_INFO" }, { "name": "ohos.permission.ACCESS_FINE_LOCATION" }, { "name": "ohos.permission.ACCESS_COARSE_LOCATION" }, { "name": "ohos.permission.MICROPHONE" }, { "name": "ohos.permission.KEEP_BACKGROUND_RUN" } ] } }这里特别提醒两个容易踩的坑:
- 定位权限分精准和模糊两级,如果只申请模糊定位,室内展区定位会飘到隔壁展厅,讲解就串台了。我在博物馆里实测,精准定位在室内依然有 5 到 15 米的误差,不处理的话根本不能用,后面单独讲解决方案。
- 麦克风权限一定要在用户点击“开始语音”时再动态申请,不要进入 App 就弹窗。景区游客警惕性高,一上来就弹“录音权限”会直接劝退。
2.2 搭建请求网关:把 MaaS 接口包得干净一点
鸿蒙提供了@kit.NetworkKit中的http模块,我用它封装了一个MaaSClient类。这样做的好处是,页面层永远不直接接触网络细节,只调用client.chat(messages),返回一个回调或流式数据,后面换平台、换接口,只改这一个类就行。
核心代码大概这样:
import { http } from '@kit.NetworkKit'; export class MaaSClient { private apiKey: string = '你的密钥'; private baseUrl: string = 'https://your-maas.example.com/v1/chat/completions'; chat(messages: Array<{ role: string; content: string }>) { return new Promise<string>((resolve, reject) => { const httpRequest = http.createHttp(); const body = JSON.stringify({ model: 'maas-chat', messages: messages, temperature: 0.7, stream: false }); httpRequest.request( this.baseUrl, { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${this.apiKey}` }, extraData: body, connectTimeout: 30000, readTimeout: 60000 }, (err, data) => { if (err) { reject(err); return; } const result = JSON.parse(data.result as string); resolve(result.choices[0].message.content); } ); }); } }注意几个细节:
- 密钥不要在代码里硬编码,上桌面的 Demo 可以临时图省事,但只要是可能对外发布的版本,务必走密钥管理服务,或者把自己的服务端做中间层签名,否则别人反编译 App 就能拿到你的 API Key,盗刷起来非常快。我这次虽然只是自用,也还是把密钥藏在了本地配置文件中,避免随手贴进 Git 仓库。
- 超时时间必须设长,景区网络抖动很常见,readTimeout 我一开始设了 30 秒,结果在人流最密集的展馆里频繁超时,后来改到 60 秒才稳定。但超时时间不是越长越好,后面讲流式输出会说明原因。
2.3 流式响应:让游客觉得 AI 反应很快
第一次用非流式接口时,体验很差。游客问一个问题,界面上面转圈圈,两三秒后整段文字一次性蹦出来,耳机突然开始长篇大论,游客站在路中间听得走不动道。
改成流式输出后,后端每生成一小段文字就推送一次,界面能像打字机一样逐字显示,语音也可以只等第一个完整句子出来就开始朗读,体感延迟直接从“两三秒没反应”变成“一秒内开始播报”。
鸿蒙端处理流式响应,不能直接用前面那个http模块的简单回调,需要把响应体按text/event-stream一段段读出来。元生代 MaaS 这类接口通常返回的是 SSE 格式,每一行是data: {json}这种结构。
我简化后的解析逻辑:
private onChunk(chunk: string) { const lines = chunk.split('\n'); for (const line of lines) { if (!line.startsWith('data:')) { continue; } const payload = line.slice(5).trim(); if (payload === '[DONE]') { this.onDone?.(); continue; } const json = JSON.parse(payload); const delta = json.choices?.[0]?.delta?.content; if (delta) { this.onMessage?.({ delta }); } } }实际应用里有个大坑:onMessage回调每秒会触发几十次,如果每次都拿去渲染 UI,ArkUI 会卡顿。我的处理办法是把文本先累积到缓冲区,每 80 毫秒刷一次屏幕,也就是手动做了一个节流。
2.4 上下文管理:别让“导游”失忆,也别让 token 爆炸
多轮对话是 AI 导游躲不开的需求,游客可能连续问三四个问题。我每次请求前都会维护一个messages数组,把历史消息都塞进去。但是,如果无限追加历史,token 消耗会快速上升,而且模型会忘记最开始设定的景区背景,回答越来越偏。
我设置的上下文窗口是:系统提示词固定保留,最近 6 轮对话保留,更早的对话做摘要。初版没做摘要时,二十多轮连续对话后,模型回答质量明显下降,而且费用飙升。后来我加了一个轻量摘要技巧:每满 6 轮,把之前的内容丢给模型做一个压缩总结,用总结替代原文继续作为上下文。
系统提示词是 AI 导游的灵魂。我用的提示词大概是这个思路:
你是一位资深的历史文化景区讲解员,服务对象是普通游客。 要求: 1. 回答控制在 150 字以内,口语化,方便语音朗读。 2. 优先回答与当前展区、文物、建筑相关的内容。 3. 如果游客问与景区无关的问题,委婉拉回景区主题。 4. 每次回答以“温馨提示”或“接下来可以往...”这类引导性语句结尾。 5. 不要输出 Markdown 标记,不要输出代码块,不要输出表情符号。这里有几个真实体验后的调整:第一个版本我没限制 Markdown,结果语音合成器把“## 标题”“加粗”这种符号都读出来了,游客一脸懵。加了第 5 条之后,输出干净很多。其次,限制在 150 字也至关重要,模型默认喜欢长篇大论,景区场景下没人有耐心听 3 分钟。
3. 实操过程:语音、定位与现场体验联动
基础链路跑通后,真正的挑战才刚开始。景区不是办公室,高分贝环境、室内定位漂移、人挤人到信号盲区,这些现实因素会把一个“看起来能用”的 Demo 打回原形。
3.1 语音交互:从按下按钮到听见回答的完整链路
我设计的语音交互流程是:
- 用户点击主界面下方的麦克风按钮,鸿蒙拉起系统的语音识别能力,把游客的话转成文本。
- 文本自动提交给 MaaS 请求网关,带上当前展区和历史上下文。
- 系统收到的第一个完整句子出来后,立刻调用文本转语音能力播放,不需要等全文结束。
这样设计是为了制造“即时反馈”的错觉。游客说完话到耳机里响起第一句话,控制在 0.8 到 1.5 秒之间,体验才称得上舒服。一旦超过 2 秒,游客会下意识低头看手机,觉得自己操作错了。
语音识别的精度是另一个问题。景区里小孩哭闹、导游喇叭声、广播声全都混在一起,手机麦克风如果离嘴比较远,识别率惨不忍睹。实测下来有两个改进有效:一是要求用户在语音开始后靠近手机下方麦克风说话,界面用动画提示;二是我把识别结果展示在屏幕上让用户确认,避免误识别直接误导答。前者提高了准确率,后者给用户兜底,误识别时能手动编辑再提交。
3.2 定位联动:搞清楚游客到底站在哪里
AI 导游的精髓是“到什么位置,讲什么内容”。我一开始天真地以为 GPS 就够了,拿到坐标后比对景点坐标,距离最近的展区就是当前展区。等到我站在一个横向跨度 300 米的古代建筑群里一站,GPS 漂移直接把我判定到了隔壁展馆,讲解内容牛头不对马嘴。
后来我采用“GPS 初筛 + 近场校正”的双层策略:
- GPS 初筛:定位到最近的两个到三个展区,作为候选。
- 近场校正:在展区入口处部署蓝牙信标,鸿蒙手机接收信标广播的 ID 来确定真正进入了哪个展区。没有信标时,用手机方向传感器辅助判断,朝向哪个方向,就优先判定为该方向上的展区。
展区的坐标表我提前录入在本地 JSON 文件里,包含展区名称、中心点坐标、半径和一段默认讲解词。定位模块的输出就是一个zoneId,请求网关把zoneId对应的背景信息加到系统提示词里,模型回答就有了空间感。
[ { "zoneId": "zone_01", "name": "前朝大殿区", "center": { "lng": 116.397, "lat": 39.918 }, "radius": 60, "intro": "这里曾是大型礼仪活动的核心场所,地面石雕和殿宇布局都有明显的中轴线对称特点。" }, { "zoneId": "zone_02", "name": "珍宝陈列区", "center": { "lng": 116.400, "lat": 39.921 }, "radius": 40, "intro": "本区集中展示各类金银器、玉器和珐琅制品,展品以清代宫廷做工最精。" } ]这个表也承担了离线兜底职责。信号不好、MaaS 请求失败时,直接把intro字段用系统 TTS 读出来,虽然不如模型生成的生动,但至少游客不会对着黑屏发呆。
3.3 现场体验:信息密度、打断、语速这些“小”问题
现场测试半小时后,我发现问题集中在几个特别细节的地方:
- 游客语速过快时,识别结果经常缺字。后来我把识别引擎参数调成“更长的静音检测时间”,结果又变成迟迟不收尾。最终找到一个平衡点:静音检测设 0.8 秒,既能断句,又不至于等太久。
- 语音播报不能从头到尾匀速,遇到重要数字和年代要稍微停顿。普通 TTS 做不到智能停顿,我就在后端规则里做了文本后处理:把“明代”“清代”“乾隆”这类词后面自动加逗号,让 TTS 在逗号处自然停顿。
- 游客随时可能打断播报,问下一个问题。我在播放器上加了全局手势,双击屏幕即停,不做复杂的唤醒词识别,避免误触发。游客普遍反馈“双击就停”很直觉。
现场还测了一个小功能:游客问“这个展品是哪年出土的”,模型回答完,我会把这句答话缓存到本地,以“展区 ID + 问题摘要”为键,下次有人问同样的问题,直接走缓存,不再请求云端。高峰期人流量大时,这个缓存能实打实降低 API 调用费用。
4. 问题追查与排查实录:那些说明书不会告诉你的坑
任何实战项目都绕不开调试环节。这次在线下测试暴露的问题,比我自己写代码时预想的要多得多。整理成速查表放在最后,下面挑几个印象最深的详细说。
4.1 请求 401:权限认证失效
MaaS 平台大多使用 API Key 鉴权,一次成功的请求之后,短时间内又突然报 401。排查后发现不是我的代码问题,而是平台接口对单个 Key 的并发连接数有限制,高峰期同时发出多个请求时有部分连接被拒绝。
解决办法有两层:
- 在应用侧实现请求队列,同一时间只保持一个正在进行的流式请求,新的请求必须等前一个完成。这个设计本身也符合导游场景,游客一般不会在同一秒问两个问题。
- 客户端的请求增加随机重试机制,首次 401 后等待 1 秒重试,二次重试间隔拉长到 3 秒,最多两次。实测这样处理后,节假日高峰期的失败率从 5% 降到 0.5% 左右。
4.2 流式响应断流:搜不到数据
景区人流密集区会出现一种奇怪现象:请求发出去,前几个字符收到了,然后断流 10 秒,再突然恢复。这不是平台问题,而是景区内多个运营商的信号互相干扰,TCP 长连接被中间设备重置。
我后来做了“断流续传”:在流式读取过程中记录已收到的文本长度,一旦断流超过 3 秒,自动用当前已生成文本作为应答结束,并本地缓存这次半成品,同时提醒游客“网络信号较弱,这是当前已获取的讲解内容”。虽然回答不完整,但比一直转圈强太多。
4.3 手机发热和耗电:长期后台运行
AI 导游的典型使用时长是 2 到 4 小时,期间麦克风、GPS、网络、TTS 全部常开,发热非常明显。系统为了省电会暗中杀掉后台任务,导致定位和语音播报突然失效。
我做了三件事来控制功耗:
- 单元化数据请求:不频繁轮询定位。我的定位策略是展区内每 30 秒拿一次坐标,静止时自动拉长到 2 分钟,走路时恢复 30 秒。又省电又不会错过关键展区切换。
- 屏幕亮度:播报讲解时自动把屏幕亮度降到 20%,让用户听就好,不用一直盯屏。游客看屏幕少了,手机温度明显降下来。
- 语音播报占用后台通道:给应用申请了
KEEP_BACKGROUND_RUN权限,并且在 V5 系统中启用“长时任务”模式,确保锁屏后语音还能继续播放。
4.4 问题排查速查表
| 现象 | 可能原因 | 处理方法 |
|---|---|---|
| 请求直接返回 401 | API Key 错误、并发超限、Token 过期 | 检查密钥配置;请求队列化;重试两次 |
| 响应为空但无报错 | 定位到的展区没有对应背景信息;上下文为空 | 补充默认景区背景提示词 |
| 语音识别老出错 | 背景噪声大、麦克风距离远、静音检测时间太短 | 调长静音检测到 0.8 秒;提示用户靠近麦克风 |
| TTS 朗读乱码符号 | 模型输出包含 Markdown 或特殊字符 | 在系统提示词中禁止输出特殊格式;后端清洗文本 |
| GPS 定位到隔壁展区 | 室内定位漂移、展区间距小 | 叠加蓝牙信标校正;用方向传感器辅助判断 |
| 手机发热严重 | GPS 高频轮询、屏幕常亮、网络重连频繁 | 频率自适应;降低屏幕亮度;启用长时任务 |
| 流量消耗过快 | 流式请求未做节流、上下文无限增长 | 屏幕渲染节流;上下文限制 6 轮 |
| 多轮对话答非所问 | 系统提示词被历史消息淹没 | 保留 6 轮上下文;定期做历史摘要 |
这次在真实景区连续跑了一整天,最大的感悟是:大模型能力再强,也必须在产品逻辑和系统资源之间做大量取舍。AI 导游看起来只是“调用一个 API”,真正决定游客体验的,反而是定位是否准确、语音是否自然、网络断流时怎么兜底这些不太起眼的细节。
如果你打算在自己熟悉的景区或博物馆复刻一套类似的鸿蒙 AI 导游,我建议顺序是:先做最简原形,把“定位展区 + 请求 MaaS + TTS 播放”这条主链路跑通;再补流式输出和上下文管理;最后才考虑多端适配和桌面卡片。等主链路稳定了,再琢磨怎么把讲解词风格调得更像真人、怎么把本地缓存做得更聪明这类锦上添花的事。我自己接下来准备把这次写死的景区展区表升级成后台可配置,这样换一个景区,不需要重新打包应用,直接在云端改数据,就能复用同一套鸿蒙端。