本地部署大模型完全指南④:API服务化与远程访问

第①~③篇完成了本地模型和知识库的搭建。现在我们要做的是——把它变成真正的服务,供团队和外部系统使用。

前言:为什么要API服务化?

本地模型跑在终端里自娱自乐有什么用?真正的价值在于让别人也能用

  • 团队协作:前端、后端、产品经理都能调用同一个模型
  • 系统集成:集成到公司现有的IM、文档系统、CI/CD流水线
  • 移动端访问:手机、平板通过API调用本地模型
  • 弹性扩展:从单机到多机集群,API接口不变

一、Ollama原生API详解

Ollama本身就内置了HTTP API,部署后默认监听 11434 端口。

1.1 API速查表

端点 方法 功能
/api/generate POST 文本生成(单轮)
/api/chat POST 对话(多轮+工具调用)
/api/embeddings POST 文本向量化
/api/tags GET 列出所有模型
/api/show POST 查看模型详情
/api/pull POST 下载模型
/api/push POST 上传模型
/api/delete DELETE 删除模型
/api/copy POST 复制模型
/api/version GET 查看版本

1.2 流式与非流式调用

非流式(等待完整响应):

import requests
import json

def generate(prompt: str):
    response = requests.post("http://localhost:11434/api/generate", json={
        "model": "deepseek-r1:7b",
        "prompt": prompt,
        "stream": False,
        "options": {
            "temperature": 0.7,
            "num_predict": 1024
        }
    })
    return response.json()["response"]

流式(逐token返回,体验更好):

def generate_stream(prompt: str):
    response = requests.post("http://localhost:11434/api/generate", json={
        "model": "deepseek-r1:7b",
        "prompt": prompt,
        "stream": True  # 开启流式
    }, stream=True)
    
    for line in response.iter_lines():
        if line:
            chunk = json.loads(line)
            if "response" in chunk:
                yield chunk["response"]

# 使用
for token in generate_stream("用Python写一个冒泡排序"):
    print(token, end="", flush=True)

1.3 多轮对话API

def chat(messages: list):
    """Ollama对话API"""
    response = requests.post("http://localhost:11434/api/chat", json={
        "model": "deepseek-r1:7b",
        "messages": messages,
        "stream": False
    })
    return response.json()["message"]["content"]

# 多轮对话示例
history = [
    {"role": "system", "content": "你是一个专业的Python开发助手"},
    {"role": "user", "content": "Python的装饰器是什么?"},
]

# 第一轮
reply1 = chat(history)
print(f"AI: {reply1}")

# 第二轮(带上历史)
history.append({"role": "assistant", "content": reply1})
history.append({"role": "user", "content": "给我举个例子"})

reply2 = chat(history)
print(f"AI: {reply2}")

二、构建API网关

直接用Ollama的API虽然能用,但缺乏权限控制、限流、日志等企业级功能。我们需要一个API网关。

2.1 用Nginx做反向代理+鉴权

# /etc/nginx/conf.d/ollama-gateway.conf

upstream ollama_backend {
    server localhost:11434;
    keepalive 64;
}

server {
    listen 8080;
    server_name ai-api.company.com;
    
    # 启用HTTPS(生产环境必配)
    # listen 443 ssl;
    # ssl_certificate /etc/nginx/certs/ai-api.crt;
    # ssl_certificate_key /etc/nginx/certs/ai-api.key;
    
    # 请求体大小限制
    client_max_body_size 100m;
    
    # 速率限制(每个IP每分钟30次请求)
    limit_req_zone $binary_remote_addr zone=ollama:10m rate=30r/m;
    
    location / {
        limit_req zone=ollama burst=5 nodelay;
        
        # API Key鉴权
        auth_request /auth;
        auth_request_set $auth_status $upstream_status;
        
        proxy_pass http://ollama_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # 流式响应支持
        proxy_set_header Connection '';
        proxy_http_version 1.1;
        chunked_transfer_encoding on;
        proxy_buffering off;
    }
    
    # 鉴权端点
    location = /auth {
        internal;
        proxy_pass http://127.0.0.1:8081/auth;
        proxy_pass_request_body off;
        proxy_set_header Content-Length "";
        proxy_set_header X-Original-URI $request_uri;
    }
}

2.2 Python鉴权服务

# auth_server.py — API Key鉴权服务
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
import hmac

# 预设的API Keys(实际使用应存储在数据库)
VALID_KEYS = {
    "sk-prod-abc123": {"user": "admin", "role": "admin"},
    "sk-prod-def456": {"user": "developer", "role": "user"},
    "sk-dev-xyz789": {"user": "tester", "role": "user", "rate_limit": 10},
}

class AuthHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        # 从Header中提取API Key
        api_key = self.headers.get("X-API-Key", "")
        
        if api_key in VALID_KEYS:
            user_info = VALID_KEYS[api_key]
            self.send_response(200)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps(user_info).encode())
        else:
            self.send_response(401)
            self.send_header("Content-Type", "application/json")
            self.end_headers()
            self.wfile.write(json.dumps({"error": "Invalid API Key"}).encode())
    
    def log_message(self, format, *args):
        """静默日志"""
        pass

if __name__ == "__main__":
    server = HTTPServer(("127.0.0.1", 8081), AuthHandler)
    print("Auth server running on port 8081...")
    server.serve_forever()

2.3 完整的API调用示例

# 通过网关调用(需要API Key)
curl -X POST http://localhost:8080/api/generate \
  -H "X-API-Key: sk-prod-abc123" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "deepseek-r1:7b",
    "prompt": "用Python写一个HTTP服务器",
    "stream": false
  }'

三、FastAPI封装的模型服务

Nginx方案适合简单的反向代理。如果你需要更多的控制逻辑(如多模型路由、负载均衡、使用统计),建议用FastAPI封装一层。

3.1 完整的模型服务

# model_service.py — 完整的模型API服务
from fastapi import FastAPI, HTTPException, Header
from pydantic import BaseModel
from typing import Optional, List
import requests
import time
import json
from datetime import datetime

app = FastAPI(title="Local LLM API Service", version="1.0.0")

# 配置
OLLAMA_BASE = "http://localhost:11434"
API_KEYS = {"sk-123": "admin", "sk-456": "user"}

# 使用统计
usage_stats = {"total_requests": 0, "total_tokens": 0}

# 请求模型
class GenerateRequest(BaseModel):
    model: str = "deepseek-r1:7b"
    prompt: str
    system: Optional[str] = None
    temperature: float = 0.7
    max_tokens: int = 1024
    stream: bool = False

class ChatRequest(BaseModel):
    model: str = "deepseek-r1:7b"
    messages: list
    temperature: float = 0.7
    stream: bool = False

class EmbeddingRequest(BaseModel):
    model: str = "bge-m3"
    input: str

async def verify_api_key(x_api_key: str = Header(None)):
    """验证API Key"""
    if x_api_key not in API_KEYS:
        raise HTTPException(status_code=401, detail="Invalid API Key")
    return API_KEYS[x_api_key]

@app.post("/v1/generate")
async def generate(req: GenerateRequest, user: str = None):
    """文本生成接口"""
    start = time.time()
    
    # 构造Ollama请求
    ollama_request = {
        "model": req.model,
        "prompt": req.prompt,
        "stream": False,
        "options": {
            "temperature": req.temperature,
            "num_predict": req.max_tokens
        }
    }
    
    if req.system:
        ollama_request["system"] = req.system
    
    # 调用Ollama
    response = requests.post(
        f"{OLLAMA_BASE}/api/generate",
        json=ollama_request
    )
    
    if response.status_code != 200:
        raise HTTPException(status_code=500, detail="Model inference failed")
    
    data = response.json()
    
    # 更新统计
    elapsed = time.time() - start
    usage_stats["total_requests"] += 1
    usage_stats["total_tokens"] += data.get("eval_count", 0)
    
    return {
        "response": data["response"],
        "model": req.model,
        "usage": {
            "total_tokens": data.get("eval_count", 0),
            "inference_time_ms": int(elapsed * 1000)
        }
    }

@app.post("/v1/chat")
async def chat(req: ChatRequest):
    """多轮对话接口"""
    ollama_request = {
        "model": req.model,
        "messages": req.messages,
        "stream": False,
        "options": {
            "temperature": req.temperature
        }
    }
    
    response = requests.post(
        f"{OLLAMA_BASE}/api/chat",
        json=ollama_request
    )
    
    if response.status_code != 200:
        raise HTTPException(status_code=500, detail="Chat failed")
    
    data = response.json()
    return {
        "response": data["message"]["content"],
        "model": req.model
    }

@app.post("/v1/embeddings")
async def embeddings(req: EmbeddingRequest):
    """文本向量化接口"""
    response = requests.post(
        f"{OLLAMA_BASE}/api/embeddings",
        json={
            "model": req.model,
            "prompt": req.input
        }
    )
    
    if response.status_code != 200:
        raise HTTPException(status_code=500, detail="Embedding failed")
    
    data = response.json()
    return {
        "embedding": data["embedding"],
        "dimensions": len(data["embedding"])
    }

@app.get("/v1/models")
async def list_models():
    """列出可用模型"""
    response = requests.get(f"{OLLAMA_BASE}/api/tags")
    models = response.json().get("models", [])
    
    return {
        "models": [
            {
                "name": m["name"],
                "size_gb": round(m["size"] / (1024**3), 2),
                "modified_at": m.get("modified_at", "")
            }
            for m in models
        ]
    }

@app.get("/v1/stats")
async def get_stats():
    """使用统计"""
    return usage_stats

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

3.2 启动服务

# 安装依赖
pip install fastapi uvicorn requests

# 启动(开发环境)
python model_service.py

# 启动(生产环境,多进程)
uvicorn model_service:app --host 0.0.0.0 --port 8000 --workers 4

# 生产环境建议用systemd管理

3.3 客户端库

# llm_client.py — 封装好的客户端
import requests
from typing import Optional, Generator

class LocalLLMClient:
    """本地大模型API客户端"""
    
    def __init__(self, base_url: str = "http://localhost:8000", 
                 api_key: str = "sk-123"):
        self.base_url = base_url
        self.headers = {"X-API-Key": api_key}
    
    def generate(self, prompt: str, model: str = "deepseek-r1:7b",
                 temperature: float = 0.7, max_tokens: int = 1024) -> str:
        """文本生成"""
        response = requests.post(
            f"{self.base_url}/v1/generate",
            headers=self.headers,
            json={
                "model": model,
                "prompt": prompt,
                "temperature": temperature,
                "max_tokens": max_tokens,
                "stream": False
            }
        )
        return response.json()["response"]
    
    def chat(self, messages: list, model: str = "deepseek-r1:7b") -> str:
        """多轮对话"""
        response = requests.post(
            f"{self.base_url}/v1/chat",
            headers=self.headers,
            json={"model": model, "messages": messages}
        )
        return response.json()["response"]
    
    def list_models(self) -> list:
        """列出可用模型"""
        response = requests.get(
            f"{self.base_url}/v1/models",
            headers=self.headers
        )
        return response.json()["models"]

# 使用
client = LocalLLMClient()
print(client.generate("用Python写一个文件监控工具"))

四、内网穿透:让外网访问你的模型

本地部署的最大限制是——只能在局域网用。如何让外网也能访问?

4.1 方案一:frp内网穿透(推荐)

# frpc.ini (内网机)
[common]
server_addr = your-server.com
server_port = 7000

[ollama-api]
type = tcp
local_ip = 127.0.0.1
local_port = 8000
remote_port = 8000

[open-webui]
type = tcp
local_ip = 127.0.0.1
local_port = 3000
remote_port = 3000
# frps.ini (公网服务器)
[common]
bind_port = 7000
vhost_http_port = 8080

4.2 方案二:Tailscale(零配置VPN)

# 安装Tailscale
# https://tailscale.com/download

# 所有设备登录同一账号后,自动组网
tailscale up

# 获取虚拟IP
tailscale ip

# 其他设备通过虚拟IP访问:http://100.x.x.x:8000

4.3 方案三:Cloudflare Tunnel(免费HTTPS)

# 安装cloudflared
# https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/

cloudflared tunnel create my-llm-tunnel
cloudflared tunnel route dns my-llm-tunnel ai-api.your-domain.com

# 创建配置文件
cat > ~/.cloudflared/config.yml << EOF
tunnel: my-llm-tunnel
credentials-file: /root/.cloudflared/my-llm-tunnel.json

ingress:
  - hostname: ai-api.your-domain.com
    service: http://localhost:8000
  - service: http_status:404
EOF

# 启动
cloudflared tunnel run my-llm-tunnel

五、安全加固建议

5.1 必做的安全措施

措施 说明 紧急程度
API Key认证 所有请求必须携带有效Key ⭐⭐⭐
HTTPS加密 防止中间人攻击 ⭐⭐⭐
速率限制 防止滥用 ⭐⭐⭐
请求审计日志 记录谁在什么时候调用了什么 ⭐⭐
内容过滤 防止生成违规内容 ⭐⭐
白名单IP 限制来源IP ⭐⭐

5.2 日志审计

# 简单审计日志中间件
import logging
from datetime import datetime

logging.basicConfig(
    filename="api_audit.log",
    level=logging.INFO,
    format="%(asctime)s | %(message)s"
)

async def audit_middleware(request, call_next):
    """审计日志"""
    start = time.time()
    
    # 记录请求
    api_key = request.headers.get("x-api-key", "unknown")
    path = request.url.path
    client_ip = request.client.host
    
    # 处理请求
    response = await call_next(request)
    
    # 记录结果
    elapsed = time.time() - start
    logging.info(
        f"{client_ip} | {api_key[:8]}... | {path} | "
        f"{response.status_code} | {elapsed:.2f}s"
    )
    
    return response

总结

至此,你的本地大模型已经从"个人玩具"升级为"企业级服务"——有API鉴权、有网关、有内网穿透、有日志审计。团队所有人都能用,也能集成到现有系统中。

下一篇预告:第⑤篇《搭建美观的对话界面(Open WebUI)》—— 给你的AI服务加上专业级的前端界面。


需要完整脚本和配置文件的同学,可以看我主页的付费资源专栏。

有问题欢迎评论区留言,大家一起讨论!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐