MacBook M4 搭建千问模型 API 服务和 Web 对话界面

在前两篇教程中,我们介绍了用 Ollama、llama.cpp 和 MLX 在 MacBook M4 上运行千问模型的方法。但这些都是命令行交互,如果你想要一个类似 ChatGPT 的 Web 界面,或者想让局域网内的其他设备也能调用千问模型 API,本文将手把手教你搭建完整的 API 服务和 Web 对话界面。

整体架构

我们将搭建以下架构:

+------------------+     +------------------+     +------------------+
|   Open WebUI     |     |  Ollama / API    |     |  Qwen 模型       |
|  (Web 对话界面)  | --> |  (API 服务层)    | --> |  (M4 GPU 推理)  |
|  localhost:8080  |     |  localhost:11434 |     |  Metal 加速      |
+------------------+     +------------------+     +------------------+

技术选型:

  • API 层:Ollama(内置 OpenAI 兼容 API)或 llama.cpp server
  • Web UI:Open WebUI(开源 ChatGPT 替代品,功能强大)
  • 局域网访问:通过 Nginx 反向代理或直接暴露端口

一、使用 Ollama 搭建 API 服务

1. 启动 Ollama 服务

Ollama 启动后默认提供 REST API,端口 11434:

# 启动 Ollama 服务(默认监听 11434)
ollama serve

# 测试 API 是否正常
curl http://localhost:11434/api/version
# 返回: {"name":"ollama","version":"0.5.x"}

2. 调用千问模型 API

Ollama 提供两种 API 格式:原生格式和 OpenAI 兼容格式。

原生 API(流式输出)

curl http://localhost:11434/api/generate -d '{
  "model": "qwen2.5:7b",
  "prompt": "你好,请用Python写一个快速排序",
  "stream": true
}'

OpenAI 兼容 API(推荐)

这个接口完全兼容 OpenAI API 格式,可以直接用 openai SDK 调用:

curl http://localhost:11434/v1/chat/completions -d '{
  "model": "qwen2.5:7b",
  "messages": [
    {"role": "system", "content": "你是一个Python编程助手"},
    {"role": "user", "content": "写一个快速排序"}
  ],
  "stream": false,
  "temperature": 0.7
}'

Python 调用示例

pip install openai

# 使用 OpenAI SDK 调用本地千问模型
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:11434/v1",
    api_key="ollama"  # Ollama 不验证 key,随便填
)

response = client.chat.completions.create(
    model="qwen2.5:7b",
    messages=[
        {"role": "system", "content": "你是一个专业的Python开发工程师"},
        {"role": "user", "content": "请写一个装饰器,用于函数执行耗时统计"}
    ],
    temperature=0.7,
    max_tokens=2000
)

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

3. 开启局域网访问

默认 Ollama 只监听 127.0.0.1,要让局域网其他设备也能访问,需要修改监听地址:

# 方法一:设置环境变量
# 编辑 ~/.zshrc 或 ~/.bash_profile
export OLLAMA_HOST=0.0.0.0:11434

# 重新启动 Ollama
ollama serve

# 方法二:使用 launchd(推荐)
# 创建 ~/Library/LaunchAgents/com.ollama.plist
# 设置 EnvironmentVariables 中的 OLLAMA_HOST

从局域网其他设备测试:

# 将 192.168.1.100 替换为你 MacBook 的 IP
curl http://192.168.1.100:11434/v1/chat/completions -d '{
  "model": "qwen2.5:7b",
  "messages": [{"role": "user", "content": "你好"}]
}'

二、使用 llama.cpp 搭建 API 服务(备选)

如果你使用 llama.cpp,它也内置了 OpenAI 兼容的 API 服务器:

# 启动 API 服务器
./build/bin/llama-server \
  -m ./models/qwen2.5-7b-instruct-q4_k_m.gguf \
  --host 0.0.0.0 \
  --port 8080 \
  --gpu-layers 99 \
  -c 4096

# 服务器启动后访问 http://localhost:8080 即可看到 Web 界面
# API 地址: http://localhost:8080/v1/chat/completions

三、部署 Open WebUI 对话界面

Open WebUI 是一个功能强大的开源 Web 界面,支持多模型切换、对话历史、文档上传、知识库等功能,界面酷似 ChatGPT。

1. Docker 部署(推荐)

# 安装 Docker Desktop for Mac(如果还没有)
# 从 https://docker.com 下载安装

# 一键启动 Open WebUI
docker run -d \
  --name open-webui \
  -p 3000:8080 \
  -e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
  -v open-webui:/app/backend/data \
  --restart always \
  ghcr.io/open-webui/open-webui:main

启动后访问 http://localhost:3000,第一次打开需要注册管理员账号。

2. 非 Docker 部署

如果不想用 Docker,也可以直接用 pip 安装:

# 创建独立虚拟环境
python3 -m venv ~/open-webui-env
source ~/open-webui-env/bin/activate

# 安装 Open WebUI
pip install open-webui

# 启动服务
open-webui --host 0.0.0.0 --port 3000

3. 配置连接 Ollama

打开 Open WebUI 后,进入 设置 → 连接,确保 Ollama 地址正确:

  • Docker 部署:http://host.docker.internal:11434
  • 非 Docker 部署:http://localhost:11434
  • 局域网其他设备:http://你的MacBook_IP:11434

4. 使用 Web 界面

配置完成后,在聊天界面选择 qwen2.5:7b 模型即可开始对话。Open WebUI 支持:

  • 多轮对话和对话历史管理
  • Markdown 渲染和代码高亮
  • 上传文档进行问答(RAG)
  • 多用户管理和权限控制
  • 自定义系统提示词
  • 导出对话记录
  • 多模型对比回答

四、用 FastAPI 搭建自定义 API 服务

如果你需要更多自定义功能(如速率限制、用户认证、日志记录),可以用 FastAPI 封装一层:

pip install fastapi uvicorn

# 创建 server.py
cat > server.py << 'PYEOF'
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
import httpx
import time
from typing import List, Optional

app = FastAPI(title="Qwen API Service")

# 允许跨域
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_methods=["*"],
    allow_headers=["*"],
)

OLLAMA_URL = "http://localhost:11434"

# 请求模型
class Message(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    messages: List[Message]
    model: str = "qwen2.5:7b"
    temperature: float = 0.7
    max_tokens: int = 2000

class ChatResponse(BaseModel):
    response: str
    model: str
    elapsed: float

def log_request(model, messages, elapsed):
    print(f"[{time.strftime('%H:%M:%S')}] model={model} "
          f"msgs={len(messages)} time={elapsed:.2f}s")

@app.post("/v1/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=120) as client:
            resp = await client.post(
                f"{OLLAMA_URL}/api/chat",
                json={
                    "model": req.model,
                    "messages": [m.dict() for m in req.messages],
                    "stream": False,
                    "options": {
                        "temperature": req.temperature,
                        "num_predict": req.max_tokens
                    }
                }
            )
            resp.raise_for_status()
            data = resp.json()
            elapsed = time.time() - start
            log_request(req.model, req.messages, elapsed)
            return ChatResponse(
                response=data["message"]["content"],
                model=req.model,
                elapsed=elapsed
            )
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

@app.get("/v1/models")
async def list_models():
    async with httpx.AsyncClient() as client:
        resp = await client.get(f"{OLLAMA_URL}/api/tags")
        return resp.json()

@app.get("/health")
async def health():
    return {"status": "ok", "service": "qwen-api"}

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

# 启动服务
python server.py

启动后即可通过 http://localhost:8000/docs 查看 API 文档,接口包括:

  • POST /v1/chat:对话接口
  • GET /v1/models:列出可用模型
  • GET /health:健康检查

五、使用 Nginx 反向代理(生产级部署)

如果你希望通过域名或统一端口访问,可以用 Nginx 做反向代理:

# 安装 Nginx
brew install nginx

# 配置文件: /opt/homebrew/etc/nginx/nginx.conf
# 添加 server 配置:

server {
    listen 80;
    server_name localhost;

    # Web UI
    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # API 代理
    location /api/ {
        proxy_pass http://127.0.0.1:11434/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        # SSE 流式输出支持
        proxy_buffering off;
        proxy_cache off;
        proxy_read_timeout 300s;
    }

    # 自定义 API
    location /v1/ {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
    }
}

# 启动 Nginx
nginx

六、性能优化技巧

1. 模型预加载

Ollama 默认在空闲后卸载模型。可以通过 API 保持模型常驻内存:

# 设置模型保持加载(TTL 为 0 表示永不卸载)
curl http://localhost:11434/api/generate -d '{
  "model": "qwen2.5:7b",
  "keep_alive": 0
}'

2. 并发请求处理

# Ollama 默认串行处理请求
# 可以设置并行处理的请求数(会增加内存占用)
export OLLAMA_NUM_PARALLEL=2

# 设置同时加载的模型数
export OLLAMA_MAX_LOADED_MODELS=2

3. 上下文长度优化

更大的上下文窗口意味着更多内存占用和更慢的推理速度。按需设置:

# Ollama 设置上下文窗口(通过 Modelfile)
cat > Modelfile << EOF
FROM qwen2.5:7b
PARAMETER num_ctx 8192
PARAMETER temperature 0.7
PARAMETER top_p 0.9
EOF

# 创建自定义模型
ollama create qwen2.5-8k -f Modelfile

七、实际应用场景

  • 编程助手:在 VS Code 中配置 Continue 插件,连接本地 Ollama API,实现 AI 代码补全
  • 文档问答:通过 Open WebUI 上传 PDF/Word 文档,利用 RAG 功能进行问答
  • 翻译服务:调用 API 实现批量文档翻译,完全离线运行
  • 数据分析:结合 Python 脚本,让千问模型分析 CSV 数据并生成报告
  • 团队协作:局域网内部署,团队成员通过浏览器访问共享 AI 助手

VS Code 配置 Continue 插件

# 在 VS Code 中安装 Continue 扩展
# 编辑 ~/.continue/config.json
{
  "models": [{
    "title": "Qwen2.5 Local",
    "provider": "ollama",
    "model": "qwen2.5-coder:7b",
    "apiBase": "http://localhost:11434"
  }],
  "tabAutocompleteModel": {
    "title": "Qwen Coder",
    "provider": "ollama",
    "model": "qwen2.5-coder:7b"
  }
}

总结

通过本系列三篇教程,你已经学会了在 MacBook M4 上部署千问模型的完整方案:

  • 第一篇:Ollama + llama.cpp 命令行快速部署
  • 第二篇:MLX 框架原生加速运行
  • 第三篇:搭建 API 服务和 Web 对话界面(本文)

MacBook M4 凭借统一内存架构和 Metal 加速,完全可以在本地流畅运行 7B-72B 级别的大模型。相比云端 API,本地部署有三大优势:数据隐私(数据不离开设备)、零成本(无需 API 费用)、离线可用(无需网络)。希望这个系列能帮助你充分利用 MacBook M4 的 AI 能力!

系列文章

发表回复

后才能评论