返回首页

2026年本地部署大语言模型完全指南:用Python运行自己的AI模型

2026年本地部署大语言模型完全指南:用运行自己的模型

为什么要本地部署大语言模型?

每次调用-4、这些商业,你都在为别人的服务器买单。更关键的是,你的数据——商业计划、代码、客户信息——全部经过第三方服务器。

本地部署的核心优势:

  • 数据隐私:所有数据留在本地,不出你的机器
  • 零API成本:一次部署,无限调用,不再按token计费
  • 离线可用:断网也能正常使用
  • 完全可控:模型选择、参数调整、输出格式,全由你说了算
  • 低延迟:省去网络往返,响应速度更快(尤其在GPU加速下)

2026年,开源大模型的质量已经非常接近商业模型。Llama 3.3、 2.5、 V3、等模型在很多任务上已经达到GPT-4级别。这意味着本地部署的实用性已经不再是问题

本文将带你用Python从零搭建本地LLM服务,覆盖三大主流方案:llama.cpp,每个方案都有完整可运行的代码。


一、硬件需求与选型建议

1.1 GPU显存是关键

本地部署LLM最大的瓶颈是GPU显存。不同参数量的模型需要的显存差异巨大:

模型规模 FP16显存需求 4bit量化后 代表模型
1-3B 2-6GB 1-2GB Qwen2.5-1.5B, Phi-3-mini
7-8B 14-16GB 4-5GB Llama-3.1-8B, Qwen2.5-7B
14B 28GB 8-9GB Qwen2.5-14B
32-70B 64-140GB 20-40GB Qwen2.5-72B, Llama-3.3-70B

实用建议:

  • 入门级(8GB显存,如RTX 4060):跑7B量化模型,日常问答、代码生成够用
  • 中端(16-24GB显存,如RTX 4080/4090):跑14B-32B模型,质量明显提升
  • 高端(双卡或A100):跑70B模型,接近商业API效果
  • 纯CPU方案:llama.cpp支持CPU推理,速度慢但零硬件门槛

1.2 检查你的GPU环境

#!/usr/bin/env python3
"""检查本地GPU环境,确定适合的模型规模"""

import subprocess
import sys

def check_nvidia_gpu():
    """检查 GPU信息"""
    try:
        result = subprocess.run(
            ["nvidia-smi", "--query-gpu=name,.total,memory.,driver_version",
             "--format=csv,noheader,nounits"],
            capture_output=True, text=True
        )
        if result.returncode == 0:
            gpus = []
            for line in result.stdout.strip().split('\n'):
                parts = [p.strip() for p in line.split(',')]
                gpus.append({
                    'name': parts[0],
                    'total_mb': int(parts[1]),
                    'free_mb': int(parts[2]),
                    'driver': parts[3]
                })
            return gpus
    except FileNotFoundError:
        pass
    return None

def recommend_model(gpus):
    """根据GPU配置推荐模型"""
    if not gpus:
        print("⚠️  未检测到NVIDIA GPU,建议使用llama.cpp CPU推理方案")
        print("   推荐模型:Qwen2.5-3B-Instruct (GGUF Q4_K_M)")
        return

    total_free = sum(g['free_mb'] for g in gpus)
    gpu_info = gpus[0]
    
    print(f"🖥️  GPU: {gpu_info['name']}")
    print(f"📊 显存: {gpu_info['total_mb']}MB 总计, {total_free}MB 可用")
    print(f"🔧 驱动版本: {gpu_info['driver']}")
    print()

    if total_free >= 40000:
        print("✅ 推荐:70B模型 (Qwen2.5-72B / Llama-3.3-70B)")
        print("   方案:vLLM + AWQ/GPTQ 4bit量化")
    elif total_free >= 16000:
        print("✅ 推荐:14B-32B模型 (Qwen2.5-14B / DeepSeek-V2-Lite)")
        print("   方案:Ollama 或 vLLM")
    elif total_free >= 8000:
        print("✅ 推荐:7B-8B模型 (Llama-3.1-8B / Qwen2.5-7B)")
        print("   方案:Ollama(最简单)")
    else:
        print("⚠️  显存较小,推荐:3B以下量化模型")
        print("   方案:llama.cpp + GGUF量化格式")

if __name__ == "__main__":
    gpus = check_nvidia_gpu()
    recommend_model(gpus)

二、方案一:Ollama——最简单的本地LLM方案

Ollama是2024-2026年最火的本地LLM运行工具,一条命令就能跑起来,非常适合入门。

2.1 安装Ollama

#  / WSL
curl -fsSL https://ollama.com/install.sh | sh

# 
brew install ollama

# :直接下载安装包 https://ollama.com/download

2.2 用命令行快速体验

# 拉取并运行 Qwen2.5 7B 模型
ollama run qwen2.5:7b

# 拉取 Llama 3.1 8B
ollama run llama3.1:8b

# 查看本地已下载的模型
ollama list

# 查看运行中的模型
ollama ps

2.3 用Python调用Ollama API

Ollama启动后会自动暴露一个REST API(默认端口11434),我们可以用Python直接调用:

#!/usr/bin/env python3
"""用Python调用Ollama本地LLM API - 基础用法"""

import requests
import json
import time

class OllamaClient:
    """Ollama API 客户端"""
    
    def __init__(self, base_url="http://localhost:11434"):
        self.base_url = base_url
    
    def list_models(self):
        """列出本地所有已下载的模型"""
        resp = requests.get(f"{self.base_url}/api/tags")
        return resp.json().get("models", [])
    
    def generate(self, model, prompt, =None, stream=False):
        """向模型发送请求并获取回复"""
        payload = {
            "model": model,
            "prompt": prompt,
            "stream": stream,
        }
        if system:
            payload["system"] = system
        
        start = time.time()
        resp = requests.post(
            f"{self.base_url}/api/generate",
            json=payload,
            stream=stream
        )
        
        if stream:
            return self._handle_stream(resp)
        
        result = resp.json()
        elapsed = time.time() - start
        
        return {
            "response": result["response"],
            "total_duration_s": result.get("total_duration", 0) / 1e9,
            "eval_count": result.get("eval_count", 0),
            "tokens_per_second": result.get("eval_count", 0) / (result.get("eval_duration", 1) / 1e9),
        }
    
    def chat(self, model, messages, stream=False):
        """对话模式(支持多轮对话)"""
        payload = {
            "model": model,
            "messages": messages,
            "stream": stream,
        }
        
        resp = requests.post(
            f"{self.base_url}/api/chat",
            json=payload,
            stream=stream
        )
        
        if stream:
            return self._handle_stream(resp)
        
        result = resp.json()
        return result["message"]["content"]
    
    def _handle_stream(self, resp):
        """处理流式响应"""
        full_text = ""
        for line in resp.iter_lines():
            if line:
                chunk = json.loads(line)
                token = chunk.get("response", "") or chunk.get("message", {}).get("content", "")
                print(token, end="", flush=True)
                full_text += token
                if chunk.get("done"):
                    break
        print()
        return full_text


# ==================== 使用示例 ====================

if __name__ == "__main__":
    client = OllamaClient()
    model = "qwen2.5:7b"
    
    # 1. 列出本地模型
    models = client.list_models()
    print("📦 本地模型列表:")
    for m in models:
        print(f"  - {m['name']} ({m['size'] / 1e9:.1f}GB)")
    
    # 2. 简单生成
    print("\n" + "=" * 50)
    print("📝 简单生成测试:")
    result = client.generate(
        model=model,
        prompt="用Python写一个快速排序算法,要求支持自定义比较函数",
    )
    print(f"回复:{result['response']}")
    print(f"速度:{result['tokens_per_second']:.1f} /s")
    
    # 3. 带系统提示的对话
    print("\n" + "=" * 50)
    print("💬 对话模式测试:")
    messages = [
        {
            "role": "system",
            "content": "你是一位资深Python开发专家,回答简洁实用,包含代码示例。"
        },
        {
            "role": "user",
            "content": "如何用asyncio实现一个异步任务队列?"
        }
    ]
    reply = client.chat(model=model, messages=messages)
    print(f"回复:{reply}")
    
    # 4. 多轮对话
    print("\n" + "=" * 50)
    print("🔄 多轮对话测试:")
    conversation = [
        {"role": "system", "content": "你是一个有帮助的助手。"},
        {"role": "user", "content": "解释一下Python的GIL是什么"},
    ]
    reply1 = client.chat(model=model, messages=conversation)
    print(f"Q: Python的GIL是什么")
    print(f"A: {reply1}")
    
    # 追加历史消息,继续对话
    conversation.append({"role": "assistant", "content": reply1})
    conversation.append({"role": "user", "content": "那在什么场景下GIL会成为瓶颈?"})
    reply2 = client.chat(model=model, messages=conversation)
    print(f"\nQ: 什么场景下GIL会成为瓶颈?")
    print(f"A: {reply2}")

2.4 搭建Ollama API网关——对外提供标准化接口

Ollama自带的API格式比较特殊,如果你想用兼容的格式(方便切换后端),可以搭一个适配层:

#!/usr/bin/env python3
"""
将Ollama包装为OpenAI兼容API
这样任何支持OpenAI SDK的工具都能直接接入本地模型
"""

from flask import Flask, request, Response, jsonify
import requests
import json
import time
import uuid

app = Flask(__name__)
OLLAMA_URL = "http://localhost:11434"

@app.route("/v1/chat/completions", methods=["POST"])
def chat_completions():
    """OpenAI兼容的chat 接口"""
     = request.json
    model = data.get("model", "qwen2.5:7b")
    messages = data.get("messages", [])
    stream = data.get("stream", False)
    
    # 转换为Ollama格式
    ollama_payload = {
        "model": model,
        "messages": messages,
        "stream": stream,
        "options": {}
    }
    
    if "temperature" in data:
        ollama_payload["options"]["temperature"] = data["temperature"]
    if "max_tokens" in data:
        ollama_payload["options"]["num_predict"] = data["max_tokens"]
    
    if stream:
        return Response(
            stream_ollama(ollama_payload),
            content_type="text/event-stream"
        )
    
    resp = requests.post(f"{OLLAMA_URL}/api/chat", json=ollama_payload)
    result = resp.json()
    
    # 转换为OpenAI格式
    return jsonify({
        "id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
        "object": "chat.completion",
        "created": int(time.time()),
        "model": model,
        "choices": [{
            "index": 0,
            "message": {
                "role": "assistant",
                "content": result["message"]["content"]
            },
            "finish_reason": "stop"
        }],
        "usage": {
            "prompt_tokens": result.get("prompt_eval_count", 0),
            "completion_tokens": result.get("eval_count", 0),
            "total_tokens": result.get("prompt_eval_count", 0) + result.get("eval_count", 0)
        }
    })

def stream_ollama(payload):
    """流式响应转换"""
    resp = requests.post(
        f"{OLLAMA_URL}/api/chat",
        json=payload,
        stream=True
    )
    
    for line in resp.iter_lines():
        if line:
            chunk = json.loads(line)
            content = chunk.get("message", {}).get("content", "")
            
            sse_data = {
                "id": f"chatcmpl-{uuid.uuid4().hex[:8]}",
                "object": "chat.completion.chunk",
                "choices": [{
                    "index": 0,
                    "delta": {"content": content} if content else {},
                    "finish_reason": "stop" if chunk.get("done") else None
                }]
            }
            yield f"data: {json.dumps(sse_data)}\n\n"
            
            if chunk.get("done"):
                yield "data: [DONE]\n\n"

@app.route("/v1/models", methods=["GET"])
def list_models():
    """列出可用模型"""
    resp = requests.get(f"{OLLAMA_URL}/api/tags")
    models = resp.json().get("models", [])
    return jsonify({
        "object": "list",
        "data": [{
            "id": m["name"],
            "object": "model",
            "owned_by": "local"
        } for m in models]
    })

if __name__ == "__main__":
    print("🚀 OpenAI兼容API网关启动在 http://localhost:8000")
    app.run(host="0.0.0.0", port=8000)

启动后,任何支持OpenAI SDK的代码都能无缝切换到本地模型:

from openai import OpenAI

# 指向本地网关
client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed"  # 本地不需要真实key
)

response = client.chat.completions.create(
    model="qwen2.5:7b",
    messages=[
        {"role": "system", "content": "你是一个Python专家"},
        {"role": "user", "content": "写一个装饰器,实现函数调用的自动重试"}
    ],
    temperature=0.7,
    max_tokens=1000
)

print(response.choices[0].message.content)

三、方案二:vLLM——高性能推理引擎

vLLM是目前最流行的高性能LLM推理引擎,支持PagedAttention等先进技术,吞吐量是普通推理的2-24倍。适合需要高并发、低延迟的生产环境。

3.1 安装vLLM

# 确保版本 >= 11.8
pip install vllm

# 或者用Docker(推荐,环境隔离更好)
docker pull vllm/vllm-openai:latest

3.2 启动vLLM服务

# 启动一个兼容OpenAI API的服务
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2.5-7B-Instruct \
    --host 0.0.0.0 \
    --port 8000 \
    --max-model-len 8192 \
    --gpu-memory-utilization 0.85

# 使用4bit量化模型(显存减半)
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen2.5-14B-Instruct-AWQ \
    --host 0.0.0.0 \
    --port 8000 \
    --quantization awq \
    --max-model-len 4096

3.3 用Python调用vLLM

#!/usr/bin/env python3
"""vLLM高性能推理客户端 - 支持批量处理和异步调用"""

import asyncio
import aiohttp
import json
import time
from dataclasses import dataclass
from typing import List

@dataclass
class LLMRequest:
    """LLM请求结构"""
    prompt: str
    max_tokens: int = 512
    temperature: float = 0.7

class VLLMClient:
    """vLLM API客户端,支持并发批量调用"""
    
    def __init__(self, base_url="http://localhost:8000", model="Qwen/Qwen2.5-7B-Instruct"):
        self.base_url = base_url
        self.model = model
    
    async def complete(self, session, prompt, max_tokens=512, temperature=0.7):
        """异步单次调用"""
        payload = {
            "model": self.model,
            "messages": [{"role": "user", "content": prompt}],
            "max_tokens": max_tokens,
            "temperature": temperature,
        }
        
        async with session.post(
            f"{self.base_url}/v1/chat/completions",
            json=payload
        ) as resp:
            result = await resp.json()
            return result["choices"][0]["message"]["content"]
    
    async def batch_complete(self, requests: List[LLMRequest], max_concurrent=10):
        """批量并发调用"""
        semaphore = asyncio.Semaphore(max_concurrent)
        results = []
        
        async def _process(req):
            async with semaphore:
                async with aiohttp.ClientSession() as session:
                    return await self.complete(
                        session, req.prompt,
                        req.max_tokens, req.temperature
                    )
        
        tasks = [_process(req) for req in requests]
        results = await asyncio.gather(*tasks, return_exceptions=True)
        return results
    
    def check_health(self):
        """检查服务状态"""
        import requests
        try:
            resp = requests.get(f"{self.base_url}/v1/models", timeout=5)
            return resp.status_code == 200
        except:
            return False


# ==================== 实战示例 ====================

async def main():
    client = VLLMClient()
    
    # 1. 检查服务
    if not client.check_health():
        print("❌ vLLM服务未启动,请先运行启动命令")
        return
    
    print("✅ vLLM服务正常")
    
    # 2. 单次调用
    async with aiohttp.ClientSession() as session:
        result = await client.complete(
            session,
            "用Python实现一个简单的LRU缓存,要求支持TTL过期",
            max_tokens=1000
        )
        print(f"\n📝 单次调用结果:\n{result}")
    
    # 3. 批量处理——同时处理多个任务
    print("\n" + "=" * 50)
    print("🚀 批量并发测试(5个任务):")
    
    tasks = [
        LLMRequest("写一个Python函数,计算两个日期之间的工作日数量"),
        LLMRequest("用Python实现生产者-消费者模式,使用asyncio"),
        LLMRequest("写一个装饰器,实现API请求的指数退避重试"),
        LLMRequest("用Python解析CSV文件并统计每列的数据分布"),
        LLMRequest("实现一个简单的Markdown解析器,支持标题、粗体和链接"),
    ]
    
    start = time.time()
    results = await client.batch_complete(tasks, max_concurrent=5)
    elapsed = time.time() - start
    
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            print(f"\n❌ 任务{i+1}失败: {result}")
        else:
            print(f"\n✅ 任务{i+1}:")
            print(result[:200] + "..." if len(result) > 200 else result)
    
    print(f"\n⏱️  5个任务总耗时: {elapsed:.2f}s")

if __name__ == "__main__":
    asyncio.run(main())

3.4 vLLM性能优化技巧

#!/usr/bin/env python3
"""vLLM性能调优配置示例"""

import subprocess

# 针对不同场景的启动配置
configs = {
    "high_throughput": {
        "desc": "高吞吐量场景:批量处理、数据标注",
        "args": [
            "--model", "Qwen/Qwen2.5-7B-Instruct",
            "--max-model-len", "4096",      # 缩短上下文,提升并发数
            "--gpu-memory-utilization", "0.92",
            "--max-num-seqs", "64",          # 最大并发序列数
            "--max-num-batched-tokens", "8192",
            "--enforce-eager",               # 禁用CUDA Graph,减少显存占用
        ]
    },
    "low_latency": {
        "desc": "低延迟场景:实时对话、API服务",
        "args": [
            "--model", "Qwen/Qwen2.5-7B-Instruct",
            "--max-model-len", "8192",
            "--gpu-memory-utilization", "0.85",
            "--max-num-seqs", "16",          # 较少并发,降低排队延迟
            "--enable-chunked-prefill",      # 分块预填充,降低首token延迟
        ]
    },
    "memory_efficient": {
        "desc": "省显存场景:显存有限时跑更大模型",
        "args": [
            "--model", "Qwen/Qwen2.5-14B-Instruct-AWQ",
            "--quantization", "awq",
            "--max-model-len", "2048",
            "--gpu-memory-utilization", "0.95",
            "--enforce-eager",
            "--max-num-seqs", "8",
        ]
    }
}

for name, cfg in configs.items():
    print(f"\n{'='*50}")
    print(f"📋 {name}: {cfg['desc']}")
    cmd = "python -m vllm.entrypoints.openai.api_server " + " ".join(cfg["args"])
    print(f"命令: {cmd}")

四、方案三:llama.cpp——纯CPU也能跑

llama.cpp是++实现的LLM推理引擎,最大特点是不需要GPU也能运行,而且支持GGUF量化格式,显存占用极低。

4.1 安装与配置

# 通过Python绑定安装(最简单)
pip install llama-cpp-python

# 如果需要GPU加速(CUDA)
CMAKE_ARGS="-DGGML_CUDA=on" pip install llama-cpp-python --force-reinstall --no-cache-dir

# 如果需要加速(macOS  Silicon)
CMAKE_ARGS="-DGGML_METAL=on" pip install llama-cpp-python --force-reinstall --no-cache-dir

4.2 下载GGUF模型

#!/usr/bin/env python3
"""自动下载GGUF格式的量化模型"""

import os
import requests
from pathlib import Path

MODELS = {
    "qwen2.5-7b-q4": {
        "url": "https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF/resolve/main/qwen2.5-7b-instruct-q4_k_m.gguf",
        "size": "4.7GB",
        "desc": "Qwen2.5-7B 4bit量化,平衡质量和速度"
    },
    "qwen2.5-3b-q4": {
        "url": "https://huggingface.co/Qwen/Qwen2.5-3B-Instruct-GGUF/resolve/main/qwen2.5-3b-instruct-q4_k_m.gguf",
        "size": "2.1GB",
        "desc": "Qwen2.5-3B 4bit量化,CPU友好"
    },
    "llama3.1-8b-q4": {
        "url": "https://huggingface.co/bartowski/Meta-Llama-3.1-8B-Instruct-GGUF/resolve/main/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
        "size": "4.9GB",
        "desc": "Llama 3.1 8B 4bit量化"
    },
    "phi-3.5-mini-q4": {
        "url": "https://huggingface.co/bartowski/Phi-3.5-mini-instruct-GGUF/resolve/main/Phi-3.5-mini-instruct-Q4_K_M.gguf",
        "size": "2.3GB",
        "desc": "微软Phi-3.5 Mini,轻量高效"
    },
}

def download_model(name, target_dir="./models"):
    """下载指定模型"""
    if name not in MODELS:
        print(f"❌ 未知模型: {name}")
        print(f"可用模型: {', '.join(MODELS.keys())}")
        return
    
    info = MODELS[name]
    os.makedirs(target_dir, exist_ok=True)
    
    filename = info["url"].split("/")[-1]
    filepath = os.path.join(target_dir, filename)
    
    if os.path.exists(filepath):
        print(f"✅ 模型已存在: {filepath}")
        return filepath
    
    print(f"📥 下载 {name} ({info['size']})")
    print(f"   {info['desc']}")
    
    resp = requests.get(info["url"], stream=True)
    total = int(resp.headers.get("content-length", 0))
    
    downloaded = 0
    with open(filepath, "wb") as f:
        for chunk in resp.iter_content(chunk_size=8192):
            f.write(chunk)
            downloaded += len(chunk)
            if total > 0:
                pct = downloaded / total * 100
                print(f"\r   进度: {pct:.1f}% ({downloaded/1e9:.2f}GB)", end="")
    
    print(f"\n✅ 下载完成: {filepath}")
    return filepath


if __name__ == "__main__":
    # 下载适合CPU运行的轻量模型
    download_model("qwen2.5-3b-q4")

4.3 用llama-cpp-python进行推理

#!/usr/bin/env python3
"""llama.cpp Python推理——CPU和GPU都支持"""

from llama_cpp import Llama
import time

class LocalLLM:
    """本地LLM推理封装"""
    
    def __init__(self, model_path, n_ctx=4096, n_gpu_layers=-1, n_threads=None):
        """
        初始化模型
        
        Args:
            model_path: GGUF模型文件路径
            n_ctx: 上下文窗口大小
            n_gpu_layers: GPU卸载层数,-1=全部GPU,0=纯CPU
            n_threads: CPU线程数,None=自动检测
        """
        print(f"⏳ 加载模型: {model_path}")
        start = time.time()
        
        self.llm = Llama(
            model_path=model_path,
            n_ctx=n_ctx,
            n_gpu_layers=n_gpu_layers,
            n_threads=n_threads,
            verbose=False
        )
        
        elapsed = time.time() - start
        print(f"✅ 模型加载完成 ({elapsed:.1f}s)")
    
    def generate(self, prompt, system_prompt=None, max_tokens=512, temperature=0.7):
        """生成回复"""
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        messages.append({"role": "user", "content": prompt})
        
        start = time.time()
        output = self.llm.create_chat_completion(
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            top_p=0.9,
        )
        elapsed = time.time() - start
        
        content = output["choices"][0]["message"]["content"]
        tokens = output["usage"]["completion_tokens"]
        tokens_per_sec = tokens / elapsed if elapsed > 0 else 0
        
        return {
            "text": content,
            "tokens": tokens,
            "time_s": elapsed,
            "": tokens_per_sec
        }
    
    def stream_generate(self, prompt, system_prompt=None, max_tokens=512, temperature=0.7):
        """流式生成"""
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        messages.append({"role": "user", "content": prompt})
        
        stream = self.llm.create_chat_completion(
            messages=messages,
            max_tokens=max_tokens,
            temperature=temperature,
            stream=True
        )
        
        full_text = ""
        for chunk in stream:
            delta = chunk["choices"][0].get("delta", {})
            content = delta.get("content", "")
            if content:
                print(content, end="", flush=True)
                full_text += content
        print()
        return full_text


# ==================== 使用示例 ====================

if __name__ == "__main__":
    # 初始化(CPU模式,适合没有GPU的机器)
    llm = LocalLLM(
        model_path="./models/qwen2.5-3b-instruct-q4_k_m.gguf",
        n_ctx=4096,
        n_gpu_layers=0,   # 纯CPU推理
        n_threads=4        # 使用4个CPU线程
    )
    
    # 1. 代码生成
    print("=" * 50)
    print("🔧 代码生成测试:")
    result = llm.generate(
        system_prompt="你是一位Python专家,只输出代码,不要解释",
        prompt="写一个Python函数,实现布隆过滤器(Bloom Filter),支持add和check操作",
        max_tokens=800
    )
    print(result["text"])
    print(f"⏱️  {result['tokens']} tokens, {result['speed']:.1f} tokens/s")
    
    # 2. 流式输出
    print("\n" + "=" * 50)
    print("📡 流式输出测试:")
    llm.stream_generate(
        system_prompt="用中文回答,简洁明了",
        prompt="Python的__slots__有什么用?什么场景下应该使用它?",
        max_tokens=300
    )
    
    # 3. 文本分析
    print("\n" + "=" * 50)
    print("📊 文本分析测试:")
    code_sample = '''
def fibonacci(n):
    if n <= 1:
        return n
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b
'''
    result = llm.generate(
        prompt=f"分析以下代码的时间复杂度和空间复杂度,并提出优化建议:\n```python\n{code_sample}\n```",
        max_tokens=400
    )
    print(result["text"])
    print(f"⏱️  {result['tokens']} tokens, {result['speed']:.1f} tokens/s")

五、构建完整的本地AI应用

光会调用模型还不够,真正的价值在于把LLM集成到实际应用中。下面是一个完整的实战项目:本地AI代码审查助手

5.1 项目架构

#!/usr/bin/env python3
"""
本地AI代码审查助手
功能:自动扫描Git仓库的代码变更,用本地LLM进行审查,生成审查报告

架构:
  Git Diff -> 代码分块 -> LLM审查 -> 结果聚合 -> 报告生成
"""

import subprocess
import json
import os
import re
from dataclasses import dataclass, field
from typing import List, Optional
from datetime import datetime
import requests

@dataclass
class CodeChange:
    """代码变更结构"""
    file_path: str
    change_type: str  # added, modified, deleted
    diff_content: str
    language: str = ""

@dataclass
class ReviewIssue:
    """审查发现的问题"""
    severity: str      # , , suggestion
    file_path: str
    line_hint: str
    description: str
    fix_suggestion: str

@dataclass
class ReviewReport:
    """审查报告"""
    repo: str
    branch: str
    commit: str
    timestamp: str
    issues: List[ReviewIssue] = field(default_factory=list)
    summary: str = ""
    score: int = 0


class CodeReviewer:
    """AI代码审查助手"""
    
    SYSTEM_PROMPT = """你是一个资深代码审查专家。你的任务是审查代码变更,发现潜在问题。

请关注以下方面:
1. **安全性**:SQL注入、、硬编码密钥、不安全的加密
2. **性能**:不必要的循环、内存泄漏、N+1查询
3. **代码质量**:命名规范、代码重复、复杂度过高
4. **Bug风险**:边界条件、异常处理、并发问题
5. **最佳实践**:类型提示、文档字符串、测试覆盖

对每个发现的问题,请用以下JSON格式输出:
```json
{
  "severity": "critical|warning|suggestion",
  "description": "问题描述",
  "line_hint": "大致在哪个位置",
  "fix_suggestion": "修复建议"
}

如果代码没有问题,返回空数组 []。 只输出JSON,不要其他内容。"""

def __init__(self, model_url="http://localhost:11434/api/generate", model="qwen2.5:7b"):
    self.model_url = model_url
    self.model = model

def get_git_diff(self, repo_path, commit="HEAD"):
    """获取Git变更"""
    os.chdir(repo_path)
    
    # 获取变更的文件列表
    result = subprocess.run(
        ["git", "diff", "--name-status", f"{commit}~1", commit],
        capture_output=True, text=True
    )
    
    changes = []
    for line in result.stdout.strip().split('\n'):
        if not line:
            continue
        parts = line.split('\t')
        status = parts[0]
        filepath = parts[1]
        
        # 获取具体diff
        diff_result = subprocess.run(
            ["git", "diff", f"{commit}~1", commit, "--", filepath],
            capture_output=True, text=True
        )
        
        lang = self._detect_language(filepath)
        change_type = {"A": "added", "M": "modified", "D": "deleted"}.get(status[0], "modified")
        
        changes.append(CodeChange(
            file_path=filepath,
            change_type=change_type,
            diff_content=diff_result.stdout,
            language=lang
        ))
    
    return changes

def _detect_language(self, filepath):
    """根据文件扩展名判断语言"""
    ext_map = {
        '.py': 'Python', '.js': '', '.ts': '',
        '.': 'Go', '.rs': '', '.java': 'Java',
        '.cpp': 'C++', '.c': 'C', '.rb': 'Ruby',
        '.php': 'PHP', '.sql': 'SQL', '.sh': 'Shell',
    }
    for ext, lang in ext_map.items():
        if filepath.endswith(ext):
            return lang
    return "Unknown"

def review_change(self, change: CodeChange) -> List[ReviewIssue]:
    """审查单个代码变更"""
    if not change.diff_content.strip():
        return []
    
    # 限制diff大小,避免超出上下文窗口
    diff_text = change.diff_content[:6000]
    
    prompt = f"""审查以下代码变更:

文件: {change.file_path} 语言: {change.language} 变更类型: {change.change_type}

{diff_text}
```"""
        
        try:
            resp = requests.post(self.model_url, json={
                "model": self.model,
                "prompt": prompt,
                "system": self.SYSTEM_PROMPT,
                "stream": False,
                "options": {"temperature": 0.1}
            }, timeout=120)
            
            result = resp.json()
            response_text = result["response"]
            
            # 解析JSON响应
            issues = self._parse_issues(response_text, change.file_path)
            return issues
            
        except Exception as e:
            print(f"⚠️  审查 {change.file_path} 时出错: {e}")
            return []
    
    def _parse_issues(self, text, file_path):
        """从LLM响应中解析问题列表"""
        issues = []
        
        # 提取JSON数组
        json_match = re.(r'\[[\s\S]*?\]', text)
        if not json_match:
            return []
        
        try:
            items = json.loads(json_match.group())
            for item in items:
                issues.append(ReviewIssue(
                    severity=item.get("severity", "suggestion"),
                    file_path=file_path,
                    line_hint=item.get("line_hint", ""),
                    description=item.get("description", ""),
                    fix_suggestion=item.get("fix_suggestion", "")
                ))
        except json.JSONDecodeError:
            pass
        
        return issues
    
    def generate_report(self, repo, branch, commit, issues) -> str:
        """生成Markdown格式的审查报告"""
        critical = [i for i in issues if i.severity == "critical"]
        warnings = [i for i in issues if i.severity == "warning"]
        suggestions = [i for i in issues if i.severity == "suggestion"]
        
        # 计算评分(100分制)
        score = max(0, 100 - len(critical) * 20 - len(warnings) * 5 - len(suggestions) * 1)
        
        severity_icons = {"critical": "🔴", "warning": "🟡", "suggestion": "💡"}
        
        report = f"""# 🔍 代码审查报告

- **仓库**: {repo}
- **分支**: {branch}
- **提交**: {commit[:8]}
- **时间**: {datetime.now().strftime('%Y-%m-%d %H:%M')}
- **评分**: {score}/100

## 📊 统计

| 级别 | 数量 |
|------|------|
| 🔴 严重问题 | {len(critical)} |
| 🟡 警告 | {len(warnings)} |
| 💡 建议 | {len(suggestions)} |

"""
        if not issues:
            report += "✅ **未发现问题,代码质量良好!**\n"
            return report
        
        report += "## 📋 问题详情\n\n"
        
        for severity in ["critical", "warning", "suggestion"]:
            group = [i for i in issues if i.severity == severity]
            if not group:
                continue
            
            report += f"### {severity_icons[severity]} {severity.upper()}\n\n"
            for i, issue in enumerate(group, 1):
                report += f"**{i}. {issue.file_path}** {issue.line_hint}\n\n"
                report += f"> {issue.description}\n\n"
                report += f"**修复建议**: {issue.fix_suggestion}\n\n---\n\n"
        
        return report
    
    def review_repo(self, repo_path, commit="HEAD"):
        """完整审查流程"""
        print(f"🔍 开始审查: {repo_path}")
        
        # 获取变更
        changes = self.get_git_diff(repo_path, commit)
        print(f"📂 发现 {len(changes)} 个文件变更")
        
        # 逐文件审查
        all_issues = []
        for change in changes:
            print(f"  审查: {change.file_path} ({change.language})")
            issues = self.review_change(change)
            all_issues.extend(issues)
            
            if issues:
                for issue in issues:
                    icon = {"critical": "🔴", "warning": "🟡", "suggestion": "💡"}[issue.severity]
                    print(f"    {icon} {issue.description[:60]}")
        
        # 生成报告
        repo_name = os.path.basename(os.path.abspath(repo_path))
        result = subprocess.run(
            ["git", "rev-parse", "--abbrev-ref", "HEAD"],
            capture_output=True, text=True, cwd=repo_path
        )
        branch = result.stdout.strip()
        
        report = self.generate_report(repo_name, branch, commit, all_issues)
        
        # 保存报告
        report_path = os.path.join(repo_path, "REVIEW_REPORT.md")
        with open(report_path, "w") as f:
            f.write(report)
        
        print(f"\n📝 审查报告已保存: {report_path}")
        print(f"📊 发现 {len(all_issues)} 个问题")
        
        return report


# ==================== 使用 ====================

if __name__ == "__main__":
    import sys
    
    repo_path = sys.argv[1] if len(sys.argv) > 1 else "."
    
    reviewer = CodeReviewer(
        model_url="http://localhost:11434/api/generate",
        model="qwen2.5:7b"
    )
    
    report = reviewer.review_repo(repo_path)
    print("\n" + "=" * 60)
    print(report)

5.2 集成到流水线

# ./workflows/ai-review.yml
# 在GitHub Actions中使用本地LLM进行代码审查
name: AI Code Review

on:
  pull_request:
    branches: [main]

jobs:
  ai-review:
    runs-on:   # 需要有GPU的自托管runner
    steps:
      - uses: actions/checkout@
        with:
          fetch-depth: 2
      
      - name: Run AI Review
        run: |
          python scripts/ai_reviewer.py . > review_output.md
          
      - name: Comment on PR
        uses: actions/github-script@v7
        with:
          script: |
            const fs = require('fs');
            const review = fs.readFileSync('review_output.md', 'utf8');
            github.rest.issues.createComment({
              owner: .repo.owner,
              repo: context.repo.repo,
              issue_number: context.issue.number,
              body: review
            });

六、模型微调:让通用模型变成领域专家

6.1 用LoRA进行低成本微调

#!/usr/bin/env python3
"""
使用LoRA微调开源模型,使其精通特定领域
适合在单卡24GB显存上微调7B模型
"""

from datasets import Dataset
from  import AutoTokenizer, AutoModelForCausalLM
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer, SFTConfig
import torch

def prepare_dataset():
    """准备训练数据——以代码审查为例"""
    # 实际项目中应该有几百到几千条高质量数据
    examples = [
        {
            "instruction": "审查以下Python代码的安全问题",
            "input": "query = f\"SELECT * FROM users WHERE name = '{user_input}'\"",
            "output": "🔴 严重安全问题:SQL注入漏洞。\n\n问题:直接将用户输入拼接到SQL语句中,攻击者可以通过构造恶意输入执行任意SQL命令。\n\n修复建议:使用参数化查询\n```python\nquery = \"SELECT * FROM users WHERE name = %s\"\ncursor.execute(query, (user_input,))\n```"
        },
        {
            "instruction": "审查以下Python代码的性能问题",
            "input": "result = []\nfor item in large_list:\n    if item not in another_list:\n        result.append(item)",
            "output": "🟡 性能警告:O(n*m)时间复杂度的列表查找。\n\n问题:`item not in another_list`对列表的查找操作是O(n),嵌套循环导致整体复杂度为O(n*m)。\n\n修复建议:将列表转换为集合\n```python\nanother_set = set(another_list)\nresult = [item for item in large_list if item not in another_set]\n```\n优化后复杂度降为O(n+m)。"
        },
    ]
    
    # 转换为对话格式
    formatted = []
    for ex in examples:
        formatted.append({
            "messages": [
                {"role": "system", "content": "你是一个代码审查专家"},
                {"role": "user", "content": f"{ex['instruction']}\n\n{ex['input']}"},
                {"role": "assistant", "content": ex["output"]}
            ]
        })
    
    return Dataset.from_list(formatted)

def train_lora():
    """LoRA微调"""
    model_name = "Qwen/Qwen2.5-7B-Instruct"
    
    # 加载模型和分词器
    tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
    model = AutoModelForCausalLM.from_pretrained(
        model_name,
        torch_dtype=torch.bfloat16,
        device_map="auto",
        trust_remote_code=True
    )
    
    # LoRA配置
    lora_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=16,                    # LoRA秩,越大拟合能力越强
        lora_alpha=32,           # 缩放系数
        lora_dropout=0.05,
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                       "gate_proj", "up_proj", "down_proj"],
        ="none",
    )
    
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()
    # 输出类似:trainable params: 13,631,488 || all params: 7,628,000,000 || 0.18%
    
    # 训练配置
    training_config = SFTConfig(
        output_dir="./lora-output",
        num_train_epochs=3,
        per_device_train_batch_size=2,
        gradient_accumulation_steps=8,
        learning_rate=2e-4,
        warmup_ratio=0.1,
        logging_steps=10,
        save_strategy="epoch",
        bf16=True,
        max_seq_length=2048,
    )
    
    # 准备数据
    dataset = prepare_dataset()
    
    # 开始训练
    trainer = SFTTrainer(
        model=model,
        args=training_config,
        train_dataset=dataset,
        processing_class=tokenizer,
    )
    
    trainer.train()
    
    # 保存LoRA权重(只保存adapter,通常几十MB)
    model.save_pretrained("./lora-output/final")
    tokenizer.save_pretrained("./lora-output/final")
    print("✅ LoRA微调完成,权重已保存")


if __name__ == "__main__":
    train_lora()

6.2 合并LoRA权重并导出GGUF

# 合并LoRA权重到基础模型
python -c "
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

base = AutoModelForCausalLM.from_pretrained('Qwen/Qwen2.5-7B-Instruct', torch_dtype=torch.float16)
model = PeftModel.from_pretrained(base, './lora-output/final')
model = model.merge_and_unload()
model.save_pretrained('./merged-model')
print('✅ 合并完成')
"

# 转换为GGUF格式(用于llama.cpp/Ollama)
python llama.cpp/convert_hf_to_gguf.py ./merged-model --outtype q4_k_m --outfile ./my-custom-model.gguf

# 在Ollama中注册自定义模型
cat > Modelfile << 'EOF'
FROM ./my-custom-model.gguf
SYSTEM "你是一个专业的代码审查AI,精通Python安全和性能优化"
PARAMETER temperature 0.1
EOF

ollama create code-reviewer -f Modelfile
ollama run code-reviewer

七、性能对比与选型建议

7.1 三大方案对比

特性 Ollama vLLM llama.cpp
安装难度 ⭐ 最简单 ⭐⭐⭐ 需要CUDA ⭐⭐ 中等
GPU推理速度 中等 ⭐⭐⭐ 最快 中等偏上
CPU推理 支持 不支持 ⭐⭐⭐ 最优
并发能力 一般 ⭐⭐⭐ 极强 一般
API兼容性 自有格式 OpenAI兼容 自有格式
量化支持 GGUF AWQ/GPTQ/FP8 GGUF
适用场景 开发测试、个人使用 生产环境、API服务 资源受限、CPU环境
社区活跃度 ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐

7.2 选型决策树

你的需求是什么?
│
├── 个人开发/学习/测试
│   └── 选 Ollama → 最快上手,一条命令搞定
│
├── 生产环境API服务
│   ├── 需要高并发 → 选 vLLM
│   └── 并发要求不高 → 选 Ollama + OpenAI适配层
│
├── 没有GPU / 只有CPU
│   └── 选 llama.cpp → CPU推理性能最优
│
└── 嵌入式/边缘设备
    └── 选 llama.cpp + 超小模型 (1-3B)

八、常见问题排查

8.1 显存不足 (CUDA out of memory)

# 方案1:使用更小的量化模型
# 原始: Qwen2.5-14B-Instruct (FP16, ~28GB)
# 替换: Qwen2.5-14B-Instruct-AWQ (4bit, ~8GB)

# 方案2:减少上下文长度
# --max-model-len 2048  (默认可能是32768)

# 方案3:降低GPU内存占用率
# --gpu-memory-utilization 0.80

8.2 模型输出乱码或重复

# 调整采样参数
params = {
    "temperature": 0.7,       # 太低会重复,太高会乱说
    "top_p": 0.9,             # 核采样
    "top_k": 40,              # 限制候选token数
    "repeat_penalty": 1.1,    # 重复惩罚
    "max_tokens": 1024,       # 防止无限生成
}

8.3 速度太慢

# 检查是否真的在用GPU
nvidia-smi  # 看看GPU利用率

# llama.cpp: 确保编译时开启了CUDA
CMAKE_ARGS="-DGGML_CUDA=on" pip install llama-cpp-python --force-reinstall

# vLLM: 确保启用了连续批处理
# --enable-chunked-prefill --max-num-seqs 32

# 通用: 使用更小的模型或更激进的量化

九、总结与下一步

本地部署LLM已经不再是高门槛的技术挑战。2026年的开源工具链已经非常成熟:

  1. 想最快上手 → 装Ollama,ollama run qwen2.5:7b 一条命令搞定
  2. 要生产级性能 → 用vLLM,支持高并发和低延迟
  3. 没有GPU → 用llama.cpp,CPU也能跑

下一步建议:

  • 结合本文的代码审查助手示例,构建自己的AI辅助开发工具
  • 尝试微调模型,让AI精通你的业务领域
  • 搭建OpenAI兼容的API网关,让团队共用本地模型

本地AI不是未来,是现在。数据安全、零成本、完全可控——这些优势在企业环境中越来越重要。现在就开始部署你的第一个本地LLM吧。


本文代码均已在实际环境中测试通过。如果部署过程中遇到问题,欢迎在评论区留言交流。

常见问题

为什么要本地部署大语言模型?

>为什么要本地部署大语言模型?每次调用GPT-4、Claude这些商业API,你都在为别人的服务器买单。更关键的是,你的数据——商业计划、代码、客户信息——全部经过第三方服务器。 本地部署LLM的核心优势: 数据隐私:所有数据留在本地,不出你的机器 零API成本:一次部署,无限调用,不再按token计费 离线可用:断网也能正常使用 完全可控:模型选择、参数调整、输出格式,全由你说了算 低延迟:省去网络往返,响应速度更快(尤其在GPU加速下) 2026年,开源大模型的质量已经非常接近商业模型。Llama 3.3、Qwen 2.5、DeepSeek V3、Mistral等模型在很多任务上已经达到G

评论