【HarmonyOS 7】鸿蒙应用集成本地 LLM 实践:能力、效果与工程方案
1. 本地 LLM 在 App 里能干什么
短问答和测试入口用来验证模型加载、tokenizer、prompt 模板、推理速度、流式输出和异常处理。
剧情反馈生成每轮只喂当前场景、相关 chunk、世界状态和用户行动,生成一段短剧情反馈。
轻量裁判辅助判断行动是否推进、是否违反世界观、是否触发关系或任务变化。MVP 阶段仍建议规则兜底。
摘要和改写对分章或 chunk 做短摘要、事件句润色、场景标题生成,但必须限制输入长度。
角色口吻补全在已有角色卡和场景约束下生成少量台词,不适合开放式陪伴聊天。
离线隐私能力小说原文留在本机,App 不需要把文本上传到云端模型。
本地 LLM 的价值不是“让手机读完整本书”,而是让手机在检索出的局部上下文里做生成、判断和润色。 长篇小说的理解仍然要靠本地切块、索引、世界状态和剧情状态机。
2. 当前实测效果
当前验证机型为 HUAWEI Mate 60 Pro,模型为 Qwen2.5-0.5B-Instruct 的 GGUF Q4_K_M 量化版本,runtime 为 native C++ + llama.cpp。 在 CPU 构建优化完成后,0.5B 测试入口的短问答已经从 20-30 秒降到约 1-2 秒。

| 阶段 | 表现 | 原因 |
|---|---|---|
| 初始 native 集成 | 简单问答 20-30 秒,部分场景 40 秒以上 | Debug native 构建未吃满 CPU 优化,prompt 和输出也偏重 |
| 异步和流式输出 | 体感改善,UI 不再卡死,但总耗时仍偏长 | 耗时推理进入 native 后台线程,ArkTS 只负责状态和渲染 |
| CPU 构建优化后 | 短问答约 1-2 秒,decode 约 40 tok/s | O3、LTO、KleidiAI、ARM dotprod/fp16 路径生效 |
| 质量修正后 | 乱答减少,但 0.5B 仍然不稳定 | 低温 / greedy、短 prompt、quality guard 和模板 fallback |
速度跑通不代表产品体验跑通。0.5B 模型可以证明端侧生成链路成立,但理解力和稳定性有限。 对互动剧情来说,它适合作为技术验证和低端 fallback;产品候选更应该从 1.5B / 2B 开始评估。
3. 推荐架构
当前方案采用 ArkTS 做应用层,Native C++ 做推理层,llama.cpp 负责 GGUF 模型加载和 token 生成。 业务层不直接把整本小说塞进模型,而是通过本地解析、切块、检索和状态机控制上下文。

核心模块分工
| 模块 | 职责 | 当前实现要点 |
|---|---|---|
| Manifest | 声明模型、runtime、格式、tokenizer、生成参数 | local_llm_validation_manifest.json |
| ArkTS Adapter | 组 prompt、控制超时、选择 fallback、更新 UI | LocalLLMGeneratorAdapter.v0 |
| 模型文件准备 | 首次把 HAP rawfile 内模型复制到应用沙箱 | 避免每次从 rawfile 直接读大文件 |
| Native Runner | 加载 GGUF、创建 context、推理、流式回调 | libbookdive_llm.so |
| Fallback | 模型失败或超时时给可读结果 | TinyBookNarrativeLM.v1 / 模板生成 |
4. 集成步骤
4.1 选择模型和格式
MVP 阶段优先选小模型和低风险格式。当前验证使用 0.5B 的 Q4 量化 GGUF,目的是先确认端侧推理、 内存、温控、流式输出和 fallback 链路,而不是追求最终质量。
{
"provider": "LocalLLMGeneratorAdapter.v0",
"selectedModel": {
"id": "Qwen/Qwen2.5-0.5B-Instruct",
"parameterCount": "0.49B"
},
"runtime": {
"selected": "NativeCppGGUF",
"nativeEntry": "libbookdive_llm.so"
},
"format": {
"preferred": "GGUF-Q4_K_M",
"preferredModelFile": "models/qwen2.5-0.5b-instruct-q4_k_m.gguf"
}
}
4.2 HAP 内置模型用于 POC
为了减少变量,当前 POC 把 GGUF 模型放进 HAP 的 resources/rawfile/models。 这样可以验证无网络、无下载、无账号的完整本地链路。
但这不是长期最佳方案。模型变成 1.5B / 2B 后,HAP 包体会明显膨胀,更合理的方式是: 首包只带基础功能,模型包由用户后续本地导入或应用内下载到沙箱,且仍不上传小说原文。
4.3 ArkTS 侧封装 Adapter
ArkTS 不直接关心 llama.cpp 的内部细节,只负责构造 prompt、设置参数、接收流式 token 和处理 fallback。
const rawResult = await bookdiveLlm.generateStream(
modelPath,
prompt,
{
maxNewTokens: 24,
timeoutMs: 9000,
temperature: 0.35,
topP: 0.75,
contextTokens: 384,
nThreads: 4,
nThreadsBatch: 4,
batchSize: 128,
ubatchSize: 64
},
(token: string) => {
receiveStreamToken(token);
}
);
4.4 Native C++ 侧接 llama.cpp
Native 层暴露 generate、generateAsync、generateStream 和 prepareModelFileAsync。耗时推理必须在 native async work 或后台线程里执行,不能阻塞 ArkUI。
// 简化后的 NAPI contract
generateAsync(modelPath, prompt, options) => Promise<string>
generateStream(modelPath, prompt, options, onToken) => Promise<string>
// 返回 JSON
{
"ok": true,
"text": "...",
"provider": "NativeGGUFRunner.llama.cpp",
"promptTokens": 73,
"decodedTokens": 16,
"firstTokenMs": 938,
"tokensPerSecond": 40.5
}
4.5 prompt 只喂必要信息
我的项目BX的本地 LLM 每轮只喂四类信息:
- 当前场景:标题、地点、人物、目标
- 相关 chunk:从本地索引检索出的少量片段摘要
- 世界状态:回合数、任务、关系、裁判标签
- 用户行动:用户这一轮输入
系统:你是 BookDive 的本地剧情生成器。只按给定世界信息续写。
用户:
场景:夜色中的客栈
地点:客栈
人物:主角、掌柜
目标:弄清门外脚步声
线索:掌柜听见门外有脚步声
状态:回合3,任务推进,关系0
用户行动:我低声询问掌柜发生了什么
要求:一段剧情反馈;不要解释规则。
5. 性能优化关键点
这次性能提升的关键不是“换了更强模型”,而是让 llama.cpp 在鸿蒙设备的 ARM CPU 上真正走优化路径。
set(GGML_LTO ON CACHE BOOL "" FORCE)
set(GGML_CPU_ARM_ARCH "armv8.2-a+dotprod+fp16" CACHE STRING "" FORCE)
set(GGML_CPU_KLEIDIAI ON CACHE BOOL "" FORCE)
set(BOOKDIVE_ARM_CPU_FLAGS "-fvectorize -ffp-model=fast -fno-finite-math-only")
set(CMAKE_C_FLAGS_DEBUG "-O3 -DNDEBUG -g0" CACHE STRING "" FORCE)
set(CMAKE_CXX_FLAGS_DEBUG "-O3 -DNDEBUG -g0" CACHE STRING "" FORCE)
| 优化项 | 作用 | 注意点 |
|---|---|---|
O3 / DNDEBUG |
避免 Debug 构建里实际走 O0,这是移动 CPU 推理的大坑 |
构建后要检查 build.ninja,确认 flag 真正生效 |
GGML_CPU_KLEIDIAI |
启用 ARM CPU 优化内核 | 需要确认目标设备 ABI 和指令能力,不要盲目上过高架构 |
LTO |
链接期优化,减少推理路径开销 | 会增加构建时间 |
| 小 ctx / 短输出 | 显著减少 prefill 和 decode 时间 | 真实剧情不能无限压缩,否则质量下降 |
| context 复用 | 避免每轮重建 llama context | 每轮要清 KV cache,防止上下文串扰 |
| 流式输出 | 改善体感,让用户尽早看到文字 | 需要处理 token callback 节流和 UI 刷新频率 |
6. 已踩过的坑
6.1 慢不一定是模型太大,可能是构建没优化
0.5B 初始表现曾经是 20-30 秒,这很容易让人误判为“手机 CPU 跑不动 LLM”。 但开启 O3、KleidiAI、LTO 后,短问答直接降到秒级。
6.2 0.5B 会乱答,要有质量护栏
小模型在短 prompt、随机采样下容易输出不相关套话。例如用户问“你能做什么”,模型可能输出与问题无关的古风句子。 修正方式包括:
- 测试入口使用
temperature=0或 greedy - 剧情生成使用低温和较小
topP - prompt 明确“只回答当前输入”
- 对固定指令、问候、能力问题做 quality guard
- 模型失败或跑题时回退到模板 / TinyBookNarrativeLM
6.3 真实 token 数要以 native tokenizer 为准
ArkTS 侧的 token 估算只能做预算提示,不能作为安全边界。一次 crash 就来自 prompt 变长后, 真实 token 数超过 n_batch,llama.cpp 内部触发 ggml_abort。 修复方式是在 native 层真实 tokenize 后按 batch 分段 decode。
bool DecodePromptInChunks(ctx, tokens, batchSize, error) {
for (offset = 0; offset < tokens.size(); offset += batchSize) {
current = min(batchSize, tokens.size() - offset);
batch = llama_batch_get_one(tokens.data() + offset, current);
if (llama_decode(ctx, batch) != 0) return false;
}
return true;
}
6.4 不要把重活放在 UI 线程
模型复制、模型加载、tokenize、prefill、decode 都是重活。ArkTS 页面只做状态管理和渲染, 本地 TXT 解析走 worker,LLM 推理走 native async work。
6.5 HAP 内置模型只适合验证
HAP 内置 0.5B 模型可以快速验证链路,但模型越大包体越不可控。1.5B / 2B 之后建议做模型包管理: 下载、校验、沙箱存储、版本切换、低端机 fallback。
更多推荐




所有评论(0)