1. 端侧Agent为什么非要本地跑一个大模型
先说个我自己的经历。之前做一个智能助手项目,Agent的推理完全走云端API,模型能力确实强,但每次工具调用、多轮对话都要等网络往返。用户在地下车库、电梯里、火车隧道中,网络一抖,整个Agent就卡住或者直接超时。后来换到端侧部署了一个7B的量化模型,虽然单轮回答不如云端聪明,但响应稳定在几百毫秒到两秒内,而且断网也能用,体验反而提升了一大截。
这个经历引出一个核心问题:端侧Agent的智能调度、工具调用、任务拆解,底层必须有一个随时可用的语言模型作为“大脑”。如果这个大脑放在云端,那么Agent的每一次思考都受制于网络稳定性、服务可用性和延迟,这对于需要实时响应的场景(智能家居中控、车载助手、工业巡检设备、穿戴设备)几乎是致命的。
端侧LLM部署,就是把一个经过压缩、量化后的语言模型,跑在用户手边的设备上。它解决的三个核心问题分别是:
- 隐私与数据主权:对话内容、传感器数据、用户习惯都不出设备,这在医疗、金融、企业内部场景是刚需。
- 延迟与稳定性:本地推理没有网络抖动,首Token延迟可以压到毫秒级,且天然支持离线运行。
- 成本与规模化:当Agent设备数以万计时,如果每个设备的调用都走云端GPU,费用会指数级上涨;端侧部署后,单位设备的边际推理成本趋近于零。
但端侧部署绝不是简单地“装一个Ollama就完事”。它涉及模型体积与能力的权衡、硬件算力的匹配、推理引擎的选择、Agent上下文的组织方式、工具调用协议的适配,甚至还有电量消耗的问题。这篇文章是《深入理解端侧Agent》系列的第二篇,我会从模型选型、推理引擎、Agent接入、实测数据、故障排查这五个维度,把一个端侧LLM从“能跑”到“跑得好”的完整路径讲清楚。
适合谁来读?如果你正在做端侧AI硬件产品、想在嵌入式设备或本地服务器上部署Agent、或者只是想把一个大模型装到自己电脑/板卡上研究一下,这篇文章的实操经验可以直接拿来用。
2. 部署前的模型选型:参数规模、量化等级与硬件算力怎么匹配
2.1 不同参数档位的真实能力差异
很多刚接触端侧LLM的人第一个问题是:“到底选多大的模型?”
我的建议是:不要追求参数大,而要追求“够用且最快”。端侧Agent的典型任务——意图识别、槽位抽取、工具调用参数生成、简单的多轮总结——其实并不需要175B级别的推理能力。经过这几年的实测,参数档位和能力的关系大致如下:
| 参数规模 | 典型能力表现 | 内存占用(4bit量化) | 适用设备 |
|---|---|---|---|
| 0.5B~1.5B | 能完成简单意图分类、关键词提取,但复杂指令跟随会崩 | 0.4~1GB | 树莓派、低端手机、MCU边缘 |
| 3B~4B | 能处理基本对话、API参数生成,偶尔会在长上下文下逻辑混乱 | 2~3GB | 中端手机、RK3588、Jetson Nano |
| 7B~8B | 工具调用、多步推理基本可靠,接近可用状态 | 4~6GB | Jetson Orin、旗舰手机、迷你主机 |
| 13B~14B | 复杂Agent任务游刃有余,但体积和功耗都明显上升 | 8~10GB | 带独显的工控机、开发工作站 |
这里要特别提醒:端侧Agent的“能力”不只是模型本身,还包括你对提示词和工具约束的工程化水平。一个3B模型如果配合结构化的工具描述和严格的输出格式校验,实际表现可能比一个裸奔的7B模型还稳定。我见过一个智能家居Agent项目,用Qwen3-4B配合严格JSON Schema约束,工具调用成功率做到了97%,这个数字很多云端模型都未必能稳定达到。
2.2 量化等级与内存/显存的换算逻辑
量化的作用是减少模型体积、降低内存带宽压力,从而提升推理速度。常见的量化格式主要有这么几类:
- FP16/FP32:原始精度,体积大,端侧基本不直接用。
- INT8:损失很小,速度提升明显,很多NPU原生支持。
- INT4(GPTQ/AWQ/GGUF Q4_K_M):体积压缩到原来的四分之一左右,是端侧部署的常用档位。
- INT3/INT2:极端压缩,只适合能力要求极低的场景。
一个7B模型,FP16下大约14GB,INT8大约7GB,INT4下只需要大约4GB多一点。这里有一个很多新手容易忽略的点:内存占用不等于模型文件大小。运行时还需要算上KV Cache、激活值、中间缓存,所以实际部署时建议留出至少模型体积1.5倍的内存余量。你下载一个Q4的7B模型只有4GB,但跑起来可能要吃6GB内存,如果你用老设备只有8GB内存,再加上系统中其他进程,就会开始疯狂换页,速度骤降。
2.3 硬件平台的算力参考与适配要点
端侧部署的硬件平台五花八门,我整理一下我实际用过的几类平台的算力特点:
| 平台 | 算力特征 | 适合的部署方式 |
|---|---|---|
| Jetson Orin系列 | 自带GPU和Tensor Core,内存带宽高,兼容CUDA生态 | 直接用llama.cpp的CUDA后端,或Ollama一键部署 |
| RK3588 | 有6TOPS NPU,但LLM对NPU的支持依赖厂商工具链,适配成本高 | 优先用CPU/GPU跑llama.cpp,NPU跑专属模型 |
| 骁龙8系手机 | 异构计算齐全,但受功耗墙限制,持续性能只有峰值的一半 | 配合厂商SDK(如高通AI Hub)或MNN/TNN |
| x86迷你主机 | CPU核数多,没有GPU时用AVX2/AVX512也能跑得动 | 无脑选Ollama,最优解 |
| 树莓派5 | 纯CPU,内存最多16GB,带宽有限 | 只建议跑4B以下量化模型,且要控制并发 |
选硬件的一条铁律:先定模型,再定引擎,最后定硬件。反过来选型你会很难受。比如你先买了树莓派,再想跑7B模型,那基本只能看着温度飙到85度然后脉冲降频。
3. 推理引擎选型与端侧部署实操
3.1 Ollama、llama.cpp、MNN 到底怎么选
端侧部署LLM的推理引擎,主流就这几个,各自的定位我讲清楚你就知道怎么选了。
Ollama是目前最省事的方案。它把模型下载、量化格式管理、HTTP服务、多模型切换全部封装好了,装上就能用。它底层用的就是llama.cpp的推理核心。对于原型验证、快速跑通、x86/Apple Silicon/Mac环境,这是第一选择。但它的缺点也很明显:可定制性弱,算子级优化基本没有,遇到OOM和特殊并发策略时你只能换引擎。
llama.cpp是纯C/C++实现,支持各种量化格式(GGUF),在CPU上做了大量优化(AVX2、NEON),也能调用CUDA、Metal、Vulkan。它的API设计非常底层,你可以精确控制推理参数、批处理大小、线程数,甚至把推理嵌入到自己写的C程序里。如果你的场景是在Jetson上用CUDA,或者在RK3588上做深度调优,llama.cpp是绕不开的底子。
MNN/TNN/NCNN是移动端推理框架,主打CNN模型,但也逐步支持了LLM。它们的好处是充分适配手机NPU、GPU、CPU异构调度,坏处是模型格式转换麻烦,很多新模型的算子支持滞后。如果你做的是Android/iOS应用的嵌入式Agent,可以关注一下MNN的LLM分支,否则建议先用llama.cpp把模型跑通,再用MNN做专项优化。
我的选型建议是:
- 想在电脑/U盘/小主机上快速看到效果:Ollama,五分钟搞定。
- 要部署到Jetson、RK3588或做深度定制:llama.cpp,一步到位,少走弯路。
- 要在手机App里集成Agent,且对功耗有要求的:MNN/高通AIHub。
3.2 用 Docker 封装端侧推理服务:完整步骤
我偏好用Docker来封装端侧推理服务,原因很简单:宿主机环境太容易脏了。Python、CUDA、编译器版本一变,AI应用“在我电脑上能跑”的惨剧就会重演。端侧设备通常需要长时间运行,Docker能让镜像锁定环境,升级和回滚都干净。
下面是一套我用过的方案,以llama.cpp的CUDA版为例。
第一步,准备模型文件。从Hugging Face下载量化好的GGUF格式模型,比如Qwen2.5-7B-Instruct的Q4_K_M版本:
mkdir -p /opt/agent/models cd /opt/agent/models # 用 huggingface-cli 或直接 wget wget https://example.com/qwen2.5-7b-instruct-q4_k_m.gguf第二步,编写Dockerfile,把模型拷贝进镜像并启动llama.cpp的server模式:
FROM ghcr.io/ggerganov/llama.cpp:server-cuda COPY ./models /models EXPOSE 8080 CMD ["--model", "/models/qwen2.5-7b-instruct-q4_k_m.gguf", \ "--host", "0.0.0.0", \ "--port", "8080", \ "--n-gpu-layers", "99", \ "--ctx-size", "8192", \ "--parallel", "4", \ "--jinja"]这里几个参数值得解释一下:
--n-gpu-layers 99:把尽可能多的层放到GPU上计算,加速效果明显。--ctx-size 8192:上下文窗口大小,直接决定KV Cache的内存占用。后面我会详细展开这里怎么设才合理。--parallel 4:允许4个并发序列共享同一个模型权重。--jinja:启用Jinja模板解析,让模型使用官方的ChatML等对话模板,这个不开启的话,对话格式容易错乱。
第三步,构建镜像并启动容器:
docker build -t agent-llm-service ./ docker run -d --name agent-llm \ --gpus all \ -p 8080:8080 \ agent-llm-service这样你的端侧推理服务就变成一个标准的HTTP端口了,Agent主程序只需要通过HTTP请求它,完全不用管底层是CPU还是GPU。
3.3 接入 Agent 的 HTTP 接口设计
llama.cpp的server模式提供了OpenAI兼容的/v1/chat/completions接口。这意味着你之前写的任何调用OpenAI API的Agent代码,只需要把base_url改成http://127.0.0.1:8080/v1,就能完成切换。
举一个Python Agent调用端侧模型的例子:
import openai client = openai.OpenAI( base_url="http://127.0.0.1:8080/v1", api_key="not-needed" ) response = client.chat.completions.create( model="qwen2.5-7b-instruct", messages=[ {"role": "system", "content": "你是端侧Agent的调度助手,只输出JSON。"}, {"role": "user", "content": "查询设备状态并生成控制指令"} ], temperature=0.2, max_tokens=1024, response_format={"type": "json_object"} )使用这份接口兼容设计,你的Agent可以在云端模型和端侧模型之间无缝切换。我建议把base_url做成配置项,在线用云端强模型做复杂任务,断网或低权限任务自动切到端侧模型,这样兼顾了能力和稳定性。
4. 端侧Agent的上下文、工具调用与会话状态管理
4.1 上下文窗口长度与KV Cache的博弈
端侧模型能处理的上下文长度,和硬件资源形成了直接矛盾。很多人一上来就把ctx-size调到32768,结果发现首Token延迟暴涨、内存直接吃满——因为KV Cache的大小大约等于:2 × batch × hidden_size × num_layers × num_kv_heads × 量化字节数 × ctx_size。
这个公式看起来很吓人,我直接给结果:一个7B模型在4bit量化下,8K上下文的KV Cache大约要额外占用2~3GB显存/内存;到了32K上下文,这个数字会翻四倍,达到8~12GB。所以上下文长度不是越长越好,而是要在满足Agent任务需求的前提下尽量短。
我的一般配置法则是:
- 单轮工具调用场景:
ctx_size=4096完全够用,低于2GB额外开销。 - 多轮对话+工具历史:
ctx_size=8192是比较好的平衡点。 - 长文档分析Agent:迫不得已才上16384或更大,但需要确认硬件的显存余量。
另外,llama.cpp在2.0之后的版本里加入了上下文“压缩/回收”机制,当上下文快满时可以自动丢弃早期消息。但从实际效果来看,提前裁剪消息比让模型自己在被截断的上下文里工作要可靠得多。语言模型对上下文“突然变短”的感知很差,容易胡言乱语。
4.2 Function Calling 在端侧模型上的实现
端侧Agent最核心的能力之一就是调用工具。云端模型通过原生Function Calling API轻松做到这一点,但端侧小模型在这方面的原生支持参差不齐。我从实践中总结出三条路径:
第一条,使用原生支持工具的模型。Qwen2.5系列、GLM-4系列等国内开源模型对工具调用做了专门训练,你只需要在提示词中给出工具描述,模型就能输出符合格式的调用请求。这类模型配合--jinja和自定义系统提示词,实测成功率较高。
第二条,用提示词+输出约束模拟工具调用。如果你的模型不支持原生Function Calling,就定义一个严格的工具描述格式,并把输出格式限定为JSON。如下:
{ "tool_calls": [ { "name": "turn_on_light", "arguments": {"device_id": "living_room_1", "brightness": 80} } ] }在系统提示词里用中文描述清楚每个工具的参数含义、取值范围、必填项。配合response_format={"type": "json_object"},小模型也能稳定输出。我实测Qwen2.5-3B在这种方案下的工具调用成功率,从30%提升到了85%以上,关键就在于格式约束。
第三条,把工具选择变成一个分类任务。当工具数量很多(十几、几十个)时,让小模型自己从列表里选,准确率会明显下降。这时候可以先用一个小模型对用户意图做粗分类,缩小候选工具范围,再调用推理模型做参数填充。这种“两段式”设计在端侧非常实用。
4.3 会话状态管理:不要让Agent“失忆”
端侧设备和云端的另一个显著差别是:设备会重启、会断电、会升级。Agent如果不知道自己“上次聊到哪了”,重启之后就会进入失忆状态。这里我推荐做三件事:
第一,把会话历史增量式落盘。每次对话结束,把消息追加到本地SQLite或JSON文件。注意不要每次都全量写,原因很简单:当上下文较长时,全量序列化的耗时和磁盘写入次数太高,尤其在嵌入式Flash存储上会加速磨损。
第二,保存一个“摘要向量”。每次对话超过N轮后,把之前的对话实时让模型总结成一段200字以内的摘要,连同最近几轮完整对话一起存入会话数据。这样即使上下文窗口有限,Agent也能通过摘要“记住”核心意图。
第三,把工具执行的结果也要持久化。很多Agent的所谓“记忆”只记录对话,但工具返回的状态(灯光现在开没开、设备温度是多少)才是用户真正关心的。把这些状态存入独立的键值存储,Agent每次决策前查询实时状态,比让模型记住一个早已过期的数值可靠得多。
5. 实测记录:Jetson Orin、RK3588、x86小主机的三平台部署
5.1 Jetson Orin 跑 DeepSeek 量化版:体验与调参
Jetson Orin系列是端侧Agent开发者的宠儿。我手上有一台Orin NX 16GB,系统是Ubuntu 22.04,CUDA 12.6。我用Ollama直接部署了DeepSeek-R1-Distill-Qwen-7B的Q4量化版,在ctx_size=8192、n-gpu-layers=99的情况下,首Token延迟大约0.3秒,生成速度约45 tokens/s。
这个速度对Agent场景来说完全够用,因为Agent每一步的核心是决策,而不是生成长篇大论。有一个参数值得注意——--parallel默认值是1。如果你打算让多个Agent实例共享这个模型,必须把它调高,否则后到的请求会排队,造成严重的假死感。
还有个细节是供电模式。Orin NX有nvpmodel可以切换15W/25W/40W功耗档,默认可能是15W。跑7B模型我建议直接切到25W或40W,否则GPU会自动降频,生成速度会掉到20 tokens/s以下,体验落差非常明显:
sudo nvpmodel -m 0 # 切到最大性能模式 sudo jetson_clocks # 锁定高频率5.2 RK3588 上CPU/GPU/NPU的取舍
RK3588是一个很有趣的平台:CPU是四核A76+四核A55,GPU是Mali-G610,还有一个6TOPS的NPU。很多人一看到“6TOPS NPU”就兴奋,以为能直接跑LLM,实际用起来才明白:NPU工具链对LLM的支持非常有限,而且各家的算子不通用。目前NPU上跑得比较顺的是目标检测类模型,比如热搜里常提到的rk3588部署yolov8那种量化CNN模型。
我在RK3588上部署7B模型的经历是:走CPU用llama.cpp,配合INT8或Q4量化,text generation速度大约是4~8 tokens/s。这个数字放到Agent场景很尴尬,因为用户等一个完整的工具调用决策,可能需要等十几秒。后来我把方案改成了“4B量化模型+两段式工具调用”,速度提到了10~15 tokens/s,总算勉强可用。
RK3588如果你想跑得更快,还有一个风扇和散热片的问题经常被忽略。这个平台满载时功耗能到10W以上,原厂开发板的被动散热根本压不住,CPU温度很快到80度然后降频。我的建议是:直接上一个5V的风扇模块,让CPU持续跑在中高频率,比什么软件调参都管用。
5.3 弱端设备的降级方案:树莓派与低配手机
不是所有Agent都跑在Jetson上。很多IoT设备、旧手机、树莓派4/5,算力弱、内存小,但同样需要本地推理能力。这种情况下我的建议很简单:降级模型,而不是硬扛。
树莓派5配8GB内存,跑Qwen2.5-1.5B的Q4量化版,生成速度大约是15 tokens/s;跑3B版本会掉到8 tokens/s,而且内存吃紧。所以如果Agent的核心场景只是“意图识别+参数提取”,1.5B是一个非常合理的选择。你可以把复杂任务继续交给云端,只把关键的低延迟任务留在端侧,这叫“云端协同”,不是“端侧全包”。
弱端设备的另一个优化手段是减少动态加载。模型加载本身就有1~3秒的冷启动开销,如果Agent频繁在不同模型之间切换,体验非常差。我的做法是一套设备固定一个“主模型”,启动时加载一次常驻内存,任何工具调用都走这个模型,只有遇到它明确表示“我搞不定”时,才切换到云端。
6. 端侧LLM的故障排查与性能调优实录
6.1 首Token延迟高、生成速度慢的排查链路
如果你部署完发现速度不对劲,别急着换模型、换框架,先按下面这条链路一步步排查,基本能定位90%的问题。
第一步,确认推理是否真的在GPU上。很多人设置了--n-gpu-layers 99,但实际因为显存不够,部分层仍然跑在CPU上,日志里会打印“offloaded 0/48 layers”或者“using CPU”。如果显存不足,有两种做法:一是先减少ctx-size降低KV Cache内存占用;二是调低n-gpu-layers,只把计算量大的几层放GPU,而不是死磕全放。
第二步,检查带宽瓶颈。端侧推理的速度瓶颈通常不是“算得快不快”,而是“权重读取有多快”。一个7B Q4模型在推理时,每生成一个Token需要把全部3~4GB权重从内存/显存中读一遍。DDR4的理论带宽大约25GB/s,DDR5是50GB/s,而Jetson的LPDDR5带宽可以达到100GB/s以上。如果你的DDR4小主机跑7B只能到5 tokens/s,这就是内存带宽的上限,跟CPU核心数量关系不大。
第三步,看线程数设置。llama.cpp的--threads并不是越大越好。CPU推理时线程数超过物理核心数反而会引入调度开销;GPU推理时CPU线程数只要够喂饱GPU就行。我实测Jetson上的经验是:--threads 4,保持默认,比盲目调到8更快更稳。树莓派5上则用6个线程,因为它的四核A76+四核A55架构,6线程能在性能和发热之间找到平衡。
第四步,确认是否有其他进程抢占CPU/GPU。在端侧设备上,摄像头、传感器、UI渲染、网络栈都可能吃算力。我在RK3588上遇到过类似情况:Docker容器里跑了YOLOv8目标检测,同时跑LLM,两个任务的延迟同时翻倍。解决办法是给容器设置CPU绑核或者用cgroup限制资源,比如让LLM容器独占四个大核,目标检测跑另外的小核。这比任何软件优化都直接。
6.2 并发请求来了怎么扛
端侧设备通常没有多卡,一个模型该不该同时处理多个请求?如果并发上去就乱套,该怎么设计?
首先要清楚一点:LLM推理的并发有两个极端。一个是串行批处理,请求多了就排队,优点是延迟稳定;另一个是动态批量(continuous batching),把多个请求拼成一个batch并行生成,能显著提升吞吐量,但单个请求的Token生成时间会变长。
llama.cpp的--parallel本质上是同时创建多条序列,底层会自动处理KV Cache的分割。根据我的测试,--parallel 4时,7B模型同时在跑的占用的显存会多出3GB左右。如果你的显存冗余不够,就不要开并发,宁可让它排队,不然多个请求一起OOM,一个都跑不出来。
如果设备的并发能力确实有限,我建议在Agent架构上加一个“请求闸门”:一台端侧设备只允许同时进行一个决策请求,其余请求先进入缓冲队列。端侧Agent的黄金准则是“一次只思考一件事”,因为用户在同一时刻通常只会产生一个意图,多个意图并发反而不符合交互逻辑。
6.3 模型升级与持续运行:别让设备“越用越卡”
端侧设备的一个隐患是长期运行后的内存碎片和缓存膨胀。Docker容器虽然隔离了环境,但模型权重常驻内存,如果内存不足,系统会开始把Agent的其他模块换出到Swap,然后你看到的现象就是“什么都在跑,但都慢半拍”。
我的长期运行经验有三条:
第一,给LLM容器分配一个固定的内存上限。Docker的--memory参数加上,防止在系统内存紧张时被OOM Killer误伤。同时给Swap预留合理空间,但不要让模型进程频繁落盘。
第二,模型升级采用镜像打标签的方式。不要把新模型直接覆盖旧文件然后重启服务,那样万一新模型有Bug,回滚非常痛苦。正确的做法是构建agent-llm:7b-q4-v1和agent-llm:7b-q4-v2两个镜像,升级时启新停旧,观察一天再删除旧镜像。这个流程对端侧的稳定性至关重要。
第三,监控温度与功耗。在Jetson和RK3588上,我习惯用脚本每隔10秒记录一次SoC温度。一旦温度超过75度,就自动降并发。这个策略需要写在Agent的调度逻辑里,比如温度高时改用1.5B模型做快速响应,温度正常时切回7B做完整规划。这个“热降级”思路我在实际项目中真的用上了,效果非常稳定。
6.4 一个完整的排查案例:从OOM到恢复
最后分享一个真实案例。我有一台16GB的Jetson Orin NX,跑7B量化模型和Agent主程序时,初期经常随机崩溃。日志里能看到CUDA OOM和killed process交替出现。
排查过程如下:先看sudo tegrastats,发现内存长期占用95%以上。然后看模型占用的固定部分:7B Q4约5GB,KV Cache开8K约3GB,加上CUDA上下文和PyTorch/Agent主程序,内存已经逼近16GB上限。再排查发现主程序里有一个循环重试机制,每失败一次就重新初始化一次客户端,导致旧连接没有释放,最终拖垮内存。
最终修复方案是:把Agent主程序拆成独立Docker容器,设置内存上限为4GB;LLM容器上限设为10GB;请求失败改用指数退避重试,不再反复重建客户端。部署后连续运行一周,再没有出现OOM。
这个案例想说明的道理是:端侧部署出问题,大部分时候不是模型不行,而是资源规划出了问题。把“模型、KV Cache、Agent代码、日志文件、系统缓存”这几块内存预算先算清楚,再落盘到容器配置里,大多数疑难杂症都能提前避免。
7. 关于端侧Agent部署,我的几点最终体会
做端侧Agent这两三年,最大的体会是:端侧LLM部署真正的难点不在“跑起来”,而在“跑得可控”。模型选型、量化格式、推理引擎这些只是基本功,真正拉开差距的是你对上下文的管理、工具调用的约束、资源上限的规划、以及故障时的降级策略。
一个能稳定运行的端侧Agent系统,往往不是由某个大模型决定的,而是由一堆“细节工程”共同支撑的。你愿意在多少并发时切换小模型、上下文满了之后是先裁剪还是先总结、温度过高是降频还是降模型,这些决策才决定你的Agent能不能7×24小时可靠工作。
如果你正处在“模型已经跑通,但Agent还不靠谱”的阶段,建议把重心放到工具调用的成功率上面,用严格的输出约束和更多的测试用例去打磨,而不是急着换更大的模型。端侧Agent的空间有限,但是只要把工程细节做到位,它依然能成为真正好用、可靠、属于用户自己的智能体。