第 44 章

Ollama 本地部署与 API 封装

第44章 Ollama 本地部署与 API 封装

导语

Ollama 是 2024-2026 年本地 LLM 部署的首选工具,没有之一。它把复杂的模型下载、量化格式转换、GPU 驱动配置全部抽象掉,让你用一行命令就能运行 Hermes 70B。本章从零开始,覆盖三大平台的安装、Hermes 模型的拉取与配置、REST API 的完整使用,以及与 Hermes Agent 集成的生产级配置。


44.1 Ollama 安装

macOS 安装

# 方式一:官网下载安装包(推荐)
# 访问 https://ollama.com/download 下载 .pkg 文件

# 方式二:Homebrew
brew install ollama

# 验证安装
ollama --version
# ollama version 0.5.x

# 启动服务(macOS 默认自动启动)
ollama serve

# 查看服务状态
curl http://localhost:11434/api/version

Linux 安装

# 一行安装脚本(支持 Ubuntu 20.04+, Debian 11+, CentOS 8+)
curl -fsSL https://ollama.com/install.sh | sh

# 脚本会自动:
# 1. 检测 CUDA 版本
# 2. 安装 CUDA 兼容库(如未安装)
# 3. 配置 systemd 服务
# 4. 创建 ollama 用户

# 验证 GPU 支持
ollama serve &
curl http://localhost:11434/api/tags
nvidia-smi  # 应看到 ollama 进程占用显存

# 配置开机自启
sudo systemctl enable ollama
sudo systemctl start ollama
sudo systemctl status ollama

# 查看日志
journalctl -u ollama -f

Windows 安装

# 方式一:下载 OllamaSetup.exe(推荐)
# https://ollama.com/download/windows

# 方式二:winget
winget install Ollama.Ollama

# 配置 WSL2 + CUDA(如果需要 GPU 加速)
# 确保已安装 WSL2 和 NVIDIA WSL2 驱动

# PowerShell 中启动服务
ollama serve

# 验证
Invoke-WebRequest -Uri "http://localhost:11434/api/version"

CUDA 版本兼容性

CUDA 版本 驱动最低版本 Ollama 版本 支持状态
CUDA 12.4 550.xx 0.4.x+ 完全支持
CUDA 12.1 530.xx 0.3.x+ 支持
CUDA 11.8 520.xx 0.2.x+ 支持(性能略低)
CUDA 11.4 470.xx 0.1.x 有限支持

44.2 拉取 Hermes 模型

官方 Modelfile 和拉取命令

# Hermes 2 Pro(7B,最快,适合日常任务)
ollama pull nous-hermes2-pro:7b

# Hermes 2(7B,平衡版本)
ollama pull nous-hermes2:7b

# Hermes 2 Mixtral(8x7B MoE,性能大幅提升)
ollama pull nous-hermes2:mixtral-8x7b

# Hermes 3(推荐,最新架构)
ollama pull nous-hermes3:8b
ollama pull nous-hermes3:70b

# Hermes 4(最新,需要较大显存)
# 注意:需要在 Ollama Hub 或手动导入

# 查看本地已有模型
ollama list

# 删除模型
ollama rm nous-hermes2:7b

# 查看模型详情
ollama show nous-hermes3:70b

从 GGUF 文件手动导入 Hermes 4

当 Ollama Hub 尚未收录最新 Hermes 版本时,使用 Modelfile 手动导入:

# 1. 下载 GGUF 文件(从 Hugging Face)
# https://huggingface.co/NousResearch/Hermes-4-70B-GGUF
wget https://huggingface.co/NousResearch/Hermes-4-70B-GGUF/resolve/main/hermes-4-70b-q4_k_m.gguf

# 2. 创建 Modelfile
cat > Modelfile << 'EOF'
FROM ./hermes-4-70b-q4_k_m.gguf

# 系统提示词(Hermes 特定格式)
SYSTEM """You are Hermes, an AI assistant. Be helpful, harmless, and honest."""

# 参数配置
PARAMETER stop "<|im_end|>"
PARAMETER stop "<|im_start|>"
PARAMETER num_ctx 65536
PARAMETER num_gpu -1
PARAMETER temperature 0.1
PARAMETER top_p 0.9
PARAMETER repeat_penalty 1.1

# 消息模板(Hermes ChatML 格式)
TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}{{ range .Messages }}<|im_start|>{{ .Role }}
{{ .Content }}<|im_end|>
{{ end }}<|im_start|>assistant
"""
EOF

# 3. 导入模型
ollama create hermes-4-70b:q4 -f Modelfile

# 4. 验证
ollama run hermes-4-70b:q4 "你好,请介绍一下你自己。"

不同量化格式的选择

# 速度优先(牺牲少量质量)
ollama pull nous-hermes3:70b-q4_0

# 质量与速度平衡(推荐)
# Q4_K_M 使用 K-quant 方法,质量优于纯 Q4
ollama create hermes4-q4km:70b -f Modelfile-Q4KM

# 质量优先(需要更多显存)
ollama create hermes4-q8:70b -f Modelfile-Q8

# 检查模型文件大小
du -sh ~/.ollama/models/

44.3 配置文件详解

Ollama 配置文件位置

# macOS / Linux
~/.ollama/config.json     # 全局配置(如果存在)

# 环境变量配置(推荐,优先级更高)
# 在 systemd service 中设置:
# /etc/systemd/system/ollama.service.d/override.conf

# 或 ~/.bashrc / ~/.zshrc
export OLLAMA_HOST=0.0.0.0:11434     # 监听地址(默认 127.0.0.1)
export OLLAMA_MODELS=~/.ollama/models # 模型存储路径
export OLLAMA_MAX_LOADED_MODELS=1    # 最大同时加载模型数
export OLLAMA_NUM_PARALLEL=4         # 并行请求处理数
export OLLAMA_MAX_QUEUE=512          # 请求队列最大长度
export OLLAMA_KEEP_ALIVE=5m          # 模型空闲卸载时间

完整的 systemd 服务配置(Linux 生产环境)

# /etc/systemd/system/ollama.service.d/override.conf
[Service]
# GPU 配置
Environment="CUDA_VISIBLE_DEVICES=0,1"     # 使用 GPU 0 和 1
Environment="OLLAMA_NUM_PARALLEL=4"         # 4 个并行请求
Environment="OLLAMA_MAX_LOADED_MODELS=2"   # 最多同时加载 2 个模型
Environment="OLLAMA_KEEP_ALIVE=10m"        # 10 分钟后卸载空闲模型

# 网络配置
Environment="OLLAMA_HOST=0.0.0.0:11434"    # 允许外部访问

# 存储配置
Environment="OLLAMA_MODELS=/data/ollama/models"  # 大容量存储路径

# 性能配置
Environment="OLLAMA_MAX_QUEUE=256"
# 应用配置
sudo systemctl daemon-reload
sudo systemctl restart ollama

Modelfile 关键参数解析

# Modelfile 完整参数说明

# ── 模型来源 ────────────────────────────────────────────
FROM ./hermes-4-70b-q4_k_m.gguf
# 或 FROM hub.ollama.com/nous-hermes3:70b

# ── GPU 配置 ────────────────────────────────────────────
PARAMETER num_gpu -1          # -1 = 自动使用所有 GPU
                              # 0  = CPU 推理
                              # N  = 使用 N 层到 GPU

# ── 上下文配置 ──────────────────────────────────────────
PARAMETER num_ctx 65536       # 上下文窗口大小(tokens)
                              # 注意:越大 VRAM 占用越多
PARAMETER num_batch 512       # 预填充批大小(影响首 token 速度)

# ── 生成参数 ────────────────────────────────────────────
PARAMETER temperature 0.1     # 0 = 确定性,1 = 随机
PARAMETER top_p 0.9           # nucleus sampling
PARAMETER top_k 40            # top-k sampling
PARAMETER repeat_penalty 1.1  # 重复惩罚
PARAMETER num_predict -1      # -1 = 无限制
PARAMETER seed -1             # -1 = 随机种子

# ── 停止词 ──────────────────────────────────────────────
PARAMETER stop "<|im_end|>"
PARAMETER stop "<|im_start|>"
PARAMETER stop "<|endoftext|>"

# ── 性能优化 ────────────────────────────────────────────
PARAMETER num_thread 8        # CPU 线程数(CPU 推理时)
PARAMETER use_mmap true       # 内存映射(节省内存)
PARAMETER use_mlock false     # 内存锁定(防换页,需 root)

44.4 REST API 使用示例

API 端点总览

端点 方法 描述
/api/generate POST 文本生成(非对话)
/api/chat POST 对话接口(带历史)
/api/embeddings POST 向量嵌入
/api/tags GET 本地模型列表
/api/show POST 模型详情
/api/pull POST 拉取模型
/api/delete DELETE 删除模型
/api/ps GET 运行中的模型
/api/version GET Ollama 版本

基础 Generate 调用

# 简单生成(curl)
curl http://localhost:11434/api/generate \
    -H "Content-Type: application/json" \
    -d '{
      "model": "nous-hermes3:70b",
      "prompt": "用一句话解释什么是量子计算",
      "stream": false
    }'

# 流式输出
curl http://localhost:11434/api/generate \
    -H "Content-Type: application/json" \
    -d '{
      "model": "nous-hermes3:70b",
      "prompt": "写一首关于 AI 的短诗",
      "stream": true
    }'

对话接口(Chat API)

# chat_example.py
import httpx
import json
import asyncio

async def chat_with_hermes():
    """完整的多轮对话示例"""
    
    messages = []  # 对话历史
    
    async with httpx.AsyncClient(timeout=120) as client:
        while True:
            user_input = input("\n你: ").strip()
            if user_input.lower() in ["exit", "quit", "退出"]:
                break
            
            messages.append({"role": "user", "content": user_input})
            
            # 流式输出
            print("\nHermes: ", end="", flush=True)
            full_response = ""
            
            async with client.stream(
                "POST",
                "http://localhost:11434/api/chat",
                json={
                    "model": "nous-hermes3:70b",
                    "messages": messages,
                    "stream": True,
                    "options": {
                        "num_ctx": 65536,
                        "temperature": 0.1,
                        "num_predict": 2048,
                    }
                }
            ) as response:
                async for line in response.aiter_lines():
                    if line:
                        data = json.loads(line)
                        content = data.get("message", {}).get("content", "")
                        if content:
                            print(content, end="", flush=True)
                            full_response += content
                        
                        if data.get("done", False):
                            # 输出使用统计
                            print(f"\n[{data.get('eval_count', 0)} tokens, "
                                  f"{data.get('eval_duration', 0)//1e6:.0f}ms]")
            
            messages.append({"role": "assistant", "content": full_response})

asyncio.run(chat_with_hermes())

嵌入向量 API

# embeddings_example.py
import httpx
import json

async def get_embeddings(texts: list[str]) -> list[list[float]]:
    """获取文本嵌入向量,用于 RAG、语义搜索等场景"""
    embeddings = []
    
    async with httpx.AsyncClient(timeout=60) as client:
        for text in texts:
            response = await client.post(
                "http://localhost:11434/api/embeddings",
                json={
                    "model": "nous-hermes3:70b",  # 也可用专门的嵌入模型如 nomic-embed-text
                    "prompt": text
                }
            )
            data = response.json()
            embeddings.append(data["embedding"])
    
    return embeddings

# 计算余弦相似度
import numpy as np

def cosine_similarity(a: list[float], b: list[float]) -> float:
    a, b = np.array(a), np.array(b)
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

44.5 与 Hermes Agent 集成的完整配置

Hermes Agent 配置文件

# hermes_agent_config.yaml
model:
  provider: ollama
  base_url: "http://localhost:11434"
  model_name: "nous-hermes3:70b"
  
  # 推理参数
  inference:
    temperature: 0.1          # Agent 任务推荐低温度
    top_p: 0.9
    max_tokens: 4096
    context_window: 65536
    stream: true

  # 连接配置
  connection:
    timeout: 120              # 秒
    max_retries: 3
    retry_delay: 2            # 秒

# Agent 行为配置
agent:
  max_iterations: 20          # 最大思考步骤
  verbose: true
  
  # 工具调用配置
  tools:
    enabled: true
    format: "chatml"          # Hermes 使用 ChatML 格式的工具调用
    max_tool_calls_per_turn: 5

# MCP 服务器配置(Agent 可访问的工具)
mcp_servers:
  filesystem:
    command: "uvx"
    args: ["mcp-server-filesystem", "/workspace"]
  
  database:
    command: "python"
    args: ["-m", "mcp_server_sqlite", "--db-path", "/data/knowledge.db"]

Python 集成代码

# hermes_agent_integration.py
"""
完整的 Hermes Agent + Ollama 集成示例。
实现工具调用、多步推理、错误恢复。
"""

import httpx
import json
import asyncio
from typing import Optional, Callable
from dataclasses import dataclass

@dataclass
class OllamaConfig:
    base_url: str = "http://localhost:11434"
    model: str = "nous-hermes3:70b"
    temperature: float = 0.1
    max_tokens: int = 4096
    context_window: int = 65536
    timeout: int = 120

class HermesOllamaAgent:
    """
    基于 Ollama 的 Hermes Agent 实现。
    支持工具调用、流式输出、错误恢复。
    """
    
    def __init__(self, config: OllamaConfig, tools: list = None):
        self.config = config
        self.tools = tools or []
        self.conversation_history = []
        self.client = httpx.AsyncClient(
            base_url=config.base_url,
            timeout=httpx.Timeout(config.timeout)
        )
    
    async def chat(
        self,
        user_message: str,
        system_prompt: Optional[str] = None,
        on_token: Optional[Callable] = None
    ) -> str:
        """
        发送消息并获取 Hermes 响应。
        
        Args:
            user_message: 用户消息
            system_prompt: 可选的系统提示词
            on_token: 流式 token 回调函数
        """
        # 构建消息列表
        messages = []
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})
        
        messages.extend(self.conversation_history)
        messages.append({"role": "user", "content": user_message})
        
        # 调用 Ollama API
        payload = {
            "model": self.config.model,
            "messages": messages,
            "stream": on_token is not None,
            "tools": self.tools if self.tools else None,
            "options": {
                "temperature": self.config.temperature,
                "num_predict": self.config.max_tokens,
                "num_ctx": self.config.context_window,
            }
        }
        
        # 过滤 None 值
        payload = {k: v for k, v in payload.items() if v is not None}
        
        response_content = ""
        tool_calls = []
        
        if on_token:
            # 流式响应
            async with self.client.stream("POST", "/api/chat", json=payload) as response:
                async for line in response.aiter_lines():
                    if line:
                        data = json.loads(line)
                        msg = data.get("message", {})
                        content = msg.get("content", "")
                        
                        if content:
                            response_content += content
                            on_token(content)
                        
                        # 处理工具调用
                        if msg.get("tool_calls"):
                            tool_calls.extend(msg["tool_calls"])
        else:
            # 非流式响应
            response = await self.client.post("/api/chat", json=payload)
            response.raise_for_status()
            data = response.json()
            msg = data.get("message", {})
            response_content = msg.get("content", "")
            tool_calls = msg.get("tool_calls", [])
        
        # 更新对话历史
        self.conversation_history.append({"role": "user", "content": user_message})
        self.conversation_history.append({"role": "assistant", "content": response_content})
        
        # 处理工具调用
        if tool_calls:
            response_content = await self._handle_tool_calls(tool_calls)
        
        return response_content
    
    async def _handle_tool_calls(self, tool_calls: list) -> str:
        """处理 Hermes 的工具调用请求"""
        results = []
        
        for tool_call in tool_calls:
            tool_name = tool_call.get("function", {}).get("name")
            tool_args = tool_call.get("function", {}).get("arguments", {})
            
            # 查找对应工具
            tool = next((t for t in self.tools if t["name"] == tool_name), None)
            if not tool:
                results.append(f"工具 '{tool_name}' 不存在")
                continue
            
            try:
                # 执行工具(这里需要实际的工具执行逻辑)
                result = await self._execute_tool(tool_name, tool_args)
                results.append(f"工具 {tool_name} 执行结果: {result}")
            except Exception as e:
                results.append(f"工具 {tool_name} 执行失败: {str(e)}")
        
        # 将工具结果反馈给 Hermes
        tool_result_message = "\n".join(results)
        return await self.chat(
            f"工具执行结果:\n{tool_result_message}\n\n请根据这些结果给出最终回答。"
        )
    
    async def _execute_tool(self, name: str, args: dict) -> str:
        """实际工具执行(由具体应用实现)"""
        raise NotImplementedError(f"工具 '{name}' 的执行逻辑未实现")
    
    def clear_history(self):
        """清除对话历史"""
        self.conversation_history = []
    
    async def close(self):
        """关闭 HTTP 客户端"""
        await self.client.aclose()


# 使用示例
async def main():
    config = OllamaConfig(
        model="nous-hermes3:70b",
        temperature=0.1,
        context_window=65536
    )
    
    agent = HermesOllamaAgent(config)
    
    system_prompt = """你是一个专业的代码分析助手。
    用中文回答,回答要简洁专业,直指关键。"""
    
    # 流式输出回调
    def on_token(token: str):
        print(token, end="", flush=True)
    
    print("Hermes Agent 已启动(输入 exit 退出)\n")
    
    while True:
        user_input = input("你: ").strip()
        if user_input.lower() == "exit":
            break
        
        print("Hermes: ", end="")
        response = await agent.chat(user_input, system_prompt, on_token)
        print()  # 换行
    
    await agent.close()

asyncio.run(main())

44.6 性能调优参数

关键性能参数对比

参数 默认值 调优方向 效果
num_ctx 2048 按需增大(最大受VRAM限制) 更长上下文理解
num_gpu -1 保持默认 自动最大化 GPU 利用
num_batch 512 增大(256-2048) 提升 prefill 速度
num_thread 物理核数 等于物理核数 CPU 部分性能
use_mmap true 保持 true 节省内存
use_mlock false 大内存机器设为 true 防止内存换页
f16_kv true 保持 true(Q8 设为 false) KV Cache 精度

性能测试脚本

#!/bin/bash
# benchmark_ollama.sh — Ollama 性能基准测试

MODEL="nous-hermes3:70b"
PROMPT="请写一篇关于机器学习的500字介绍文章。"

echo "=== Ollama 性能基准测试 ==="
echo "模型: $MODEL"
echo ""

# 测试 1: 首次请求(冷启动)
echo "--- 冷启动测试 ---"
time curl -s http://localhost:11434/api/generate \
    -d "{\"model\": \"$MODEL\", \"prompt\": \"$PROMPT\", \"stream\": false}" \
    | python3 -c "
import sys, json
data = json.load(sys.stdin)
print(f'生成 tokens: {data[\"eval_count\"]}')
print(f'推理速度: {data[\"eval_count\"] / (data[\"eval_duration\"] / 1e9):.1f} tokens/sec')
print(f'首 token 延迟: {data[\"prompt_eval_duration\"] / 1e6:.0f} ms')
"

echo ""
echo "--- 并发压测(4并发,20请求)---"
# 使用 ab 测试并发
for i in $(seq 1 4); do
    curl -s -o /dev/null -w "%{time_total}\n" \
        -X POST http://localhost:11434/api/generate \
        -H "Content-Type: application/json" \
        -d "{\"model\": \"$MODEL\", \"prompt\": \"一句话总结量子计算\", \"stream\": false}" &
done
wait
echo "并发测试完成"

num_ctx 对性能的影响

# ctx_benchmark.py
import httpx
import time
import asyncio

async def benchmark_ctx(ctx_sizes: list[int]):
    """测试不同 context 大小对推理速度的影响"""
    
    prompt = "解释什么是神经网络" * 100  # ~500 tokens
    
    async with httpx.AsyncClient(timeout=300) as client:
        for ctx in ctx_sizes:
            start = time.time()
            
            response = await client.post(
                "http://localhost:11434/api/generate",
                json={
                    "model": "nous-hermes3:70b",
                    "prompt": prompt[:200],  # 固定输入长度
                    "stream": False,
                    "options": {
                        "num_ctx": ctx,
                        "num_predict": 100,  # 固定输出长度
                    }
                }
            )
            
            data = response.json()
            elapsed = time.time() - start
            eval_tps = data['eval_count'] / (data['eval_duration'] / 1e9)
            
            print(f"num_ctx={ctx:6d}: {eval_tps:.1f} tokens/sec, "
                  f"首token={data['prompt_eval_duration']/1e6:.0f}ms, "
                  f"总耗时={elapsed:.1f}s")

asyncio.run(benchmark_ctx([4096, 8192, 16384, 32768, 65536]))

典型输出:

num_ctx=  4096: 28.3 tokens/sec, 首token=450ms, 总耗时=4.2s
num_ctx=  8192: 26.1 tokens/sec, 首token=890ms, 总耗时=4.6s
num_ctx= 16384: 22.8 tokens/sec, 首token=1780ms, 总耗时=5.3s
num_ctx= 32768: 18.5 tokens/sec, 首token=3560ms, 总耗时=6.8s
num_ctx= 65536: 12.1 tokens/sec, 首token=7120ms, 总耗时=10.3s

本章小结

Ollama 是 Hermes 本地部署的最低阻力路径:

  1. 安装:三平台均一行命令搞定,自动处理 GPU 驱动兼容性
  2. 模型管理ollama pull 拉取已有模型,Modelfile 导入自定义 GGUF
  3. 核心 API/api/chat(对话)和 /api/generate(生成)覆盖 95% 场景
  4. 性能关键参数num_ctx(上下文)和 num_gpu(GPU 层数)是最重要的两个
  5. 与 Hermes Agent 集成:通过配置文件指定 provider: ollama 即可无缝对接

最佳实践:开发环境用 Ollama(易用),生产环境高并发场景换 vLLM(见第45章)。

思考题

  1. Ollama 的 KEEP_ALIVE 参数控制模型在内存中保持加载的时间。如果你的服务器有 80GB 显存但需要在 Hermes 70B 和另一个 7B 模型之间切换,如何设置 KEEP_ALIVE 以最小化用户等待时间?

  2. 当 Ollama 的 num_ctx 设置为 65536 时,首 token 延迟会显著增加(见基准测试)。这是什么原因导致的?有没有办法在保持大上下文支持的同时减少首 token 延迟?

  3. 在上面的 HermesOllamaAgent 代码中,对话历史 conversation_history 会随对话增长。当历史长度超过 num_ctx 限制时,应该怎么处理?设计一个自动截断策略。

本章评分
4.8  / 5  (3 评分)

💬 留言讨论