资讯详情

资讯详情

基于MaaS与鸿蒙元服务的AI行程规划应用实战

1. 从一句标题拆出来的真实需求国庆黄金周出行这件事每年都在重复同一个剧本提前半个月开始刷攻略收藏夹里躺了三十篇XX地三日游保姆级教程真到出发那天还是手忙脚乱。问题不在于信息不够而在于信息太多、太散、太同质化。一个普通家庭想规划一次五天四晚的跨省旅行要同时处理交通班次、酒店比价、景点预约、天气变化、老人小孩的体力节奏、临时封路或限流公告这些变量叠在一起本质上是一个多约束条件下的动态规划问题。大模型恰好擅长处理这类模糊需求进、结构化方案出的任务。你把带两个老人一个小孩不想太赶预算中等偏好自然风光这句话丢给它它能拆解成每天的行程节奏、景点间的通勤时间、备选方案和风险提示。但光有模型不够——你得让用户在手机上随时能问、随时能改这就需要一个能扛住高并发推理请求的服务端和一个足够轻、足够顺手的客户端。这个项目要解决的核心问题就三个第一把大模型的行程规划能力封装成稳定可调用的服务第二让这个服务在国庆这种流量洪峰下不崩第三把它塞进用户每天都在用的手机系统里做到打开就能用。适合谁来参考如果你正在做 AI 应用落地、对 MaaS 平台和移动端集成感兴趣或者单纯想给自己做一个趁手的出行工具这篇内容里的思路和踩坑记录都能直接用。我先把整体架构的逻辑讲清楚再往下拆每个环节的具体做法。2. 整体架构设计与技术选型逻辑2.1 为什么是 MaaS 而不是自己搭推理服务自己部署大模型推理这件事我试过。买卡、配环境、调并发、做量化、处理显存溢出一套流程走下来模型还没上线人已经先累了。更现实的问题是国庆期间的流量是平日的几十倍你按峰值配置资源平时就是浪费按平时配置峰值直接雪崩。MaaSModel as a Service的核心价值就在这里——它把推理资源的弹性伸缩这件事从你手里接走了。你只需要关心我要调哪个模型、传什么参数、拿什么结果底层的算力调度、模型加载、并发控制由平台负责。对于行程规划这种典型的请求量波动极大、单次推理耗时中等的场景MaaS 的按需调用模式比自建服务在成本和稳定性上都更划算。选蓝耘元生代 MaaS 的原因很直接它提供了标准的 API 接口模型选择比较灵活而且对国内开发者来说网络访问的稳定性是一个不能忽视的实际因素。你在做移动端应用时接口的响应延迟直接决定用户体验这一点在选型时权重很高。2.2 为什么客户端选鸿蒙鸿蒙在这个项目里的角色是最后一公里。用户不会为了规划行程专门下载一个 App但他们会用手机上的语音助手、卡片服务或者轻量级元服务。鸿蒙的元服务原子化服务机制允许你把功能拆成一个个独立可调用的卡片用户不需要安装完整应用就能使用。具体到这个项目我把它做成了一个行程规划卡片用户在桌面添加卡片后直接输入出行需求卡片调用后端 MaaS 接口返回结构化的行程方案。整个过程不需要打开任何 App交互路径最短。另一个考虑是鸿蒙的分布式能力。国庆出行场景下用户可能在手机上看行程、在平板上看地图、在车机上导航鸿蒙的跨设备流转机制让这些场景之间的切换比较自然。当然这个项目里我主要用到了手机端的卡片能力分布式部分只做了基础适配。2.3 整体数据流整个系统的数据流可以拆成四段用户输入在鸿蒙卡片中输入自然语言需求比如国庆去成都五天带父母不想爬山。请求封装客户端把输入加上一些隐式上下文当前日期、用户历史偏好标签打包成结构化请求。MaaS 推理后端服务接收请求调用 MaaS 平台的对话模型通过精心设计的提示词模板生成行程方案。结果渲染返回的 Markdown 或 JSON 结构化数据在卡片上渲染成可折叠的日程列表支持一键复制和分享。这个链路里最关键的环节是提示词工程和结果结构化。模型输出的稳定性直接决定用户体验——如果每次返回的格式都不一样前端就没法做统一的渲染。我在提示词里强制要求模型按固定 JSON Schema 输出同时在服务端做了一层校验和兜底转换。3. 核心细节解析与实操要点3.1 提示词模板的设计让模型稳定输出结构化行程大模型有个通病你让它规划一个行程它可能给你写一篇散文也可能给你列一个表格格式全凭心情。要让它稳定输出可解析的结构提示词必须做到三件事角色设定明确、输出格式锁定、边界条件说清。我用的提示词模板大致是这样的结构你是一个专业的行程规划助手。根据用户需求生成行程方案。 用户需求{user_input} 出行天数{days} 同行人员{companions} 要求 1. 每天安排不超过3个主要景点景点间通勤时间不超过40分钟 2. 如果同行人员包含老人或儿童每天步行距离控制在5公里以内 3. 输出必须是严格的JSON格式不要包含任何其他文字 4. JSON结构如下 { destination: 目的地, days: [ { day: 1, theme: 当日主题, spots: [ {name: 景点名, duration: 建议时长, tip: 实用提示} ], meals: [推荐餐厅类型], transport: 交通建议 } ], warnings: [注意事项] }这里有几个细节值得展开说。第一为什么用 JSON 而不是 MarkdownMarkdown 对人类友好但对程序解析不友好。你要从一段 Markdown 里提取第二天去了哪几个景点得写正则表达式而模型的 Markdown 格式每次都可能微调正则很容易失效。JSON 有明确的键值结构解析稳定性高一个量级。第二为什么要限制每天景点数量不限制的话模型会给你塞五六个景点看起来很充实实际根本走不完。我实测下来每天2到3个主要景点是比较合理的节奏留出足够的缓冲时间应对排队、找路、休息。第三warnings 字段的价值。这个字段让模型主动输出风险提示比如国庆期间该景点需提前3天预约、当日可能有雨建议备伞。这些信息对用户的实际价值往往比行程本身还高。注意提示词里的 JSON 示例不要写得太复杂层级超过三层后模型的输出错误率会明显上升。如果确实需要更复杂的结构建议拆成多次调用每次生成一部分。3.2 鸿蒙卡片的开发要点鸿蒙元服务的卡片开发有一套自己的规范和传统的 Android Widget 或 iOS Widget 思路不太一样。核心概念是FormAbility——卡片本身是一个独立的 Ability有自己的生命周期。开发流程大致是在 DevEco Studio 中创建元服务项目选择卡片模板。定义卡片的外观用 ArkTS 的声明式 UI 描述。实现 FormAbility 的 onCreateForm、onUpdateForm 等生命周期回调。在卡片中通过 HTTP 请求调用后端 MaaS 接口。这里踩过的坑主要集中在卡片的刷新机制上。鸿蒙卡片默认不支持频繁刷新系统对刷新频率有限制。行程规划这种场景用户输入后需要等待模型推理通常3到10秒这段时间卡片需要显示加载状态推理完成后更新内容。我的做法是卡片首次创建时显示一个输入框和开始规划按钮用户点击后触发一个后台任务任务完成后通过formProvider.updateForm主动更新卡片内容。这样避开了卡片自动刷新的频率限制用户体验也比较流畅。// 卡片更新核心逻辑示意 import formProvider from ohos.app.form.formProvider; import formBindingData from ohos.app.form.formBindingData; async function updateCardContent(formId: string, planData: object) { let formData formBindingData.createFormBindingData({ planJson: JSON.stringify(planData), status: completed }); await formProvider.updateForm(formId, formData); }提示鸿蒙卡片的网络请求需要在 module.json5 中声明网络权限同时注意卡片进程和主应用进程是隔离的不能直接共享内存状态需要通过持久化存储或事件机制传递数据。3.3 服务端的并发处理与降级策略国庆期间的流量特征是脉冲式的——早上七八点和晚上八九点是两个高峰中间相对平缓。服务端如果直接透传请求到 MaaS高峰期会出现大量请求排队用户等待时间从几秒变成几十秒。我加了两层处理。第一层是请求队列和限流服务端维护一个请求队列超过并发上限的请求进入等待队列同时给客户端返回正在排队的状态让用户知道系统在工作而不是卡死了。第二层是缓存对于热门目的地比如几个传统旅游城市的常见需求组合把生成的行程方案缓存起来相同或相似请求直接返回缓存结果不走模型推理。缓存的键怎么设计是个学问。直接用用户输入的原文做键命中率很低因为每个人的表达方式都不一样。我的做法是先对输入做一次轻量归一化——提取目的地、天数、同行人员类型这三个核心维度用这三个维度的组合作为缓存键。比如成都5天带老人这个组合很多用户的表述不同但核心需求一致缓存命中率能到30%左右。降级策略方面当 MaaS 接口响应超时或返回错误时服务端不应该直接把错误抛给用户。我的处理是如果缓存里有相似目的地的历史方案返回一个标注了参考方案的结果同时提示用户当前规划服务繁忙以下为参考行程建议稍后重新生成。4. 实操过程与核心环节实现4.1 环境准备与项目初始化先把手头的工具链理清楚。服务端我用的是 Node.js也可以用 Python看个人习惯客户端是 DevEco Studio。MaaS 平台需要先注册账号、创建应用、拿到 API Key 和接口地址。服务端项目初始化mkdir travel-planner-server cd travel-planner-server npm init -y npm install express axios dotenv目录结构保持简单travel-planner-server/ ├── .env # 存放 API Key 等敏感配置 ├── server.js # 入口 ├── services/ │ ├── maas.js # MaaS 接口封装 │ └── cache.js # 缓存逻辑 ├── prompts/ │ └── itinerary.js # 提示词模板 └── utils/ └── validator.js # 输出校验.env文件里放 MaaS 的配置MAAS_API_KEYyour_api_key_here MAAS_BASE_URLhttps://api.example-maas.com/v1 MAAS_MODELyour_chosen_model PORT3000注意API Key 绝对不能硬编码在客户端代码里。鸿蒙卡片的所有网络请求都应该经过你自己的服务端中转由服务端去调用 MaaS。客户端直连 MaaS 会导致 Key 泄露也会让你的账单失控。4.2 MaaS 接口封装与调用封装 MaaS 调用的核心是把发送请求、处理响应、错误重试这三件事做扎实。// services/maas.js const axios require(axios); const client axios.create({ baseURL: process.env.MAAS_BASE_URL, timeout: 30000, headers: { Authorization: Bearer ${process.env.MAAS_API_KEY}, Content-Type: application/json } }); async function generateItinerary(prompt, retries 2) { for (let i 0; i retries; i) { try { const response await client.post(/chat/completions, { model: process.env.MAAS_MODEL, messages: [ { role: system, content: 你是一个专业的行程规划助手只输出JSON格式结果。 }, { role: user, content: prompt } ], temperature: 0.7, max_tokens: 2000 }); return response.data.choices[0].message.content; } catch (err) { if (i retries) throw err; await new Promise(r setTimeout(r, 1000 * (i 1))); } } } module.exports { generateItinerary };这里有几个参数需要解释。temperature 设为 0.7是一个平衡点太低比如0.2会让输出过于保守每个城市的行程都差不多太高比如1.0会让输出发散甚至出现不存在的景点。0.7 在创造性和可靠性之间取了一个折中。max_tokens 设为 2000是基于输出长度的估算。一个五天行程的 JSON 结构包含每天的景点、餐饮、交通、提示大概在1200到1800个 token 之间。设2000留了一定余量又不至于让模型输出过多冗余内容。重试机制是必须的。MaaS 接口偶尔会因为网络抖动或服务端负载出现超时重试两次能覆盖绝大多数偶发故障。重试间隔用递增的方式1秒、2秒避免短时间内反复冲击已经过载的服务。4.3 输出校验与兜底转换模型返回的内容不一定总是合法 JSON。常见的问题包括JSON 外面包了一层 Markdown 代码块标记、某个字段缺失、字符串里有多余的换行符导致解析失败。校验层要做的事情// utils/validator.js function extractJSON(raw) { // 去掉可能的 markdown 代码块标记 let cleaned raw.replace(/json\s*/g, ).replace(/\s*/g, ).trim(); // 尝试直接解析 try { return JSON.parse(cleaned); } catch (e) { // 尝试提取第一个 { 到最后一个 } 之间的内容 const start cleaned.indexOf({); const end cleaned.lastIndexOf(}); if (start ! -1 end ! -1) { try { return JSON.parse(cleaned.slice(start, end 1)); } catch (e2) { return null; } } return null; } } function validateItinerary(data) { if (!data || !data.destination || !Array.isArray(data.days)) { return { valid: false, reason: missing_core_fields }; } for (const day of data.days) { if (!day.day || !Array.isArray(day.spots)) { return { valid: false, reason: invalid_day_structure }; } } return { valid: true }; }如果校验不通过服务端不直接返回错误而是返回一个简化版的行程——只包含目的地和每天的景点名称列表去掉所有嵌套的提示信息。这样用户至少能看到一个可用的结果而不是一个报错页面。4.4 鸿蒙卡片端的完整实现卡片端的 UI 用 ArkTS 写核心是一个输入区域和一个结果展示区域。为了控制卡片高度结果区域默认只显示第一天行程用户点击展开后显示全部。// 卡片 UI 核心结构示意 Entry Component struct TravelCard { State planData: string ; State isLoading: boolean false; State inputText: string ; build() { Column() { if (this.isLoading) { LoadingProgress().width(40).height(40) Text(正在规划行程...).fontSize(14) } else if (this.planData) { Text(你的专属行程).fontSize(16).fontWeight(FontWeight.Bold) // 行程内容渲染 Text(this.planData).fontSize(12).maxLines(10) } else { TextInput({ placeholder: 说说你的出行计划 }) .onChange((value) { this.inputText value }) Button(开始规划) .onClick(() { this.startPlanning() }) } } .padding(12) } async startPlanning() { this.isLoading true; // 调用服务端接口 // 成功后更新 planData失败后显示错误提示 } }卡片和服务端之间的通信走 HTTPS请求体里带上用户输入和必要的上下文。服务端返回的 JSON 在卡片端做二次渲染把结构化的数据转成用户可读的文本。实操心得鸿蒙卡片的默认高度有限内容过多会被截断。我的做法是在卡片上只显示摘要信息目的地、天数、第一天行程完整内容通过点击卡片跳转到元服务的详情页查看。这样既保证了卡片的简洁又不丢失信息。5. 常见问题与排查技巧实录5.1 模型输出不稳定怎么办这是最常见的问题。同一个提示词有时候返回完美的 JSON有时候返回一段带解释文字的结果。排查思路按优先级排列问题现象可能原因解决方向输出包含解释性文字提示词约束不够强在提示词末尾加只输出JSON不要任何其他文字JSON 格式错误模型对嵌套结构理解偏差简化 JSON 结构减少嵌套层级字段缺失max_tokens 不够提高 max_tokens 或精简输出要求内容重复temperature 过低适当提高 temperature 到 0.7-0.8景点不存在模型幻觉在提示词中要求只推荐真实存在的景点我实测下来最有效的单一改进是在提示词里给出一个完整的输出示例。模型看到示例后格式遵循率能从70%左右提升到95%以上。示例不需要很长一个包含两天行程的完整 JSON 就够了。5.2 高峰期响应慢的优化路径国庆期间的响应时间从平时的3秒变成15秒以上用户流失率会急剧上升。优化路径按投入产出比排序第一加缓存。这是性价比最高的手段。热门目的地的常见需求组合缓存命中后响应时间降到100毫秒以内。缓存的有效期设为6小时比较合适——太短了命中率低太长了行程信息可能过时比如景点临时关闭。第二流式返回。MaaS 接口通常支持流式输出streaming模型生成一个 token 就返回一个 token。客户端可以边接收边渲染用户感知到的等待时间从完整生成时间变成第一个 token 的到达时间体验提升明显。不过鸿蒙卡片的渲染机制对流式更新的支持有限这个方案更适合元服务的详情页。第三请求合并。如果多个用户在相近时间请求了相似的目的地可以把这些请求合并成一次模型调用生成一个通用方案后再做个性化微调。这个方案实现复杂度较高适合流量规模较大的场景。5.3 卡片不刷新或显示空白鸿蒙卡片开发中遇到最多的问题就是卡片没反应。排查清单检查 module.json5 中是否声明了ohos.permission.INTERNET权限检查 FormAbility 的 onUpdateForm 回调是否被正确触发检查卡片进程的网络请求是否被系统限制卡片进程的网络能力比主应用弱检查 formBindingData 的数据是否超过了卡片支持的大小限制通常不超过1MB我遇到过一次卡片显示空白的情况排查了半天发现是返回的 JSON 数据里有一个字段包含了特殊字符导致 formBindingData 序列化失败。后来在服务端加了一层字符过滤把所有非 UTF-8 基本平面的字符都替换掉问题就解决了。避坑技巧卡片开发阶段建议在 FormAbility 的每个生命周期回调里都加上日志输出通过 DevEco Studio 的日志窗口观察卡片的实际执行流程。很多问题不是代码写错了而是某个回调根本没被触发。5.4 成本控制的几个实操手段MaaS 按调用量计费国庆期间如果不管控账单会很难看。几个有效的控制手段设置单用户每日调用上限。在服务端记录每个设备标识的调用次数超过上限后返回提示而不是继续调用模型。上限设为10次左右比较合理正常用户一天不会规划超过10次行程。输入长度限制。用户输入超过200个字符时先做一次摘要压缩再送给模型。长输入不仅增加 token 消耗还会降低模型输出的聚焦度。结果复用。同一个用户在同一天内对同一目的地的重复请求直接返回上次的结果不重新调用模型。用户改主意了想重新生成可以通过一个重新规划按钮显式触发。6. 后续可以继续打磨的方向这个项目从想法到跑通大概花了两周左右的业余时间核心功能已经可用但还有不少可以继续打磨的地方。比如把用户的历史行程偏好持久化下来下次规划时自动带上上次你喜欢自然风光类的景点这样的上下文比如接入实时的天气和交通数据让行程方案能根据当天实际情况动态调整再比如把行程方案导出成日历事件用户一键导入手机日历后就能收到每个景点的提醒。我个人在实际操作中的体会是大模型应用落地的难点从来不在模型本身而在模型和真实场景之间的那层适配层。提示词怎么写、输出怎么校验、异常怎么兜底、体验怎么优化这些看起来不起眼的工程细节才是决定一个 AI 应用好不好用的关键。模型能力是上限工程能力是下限而下限往往比上限更重要。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →