端侧 AI 与小模型测试
2026 年是端侧 AI 真正落地的一年。Apple Intelligence Foundation Models 随 iOS 26 进入数亿台设备,Gemini Nano v3 在 Android 16 中开放给第三方应用,PC 上 MLX-LM 与 llama.cpp 已经能在 16GB 内存里跑 Llama 4 8B,浏览器里 Transformers.js 4.x 直接通过 WebGPU 调度 Phi-4 Mini。这一篇讲清楚端侧测试为什么完全不同于云侧测试——你要测的不再是模型本身,而是"模型 × 芯片 × 系统 × 内存 × 温度 × 电量"这六维空间。
教学导读
**定位:**这一章是教程的最后一篇,专门讲云侧测试方法论无法覆盖的端侧问题。它把"模型质量评测"和"嵌入式系统验证"两套方法论合在一起,是一个跨学科话题。 **前置依赖:**建议已读 第 12 篇 LLM 性能与稳定性测试 中的 TPS / TTFT 概念,第 43 篇 LLM 评测科学 的 Judge 方法,以及 第 55 篇 AI 应用合规与备案测试 的本地化推理合规要求。 **适用场景:**所有计划把 LLM 能力下沉到客户端的产品——iOS App、Android App、桌面客户端、PWA、IoT、车机、AR 眼镜。如果你的应用还停留在"全部走云端 API"的阶段,这一篇可以当作前瞻预读。 **学完产出:**你能制定一份覆盖 3 端 4 框架的端侧测试计划,能在不同价位机型上做基准跑分、热降频测量、内存峰值采集、隐私验证,并能给出"端云协同"的兜底测试方案。
第1章:2025-2026 端侧 AI 大爆发
1.1 一组数字感受这场拐点
云端推理一直是 LLM 的主战场——OpenAI、Anthropic、Google、字节、阿里都在堆千亿参数大模型。但 2025 下半年开始,端侧 AI 经历了一次能力跃迁。下面这组数据来自 2026 年 Q1 公开报告,可以直观感受这场拐点:
| 设备 / 系统 | 2024 年端侧能力 | 2026 年端侧能力 | 跨度 |
|---|---|---|---|
| iPhone 17 Pro · A19 Pro · 16GB | iOS 18 内置 ~3B 模型,仅可做摘要 | iOS 26.2 Foundation Models 3B INT4,TPS 65~80,可做工具调用、RAG、Agent | 能力 3 倍 |
| Pixel 11 Pro · Tensor G6 · 16GB | Gemini Nano v1,仅 Pixel 自家应用可用 | Gemini Nano v3,Android 16 系统级 API,第三方 App 可调用 | 开放范围 100 倍 |
| MacBook Air M5 · 24GB | llama.cpp 跑 7B Q4 约 18 TPS | MLX-LM 跑 Llama 4 8B Q4 约 42 TPS,Phi-4 14B Q4 约 22 TPS | 2.3 倍 |
| 红米 K90 · 骁龙 8 Gen 5 · 12GB | 无法跑 LLM | Qwen3-1.7B Q4 跑 22 TPS,可做语音助手与离线摘要 | 从 0 到 1 |
| 桌面 Chrome · WebGPU | 仅可跑 BERT 级别小模型 | Transformers.js 4.x + WebGPU 跑 Phi-4 Mini 3.8B Q4,约 15~25 TPS | 规模 100 倍 |
1.2 三个驱动因素
这场跃迁不是单一技术突破造成的,而是三条曲线在 2025 同时达到拐点:
- NPU 算力暴涨。Apple A19 Pro 的 Neural Engine 达到 38 TOPS(INT8),骁龙 8 Gen 5 的 Hexagon NPU 达到 48 TOPS,Tensor G6 的 TPU 达到 32 TOPS。比 2023 年同档次旗舰提升 3~4 倍。
- 小模型质量跨越"可用线"。3B~8B 量级模型在 2025 下半年集体跨越"对话能用、工具调用稳定、推理可接受"的门槛。Phi-4 14B、Qwen3-7B、Llama 4 8B、DeepSeek-Coder-V3-Mini 7B 都是代表。
- 量化技术成熟。GGUF 的 Q4_K_M 量化在 7B 模型上质量损失稳定低于 3%,Apple 自家 INT4 + 块量化方案接近无损。Llama.cpp 与 MLX-LM 把量化部署门槛降到几乎为零。
1.3 端侧 AI 不是"云端 AI 的轻量版"
这是 2026 年最容易被忽略的认知——端侧 AI 解决的不是"省钱"问题,而是云端无法解决的四类问题。
问题 A · 隐私
用户的健康数据、邮件内容、聊天记录、相册照片,越来越多场景禁止上云。Apple 2024 年把"端侧推理"作为 Apple Intelligence 的核心卖点之一,背后是欧盟和加州的强监管。
问题 B · 延迟
语音助手要在 200ms 内首响,输入法补全要在 100ms 内出结果,AR 眼镜要在 50ms 内识别物体——这些场景云端往返就已经超时。
问题 C · 离线可用
飞机、高铁、地下车库、海外出差、地震断网——任何"可用性 SLA"都要求关键功能离线可达。
问题 D · 个性化
个人偏好、写作风格、工作上下文,这些数据在端侧训练 LoRA 比上云训练更安全也更敏捷。 这意味着端侧 AI 不是"为了省 token 钱"而存在,而是承担了云端无法承担的责任。测试的目标也跟着变——不是"端侧能不能凑合用",而是"端侧能不能做到云端无法做到的事"。
1.4 测试团队最常掉的三个坑
过去一年,我们看到不少团队在端侧测试上交了学费,最常见的三类失败模式:
- 用云侧的指标体系套端侧。云侧关心"准确率、Latency、QPS"——端侧最关键的是"内存峰值、热降频曲线、电池消耗、首次加载",前者完全不能反映后者。
- 在开发机上测,不在用户机型上测。MacBook Pro M3 Max 64GB 上跑得飞起的模型,在 iPhone 15 标准版 8GB 上可能直接 OOM 闪退。
- 只测冷启动一次跑分,不测长时间使用。设备温度上来后 NPU 降频,TPS 可能从 60 直接掉到 20。这是端侧测试和云侧测试最大的差异之一。
本章核心观点。
端侧 AI 的爆发是"硬件 NPU 算力 × 小模型质量 × 量化成熟度"三条曲线交汇的结果。它解决的不是省钱问题,而是云端无法解决的隐私、延迟、离线、个性化四类问题。这意味着端侧测试不是云侧测试的削减版,而是一套全新的、跨"模型质量评测 + 嵌入式系统验证"的方法论。
第2章:端侧 AI 测试的特殊性(设备 × 系统 × 性能 × 隐私)
2.1 端侧测试的"六维空间"
云侧测试的变量基本是"模型 × 提示词"两个维度。端侧测试的变量空间至少有六个维度,且互相耦合:
| 维度 | 典型取值 | 对结果的影响 |
|---|---|---|
| 模型 | Apple FM 3B / Gemini Nano v3 / Phi-4 14B / Qwen3-4B | 质量与体积差异 |
| 设备 / 芯片 | iPhone 17 Pro A19 Pro / Pixel 11 Pro Tensor G6 / 红米 K90 骁龙 8 Gen 5 | NPU 算力差 3~5 倍,TPS 直接相关 |
| 操作系统版本 | iOS 26.0 / 26.2 / Android 16 / 17 Beta | 系统调度策略影响 NPU 抢占 |
| 内存压力 | 空闲 / 后台 5 个 App / 后台 20 个 App | 可能触发模型被杀重载 |
| 温度 | 22°C 室温 / 35°C 暖环境 / 持续推理 30 分钟 | 降频后 TPS 衰减 40~60% |
| 电量与省电模式 | 满电正常 / 20% 自动省电 / 低电量模式手动开 | NPU 降频甚至禁用 |
这意味着同一个模型、同一段提示词,在不同维度组合下的表现可能差 5 倍。"端侧 AI 的性能数据,必须带上完整的环境标签才有意义"——这是端侧测试报告的第一原则。
2.2 端侧测试需要的全新指标
云侧测试的标准指标是 Latency、TTFT(Time To First Token)、TPS、QPS、错误率。这些在端侧仍然有用,但还远远不够。下面这些指标是端侧独有的:
| 端侧指标 | 定义 | 采集方法 | 典型阈值 |
|---|---|---|---|
| 模型加载时间(Load Time) | 从调用 API 到首次可推理的耗时 | API 端到端打点 | ≤ 1.5s(首次)/ ≤ 200ms(缓存命中) |
| 峰值常驻内存(Peak RSS) | 推理过程中进程驻留内存最大值 | iOS Instruments / Android Profiler | ≤ 物理内存 25% |
| NPU 利用率(NPU Util) | NPU 在推理时段的繁忙占比 | powermetrics / perfetto | ≥ 60% |
| 每千 token 能耗(mWh/1k) | 生成 1000 token 消耗的电池毫瓦时 | powermetrics / dumpsys batterystats | ≤ 12 mWh/1k(手机) |
| 降频后 TPS 衰减率 | 持续 30 分钟推理后 TPS 相对初始值的比例 | 定时基准 + 温控曲线 | ≥ 40%(不应低于初始的 40%) |
| OOM 触发率 | 在低内存机型上的 OOM 闪退比例 | Crashlytics + 内存压力测试 | ≤ 0.05% |
| 冷启动失败率 | 设备重启后首次调用失败比例 | 设备重启自动化脚本 | ≤ 0.1% |
2.3 端侧测试的"三个不一样"
把云侧测试经验直接迁移到端侧,最容易踩坑的是这三个本质差异:
- 结果是不可重复的。同一台设备、同一段输入,跑两次得到的 TPS 可能差 30%——因为后台进程、温度、电量都不同。要做的是"分布性测量"——跑 100 次取 P50/P95/P99 而不是单次跑分。
- 测试环境会被自己污染。连续跑 100 次推理之后设备升温,第 101 次的数据已经不是"基础性能"。每次基准之间需要 5~10 分钟降温,这极大延长测试时间。
- 用户机型很难还原。云侧只有几台 GPU 服务器,端侧用户机型有几百种。你不可能买齐所有机型,只能选"代表性机型矩阵"——这一节后面会详细讲。
常见误区:用 Mac mini M4 模拟 iPhone。
很多团队用 Mac 跑模型基准,认为"M 系列芯片和 A 系列同源"。这在 CPU/GPU 上勉强成立,但在功耗 / 温控 / 内存带宽上完全不同。Mac 散热远好于手机,没有热降频;Mac 内存带宽接近手机的 4 倍。Mac 跑分只能反映"模型本身能不能跑",不能反映"用户体验"。所有面向手机的端侧 AI 必须在真机上跑基准。
2.4 端侧测试和合规的强绑定
这一篇之所以放在第 56 篇而不是更早,是因为端侧测试和上一篇《AI 应用合规与备案测试》形成了一对孪生话题。端侧推理在合规视角下有特殊地位——它是"数据不出域"的最直接证据。但反过来,"声称端侧推理"也必须能被测试证明:
- 用户输入的敏感数据是否真的没有上云?
- 模型权重的下载和更新通道是否安全?
- 模型是否被分级灰度?低端机降级为云端时是否做了用户告知?
- 本地缓存的模型输出是否符合数据保护策略?
这些问题既是测试题也是合规题,本篇会在第 9 章和第 15 章给出可执行验证方法。
第3章:主流端侧模型全景
2026 年端侧可用的代表性模型分成三类:系统内置模型(用户无感)、开放权重模型(开发者自部署)、垂直领域模型。
3.1 系统内置模型
| 模型 | 厂商 / 系统 | 规模 | 调用入口 | 特点 |
|---|---|---|---|---|
| Apple Intelligence Foundation Models | Apple · iOS 26 / macOS 26 | ~3B 参数(INT4 块量化) | Foundation Models Framework(Swift) | 系统级隐私沙箱,零额外内存占用,工具调用稳定 |
| Apple Server Foundation Models | Apple · Private Cloud Compute | 未公开(推测 ~70B) | 同一 SDK 自动路由 | 端云一体 API,开发者无感切换 |
| Gemini Nano v3 | Google · Android 16+ | ~3.25B 参数 | AICore / AIFeature API(Kotlin) | 仅 Pixel/三星/小米旗舰可用,其他机型走云端兜底 |
| Gemini Nano XS | Google · Chrome 130+ | ~1.8B | window.ai.languageModel | 浏览器内置实验 API,需 origin trial |
3.2 开放权重小模型(GGUF / safetensors / mlx)
| 模型 | 规模 | 2026 推荐量化 | 典型用途 | 下载体积(Q4_K_M) |
|---|---|---|---|---|
| Phi-4 14B | 14B | Q4_K_M | 桌面端通用对话,需要 16GB+ 内存 | ~8.4 GB |
| Phi-4 7B | 7B | Q4_K_M | 主流桌面 / 高端手机 | ~4.4 GB |
| Phi-4 Mini 3.8B | 3.8B | Q4_K_M / Q5_K_M | WebGPU、低端桌面、车机 | ~2.3 GB |
| Qwen3-1.7B | 1.7B | Q4_K_M | 低端手机、IoT | ~1.0 GB |
| Qwen3-4B | 4B | Q4_K_M | 主流手机、桌面 | ~2.5 GB |
| Qwen3-7B | 7B | Q4_K_M | 桌面、高端手机 | ~4.3 GB |
| DeepSeek-Coder-V3-Mini | 7B | Q4_K_M / Q5_K_M | 桌面端代码补全 / IDE 插件 | ~4.2 GB |
| Llama 4 8B Instruct | 8B | Q4_K_M | 桌面、Mac、高端手机 | ~4.8 GB |
| GLM-4.5-Edge 4B | 4B | Q4_K_M | 中文为主的端侧应用 | ~2.5 GB |
3.3 垂直领域端侧模型
- Whisper.cpp small / medium:本地语音识别,约 250MB / 800MB。
- SAM 2 端侧版:图像分割,约 80~250MB,可用于相册编辑。
- StableLM Code 3B:本地代码补全,IDE 插件用。
- BGE-M3 端侧版:本地嵌入模型,用于端侧 RAG,约 280MB。
- Embedding-Gemma 0.3B:Google 2026 推出的端侧嵌入模型,120MB。
3.4 选型决策图
用户机型矩阵 → 最低端机内存 ≥ 8GB? → 是 → 1.7B~3B → 否 → 走云端兜底
主流机型内存 → 8~12GB → Phi-4 Mini / Qwen3-4B → 12~16GB → Qwen3-7B / Llama 4 8B → 16GB+ → Phi-4 14B
测试人员的选型建议。
不要让"模型选型"成为算法团队单方面的决定。测试团队必须提供"用户机型分布数据"作为输入——比如你的产品有 70% 用户机型内存 ≤ 12GB,那就不能选 7B 以上模型作为主路径。这是一个"工程现实"问题,不是"模型质量"问题。
第4章:端侧推理框架对比
2026 年端侧推理框架是一个"百花齐放"的局面,没有一家通吃所有平台。下面给一张"框架 × 平台 × 模型格式"的对照表。
4.1 框架矩阵
| 框架 | 2026 主流版本 | 支持平台 | 主用模型格式 | 核心优势 |
|---|---|---|---|---|
| Apple Foundation Models Framework | iOS 26.x SDK 内置 | iOS / macOS / iPadOS / visionOS | Apple 私有格式 | 系统级 NPU 直通,零部署成本 |
| CoreML | CoreML 9 | iOS / macOS | .mlpackage | 苹果生态,模型需自行转换 |
| MLX-LM | 0.20.x | macOS(M 系列) | .safetensors / mlx | Apple Silicon 上 LLM 跑分最快,社区活跃 |
| llama.cpp | b5xxx(5000 系列构建号) | 全平台 CPU/GPU | GGUF | 跨平台、社区最大、量化最全 |
| MLC-LLM | 0.20.x | iOS / Android / Desktop / WebGPU | MLC 私有格式(基于 TVM) | 统一一份模型多端部署 |
| ONNX Runtime | 1.21.x | 全平台 + ARM NPU | .onnx | 企业部署成熟,硬件覆盖广 |
| TFLite | 2.20.x(LiteRT 改名) | Android / iOS / 嵌入式 | .tflite | Android 上和 NNAPI 集成最深 |
| Transformers.js | 4.x | 浏览器(WebGPU/WASM) | ONNX | 浏览器零配置 LLM 推理 |
| WebLLM | 0.4.x | 浏览器(WebGPU) | MLC | 浏览器跑 7B 级模型 |
| Gemini Nano API | Android 16 内置 | Android(部分机型) | 系统私有 | 系统级调用,无需自部署 |
4.2 框架选型:按目标平台
iOS / macOS
**首选:**Apple Foundation Models(直接调系统模型)
**次选:**MLX-LM(自部署开放权重)
**不推荐:**llama.cpp 在 iPhone 上无法直通 NPU,只能用 CPU/GPU。
Android
**首选:**Gemini Nano API(Pixel / 三星 / 小米旗舰)
**次选:**MLC-LLM(跨机型,能用 OpenCL/Vulkan)
**兜底:**llama.cpp + GGUF(CPU 路径,老机型最后选择)
桌面(macOS / Windows / Linux)
**Mac 首选:**MLX-LM(M 系列上吊打其他方案)
**Win/Linux 首选:**llama.cpp + GGUF(CUDA / ROCm / Vulkan)
**企业首选:**ONNX Runtime(生态成熟)
浏览器
**首选:**Transformers.js 4.x(接口友好)
**大模型:**WebLLM(能跑 7B)
**限制:**WebGPU 仅 Chrome / Edge / 较新 Safari 稳定,Firefox 部分支持。
4.3 框架性能对比(同模型 同设备)
下表是 Qwen3-4B Q4_K_M 在 MacBook Air M5 24GB 上的同模型不同框架对比,2026-03 实测:
| 框架 | TTFT | TPS | 峰值内存 | 能耗 mWh/1k |
|---|---|---|---|---|
| MLX-LM 0.20 | 180 ms | 62 | 3.1 GB | 3.8 |
| llama.cpp b5180(Metal) | 240 ms | 48 | 2.9 GB | 4.5 |
| MLC-LLM 0.20 | 320 ms | 41 | 3.4 GB | 5.1 |
| ONNX Runtime 1.21(CoreML EP) | 410 ms | 28 | 3.6 GB | 6.8 |
结论:在 Apple Silicon 上 MLX-LM 是当前性能王者;llama.cpp 是跨平台兜底首选;MLC-LLM 适合"一次构建多端部署";ONNX Runtime 适合企业内部多硬件平台统一。
4.4 框架的"看不见"成本
- 包体积影响:把 llama.cpp 静态链接进 iOS App,IPA 体积增加 ~6MB;把 ONNX Runtime 完整库加入 ~12MB;MLC-LLM Runtime ~9MB。
- 启动初始化:MLX-LM 加载 4B Q4 模型大约 0.8 秒;llama.cpp Metal 后端约 1.2 秒;ONNX Runtime CoreML EP 约 2.1 秒(首次)。
- 更新与签名:动态下载模型权重在 iOS 上需要满足 App Store 审核条款;Android 上则需要满足 Play Store 政策。这部分合规风险测试人员要提前评估。
第5章:端侧 vs 云侧——能力差距的量化
"端侧能不能替代云侧"是产品方最关心的问题。这一章不给情绪化答案,给数据化方法。
5.1 三类能力的差距分布
2026 年我们做了一个跨 200 道任务的端云对比评测,把任务分成三类:
| 任务类别 | 云侧 GPT-5 平均分 | 端侧 Apple FM 3B 平均分 | 端侧 Qwen3-4B 平均分 | 端侧 Phi-4 14B 平均分 |
|---|---|---|---|---|
| 简单摘要 / 抽取 | 9.2 / 10 | 8.5 | 8.3 | 9.0 |
| 多步推理 / 数学 | 8.8 | 5.2 | 5.8 | 7.4 |
| 专业代码生成 | 8.6 | 4.5 | 5.2 | 7.1 |
| 长上下文(>32K) | 9.0 | 4.8 | 5.0 | 6.5 |
| 工具调用 / Agent | 9.1 | 7.2 | 6.8 | 8.1 |
| 中文写作 | 8.8 | 7.0 | 8.0 | 7.5 |
| 跨语言翻译 | 9.0 | 7.5 | 7.8 | 8.2 |
从这张表可以读出几个关键事实:
- 简单摘要 / 抽取上端侧已经追平。这意味着输入法补全、邮件摘要、短文本润色这些场景完全可以下沉。
- 多步推理和长上下文是端侧硬伤。任何"需要思考"的任务都建议走云端。
- 14B 量级是质变拐点。Phi-4 14B 在多数任务上接近 8 分,已经能挑大梁。但它需要桌面端,不能在手机跑。
5.2 端云能力差距的量化方法
怎么把"差不多"变成"差几分"?这一节给出可复用的评测协议。
- 构造任务集:从你产品真实日志中采样 100~300 条提示词,按任务类型打标签。
- 双跑:同提示词分别在端侧和云侧跑一次,记录输出。
- Judge 评分:用 GPT-5 或 Claude Opus 4.7 作为 Judge 对每条做 1-10 分质量打分。
- 分组聚合:按任务类型分组,给出端侧相对云侧的"质量保留率"(端侧分 / 云侧分)。
- 设阈值:质量保留率 ≥ 90% 视为可下沉;70~90% 需做用户实验;< 70% 必须走云端。
5.3 评测代码示例
import asyncio, json
from openai import AsyncOpenAI
from anthropic import AsyncAnthropic
cloud = AsyncOpenAI()
judge = AsyncAnthropic()
async def cloud_run(prompt):
r = await cloud.chat.completions.create(
model="gpt-5",
messages=[{"role": "user", "content": prompt}],
temperature=0,
)
return r.choices[0].message.content
async def edge_run(prompt):
"""通过本地 HTTP 服务调用端侧模型,例如 mlx_lm.server / llama.cpp server"""
import httpx
async with httpx.AsyncClient(timeout=60) as cli:
r = await cli.post("http://127.0.0.1:8080/v1/chat/completions", json={
"model": "Qwen3-4B-Instruct-Q4_K_M",
"messages": [{"role": "user", "content": prompt}],
"temperature": 0,
})
return r.json()["choices"][0]["message"]["content"]
JUDGE_PROMPT = """你是评测员。请对下面这段回答打 1-10 分(只输出数字):
任务: {task}
用户问题: {prompt}
回答: {answer}
打分维度:信息完整性、准确性、有用性、表达质量。"""
async def judge_score(task, prompt, answer):
r = await judge.messages.create(
model="claude-opus-4-7",
max_tokens=4,
messages=[{"role": "user", "content": JUDGE_PROMPT.format(
task=task, prompt=prompt, answer=answer)}],
)
txt = r.content[0].text.strip()
try:
return float(txt.split()[0])
except Exception:
return None
async def evaluate_one(item):
cloud_ans = await cloud_run(item["prompt"])
edge_ans = await edge_run(item["prompt"])
cloud_score = await judge_score(item["task"], item["prompt"], cloud_ans)
edge_score = await judge_score(item["task"], item["prompt"], edge_ans)
return {
"task": item["task"],
"cloud_score": cloud_score,
"edge_score": edge_score,
"retention": (edge_score / cloud_score) if cloud_score else None,
}
async def main(dataset_path):
with open(dataset_path) as f:
items = [json.loads(l) for l in f]
results = await asyncio.gather(*(evaluate_one(it) for it in items))
by_task = {}
for r in results:
by_task.setdefault(r["task"], []).append(r["retention"])
for task, vals in by_task.items():
vals = [v for v in vals if v is not None]
avg = sum(vals) / len(vals)
verdict = "可下沉" if avg >= 0.9 else ("需实验" if avg >= 0.7 else "必须云端")
print(f"{task:20s} 保留率 {avg:.2%} → {verdict}")
asyncio.run(main("tasks.jsonl"))不要追求"端云完全平替"。
端云协同的常态是"端侧做 70% 简单任务,云侧兜底 30% 复杂任务"——而不是"端侧替代云侧"。把"质量保留率"作为路由决策变量,比把它作为产品对外口径更现实。下沉是"省 token + 提隐私 + 降延迟"的工程优化,不是"我们多牛"的市场宣言。
第6章:端侧 AI 测试的 5 维度矩阵(功能 / 性能 / 内存 / 能效 / 隐私)
6.1 维度全景
端侧 AI 测试可以浓缩为 5 个维度,每个维度都对应不同的方法论与工具:
维度 1 · 功能正确性
**问什么:**同提示词,端侧输出和云侧输出语义是否等价?端侧的工具调用是否能正确触发?JSON 输出是否能解析? **方法:**双跑 + Judge 评分;JSON Schema 强校验;工具调用 trace 比对。
维度 2 · 性能(TTFT / TPS)
**问什么:**首 token 时间、稳定生成速度、长生成衰减?冷启动 vs 热启动有多大差距? **方法:**多次跑取 P50/P95,区分冷热启动;按 prompt 长度做分桶。
维度 3 · 内存(峰值 / 驻留)
**问什么:**模型加载常驻多少?推理过程峰值内存?多模型并发会不会被系统杀进程? **方法:**iOS Instruments / Android Profiler + 自动化触发 OOM 边界。
维度 4 · 能效(mWh/1k token)
**问什么:**每千 token 耗电多少?长时间使用对续航影响?是否触发设备温度告警? **方法:**powermetrics / dumpsys batterystats;连续推理 30 分钟的电量曲线。
维度 5 · 隐私
**问什么:**声称端侧推理是否真的没有上云?模型权重下载是否安全?日志是否泄露用户输入? **方法:**抓包 + 沙箱权限审计;mitmproxy / Charles + macOS Endpoint Security。
6.2 5 维度测试矩阵的执行顺序
顺序是有讲究的——错了顺序会浪费大量人力。建议的顺序如下:
维度 5 隐私(先做) → 维度 1 功能正确性 → 维度 2 性能 → 维度 3 内存 → 维度 4 能效(最后做)
原因:
- 隐私先做——不通过则后续测试都无意义。
- 功能正确性其次——质量不达标也不需要再测性能。
- 性能放中间——暴露最容易被发现的瓶颈。
- 内存放第四——内存测试需要构造极端场景,比性能测试耗时。
- 能效放最后——能效测试需要满电、满电池循环、稳定温度,是最贵的资源。
6.3 矩阵化测试用例样例
下面是真实可执行的端侧测试用例样板,可以直接套用到测试管理工具:
| 用例 ID | 维度 | 设备 | 模型 | 触发条件 | 预期 |
|---|---|---|---|---|---|
| EDGE-FN-001 | 功能 | iPhone 17 Pro / iOS 26.2 | Apple FM 3B | "摘要这段邮件" + 200 字邮件 | 语义保留 ≥ 0.9(vs 云侧) |
| EDGE-PF-001 | 性能 | iPhone 17 Pro / iOS 26.2 | Apple FM 3B | 1k 输入 → 200 token 输出 | TTFT ≤ 250ms · TPS ≥ 55 |
| EDGE-MEM-001 | 内存 | iPhone 16 / 8GB | Apple FM 3B | 后台开 10 个 App 后调用 | 峰值常驻 ≤ 350MB · 无 OOM |
| EDGE-PWR-001 | 能效 | iPhone 17 Pro | Apple FM 3B | 连续生成 10 分钟 | 电量下降 ≤ 4% · 不触发温控 |
| EDGE-PRV-001 | 隐私 | iPhone 17 Pro · 抓包代理 | Apple FM 3B | "摘要这段健康数据" | 无外发数据包 · 仅本地推理 |
| EDGE-FN-002 | 功能 | Pixel 11 Pro / Android 16 | Gemini Nano v3 | JSON 输出工具调用 10 次 | 解析成功率 100% · Schema 0 错误 |
| EDGE-PF-002 | 性能 | 红米 K90 · 骁龙 8 Gen 5 | Qwen3-1.7B Q4_K_M | 500 输入 → 100 输出 | TTFT ≤ 600ms · TPS ≥ 18 |
| EDGE-MEM-002 | 内存 | 红米 K90 · 12GB | Qwen3-4B Q4_K_M | 清理后台后冷启动 | 加载 ≤ 3.5s · 峰值 ≤ 2.8GB |
| EDGE-PWR-002 | 能效 | MacBook Air M5 | Llama 4 8B Q4_K_M | 连续生成 30 分钟 | 每千 token ≤ 6 mWh · 风扇不启动 |
| EDGE-PRV-002 | 隐私 | Chrome 130 · WebGPU | Phi-4 Mini Q4 | 抓包验证 | 仅模型权重下载,无用户输入外发 |
6.4 测试矩阵执行的"机器人化"
端侧测试最大的成本不是想用例,是"重复在 N 个机型上跑同一组用例"。这一类工作必须自动化:
- iOS:用 Xcode UI Tests + xcrun simctl 控制真机做基准跑分;用 iperf3 模拟 4 种网络。
- Android:用 ADB + Appium 2 + scrcpy;用 perfetto 采集 NPU/温度。
- 桌面:用 Bash/PowerShell + powermetrics(macOS)/ Intel Power Gadget(Win)。
- Web:用 Playwright + Chrome DevTools Protocol,调用 performance.measureUserAgentSpecificMemory()。
第7章:设备热降频测试方法
7.1 为什么"热降频"是端侧测试的第一痛点
云侧 GPU 在数据中心里风冷水冷齐上,温度恒定。手机 / PC 没有主动散热(或散热极弱),NPU 持续高负载几分钟后必然升温——升温后系统会自动"降频"以保护硬件。这导致:
- 第 1 分钟跑 60 TPS
- 第 10 分钟掉到 40 TPS
- 第 30 分钟掉到 22 TPS
- 持续 1 小时后用户感知"AI 卡得越来越离谱"
这条曲线是端侧体验的"隐形杀手",必须在测试阶段量化。
7.2 热降频曲线测量协议
- 恒温环境:把设备放在 22°C ± 1°C 的房间,提前静置 30 分钟达到环境温度。
- 固定输入:选一段标准 prompt(建议 1000 input tokens / 200 output tokens)。
- 循环推理:脚本每 30 秒触发一次推理,连续 30 分钟,共 60 次。
- 采集指标:每次记录 TTFT、TPS、设备温度、CPU/NPU 频率。
- 画曲线:以时间为 X 轴,TPS 和温度为 Y 轴,看拐点。
7.3 iOS 上的热降频采集脚本
iOS 上没有公开 API 直接读 SoC 温度,但可以用 ProcessInfo.thermalState 间接判断(nominal / fair / serious / critical)。下面是 Swift 测试脚本:
import Foundation
import FoundationModels
@available(iOS 26.0, *)
func runThermalBenchmark() async {
let session = LanguageModelSession()
let prompt = String(repeating: "请总结以下要点 ", count: 50)
let outputFile = "thermal_log.csv"
let header = "iter,timestamp,thermal_state,ttft_ms,tps\n"
try? header.write(toFile: outputFile, atomically: true, encoding: .utf8)
for iter in 0..<60 {
let start = Date()
var firstTokenAt: Date?
var tokenCount = 0
let stream = session.streamResponse(to: prompt, options: .init(maximumResponseTokens: 200))
for try await chunk in stream {
if firstTokenAt == nil { firstTokenAt = Date() }
tokenCount += chunk.tokens.count
}
let ttft = firstTokenAt?.timeIntervalSince(start) ?? 0
let total = Date().timeIntervalSince(firstTokenAt ?? start)
let tps = Double(tokenCount) / max(total, 0.001)
let state = thermalStateString(ProcessInfo.processInfo.thermalState)
let row = "\(iter),\(Date().timeIntervalSince1970),\(state),\(Int(ttft*1000)),\(String(format: "%.2f", tps))\n"
if let handle = FileHandle(forWritingAtPath: outputFile) {
handle.seekToEndOfFile()
handle.write(row.data(using: .utf8)!)
handle.closeFile()
}
try? await Task.sleep(nanoseconds: 30_000_000_000)
}
}
func thermalStateString(_ s: ProcessInfo.ThermalState) -> String {
switch s {
case .nominal: return "nominal"
case .fair: return "fair"
case .serious: return "serious"
case .critical: return "critical"
@unknown default: return "unknown"
}
}7.4 Android 上的热降频采集
Android 提供 PowerManager.getCurrentThermalStatus() 返回 0~6 的等级。下面是 Kotlin 示例:
import android.content.Context
import android.os.PowerManager
import com.google.ai.edge.aicore.GenerativeModel
import com.google.ai.edge.aicore.generationConfig
import kotlinx.coroutines.delay
import java.io.FileWriter
suspend fun runThermalBenchmark(context: Context) {
val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
val model = GenerativeModel(
generationConfig = generationConfig {
temperature = 0f
maxOutputTokens = 200
}
)
val prompt = "请总结以下要点:".repeat(50)
val writer = FileWriter(context.getExternalFilesDir(null), "thermal_log.csv")
writer.appendLine("iter,timestamp,thermal_status,ttft_ms,tps")
for (iter in 0 until 60) {
val startNs = System.nanoTime()
var firstTokenNs = 0L
var tokenCount = 0
model.generateContentStream(prompt).collect { chunk ->
if (firstTokenNs == 0L) firstTokenNs = System.nanoTime()
tokenCount += chunk.text?.length ?: 0
}
val ttft = (firstTokenNs - startNs) / 1_000_000L
val total = (System.nanoTime() - firstTokenNs) / 1_000_000_000.0
val tps = tokenCount / total
val status = pm.currentThermalStatus
writer.appendLine("$iter,${System.currentTimeMillis()},$status,$ttft,${"%.2f".format(tps)}")
writer.flush()
delay(30_000)
}
writer.close()
}7.5 数据解读:典型曲线长什么样
下面是 2026-03 在 iPhone 17 Pro 上跑 Apple FM 3B 的实测曲线(节选):
| 时间 | TPS | thermalState | 说明 |
|---|---|---|---|
| 0 min | 72 | nominal | 冷启动峰值 |
| 5 min | 68 | nominal | 稳定阶段 |
| 12 min | 52 | fair | 开始降频 |
| 20 min | 38 | serious | 明显降频 |
| 30 min | 22 | serious | 降频后稳定 |
| 45 min | 15 | critical | 系统强限频,用户已感知 |
把这条曲线绘出来后,产品方就能做决策:是限制单次会话最长 10 分钟?还是在 thermalState=fair 时主动降级到云端?这是工程权衡,不是"端侧能不能跑"的问题。
测试报告必须含降频曲线。
很多团队的端侧性能报告只写一行"TPS = 65"——这相当于车评只写"百公里加速 5 秒"而不提"持续高速油耗多少"。建议端侧性能报告统一用"3 行汇总":冷启动 TPS、稳态 TPS(5 分钟后)、降频底部 TPS(30 分钟后),缺一不可。
第8章:端云协同的兜底测试
8.1 端云协同的三种模式
实际产品几乎不会"纯端侧"或"纯云端",而是一组协同策略:
模式 A · 静态分流
按任务类型路由:摘要走端侧,复杂推理走云端。决策表写死在客户端。 **测试要点:**路由表覆盖率、误路由概率、不走预期路径的兜底。
模式 B · 动态决策
客户端根据电量、温度、网络情况动态决定走端还是走云。 **测试要点:**4 种网络 × 3 种电量 × 4 种温度的全组合路由验证。
模式 C · 端侧先答 + 云侧 Re-rank
端侧先给草稿,云侧异步精修后替换显示。Apple Server FM、Gemini Live API 都用这个模式。 **测试要点:**替换时机、用户感知、端云一致性、网络断开时的兜底。
模式 D · 端侧失败 fallback 云侧
端侧 OOM / 超时 / 解析失败时自动切云。 **测试要点:**故障注入、切换时延、用户提示、计费统计。
8.2 端云一致性测试方法
当系统在不同条件下走不同路径,"用户得到的答案是否一致"就是关键测试题。下面是一个典型用例设计:
# 端云一致性测试:同一提示词在 4 种条件下的输出对比
test_cases = [
{"id": "TC-001", "network": "wifi", "battery": 80, "thermal": "nominal", "expect_route": "edge"},
{"id": "TC-002", "network": "5g", "battery": 80, "thermal": "nominal", "expect_route": "edge"},
{"id": "TC-003", "network": "4g", "battery": 18, "thermal": "fair", "expect_route": "cloud"},
{"id": "TC-004", "network": "off", "battery": 80, "thermal": "nominal", "expect_route": "edge"},
{"id": "TC-005", "network": "off", "battery": 80, "thermal": "critical","expect_route": "fail-with-toast"},
]
for tc in test_cases:
ctx = setup_device_state(tc["network"], tc["battery"], tc["thermal"])
actual_route = trigger_chat(ctx, prompt="请帮我总结这段邮件...")
assert actual_route == tc["expect_route"], f"{tc['id']} 路由错误: 期望{tc['expect_route']} 实际{actual_route}"
if actual_route in ("edge", "cloud"):
ans = ctx.last_response
assert is_semantically_equivalent(ans, ground_truth[tc["id"]]), f"{tc['id']} 输出语义偏差"8.3 故障注入:验证 fallback 真的会走
"端侧 OOM 自动切云"在文档里通常是一句话——但实际上 90% 的实现都没有正确处理。验证方法:
- iOS:用 Memory Warning Simulator(Xcode)触发模拟内存告警,看是否切云。
- Android:用 adb shell am send-trim-memory RUNNING_CRITICAL 触发系统级低内存。
- 桌面:用 stress-ng --vm 4 --vm-bytes 80% 占用大部分内存。
- 断网注入:用 mitmproxy 把云侧 API 域名 DROP 掉,看端侧是否撑住。
8.4 端云一致性的"输出漂移"问题
端侧和云侧用的不是同一个模型,输出风格会有差异。如果产品方期望"用户感知一致",需要做以下控制:
- 统一系统提示词:端侧 prompt 中加入云侧相同的人设、口吻、格式约束。
- 风格 LoRA:在端侧模型上叠加一个轻量 LoRA 让风格贴近云侧。
- JSON 强约束:用 schema 约束输出结构,弱化风格差异。
- 用户告知:极端情况下直接告知用户"已切换到本地模式,回答可能更简短"。
测试团队的边界。
端云协同的"决策逻辑"通常由算法/客户端架构团队设计。测试团队的边界是:把决策逻辑当作"黑盒规则",验证它在所有边界条件下是否符合预期。如果发现规则有漏洞——例如"5G + 低电量 + 长输入"组合下没有定义路由——这是测试报告的关键产出。
第9章:隐私本地化推理验证
9.1 为什么这是测试的"硬题"
"端侧推理 = 数据不出域"是产品方常用的对外承诺,但测试团队必须能拿出证据证明。这一节给出可执行的隐私验证方法。
9.2 抓包法:从最外层验证
最直接的方法是用 mitmproxy / Charles 给设备装根证书,做全局 HTTPS 抓包。验证步骤:
- 设备开启抓包模式,导入根证书。
- 触发包含敏感关键词的端侧推理(例如"我的身份证号是 110101...")。
- 观察抓包列表,过滤"输出后 30 秒内"的所有外发请求。
- 检查是否有任何请求体含有敏感关键词(哪怕是 hash 形式也算泄露)。
# mitmproxy 自动审计脚本
import mitmproxy.http
SENSITIVE_PATTERNS = ["身份证", "110101", "我的健康", "薪资", "blood_type"]
def request(flow: mitmproxy.http.HTTPFlow):
body = flow.request.get_text() or ""
for p in SENSITIVE_PATTERNS:
if p in body:
print(f"[ALERT] 敏感数据外发: host={flow.request.host} url={flow.request.path} pattern={p}")
with open("privacy_violation.log", "a") as f:
f.write(f"{flow.request.host}\t{flow.request.path}\t{p}\n")9.3 系统沙箱权限审计
iOS 上端侧推理框架不应该申请"网络权限"以外的危险权限。验证:
- iOS:用 Xcode → Capabilities 检查。Apple Foundation Models Framework 不应触发"Privacy - Network Usage"提示。
- macOS:用 Console.app 过滤 Endpoint Security 事件,看模型进程有没有意外的网络/文件访问。
- Android:检查 AndroidManifest.xml 的 INTERNET / ACCESS_NETWORK_STATE 权限是否真的需要。
9.4 模型权重下载渠道审计
"端侧推理"经常被误读为"完全离线"。实际上模型权重通常需要从 CDN 下载。这部分也需要审计:
- 来源验证:HTTPS + 证书钉扎,防止中间人篡改。
- 完整性校验:下载后做 SHA-256 校验,对比 manifest。
- 回滚机制:损坏的模型应能自动回滚到上一个版本。
- 审计日志:模型版本切换需要可追溯。
9.5 端侧日志的最小化
即使推理在端侧,如果客户端把"用户输入 + 模型输出"原样写入本地日志,并通过 Crashlytics 等工具上报到云端——这本质还是泄露。验证清单: 端侧日志合规检查清单
- 本地日志中是否屏蔽了用户提示词原文?(建议只记 hash 或 token 数)
- Crashlytics 上报是否过滤了 prompt / completion 字段?
- 分析事件中是否有"输入字符串"作为属性?
- 用户主动反馈"举报"功能上传内容时,是否有明确弹窗告知会上传?
- 本地数据库(Core Data / Room)的对话历史是否加密?
合规对照。
《个人信息保护法》第 17 条要求"告知 + 单独同意"原则。如果你的产品宣传"端侧推理保护隐私"但实际把 prompt 上传 Crashlytics,这就是虚假宣传 + 个保法违规。这种"小细节"在 2025 年已经有真实处罚案例(某教育 App 被某地网信办通报罚款)。
第10章:iOS 端 Apple Foundation Models 测试
10.1 Apple Foundation Models Framework 简介
2025 WWDC 发布的官方 SDK,2026 年随 iOS 26 大规模铺开。它给开发者一个统一接口,背后会自动选择"端侧 ~3B 模型"或"Private Cloud Compute 大模型"。 核心 API 在 FoundationModels 框架下,主要类:
LanguageModelSession:会话单例,管理上下文。SystemLanguageModel:访问可用模型元信息。Tool协议:声明工具调用。Generable:声明结构化输出 Schema(基于 macro)。
10.2 完整调用示例
import FoundationModels
@available(iOS 26.0, *)
class EdgeSummarizer {
let session = LanguageModelSession(
instructions: "你是一个简洁的邮件摘要助手,输出不超过 60 字。"
)
func summarize(email: String) async throws -> String {
let resp = try await session.respond(to: email, options: .init(
temperature: 0.2,
maximumResponseTokens: 80
))
return resp.content
}
}
@available(iOS 26.0, *)
@Generable
struct ScheduleItem {
@Guide(description: "事件标题,10 字以内") let title: String
@Guide(description: "ISO8601 开始时间") let startsAt: String
@Guide(description: "事件地点") let location: String?
}
@available(iOS 26.0, *)
class EdgeScheduleExtractor {
let session = LanguageModelSession(
instructions: "提取邮件中的会议事件,严格按 Schema 输出。"
)
func extract(email: String) async throws -> [ScheduleItem] {
try await session.respond(to: email, generating: [ScheduleItem].self).content
}
}10.3 测试代码:基准跑分
import XCTest
@testable import MyApp
@available(iOS 26.0, *)
final class FoundationModelsBenchmark: XCTestCase {
let summarizer = EdgeSummarizer()
func testColdStartLoadTime() async throws {
let start = Date()
_ = try await summarizer.summarize(email: "Hi, 这是一封测试邮件...")
let elapsed = Date().timeIntervalSince(start)
XCTAssertLessThan(elapsed, 1.5, "首次加载应在 1.5s 内")
}
func testWarmTPS() async throws {
_ = try await summarizer.summarize(email: "warmup")
var tpsList: [Double] = []
for _ in 0..<20 {
let prompt = sampleEmail()
let start = Date()
let resp = try await summarizer.summarize(email: prompt)
let elapsed = Date().timeIntervalSince(start)
let tokens = Double(resp.count / 2)
tpsList.append(tokens / elapsed)
}
let p50 = tpsList.sorted()[10]
let p95 = tpsList.sorted()[19]
print("P50 TPS: \(p50), P95 TPS: \(p95)")
XCTAssertGreaterThan(p50, 50)
}
func testJSONExtractAccuracy() async throws {
let extractor = EdgeScheduleExtractor()
let cases = loadGoldenCases()
var correct = 0
for c in cases {
let result = try await extractor.extract(email: c.email)
if compareSchedules(result, c.expected) { correct += 1 }
}
let acc = Double(correct) / Double(cases.count)
XCTAssertGreaterThan(acc, 0.92)
}
}10.4 内存峰值采集(Instruments)
iOS 上无法用代码精确读 RSS。推荐流程:
- Xcode → Product → Profile → Instruments → Allocations + Activity Monitor。
- 触发测试 case,关注"VM Tracker → Resident Memory"曲线。
- 峰值出现在第一次 LanguageModelSession 初始化时(系统加载模型到共享内存)。
- 记录"基线内存"和"调用后峰值",差值即为推理 overhead。
Apple 设计的 Foundation Models 模型权重是"系统级共享内存",所以你的 App 进程峰值通常增加不超过 50~80MB——这是它相对自部署模型的核心优势。
10.5 测试报告样板(iPhone 17 Pro · iOS 26.2)
| 指标 | P50 | P95 | 说明 |
|---|---|---|---|
| 冷启动加载 | 720 ms | 1.2 s | 含模型初始化 |
| 热路径 TTFT | 180 ms | 240 ms | 1k input |
| 稳态 TPS | 68 | 78 | 200 token output |
| 峰值进程内存增量 | 62 MB | 88 MB | 共享系统模型 |
| 30 分钟连续推理 TPS 衰减 | 降至 32(约 47%) | — | thermalState 进入 serious |
| JSON 解析成功率 | 99.4% | — | Generable 强类型保证 |
第11章:Android 端 Gemini Nano 测试
11.1 Gemini Nano API 概览
Android 16 把 Gemini Nano 通过 AICore 系统服务暴露给第三方。在 Pixel 11 Pro / Galaxy S26 / 小米 17 Ultra 等旗舰上原生支持,其他机型走云端 fallback。 开发者用的是 com.google.ai.edge.aicore 包。
11.2 完整 Kotlin 调用示例
import android.app.Application
import com.google.ai.edge.aicore.DownloadCallback
import com.google.ai.edge.aicore.GenerateContentResponse
import com.google.ai.edge.aicore.GenerationConfig
import com.google.ai.edge.aicore.GenerativeModel
import com.google.ai.edge.aicore.generationConfig
import kotlinx.coroutines.flow.Flow
class EdgeAssistant(application: Application) {
private val config: GenerationConfig = generationConfig {
temperature = 0.2f
topK = 40
topP = 0.95f
maxOutputTokens = 256
}
private val model = GenerativeModel(
generationConfig = config,
downloadConfig = com.google.ai.edge.aicore.DownloadConfig(
object : DownloadCallback {
override fun onDownloadStarted(bytesToDownload: Long) {
log("模型下载开始 size=$bytesToDownload")
}
override fun onDownloadProgress(totalBytesDownloaded: Long) {
log("下载进度 $totalBytesDownloaded")
}
override fun onDownloadCompleted() { log("模型就绪") }
override fun onDownloadFailed(failureStatus: String, e: Exception) {
log("下载失败 $failureStatus")
}
}
)
)
suspend fun summarize(email: String): String {
val prompt = "请用不超过 60 字总结这段邮件:\n\n$email"
val r: GenerateContentResponse = model.generateContent(prompt)
return r.text ?: ""
}
fun summarizeStream(email: String): Flow {
val prompt = "请用不超过 60 字总结这段邮件:\n\n$email"
return model.generateContentStream(prompt)
}
}11.3 基准跑分代码
import androidx.benchmark.junit4.BenchmarkRule
import androidx.benchmark.junit4.measureRepeated
import kotlinx.coroutines.runBlocking
import org.junit.Rule
import org.junit.Test
class GeminiNanoBench {
@get:Rule val benchmarkRule = BenchmarkRule()
@Test fun coldStartTTFT() = benchmarkRule.measureRepeated {
val app = androidx.test.core.app.ApplicationProvider.getApplicationContext()
runWithTimingDisabled { /* 重新初始化以确保冷启 */ }
runBlocking {
val assistant = EdgeAssistant(app)
assistant.summarize(SAMPLE_EMAIL)
}
}
@Test fun warmTPS() = benchmarkRule.measureRepeated {
runBlocking {
val out = assistant.summarize(SAMPLE_EMAIL)
check(out.isNotEmpty())
}
}
}11.4 内存采集(Android Profiler 自动化)
SystemLanguageModel.default.availability