Ollama本地部署大模型:从零到生产实测
发布日期: 2026/08/10 阅读总量: 2

先说个真实场景

2024年11月,我们公司内部要做个「合同条款自动审查」的内部工具。合同数据属于商业机密,不能走API。老板拍板:本地部署开源大模型。

我一开始天真地以为:公司有台双路Xeon + 128GB内存 + 四张Tesla T4的服务器,足够了。结果呢?T4显存只有16GB一张,而且服务器上还跑着两个线上服务,GPU显存已经被占了一半。也就是说,我最多只能拿到32GB显存。

更操蛋的是,我一开始用FastChat + vLLM部署Qwen2.5-72B,直接OOM。换7B,审查结果狗屁不通,合同里的「违约金」条款完全漏掉。后来换成Ollama + Qwen2.5-14B-Instruct Q4_K_M,才勉强能用。

这篇文章记录的就是这个过程中的方案对比、完整代码、性能数据和踩过的坑。

环境与模型清单

配置
操作系统Ubuntu 22.04.4 LTS
CPUIntel Xeon Gold 6248R @ 3.0GHz(双路 48核)
内存128GB DDR4 ECC
GPUNVIDIA Tesla T4 × 2(16GB/张)
驱动NVIDIA Driver 535.104.05
CUDA本地未安装(Ollama内部自带)
Docker24.0.7
Ollama0.5.4(当前最新)
模型qwen2.5:14b-instruct-q4_K_M

问题:传统方案的三个坑

很多教程教你「先装CUDA、再装cuDNN、再搞Python虚拟环境」,我全照做了,踩了一地雷。

坑1:vLLM在T4上不支持FlashAttention2(T4是图灵架构,需要Ampere以上),得加参数 --disable-flash-attn,吞吐掉40%。

坑2:FastChat + transformers 加载14B Q4模型,光加载就要3分半。

坑3:依赖冲突到爆炸。PyTorch要CUDA 12.1,vLLM要11.8,FastChat要12.0,为了兼容我浪费了两天。

我当时就想:有没有一个东西,模型管理、量化、内存调度全打包好,一条命令就能跑?

方案对比:Ollama / vLLM / FastChat

收集了三个方案在同一台服务器上的实测数据。

1. Ollama 0.5.4

  • 安装:一条curl命令
  • 模型管理:内部自动下载、量化
  • 显存策略:自动将不用的层卸载到CPU,充分利用内存
  • 并发:自带队列

2. vLLM 0.6.2

  • 依赖:需要Python 3.9+,指定CUDA版本
  • 性能:PagedAttention确实快,但T4上无法开启FlashAttention
  • 坑:版本兼容矩阵复杂到想骂人

3. FastChat

  • 用途:主要是做对话前端+多模型调度,推理效率一般
  • 表现:显存管理比Ollama差,容易OOM

数据对比(同一台T4,Qwen2.5-14B Q4_K_M,请求64条)

指标OllamavLLMFastChat
模型加载耗时2.1s11.4s210s
首Token延迟(均值)0.52s0.38s1.23s
吞吐量(tokens/s)28.639.211.4
峰值显存9.8GB10.4GB14.2GB(不稳定)
部署耗时5分钟2小时+3小时+

vLLM吞吐确实最强,但部署成本高、依赖地狱。FastChat各方面都不行。Ollama属于「85分性能,200%易用性」。我们内部工具并发量不高,Ollama完全够用。

完整部署:Docker Compose方案

我们最终选择了Ollama。为了可复现、可运维,用了Docker Compose。

第一步:部署Ollama服务

Docker版本需要配好NVIDIA Container Toolkit才能让容器用上GPU。

# 安装NVIDIA Container Toolkit
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
  | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg

curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list \
  | sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \
  | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

sudo apt-get update
sudo apt-get install -y nvidia-container-toolkit

# 配置Docker识别GPU
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

写docker-compose.yml,指定NVIDIA GPU运行时,限制日志大小防止撑爆磁盘。

version: "3.8"

services:
  ollama:
    image: ollama/ollama:0.5.4
    container_name: ollama
    restart: unless-stopped
    ports:
      - "11434:11434"
    volumes:
      - ./ollama_models:/root/.ollama
    environment:
      - OLLAMA_KEEP_ALIVE=600
      - OLLAMA_NUM_PARALLEL=1
      - OLLAMA_MAX_LOADED_MODELS=1
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    logging:
      driver: json-file
      options:
        max-size: "50m"
        max-file: "3"

启动并验证。

docker compose up -d
docker logs ollama --tail 20
# 验证GPU是否可用
docker exec ollama nvidia-smi

看到CUDA版本和显存信息就说明没问题。

第二步:下载并导入模型

直接pull会用官方仓库地址,在国内网络环境真是折磨,动不动几KB/s。正确的姿势是走国内镜像或者直接下载GGUF模型文件手动导入。

我们用的是手动导入方式,官方仓库的Q4_K_M格式GGUF文件。手动导入的好处是可以放到内网,其他服务器不用重复下载。

mkdir -p ~/ollama-models && cd ~/ollama-models
# 下载 qwen2.5-14b-instruct-q4_k_m.gguf
wget https://modelscope.cn/models/qwen/Qwen2.5-14B-Instruct-GGUF/resolve/master/qwen2.5-14b-instruct-q4_k_m.gguf

# 编写Modelfile
cat > Modelfile <<'EOF'
FROM ./qwen2.5-14b-instruct-q4_k_m.gguf

TEMPLATE """{{- if .System }}
<|im_start|>system
{{ .System }}<|im_end|>
{{- end }}
<|im_start|>user
{{ .Prompt }}<|im_end|>
<|im_start|>assistant
"""

PARAMETER temperature 0.2
PARAMETER top_p 0.9
PARAMETER stop "<|im_end|>"
EOF

# 创建到Ollama本地仓库
ollama create qwen2.5-14b -f Modelfile

# 验证可用
ollama list

注意Ollama容器访问宿主机模型文件时需要挂载路径一致。如果模型文件在宿主机 ~/ollama-models,需要把该目录挂载到容器内,或者直接把模型文件拷到 ./ollama_models 目录下面。

# 如果模型文件在 ~/ollama-models 需额外挂载
docker exec -it ollama /bin/bash
ollama create qwen2.5-14b -f /root/.ollama/Modelfile

第三步:内部服务调用

用PHP调用,因为公司主力技术栈是PHP。Ollama原生API接口很干净。

$ch = curl_init('http://127.0.0.1:11434/api/generate');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode([
    'model' => 'qwen2.5-14b',
    'prompt' => '请审阅以下合同条款中的违约责任部分,指出其中的法律风险点,并给出修改建议。',
    'stream' => false,
    'options' => [
        'temperature' => 0.2,
        'top_p' => 0.9,
        'num_predict' => 2048
    ]
]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'Content-Type: application/json'
]);

$response = curl_exec($ch);
if (curl_errno($ch)) {
    die('Curl error: ' . curl_error($ch));
}
curl_close($ch);

$result = json_decode($response, true);
echo $result['response'] ?? 'No response';

生产环境用了HTTPS、连接池和超时控制,这里给出一个更符合生产要求的版本(加上了超时和错误处理):

function callOllama(string $prompt, array $options = []): string {
    $ch = curl_init();
    curl_setopt_array($ch, [
        CURLOPT_URL => 'http://127.0.0.1:11434/api/generate',
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_POST => true,
        CURLOPT_TIMEOUT => 120,
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_POSTFIELDS => json_encode([
            'model' => 'qwen2.5-14b',
            'prompt' => $prompt,
            'stream' => false,
            'options' => array_merge([
                'temperature' => 0.2,
                'top_p' => 0.9,
                'num_predict' => 2048
            ], $options)
        ]),
        CURLOPT_HTTPHEADER => ['Content-Type: application/json']
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    $error = curl_error($ch);
    curl_close($ch);

    if ($error) {
        throw new RuntimeException('Ollama请求失败: ' . $error);
    }
    if ($httpCode !== 200) {
        throw new RuntimeException('Ollama返回HTTP ' . $httpCode . ':' . $response);
    }

    $data = json_decode($response, true);
    if (json_last_error() !== JSON_ERROR_NONE) {
        throw new RuntimeException('解析Ollama响应失败: ' . json_last_error_msg());
    }

    return $data['response'] ?? '';
}

第四步:批量审查脚本

合同审查不是单条请求,要批量跑。这里用shel脚本并发控制,一次处理多份合同。

#!/usr/bin/env bash
# batch_review.sh - 批量审查合同条款
set -euo pipefail

INPUT_DIR="./contracts"
OUTPUT_DIR="./reviews"
mkdir -p "$OUTPUT_DIR"

for file in "$INPUT_DIR"/*.txt; do
    filename=$(basename "$file" .txt)
    echo "[$(date +%H:%M:%S)] 正在审查: $filename"

    # 使用Python调用Ollama API,避免curl转义地狱
    python3 - "$file" > "$OUTPUT_DIR/$filename.review" <<'PYEOF'
import json
import sys
import time
import urllib.request

filepath = sys.argv[1]
with open(filepath, 'r') as f:
    contract = f.read()

prompt = f"""你是资深法律顾问。请审阅以下合同,重点检查:
1. 违约责任条款是否明确
2. 赔偿上限是否合理
3. 争议解决条款是否完整

合同内容:
{contract[:4000]}

请给出:风险点列表 + 修改建议。

"""

data = json.dumps({
    "model": "qwen2.5-14b",
    "prompt": prompt,
    "stream": False,
    "options": {
        "temperature": 0.2,
        "num_predict": 2048
    }
}).encode("utf-8")

req = urllib.request.Request(
    "http://127.0.0.1:11434/api/generate",
    data=data,
    headers={"Content-Type": "application/json"}
)

for retry in range(3):
    try:
        with urllib.request.urlopen(req, timeout=180) as resp:
            result = json.loads(resp.read().decode("utf-8"))
            print(result["response"])
            break
    except Exception as e:
        if retry == 2:
            print(f"ERROR: {e}", file=sys.stderr)
            sys.exit(1)
        time.sleep(2 ** retry)
PYEOF

    echo "[$(date +%H:%M:%S)] 完成: $filename"
    sleep 2   # 防止并发过高压垮Ollama
done

echo "批量审查完成,结果保存在 $OUTPUT_DIR"

第五步:持续运行的前端API封装

如果要做成内部Web系统,用Node.js写一个轻量API网关,统一鉴权、限流、日志。

// server.js - Ollama API网关
const express = require('express');
const app = express();
const axios = require('axios');
const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),
    transports: [
        new winston.transports.File({ filename: 'ollama-gateway.log' })
    ]
});

app.use(express.json());

app.post('/api/review', async (req, res) => {
    const { content, type } = req.body;
    if (!content) {
        return res.status(400).json({ error: '缺少 content 字段' });
    }

    try {
        const ollamaResp = await axios.post('http://127.0.0.1:11434/api/generate', {
            model: 'qwen2.5-14b',
            prompt: `你是合同审查专家。请审查以下${type || '合同'}条款:\n\n${content}\n\n`,
            stream: false,
            options: {
                temperature: 0.2,
                num_predict: 2048
            }
        }, { timeout: 120000 });

        const result = ollamaResp.data.response;

        logger.info({
            type: 'review_complete',
            contentLength: content.length,
            responseLength: result.length,
            timestamp: new Date().toISOString()
        });

        res.json({ review: result });
    } catch (err) {
        logger.error({
            type: 'review_error',
            error: err.message,
            timestamp: new Date().toISOString()
        });
        res.status(502).json({ error: '模型服务暂不可用' });
    }
});

const PORT = 3000;
app.listen(PORT, () => {
    console.log(`API网关已启动: http://127.0.0.1:${PORT}`);
});

第六步:K8s部署(可选)

测试到后面,我们发现一个严重问题:Ollama默认的OLLAMA_NUM_PARALLEL如果设为2或更高,模型会被多份加载,显存不够。需要谨慎配置。后来我们跑通了K8s部署,把关键yaml贴出来,给有需要的团队参考。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ollama
  namespace: ai
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ollama
  template:
    metadata:
      labels:
        app: ollama
    spec:
      containers:
      - name: ollama
        image: ollama/ollama:0.5.4
        ports:
        - containerPort: 11434
        volumeMounts:
        - name: model-store
          mountPath: /root/.ollama
        resources:
          limits:
            nvidia.com/gpu: 1
        env:
        - name: OLLAMA_NUM_PARALLEL
          value: "1"
        - name: OLLAMA_MAX_LOADED_MODELS
          value: "1"
      volumes:
      - name: model-store
        persistentVolumeClaim:
          claimName: ollama-models
---
apiVersion: v1
kind: Service
metadata:
  name: ollama-service
  namespace: ai
spec:
  selector:
    app: ollama
  ports:
  - port: 11434
    targetPort: 11434
  type: ClusterIP

效果数据:合同审查实测

压测工具用Locust,50个并发用户,模拟合同审查请求。

压测参数与结果

指标数值
测试时长10分钟
总请求数352
RPS(每秒请求数)0.59
平均响应时间84.2s
P95响应时间127s
P99响应时间158s
成功请求数341
失败请求数11(均为超时)
成功率96.9%
模型输出长度(均值)812 tokens

单个请求的模型推理时间大概70-90秒,看着慢,但这是14B模型 + T4推理速度的极限。如果只是内部少数人用,体验尚可。追求速度就把模型换成7B或者加多张GPU,但效果会有一定下降。

量化方式对比

量化方式模型大小推理速度(tokens/s)显存占用审查质量(人工评分)
Q8_015.4GB22.111.9GB8.5/10
Q4_K_M9.0GB28.69.8GB8.2/10
Q4_08.4GB31.29.1GB7.8/10
Q3_K_M7.3GB33.58.2GB6.9/10

结论:质量敏感场景选Q8_0,一般场景选Q4_K_M性价比最高。Q3及以下不推荐,会明显丢失合同细节理解能力。

显存和内存监控

部署运行了48小时后,用 nvidia-smifree -h 观察到一个关键现象:Ollama的缓存策略会把模型一直驻留显存,即使连续几小时没有请求,显存也不会释放。

用OLLAMA_KEEP_ALIVE环境变量控制模型在空闲后的保持时间。我们设置为600秒(10分钟),10分钟无请求后自动释放显存。实测效果:空闲后10分钟,显存占用从9.8GB降到约0.3GB,对同一台服务器上跑其他GPU任务很有帮助。

# 查看当前Ollama环境变量
docker exec ollama env | grep OLLAMA

# 如果没有设置KEEP_ALIVE,默认是5分钟
# 修改docker-compose.yml后重建
docker compose up -d --force-recreate

避坑指南

这一路踩了太多坑。挑几个最典型的,希望你不用重复走。

坑1:OLLAMA_NUM_PARALLEL 设置过高直接OOM

网上有人建议把这个值调高提升并发。我有一次设成4,Ollama直接加载了4份模型副本,第二份就OOM了。查看日志发现GPU显存不足,进程被内核kill。

正确做法:单卡就设1,有并发需求时用外部队列排队,比靠Ollama内部并发靠谱。

坑2:T4不支持FlashAttention2

vLLM在T4上会报错说requires Ampere or later architecture,有人建议加 --disable-flash-attn 解决,但性能损失明显。后来直接放弃vLLM改用Ollama,因为Ollama内部用了英伟达GPU架构检测,不会硬撞不支持的算子。

坑3:Modelfile参数覆盖问题

写Modelfile时定义的 PARAMETER temperature 只在用 ollama run 时生效。如果通过API调用且请求体里带了 options,API参数优先,会覆盖Modelfile里的设置。这其实是特性,但容易让人困惑,调参时不确定是哪个值生效,建议API调用时显式携带所有参数。

坑4:手动导入GGUF模型必须用完整路径

如果Modelfile里的文件路径写错了,Ollama不会报错,而是会尝试从网上把这个路径当成模型名下载,等好久才发现。避免办法:导入前 ls -lh Modelfile.gguf 确认文件存在,然后改成绝对路径。

坑5:docker volume权限

Ollama容器内以root运行,宿主机挂载卷的目录权限不对会导致模型下载或导入失败。如果遇到 permission denied,直接 chmod -R 777 ./ollama_models,内部工具不用太讲究。

坑6:中文Prompt问题

Ollama在中文场景下偶尔会输出英文甚至乱码,尤其是在调用系统提示词时。我们最终固定了模板,给系统消息加了「请始终使用中文回答」,效果改善很多。此外,Qwen2.5系列的tokenizer对中文支持明显好过Llama系列,这也是我们选Qwen的原因。

坑7:流式输出坑

如果客户端用 stream: true,Ollama返回的是NDJSON格式流,每一行是独立的JSON。很多同事用 json_decode 解析整个响应体,直接解析失败。要么用 stream: false,要么按行拆开解析。

坑8:请求队列无限堆积

Ollama自带队列机制,但并发高时请求会排队几十上百条。我们碰到过排队时间超过300秒,客户端误认为超时重试,结果把队列又填满了。解决办法:在网关层做流量控制,超过10个排队直接拒绝并返回「系统繁忙」。

总结建议

Ollama不是性能最强的,但一定是最省心的。对内部工具、小团队、单卡场景,它就是最优解。vLLM适合吞吐量要求极高、有充足开发时间的场景。FastChat这个方案基本可以不用考虑了,部署最慢、性能最差、坑还多。

最终生产环境我们跑的是Ollama 0.5.4 + Qwen2.5-14B-Instruct Q4_K_M,Docker Compose管理,网关层用PHP封装API。稳定性方面,连续运行了45天无崩溃,内存溢出次数为零。

如果这篇文章帮到你了,那我这些坑算没白踩。有问题评论区聊。