前阵子接了个活儿:给一家内容团队做一款AI内容创作应用,要求是手机、平板都能跑,界面要顺滑,还得兼顾写作、配图、短视频脚本这些多个场景。技术选型阶段我们翻了几个方向,最后敲定用 ArkTS 来做这套跨端 UI,配合云端大模型 API 完成生成能力。现在回头看,这个“ArkTS + 智能融合”的组合看起来不复杂,真正落地时牵扯的东西比想象中多很多:声明式 UI 的状态管理、流式响应、多模型编排的 Agent 工作流、并发控制、还有内容合规审核。这篇文章就把整个项目的设计思路、工程搭建、关键代码和踩过的坑完整拆一遍,给想动手做鸿蒙端 AI 应用的朋友一条能直接参考的技术路线。
如果你正打算用 ArkTS 开发自己的 AI 应用,或者已经在写了但卡在工程结构和工作流设计上,这篇内容应该能帮你省下不少试错时间。我先说结论:纯端侧做 AI 内容生成可以跑通,但要做得像成熟产品,核心不在 UI,而在“如何把多个 AI 能力编排成稳定、可维护的生成管线”。下文会具体展开。
1. 整体设计与方案选型
1.1 为什么选 ArkTS 而不是 WebView 套壳
最开始方案评审时,有人提过 WebView 套壳,理由是可以复用前端生态,开发效率高。但仔细一想,这个思路在 AI 内容创作场景里有三个硬伤。
第一个是长文本和图片渲染的流畅度。内容创作类页面要频繁刷新大段文本、图片网格、进度状态,WebView 在滚动和重渲染上容易出现白屏或卡顿,尤其在平板和大屏场景下更明显。ArkTS 走的是声明式原生渲染,结合组件级刷新,长列表和图片墙表现稳得多。实测下来,同样加载 50 张生成图缩略图,ArkTS 的滚动跟手度明显好过 WebView。
第二个是系统能力集成。AI 创作应用不只是“生成内容”,后续还要保存草稿、分享到社交平台、调用系统分享面板、写本地文件。ArkTS 可以直接调系统 API,这些能力在 WebView 里绕一圈还要做 JSBridge,性能和稳定性都打折。
第三个是状态管理。AI 生成过程是典型的异步状态流:空闲、生成中、流式输出、完成、失败。WebView 里如果前端框架没设计好,状态一多就乱。ArkTS 内置的 @State、@Prop、@Link 这类状态装饰器,配合响应式刷新机制,天然适合表达这种状态机。我们后期加“生成中逐字显示”功能时,几乎没改架构,只是加了几个状态字段和渲染分支,这正是声明式 UI 的优势。
1.2 “智能融合”融合的是三层能力
项目名里的“智能融合”,我理解不是简单的“App 里塞了个 AI 按钮”,而是三层能力的叠加。
第一层是端侧交互与云端算力的融合。ArkTS 只负责界面、交互、本地缓存,真正的大模型推理全部走云端 API。这样端侧包体小,模型迭代也无需发版,换模型只需要改服务端配置。但这层融合的关键点是网络层的健壮性,后面会详细讲超时、重试和流式读取。
第二层是多模态生成的融合。内容创作天然需要多模态:写文章要有文本模型,做配图要有图像模型,做短视频脚本还要有结构化分镜能力。我们把文本生成、图像生成、结构化输出都收口到统一网关,再在 ArkTS 侧按业务场景组合调用。
第三层是 Agent 工作流的融合。这是“融合”里最有价值也最容易被忽略的部分。单个模型调用只能解决单点任务,比如“写一段话”或者“画一张图”。但真实创作者要的是“给一个主题,产出一套完整内容包”,包含大纲、正文、标题、摘要、配图描述和图片。这就需要把多个模型按照工作流编排,节点之间传递上下文,这就是 Agent 的雏形。我们在第 4 章会专门拆这部分实现。
1.3 端侧直连大模型,还是统一走网关
技术方案里最容易被低估的一个决策,就是端侧到底怎么连模型服务。很多 Demo 直接在 ArkTS 里写死 API Key,然后请求模型厂商的开放接口。如果只是本地自测没问题,一旦上线,这么做等于把密钥和计费开关交给所有人。
我们的做法是搭建一层极简网关,专门负责:密钥管理、请求鉴权、流量控制、内容审核日志。端侧只携带用户登录态,请求统一发到网关域名,由网关决定路由到哪个模型。
这样设计有三个实际好处。第一,换模型供应商时端侧代码零改动,只需要网关调整路由配置。第二,可以在网关层做请求级别的内容安全审核,所有用户输入先过一遍校验再送模型,所有模型输出先过一遍审核再回端侧。第三,方便统计成本,每个用户每天生成了多少 token、多少张图,都能按账号维度汇总,避免被恶意刷爆预算。
2. 核心功能拆解与场景设计
2.1 AI 写作助手:长文生成、改写与润色
先拆分最核心的写作功能。产品目标不是做一个“打开就输入一句话出全文”的玩具,而是覆盖创作者的真实工作流:定主题、列大纲、写初稿、改标题、润色语气、压缩长度。
所以我们在 ArkTS 侧把写作助手设计成了多步骤页面,而不是单框输入。第一步让用户输入主题或零散想法,第二步系统生成大纲,用户可以在页面上直接增删大纲节点,第三步按大纲逐段生成正文,第四步提供标题候选、摘要生成、语气调整三个后处理按钮。
这里有个体验细节值得提:长文生成不能干等一个接口返回全文,一定要做成流式输出。用户看着文字逐段出现,感知速度比白屏等待好非常多,而且中途如果发现方向不对,可以立即停止,节省 token。流式输出的实现方式我会在第 4 章给出具体做法。
2.2 AI 绘画:提示词解析与参数化出图
绘画功能的关键不是“调用一个文生图接口”,而是把图片生成的不可控程度降下来。我们做了一层提示词模板化:用户只需要选择画面主体、风格、光线、镜头语言等维度,页面自动拼接成结构化提示词。
这些维度在 ArkTS 侧直接渲染成下拉选择和滑块组件。比如“画面主体”选择“城市夜景”“森林小径”“赛博朋克街角”,“风格”选择“写实摄影”“水彩插画”“3D 渲染”,每个选项背后都对应预置的提示词片段。拼装完成后,用户还可以在“高级模式”里直接编辑完整提示词。
图像生成还需要暴露几个关键参数:分辨率、采样步数、提示词引导强度 CFG。很多新手不知道,CFG 值过高会让图像出现过度饱和和伪影,过低则偏离提示词;我们在 UI 上把滑块的默认值设为 7.5,并加了实时提示的说明文案,避免用户一顿乱调出废图。
2.3 AI 短视频脚本:从主题到分镜
短视频创作需求其实比长文更旺盛,也更吃结构。我们的实现方式是让 AI 先生成脚本框架,包括:时长设定、口播文案、画面描述、分镜顺序、字幕建议、BGM 情绪。这个过程用一次结构化输出完成,远端模型返回 JSON,ArkTS 侧解析后渲染成可编辑的分镜卡片。
用户可以在卡片上直接修改每一镜的口播词或画面描述,修改后再触发“重新生成分镜”的局部更新功能。这个交互做起来比听起来复杂,因为要保证只重生成被修改的那几镜,而不是整个脚本推倒重来。我们在提示词里严格约束了输入格式和范围,后面会提到。
这项功能对内容团队最大的价值是“批量产出”。运营同时输入 10 个选题,系统自动跑出 10 套脚本初稿,人工只需要微调,日更压力小很多。
2.4 一键内容包:Agent 工作流的场景价值
如果只有上述单点功能,它仍然是个工具集合,谈不上“智能融合”。真正让我们项目通过评审的,是“一键内容包”功能:用户只输入一个主题,系统自动完成大纲、正文、标题、摘要、配图、脚本提示词全套产出。
这背后的架构就是 Agent 工作流。我们把一次“内容包生成”看成一个流水线,每个节点是一个模型的独立调用,节点与节点之间通过上下文对象传递产物。大纲节点的输出是正文节点的输入,正文节点的输出又同时喂给标题节点和摘要节点,配图节点的提示词来自标题和正文的组合。
这个设计让“多 AI 协作”不再是概念,而是实打实的执行过程。用户在 ArkTS 页面点一下“生成内容包”,看到的是一次多步骤任务的进度展示——大纲完成、正文完成、标题完成、配图完成,每一阶段都可以单独重试。比起一次长请求等待到底,这种过程透明化的体验更容易建立信任感。
3. 环境准备与工程搭建
3.1 DevEco Studio 工程与权限准备
开发环境用的是 DevEco Studio,版本选的是支持 HarmonyOS NEXT 的稳定版。创建工程时选择“Empty Ability”模板,后续所有页面基于 ArkTS 声明式语法编写。编译器对 API 版本有要求,我们统一用 API 12 以上的 SDK,因为新版网络组件和媒体组件都更完整,而且官方推荐用 @kit 开头的 Kit 模块引用。
新建工程后第一件事不是写页面,而是配置权限。访问云端 AI 服务必须申请网络权限,在module.json5的requestPermissions里加上 INTERNET 权限:
{ "module": { "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ] } }这里有个新人也容易忽略的坑:如果后端还是 HTTP 调试(生产环境千万别用 HTTP),还需要在网络安全配置里放行明文流量。我们在本地联调阶段就因为这个白屏了好一阵,后来在工程的网络安全配置里加了cleartextTraffic允许项才跑通。正式环境全部切 HTTPS,这个调试配置只留在测试包中。
3.2 后端服务接口约定
统一网关的接口形态要提前约定好,不然 ArkTS 侧和后端服务经常扯皮。我们最终约定了一套极简规范,三个核心接口:
- POST
/api/v1/chat:文本生成,支持流式返回 - POST
/api/v1/image:图像生成,返回图片 URL 列表 - POST
/api/v1/workflow:内容包工作流,返回分阶段产物
请求体统一为 JSON 格式,至少包含prompt和params两个字段;响应体统一为{ code, message, data }结构。图片接口的data是 URL 数组;文本接口的data在非流式模式下是完整文本,在流式模式下走 SSE 或 WebSocket 分帧。
因为 ArkTS 内置的 http 请求模块对流式读取支持有限,我们把大模型流式输出统一转成了 WebSocket 推流。网关收到模型流式内容后做第一轮审核过滤,通过的内容按帧推给端侧,端侧每帧增量渲染。这个设计同时解决了体验问题和内容安全校验问题。
3.3 ArkTS 页面骨架:Tab 栏、列表与状态管理
应用首屏是四个 Tab:创作、画图、内容包、历史。用Tabs组件实现,每个 Tab 对应一个独立页面组件。
页面状态管理有几个核心字段:prompt(用户输入)、generating(是否生成中)、progress(流式进度)、resultItems(生成结果列表)、errorMsg(错误信息)。都定义在页面组件的顶部,用 @State 装饰器管理:
@Entry @Component struct CreatePage { @State prompt: string = ''; @State generating: boolean = false; @State progress: string = ''; @State resultItems: ContentItem[] = []; @State errorMsg: string = ''; }当流式输出不断更新progress时,ArkTS 的响应式系统会自动刷新对应 Text 组件;当生成完成一次性更新resultItems时,ForEach渲染列表自动重建。这个数据流非常直观,也是我们放弃 WebView 方案的原因之一——在 ArkTS 里写这样的异步状态更新,代码量几乎只有前端方案的一半。
4. 核心实现与关键代码
4.1 网络请求封装:从 HTTP 到 WebSocket 流式输出
先看普通文本请求的封装。ArkTS 里可以用@kit.NetworkKit提供的 http 模块:
import { http } from '@kit.NetworkKit'; class AiService { private baseUrl: string = 'https://api.example.com/api/v1'; async generateText(prompt: string, params: Record<string, Object>): Promise<string> { const httpRequest = http.createHttp(); const response = await httpRequest.request( this.baseUrl + '/chat', { method: http.RequestMethod.POST, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + getToken() }, extraData: JSON.stringify({ prompt, params }), expectDataType: http.HttpDataType.STRING, readTimeout: 60000, connectTimeout: 15000 } ); if (response.responseCode === 200) { const result = JSON.parse(response.result as string); if (result.code === 0) { return result.data.text; } throw new Error(result.message); } throw new Error('HTTP ' + response.responseCode); } }这个封装有几点要注意。第一,readTimeout必须调大,AI 推理不是普通接口,一个长文请求跑 30 秒很正常,默认超时肯定不够。第二,错误处理不能只看 HTTP 状态码,业务错误在code字段里,比如内容审核不通过、余额不足、模型限流,都要单独抛出并反馈给用户。第三,生产环境不要在代码里拼装 token,从安全存储模块读取。
流式输出我们走 WebSocket。每一帧的格式约定为{ type: 'delta', content: '...' },端侧收到后追加到当前文本尾部:
socket.on('message', (frame: string) => { const data = JSON.parse(frame); if (data.type === 'delta') { this.progress += data.content; } else if (data.type === 'done') { this.generating = false; } else if (data.type === 'error') { this.errorMsg = data.message; this.generating = false; } });这样用户看到的生成过程是逐字跳出来的,而不是白屏等待后突然出现整篇文章。实测这个改动对体验提升明显,内测用户普遍反馈“感觉 AI 在思考”。
4.2 创作页面的完整状态处理
创作页面的核心交互是:输入主题 → 生成大纲 → 用户编辑大纲 → 生成正文。页面逻辑要处理好多个状态的切换和异常回滚。
状态机设计为四个主要状态:input(输入阶段)、outlineLoading(大纲生成中)、editing(大纲编辑中)、articleLoading(正文生成中)。每个状态对应不同的按钮可用性和页面展示。ArkTS 中我们用 @State 加条件渲染来表达:
if (this.stage === 'outlineLoading') { LoadingProgress().width(32).height(32); } else if (this.stage === 'editing') { OutlineEditor({ outline: this.outline, onChange: this.onOutlineChange }); }这里有个实操经验:生成大纲和生成正文是两个独立接口调用,而不是一个接口返回全部。看起来多了一次请求,但好处是用户可以在大纲阶段就介入,避免在错误方向上浪费 token。我们内测时发现,没有大纲确认环节时,生成的长文跑题率大约 30%,加了确认环节后降到 5% 以下。
4.3 参数调优:温度、最大长度、Top-P 怎么设
AI 生成的随机性参数如果不调,效果会非常不稳定。我们在 ArkTS 页面上暴露了温度、Top-P、最大长度三个参数,每个参数都给了一个适合当前场景的推荐值。
| 场景 | 温度 | Top-P | 最大长度 | 备注 |
|---|---|---|---|---|
| 新闻摘要 | 0.3 | 0.6 | 500 | 越接近 0 越稳 |
| 营销文案 | 0.8 | 0.9 | 1000 | 需要创意发散 |
| 长文生成 | 0.7 | 0.9 | 3000 | 平衡连贯与变化 |
| 代码生成 | 0.1 | 0.5 | 1000 | 尽量低随机性 |
| 分镜脚本 | 0.6 | 0.8 | 1500 | 需要结构化创新 |
温度是控制随机性的核心参数。做摘要时温度设 0.3 会让输出更稳妥,几乎不会出现事实偏移;做广告文案时温度提到 0.8,模型会给出更多意想不到的表述。Top-P 与温度是互补关系,一般固定 0.9 不用动,除非遇到特别需要确定性的场景。
我们的建议是把这些参数内置到每个功能模块的默认配置里,高级模式才让用户改。而不是在一级页面堆一堆滑块,普通用户根本不知道怎么选,反而破坏体验。
4.4 Agent 工作流落地:多模型协作下的任务编排与重试
内容包功能是项目里最有 Agent 味道的部分。它的实现核心是一个任务编排器,负责把一个复杂的生成任务拆成多步独立调用,并管理依赖关系、上下文传递和失败重试。
简化后的代码逻辑是这样的:
async function runContentWorkflow(topic: string): Promise<ContentPackage> { const steps = [ { name: '大纲', run: () => callChat('生成大纲', topic, outlineParams) }, { name: '正文', run: (ctx) => callChat('扩写正文', ctx.outline, articleParams) }, { name: '标题', run: (ctx) => callChat('生成标题', ctx.article, titleParams) }, { name: '摘要', run: (ctx) => callChat('生成摘要', ctx.article, digestParams) }, { name: '配图', run: (ctx) => callImage('配图描述', ctx.title, imageParams) } ]; const context: Record<string, Object> = {}; for (const step of steps) { for (let attempt = 0; attempt < 3; attempt++) { try { context[step.name] = await step.run(context); break; } catch (e) { if (attempt === 2) throw e; await wait(1000 * Math.pow(2, attempt)); } } } return context as ContentPackage; }每步的输入来自前面步骤的输出,通过context对象传递。这种 Chain 式编排虽然简单,但在内容场景已经足够稳定。要注意两点:每步之间要允许用户中断,比如“只要大纲没走到正文,点击取消就直接停止”;每步要单独设置超时和重试,避免某一次模型限流导致整个工作流失败。
配图节点是一个容易出问题的地方。描述写得好,图就出得好;描述写得空,图就随机得离谱。所以我们在提示词里把配图描述规则约束为“必须包含主体、场景、光线、构图、镜头语言五个要素”,这样下一环节的图像模型拿到的是稳定的结构化输入,图的质量有明显提升。
4.5 并发与性能:请求队列与结果缓存
内容创作用户的点击惯性很强,经常连点好几次生成按钮。如果不加控制,同一个账号可能瞬间发出十几个并发请求,网关一看就是在刷接口。
我们在 ArkTS 侧做了三重防护。第一重,生成中状态把按钮置灰,从入口杜绝重复提交。第二重,前端维护一个简单的请求队列,如果确有批量生成需求(比如一次跑 10 个选题),请求按序执行,并发数限制在 2 个以内。第三重,网关层做账号维度的并发限制,超过阈值直接返回“请求繁忙,请稍后再试”。
缓存策略是减少重复请求的另一招。同一个提示词和参数组合生成过的结果,在服务端做哈希缓存,重复请求直接命中缓存返回,省 token 也省等待时间。历史记录直接存本地数据库,ArkTS 侧用@ohos.data.relationalStore建了一张简单的生成记录表,字段包括 prompt、类型、结果摘要、创建时间,方便用户随时翻看旧内容。
5. 常见问题与排查技巧实录
5.1 响应超时与断流:流式输出中断怎么恢复
实际运营中遇到最频繁的问题是长文生成中途断流。原因通常是网关到模型服务之间的连接不稳定,或者模型推理时间超过了中间层的超时阈值。
排查思路分三步:先看错误发生在端侧还是网关侧。端侧表现为帧消息中断且无 error 帧;网关侧则能看到日志中模型服务返回了超时错误码。如果出现在网关侧,就调整网关到模型服务的 readTimeout,并且让模型以增量方式返回结果,不要等服务端攒满全部 token 再推流。
端侧的恢复策略是“断点续生”。内容是分段追加的,中断时记录已生成的进度位置,用户点击“继续生成”时,端侧把已生成的最后 200 字作为上文,连同原始提示词一起再请求一次,让模型接着续写。虽然偶尔会有衔接重复,但比从头再来体验好得多。
5.2 中文乱码与半截字符
SSE 推流时容易出现半截字符的问题。ArkTS 端如果直接按帧内容追加字符串,偶尔会遇到多字节字符被切断的情况,比如中文字符或者 emoji 被拆成两半,界面出现“�”。
解决方法是做一个缓冲拼接层:收到一帧后先和上一帧的残留内容拼起来,再按字符边界拆出完整部分渲染。实际操作中我们不直接判断字节,而是在文本层校验,只有拿到结束标记(比如换行符或标点)才输出当前行。这个方法虽然简单,但能处理绝大多数乱码问题。
还有一个隐藏点:网络模块默认可能用 ISO-8859-1 解析,我们在请求头里明确声明Accept-Charset: utf-8,后端也统一返回 UTF-8,避免字符集不一致导致的乱码。
5.3 状态不同步与列表刷新不及时
ArkTS 的响应式刷新偶尔会有“页面没更新”的错觉。排查下来大部分情况不是框架 bug,而是开发者把可变对象直接赋值给了 @State,但对象内部字段变化没有触发刷新。
解决方法是:不要直接修改 @State 对象的嵌套属性,而是生成新对象再整体赋值。比如更新结果列表,不要用this.items[0].title = 'xxx',而是:
const newItems = this.items.map((item, index) => { if (index === 0) { return { ...item, title: 'xxx' }; } return item; }); this.items = newItems;数组整体替换的方式 ArkTS 能明确识别到状态变化,列表渲染也会稳定刷新。这个坑我们团队新同学踩过好几次,写在这里希望大家少走弯路。
5.4 AI 输出的内容安全与隐私治理
AI 内容创作应用上线前必须面对内容安全合规问题,这部分不是可选项,而是基础工作。我们在架构上做了几层防护。
第一层是用户输入过滤。所有用户输入的 prompt 先过关键词校验规则,异常内容直接阻断请求,不进模型。第二层是模型输出审核。流式输出经过网关时,每一帧都做内容复核,有问题就过滤掉该帧并返回提示。第三层是数据治理:客户端不长期保留用户对话记录,只保存用户主动点击“保存草稿”的内容;后端日志脱敏,不记录完整 prompt,只记录长度和请求时间戳用于排障。
隐私声明也做了明确说明,告知用户哪些数据会上传、用于什么目的、保存多久。灰度内测阶段我们就同步上线了这套机制,审核没出过问题。这些工作在项目初期就要设计进去,如果等量大了再补,改造成本会非常高。
最后分享点实际体会
这类 AI 内容创作应用,最花时间的不是调模型接口,而是把“生成体验”打磨得稳定。我自己养成的习惯是:先跑通一个最小闭环,比如只做“输入主题→生成标题”,把链路里的超时、重试、状态管理、错误提示全部走顺,确认骨架没问题,再往上加长文、配图、工作流这些重功能。
另一个经验是多让真实用户参与测试。第一版我们内部觉得挺好用的内容包功能,内测用户反馈说“步骤太多,不知道怎么从草稿继续改”。后来把历史记录和内容包入口合并成一个“创作台”页面,用户路径短了一半,使用率明显上来。技术选型再合理,也要认认真真面对用户的实际习惯。这套从技术到体验的完整落地过程,比单纯接几个模型 API 有价值得多,值得每一步都做扎实。