Skip to content

端侧 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 · 16GBiOS 18 内置 ~3B 模型,仅可做摘要iOS 26.2 Foundation Models 3B INT4,TPS 65~80,可做工具调用、RAG、Agent能力 3 倍
Pixel 11 Pro · Tensor G6 · 16GBGemini Nano v1,仅 Pixel 自家应用可用Gemini Nano v3,Android 16 系统级 API,第三方 App 可调用开放范围 100 倍
MacBook Air M5 · 24GBllama.cpp 跑 7B Q4 约 18 TPSMLX-LM 跑 Llama 4 8B Q4 约 42 TPS,Phi-4 14B Q4 约 22 TPS2.3 倍
红米 K90 · 骁龙 8 Gen 5 · 12GB无法跑 LLMQwen3-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 同时达到拐点:

  1. NPU 算力暴涨。Apple A19 Pro 的 Neural Engine 达到 38 TOPS(INT8),骁龙 8 Gen 5 的 Hexagon NPU 达到 48 TOPS,Tensor G6 的 TPU 达到 32 TOPS。比 2023 年同档次旗舰提升 3~4 倍。
  2. 小模型质量跨越"可用线"。3B~8B 量级模型在 2025 下半年集体跨越"对话能用、工具调用稳定、推理可接受"的门槛。Phi-4 14B、Qwen3-7B、Llama 4 8B、DeepSeek-Coder-V3-Mini 7B 都是代表。
  3. 量化技术成熟。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 测试团队最常掉的三个坑

过去一年,我们看到不少团队在端侧测试上交了学费,最常见的三类失败模式:

  1. 用云侧的指标体系套端侧。云侧关心"准确率、Latency、QPS"——端侧最关键的是"内存峰值、热降频曲线、电池消耗、首次加载",前者完全不能反映后者。
  2. 在开发机上测,不在用户机型上测。MacBook Pro M3 Max 64GB 上跑得飞起的模型,在 iPhone 15 标准版 8GB 上可能直接 OOM 闪退。
  3. 只测冷启动一次跑分,不测长时间使用。设备温度上来后 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 5NPU 算力差 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 端侧测试的"三个不一样"

把云侧测试经验直接迁移到端侧,最容易踩坑的是这三个本质差异:

  1. 结果是不可重复的。同一台设备、同一段输入,跑两次得到的 TPS 可能差 30%——因为后台进程、温度、电量都不同。要做的是"分布性测量"——跑 100 次取 P50/P95/P99 而不是单次跑分。
  2. 测试环境会被自己污染。连续跑 100 次推理之后设备升温,第 101 次的数据已经不是"基础性能"。每次基准之间需要 5~10 分钟降温,这极大延长测试时间。
  3. 用户机型很难还原。云侧只有几台 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 ModelsApple · iOS 26 / macOS 26~3B 参数(INT4 块量化)Foundation Models Framework(Swift)系统级隐私沙箱,零额外内存占用,工具调用稳定
Apple Server Foundation ModelsApple · Private Cloud Compute未公开(推测 ~70B)同一 SDK 自动路由端云一体 API,开发者无感切换
Gemini Nano v3Google · Android 16+~3.25B 参数AICore / AIFeature API(Kotlin)仅 Pixel/三星/小米旗舰可用,其他机型走云端兜底
Gemini Nano XSGoogle · Chrome 130+~1.8Bwindow.ai.languageModel浏览器内置实验 API,需 origin trial

3.2 开放权重小模型(GGUF / safetensors / mlx)

模型规模2026 推荐量化典型用途下载体积(Q4_K_M)
Phi-4 14B14BQ4_K_M桌面端通用对话,需要 16GB+ 内存~8.4 GB
Phi-4 7B7BQ4_K_M主流桌面 / 高端手机~4.4 GB
Phi-4 Mini 3.8B3.8BQ4_K_M / Q5_K_MWebGPU、低端桌面、车机~2.3 GB
Qwen3-1.7B1.7BQ4_K_M低端手机、IoT~1.0 GB
Qwen3-4B4BQ4_K_M主流手机、桌面~2.5 GB
Qwen3-7B7BQ4_K_M桌面、高端手机~4.3 GB
DeepSeek-Coder-V3-Mini7BQ4_K_M / Q5_K_M桌面端代码补全 / IDE 插件~4.2 GB
Llama 4 8B Instruct8BQ4_K_M桌面、Mac、高端手机~4.8 GB
GLM-4.5-Edge 4B4BQ4_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 FrameworkiOS 26.x SDK 内置iOS / macOS / iPadOS / visionOSApple 私有格式系统级 NPU 直通,零部署成本
CoreMLCoreML 9iOS / macOS.mlpackage苹果生态,模型需自行转换
MLX-LM0.20.xmacOS(M 系列).safetensors / mlxApple Silicon 上 LLM 跑分最快,社区活跃
llama.cppb5xxx(5000 系列构建号)全平台 CPU/GPUGGUF跨平台、社区最大、量化最全
MLC-LLM0.20.xiOS / Android / Desktop / WebGPUMLC 私有格式(基于 TVM)统一一份模型多端部署
ONNX Runtime1.21.x全平台 + ARM NPU.onnx企业部署成熟,硬件覆盖广
TFLite2.20.x(LiteRT 改名)Android / iOS / 嵌入式.tfliteAndroid 上和 NNAPI 集成最深
Transformers.js4.x浏览器(WebGPU/WASM)ONNX浏览器零配置 LLM 推理
WebLLM0.4.x浏览器(WebGPU)MLC浏览器跑 7B 级模型
Gemini Nano APIAndroid 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 实测:

框架TTFTTPS峰值内存能耗 mWh/1k
MLX-LM 0.20180 ms623.1 GB3.8
llama.cpp b5180(Metal)240 ms482.9 GB4.5
MLC-LLM 0.20320 ms413.4 GB5.1
ONNX Runtime 1.21(CoreML EP)410 ms283.6 GB6.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 / 108.58.39.0
多步推理 / 数学8.85.25.87.4
专业代码生成8.64.55.27.1
长上下文(>32K)9.04.85.06.5
工具调用 / Agent9.17.26.88.1
中文写作8.87.08.07.5
跨语言翻译9.07.57.88.2

从这张表可以读出几个关键事实:

  • 简单摘要 / 抽取上端侧已经追平。这意味着输入法补全、邮件摘要、短文本润色这些场景完全可以下沉。
  • 多步推理和长上下文是端侧硬伤。任何"需要思考"的任务都建议走云端。
  • 14B 量级是质变拐点。Phi-4 14B 在多数任务上接近 8 分,已经能挑大梁。但它需要桌面端,不能在手机跑。

5.2 端云能力差距的量化方法

怎么把"差不多"变成"差几分"?这一节给出可复用的评测协议。

  1. 构造任务集:从你产品真实日志中采样 100~300 条提示词,按任务类型打标签。
  2. 双跑:同提示词分别在端侧和云侧跑一次,记录输出。
  3. Judge 评分:用 GPT-5 或 Claude Opus 4.7 作为 Judge 对每条做 1-10 分质量打分。
  4. 分组聚合:按任务类型分组,给出端侧相对云侧的"质量保留率"(端侧分 / 云侧分)。
  5. 设阈值:质量保留率 ≥ 90% 视为可下沉;70~90% 需做用户实验;< 70% 必须走云端。

5.3 评测代码示例

python
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.2Apple FM 3B"摘要这段邮件" + 200 字邮件语义保留 ≥ 0.9(vs 云侧)
EDGE-PF-001性能iPhone 17 Pro / iOS 26.2Apple FM 3B1k 输入 → 200 token 输出TTFT ≤ 250ms · TPS ≥ 55
EDGE-MEM-001内存iPhone 16 / 8GBApple FM 3B后台开 10 个 App 后调用峰值常驻 ≤ 350MB · 无 OOM
EDGE-PWR-001能效iPhone 17 ProApple FM 3B连续生成 10 分钟电量下降 ≤ 4% · 不触发温控
EDGE-PRV-001隐私iPhone 17 Pro · 抓包代理Apple FM 3B"摘要这段健康数据"无外发数据包 · 仅本地推理
EDGE-FN-002功能Pixel 11 Pro / Android 16Gemini Nano v3JSON 输出工具调用 10 次解析成功率 100% · Schema 0 错误
EDGE-PF-002性能红米 K90 · 骁龙 8 Gen 5Qwen3-1.7B Q4_K_M500 输入 → 100 输出TTFT ≤ 600ms · TPS ≥ 18
EDGE-MEM-002内存红米 K90 · 12GBQwen3-4B Q4_K_M清理后台后冷启动加载 ≤ 3.5s · 峰值 ≤ 2.8GB
EDGE-PWR-002能效MacBook Air M5Llama 4 8B Q4_K_M连续生成 30 分钟每千 token ≤ 6 mWh · 风扇不启动
EDGE-PRV-002隐私Chrome 130 · WebGPUPhi-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 热降频曲线测量协议

  1. 恒温环境:把设备放在 22°C ± 1°C 的房间,提前静置 30 分钟达到环境温度。
  2. 固定输入:选一段标准 prompt(建议 1000 input tokens / 200 output tokens)。
  3. 循环推理:脚本每 30 秒触发一次推理,连续 30 分钟,共 60 次。
  4. 采集指标:每次记录 TTFT、TPS、设备温度、CPU/NPU 频率。
  5. 画曲线:以时间为 X 轴,TPS 和温度为 Y 轴,看拐点。

7.3 iOS 上的热降频采集脚本

iOS 上没有公开 API 直接读 SoC 温度,但可以用 ProcessInfo.thermalState 间接判断(nominal / fair / serious / critical)。下面是 Swift 测试脚本:

python
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 示例:

python
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 的实测曲线(节选):

时间TPSthermalState说明
0 min72nominal冷启动峰值
5 min68nominal稳定阶段
12 min52fair开始降频
20 min38serious明显降频
30 min22serious降频后稳定
45 min15critical系统强限频,用户已感知

把这条曲线绘出来后,产品方就能做决策:是限制单次会话最长 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 端云一致性的"输出漂移"问题

端侧和云侧用的不是同一个模型,输出风格会有差异。如果产品方期望"用户感知一致",需要做以下控制:

  1. 统一系统提示词:端侧 prompt 中加入云侧相同的人设、口吻、格式约束。
  2. 风格 LoRA:在端侧模型上叠加一个轻量 LoRA 让风格贴近云侧。
  3. JSON 强约束:用 schema 约束输出结构,弱化风格差异。
  4. 用户告知:极端情况下直接告知用户"已切换到本地模式,回答可能更简短"。

测试团队的边界。

端云协同的"决策逻辑"通常由算法/客户端架构团队设计。测试团队的边界是:把决策逻辑当作"黑盒规则",验证它在所有边界条件下是否符合预期。如果发现规则有漏洞——例如"5G + 低电量 + 长输入"组合下没有定义路由——这是测试报告的关键产出。

第9章:隐私本地化推理验证

9.1 为什么这是测试的"硬题"

"端侧推理 = 数据不出域"是产品方常用的对外承诺,但测试团队必须能拿出证据证明。这一节给出可执行的隐私验证方法。

9.2 抓包法:从最外层验证

最直接的方法是用 mitmproxy / Charles 给设备装根证书,做全局 HTTPS 抓包。验证步骤:

  1. 设备开启抓包模式,导入根证书。
  2. 触发包含敏感关键词的端侧推理(例如"我的身份证号是 110101...")。
  3. 观察抓包列表,过滤"输出后 30 秒内"的所有外发请求。
  4. 检查是否有任何请求体含有敏感关键词(哪怕是 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 完整调用示例

python
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 测试代码:基准跑分

python
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。推荐流程:

  1. Xcode → Product → Profile → Instruments → Allocations + Activity Monitor。
  2. 触发测试 case,关注"VM Tracker → Resident Memory"曲线。
  3. 峰值出现在第一次 LanguageModelSession 初始化时(系统加载模型到共享内存)。
  4. 记录"基线内存"和"调用后峰值",差值即为推理 overhead。

Apple 设计的 Foundation Models 模型权重是"系统级共享内存",所以你的 App 进程峰值通常增加不超过 50~80MB——这是它相对自部署模型的核心优势。

10.5 测试报告样板(iPhone 17 Pro · iOS 26.2)

指标P50P95说明
冷启动加载720 ms1.2 s含模型初始化
热路径 TTFT180 ms240 ms1k input
稳态 TPS6878200 token output
峰值进程内存增量62 MB88 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 调用示例

python
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 基准跑分代码

python
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