把本地大模型跑通,做成一个能随时对话的助手,这件事听起来复杂,但用 FastAPI 加 Ollama,一个晚上就能搭出原型。我最近刚给团队做完这套东西,从装模型到写后端接口再到内网访问,整个过程踩了不少坑,今天把能直接照抄的方案和配置都整理出来。
这套组合解决的事情很明确:第一,数据不出本地,对话记录和模型文件都在自己机器上,适合内部知识库、隐私敏感场景;第二,不依赖外部 API,断网也能用;第三,FastAPI 作为后端层,把 Ollama 的 HTTP 接口包一层,可以自己做鉴权、上下文管理、对接前端。不管你是想给团队做个私有问答工具,还是纯粹想玩明白大模型的应用开发,这篇文章都能给你一条走通的路。
1. 为什么这么组合:FastAPI + Ollama 的选型逻辑
1.1 Ollama 是什么,解决了什么问题
Ollama 本质上是一个大模型运行时管理器。它把模型下载、加载、推理、API 暴露这几个环节都封装好了,让你不用去手动处理 Python 的 Torch 环境、CUDA 版本、模型权重文件格式这些极其容易翻车的底层问题。
我最初试着直接跑 Transformers 加载模型,光是把 CUDA 和 cuDNN 的版本对齐就折腾了两天,后来换成 Ollama,五分钟内就把一个 7B 模型跑起来了。Ollama 对模型文件做了统一的目录管理,通过一条命令就能换模型、删模型、看模型列表,完全不需要关心权重文件散落在哪。
Ollama 自带 HTTP API,默认监听 11434 端口,支持/api/generate和/api/chat两个核心接口。这意味着它天然适合作为后端服务暴露给上层应用调用,你不需要写一行模型推理代码。模型加载后常驻内存,后续请求的响应速度很快,不会有冷启动的等待时间。
1.2 FastAPI 在这里扮演什么角色
直接拿 Ollama 的 API 给前端用,不是不行,但会有几个实际痛点。一是没有鉴权,局域网内谁拿到端口就能调用,模型会被别人白嫖,甚至被塞垃圾 prompt。二是无法做多轮会话管理,Ollama 的 chat 接口虽然能传历史消息,但历史消息的存储、截断策略你得自己实现。三是返回格式需要统一,前端可能想要一个固定的 JSON 结构或者 SSE 流式格式。
FastAPI 的价值就在这里:它作为胶水层,把 Ollama 的裸接口包装成业务接口。FastAPI 基于 ASGI,天生支持异步,而流式输出恰恰需要异步支持——在响应用户请求的同时,持续从 Ollama 那边读取数据块并转发给前端。另外 FastAPI 自带 OpenAPI 文档,调试接口非常方便,不需要额外装 Postman 之类的工具。
1.3 整体架构与请求流转
整套系统的数据流是这样的:用户在前端页面输入问题,浏览器把请求发送到 FastAPI 服务的/api/chat接口,FastAPI 解析请求后,把对话历史拼接好,通过 AsyncClient 调用 Ollama 的/api/chat接口,并开启流式模式。Ollama 返回的是一串分块的流数据,FastAPI 逐块接收,再以流式方式转发给浏览器,浏览器逐步渲染出文字。
用文字描述一下这个链路:前端页面 → FastAPI 后端(端口 8000) → Ollama 服务(端口 11434) → 本地 GPU/CPU 推理。中间每一层都能独立替换,比如把 Ollama 换成 vLLM,或者把前端从 HTML 升级成 React 应用,后端接口不用大改。
2. 准备工作与模型获取:本地部署的第一步
2.1 安装 Ollama:三个平台的快速上手
Ollama 官方提供了 Windows、macOS、Linux 三个平台的安装方式,不需要编译源码,直接装就行。
Windows 用户去官网下载安装包,双击安装后,Ollama 会自动注册为后台服务,托盘图标能看到运行状态。安装完成后打开 PowerShell,执行ollama --version验证安装是否成功。
macOS 用户更简单,有 Homebrew 的话一条命令:brew install ollama。装完后用ollama serve启动服务端,再开一个终端窗口执行模型相关命令。也可以直接下载官方 .zip 包解压运行。
Linux 服务器用户用官方脚本安装,执行下面这行:
curl -fsSL https://ollama.com/install.sh | sh脚本会自动完成环境变量配置和 systemd 服务注册,装完直接systemctl start ollama就能用。数据库、缓存、模型文件的默认存储位置在~/.ollama/models,可以通过设置OLLAMA_MODELS环境变量来修改这个路径,我建议在一开始就把它改到剩余空间大的磁盘分区,因为模型文件动不动就是几个 GB 起。
2.2 怎么选模型:参数规模与硬件匹配
这是最多人问我的一个问题:我的机器能跑多大的模型?我给你一个可以照抄的参考表,基于我的实际测试经验和官方推荐值:
| 模型参数规模 | 量化版本 | 最低内存/显存 | 可用设备 | 体验感受 |
|---|---|---|---|---|
| 0.5B ~ 1.8B | Q4_K_M | 2GB | 老笔记本 CPU | 响应极快,但智能水平有限 |
| 3B ~ 4B | Q4_K_M | 4GB | 中端笔记本 | 对话流畅,适合简单问答 |
| 7B ~ 8B | Q4_K_M | 8GB | 主流显卡/大内存机器 | 综合体验最佳,通用任务够用 |
| 13B ~ 14B | Q4_K_M | 16GB | 高端显卡 | 逻辑能力明显提升 |
| 32B ~ 33B | Q4_K_M | 24GB ~ 32GB | 多卡/专业卡 | 接近在线中档模型水平 |
| 70B+ | Q4_K_M | 48GB+ | 服务器级配置 | 不建议个人尝试 |
Ollama 的模型命名规则里会有标签,比如qwen2.5:7b、llama3.1:8b、deepseek-r1:7b,标签最后面的数字就是参数量。默认会下载最新版本,你可以指定具体标签来固定版本。
我的建议是,第一次跑,优先选qwen2.5:7b或deepseek-r1:7b这类中文能力强的模型,然后在“能跑通”和“效果好”之间找平衡。8GB 显存以下老老实实选 7B 及以下,不要硬上 14B,加载后大概率 OOM,反而浪费时间调参。
2.3 模型下载慢的解决方案:离线导入全流程
Ollama 拉取模型默认从官方仓库下载,在大陆网络环境下经常卡在半路。我试过挂一整晚,早上起来发现进度条纹丝不动,心态直接崩了。
后来我找到一条非常稳的路:从国内的合规模型社区下载 GGUF 格式的模型文件,然后通过ollama create命令导入。这里我用的是 ModelScope(魔搭社区),它提供阿里系镜像节点,下载速度稳定,而且是国内合规可用的平台。
具体步骤分四步:
第一步,在 ModelScope 搜索你需要的模型的 GGUF 版本。比如找Qwen/Qwen2.5-7B-Instruct-GGUF,下载qwen2.5-7b-instruct-q4_k_m.gguf这个文件(注意选 Q4_K_M 量化,兼顾体积和效果)。
第二步,把下载好的.gguf文件放到一个专门目录,比如D:\models\qwen2.5-7b\。
第三步,写一个Modelfile文件,内容就一行核心指令:
FROM ./qwen2.5-7b-instruct-q4_k_m.gguf如果要加对话模板,可以在后面补 SYSTEM 语句,但大多数 GGUF 文件里已经集成了模板,直接按上面这行就行。
第四步,在终端执行模型导入命令:
ollama create qwen2.5-7b-local -f ./Modelfile完成后运行ollama list,看到qwen2.5-7b-local就说明导入成功了。之后ollama run qwen2.5-7b-local就能直接在命令行里对话。这个方法完全绕开了官方仓库下载,速度取决于你的宽带质量,实测比直连拉模型快一个量级。
3. FastAPI 后端服务:从零搭建对话接口
3.1 项目目录结构参考
我习惯用模块化的方式组织 FastAPI 项目,这样后续加功能不会越改越乱。这是我现在在用的目录结构,你可以直接照抄:
fastapi-ollama-assistant/ ├── app/ │ ├── __init__.py │ ├── main.py # FastAPI 应用入口,注册路由 │ ├── config.py # 全局配置(Ollama 地址、模型名等) │ ├── models/ │ │ ├── __init__.py │ │ └── chat.py # Pydantic 数据模型(请求/响应结构) │ ├── routers/ │ │ ├── __init__.py │ │ └── chat.py # /api/chat 相关路由 │ └── services/ │ ├── __init__.py │ └── ollama_client.py # Ollama API 的异步封装 ├── static/ │ └── index.html # 简易对话前端 ├── requirements.txt └── .env # 环境变量配置config.py用 Pydantic 的BaseSettings读取 .env,把 Ollama 地址和模型名都放到配置里,不硬编码。routers/按业务模块拆路由文件,避免 main.py 变成大杂烩。services/放业务逻辑,比如调用 Ollama、拼历史消息、做流式转发。static/放前端页面,FastAPI 可以挂载静态目录,开发阶段一个服务搞定前后端。
3.2 核心配置与 Ollama 客户端封装
先写配置文件app/config.py:
from pydantic_settings import BaseSettings class Settings(BaseSettings): ollama_base_url: str = "http://127.0.0.1:11434" model_name: str = "qwen2.5-7b-local" system_prompt: str = "你是一个智能助手,请用简洁专业的语言回答用户问题。" class Config: env_file = ".env" settings = Settings()然后封装 Ollama 的异步客户端。为什么用httpx.AsyncClient不用requests?因为 FastAPI 是异步框架,如果用同步的 requests 库,遇到慢请求会阻塞整个事件循环,并发一上来,接口就卡死了。所以必须用异步客户端。
import httpx from app.config import settings class OllamaClient: def __init__(self): self.base_url = settings.ollama_base_url self.timeout = httpx.Timeout(connect=10.0, read=300.0, write=60.0, pool=10.0) async def chat_stream(self, messages: list[dict]): payload = { "model": settings.model_name, "messages": messages, "stream": True } async with httpx.AsyncClient(timeout=self.timeout) as client: async with client.stream( "POST", f"{self.base_url}/api/chat", json=payload ) as response: response.raise_for_status() async for line in response.aiter_lines(): if line.strip(): yield line这里有个要点:read=300.0这个超时时间很重要。大模型生成长文本可能超过一分钟,如果超时设短了,生成过程中连接被断开,你会看到前端流到一半就停了,看起来像是模型出了问题,其实是超时配置的问题。
client.stream()配合response.aiter_lines()可以拿到 Ollama 返回的每一行 JSON,每行对应一个生成片段,效率很高。
还有一个关键点:OllamaClient不应该每次请求都重新创建AsyncClient,建议把它变成单例或者用 FastAPI 的Depends机制复用连接池。上面的写法是为了思路清晰,实际生产你可以把 AsyncClient 放在__init__里,只建一次。
3.3 实现流式对话接口
对话接口是整个服务的核心。用户请求进来,我们调用 Ollama 的流式接口,把生成的数据流逐段转发给前端。
路由文件app/routers/chat.py:
import json from fastapi import APIRouter, Depends from fastapi.responses import StreamingResponse from app.models.chat import ChatRequest from app.services.ollama_client import OllamaClient router = APIRouter(prefix="/api/chat", tags=["chat"]) async def get_client(): return OllamaClient() @router.post("/") async def chat(req: ChatRequest, client: OllamaClient = Depends(get_client)): messages = [{"role": "system", "content": settings.system_prompt}] messages += [{"role": m.role, "content": m.content} for m in req.messages] async def generate(): async for line in client.chat_stream(messages): data = json.loads(line) if data.get("done"): break chunk = data.get("message", {}).get("content", "") if chunk: yield f"data: {json.dumps({'content': chunk}, ensure_ascii=False)}\n\n" return StreamingResponse( generate(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "X-Accel-Buffering": "no"} )这里有几个值得注意的设计决策:
- 使用 SSE(Server-Sent Events)格式,
data: ... \n\n是 SSE 的标准协议格式。前端可以用EventSource或者 fetch 流式解析。如果单纯返回text/plain,拿到的是一坨拼接好的文字,无法逐字渲染。 X-Accel-Buffering: no是给 Nginx 反向代理时用的,告诉 Nginx 不要缓冲这个响应,否则流式效果会变成一次性吐全部内容。- 用
Depends(get_client)注入客户端,方便后续做 Mock 测试。
请求体用 Pydantic 模型定义:
from pydantic import BaseModel from typing import List, Literal class ChatMessage(BaseModel): role: Literal["system", "user", "assistant"] content: str class ChatRequest(BaseModel): messages: List[ChatMessage] stream: bool = True3.4 CORS 与访问安全控制
如果你打算让局域网内其他设备访问这个服务,必须处理跨域问题。浏览器会拦截跨域的 JavaScript 请求,所以要在 FastAPI 应用里配置 CORS 中间件。
入口文件app/main.py:
from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from app.routers import chat app = FastAPI(title="本地 AI 对话助手") app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"], ) app.include_router(chat.router) app.mount("/", StaticFiles(directory="static", html=True), name="static")生产环境里,allow_origins建议把*改成你前端页面的实际地址,避免任何网站都能往你的本地模型发请求。如果服务部署在公网,一定要在更上层加一层鉴权(比如简单 Token 验证),不然你训练好的模型会被别人拿来白嫖。
4. 前端对话界面:让助手真正能用
4.1 极简 HTML 页面实现流式输出
后端接口通了,前端需要一个能展示逐字打字效果的页面。我用一个单文件 HTML 实现,不需要任何构建工具,浏览器直接打开就能用。
核心是用fetch的ReadableStream解析 SSE 格式的数据:
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>本地 AI 助手</title> <style> body { font-family: "Microsoft YaHei", sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; } #chat-box { border: 1px solid #ddd; height: 500px; overflow-y: auto; padding: 16px; border-radius: 8px; } .user-msg, .ai-msg { margin: 8px 0; padding: 10px; border-radius: 6px; } .user-msg { background: #e3f2fd; text-align: right; } .ai-msg { background: #f5f5f5; } #input-area { display: flex; gap: 10px; margin-top: 12px; } #input { flex: 1; padding: 8px; font-size: 14px; } button { padding: 8px 20px; } </style> </head> <body> <h2>本地 AI 对话助手</h2> <div id="chat-box"></div> <div id="input-area"> <input id="input" placeholder="输入你的问题..." /> <button onclick="sendMessage()">发送</button> </div> <script> let history = []; async function sendMessage() { const input = document.getElementById('input'); const text = input.value.trim(); if (!text) return; input.value = ''; history.push({ role: 'user', content: text }); appendMessage('user', text); appendMessage('ai', ''); const aiMsgElement = document.querySelector('.ai-msg:last-child'); let answer = ''; try { const response = await fetch('/api/chat/', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ messages: history }) }); const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const events = buffer.split('\n\n'); buffer = events.pop(); for (const event of events) { if (event.startsWith('data: ')) { try { const data = JSON.parse(event.slice(6)); answer += data.content; aiMsgElement.textContent = answer; document.getElementById('chat-box').scrollTop = 9999; } catch (e) {} } } } history.push({ role: 'assistant', content: answer }); } catch (err) { aiMsgElement.textContent = '请求失败,请检查 Ollama 服务是否运行'; } } </script> </body> </html>流式解析的逻辑不复杂,但有一个细节值得说:buffer += decoder.decode(value, { stream: true })里的stream: true参数很关键。如果漏掉,当多字节的 UTF-8 字符被拆到两个网络包里时,会出现中文乱码。这个参数告诉解码器当前的字节流可能是不完整的,先不要急着抛错,等后续字节到了再补全。
4.2 多轮对话上下文管理
上面的实现把整个history数组每次全量发给后端。这是最简单可靠的做法,对于 7B 模型来说,几十轮对话的 token 量还在上下文窗口承受范围内。
但如果对话很长,历史消息会撑爆上下文窗口。我踩过这个坑:连续聊了两个小时后,模型开始“失忆”,连前面聊什么都忘了,更严重的会出现重复输出甚至报错。解决办法是做一个滑动窗口,只保留最近 N 轮对话:
const MAX_HISTORY = 10; if (history.length > MAX_HISTORY * 2) { history = history.slice(-MAX_HISTORY * 2); }同时在后端也要做一次保护性截断,防止异常请求把超长文本直接打到模型里。后端可以限制messages的总长度,超过的一定字数就丢弃最老的对话:
from pydantic import field_validator class ChatRequest(BaseModel): messages: List[ChatMessage] @field_validator("messages") @classmethod def limit_messages(cls, v): if len(v) > 40: return v[-40:] return v其实更好的方案是让后端统计 token 数,按 token 截断而不是按消息条数截断,但那样需要额外加 tokenizer 依赖,对个人项目来说按消息条数截断已经够用了。
4.3 请求状态与错误提示优化
前端界面上还有一个容易被忽略的点:请求状态的反馈。模型生成要几秒到几十秒,期间用户可能会重复点击发送,造成多个请求同时打到 Ollama,不仅乱序,还会拖慢推理速度。
我在页面里加了一个锁定逻辑:
let isWaiting = false; function sendMessage() { if (isWaiting) return; isWaiting = true; // ...请求结束后 isWaiting = false; }按钮文案也可以做成“正在生成...”来给用户明确反馈。另外,Ollama 在单卡上默认是串行推理的,如果你同时发了两个请求,第二个会排队,体验很糟,所以前端锁住是最简单的防并发方案。
5. 部署细节与服务化:从调试到可长期运行
5.1 使用 Docker Compose 一键编排
本地跑通之后,你会想把它部署到一台常开的机器上,可能是办公室的 Linux 服务器,也可能是一台 mini 主机。Docker 是个人觉得最省心的部署方式,把 Ollama 和 FastAPI 服务都容器化,一条命令全部启动。
我用的docker-compose.yml:
services: ollama: image: ollama/ollama:latest container_name: ollama restart: unless-stopped ports: - "11434:11434" volumes: - ollama_models:/root/.ollama/models deploy: resources: reservations: devices: - driver: nvidia count: all capabilities: [gpu] assistant: build: . container_name: fastapi-assistant restart: unless-stopped depends_on: - ollama ports: - "8000:8000" environment: - OLLAMA_BASE_URL=http://ollama:11434 volumes: - ./static:/app/static volumes: ollama_models:这里有个跨容器通信的细节:FastAPI 容器里不能再用http://127.0.0.1:11434访问 Ollama,因为 Docker 容器是隔离网络,得用服务名ollama作为主机名。在.env里把OLLAMA_BASE_URL改成http://ollama:11434。
还有 GPU 透传配置,deploy.resources.reservations.devices这段是给 Docker 用 NVIDIA Container Toolkit 时的写法。如果没有 GPU 或者不想配置,去掉这段就行,纯 CPU 也能跑,只是慢一些。
5.2 注册系统服务实现开机自启
如果你不想引入 Docker,直接在本机裸启动的话,Linux 下用 systemd 管理进程更稳。写一个 service 文件:
[Unit] Description=FastAPI Ollama Assistant After=network.target ollama.service [Service] Type=simple WorkingDirectory=/opt/assistant Environment="OLLAMA_BASE_URL=http://127.0.0.1:11434" ExecStart=/usr/bin/python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 Restart=always RestartSec=3 User=assistant Group=assistant [Install] WantedBy=multi-user.target几个关键点说明:
Restart=always保证进程崩了自动拉起,实测 Ollama 偶尔会因为显存不足崩掉,后端进程不能跟着挂。User=assistant用专用账号跑服务,避免用 root 暴露不必要的权限。我之前偷懒用 root,后来一次代码安全事故让我改了习惯。RestartSec=3是崩溃重启的等待时间,别设太长,也别太短——太短会导致循环重启时 CPU 空转。
前端静态文件如果不在容器里,可以直接由 FastAPI 挂载,也可以交给 Nginx 处理静态资源、反代 API 请求,后者对大流量场景更合适。
5.3 性能参数与体验调优
跑起来只是第一步,快和稳才是目标。我在配置层面踩了几个非常实用的点:
首先是并发数。Ollama 默认最大并发是可选配置的,模型加载时通过并发窗口控制。如果你只有一块显卡,老老实实把并发设为 1,否则会出现“两个请求轮流占显存,每个都慢得像乌龟”的情况。设置环境变量OLLAMA_NUM_PARALLEL=1可以确保每次只处理一个请求,对个人助手场景来说够了。
其次是模型常驻内存。Ollama 默认会把模型在内存中保留一段时间,但如果你设置了太激进的OLLAMA_KEEP_ALIVE(默认 5 分钟),频繁对话时模型会被卸载重新加载,每次加载 7B 模型都要十几秒。我把它改成OLLAMA_KEEP_ALIVE=3600,一小时不活动才卸载,对话时基本秒回。
还有上下文的num_ctx参数,默认通常是 2048 或 4096,如果对话较长,可以在请求体里加上:
"options": { "num_ctx": 4096, "temperature": 0.7 }num_ctx越大,模型能记住的上下文越长,但显存占用也越高。4096 是一个平衡点,适合绝大多数对话场景。改这个参数时务必要做压力测试,别贪大,否则内存直接顶爆。
5.4 局域网访问与地址绑定
默认情况下 uvicorn 监听127.0.0.1:8000,只有本机能访问。要让局域网内其他设备能用,启动命令需要绑定0.0.0.0:
uvicorn app.main:app --host 0.0.0.0 --port 8000完成之后,同网段的手机、平板、同事电脑就能直接访问http://你的IP:8000。Ollama 同理,默认监听 11434 的 127.0.0.1,如果你计划让其他机器直接访问 Ollama 原生接口,也需要设置OLLAMA_HOST=0.0.0.0。
但这里要提醒一下:如果部署在办公室或学校这类复杂网络里,绑定0.0.0.0意味着所有能 ping 到你机器的人都能访问。建议至少加一层简单的 Token 校验,别裸奔。
6. 常见问题与排查实录
6.1 Python 依赖安装报错与版本兼容
FastAPI 项目的依赖安装是第一个容易翻车的点。推荐用 virtualenv 隔离环境,避免和系统 Python 冲突:
python -m venv venv source venv/bin/activate pip install fastapi uvicorn httpx pydantic-settings如果你用的是 Python 3.11 以下版本,pydantic-settings可能需要额外安装python-dotenv。而uvicorn最好装带标准库版本:pip install uvicorn[standard],这样能启用uvloop,性能提升在压力测试下很明显。
我曾经在一台干净的服务器上直接pip install fastapi,结果装出来的 FastAPI 版本太老,Depends的用法都不同,代码照着新文档写结果报错。后来我养成了用requirements.txt锁定版本的习惯:
fastapi==0.115.0 uvicorn[standard]==0.30.6 httpx==0.27.2 pydantic-settings==2.5.2这几个版本组合是经过我验证的,直接抄作业不会有兼容性问题。
6.2 Ollama 连接不上和模型加载失败
后端报Cannot connect to host是最常见的问题。排查顺序是:先看 Ollama 进程是否存活(ps aux | grep ollama),再看端口是否监听(curl http://127.0.0.1:11434),最后看防火墙是否放行 11434 端口。
模型加载失败的报错分两种。一种是model not found,说明本地没有这个模型或者名字写错了,用ollama list核对标签名。另一种是failed to load model,通常是显存不足,把模型换成更小的量化版本,或者关掉其他吃显存的程序再试。
还有一个隐蔽的坑:当你通过Modelfile导入自定义模型后,ollama list里显示的名字是全小写加连字符格式,比如qwen2.5-7b-local。如果你的代码里写的是qwen2.5-7B-local,注意大小写不匹配会导致模型找不到,这个我栽过一次,排查了半小时。
6.3 生成速度慢、CPU 占用高的问题
CPU 跑 7B 模型,生成速度通常在每秒 5~15 token,属正常现象,不用焦虑。但如果速度低于这个区间,先确认是不是没有用到 GPU。输入ollama ps查看,如果显示PROCESSOR 100% CPU,说明模型在 CPU 上跑。
解决思路是装 NVIDIA Container Toolkit(Docker 环境)或者检查显卡驱动和 CUDA 版本(裸机环境)。NVIDIA 驱动用nvidia-smi验证,没有输出就是驱动问题。驱动装好后,再ollama ps,应该能看到PROCESSOR 100% GPU,生成速度会有质的飞跃。
还有一个影响速度的隐藏因素:如果你同时开着浏览器一堆标签页或者别的吃内存的服务,系统内存不够时显卡数据要换进换出,速度直接掉到龟速。关闭无关程序再测,效果立竿见影。
6.4 请求超时和上游连接断开
用 Python 的httpx调用 Ollama 时,如果 Ollama 正在加载模型、或者生成了很长的回复,上游需要很长时间才返回第一个字节。默认超时 5 秒的话,会直接报超时错误。
解决办法是把读超时调大,我在封装客户端时的配置是httpx.Timeout(connect=10.0, read=300.0, write=60.0, pool=10.0)。区分几个超时概念很有必要:
connect:建立 TCP 连接的超时时间,通常几秒就够。read:等待服务端返回数据的超时时间,这个必须设大,因为模型生成一篇长文可能几十秒。write:发送请求体的超时时间,这个不需要太大,几秒到几十秒均可。
如果调整后还是会断开,检查 Nginx 的proxy_read_timeout和proxy_buffering配置,这两个参数是 Nginx 层最容易打断 SSE 流的元凶。
6.5 Docker 环境下 GPU 不可用
Docker 容器里跑 Ollama,最常见的问题是容器内看不到 GPU。ollama ps显示 CPU 推理,速度感人。八成是没装 NVIDIA Container Toolkit。
Ubuntu/Debian 系统的快速安装步骤:
sudo apt-get install -y nvidia-container-toolkit sudo systemctl restart docker装完后,在 docker-compose 文件里写 GPU 资源配置,重新创建容器验证。docker run时加上--gpus all也可以。如果已经装了 toolkit 还是不行,看看是不是显卡太老导致驱动版本不兼容,老显卡需要安装对应旧版的 toolkit。
6.6 模型文件占用磁盘过大的管理技巧
Ollama 的模型文件都放在~/.ollama/models,一个 7B 模型 Q4 量化大约 4.7GB,如果多下几个模型,几十 GB 空間说没就没。我分享几个管理技巧:
- 用
ollama rm 模型名删除不用的模型,空间立刻释放。 - 定期用
du -sh ~/.ollama/models/*查看各模型大小,做到心中有数。 - 把
OLLAMA_MODELS目录迁移到数据盘,避免系统盘撑爆。具体做法是在启动脚本里export OLLAMA_MODELS=/data/ollama_models,然后重启服务。 - 同一个模型的不同量化版本不要都留着,
q4_k_m和q8_0各留一个即可,小版本差异在观感上基本无法区分。
7. 从原型到使用:关于这套方案的感想
整个项目从零到能稳定对话,我大概花了一个周末。Ollama 把模型管理这块的复杂度吸收掉了,FastAPI 把服务化这件事做得很干净,两者的结合点就是那个轻量的 HTTP 调用。
在选型上,我特意绕开了更重的方案。比如有人会建议直接用 LangChain,但在对话助手这个场景里,引入框架反而增加学习成本和调试难度。也有人会用 Gradio 或者 Streamlit 做界面,但那个更适合原型演示,真正做成一个可以长期使用的工具,还是 FastAPI 加一个前端页面更可控。等到你确实需要做 RAG、Agent 的时候,再把 LangChain 引进来也不迟。
另外想提醒一点:模型能力受限于参数规模和量化方式,本地 7B 模型肯定比不上在线大模型,这是物理规律,不是配置问题。如果想要更好的效果,可以先用本地模型做粗排和意图识别,再决定是否需要调用更大的模型;或者针对特定领域做微调,把自定义能力做强。
最后分享一个我目前的使用习惯:在这套本地助手里,我把系统 Prompt 固定成中文回复加 Markdown 格式化,然后用 n8n 做了一层知识库检索的外壳,这样既能享受本地模型的数据安全优势,又能在需要联网信息时补充上下文。如果定位是个人知识库助手,这套结构的扩展性足够用半年以上,等真正跑出瓶颈了再往分布式推理方向演进。