
1. 端侧 LLM 部署到底在解决什么问题很多人第一次听到“端侧 LLM 部署”脑子里浮现的画面是把 ChatGPT 塞进手机里。这个理解不算错但太窄了。端侧 LLM 部署真正要解决的核心矛盾是推理能力与数据主权之间的冲突——你既想让设备具备语言理解和生成能力又不愿意把每一条输入都送到远端服务器上去。我在过去一年里陆续在几类硬件上折腾过端侧模型部署从 RK3588 这类边缘计算板卡到手机端的 NCNN 推理再到笔记本上的 Ollama 本地跑模型。踩过的坑比想象中多得多。表面上看部署流程无非是“下载模型 → 量化 → 加载 → 推理”四步但每一步都有大量隐藏的决策点模型选多大、量化到什么精度、推理框架用哪个、内存怎么管、并发怎么扛。这些问题在云端部署时往往被充裕的算力掩盖了一旦落到端侧全部暴露出来。这篇文章是“深入理解端侧 Agent”系列的第二篇聚焦在 LLM 本身的端侧部署。我不会泛泛地讲“端侧 AI 是趋势”这种废话而是把我在实际项目中积累的选型逻辑、量化策略、内存管理技巧和性能调优经验完整地拆开来讲。如果你正在做端侧 Agent 的开发或者打算把某个 LLM 能力集成到本地设备上这篇内容应该能帮你少走至少两三个星期的弯路。先明确一个前提端侧 LLM 部署和云端部署的本质区别不在于“模型大小”而在于资源约束的刚性程度。云端你可以弹性扩容端侧不行。端侧的 RAM、闪存、算力、功耗、散热都是硬上限超出一点就是 OOM 崩溃或者降频卡死。所以端侧部署的第一原则不是“跑起来”而是“在资源预算内稳定跑起来”。2. 模型选型不是越小越好而是越匹配越好2.1 参数量与端侧硬件的匹配逻辑选模型这件事很多人上来就问“哪个模型最小”。这是个危险的思路。模型太小能力不够Agent 的决策质量会断崖式下降模型太大跑不动或者跑得极慢用户体验直接崩盘。正确的做法是先确定你的硬件资源预算再反推可选的模型范围。我一般用这样一个粗略的估算公式来快速筛选模型推理所需内存 ≈ 参数量 × 每参数字节数 × 1.2运行时开销系数其中每参数字节数取决于量化精度量化精度每参数字节数7B 模型所需内存估算适用硬件档次FP162.0~16.8 GB高端 GPU / 16GB 内存设备INT81.0~8.4 GB中高端设备 / 8GB 内存INT40.5~4.2 GB主流手机 / 4GB 可用内存INT4 分组量化0.5~0.6~4.5 GB同上精度略好这个公式是经验值实际会有偏差但用来做第一轮筛选足够了。比如你手上是一块 8GB 内存的 RK3588 板卡系统本身占掉 1.5~2GB留给模型的大概 6GB 左右。那 INT4 量化的 7B 模型是勉强能跑的INT8 的 3B 模型则更稳妥。这里有个容易被忽略的点内存带宽往往比算力更早成为瓶颈。端侧设备的 NPU 算力标称值看起来很漂亮但 LLM 推理是 memory-bound 的任务token 生成速度很大程度上取决于内存带宽。RK3588 的 NPU 算力有 6 TOPS但实际跑 7B INT4 模型时token 生成速度可能只有 5~8 tokens/s原因就是内存带宽限制。所以选型时不能只看算力参数内存带宽同样关键。2.2 主流端侧模型的横向对比目前端侧能跑的 LLM 大致分几个梯队我按实际使用体验来排第一梯队Qwen2.5 系列0.5B / 1.5B / 3B / 7B这是我目前最推荐的端侧模型家族。原因有三一是尺寸覆盖全从 0.5B 到 7B 都有方便按硬件选二是中文能力在同等尺寸下明显优于 Llama 系三是社区量化版本丰富GGUF、AWQ、GPTQ 都有现成的。1.5B 和 3B 这两个尺寸在端侧特别实用1.5B 可以在手机上流畅跑3B 在边缘板卡上表现很好。第二梯队Llama 3.2 系列1B / 3BMeta 出的端侧专用版本英文能力很强但中文能力相比 Qwen 有明显差距。如果你的 Agent 主要处理英文任务Llama 3.2 是很好的选择如果涉及中文建议优先考虑 Qwen。第三梯队Phi-3 系列3.8B微软的 Phi-3 在推理能力上表现不错尤其是逻辑推理和代码生成。但它的 tokenizer 对中文不够友好中文场景下 token 效率偏低同样的文本会消耗更多 token。特殊场景Gemma 22B / 9BGoogle 的 Gemma 2 在 2B 尺寸上表现均衡9B 版本能力很强但对端侧来说偏大。适合有 8GB 可用内存的设备。选型时还有一个实用技巧优先选有官方或社区 GGUF 量化版本的模型。自己从头量化不仅费时还容易因为校准数据集选择不当导致精度损失过大。GGUF 格式配合 llama.cpp 生态是目前端侧部署最成熟的方案。2.3 量化策略精度与体积的平衡术量化是端侧部署绕不开的环节。简单说量化就是把模型权重从高精度浮点数FP16/FP32压缩成低精度整数INT8/INT4从而减少内存占用和计算量。但量化不是免费的午餐精度损失是必然的关键是怎么把损失控制在可接受范围内。我实测下来不同量化方案的效果差异很大GGUF 的 Q4_K_M 是目前端侧的甜点llama.cpp 的 GGUF 格式提供了多种量化级别其中 Q4_K_M4-bit 带 K-quant 混合精度是我最推荐的。它在 4-bit 的基础上对关键层如 attention 的 Q/K/V 投影保留更高精度对不太敏感的层用更低精度。实测 7B 模型用 Q4_K_M 量化后体积从 14GB 降到约 4.4GB而困惑度perplexity只上升了不到 5%。这个 trade-off 非常划算。Q5_K_M 适合对精度要求高的场景如果硬件内存允许Q5_K_M 是更好的选择。体积约 5.3GB精度损失更小。在 Agent 场景下如果模型需要做复杂的工具调用决策建议用 Q5 而不是 Q4因为量化误差在长链条推理中会被放大。Q3 和 Q2 要慎用低于 4-bit 的量化精度损失会明显加剧。Q3_K_S 在简单对话上还能用但一旦涉及结构化输出如 JSON 格式的工具调用参数出错率会显著上升。Q2 基本只能做非常简单的任务不建议在 Agent 场景使用。AWQ 和 GPTQ 的适用场景这两种量化方案主要针对 GPU 推理优化在端侧 NPU 上支持不如 GGUF 广泛。如果你的端侧设备有 GPU如 Jetson 系列AWQ 是不错的选择它在 4-bit 下的精度通常略优于 GGUF 的 Q4_K_M。但如果是纯 NPU 或 CPU 推理还是优先选 GGUF。一个实操经验量化后的模型一定要做一轮实际任务的验证不能只看困惑度指标。我遇到过 Q4 量化后困惑度只涨了 3%但在工具调用任务上错误率翻倍的情况。困惑度是平均指标掩盖了特定能力维度的退化。3. 推理框架的选型与实战配置3.1 llama.cpp、Ollama、MLC-LLM 的定位差异端侧 LLM 推理框架目前是三足鼎立的局面但它们的定位其实差异很大选错了会走很多弯路。llama.cpp最底层的控制力llama.cpp 是 C 实现的推理引擎支持 GGUF 格式可以在 CPU、CUDA、Metal、Vulkan 等多种后端上运行。它的优势是控制粒度最细你可以精确控制线程数、批大小、上下文长度、内存映射等参数。缺点是上手门槛高需要自己编译、自己管理模型加载。如果你的端侧设备是定制硬件或者你需要对推理过程做深度优化llama.cpp 是首选。我在 RK3588 上就是用 llama.cpp 配合其 CPU 后端跑的通过调整线程亲和性和 NEON 指令优化把 3B 模型的生成速度从 4 tokens/s 提到了 9 tokens/s。Ollama快速验证的最佳工具Ollama 本质上是 llama.cpp 的封装加上模型管理、API 服务和一些便利功能。它的优势是开箱即用一条命令就能拉模型跑起来。适合快速验证想法和做原型开发。但 Ollama 在端侧生产环境有几个问题一是它默认会常驻内存对资源紧张的设备不友好二是它的 API 抽象层带来额外开销三是定制化能力有限很多底层参数调不了。我的建议是用 Ollama 做验证用 llama.cpp 做生产。MLC-LLM编译优化的路线MLC-LLM 走的是另一条路——用 TVM 编译器把模型编译成针对特定硬件的优化代码。理论上能获得更好的性能尤其是在移动端 GPU 上。但它的模型格式和生态相对封闭支持的模型数量不如 GGUF 丰富调试也更困难。适合有编译优化经验的团队。框架上手难度性能上限定制能力生态丰富度适用场景llama.cpp中高高强丰富生产部署、定制硬件Ollama低中弱丰富原型验证、快速测试MLC-LLM高高中一般移动端 GPU 优化3.2 在资源受限设备上跑通第一个模型我以 llama.cpp 在 ARM 架构 Linux 设备上的部署为例把完整流程走一遍。这套流程在 RK3588、树莓派 5、以及各种 ARM 边缘盒子上都通用。第一步编译 llama.cpp不要直接用 apt 安装的版本那个版本通常没有针对你的硬件做优化。从源码编译git clone https://github.com/ggerganov/llama.cpp cd llama.cpp cmake -B build -DCMAKE_BUILD_TYPERelease -DLLAMA_NATIVEON cmake --build build --config Release -j$(nproc)LLAMA_NATIVEON会让编译器针对当前 CPU 架构生成优化指令在 ARM 上会启用 NEON在 x86 上会启用 AVX2/AVX512。这个选项对性能影响很大实测能带来 20%~40% 的提升。第二步准备量化模型从 Hugging Face 下载 GGUF 格式的模型文件。以 Qwen2.5-3B-Instruct 的 Q4_K_M 版本为例文件大约 2GB。放到设备的存储上建议放在 SSD 或高速 eMMC 上不要放在低速 SD 卡上否则加载速度会让你怀疑人生。第三步启动推理服务./build/bin/llama-server \ -m /path/to/qwen2.5-3b-instruct-q4_k_m.gguf \ -c 4096 \ -t 4 \ --host 0.0.0.0 \ --port 8080 \ -ngl 0 \ --mlock参数逐个解释-c 4096上下文长度设为 4096。端侧不要设太大KV Cache 会吃掉大量内存。每 1024 token 的上下文大约消耗 100~200MB 内存取决于模型层数和头数。-t 4使用 4 个线程。ARM 设备通常 4 个大核设多了反而因为调度开销降低性能。建议设为大核数量。-ngl 0GPU 层数为 0即纯 CPU 推理。如果有 GPU可以调大这个值把部分层卸载到 GPU。--mlock锁定内存防止模型被交换到 swap。端侧设备 swap 性能极差锁内存能避免推理时的卡顿。第四步验证推理curl http://localhost:8080/v1/chat/completions \ -H Content-Type: application/json \ -d { messages: [{role: user, content: 你好请用一句话介绍你自己}], max_tokens: 100, temperature: 0.7 }如果返回正常的 JSON 响应说明部署成功。接下来就是性能调优的事了。3.3 内存映射与 KV Cache 的精细控制端侧部署最容易翻车的地方就是内存管理。我见过太多“模型能加载但一推理就 OOM”的案例。核心问题出在两个地方模型权重加载方式和 KV Cache 管理。mmap 是把双刃剑llama.cpp 默认使用 mmap内存映射加载模型好处是加载快、多进程共享内存。但在端侧设备上mmap 有个坑当系统内存紧张时被 mmap 的模型页可能被换出下次访问时触发缺页中断导致推理突然卡顿几百毫秒。我的做法是如果设备内存足够模型大小 × 1.5 可用内存用--no-mmap强制把模型全部读入内存配合--mlock锁定。如果内存紧张保留 mmap 但关闭 swap避免换出。KV Cache 的量化KV Cache 是推理过程中缓存的历史 key/value 向量它的内存占用随上下文长度线性增长。对于 7B 模型、4096 上下文FP16 的 KV Cache 大约占 1~2GB。端侧设备上这个开销很可观。llama.cpp 支持 KV Cache 量化通过--cache-type-k和--cache-type-v参数控制--cache-type-k q8_0 --cache-type-v q8_0把 KV Cache 量化到 8-bit内存占用减半精度损失很小。实测在对话任务上几乎无感知。如果内存极度紧张可以试 q4_0但长上下文下精度损失会明显一些。注意KV Cache 量化对某些模型的影响比其他模型大。Qwen 系列对 KV Cache 量化比较鲁棒Llama 系列在 q4_0 下可能出现重复生成的问题。上线前务必做一轮长对话测试。4. 性能调优从能跑到跑得好4.1 Token 生成速度的瓶颈定位端侧 LLM 的性能指标主要有两个prefill 速度处理输入 prompt 的速度和decode 速度逐 token 生成的速度。这两个的瓶颈来源不同优化手段也不同。Prefill 阶段是计算密集型的瓶颈在算力。优化手段主要是启用硬件加速NPU/GPU、增大批大小、使用更高效的 attention 实现如 Flash Attention。Decode 阶段是内存带宽密集型的瓶颈在内存带宽。优化手段主要是量化权重、量化 KV Cache、减少内存拷贝。我一般用 llama.cpp 自带的 benchmark 工具来定位瓶颈./build/bin/llama-bench \ -m /path/to/model.gguf \ -p 512 \ -n 128 \ -t 4-p 512测试 512 token 的 prefill 速度-n 128测试生成 128 token 的 decode 速度。输出会分别给出 ppprompt processing和 tgtoken generation的吞吐。在 RK3588 上跑 Qwen2.5-3B Q4_K_M 的典型数据是pp 约 40 tokens/stg 约 8 tokens/s。如果 tg 低于 5 tokens/s用户体验就会明显感到卡顿如果 pp 低于 20 tokens/s长 prompt 的响应延迟会很难受。4.2 线程数与 CPU 亲和性的调优ARM 设备通常是大核 小核的 big.LITTLE 架构。默认情况下Linux 调度器会把线程分配到所有核心上包括小核。但 LLM 推理是计算密集型的跑在小核上会严重拖慢速度。解决办法是用taskset把推理进程绑定到大核上taskset -c 4-7 ./build/bin/llama-server -m model.gguf -t 4 ...这里假设核心 4-7 是大核。具体哪些是大核可以通过lscpu或查看/sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq来确定频率高的就是大核。线程数设置也有讲究。我的经验是线程数设为大核数量不要超过。设多了会因为线程调度和缓存竞争导致性能下降。在 4 大核 4 小核的设备上-t 4通常是最优的。我实测过-t 6和-t 8速度反而比-t 4慢 10%~15%。还有一个细节关闭 CPU 频率调节的节能模式。端侧设备默认可能是 powersave 或 schedutil 调频策略推理时频率上不去。改成 performance 模式echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor这个操作会增大功耗和发热但在有主动散热的设备上问题不大。如果是被动散热的设备要权衡一下因为过热降频反而更慢。4.3 批处理与并发请求的处理策略端侧 Agent 场景下并发请求是绕不开的。比如一个语音助手可能同时有语音识别、意图理解、回复生成三个 LLM 调用。如果串行处理延迟会累加。llama.cpp 的 server 支持 continuous batching可以同时处理多个请求。通过-np参数设置并行槽位数./build/bin/llama-server -m model.gguf -np 4 -c 8192 ...-np 4表示支持 4 个并发请求-c 8192表示总上下文 8192会平均分配给 4 个槽位每个 2048。但端侧设备上并发不是越多越好。每个并发请求都会占用 KV Cache 内存而且计算资源是共享的并发数增加会导致单个请求的延迟上升。我的建议是端侧并发数控制在 2~4 之间超过这个数总吞吐可能不再增加但延迟会明显恶化。如果 Agent 的多个 LLM 调用之间有依赖关系比如必须先意图理解再生成回复那并发帮助不大重点应该放在减少单次调用的延迟上。这时候可以考虑用小模型做前置任务如意图分类用 0.5B 模型大模型只做最终生成。5. 端侧 Agent 场景下的特殊考量5.1 工具调用对模型精度的额外要求端侧 Agent 和单纯的端侧对话模型有个本质区别Agent 需要做工具调用function calling也就是模型要输出结构化的 JSON 来指定调用哪个工具、传什么参数。这对模型的指令遵循能力和结构化输出能力要求更高。量化对工具调用能力的影响比对话能力更大。我做过一组对比测试用同一个 Qwen2.5-3B 模型在不同量化精度下测试工具调用的成功率量化精度简单工具调用成功率复杂嵌套参数成功率模型体积Q8_098%92%3.2 GBQ5_K_M96%88%2.3 GBQ4_K_M93%79%2.0 GBQ3_K_M85%62%1.6 GB可以看到Q4_K_M 在复杂嵌套参数上的成功率已经掉到 79%意味着每 5 次调用就有 1 次失败。在 Agent 场景下这是不可接受的因为一次工具调用失败可能导致整个任务链条中断。所以我的建议是Agent 场景下模型量化精度至少用 Q5_K_M如果内存允许Q8_0 更稳妥。如果硬件实在跑不动 Q5那就换更小的模型如 1.5B配 Q8_0而不是大模型配 Q3。小模型高精度的组合在工具调用任务上往往优于大模型低精度。5.2 上下文管理与记忆压缩端侧 Agent 通常需要维护对话历史和任务状态这些都要塞进上下文窗口。但端侧设备的上下文窗口有限通常 2048~8192很容易被填满。我常用的策略是分层记忆管理最近 N 轮对话完整保留保证短期上下文连贯。较早的对话用规则或小模型做摘要压缩把多轮对话压缩成一段简短摘要。长期记忆存到本地向量数据库需要时通过检索召回。摘要压缩这一步可以用端侧的小模型如 Qwen2.5-0.5B来做不占用主模型的推理资源。实测把 10 轮对话压缩成 100 字左右的摘要信息保留率在 80% 以上足够维持对话连贯性。还有一个技巧是系统提示词的精简。Agent 的系统提示词通常很长包含工具定义、行为规范等可能占掉 1000 token。可以把工具定义从系统提示词中抽出来只在需要时动态注入减少常驻上下文的占用。5.3 模型热更新与版本管理端侧设备部署后模型更新是个麻烦事。设备可能分布在不同地方网络带宽有限不可能每次都推全量模型。我的做法是差分更新 双分区设备上保留两个模型分区A/B当前运行 A新版本写入 B写入完成后切换。模型文件做差分压缩只传输变化的部分。GGUF 格式的模型量化后不同版本之间的差异通常只有几百 MB。更新在后台低优先级进行不影响当前推理服务。如果设备存储紧张可以考虑只保留一个分区但更新时要先停止服务、替换文件、重启服务。这会导致短暂的服务中断适合对可用性要求不高的场景。一个容易忽略的点模型更新后KV Cache 的格式可能不兼容必须清空重建。如果 Agent 有持久化的对话状态要确保状态和模型版本的绑定关系避免用旧状态配新模型导致输出异常。6. 几个真实踩坑案例的完整复盘6.1 内存泄漏导致的间歇性崩溃项目背景在一个 ARM 边缘盒子上部署 Qwen2.5-3B 做本地客服 Agent设备内存 4GB模型 Q4_K_M 约 2GB。现象服务启动后正常运行但跑几个小时后就 OOM 崩溃。重启后又能跑几小时。排查过程先用free -h监控内存发现内存使用量随时间缓慢上升每小时涨 100~200MB。用valgrind跑了一遍没发现明显泄漏。后来用pmap查看进程内存映射发现 mmap 的模型文件区域在增长。根因llama.cpp 的 mmap 加载方式下每次推理都会触发新的页映射而旧页在某些情况下没有被正确释放。这是 mmap 多线程推理的一个已知问题。解决方案改用--no-mmap加载模型配合--mlock锁定内存。内存占用变成固定的 2GB KV Cache不再增长。代价是启动时间从 2 秒变成 8 秒但稳定性大幅提升。这个坑的教训是端侧长时间运行的服务内存稳定性比启动速度重要得多。宁可启动慢一点也要保证内存不涨。6.2 量化模型的 tokenizer 不匹配问题项目背景把一个在云端验证好的 Agent 逻辑迁移到端侧模型从 FP16 换成 Q4_K_M 量化版。现象模型能正常对话但工具调用的 JSON 输出经常格式错误比如少括号、多逗号、字段名拼错。排查过程一开始怀疑是量化精度问题换了 Q5、Q8 都没解决。后来对比云端和端侧的原始输出发现端侧模型输出的 token 序列在特殊字符如{、}、上经常出错。根因下载的 GGUF 量化模型使用的 tokenizer 配置和原始模型不一致。具体来说量化模型在转换时用了旧版的 tokenizer 词表导致特殊字符的 token 映射错位。解决方案从官方渠道重新下载 GGUF 模型确保 tokenizer 配置和原始模型一致。验证方法是拿一段包含大量特殊字符的文本对比原始模型和量化模型的 tokenize 结果应该完全一致。这个坑的教训是量化模型一定要从可信来源下载并且验证 tokenizer 一致性。tokenizer 问题很隐蔽因为它不影响普通对话只在结构化输出时暴露。6.3 并发场景下的 KV Cache 竞争项目背景Agent 需要同时处理语音输入和文本输入两路请求配置了-np 2支持并发。现象单路请求时速度正常8 tokens/s两路并发时速度骤降到每路 2~3 tokens/s而且偶尔出现输出乱码。排查过程用llama-server的日志查看请求调度情况发现两个请求的 KV Cache 槽位分配有重叠。进一步查看代码发现-np 2配合-c 4096时每个槽位分配 2048 上下文但实际运行时某个请求的上下文超过了 2048侵占了另一个槽位的空间。根因上下文长度估算不准确。Agent 的系统提示词 工具定义 对话历史实际消耗的 token 数超过了预估。当单个请求的上下文超过分配值时llama.cpp 的处理是截断但截断逻辑在并发场景下有 bug导致槽位越界。解决方案把-c增大到 8192-np保持 2每个槽位 4096 上下文留足余量。同时在应用层做上下文长度检查超过 3500 token 就触发摘要压缩确保不会触及上限。这个坑的教训是并发配置要留足上下文余量不能按理论值卡着配。端侧场景下prompt 长度往往比预期长因为系统提示词和工具定义占了很多。7. 端侧 LLM 部署的边界与取舍折腾了这么多项目我越来越觉得端侧 LLM 部署的核心不是技术问题而是取舍问题。你不可能在端侧获得和云端一样的体验关键是想清楚哪些能力必须本地化哪些可以妥协。如果 Agent 处理的是隐私敏感数据如个人对话、本地文档那端侧部署是刚需这时候就要接受模型能力下降、响应速度变慢的代价。如果只是想把简单任务本地化以降低云端成本那可以选择更小的模型把复杂任务回退到云端。我目前的做法是混合架构端侧跑一个 1.5B~3B 的模型处理高频简单任务如意图分类、简单问答、工具调用参数生成复杂任务如长文分析、多步推理转发到云端。端侧模型负责快速响应和隐私保护云端模型负责深度处理。这样既保证了体验又控制了成本。还有一个实际经验端侧模型的 prompt 工程和云端不一样。端侧模型能力弱需要更明确的指令、更少的示例、更结构化的输出格式要求。在云端用 few-shot 能解决的问题端侧可能需要改成 zero-shot 严格的格式约束。这个适配过程需要反复调试不能直接把云端的 prompt 搬过来用。最后说一个我踩过的认知坑一开始我总想着“等硬件再强一点端侧就能跑大模型了”。但实际做下来发现硬件提升的同时模型也在变大端侧的资源约束永远存在。与其等硬件不如现在就把小模型的能力榨干。一个精心调优的 3B 模型在特定任务上完全可以超过一个没调优的 7B 模型。端侧部署的竞争力更多来自工程优化和场景适配而不是模型规模本身。