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 O3LTOKleidiAI、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 层暴露 generategenerateAsyncgenerateStream 和 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”。 但开启 O3KleidiAILTO 后,短问答直接降到秒级。

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。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐