小模型回到本地:NPU、端侧推理和开发者的新耐心
原创 · 约 34 分钟阅读 · 阅读 --
Last updated on

小模型回到本地:NPU、端侧推理和开发者的新耐心

作者: Alex Xiang


古董级程序员,大厂出来后一直在创业公司,现在仍在一线做 AI 相关的工程。更完整的技术记录写在微信公众号「字与码」:工作经历、对新工具的看法,以及这些年踩过的坑,会不定期发在那里。若这篇对你有用,欢迎顺手关注。

我对端侧小模型的兴趣,不是来自“把大模型塞进手机”这类口号,而是来自一个很具体的需求:做一个离线会议助手。它要在笔记本和手机上工作,能在没有网络的会议室里生成本地摘要,能先把敏感信息在设备上处理掉,必要时再把脱敏后的材料交给云端兜底。

这个需求听起来不大,却能把端侧 AI 的难点都翻出来:模型下载、冷启动、NPU 和 CPU fallback、音频转写后的本地摘要、隐私缓存、云端兜底开关、设备碎片化、发布灰度、回滚和验收。端侧小模型不是云端大模型的迷你版,它更像一个必须长期住在用户设备里的功能模块。

下面我用这个离线会议助手做主线,记录一套可以复制的工程方案。它不是某个真实产品的内部实现,所有名称、URL、token 和客户信息都用公开示例替代。

端侧小模型运行链路图

先把产品边界写窄

离线会议助手第一版只承诺四件事。

它能在本地处理一段会议音频转写文本,生成 5 到 8 条要点。它能识别并遮蔽明显的手机号、邮箱、身份证样式字段、合同号样式字段。它能在用户同意后,把脱敏摘要和片段索引发给云端,让云端生成更完整的纪要。它能在没有网络时保存草稿,等用户确认后再同步。

它不承诺自动识别所有发言人,不承诺做法律级事实核验,不承诺离线回答最新政策,也不承诺把两小时会议完整压进一个小模型上下文。边界写窄不是保守,而是端侧产品必须对资源和信任负责。

第一版的本地链路是这样的:

音频文件或实时转写文本
  -> 本地分段器
  -> 隐私预处理器
  -> 小模型摘要器
  -> 本地草稿和片段索引
  -> 用户确认
  -> 可选云端兜底

这里的小模型不是唯一主角。分段、脱敏、缓存、权限和兜底一样重要。端侧 AI 做得好,常常不是因为模型特别聪明,而是每一步都知道自己该做多少。

一个请求长什么样

本地接口没有必要模仿云端 OpenAI API,但保留结构化请求会让调试和回放方便很多。会议助手内部给本地推理服务发这样的请求:

{
  "request_id": "local_meeting_20260615_001",
  "mode": "offline_summary",
  "device_context": {
    "platform": "macos",
    "power": "battery",
    "network": "offline",
    "accelerator": "ane",
    "memory_pressure": "normal"
  },
  "privacy_policy": {
    "redact_before_cache": true,
    "allow_cloud_fallback": false,
    "retention_hours": 24
  },
  "input": {
    "meeting_title": "产品周会",
    "language": "zh-CN",
    "segments": [
      {
        "start_ms": 12000,
        "end_ms": 42600,
        "speaker": "S1",
        "text": "我们本周先把离线摘要做成 beta,不要默认上传音频。"
      },
      {
        "start_ms": 43100,
        "end_ms": 86500,
        "speaker": "S2",
        "text": "如果用户打开云端增强,只上传脱敏后的摘要和片段索引。"
      }
    ]
  },
  "output": {
    "max_bullets": 8,
    "include_actions": true,
    "include_risks": true
  }
}

返回也要能解释本地做了什么:

{
  "request_id": "local_meeting_20260615_001",
  "status": "ok",
  "summary": [
    "离线摘要 beta 阶段不默认上传音频。",
    "云端增强需要用户主动开启,并且只接收脱敏后的摘要和片段索引。"
  ],
  "actions": [
    {
      "owner": "未识别",
      "text": "补充云端增强的授权弹窗文案。",
      "evidence_segment_ids": [2]
    }
  ],
  "redactions": [
    {
      "type": "email",
      "count": 1,
      "replacement": "[EMAIL_1]"
    }
  ],
  "runtime": {
    "model": "meeting-summarizer-1.8b-q4",
    "backend": "ane",
    "input_tokens": 1462,
    "output_tokens": 214,
    "prefill_ms": 382,
    "decode_ms": 1040,
    "peak_memory_mb": 1180,
    "cloud_fallback_used": false
  }
}

这类结构化返回有两个好处。产品可以把“云端未使用”“已脱敏”“本地草稿保存 24 小时”清楚地展示给用户;工程也可以在不记录原文的情况下知道模型是否跑在加速器上、耗时多少、是否触发 fallback。

设备能力探测比模型榜单更早

端侧项目最容易犯的错,是先挑一个看起来分数不错的模型,再想办法让它跑起来。会议助手反过来做。先定义设备矩阵,再选模型。

第一版只覆盖三类设备:

设备档位目标允许策略
高端笔记本10 分钟会议 15 秒内出草稿本地摘要、脱敏、可选云端增强
普通手机10 分钟会议 30 秒内出草稿本地短摘要、低电量降级
低端或旧设备只做隐私预处理和片段索引不强行本地生成长摘要

设备探测在功能入口就发生,而不是推理失败后才补救:

{
  "device_id_hash": "dev_anon_7f2a",
  "platform": "android",
  "os_version": "15",
  "app_version": "1.4.0",
  "accelerators": ["nnapi", "cpu"],
  "recommended_backend": "nnapi",
  "available_memory_mb": 3260,
  "battery_level": 0.42,
  "thermal_state": "nominal",
  "model_profile": "meeting-lite"
}

如果设备只能稳定跑 meeting-lite,界面就不要给用户承诺“完整纪要”。可以给“本地要点”和“云端增强”两个清晰选择。端侧 AI 的体验很大一部分来自诚实,不是来自把所有能力都塞进一个按钮。

模型包不只是一个文件

会议助手用了两个本地模型包。一个很小,负责隐私预处理和意图分类;一个稍大,负责会议摘要草稿。它们和 tokenizer、prompt 模板、后处理规则绑定发布。

模型清单像这样:

models:
  - id: privacy-filter-280m-q8
    role: privacy_preprocess
    size_mb: 210
    min_app_version: "1.4.0"
    backends: ["ane", "nnapi", "cpu"]
    checksum: "sha256:example-privacy-filter"
    retention: bundled
  - id: meeting-summarizer-1.8b-q4
    role: offline_summary
    size_mb: 1260
    min_app_version: "1.4.0"
    backends: ["ane", "nnapi", "directml", "cpu"]
    checksum: "sha256:example-meeting-summarizer"
    download:
      wifi_only: true
      resume: true
      keep_previous_version: true
prompts:
  summary_template_version: "summary-v6"
  action_template_version: "action-v3"
postprocess:
  schema_version: "meeting-note-v2"

这里最容易被低估的是版本绑定。新模型可能要求新的 tokenizer,也可能改变输出格式。只替换权重文件,不更新 prompt 和后处理,很容易让客户端解析失败。端侧发布比服务端更麻烦,因为坏版本已经到了用户设备上,不是重启一个 Pod 就能消失。

所以模型包必须有签名、校验、最小 App 版本、可回滚的上一版本。下载也要可恢复,不能让用户在会议前卡在一个 1GB 文件上。

本地摘要不是把整场会议塞进去

小模型上下文有限,移动设备更有限。会议助手不把整场会议一次性喂给模型,而是先做分段,再做局部摘要,最后合并。一个 45 分钟会议可能被拆成 20 到 40 个片段。

分段器用简单规则和轻量模型混合:静音超过一定时间、话题关键词变化、发言人变化、片段 token 超过上限。每段生成一个局部草稿:

{
  "segment_id": 12,
  "time_range": "00:18:22-00:21:05",
  "input_tokens": 1180,
  "local_summary": "团队决定 beta 阶段默认只保存本地草稿,云端增强需要用户主动确认。",
  "actions": [
    "补充授权弹窗",
    "把云端增强的上传字段写入隐私说明"
  ],
  "risk_flags": ["privacy_notice_required"]
}

全局摘要只读取这些局部草稿和少量证据片段,而不是读取完整原文。这样做牺牲了一部分跨段推理能力,但换来稳定的内存、可解释的证据和更低的隐私风险。

端侧小模型适合做“足够好的草稿”,不是替人拍板。会议纪要这种场景尤其要保留证据片段,让用户能点回去看原话。没有证据的漂亮总结,在线上看起来聪明,在实际工作里很危险。

隐私预处理在缓存之前

“数据不出设备”不等于隐私问题自动解决。会议助手会在本地缓存草稿、片段索引和模型中间结果。如果缓存里保留了手机号、邮箱、合同号,风险仍然在设备上长期存在。

所以我们把隐私预处理放在缓存之前。原始转写文本只保存在受保护的临时区,默认 24 小时过期。进入持久缓存的内容必须先脱敏。

privacy:
  raw_transcript:
    storage: protected_tmp
    ttl_hours: 24
    sync: false
  redacted_segments:
    storage: encrypted_local_db
    ttl_days: 30
    sync: optional
  cloud_payload:
    allowed_fields:
      - redacted_summary
      - action_items
      - evidence_segment_ids
      - language
    denied_fields:
      - raw_audio
      - raw_transcript
      - speaker_voiceprint

脱敏规则不要只靠大模型。确定性规则负责邮箱、手机号、证件号样式、银行卡样式;小模型负责识别“这可能是客户名称、内部项目代号、合同编号”这类上下文信息。两者都不完美,所以界面上要允许用户查看将要上传的内容。

云端兜底的请求因此长这样:

{
  "request_id": "cloud_enhance_20260615_001",
  "consent_id": "consent_local_9a21",
  "source": "offline_meeting_assistant",
  "payload": {
    "language": "zh-CN",
    "redacted_summary": [
      "团队决定 beta 阶段默认只保存本地草稿。",
      "[PROJECT_1] 需要补充授权弹窗和隐私说明。"
    ],
    "action_items": [
      "补充授权弹窗",
      "确认云端增强上传字段"
    ],
    "evidence_segment_ids": [3, 12, 18]
  }
}

注意这里没有原始音频、没有完整转写、没有真实人名邮箱。云端拿到的是本地预处理后的材料。这个链路不如“全量上云”聪明,但它更容易被用户和企业安全团队接受。

云端兜底是正常路径,不是失败路径

很多端侧方案把云端兜底写成“本地失败才调用云端”。会议助手里不这么设计。云端增强是一条用户可见的正常路径:本地先出草稿,用户看过脱敏内容后,选择是否增强。

本地模式下,用户得到的是要点、待办和风险提示。云端增强模式下,用户可以得到更完整的章节、决策背景、冲突点整理和格式化纪要。二者不是谁替代谁,而是成本、隐私和质量的不同选择。

工程上也不能做双倍浪费。云端增强不从零处理会议,而是接收本地摘要、待办、片段索引和少量已脱敏证据。这样云端上下文更短,费用更低,用户也知道上传了什么。

如果企业策略禁止云端,功能仍然可用,只是不展示增强入口:

{
  "policy": "local_only",
  "features": {
    "offline_summary": true,
    "privacy_preprocess": true,
    "cloud_enhance": false,
    "share_redacted_note": true
  },
  "message_code": "cloud_disabled_by_policy"
}

这个错误语义比“网络不可用”重要。用户需要知道是离线、策略禁止、低电量降级,还是设备不支持。

指标只记录运行状态,不记录会议内容

端侧 AI 也需要观测,否则线上问题会变成客服截图。难点是不能把用户会议内容传回去。会议助手只上报匿名运行指标和失败原因。

第一版指标字段如下:

字段用途
eventmodel_loadedsummary_completedfallback_used
app_version排查客户端版本问题
model_id模型版本和量化版本
backendanennapidirectmlcpu
device_tier高端、中端、低端的匿名档位
network_state在线、离线、弱网
battery_state插电、电池、低电量
thermal_state正常、温热、过热降级
input_token_bucket只记录分桶,不记录文本
latency_ms端到端耗时
prefill_ms输入处理耗时
decode_ms生成耗时
peak_memory_mb峰值内存
fallback_reasonCPU 回退、模型缺失、策略禁止等
redaction_count_bucket脱敏数量分桶,不含具体值

一条上报像这样:

{
  "event": "summary_completed",
  "app_version": "1.4.0",
  "model_id": "meeting-summarizer-1.8b-q4",
  "backend": "nnapi",
  "device_tier": "mid",
  "network_state": "offline",
  "battery_state": "battery",
  "thermal_state": "nominal",
  "input_token_bucket": "8k-16k",
  "latency_ms": 23840,
  "prefill_ms": 6920,
  "decode_ms": 11860,
  "peak_memory_mb": 1420,
  "fallback_reason": "none",
  "cloud_fallback_used": false
}

这里故意不上传会议标题、发言人、摘要文本、具体脱敏值。端侧功能的观测要克制,宁可少一点,也不要把信任优势毁掉。

排障记录:为什么一台手机突然慢了三倍

试点时遇到过一个典型问题:同一个 10 分钟会议样本,高端笔记本 11 秒出草稿,中端手机有时 24 秒,有时接近 70 秒。模型没变,输入也没变。

排查不是从模型质量开始,而是从指标分桶开始。慢请求有三个共同点:backend=cputhermal_state=warmfallback_reason=accelerator_compile_failed。也就是说,NNAPI 编译失败后回退到了 CPU,手机又处于温热状态,频率下降,延迟自然翻倍。

继续查本地日志,发现失败集中在一个算子组合上。模型里有一段动态 shape,对某个驱动版本不稳定。解决方式不是让用户重试,而是给这类设备下发 meeting-lite 配置,限制片段长度,并禁用触发问题的图优化。

配置像这样:

device_overrides:
  - match:
      platform: android
      accelerator: nnapi
      driver_family: "example-driver-31"
    model_profile: meeting-lite
    max_segment_tokens: 900
    graph_optimizations:
      dynamic_shape_fusion: false
    fallback:
      allow_cpu: true
      max_latency_ms: 45000

这个问题说明端侧排障有自己的节奏。服务端慢了,可以看 Pod、看 GPU、看队列。端侧慢了,要看设备温度、驱动、后端、内存压力、是否插电、是否刚下载完模型。没有匿名指标,团队只能在少数测试机上猜。

发布:模型灰度要像客户端灰度

会议助手的发布不是把模型文件放到 CDN 就结束。第一版发布分成四层:客户端代码、模型清单、模型文件、远程策略。任何一层都可能需要回滚。

一个可执行的发布步骤如下。

先发布客户端 1.4.0,只带隐私过滤小模型,摘要模型按需下载。功能入口默认对 5% beta 用户开放。

rollout:
  app_min_version: "1.4.0"
  audience: beta
  percent: 5
  default_mode: local_summary
  cloud_enhance_default: off

再下发模型清单,但不自动下载大模型。用户进入会议助手时,如果设备符合条件且在 Wi-Fi 下,才提示下载。

download_policy:
  meeting-summarizer-1.8b-q4:
    wifi_only: true
    require_battery_level_gte: 0.3
    allow_metered_network: false
    keep_previous_version: true

接着扩大到 20%,观察模型加载成功率、下载失败率、摘要完成率、CPU fallback 比例、低电量降级比例。这里的核心不是 DAU,而是端侧运行健康。

最后才开放云端增强。云端增强必须单独灰度,因为它牵涉授权文案、脱敏 payload、服务端成本和企业策略。

cloud_enhance:
  percent: 10
  require_user_consent: true
  upload_payload: redacted_only
  max_payload_tokens: 6000
  blocked_when:
    - enterprise_policy_local_only
    - redaction_confidence_low

这个节奏慢一些,但端侧功能需要这种耐心。坏模型一旦下载到大量设备,回收成本比服务端高得多。

回滚:用户设备上的坏版本不会自动消失

端侧回滚分四种情况。

如果只是策略问题,比如云端增强授权文案不够清楚,关闭远程开关即可:

features:
  cloud_enhance:
    enabled: false
    reason: "consent_copy_revision"

如果是模型质量问题,把模型清单指回上一版,并保留已下载的新模型但不再选择它。不要立刻删除,避免用户在弱网下反复下载。

models:
  meeting_summary_active: meeting-summarizer-1.8b-q4-20260610
  meeting_summary_blocked:
    - id: meeting-summarizer-1.8b-q4-20260615
      reason: "action_item_regression"

如果是某类设备的运行时问题,只对该设备族降级到 lite 模型或 CPU 路径。不要全量回滚,否则会牺牲其他设备上已经稳定的体验。

device_overrides:
  - match:
      device_tier: mid
      backend: nnapi
      os_version_lte: "15"
    force_model: meeting-summarizer-lite-900m-q4

如果是客户端解析崩溃,才需要紧急发版,并通过远程策略关闭相关模型能力。端侧系统要默认假设“有一部分用户暂时不会升级”,所以旧客户端兼容性要保留一段时间。

回滚演练也要做。验收不是写一段文档,而是在测试设备上真的把模型从新版本切回旧版本,确认草稿还能打开,缓存不会损坏,用户不会丢会议记录。

验收:端侧 AI 要看体验,也要看不打扰

会议助手第一版的验收线写得很具体。

指标验收标准
高端笔记本 10 分钟会议草稿P95 小于 15 秒
中端手机 10 分钟会议草稿P95 小于 30 秒
本地隐私预处理邮箱、手机号样式召回率大于 99%
云端 payload不包含原始音频和完整原文
模型冷启动热启动小于 2 秒,冷启动小于 8 秒
CPU fallback 比例支持设备上小于 5%
低电量降级电量低于 20% 时不启动大模型
崩溃率beta 用户不高于基线 0.1 个百分点
回滚耗时远程策略 5 分钟内生效
用户可见控制能查看本地保存、上传内容和清除缓存

质量验收也不能只看自动分数。我们准备了一组会议样本:短站会、产品评审、客户访谈、技术排障、带大量数字的项目同步。每个样本都有人类参考摘要,检查三类问题:关键决策有没有漏,待办有没有编造,敏感字段有没有外泄。

端侧还有一个特殊验收:不打扰。模型下载不能抢用户流量,低电量时不能硬跑,手机发热时不能继续生成长摘要,用户关闭云端增强后不能偷偷上传。很多 AI 功能失败在太积极,而端侧功能尤其要学会克制。

这套方案真正改变了什么

做完这个离线会议助手,我对端侧小模型的判断更朴素了。

它不是云端大模型的替代品。它更适合做第一层:离数据近、响应快、隐私敏感、可以离线、成本稳定。它负责把原始材料变成更干净、更短、更有结构的中间结果。云端负责更复杂的推理和更完整的表达,但前提是用户知道并同意哪些数据会离开设备。

开发上也要换心态。服务端 AI 项目常常围绕吞吐、队列和 GPU 成本转;端侧 AI 项目则绕不开电量、温度、包体、下载、驱动、缓存、权限和回滚。NPU 不是自动加速按钮,小模型也不是随便放进 App 就能用。

最可靠的路径是从一个窄场景开始。比如会议助手只先做好本地摘要、隐私预处理和云端增强确认。把请求样例、配置、指标、排障、发布、回滚和验收都跑通以后,再谈更多能力。端侧 AI 最后拼的不是一句“本地运行”,而是它在用户设备上长期、稳定、可解释地运行。

当一个功能能在断网时给出可用草稿,在联网时清楚说明上传内容,在低电量时主动降级,在出问题时能回滚到旧模型,我才会觉得它真的进入了产品阶段。否则它只是一次演示。

参考资料