第 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 本地部署的最低阻力路径:
- 安装:三平台均一行命令搞定,自动处理 GPU 驱动兼容性
- 模型管理:
ollama pull拉取已有模型,Modelfile 导入自定义 GGUF - 核心 API:
/api/chat(对话)和/api/generate(生成)覆盖 95% 场景 - 性能关键参数:
num_ctx(上下文)和num_gpu(GPU 层数)是最重要的两个 - 与 Hermes Agent 集成:通过配置文件指定
provider: ollama即可无缝对接
最佳实践:开发环境用 Ollama(易用),生产环境高并发场景换 vLLM(见第45章)。
思考题
-
Ollama 的
KEEP_ALIVE参数控制模型在内存中保持加载的时间。如果你的服务器有 80GB 显存但需要在 Hermes 70B 和另一个 7B 模型之间切换,如何设置 KEEP_ALIVE 以最小化用户等待时间? -
当 Ollama 的
num_ctx设置为 65536 时,首 token 延迟会显著增加(见基准测试)。这是什么原因导致的?有没有办法在保持大上下文支持的同时减少首 token 延迟? -
在上面的
HermesOllamaAgent代码中,对话历史conversation_history会随对话增长。当历史长度超过num_ctx限制时,应该怎么处理?设计一个自动截断策略。