Ollama: Phase2综合练习
Phase 2 综合练习是从"会用"到"精通"的跃迁——Modelfile 定制、GPU 提速、API 迁移一网打尽。
💡 提示:本综合练习旨在巩固Phase 2的核心技能(Modelfile定制、Python SDK集成、GPU加速、OpenAI兼容API、多模态模型)。建议按照练习1→2→3→4的顺序逐步完成,每个练习都依赖前面练习的知识和产出。完成全部练习后,你将拥有一个完整的SupportBot V2架构规划,为Phase 3的Docker部署和RAG集成打下坚实基础。
📋 前置知识:需要先掌握以下内容
- 第1课:本地AI概念与环境认知
- 第2课:Ollama安装与环境配置
- 第3课:CLI基础交互
- 第4课:模型管理
- 第5课:REST API入门
- 第6课:Phase1综合练习
- 第7课:Python SDK集成
- 第8课:Modelfile自定义模型
- 第9课:GPU与CUDA配置
- 第10课:多模态模型
- 第11课:OpenAI兼容API
1. 你将学到
- Modelfile + Python SDK 联合实战
- CPU vs GPU 推理速度基准测试
- OpenAI 应用迁移到 Ollama 完整流程
- 多模态图片描述微服务实现
- SupportBot V2 架构规划
2. 一个 SaaS 创业者的真实故事
ℹ️ 信息: Phase 2 综合练习的产出是 SupportBot V2 架构规划——这不仅是练习,更是后续 Phase 3(Docker/量化/多模型编排)和 Phase 4(安全/监控/生产部署)的输入。花时间做好规划,后续会事半功倍。
⚠️ 警告: OpenAI 兼容 API 迁移后务必做回归测试——本地模型的行为与 GPT-4 不完全一致,可能出现输出格式偏差、System Prompt 遵循度不同、多轮对话上下文理解差异等问题。
(1) 痛点:原型功能太单一
Alice 的 SupportBot V1 只能做简单文本对话,缺少角色定制、GPU 加速、图片分析、API 兼容。她需要将 Phase 2 学到的所有能力整合,构建更强大的 V2 版本。
(2) 解法:Phase 2 全能力整合
PYTHON
# SupportBot V2 leverages all Phase 2 skills:
# - Custom Modelfile for role specialization
# - GPU acceleration for speed
# - OpenAI-compatible API for easy integration
# - Multimodal for image analysis
3. 练习 1:Modelfile + Python SDK 联合实战
💡 提示: Modelfile + Python SDK 是最佳组合——Modelfile 固化角色和参数,SDK 处理业务逻辑和对话管理。不要在代码中硬编码 System Prompt,让 Modelfile 承担"配置文件"的角色,代码只管调用。
(1) 创建自定义模型并用 SDK 调用
flowchart TD
A[Write Modelfile] --> B[ollama create]
B --> C[Python SDK Import]
C --> D[Build Application]
| 步骤 | 操作 | 关键点 |
|---|---|---|
| 1 | 编写 SupportBot Modelfile | SYSTEM + PARAMETER + MESSAGE |
| 2 | ollama create |
构建自定义模型 |
| 3 | Python 调用 | 无需传 System Prompt |
| 4 | 对比有无 Modelfile 的输出差异 | 角色一致性提升 |
▶ 示例 1: Modelfile + SDK 完整流程
TEXT
📖 仅展示
# supportbot.Modelfile
FROM qwen2.5:7b
SYSTEM You are SupportBot for GlobalShop e-commerce. Be polite, concise (2-3 sentences). For order queries ask for order number. If unsure, say 'Let me connect you with a human agent.'
PARAMETER temperature 0.4
PARAMETER num_ctx 4096
PARAMETER num_predict 256
MESSAGE user What is your return policy?
MESSAGE assistant Our return policy allows returns within 30 days in original condition. Free return shipping included.
PYTHON
import ollama
# Create model (run once)
# ollama.create(model='supportbot', from_='./supportbot.Modelfile')
# Use the custom model - no system prompt needed!
def test_supportbot():
questions = [
"I want to return order #12345",
"Do you ship to Germany?",
"What's the price of the iPhone 15?"
]
for q in questions:
response = ollama.chat(
model='supportbot',
messages=[{'role': 'user', 'content': q}],
stream=False
)
print(f"Q: {q}")
print(f"A: {response['message']['content']}\n")
test_supportbot()
4. 练习 2:CPU vs GPU 基准测试
⚠️ 注意:基准测试时务必先进行2-3次热身调用(丢弃结果),再开始正式计时。首次请求包含模型从磁盘加载到内存的冷启动时间(5-30秒),会严重拉高平均值。同时测试期间不要运行其他GPU密集型任务,确保对比的公平性。
(1) 基准测试维度
| 指标 | CPU 预期 | GPU 预期 | 差异 |
|---|---|---|---|
| 首 Token 延迟 (TTFT) | 2-5s | 0.2-0.5s | 5-10x |
| 生成速度 (tok/s) | 5-10 | 30-60 | 5-10x |
| 总延迟 (200 tokens) | 20-40s | 3-7s | 5-10x |
| 内存占用 | RAM | VRAM | 不同类型 |
▶ 示例 2: CPU vs GPU 对比脚本
PYTHON
import ollama
import time
from statistics import mean
def benchmark(model: str, prompt: str, runs: int = 5) -> dict:
"""Benchmark model inference speed."""
latencies = []
token_speeds = []
# Warm up (first call includes model loading)
ollama.chat(model=model, messages=[{'role': 'user', 'content': 'warm up'}],
stream=False)
for _ in range(runs):
start = time.time()
response = ollama.chat(
model=model,
messages=[{'role': 'user', 'content': prompt}],
stream=False
)
elapsed = time.time() - start
# Estimate tokens (rough: ~4 chars per token for English)
content = response['message']['content']
token_count = len(content) // 4
latencies.append(elapsed)
token_speeds.append(token_count / elapsed if elapsed > 0 else 0)
return {
"model": model,
"avg_latency_s": round(mean(latencies), 2),
"avg_tokens_per_s": round(mean(token_speeds), 1),
"runs": runs
}
# Run benchmark
print("=== CPU vs GPU Benchmark ===")
result = benchmark("qwen2.5", "Explain machine learning in 100 words")
print(f"Model: {result['model']}")
print(f"Avg latency: {result['avg_latency_s']}s")
print(f"Avg speed: {result['avg_tokens_per_s']} tokens/s")
# Compare different models
for model in ["llama3.2:3b", "qwen2.5", "mistral"]:
result = benchmark(model, "Explain AI in 50 words", runs=3)
print(f"{model}: {result['avg_tokens_per_s']} tok/s, {result['avg_latency_s']}s")
输出:
TEXT
📖 仅展示
=== CPU vs GPU Benchmark ===
5. 练习 3:OpenAI 应用迁移
(1) 迁移流程
flowchart TD
A[Original OpenAI App] --> B{Identify API calls}
B --> C[Change base_url to Ollama]
C --> D[Change model name]
D --> E[Test each feature]
E --> F{All features work?}
F -->|Yes| G[Migration Complete]
F -->|No| H[Fix incompatibilities]
H --> E
| 检查步骤 | 操作 | 验证 |
|---|---|---|
| 1 | 改 base_url | curl localhost:11434/v1/models 返回模型列表 |
| 2 | 改 model 名 | qwen2.5 能正常响应 |
| 3 | 测试流式输出 | stream=True 正常工作 |
| 4 | 测试 JSON 模式 | response_format 正常工作 |
| 5 | 测试 Embeddings | nomic-embed-text 正常工作 |
| 6 | 测试 Function Calling | 如不支持,改用 JSON 模式替代 |
▶ 示例 3: 完整迁移实战
PYTHON
from openai import OpenAI
import json
# Before migration
# client = OpenAI(api_key="sk-xxx")
# model = "gpt-4"
# After migration - only these 3 lines changed
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
model = "qwen2.5"
embed_model = "nomic-embed-text"
# Test 1: Chat
print("=== Test 1: Chat ===")
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Hello, who are you?"}],
temperature=0.3
)
print(response.choices[0].message.content)
# Test 2: Streaming
print("\n=== Test 2: Streaming ===")
stream = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "Count from 1 to 5"}],
stream=True
)
for chunk in stream:
if chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="")
print()
# Test 3: JSON mode
print("\n=== Test 3: JSON Mode ===")
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": "List 3 colors as JSON array"}],
response_format={"type": "json_object"},
temperature=0.1
)
print(response.choices[0].message.content)
# Test 4: Embeddings
print("\n=== Test 4: Embeddings ===")
response = client.embeddings.create(model=embed_model, input="Hello world")
print(f"Embedding dimension: {len(response.data[0].embedding)}")
print("\n=== All tests passed! ===")
输出:
TEXT
📖 仅展示
=== Test 1: Chat ===
\n=== Test 2: Streaming ===
\n=== Test 3: JSON Mode ===
\n=== Test 4: Embeddings ===
\n=== All tests passed! ===
6. 多模态挑战:图片描述微服务
(1) 微服务架构
flowchart TD
A[HTTP Request<br/>image + prompt] --> B[FastAPI Endpoint]
B --> C[Base64 Encode Image]
C --> D[Ollama llava Model]
D --> E[Image Description]
E --> F[JSON Response]
▶ 示例 4: FastAPI 图片描述微服务
PYTHON
from fastapi import FastAPI, UploadFile, File, Form
from fastapi.responses import JSONResponse
import ollama
import base64
app = FastAPI(title="Image Description Service")
@app.post("/describe")
async def describe_image(
file: UploadFile = File(...),
prompt: str = Form("Describe this image in detail")
):
"""Describe an uploaded image using llava."""
image_data = base64.b64encode(await file.read()).decode("utf-8")
response = ollama.chat(
model="llava",
messages=[{
"role": "user",
"content": prompt,
"images": [image_data]
}],
stream=False,
options={"temperature": 0.3}
)
return JSONResponse({
"description": response["message"]["content"],
"model": "llava",
"filename": file.filename
})
@app.post("/extract-text")
async def extract_text(file: UploadFile = File(...)):
"""OCR-like text extraction from image."""
image_data = base64.b64encode(await file.read()).decode("utf-8")
response = ollama.chat(
model="llava",
messages=[{
"role": "user",
"content": "Extract all visible text from this image verbatim.",
"images": [image_data]
}],
stream=False,
options={"temperature": 0.1}
)
return JSONResponse({
"text": response["message"]["content"],
"model": "llava"
})
# Run: uvicorn service:app --host 0.0.0.0 --port 8000
输出:
TEXT
📖 仅展示
# 函数定义成功
▶ 示例 5: SupportBot V2 架构规划
PYTHON
# SupportBot V2 Architecture Blueprint
SUPPORTBOT_V2_SPEC = {
"name": "SupportBot V2",
"components": {
"custom_model": {
"tool": "Modelfile",
"purpose": "Role-specialized customer service model",
"config": "qwen2.5:7b + custom SYSTEM + PARAMETER"
},
"api_gateway": {
"tool": "OpenAI compatible API",
"purpose": "Easy integration with existing tools",
"config": "/v1/chat/completions endpoint"
},
"image_service": {
"tool": "llava multimodal",
"purpose": "Damage report analysis, product photo description",
"config": "FastAPI + llava + Base64 encoding"
},
"embedding_service": {
"tool": "nomic-embed-text",
"purpose": "Product knowledge base (RAG ready for Phase 3)",
"config": "/v1/embeddings endpoint"
},
"gpu_acceleration": {
"tool": "CUDA / Metal",
"purpose": "Fast inference for real-time responses",
"config": "Auto-detected by Ollama"
}
},
"cost_comparison": {
"before": "GPT-4 API: ~2,000 USD/month",
"after": "Local Ollama: ~100 USD/month (hardware + electricity)",
"saving": "1,900 USD/month (95% reduction)"
}
}
print("SupportBot V2 Architecture:")
for name, spec in SUPPORTBOT_V2_SPEC["components"].items():
print(f" {name}: {spec['tool']} - {spec['purpose']}")
输出:
TEXT
📖 仅展示
SupportBot V2 Architecture:
7. 综合示例:Phase 2 全技能验证
PYTHON
# ============================================
# Comprehensive: Phase 2 full skills verification
# Modelfile + SDK + GPU + OpenAI compat + Multimodal
# ============================================
import ollama
from openai import OpenAI
import base64
import time
import json
from pathlib import Path
def phase2_verify():
results = {}
# Test 1: Custom Modelfile model
print("=== Test 1: Custom Modelfile Model ===")
try:
resp = ollama.chat(
model="supportbot",
messages=[{"role": "user", "content": "I want a refund for order #12345"}],
stream=False
)
results["modelfile"] = "PASS"
print(f" Response: {resp['message']['content'][:80]}...")
except Exception as e:
results["modelfile"] = f"FAIL: {e}"
# Test 2: GPU acceleration
print("\n=== Test 2: GPU Acceleration ===")
start = time.time()
resp = ollama.chat(
model="qwen2.5",
messages=[{"role": "user", "content": "Hello"}],
stream=False
)
elapsed = time.time() - start
results["gpu_speed"] = f"{elapsed:.2f}s"
print(f" Response time: {elapsed:.2f}s ({'GPU' if elapsed < 3 else 'CPU'})")
# Test 3: OpenAI compatible API
print("\n=== Test 3: OpenAI Compatible API ===")
try:
client = OpenAI(base_url="http://localhost:11434/v1", api_key="ollama")
resp = client.chat.completions.create(
model="qwen2.5",
messages=[{"role": "user", "content": "Say OK"}],
temperature=0.1
)
results["openai_compat"] = "PASS"
print(f" Response: {resp.choices[0].message.content}")
except Exception as e:
results["openai_compat"] = f"FAIL: {e}"
# Test 4: Embeddings
print("\n=== Test 4: Embeddings ===")
try:
resp = client.embeddings.create(model="nomic-embed-text", input="test")
dim = len(resp.data[0].embedding)
results["embeddings"] = f"PASS ({dim}d)"
print(f" Embedding dimension: {dim}")
except Exception as e:
results["embeddings"] = f"FAIL: {e}"
# Test 5: Streaming
print("\n=== Test 5: Streaming ===")
try:
stream = client.chat.completions.create(
model="qwen2.5",
messages=[{"role": "user", "content": "Count 1 to 3"}],
stream=True
)
chunks = 0
for chunk in stream:
if chunk.choices[0].delta.content:
chunks += 1
results["streaming"] = f"PASS ({chunks} chunks)"
print(f" Received {chunks} chunks")
except Exception as e:
results["streaming"] = f"FAIL: {e}"
# Summary
print("\n" + "=" * 40)
print("Phase 2 Verification Results:")
for test, status in results.items():
print(f" {test}: {status}")
passed = sum(1 for v in results.values() if "PASS" in str(v))
print(f"\n Total: {passed}/{len(results)} passed")
if passed >= 4:
print(" ✅ Ready for Phase 3!")
else:
print(" ⚠️ Review failed tests.")
if __name__ == "__main__":
phase2_verify()
❓ 常见问题
Q Modelfile 创建的模型在 Python SDK 中找不到怎么办?
A 确保 成功(无报错)。用 确认模型存在。Python SDK 调用的 model 名必须与 时指定的一致。
Q 基准测试结果差异很大怎么办?
A 首次调用含模型加载时间(冷启动)。先做 2 次热身调用再测。测试期间不要运行其他程序。取 5 次以上平均值。
Q OpenAI 迁移后某些功能异常怎么办?
A 逐一排查——先测基础 Chat,再测 Streaming,再测 JSON 模式。每个功能独立验证,定位具体不兼容点。
Q llava 模型还没拉取怎么办?
A 练习 4 需要先 。如果磁盘不足,可以先跳过练习 4,Phase 3 的 RAG 不需要多模态。
Q SupportBot V2 和 V1 的核心区别是什么?
A V1 是 Python 脚本级别的封装;V2 集成了 Modelfile 角色定制、OpenAI 兼容 API 网关、多模态图片分析,是微服务架构。
Q Phase 3 需要什么预备知识?
A LangChain 基础(Prompt Template、Chain、Agent)、RAG 概念(文档加载、向量检索)、Docker 基础。Lesson 13-18 将逐一讲解。
📖 小节
- Modelfile + Python SDK 组合让自定义模型在代码中零配置使用
- 基准测试需热身 + 多次平均,CPU vs GPU 差异 5-10 倍
- OpenAI 迁移只需改 base_url/api_key/model 三处
- 多模态微服务用 FastAPI + Base64 编码 + llava 实现
- SupportBot V2 整合 Modelfile + OpenAI 兼容 + 多模态 + 嵌入
- Phase 2 全技能验证 5 项测试通过即可进入 Phase 3
📝 作业
- 基础题(难度⭐):创建一个 SupportBot Modelfile,用 Python SDK 调用并测试 3 种不同问题,记录角色一致性。
- 进阶题(难度⭐⭐):运行 CPU vs GPU 基准测试,对比 3 个不同模型的速度,输出一份性能报告表格。
- 挑战题(难度⭐⭐⭐):完成 SupportBot V2 的完整实现——Modelfile 角色定制 + OpenAI 兼容 API + 多模态图片分析(可选),并编写迁移文档。