国庆那天,我随身带的除了手机、相机,还有一个刚编译好的鸿蒙安装包。别人在故宫看人海,我在故宫测“AI 导游”:打开 App,对着一座大殿拍照,它辨认建筑并讲解;对着一口铜缸提问,它回答来历;走累了,随口问一句“下一站去哪”,它按当前位置给路线。这个工程把蓝耘元生代 MaaS 的多模态大模型接口接进了鸿蒙 HarmonyOS 应用,整套链路从开发到真机验证花了不到两周。
这篇不是教程,是实战全记录。里面会有我踩过的坑、实测数据,以及最终可复用的代码思路。适合正在做鸿蒙应用、想接云端 AI 能力的移动端工程师,也适合文旅数字化方向的开发者参考。我尽量把“为什么这样做”也讲透,而不是只给一段能跑的代码。
1. 项目思路:为什么故宫需要“AI 导游”
1.1 场景判断:文旅导览的真实痛点
先说我为什么盯上“故宫 AI 导游”这个场景。国庆期间的故宫,人工讲解员基本约不到,租自动讲解器要排队,讲解内容还是固定的:你走到某个点位,它播一段录音,没法追问。扫码看文字更麻烦,人多的时候小程序都加载半天。真正走下来你会发现,游客的诉求其实特别朴素——看到一座不认识的大殿,想知道“这是什么”“什么时候建的”“发生过什么”;带着孩子来的,想知道“房顶上的小兽叫什么”。这些问题都是开放式的、不可预测的,传统导览设备完全应付不了。
大模型天然适合这个场景。它能听懂自然语言提问,能结合图片内容做识别和介绍,回答风格还能调成“讲解员模式”。我把这个思路总结成一句话:传统导览是“广播”,AI 导览是“对话”。在故宫这种人流密集、信息密度极高的场景里,一个能随时追问、永远不烦、还能看图的对话式导览助手,是能真实解决用户痛点的。
1.2 方案选型:鸿蒙 + MaaS 的搭配逻辑
确定场景后,接下来是技术选型。终端我选了鸿蒙 HarmonyOS,原因很直接:我本身就在做鸿蒙应用,ArkTS 开发效率不错,ArkUI 写这类单页交互界面很顺手,且鸿蒙生态在文旅、政务这类国内场景落地越来越常见,做出来的东西能真正跑在用户手机上。更重要的是,标题里提到的“鸿蒙AI”并不是指在端侧跑一个模型,而是“鸿蒙应用 + AI 服务”的组合方案,这个组合在工程上更务实。
模型侧我没有自己部署,选了蓝耘元生代 MaaS 这类 MaaS 平台。说人话就是,Model as a Service,模型以服务的形式租给你:平台把大模型部署在云端,我申请 API Key,通过 HTTP 请求就能调用视觉理解、对话生成这些能力。我不需要买 GPU、不需要部署推理服务、不需要处理模型版本迭代。对于这个项目来说,我的核心价值在应用层和体验层,不在模型训练和运维上,用 MaaS 是最省力的路径。而且这类平台普遍兼容 OpenAI 的 Chat Completions 协议,换模型、换参数都很方便,后续想接更强的新模型,改一个 model 字段就行。
2. 工程搭建:鸿蒙应用与 MaaS 平台的连接准备
2.1 开发环境与工程结构
开发工具用的是 DevEco Studio,新建工程选“Empty Ability”,语言选 ArkTS。鸿蒙的项目结构里,关键目录是entry/src/main,页面代码在ets/pages,配置文件是module.json5和app.json5。我建的工程里主要分三块:
- 页面层:负责相机页、讲解展示页、路线建议页的 UI;
- 服务层:封装了一个
MaaSGuideClient,所有模型请求都走这个类,页面不直接拼 HTTP; - 工具层:图片压缩工具、本地缓存工具、网络状态监听工具。
分层的好处是,国庆现场临时调策略时不用改 UI。比如后来我要在系统提示词里追加 GPS 位置先验,只需要改服务层的一个方法,页面无感。这个工程结构比较常规,但对这种“App 壳 + 云模型”的项目非常合适,建议后续有人做同类项目时直接照这个分层走。
2.2 申请权限与网络配置
AI 导游要拍照,要录语音提问,最好还能定位,所以涉及三个权限:相机、麦克风、位置。鸿蒙的权限配置在module.json5里声明,运行时还要用abilityAccessCtrl动态申请。module.json5的权限声明大致这样:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.CAMERA", "reason": "用于拍摄建筑和文物照片", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.MICROPHONE", "reason": "用于语音提问", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } }, { "name": "ohos.permission.LOCATION", "reason": "用于推荐参观路线", "usedScene": { "abilities": ["EntryAbility"], "when": "inuse" } } ] } }这里有个容易踩的坑:鸿蒙真机调试时,如果权限没有在usedScene里配置完整,或者运行时没有触发动态授权弹窗,请求会静默失败,表现为相机黑屏或者拿不到定位。我一开始在模拟器上一切正常,上真机后相机一直打不开,排查半天才发现是权限配置里when字段写错了。
网络配置同样要提前处理。鸿蒙应用默认不允许明文 HTTP 请求,调试时如果 MaaS 平台给了测试用的 HTTP 地址,需要在网络安全配置里放开,否则请求直接报“不允许明文流量”。我的做法是一律用 HTTPS 地址,开发阶段就在network_security_config.json里临时放行内网测试域名,正式包再收紧。关于证书校验,切记不要全局关闭校验,只针对测试域名做豁免,不然应用发布后会成为安全隐患。
2.3 先定接口:统一一个“导游引擎”协议
动工写页面之前,我先把接口协议定死了。MaaS 平台虽然不同厂商有差异,但主流平台都兼容 Chat Completions 格式:POST 一个 JSON 到/v1/chat/completions,body 里带model、messages、temperature、max_tokens,返回体里用choices[0].message.content取结果。我当时对接的蓝耘元生代 MaaS 视觉模型就是这个协议。
我先定义了请求消息的结构类型:
// 消息内容可以是纯文本,也可以是图文混合数组 type GuideMessageContent = string | GuideContentPart[]; interface GuideContentPart { type: 'text' | 'image_url'; text?: string; image_url?: { url: string; }; } interface GuideMessageItem { role: 'system' | 'user' | 'assistant'; content: GuideMessageContent; } interface GuideResponse { choices: Array<{ message: { content: string; }; }>; }定义这个协议的意义在于:后续无论平台换成哪个模型,只要返回结构不变,App 端零改动。后来我在现场遇到识别不准的问题时,想在请求里临时加参数,也只改服务层,页面完全不受影响。现在回头看,这个“先定协议再写界面”的习惯帮我节省了不少调试时间。
3. 核心实现:从“拍照”到“讲解”的完整链路
3.1 相机采集与图片压缩
AI 导游的第一步是获取建筑或文物的图片。我的实现里有两条入口:一条是调起系统相机拍照,另一条是从相册选图。核心逻辑在拍照后——原图必须经过压缩才能发给模型,否则会出现两个问题:一是图片体积太大,Base64 编码后请求体动辄几十 MB,平台直接拒收;二是上传慢,在国庆这种人挤人、信号差的场景里根本传不上去。
图片压缩我用的是鸿蒙自带的图像处理能力,把长边压到 1280 像素、JPEG 质量 82%,单张图片体积控制在 300KB 以内。代码逻辑大致如下:
import { image } from '@kit.ImageKit'; import { util } from '@kit.ArkTS'; async function compressImageToBase64( photoUri: string, maxEdge: number = 1280 ): Promise<string> { const source = await image.createImageSource(photoUri); const imagePacker = image.createImagePacker(); const packingOption: image.PackingOption = { format: 'image/jpeg', quality: 82 }; const packedData = await imagePacker.packing(source, packingOption); const base64 = util.Base64Helper.encodeSync(packedData); return `data:image/jpeg;base64,${base64}`; }有一个细节需要注意:如果你从相册拿到的是 PNG 图片,里面包含透明通道,直接转 JPEG 可能得到黑底图。我在测试时遇到过几次模型反馈“图片内容不清晰”,后来才发现是透明通道被填充成黑色了。处理办法是压缩前先让图片对象转换成不透明的 RGBA 图层,再输出 JPEG。这个细节很容易被忽略,但对识别质量影响很大。
3.2 调用 MaaS 多模态接口
图片准备好之后,就该调模型接口了。我在工程里封装了一个MaaSGuideClient,对外暴露ask(question, imageBase64?)方法。如果传了图片,就把图片 Base64 放在消息数组里;如果不传,直接发文字问题。调用的核心代码:
import { http } from '@kit.NetworkKit'; export class MaaSGuideClient { private readonly apiKey: string = 'your-api-key'; private readonly baseUrl: string = 'https://your-maas-endpoint.example/v1/chat/completions'; async ask(question: string, imageBase64?: string): Promise<string> { const httpRequest = http.createHttp(); const userContent: GuideContentPart[] = [{ type: 'text', text: question }]; if (imageBase64) { userContent.push({ type: 'image_url', image_url: { url: imageBase64 } }); } const requestBody = { model: 'vision-chat-v1', messages: [ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: userContent } ], temperature: 0.6, max_tokens: 512 }; const response = await httpRequest.request(this.baseUrl, { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.apiKey}` }, extraData: JSON.stringify(requestBody), connectTimeout: 15000, readTimeout: 30000 }); const result = JSON.parse(response.result as string) as GuideResponse; return result.choices[0].message.content; } }这里有几个参数是我实测后定下来的:temperature设 0.6,太低回答太死板,太高容易胡说;max_tokens设 512,因为导游讲解场景不需要长篇大论,300 字左右刚好;connectTimeout设 15 秒,readTimeout设 30 秒,这两个值在弱网环境下非常关键。我最初用的是默认 60 秒超时,结果用户拿着手机在广场上傻等一分钟,体验极差。
3.3 讲解结果渲染与本地缓存
模型返回的是一段 JSON 字符串,页面拿到后直接渲染到 Text 组件里。为了让阅读体验更好,我加了“打字机”效果:用一个定时器把文本按 30ms 一个字逐渐展示。这个效果成本很低,但现场反馈很好,游客会觉得“它在边想边说”,比一下子蹦出一大段文字更自然。
缓存方面,我做了两层。第一层是会话级缓存:同一个建筑或文物,用户 30 分钟内重复拍照查询,直接返回上一次的结果,不重复调用模型。第二层是本地持久化:用鸿蒙的 Preferences 存了查询历史,App 杀掉重开之后还能看到之前的讲解记录。这样做一是省钱,二是更快。故宫里相似的建筑很多,游客随手一拍可能就触发一次模型调用,没有缓存的话,一个上午几百个用户的请求量就会很可观。
3.4 提示词设计:怎么让模型“不说车轱辘话”
这可能是整篇文章里最容易被低估的部分。决定 AI 导游体验好坏的,往往不是模型能力,而是提示词设计。我调试过很多版系统提示词,最后固定下来的核心版本是:
你是一名专业且亲切的故宫讲解员。你在回答游客的现场提问。 要求: 1. 回答不超过300字,第一句话先给最直接的结论; 2. 如果用户发来照片,先描述你看到的建筑或文物类别,再结合历史背景讲解; 3. 使用口语化表达,称呼游客用“您”,不用Markdown标题; 4. 遇到不确定的历史细节,明确说“这个细节建议以官方讲解为准”; 5. 回答中不要输出与讲解无关的内容,不要反复建议游客去查询官网。我特意加了“不超过 300 字”和“先给结论”这两条。原因很现实:游客站在大太阳底下,没耐心看一屏长文;而且输出越短,模型胡说八道的概率越低。有一次我测试时没限定字数,模型洋洋洒洒写了 800 字的“建筑学论文”,里面甚至编了一个不存在的建造年代。收敛字数之后,错误率明显下降。
另外一个重要调整是加了兜底话术。模型再强也有知识盲区,与其让它硬编,不如让它承认不确定。我在提示词里明确“不确定就建议以官方讲解为准”,这句话看似简单,实际能挡住很多虚假信息。
4. 国庆现场实测:真实人潮里的表现
4.1 现场环境观察
国庆当天的情况比我预想得更极端。午门广场和太和殿广场基本是人挨人,手机信号在人多的地方频繁在 5G 和 4G 之间跳,偶尔直接掉到 3G。室内展馆——尤其是珍宝馆和钟表馆——信号更差,接口请求经常超时。光线条件也考验视觉识别:户外强光下逆光拍摄严重,檐下细节全是黑的;到了室内,光线不足导致照片噪点很多。
这些环境因素直接影响了我的测试结果。最初我以为“有网就能用”,到了现场才发现,整个链路的瓶颈不在模型聪明不聪明,而在网络稳定性和图片质量。如果现场做产品,这两点必须优先解决。
4.2 三类功能的实测数据
我记录了一批现场数据,混合了弱网和强网环境,整体情况如下:
| 测试场景 | 输入方式 | 识别成功率 | 平均响应时间 | 备注 |
|---|---|---|---|---|
| 太和殿正面拍摄 | 拍照 + “这是哪里” | 约100% | 3.2秒 | 正门光线较好,识别稳定 |
| 太和门正反面 | 拍照 + “介绍一下” | 约75% | 4.1秒 | 视觉上与太和殿相似,有混淆 |
| 铜缸局部特写 | 拍照 + “这是做什么的” | 约75% | 4.1秒 | 能识别是铜缸,但历史背景细节一般 |
| 行走中随机拍摄 | 拍照 + “这是什么” | 约60% | 5秒以上 | 逆光、遮挡、抖动影响大 |
| 纯文字问路线 | 文字 + “从保和殿到珍宝馆怎么走” | 约100% | 1.8秒 | 文字请求明显更快 |
这个数据说明了几个问题:文字交互的响应速度远高于图片识别;正面、光线好的场景识别率很高,但复杂场景掉得厉害;模型对“相似建筑”的区分能力有限。后面我在 App 里加入了 GPS 定位作为辅助信息,识别成功率有大幅提升,太和门和太和殿混淆的问题基本解决。
4.3 翻车现场与应急处理
现场翻过两次车,印象很深。
第一次是在太和门和太和殿的识别上。我从远处对着太和门拍了一张,模型回答“这是太和殿,是紫禁城中规模最大的殿宇”。从建筑外观看,两个殿确实有点像,光靠视觉特征很难区分。那天的临时解决办法是人为给模型加了一个“位置线索”。我在系统提示词里追加了一段:大用户当前 GPS 坐标,结合坐标判断用户在哪个殿附近。由于太和门和太和殿在经纬度上差距明显,加上坐标先验后,模型每次都能准确判断。这也验证了一个观点:多模态识别的准确率,不能只依赖模型视觉能力,要给模型足够多的上下文。
第二次是流式输出中断。我一开始没有做流式输出,而是等模型生成完一整段再返回。结果在弱网下人稍微多点,请求就很容易超时。后来我临时把readTimeout调大到 60 秒,才勉强撑过人流高峰。但这个治标不治本,真正稳妥的方案是后续接流式接口,让用户先看到部分内容,再边看边补全。
5. 实战避坑指南:这些问题第一次真的会踩
5.1 弱网下的超时与重连
文旅场景最常见的问题就是网络不稳定。故宫里 5G 覆盖看着不错,但国庆这种人流密度下,基站拥塞非常严重。我的建议是:所有 HTTP 请求必须设置合理的connectTimeout和readTimeout,不能依赖默认值;同时在请求失败时提供本地兜底。
我当时做了一层“离线词条兜底”:在 App 本地内置了故宫 20 个主要宫殿的 JSON 简介。当模型接口超时或网络完全断开时,客户端匹配用户当前 GPS 坐标和关键词,先从本地词条里找答案。虽然不如大模型灵活,但至少不会让用户面对一个空白屏幕。这个兜底逻辑在珍宝馆里救了不少次。
5.2 图片格式与大小引发的“哑火”
除了前面提到的 PNG 透明通道问题,还有一个坑是请求体过大。某次测试我把一张 5MB 原图直接塞进请求,平台直接返回 413 错误,请求实体过大。压缩到 300KB 以内后问题消失。另外,如果图片的 EXIF 信息里带有 GPS 位置,直接传图等于把用户坐标一起发给了平台,建议压缩时把 EXIF 一并剥离。
5.3 端侧算力与“AI 导游”的边界
有些朋友会问,为什么不用鸿蒙端侧的小模型做识别,非要请求云端 MaaS?我的结论是:端侧模型适合做“意图判断”“语音转写”这类轻量任务,但不适合做“故宫知识问答”这种重知识型任务。故宫几百座建筑、几十万件文物,端侧小模型装不下这么多知识,强行本地化只会得到大量幻觉。云端大模型知识面广、理解能力强,配合 MaaS 平台按量付费,对于项目原型和中小规模用户量来说性价比最优。后续如果要规模化部署,再由应用侧加缓存和路由来做成本控制。
5.4 文明导览:拍照与排队的红线
最后说一个容易被技术开发忽略、但真实存在的一件事:在展馆里用 AI 导游拍照提问时,记得关闭闪光灯。故宫的书画、丝织品类文物对光线敏感,闪光灯会造成不可逆损伤。我在提示词里也写了一条:当用户询问“能否拍照”时,模型要回答“请遵守现场规定,不要使用闪光灯,不要触摸展柜,人多时请按单向动线行走”。AI 导游不能只提供便利,还要引导文明参观。这个细节从产品角度看也是加分项,对场馆方来说更是一个靠谱的信号。
6. 后续还能怎么玩(个人经验收尾)
项目做到国庆实测这个程度,核心链路已经跑通了。我个人体会最深的一点是:AI 导游这类应用的胜负手,不在模型有多聪明,而在“场景理解”和“兜底能力”。场景理解靠的是提示词设计和上下文补充,兜底靠的是缓存、离线词条和优雅的失败提示。这些是纯模型能力之外的工程活,也是最耗时间的部分。
后续我想做几个方向的扩展。第一是把图片识别和北斗定位结合起来,做一个“走到哪讲到哪”的自动触发模式,用户不用掏手机拍照,走近某个大殿,App 就自动推送讲解。第二是接入语音合成,把文字讲解转成语音播放,适合边走边听的场景,省得一直看屏幕。第三是做成鸿蒙元服务,免安装、扫码即用,在故宫入口放一个码,游客扫一扫就能开始用,这对文旅场景的分发效率提升很大。技术上都不算难,难的是把体验细节打磨好。
如果这篇记录能给正在做“鸿蒙 + MaaS”或文旅 AI 应用的你一点启发,那就够了。我最后想说的其实是:故宫的每一座建筑背后都有故事,技术只是把故事讲出来的手段,真正打动人的还是内容本身。做产品时,别忘了这个出发点。