:Ollama + Open WebUI生產(chǎn)實(shí)踐)
很多人以為部署大模型是算法工程師的專屬技能——下載幾個(gè)Python包跑個(gè)transformersdemo就算完事。但真正到了生產(chǎn)環(huán)境問(wèn)題才剛開(kāi)始模型版本怎么管理GPU顯存怎么分配API并發(fā)撐不住怎么辦日志和監(jiān)控怎么接這篇文章不講理論只講一個(gè)Java后端工程師能直接上手落地的方案用Docker容器化部署Ollama推理服務(wù)配合Open WebUI提供可視化界面再用vLLM解決高并發(fā)瓶頸。全程代碼可復(fù)制配置可運(yùn)行。一、整體架構(gòu)我們?cè)诖罱ㄊ裁聪雀闱宄畹倪@套東西長(zhǎng)什么樣核心組件就三個(gè)Ollama本地大模型推理引擎負(fù)責(zé)加載模型、管理版本、暴露REST APIOpen WebUI基于Web的ChatGPT風(fēng)格對(duì)話界面支持多用戶、多模型切換、對(duì)話歷史Docker把整個(gè)環(huán)境打包成可移植的容器開(kāi)發(fā)環(huán)境一鍵復(fù)制到生產(chǎn)環(huán)境二、Ollama Docker部署5分鐘跑起來(lái)Ollama的Docker鏡像已經(jīng)預(yù)裝了推理運(yùn)行時(shí)不需要你本地安裝CUDA工具鏈也不需要配Python環(huán)境。2.1 基礎(chǔ)部署CPU模式適合測(cè)試# docker-compose.yml —— Ollama基礎(chǔ)版 version: 3.8 ? services: ollama: image: ollama/ollama:0.3.6 container_name: ollama ports: - 11434:11434 volumes: # 模型文件持久化避免每次重啟重新下載 - ollama-models:/root/.ollama environment: # 允許跨域訪問(wèn)WebUI需要 - OLLAMA_ORIGINS* # 監(jiān)聽(tīng)所有接口 - OLLAMA_HOST0.0.0.0:11434 restart: unless-stopped ? volumes: ollama-models:啟動(dòng)命令docker compose up -d # 拉取模型以通義千問(wèn)7B為例約4.5GB docker exec -it ollama ollama pull qwen2:7b # 驗(yàn)證模型列表 docker exec -it ollama ollama list關(guān)鍵點(diǎn)volumes一定要配。模型文件動(dòng)輒幾個(gè)GB不配持久化卷容器重啟就全丟下次啟動(dòng)重新下載血淚教訓(xùn)。2.2 直接調(diào)用Ollama APIOllama暴露的是兼容OpenAI格式的REST API從你的Java后端調(diào)起來(lái)非常直接/** * Ollama API 調(diào)用示例 * 依賴Spring Boot 3.2 Spring Web */ Service public class OllamaChatService { ? private final WebClient webClient; ? public OllamaChatService(WebClient.Builder builder) { // 連接本地Ollama服務(wù) this.webClient builder .baseUrl(http://localhost:11434) .build(); } ? /** * 同步對(duì)話調(diào)用 */ public String chat(String userMessage) { MapString, Object request Map.of( model, qwen2:7b, messages, List.of( Map.of(role, system, content, 你是一個(gè)Java技術(shù)專家), Map.of(role, user, content, userMessage) ), stream, false, options, Map.of( temperature, 0.7, num_ctx, 4096 // 上下文窗口大小 ) ); ? return webClient.post() .uri(/api/chat) .bodyValue(request) .retrieve() .bodyToMono(String.class) .block(); } ? /** * 流式輸出SSE—— 用于實(shí)時(shí)打字機(jī)效果 */ public FluxString chatStream(String userMessage) { MapString, Object request Map.of( model, qwen2:7b, messages, List.of( Map.of(role, user, content, userMessage) ), stream, true ); ? return webClient.post() .uri(/api/chat) .bodyValue(request) .retrieve() .bodyToFlux(String.class); } }參數(shù)說(shuō)明temperature控制生成隨機(jī)性0.1~0.3適合代碼/問(wèn)答0.7~0.9適合創(chuàng)意寫(xiě)作num_ctx上下文token數(shù)7B模型建議409613B可開(kāi)到8192streamtrue開(kāi)啟SSE流式false等完整結(jié)果返回三、Open WebUI給推理服務(wù)穿上衣服光有API不夠團(tuán)隊(duì)里的產(chǎn)品、測(cè)試、運(yùn)營(yíng)也需要一個(gè)界面來(lái)跟模型對(duì)話。Open WebUI是目前最成熟的方案功能對(duì)標(biāo)ChatGPT# docker-compose.yml —— Ollama Open WebUI 完整版 version: 3.8 ? services: ollama: image: ollama/ollama:0.3.6 container_name: ollama ports: - 11434:11434 volumes: - ollama-models:/root/.ollama environment: - OLLAMA_ORIGINS* - OLLAMA_HOST0.0.0.0:11434 restart: unless-stopped ? open-webui: image: ghcr.io/open-webui/open-webui:0.3.10 container_name: open-webui ports: - 3000:8080 volumes: - open-webui-data:/app/backend/data environment: # 指向Ollama服務(wù)容器內(nèi)通過(guò)服務(wù)名訪問(wèn) - OLLAMA_BASE_URLhttp://ollama:11434 # 允許新用戶注冊(cè)生產(chǎn)環(huán)境建議關(guān)閉改用手動(dòng)導(dǎo)入 - ENABLE_SIGNUPtrue # 默認(rèn)語(yǔ)言 - DEFAULT_LOCALEzh-CN depends_on: - ollama restart: unless-stopped ? volumes: ollama-models: open-webui-data:啟動(dòng)后訪問(wèn)http://localhost:3000注冊(cè)一個(gè)賬號(hào)就能在界面里選擇已下載的模型開(kāi)始對(duì)話。生產(chǎn)環(huán)境注意ENABLE_SIGNUPtrue只適合內(nèi)網(wǎng)測(cè)試。外網(wǎng)部署時(shí)建議關(guān)閉注冊(cè)通過(guò)管理員后臺(tái)批量導(dǎo)入用戶或者接入OAuth2支持GitHub、Google、企業(yè)微信等前面加一層Nginx做HTTPS和基礎(chǔ)認(rèn)證四、GPU加速讓推理速度翻5倍CPU跑7B模型生成速度大概5~10 token/秒能用但體驗(yàn)差。上了GPU同樣模型能跑到60~100 token/秒差距肉眼可見(jiàn)。4.1 nvidia-docker 配置前提宿主機(jī)已安裝NVIDIA驅(qū)動(dòng) NVIDIA Container Toolkit# docker-compose.yml —— GPU加速版 version: 3.8 ? services: ollama: image: ollama/ollama:0.3.6 container_name: ollama ports: - 11434:11434 volumes: - ollama-models:/root/.ollama environment: - OLLAMA_ORIGINS* - OLLAMA_HOST0.0.0.0:11434 # GPU 配置核心 deploy: resources: reservations: devices: - driver: nvidia count: 1 # 使用1張GPUall表示全部 capabilities: [gpu] # restart: unless-stopped ? open-webui: image: ghcr.io/open-webui/open-webui:0.3.10 container_name: open-webui ports: - 3000:8080 volumes: - open-webui-data:/app/backend/data environment: - OLLAMA_BASE_URLhttp://ollama:11434 - ENABLE_SIGNUPtrue depends_on: - ollama restart: unless-stopped ? volumes: ollama-models: open-webui-data:驗(yàn)證GPU是否生效# 進(jìn)入容器查看 docker exec -it ollama nvidia-smi ? # 運(yùn)行模型時(shí)觀察顯存占用 docker exec -it ollama ollama run qwen2:7b # 另開(kāi)一個(gè)終端 docker exec -it ollama nvidia-smi4.2 顯存占用參考表模型參數(shù)量FP16顯存4-bit量化建議GPUqwen27B~14GB~4GBRTX 3060 12GBqwen214B~28GB~8GBRTX 3090 24GBllama38B~16GB~5GBRTX 4060 Ti 16GBllama370B~140GB~40GBA100 40GB × 2省錢技巧Ollama默認(rèn)會(huì)自動(dòng)選擇量化級(jí)別。顯存不夠時(shí)它會(huì)自動(dòng)加載Q4_K_M量化版本犧牲一點(diǎn)精度換運(yùn)行能力。你也可以手動(dòng)指定ollama pull qwen2:7b-q4_K_M五、vLLM高并發(fā)場(chǎng)景的核武器Ollama適合個(gè)人開(kāi)發(fā)和中小團(tuán)隊(duì)使用但遇到高并發(fā)比如同時(shí)幾十個(gè)用戶提問(wèn)單實(shí)例Ollama會(huì)排隊(duì)處理延遲直線上升。這時(shí)候需要vLLM。5.1 vLLM核心優(yōu)勢(shì)vLLM是UC Berkeley開(kāi)源的推理引擎核心創(chuàng)新是PagedAttention技術(shù)——把GPU顯存管理從粗粒度的預(yù)分配一大塊改成細(xì)粒度的按需分頁(yè)顯著提升吞吐量。實(shí)際壓測(cè)數(shù)據(jù)單張RTX 4090qwen2:7b模型方案并發(fā)數(shù)平均延遲吞吐量(token/s)Ollama1800ms45Ollama83200ms38vLLM1750ms48vLLM81100ms180vLLM322800ms420結(jié)論高并發(fā)下vLLM吞吐量是Ollama的10倍以上。5.2 vLLM Docker部署# docker-compose.yml —— vLLM高并發(fā)版 version: 3.8 ? services: vllm: image: vllm/vllm-openai:v0.5.4 container_name: vllm-server ports: - 8000:8000 volumes: # 掛載宿主機(jī)上的模型目錄 - /data/models:/models environment: - CUDA_VISIBLE_DEVICES0 # 啟動(dòng)命令加載Qwen2-7B啟用OpenAI兼容API command: --model /models/Qwen2-7B-Instruct --served-model-name qwen2-7b --dtype half --tensor-parallel-size 1 --max-model-len 4096 --gpu-memory-utilization 0.9 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] restart: unless-stopped關(guān)鍵參數(shù)解析--tensor-parallel-size多GPU張量并行2表示用2張卡同時(shí)算--gpu-memory-utilization 0.9使用90%顯存留10%給KV Cache動(dòng)態(tài)增長(zhǎng)--max-model-len最大上下文長(zhǎng)度超過(guò)會(huì)截?cái)?.3 Java后端接入vLLMvLLM暴露的是標(biāo)準(zhǔn)OpenAI APISpring AI直接就能對(duì)接/** * Spring AI 接入 vLLM 本地推理服務(wù) * 依賴org.springframework.ai:spring-ai-openai-spring-boot-starter:1.0.0-M1 */ Configuration public class VllmConfig { ? Bean public OpenAiApi openAiApi() { // 指向本地vLLM服務(wù)而非OpenAI官方 return new OpenAiApi( http://localhost:8000/v1, // vLLM的OpenAI兼容端點(diǎn) sk-no-key-required // 本地服務(wù)不需要真實(shí)API Key ); } ? Bean public OpenAiChatModel chatModel(OpenAiApi api) { var options OpenAiChatOptions.builder() .withModel(qwen2-7b) // 與vLLM的served-model-name一致 .withTemperature(0.7) .withMaxTokens(2048) .build(); return new OpenAiChatModel(api, options); } } ? Service public class AiChatService { ? Autowired private OpenAiChatModel chatModel; ? public String ask(String question) { return chatModel.call(question); } ? public FluxString askStream(String question) { return chatModel.stream(question) .map(chunk - chunk.getResult().getOutput().getContent()); } }兼容性說(shuō)明vLLM的/v1/chat/completions端點(diǎn)與OpenAI API完全兼容所以Spring AI的OpenAiChatModel可以直接復(fù)用一行不改。六、壓測(cè)與性能調(diào)優(yōu)部署完了得知道它能扛多少并發(fā)。推薦用locust或k6做壓測(cè)。# locustfile.py —— 簡(jiǎn)單的Ollama壓測(cè)腳本 from locust import HttpUser, task, between ? class OllamaUser(HttpUser): wait_time between(1, 3) ? task def chat(self): self.client.post(/api/chat, json{ model: qwen2:7b, messages: [{role: user, content: 用Java寫(xiě)一個(gè)單例模式}], stream: False })運(yùn)行l(wèi)ocust -f locustfile.py --host http://localhost:11434調(diào)優(yōu) checklist模型量化顯存不夠 → 換Q4量化版精度損失通常在可接受范圍上下文截?cái)鄋um_ctx不要設(shè)太大按需分配省顯存批處理大小vLLM的--max-num-seqs控制最大并發(fā)序列數(shù)默認(rèn)256可根據(jù)GPU調(diào)整多實(shí)例負(fù)載均衡單卡撐不住時(shí)開(kāi)多個(gè)Ollama/vLLM實(shí)例前面掛Nginx輪詢七、建議建議一開(kāi)發(fā)用Ollama生產(chǎn)用vLLMOllama的模型管理和WebUI生態(tài)更完善適合開(kāi)發(fā)調(diào)試階段。正式上線后如果QPS超過(guò)10建議切到vLLM吞吐量提升一個(gè)數(shù)量級(jí)。建議二模型文件做CDN緩存團(tuán)隊(duì)多人部署時(shí)每個(gè)人重新下載幾個(gè)GB的模型很浪費(fèi)時(shí)間。可以在內(nèi)網(wǎng)搭一個(gè)Harbor或Nexus把常用模型鏡像緩存起來(lái)新人入職docker pull幾分鐘搞定。建議三監(jiān)控必須接否則出事找不到根因至少監(jiān)控三個(gè)指標(biāo)GPU顯存占用nvidia-smi或DCGM exporter推理延遲P99Prometheus Grafana模型加載狀態(tài)Ollama的/api/tags接口輪詢部署大模型和部署MySQL本質(zhì)上沒(méi)有區(qū)別——都是起一個(gè)服務(wù)、掛一個(gè)卷、配一個(gè)端口。區(qū)別在于大模型的數(shù)據(jù)庫(kù)是幾十億個(gè)參數(shù)查詢一次要燒幾焦耳的電。明天我們聊一個(gè)更接地氣的話題國(guó)內(nèi)三大AI云平臺(tái)阿里云百煉 / 騰訊云混元 / 火山引擎方舟的企業(yè)級(jí)接入對(duì)比。如果你不想自己運(yùn)維GPU機(jī)器那篇就是為你寫(xiě)的。