昨晚凌晨两点,我在一块Jetson Orin NX上跑一个多模态Agent,Ollama拉下来的Qwen2.5-VL-7B,推理速度慢到令人发指——每秒0.3个token,Agent调用一次工具函数要等两分钟。更离谱的是,内存直接爆了,系统OOM killer把SSH服务都给杀了。我盯着串口日志,看到一行“Killed process 3276 (ollama_llama_server)”的时候,突然意识到一个问题:我们这些搞Agent的人,是不是太依赖云端API了?本地部署这件事,远不是“pip install ollama”然后“ollama run”那么简单。
今天这篇笔记,我就把Ollama、vLLM、llama.cpp这三套方案在Agent场景下的真实表现、踩坑记录、以及边缘设备适配的硬核经验,全部摊开来讲。不废话,直接上干货。
一、Ollama:最易用,但最容易被坑
Ollama确实降低了本地跑模型的门槛,但如果你把它当生产环境用,迟早要出事。我见过太多人写Agent代码时直接调Ollama的HTTP API,然后抱怨“为什么我的Agent响应这么慢”。
Ollama的并发模型是个坑。默认情况下,Ollama的API是串行处理的。你发一个请求,它开始推理,这时候第二个请求进来,会直接排队。更坑的是,Ollama的请求队列没有超时机制——如果你的Agent同时调用了多个工具,每个工具都发一个推理请求,后面的请求会一直等,直到前面的跑完。我遇到过Agent卡死半小时的情况,就是因为一个工具函数调用了三次LLM,三次请求串行执行,每次推理30秒。
解决办法?别用Ollama的默认配置。在启动Ollama服务时,加上--num-parallel 4参数,允许并行处理。但注意,并行数不是越大越好,取决于你的显存。我实测RTX 4090上跑Qwen2.5-7B,--num-parallel 2是安全值,超过4就会OOM。
另一个坑是Ollama的模型加载策略。Ollama默认会把整个模型加载到显存,然后一直占着。如果你的Agent需要动态切换模型(比如先调一个7B的通用模型做规划,再调一个1.5B的专用模型做分类),Ollama会先卸载旧模型再加载新模型,这个过程耗时5-10秒。我写过一个Agent,每次工具调用都要切换模型,结果90%的时间花在模型加载上。
我的做法:如果必须用Ollama,就开两个Ollama实例,一个跑大模型,一个跑小模型,分别监听不同端口。Agent代码里根据任务类型路由到不同实例。虽然浪费一点显存,但避免了切换开销。
二、vLLM:吞吐量王者,但Agent场景有隐藏问题
vLLM的PagedAttention和continuous batching在纯文本生成场景下确实牛,吞吐量能比Ollama高3-5倍。但用在Agent上,有几个坑你必须知道。
第一个坑:vLLM的API设计偏向“一次性生成”。Agent场景下,我们经常需要流式输出(streaming),让用户看到思考过程。vLLM支持streaming,但它的streaming实现有个问题——如果你在生成过程中中断请求(比如Agent发现不需要继续推理了,直接关闭连接),vLLM的worker进程不会立即释放资源。我遇到过vLLM的worker进程卡死,导致后续所有请求都超时的情况。
解决办法:在Agent代码里,不要直接关闭HTTP连接,而是调用vLLM的/v1/chat/completions接口时,设置stop参数或者发送一个特殊的abort信号。vLLM官方文档里其实有提到这个,但很多人没注意。具体做法是:在请求体里加一个"stop_token_ids": [tokenizer.eos_token_id],这样Agent可以主动触发停止。
第二个坑:vLLM的显存管理在边缘设备上会翻车。我在Jetson Orin上试过vLLM,它默认会预留大量显存给KV cache,导致模型本身都放不下。vLLM的--max-model-len参数默认是4096,但在边缘设备上,你需要手动调小。比如在8GB显存的设备上跑Qwen2.5-7B,--max-model-len设成2048才能跑起来。但注意,调小这个参数意味着Agent的上下文窗口变小,如果你的Agent需要处理长对话历史,可能会截断。
我的经验:vLLM适合做Agent的推理后端,但前提是你有足够的显存(至少16GB以上)。在边缘设备上,vLLM的收益不如llama.cpp明显。
三、llama.cpp:边缘设备的救星,但需要手搓
llama.cpp是我在边缘设备上最常用的方案。它用C++实现,没有Python的GIL锁问题,内存占用极低。我在树莓派5上跑过Qwen2.5-1.5B,量化到Q4_K_M,推理速度能达到每秒8个token,虽然慢,但至少能跑。
但llama.cpp的坑在于它的API设计。它默认只提供一个简单的HTTP server,功能非常简陋。没有vLLM那样的continuous batching,没有Ollama那样的模型管理。你需要自己写代码来管理模型的生命周期。
我踩过的一个大坑:llama.cpp的server模式默认是单线程的。如果你用Python的requests库并发调用,它会直接崩溃。我一开始没注意,写了一个Agent,同时调用了两次llama.cpp的API,结果server直接segfault了。查了半天才发现,llama.cpp的server需要加--threads 4参数才能处理并发请求,而且这个参数不是越大越好,取决于CPU核心数。
另一个坑:llama.cpp的量化模型在Agent场景下,输出质量会下降。特别是当Agent需要做复杂的推理(比如数学计算、代码生成)时,量化后的模型容易出错。我做过对比测试:Qwen2.5-7B在Q4_K_M量化下,Agent的tool calling准确率从95%降到了82%。如果你的Agent对准确性要求高,建议至少用Q6_K或者Q8_0量化。
我的做法:在边缘设备上,我用llama.cpp跑一个1.5B的小模型做Agent的“快速响应”模块,处理简单的分类和格式化任务。复杂的推理任务,通过MQTT转发到服务器上的大模型。这样既保证了响应速度,又保证了准确性。
四、边缘设备适配:从Jetson到树莓派的实战经验
边缘设备适配是Agent本地部署最难的部分。不同设备的架构、内存、算力差异巨大,没有一个通用的方案。
Jetson Orin系列:这是目前最适合跑Agent的边缘设备。它有专用的DLA(深度学习加速器)和GPU,但问题在于驱动和库的兼容性。我试过在Jetson Orin NX上装Ollama,结果发现Ollama依赖的CUDA版本和Jetson的JetPack版本不匹配。折腾了两天,最后用Docker解决了——NVIDIA官方提供了JetPack的Docker镜像,里面预装了所有依赖。
注意:Jetson上的显存是共享的(CPU和GPU共用),所以不能直接用nvidia-smi看显存。要用tegrastats工具监控实际可用内存。我写了一个脚本,在Agent启动前检查可用内存,如果低于4GB就自动切换到更小的模型。
树莓派5:这玩意儿跑Agent纯属找虐。但如果你非要在上面跑,记住几点:第一,用llama.cpp,别用Ollama或vLLM,它们依赖的库在ARM架构上编译会出问题。第二,模型必须量化到Q4_K_M以下,而且模型参数量不能超过3B。第三,关闭所有不必要的服务,包括桌面环境。我在树莓派5上跑Agent,只保留了一个SSH服务和llama.cpp的server,其他全关了。
一个血泪教训:树莓派的SD卡写入速度很慢,而llama.cpp在加载模型时会频繁读写磁盘。如果你用SD卡,模型加载时间会从几秒变成几分钟。解决办法:用外接SSD或者把模型加载到内存盘(tmpfs)。我写了个脚本,在Agent启动时把模型文件复制到/dev/shm(共享内存),加载速度提升了10倍。
Intel NUC / 迷你主机:这是最平衡的方案。有x86架构,有Intel的OpenVINO加速。我试过在NUC上跑Ollama,配合Intel的Arc显卡,推理速度比纯CPU快3倍。但注意,Intel的显卡驱动在Linux上还是有点坑,需要手动安装intel-opencl-icd和level-zero库。
五、Agent框架与本地推理引擎的集成
不管你用哪个推理引擎,最终都要和Agent框架(比如LangChain、AutoGPT、或者你自己写的框架)集成。这里有几个关键点:
1. 流式输出的处理。Agent框架通常需要流式输出来实现“打字机效果”或者实时更新状态。但不同的推理引擎,流式输出的格式不一样。Ollama返回的是JSON Lines,vLLM返回的是Server-Sent Events,llama.cpp返回的是纯文本。我写了一个统一的适配层,把三种格式都转成Python的异步生成器,这样Agent框架就不用关心底层用的是哪个引擎。
2. 工具调用的格式。现在主流模型都支持function calling,但不同推理引擎对function calling的支持程度不同。Ollama原生支持,vLLM需要加--enable-auto-tool-choice参数,llama.cpp需要自己解析模型的输出。我建议在Agent框架里统一用OpenAI的API格式,然后通过适配层转换成各个引擎的格式。这样换引擎时只需要改适配层,不用改Agent逻辑。
3. 错误处理和重试。本地推理引擎比云端API更容易出错。显存不足、模型加载失败、推理超时,都是家常便饭。我写了一个重试机制:如果推理失败,先检查显存,如果显存不足就释放一些缓存,然后重试。如果重试三次还失败,就切换到备用模型(比如从7B降到1.5B)。
六、个人经验性建议
写了这么多,最后说点实在的。
别迷信“一键部署”。任何号称“一行命令部署Agent”的方案,在真实场景下都会出问题。本地部署的本质是资源管理,你需要理解你的硬件、你的模型、你的Agent框架,然后找到它们之间的平衡点。
量化不是万能的。量化确实能降低内存占用,但会牺牲模型质量。如果你的Agent需要做精确的数学计算或者代码生成,建议用FP16或者BF16,别用INT4。我见过一个Agent因为用了Q4_K_M量化,在计算“1+1”时输出了“3”,导致整个流程崩溃。
边缘设备上,别跑大模型。7B以上的模型在边缘设备上基本跑不动,即使跑起来,速度也慢到无法接受。我建议在边缘设备上只跑1.5B-3B的小模型,处理简单的任务。复杂的任务通过边缘-云端协同来解决。
最后,做好日志和监控。本地部署的Agent比云端更容易出问题,而且出了问题很难排查。我写了一个监控脚本,每5秒检查一次推理引擎的状态,包括显存占用、推理延迟、请求队列长度。如果发现异常,自动重启引擎并发送告警。这个脚本救了我好几次。
本地部署Agent这条路,没有捷径。但当你真正把Agent跑在边缘设备上,看到它离线也能正常工作的时候,那种成就感,是调用云端API永远体会不到的。