前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
原理篇
面经篇
AI 面试
自检篇
每日一题
  • 综合
    • 综合题型
    • 其他问题
    • 设计模式
    • 思维导图
    • 学习路线
  • 前端基础
    • HTTP
    • 浏览器
    • 计算机基础
  • 进阶学习
    • NPM工作流
    • Docker
    • Canvas
    • Node学习指南
    • 前端综合文章
  • 其他
    • Handbook
    • 职场话题
    • CSS可视化
小程序题库
公众号动态
博客动态
AI 热点
开发者导航
基础篇
进阶篇
高频篇
精选篇
手写篇
原理篇
面经篇
AI 面试
自检篇
每日一题
  • 综合
    • 综合题型
    • 其他问题
    • 设计模式
    • 思维导图
    • 学习路线
  • 前端基础
    • HTTP
    • 浏览器
    • 计算机基础
  • 进阶学习
    • NPM工作流
    • Docker
    • Canvas
    • Node学习指南
    • 前端综合文章
  • 其他
    • Handbook
    • 职场话题
    • CSS可视化
小程序题库
公众号动态
博客动态
AI 热点
开发者导航
返回 AI 情报前线
All News · 全部资讯7474
  • 2026 年 Agent 沙箱横评:冷启动、计费方式、网络策略对比
  • OpenAI安全测试:1200个AI Agent自发组织并发动攻击
  • NVIDIA MPS 让 Amazon EC2 ASR 推理成本下降 75%
  • Sai AI 助手刷榜 OSWorld 2.0:73% 成功率
  • ConardLi/garden-skills:AI编码智能体生产级技能库
  • 六个用户的全部对话暴露了我们的进度报告Bug
  • Vercel AI SDK harness层正式支持Cursor
  • 谷歌要求安卓应用2027年前降低内存占用,设立新性能门槛
  • 企业 AI 真正风险不在 Agent 本身,而在 Agent 之间的复杂度
  • Claude/Codex/Hermes在企业内网安装了无人认领的代码
  • GraphRAG 如何解决基础 RAG 多跳推理失效问题
  • NVIDIA首款Agent CPU Vera正式出货
  • Anthropic Files API 正式发布:省时间但不省钱
  • OpenAI 1200个Agent未经授权联手越狱Hugging Face
  • Archify: AI编程代理的架构图生成工具
  • browser-use: AI代理控制浏览器自动化
  • 身份认证Agent的对抗性压测框架:Last Cradle
  • AI Agent的治理必须在数据层实时生效,而非事后策略
  • GLM-5.3-Flash 接近顶级模型效果,成本仅七分之一
  • AI 写技术文档为何越写越错:模型在「幻觉文档」
  • FastAPI + Ollama 打造生产级本地 LLM API 服务
  • AI 读 YouTube 字幕建错向量,根源在视频信息损耗
  • Google Cloud TPU 跑 Qwen3 Embedding:生产级长上下文检索实战
  • Nvidia拟以129亿美元收购HuggingFace,剑指AI云服务
  • LLM 可解释性实战:通过提示词消融定位关键句子
  • 阿里 Qwen3.8-Flash 发布:生成速度翻倍、Token 消耗降75%
  • GLM-5.3-Flash 评测:强模型降价潮中的工程决策
  • 千问Qwen3.8-Flash首发:生成速度翻倍,Token消耗降75%
  • Nvidia 拟130亿美元收购Hugging Face
  • 通义千问开源 Qwen3.8-Flash-Next:125B 稀疏混合专家模型
  • Agent 架构新范式:持久化运行而非常驻进程
  • OpenAI 模型 1 小时攻破沙箱:Agent 安全运营的血泪教训
  • 智谱GLM-5.3-Flash开源:320B总参/18B活跃的百万上下文MoE
  • Google Cloud TPU 原生集成 vLLM:长上下文Embedding 推理优化实践
  • 实测两个 computer-use Agent 一个月:瓶颈不是模型而是机器占用率
  • 用 Multica 任务面板同时管理三个编程 Agent
  • AWS OpenSearch 推出 Agent 可观测性 MCP 应用
  • GitHub Copilot 自动化 Dependabot PR 分类
  • Amazon Bedrock 推出框架无关的 Agent 评估工具
  • OpenAI Agent入侵Hugging Face技术报告出炉
  • AI Agent 劫持演示20/20全失败:攻击强度才是决定性因素
  • 监督微调数据准备进阶篇:数据子集选择与合成增强
  • 监督微调数据准备基础篇:格式、质量与数据划分
  • Hister:支持 MCP 的本地私密搜索引擎
  • Perplexity 企业版新动向:本地化 AI Agent 来了
  • Lovable CTO:未来 SaaS 需要能被 AI Agent 调用的 API
  • AI Guardrails 红队实测:accent mark 绕过真实案例
  • Next.js中间件优化:公共路由跳过会话解析
  • 阿里Qwen3.8:125B MoE模型仅6B激活参数
  • WebMCP:让网站开口与 AI 代理对话的协议
  • 阿里Qwen3.8 Flash-Next主打极致成本效率
  • 已加载 51 / 7474
8.0
热点
AI SCORE
工具产品2026-08-27 18:02

FastAPI + Ollama 打造生产级本地 LLM API 服务

dev.to · AI#Ollama#FastAPI#LLM本地部署
Editor brief · 编辑速览

Ollama 原生 HTTP 接口过于简陋,作者用 FastAPI 封装了一层:添加认证、请求校验、流式响应、统一错误处理,形成可部署的生产级服务,适合需要本地推理且数据不能出境的场景。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

在本地运行语言模型并将其暴露为标准的 REST API 有真实的实用价值:没有速率限制、没有按 token 计费、数据不会离开你的基础设施。问题在于 Ollama 原生的 HTTP 接口是有意简化的——能用,但在任何实际工作负载中使用之前,你肯定需要认证、结构化的请求验证、流式响应支持和完善的错误处理。

这份指南构建的是一个你真正会部署的、围绕 Ollama 的薄层 FastAPI 封装。

你需要安装并运行 Ollama,且至少 pull 一个模型:

# Install Ollama (Linux)
curl -fsSL https://ollama.ai/install.sh | sh

# Pull a model
ollama pull llama3.2:3b

# Verify
ollama list

对于 FastAPI 层:

pip install fastapi uvicorn httpx python-dotenv

Ollama 在 localhost:11434 上运行自己的 HTTP 服务器。这里的策略很简单:

FastAPI 处理来自客户端的请求

它用 Pydantic 验证载荷

将请求转发给 Ollama

返回响应——要么是完整的 JSON,要么是流式 SSE 响应

这样 Ollama 进程就保持为哑后端,让你在 FastAPI 层叠加认证、日志和请求转换,而无需触碰 Ollama 的配置。

# server.py
import os
import httpx
import asyncio
from typing import AsyncGenerator
from fastapi import FastAPI, HTTPException, Depends, Header
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field

OLLAMA_BASE_URL = os.getenv("OLLAMA_BASE_URL", "http://localhost:11434")
API_KEY = os.getenv("API_KEY", "change-me-in-production")

app = FastAPI(title="Local LLM API", version="1.0.0")

class ChatMessage(BaseModel):
    role: str = Field(..., pattern="^(system|user|assistant)$")
    content: str

class ChatRequest(BaseModel):
    model: str = Field(default="llama3.2:3b")
    messages: list[ChatMessage]
    stream: bool = False
    temperature: float = Field(default=0.7, ge=0.0, le=2.0)
    max_tokens: int = Field(default=1024, ge=1, le=8192)

def verify_api_key(x_api_key: str = Header(...)):
    if x_api_key != API_KEY:
        raise HTTPException(status_code=401, detail="Invalid API key")
    return x_api_key

async def stream_ollama_response(payload: dict) -> AsyncGenerator[str, None]:
    async with httpx.AsyncClient(timeout=120) as client:
        async with client.stream(
            "POST",
            f"{OLLAMA_BASE_URL}/api/chat",
            json=payload,
        ) as response:
            async for line in response.aiter_lines():
                if line:
                    yield f"data: {line}\n\n"

@app.post("/v1/chat")
async def chat(request: ChatRequest, _: str = Depends(verify_api_key)):
    payload = {
        "model": request.model,
        "messages": [m.model_dump() for m in request.messages],
        "stream": request.stream,
        "options": {
            "temperature": request.temperature,
            "num_predict": request.max_tokens,
        },
    }

    if request.stream:
        return StreamingResponse(
            stream_ollama_response(payload),
            media_type="text/event-stream",
        )

    async with httpx.AsyncClient(timeout=120) as client:
        try:
            response = await client.post(
                f"{OLLAMA_BASE_URL}/api/chat",
                json=payload,
            )
            response.raise_for_status()
        except httpx.HTTPStatusError as e:
            raise HTTPException(status_code=502, detail=f"Ollama error: {e.response.text}")
        except httpx.ConnectError:
            raise HTTPException(status_code=503, detail="Ollama is not running")

    return response.json()

@app.get("/v1/models")
async def list_models(_: str = Depends(verify_api_key)):
    async with httpx.AsyncClient(timeout=10) as client:
        try:
            resp = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
            resp.raise_for_status()
            return resp.json()
        except httpx.ConnectError:
            raise HTTPException(status_code=503, detail="Ollama is not running")

@app.get("/health")
async def health():
    try:
        async with httpx.AsyncClient(timeout=5) as client:
            resp = await client.get(f"{OLLAMA_BASE_URL}/api/tags")
        return {"status": "ok", "ollama": resp.status_code == 200}
    except httpx.ConnectError:
        return {"status": "degraded", "ollama": False}
API_KEY=my-secret-key uvicorn server:app --host 0.0.0.0 --port 8000
curl -X POST http://localhost:8000/v1/chat \
  -H "x-api-key: my-secret-key" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama3.2:3b",
    "messages": [{"role": "user", "content": "What is a JWT?"}]
  }'

处理流式响应

Ollama 在流式响应时返回换行符分隔的 JSON。stream_ollama_response 生成器将每个 chunk 包装为 Server-Sent Event。以下是一个消费它的客户端:

# client_stream.py
import json
import httpx

def stream_chat(prompt: str, api_key: str, base_url: str = "http://localhost:8000"):
    payload = {
        "model": "llama3.2:3b",
        "messages": [{"role": "user", "content": prompt}],
        "stream": True,
    }
    with httpx.Client(timeout=120) as client:
        with client.stream(
            "POST",
            f"{base_url}/v1/chat",
            json=payload,
            headers={"x-api-key": api_key},
        ) as response:
            for line in response.iter_lines():
                if line.startswith("data: "):
                    chunk = json.loads(line[6:])
                    if not chunk.get("done"):
                        print(chunk["message"]["content"], end="", flush=True)
    print()

if __name__ == "__main__":
    stream_chat("Explain TLS handshake in 3 sentences.", "my-secret-key")

输出逐 token 出现——这与任何托管 API 的行为一致。

生产环境注意事项

在 localhost 之外部署之前,有几件事需要注意。

模型验证。当前代码接受任意模型字符串,让 Ollama 以 500 错误失败。更好的做法是在启动时获取可用模型列表,然后在每个请求时验证模型是否在那个集合中。

并发控制。Ollama 默认一次只处理一个请求(除非你的 GPU 支持真正的并行)。并发请求会在内部排队,但在高负载下会迅速堆积。用 asyncio.Semaphore 限制并发推理调用数:

_sem = asyncio.Semaphore(2)  # max 2 concurrent Ollama requests

@app.post("/v1/chat")
async def chat(request: ChatRequest, _: str = Depends(verify_api_key)):
    async with _sem:
        # ... forward to Ollama
        pass

超时。timeout=120 是一个上限,不是保证。在消费级硬件上,一个 13B 模型生成长响应可能超过 2 分钟。按模型调优超时时间,或者将超时暴露为可选的请求字段。

日志。在请求级别添加结构化日志:模型名称、输入 token 数(Ollama 会返回这个)、延迟和状态码。没有这些,调试慢响应会很痛苦。

安全响应头。如果这个 API 放在 nginx 后面,添加 X-Content-Type-Options、X-Frame-Options 和一个严格的 Content-Security-Policy。关于结构化 API 加固参考,这份安全检查清单涵盖了部署时容易被忽视的缺口。

TLS。不要在没有 TLS 终止的情况下直接暴露在公网接口上。在前面用 Caddy 或 nginx,即使对内部使用也是如此。

在语言模型前面加一层 FastAPI 封装大约需要 100 行代码,换来的是一个具有完善验证、认证和流式响应能力的自托管推理端点。与托管 API 相比,主要的权衡是:你拥有硬件和延迟特征,模型大小受限于显存,而且你需要负责更新和可靠性。

对于内部工具、空气隔离环境或成本敏感的工作负载——每天调用 API 数千次的情况下——这个技术栈具有真正的竞争力。它的局限在于高并发吞吐量——到了那个程度,改用 vLLM 而不是 Ollama,vLLM 是专为高吞吐量而非单请求延迟构建的。


Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
AI 写技术文档为何越写越错:模型在「幻觉文档」
下一篇
AI 读 YouTube 字幕建错向量,根源在视频信息损耗