FastAPI+Ollama实战:搭建本地大模型对话服务
发布时间:2026/9/26 18:18:32来源:尧图网络
1. 为什么是FastAPI和Ollama本地大模型服务化的选型分析先说个我自己的场景。有段时间我需要频繁用大模型处理一些内部文档和代码审查但数据不能传到外部接口每次把内容喂给在线API心里都不踏实。后来接触了Ollama发现本地跑大模型这件事比我预想的成熟得多再加上FastAPI做接口层整套方案不到半天就能跑起来而且完全离线可用。很多人第一反应是“本地部署大模型是不是很麻烦”实际上Ollama把模型下载、加载、运行、API暴露这些脏活全都封装好了你只需要管好“怎么把对话请求送进去、怎么把结果拿回来”。而FastAPI作为后端框架天然适合干这件事异步支持、自动生成接口文档、基于类型提示的请求校验写起来比Flask更利落性能上也扛得住并发。这篇内容适合谁适合那些想在本地跑一个对话助手、但又不想折腾底层推理框架的开发者也适合正在做私有化部署评估的团队。我会从环境搭建、模型获取、后端接口设计、前端对接、再到性能优化和上线部署把整条链路完整走一遍。中间会穿插一些我实际踩过的坑和绕路的方法这些都是文档里不会写的东西。1.1 本地部署到底解决了什么问题选择一个方案之前得先明白它帮你省了什么。本地部署大模型的核心价值有三点。第一是数据隐私。文档、代码、聊天记录都留在本机不经过任何外部服务这在处理敏感内容的时候是刚需。第二是可控成本。在线API按token计费高频调用一个月下来不是小数而本地跑模型只需要电费。第三是离线可用。没有公网环境、或者网络不稳定的时候本地模型是唯一选择。当然本地部署也有明确的代价。显存和内存是绕不开的硬约束模型小了效果一般模型大了硬件扛不住。我个人的经验是先从7B到14B这个量级的量化模型入手比如Qwen2.5 7B或者Llama 3.1 8B4bit量化之后显存需求大概在5到8GB之间一张普通的消费级显卡就能跑起来。如果纯CPU跑速度会慢不少但作为学习验证也够用。1.2 FastAPI凭什么能担起后端职责FastAPI在Python社区里越来越流行不是没有原因的。它底层依赖Starlette和Pydantic异步接口的并发能力比传统WSGI框架好一个量级更重要的是开发体验非常舒服。拿最基础的接口定义来说FastAPI会自动做请求参数的解析和类型校验我只需要写清楚函数签名框架就能生成对应的OpenAPI文档。调用方甚至可以直接在浏览器打开/docs页面交互式调试接口这在联调阶段能省掉大量沟通成本。此外FastAPI对异步生成器async generator的支持非常到位。本地大模型流式输出的时候恰恰需要这种能力——模型每生成一个token就立刻推送到前端而不是等全部生成完了才一次性返回。这种流式交互体验和现在主流AI助手的打字机效果是一致的用户感知上的“速度”会快很多。1.3 Ollama让本地模型管理变得像Docker一样简单在没有Ollama之前本地跑大模型通常意味着要去处理模型权重文件、量化格式、推理框架、显存分配、依赖冲突这些问题。如今Ollama把这些东西全部收敛成几个简单命令模型管理体验和Docker非常像拉镜像、跑容器、起服务、看状态。Ollama底层用的是llama.cpp那一套推理能力对CPU和GPU都做了不错的优化量化格式统一用GGUF省心。它还会自动帮你处理显存不足时的分层加载虽然速度会受影响但至少不会直接崩。最关键的还是它自带一套HTTP API。默认监听11434端口提供/api/generate和/api/chat等接口。这意味着我根本不需要在Python进程里直接加载模型库只要通过HTTP请求和Ollama服务通信就可以。FastAPI在这里的角色就变成了“代理层”接收前端请求转发给Ollama再把流式输出回传给前端。这种松耦合的架构后续想换模型、换推理后端都只需要改配置。2. Ollama的安装与模型获取绕开下载瓶颈的实操方案这一节先解决最基础但也最容易挡住人的两个问题怎么安装Ollama以及怎么把模型下载下来。很多人在这两步就卡住了尤其是下载模型动不动失败体验非常劝退。2.1 安装与基础命令Ollama提供了Windows、macOS和Linux的安装包。Windows直接下载安装程序装完命令行里就能用ollama命令。Linux一行脚本搞定curl -fsSL https://ollama.com/install.sh | sh装完后先确认服务状态运行一个最简单的模型试试ollama run qwen2.5:7b如果这条命令能跑通说明Ollama核心安装没问题。下面是几个日常高频命令ollama list # 查看本地已有的模型列表 ollama pull qwen2.5:7b # 拉取模型到本地 ollama rm qwen2.5:7b # 删除模型 ollama show qwen2.5:7b # 查看模型的参数、量化级别等信息需要提醒一下ollama run会同时完成“如果没有就自动下载”的动作所以第一次执行的时候如果网络不好很容易卡住。我的建议是先ollama pull确认下载完成之后再去run。2.2 模型下载慢、下载失败的三种解决路径这是本地部署大模型最常被吐槽的点。Ollama默认从官方仓库拉模型文件国内网络环境下经常几分钟不动或者下到一半报错。我试过几种办法按推荐程度排序如下。第一种是配置国内镜像源。Ollama支持通过环境变量覆盖模型下载地址OLLAMA_HOST、OLLAMA_MODELS这些变量各有用途其中镜像源相关的配置主要影响模型文件的拉取。具体来说在启动Ollama服务之前设置好环境变量让模型下载走国内可用的镜像速度会有明显改善。Windows用户可以在系统环境变量里添加Linux用户写入~/.bashrc或/etc/systemd/system/ollama.service里。第二种是手动下载模型文件再导入。如果镜像源也不稳定还可以直接去ModelScope这类模型社区下载GGUF格式的模型文件然后通过Modelfile导入Ollama。Modelfile的写法很简单本质就是告诉Ollama“这个模型文件在哪、叫什么名字”FROM ./qwen2.5-7b-instruct-q4_k_m.gguf保存成Modelfile然后执行ollama create qwen2.5-7b -f Modelfile这样就能绕过Ollama官方仓库的网络瓶颈模型文件本身在你手里也更方便离线迁移。第三种是针对已有下载缓存的情况。有时候下载到一半中断重新执行ollama pull并不会自动续传反而会在某个进度处反复失败。这种时候先把显存和磁盘空间确认一下然后清理掉Ollama缓存目录里对应的临时文件重新拉取。如果某个版本反复拉不动可以考虑换一个量化级别更低的版本文件体积小成功率更高。2.3 Ollama的本地API是理解后续代码的钥匙Ollama自带HTTP服务Windows和macOS安装后默认会作为后台服务运行Linux可能需要手动启动ollama serve服务起来之后本地就有了一个OpenAI兼容风格的接口。先自己用curl验证一下确保后续FastAPI代码能真正调通curl http://localhost:11434/api/generate -d { model: qwen2.5:7b, prompt: 你好简单介绍一下你自己, stream: false }/api/generate适合单轮补全场景/api/chat则支持多轮对话消息结构。对于对话助手这个项目我主要用/api/chat因为前端需要把历史消息一起传过来模型才能有上下文记忆。3. FastAPI后端核心设计与流式对话实现Ollama装好、模型能跑之后接下来就是本项目的重头戏用FastAPI写一个后端服务把对话请求代理到Ollama并通过流式响应推送给前端。先说项目结构再逐块拆代码。3.1 项目目录结构先摆好FastAPI的项目结构不复杂但一开始就分好工能省掉后面很多麻烦。我的目录长这样llm-chat-assistant/ ├── main.py # FastAPI入口路由注册 ├── app/ │ ├── __init__.py │ ├── config.py # 配置项如Ollama地址、模型名 │ ├── routers/ │ │ ├── __init__.py │ │ └── chat.py # 对话接口 │ ├── schemas/ │ │ ├── __init__.py │ │ └── chat.py # 请求和响应的数据模型 │ └── services/ │ ├── __init__.py │ └── ollama_client.py # 和Ollama通信的客户端 ├── static/ │ └── index.html # 前端聊天页面 └── requirements.txt之所以拆成routers、schemas、services三层是为了让路由只做参数接收和响应返回业务逻辑提出来单独测。后期如果要把Ollama换成别的推理后端只需要改services层路由和前端完全不动。3.2 把Ollama接入FastAPI的请求核心先看依赖清单只需三个包fastapi uvicorn[standard] httpx其中httpx是和Ollama交互用的HTTP客户端它对异步和流式传输的支持是requests库比不了的。配置项放在config.py里import os OLLAMA_BASE_URL os.getenv(OLLAMA_BASE_URL, http://localhost:11434) DEFAULT_MODEL os.getenv(DEFAULT_MODEL, qwen2.5:7b)对话接口的输入输出结构用Pydantic定义。请求体里除了消息列表还要预留温度、最大生成长度这类推理参数from typing import List, Optional from pydantic import BaseModel, Field class ChatMessage(BaseModel): role: str content: str class ChatRequest(BaseModel): messages: List[ChatMessage] model: str Field(defaultqwen2.5:7b) temperature: Optional[float] Field(default0.7, ge0, le2) max_tokens: Optional[int] Field(default2048, ge1, le8192)这里有个细节值得注意temperature字段的范围我用ge和le做了校验避免前端传一个离谱的参数进来。而max_tokens是生成上限不是Ollama原生参数Ollama用的是num_predict所以真正发请求的时候需要做字段映射这一点很多人会踩坑。3.3 流式响应的关键实现与取舍FastAPI返回流式响应本质上是创建一个StreamingResponse其内容来自一个异步生成器。也就是说FastAPI会不断从生成器里取数据块再通过HTTP分块传输给浏览器。先来看和Ollama通信的service层。这里的核心是httpx.AsyncClient的stream方法它会保持连接打开逐块读取Ollama返回的JSON流import json import httpx from app.config import OLLAMA_BASE_URL async def stream_chat(messages: list, model: str, temperature: float, max_tokens: int): url f{OLLAMA_BASE_URL}/api/chat payload { model: model, messages: messages, stream: True, options: { temperature: temperature, num_predict: max_tokens, }, } timeout httpx.Timeout(connect20, read300, write30, pool60) async with httpx.AsyncClient(timeouttimeout) as client: async with client.stream(POST, url, jsonpayload) as response: async for line in response.aiter_lines(): if not line.strip(): continue data json.loads(line) delta data.get(message, {}).get(content, ) if delta: yield fdata: {json.dumps({content: delta}, ensure_asciiFalse)}\n\n if data.get(done): break有几个细节要说明一下。Ollama的流式输出是“一行一个JSON对象”每行包含当前生成的增量内容最后收到done: true才结束。所以读取的时候用aiter_lines按行遍历而不是按chunk遍历这样解析最稳定。超时配置里我把read设成了300秒这是考虑到大模型生成速度本来就不快尤其CPU推理可能几十秒才出一个完整回答如果超时太短会被迫中断。然后看路由层的代码from fastapi import APIRouter from fastapi.responses import StreamingResponse from app.schemas.chat import ChatRequest from app.services import ollama_client router APIRouter(prefix/api/chat, tags[chat]) router.post() async def chat(request: ChatRequest): messages [{role: m.role, content: m.content} for m in request.messages] return StreamingResponse( ollama_client.stream_chat( messagesmessages, modelrequest.model, temperaturerequest.temperature, max_tokensrequest.max_tokens, ), media_typetext/event-stream, headers{ Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no, }, )这里media_type设成text/event-stream前端就能按SSEServer-Sent Events的方式去解析。X-Accel-Buffering: no是给Nginx看的如果不加这个响应头Nginx默认会缓冲响应内容流式效果就没了。虽然没有Nginx的时候这行没用但加了能避免以后部署时遗漏。主入口文件main.py则负责挂载路由、静态目录和CORS中间件from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles from app.routers import chat app FastAPI(title本地大模型对话助手) app.add_middleware( CORSMiddleware, allow_origins[*], allow_credentialsFalse, allow_methods[*], allow_headers[*], ) app.include_router(chat.router) app.mount(/, StaticFiles(directorystatic, htmlTrue), namestatic)注意一下allow_origins[*]和allow_credentialsTrue不能同时用浏览器会拒绝这种组合。本地调试阶段用*就行后面如果要上生产建议把域名收敛到明确列表里。启动服务用一条命令uvicorn main:app --host 0.0.0.0 --port 80004. 前端聊天界面与CORS踩坑记录后端流式接口跑通之后最直观的验证方式就是写一个聊天页面。本来我以为这部分很简单结果实现过程中在流式文本拼装和跨域问题上浪费了不少时间这里把我最终可用的方案和避坑点整理出来。4.1 极简前端实现流式聊天框我不打算引入React或Vue一个原生HTML页面加上少量JavaScript就足够验证整条链路。页面结构就是一个消息列表区加一个输入框和发送按钮。核心的请求逻辑如下async function sendMessage() { const input document.getElementById(user-input); const text input.value.trim(); if (!text) return; appendMessage(user, text); input.value ; const assistantDiv appendMessage(assistant, ); 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(utf-8); 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) { const line event.replace(/^data: /, ).trim(); if (!line) continue; const data JSON.parse(line); assistantDiv.textContent data.content; } } } catch (err) { assistantDiv.textContent 请求出错了 err.message; } }流式解析的关键在于服务端每次发送的SSE数据块在网络传输过程中可能被拆散或合并。所以用buffer缓存未完整解析的部分每次以\n\n作为事件结束标记这是SSE协议的标准做法。如果只是简单地按每次read的数据直接append很容易出现半个JSON解析失败、内容断行等问题。历史消息数组history在每次用户发送时更新同时把助手的回复也推入下次请求就把完整的对话上下文一起发给Ollama这样模型才能记住之前聊过什么。4.2 跨域问题与localStorage的坑如果前端页面和后端接口同源由FastAPI托管静态文件那么不会触发CORS。但如果你用VS Code Live Server或直接双击打开HTML页面跑在http://127.0.0.1:5500这样的地址上而后端是http://localhost:8000跨域就出现了。最直观的现象是浏览器控制台报CORS policy错误但后端日志里一切正常。处理方案有两种一种是让API支持跨域就是我上面在main.py里加CORSMiddleware的方式另一种是统一用FastAPI托管静态文件推荐后一种因为同源模式下代码更简单也不会有预检请求带来的额外开销。如果想保留浏览器调试时的历史记录localStorage是个方便的选择。页面加载时读取本地存的消息数组发送成功后写入。但有几个坑需要注意localStorage存的是字符串读写都要做JSON序列化和反序列化同一个域名下如果换模型或者改场景旧消息可能会污染新的会话所以建议存储时加一个版本字段存储容量有5MB左右限制多轮超长对话容易撑爆我加了一个简单逻辑消息总数超过40条就自动截断最早的历史。5. 性能优化与生产部署经验本地模型API跑通只是第一步真正要考虑的是如何稳定、高效地对外提供能力。我在使用过程中遇到几个典型问题这里按“客户端连接优化—超时与错误处理—开机自启和远程访问”这个顺序来写。5.1 并发连接与连接复用优化FastAPI的异步特性让它可以同时接收大量HTTP请求但真正处理这些请求时瓶颈在Ollama一侧。Ollama默认对同一个模型会做请求排队如果同时来了四个对话请求模型只能逐个处理。所以后端要做的事情不是“提高Ollama的吞吐”而是“把请求合理地排队并反馈状态”。我在实践中发现httpx.AsyncClient最好不要在每次请求时都重新创建。虽然连接池的创建和销毁开销不算夸张但在高频调用下还是会造成大量的TIME_WAIT连接。建议在应用启动时创建一个全局的AsyncClientimport httpx from app.config import OLLAMA_BASE_URL _client None def get_client() - httpx.AsyncClient: global _client if _client is None: _client httpx.AsyncClient( base_urlOLLAMA_BASE_URL, timeouthttpx.Timeout(connect20, read300, write30, pool60), limitshttpx.Limits(max_connections20, max_keepalive_connections10), ) return _clientmax_connections控制并发连接上限max_keepalive_connections控制空闲连接缓存数量。设置成多少取决于你的Ollama部署在哪台机器、并发压力多大。本地单机场景20个连接足够如果是多人同时使用建议把这两个数字调大同时要留意Ollama侧会不会成为瓶颈。5.2 超时、错误处理与日志流式接口最容易出现的问题就是“前端等不到结果”。造成这个问题的原因一方面是Ollama模型加载和推理时间太长另一方面是FastAPI或反向代理默认的超时时间太短。我在开发时把读超时设到300秒如果跑的是比较大的模型比如30B以上建议再往上调。另外Ollama偶尔会返回错误JSON比如模型不存在、显存不足、请求格式错误。service层解析JSON时如果不做错误分支异常会直接抛到FastAPI的默认错误处理里返回堆栈信息给前端既不安全也不友好。我加了一层包装先检查HTTP状态码再检查JSON里有没有error字段最后做一些分类转化给前端返回可读的错误信息。日志方面我在路由处理里加了一个简单的logging记录把每次请求的模型名、消息数、耗时、首字延迟都记录下来。首字延迟从发送请求到返回第一个token的时间是最能反映用户感知流畅度的指标。通过观察这个数值可以判断是模型加载问题还是网络传输问题。5.3 开机自启与远程访问本地部署大模型的应用最常见的运行环境是一台开机就不关的机器。Windows上可以用任务计划程序启动uvicornLinux下建议直接用systemd服务。一个最小可用的systemd配置如下[Unit] DescriptionLLM Chat Assistant Afternetwork.target [Service] Useryourname WorkingDirectory/path/to/llm-chat-assistant ExecStart/usr/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restartalways RestartSec5 [Install] WantedBymulti-user.target配置文件写好之后sudo cp llm-chat.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl enable llm-chat sudo systemctl start llm-chatOllama服务本身在Linux安装后也默认注册了systemd服务所以只要模型在启动脚本里预加载过重启后整个链路会自动恢复。远程访问的话最直接的方式是前面提到的FastAPI启动参数里--host 0.0.0.0然后通过路由器端口映射把服务暴露出去。但这样会有两个隐患一是Ollama默认监听所有网卡如果你的网络环境不可信别人可以直接访问11434端口操作你的模型后端二是没有鉴权任何人都能调用你的对话接口。我的建议是Ollama服务端通过环境变量OLLAMA_HOST127.0.0.1限制只能本机访问FastAPI应用对外提供服务然后在FastAPI层简单加一个API Key校验中间件前端请求时在Header里带上密钥这样至少能挡住绝大多数乱扫端口的人。想更专业一点就再加一层Nginx做TLS和访问控制不过本地场景先把前两步做了就够了。6. 实际使用中的体验与后续扩展方向把项目跑通之后我连续用了一周每天都用这个对话助手来整理笔记、解释代码片段、润色邮件。整体感受是Ollama跑7B模型的响应速度在消费级GPU上完全可用首字延迟在1到2秒左右生成速度大致是每秒20到40个token配合流式输出体验上已经有点商用AI助手的意思了。几个实际使用中的体会分享一下。模型选择上不要一味追大。我最初试过14B模型生成质量确实好一点但显存占用接近翻倍生成速度也下降得很明显。对于日常对话、文档总结这些场景7B量级的量化模型性价比最高。如果对中文特别敏感优先试Qwen家族如果跑代码相关任务CodeLlama或Qwen-Coder系列会更顺手。多轮对话的上下文窗口要注意控制。Ollama默认的上下文窗口是2048或4096取决于模型和配置。对话太长会导致模型“忘记”前面的内容甚至触发报错。我现在的做法是在前端把历史消息裁剪到最近10轮左右同时计算总字数超过一定阈值就把更早的消息摘要成一句话再拼进去。这个方案虽然简单但在实际使用中效果非常好。另外如果希望在更多设备上使用助手可以在局域网内通过http://主机IP:8000直接访问页面手机浏览器也能用。我还试过把这个接口接到微信机器人上本质上就是统一消息格式、调用同一个FastAPI接口扩展成本很低。最有价值的扩展方向我觉得是给对话助手加上记忆能力。目前它每轮对话都依赖会话内的历史消息关掉页面就什么都忘了。下一步我计划把对话记录存到SQLite里每次请求前自动加载相关历史这样跨会话的长期记忆就有了。再往后可以做文档知识库用embedding模型把本地文档向量化对话时先检索再回答。整个项目从零到可用花费的时间不超过一个晚上。FastAPI负责服务编排Ollama负责模型推理两者各司其职组合起来非常顺手。如果你正在犹豫要不要在本地部署大模型先用这套方案跑通一次完整链路你会发现这件事的门槛远低于预期。
网站建设高端定制企业官网