
如果你最近在折腾 AI 应用开发翻过几个开源项目的源码大概会有一种感觉那些真正跑在服务器上的大模型服务最后往往不是被 Flask 或 Django 接走的而是被一个叫 FastAPI 的框架包了起来。这个现象不是偶然。FastAPI 的异步特性、类型校验和自动文档生成几乎就像是为 AI 服务的调用来量身定做的。今天这篇就从我自己的学习路径出发聊透一个 FastAPI 应用是怎么从零搭到能支撑 AI 场景的以及那些文档里不会写的坑我帮你先踩一遍。1. 为什么 AI 应用开发FastAPI 几乎是绕不开的那一层先说个真实的感受。最早我也是从 Flask 入手写接口的写得很顺手后来接大模型请求问题就开始冒出来了大模型的生成是流式的客户端要的是 token 一个接一个往外蹦而 Flask 原生对流式响应和 WebSocket 的支持都比较费劲再者AI 接口的请求体往往嵌套复杂用户的输入参数、对话历史、模型参数、工具定义全都塞在一个 JSON 里靠手写校验真的很痛苦。后来换成 FastAPI两个痛点同时解决了。1.1 异步机制与大模型调用的天然匹配大模型推理的特点是慢一次请求可能要几秒甚至几十秒。传统的同步框架在处理这种请求时线程会被长时间占用并发一上来机器就吃不消了。FastAPI 基于 Starlette 的异步非阻塞模型在调用大模型服务的那个时间窗口里事件循环可以继续处理其他请求整个服务的吞吐量就上来了。我做过一个粗略对比一台 2 核 4G 的云主机用 Flask 写一个简单的流式对话接口并发到 30 左右就开始大面积超时同样是这个逻辑换 FastAPI 重写以后并发到 80 依然稳定。这个差异在接口只做数据库读写时还不明显但一旦绑定大模型服务立刻拉开差距。1.2 类型校验和 Pydantic就是给 AI 接口量身做的AI 接口最怕什么怕脏数据。用户传上来的消息可能不是字符串而是一个列表temperature可能填成了字符串0.7可选字段缺失导致模型服务端直接爆 500。Pydantic 模型可以做到解析即校验字段值进到函数之前就已经被检查过一遍了。from typing import Literal from pydantic import BaseModel, Field class ChatMessage(BaseModel): role: Literal[system, user, assistant] content: str Field(..., min_length1, max_length20000) class ChatRequest(BaseModel): model: str qwen-plus messages: list[ChatMessage] temperature: float Field(0.7, ge0.0, le2.0) stream: bool True这段代码写完之后FastAPI 会自动根据这个模型校验请求体。传错类型、超范围、缺字段全部在入口处被拦截返回给客户端一条带具体原因的 422 错误。比起大模型服务端返回一段不知所云的堆栈用户体验完全不是一个级别。1.3 自动生成 API 文档AI 前后端联调的效率神器另一个我很看重的点是接口文档。传统开发模式里后端写完接口要维护一份 Markdown 文档前端再照着文档对接字段一改就乱。FastAPI 基于 OpenAPI 规范会在启动时自动生成交互式文档访问/docs就能看到所有接口并且可以直接在页面上填参数发起请求。联调大模型接口的时候这个功能尤其好用。模型服务商返回的字段结构经常会有细微调整我直接在/docs页面里发一次请求看返回比翻聊天记录找参数快多了。2. 从零搭出 FastAPI 骨架目录结构、ASGI 与 Pydantic 的三角关系我见过不少人上来就写一个main.py堆几千行接口、模型、业务逻辑全塞在一起。前期很爽后面每次加功能都像拆炸弹。这里分享一个我目前在 AI 项目上使用的目录骨架适合中小型应用。2.1 一套能直接开跑的目录结构fastapi-ai-app/ ├── app/ │ ├── __init__.py │ ├── main.py # 应用入口创建 FastAPI 实例 │ ├── config.py # 配置管理读取环境变量 │ ├── models/ # Pydantic 模型请求/响应 │ │ ├── __init__.py │ │ └── chat.py │ ├── routers/ # 路由模块 │ │ ├── __init__.py │ │ └── chat.py │ ├── services/ # AI 服务封装层 │ │ ├── __init__.py │ │ └── llm.py │ └── core/ # 基础能力鉴权、日志、异常处理 │ ├── __init__.py │ ├── auth.py │ └── logger.py ├── tests/ ├── requirements.txt └── .env.example这套结构的核心思想是把“路由”、“模型”、“服务”三者切开。路由负责接收请求和返回响应模型负责数据结构与校验服务负责真正的业务逻辑比如调用大模型 SDK。好处是替换模型服务商时不用动路由层和模型层只改services/llm.py内部实现。2.2 ASGI 与 UvicornFastAPI 的运行底座FastAPI 应用本身是一个 ASGI 应用需要一个 ASGI 服务器来跑。我常用的组合是 Uvicorn 或者 Uvicorn Gunicorn。理解这一点很关键因为很多人直接uvicorn main:app就上线了性能其实没吃满。单体服务测试环境用单个 Uvicorn 进程没问题。线上我通常用 Gunicorn 托管多个 Uvicorn worker再用 Nginx 做反向代理和负载均衡。启动命令参考gunicorn app.main:app \ --workers 4 \ --worker-class uvicorn.workers.UvicornWorker \ --bind 0.0.0.0:8000 \ --timeout 120注意--worker-class必须是uvicorn.workers.UvicornWorker绝对不能继续用默认的syncworker否则 FastAPI 的异步优势就全部失效了。这是我早期踩过的一个坑改动之前以为并发问题出在代码里其实是 Gunicorn 用的同步 worker 把异步接口全部阻塞住了。2.3 Pydantic v2 与 FastAPI 版本配套当前 FastAPI 新版本已经全面切换到 Pydantic v2性能比 v1 提升了不少但 API 也有些破坏性变化。如果你是从旧教程学过来的要特别注意v2 里validator变成了field_validatorparse_obj换成了model_validate。安装的时候建议直接锁版本fastapi0.115.6 uvicorn[standard]0.34.0 pydantic2.10.4锁版本这件事在 AI 项目里尤其重要。大模型的生态迭代非常快依赖动不动就升级不锁版本的话可能某天你一装依赖FastAPI 被升级到新版本然后pydantic跟着变紧接着你的某个模型配置字段就解析失败了。我的习惯是每次跑通一个项目就把requirements.txt提交到仓库里而不是只写一个宽松的不等式。3. 路径参数与请求校验AI 接口最容易被忽略的细节FastAPI 的路径参数是新手最容易上手的知识点但也是 AI 应用里最容易埋雷的地方。为什么因为 AI 场景下的路径参数往往不只是简单的int或str它还可能夹带一些特殊符号。你稍微处理不好整个接口就废了。3.1 路径参数的类型声明与转换GET /api/v1/agents/{agent_id}如果agent_id需要限制为正整数最优雅的写法是from typing import Annotated from fastapi import APIRouter, Path, HTTPException router APIRouter(prefix/api/v1/agents, tags[agents]) router.get(/{agent_id}) async def get_agent( agent_id: Annotated[int, Path(ge1, le999999)] ): # 业务逻辑 return {agent_id: agent_id, name: demo-agent}这里Annotated[int, Path(ge1, le999999)]会同时完成三件事类型转换、范围校验、文档生成。如果客户端传了一个abcFastAPI 直接返回 422如果传了 -5也会因为ge1被拦下。3.2 路径参数中的特殊字符处理AI 项目里有一种常见情况路径参数放的是模型名称或工具名称比如gpt-4o-mini、qwen-plus、BGE-M3。这些字符串里常带点号、连字符、斜杠。点号和连字符还好说如果参数值本身包含斜杠比如fastapi/docs直接放到路径里是会被路由拆开的。这种场景我建议用路径转换器让一个路径段也能匹配斜杠router.get(/files/{file_path:path}) async def get_file(file_path: str): return {file_path: file_path}{file_path:path}这个语法在 FastAPI 里代表着“贪婪匹配”它会吃掉剩下的所有路径。我实际项目里用来做知识库文件下载接口效果很好。但要注意带斜杠的路径参数在代理层可能会被提前改写Nginx 默认配置下不会动 URL但你如果开了别的代理规则很可能就被截断了。3.3 Query 参数与 Body 参数的边界划分AI 服务的接口我一般遵循一条规则简单筛选参数放 query复杂结构化数据放 body。比如分页、排序字段、开关项这些用 query 即可而对话消息、待生成的文本、工具定义这些都放 body。原因很简单query 参数的长度在多数网关和浏览器里都有限制而 AI 请求动辄几十上百 KB放 query 很容易被截断。一个容易出现的问题是把长文本塞进 query。有次联调一个“文本摘要”接口对方把整篇待摘要的文档放在了 URL 后面结果请求发出去直接被 Nginx 以 414 状态码弹了回来。所以凡是超过几百字节的内容一律走 body。4. 权限管理给大模型接口装上“门禁”AI 接口和普通 CRUD 接口在安全上有本质区别。普通接口泄露的顶多是数据AI 接口泄露的往往是整个模型服务的调用权限如果被恶意刷量费用会以肉眼可见的速度飙升。我见过一个真实案例某团队把没加鉴权的大模型代理接口暴露到了公网一天之内被刷了几十万次请求账单直接爆掉。4.1 最少但必需API Key 认证最开始不要上一堆复杂的权限模型先用 API Key 把门锁住。实现方式很简单依赖注入做一个全局检查from fastapi import Depends, HTTPException, Security from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials security HTTPBearer() async def verify_api_key( credentials: HTTPAuthorizationCredentials Security(security) ): api_key credentials.credentials if not is_valid_api_key(api_key): raise HTTPException(status_code401, detailInvalid API key) return credentials # 在路由中使用 router.post(/chat/completions) async def chat_completion( request: ChatRequest, _: HTTPAuthorizationCredentials Depends(verify_api_key) ): # 业务逻辑 pass这里用到的HTTPBearer是 FastAPI 内置的安全方案会从请求头的Authorization: Bearer token里取凭据。比自定义一个X-API-Key头更规范也方便以后接入更成熟的认证体系。4.2 多级权限用户维度与模型维度实际项目里权限不能只做到“能进不能进”还要区分“能调什么模型”。比如免费用户只能调用轻量模型付费用户才能调用大参数模型。我的做法是把权限信息放进一个依赖函数里把它当作路由的一个参数传入from typing import Literal from dataclasses import dataclass dataclass class UserPermission: user_id: str allowed_models: list[str] async def get_user_permission( credentials: HTTPAuthorizationCredentials Depends(verify_api_key) ) - UserPermission: # 根据 API key 从数据库/缓存中获取权限 return UserPermission(user_id123, allowed_models[qwen-turbo, qwen-plus])在具体的服务函数里再判断一次要调用的模型是否在allowed_models中。这里有个经验权限判断尽量放在业务函数里做而不是依赖路由层把模型参数写死。因为 AI 应用经常要做模型路由和降级同一个接口可能根据用户身份自动切换模型路由层写死的话后面扩展会很痛苦。4.3 敏感操作与成本控制还有一个很多人忽略的维度流式接口的鉴权。SSEServer-Sent Events流式返回时连接会持续很久鉴权不能在连接建立后就不管了。我一般会在请求开始时验证一次权限同时在流式输出循环里加一个配额检查比如累计输出超过多少 token 就主动断开。这样既保护了成本也避免客户端因为断网导致连接一直挂着的尴尬。5. 把大模型服务请进 FastAPI流式输出、超时和并发的实战处理跑通一个 Hello World 很简单真正把大模型能力整合进 FastAPI 并能稳定服务要处理的细节非常多。这一节我讲几个最实际的问题。5.1 流式响应的实现方式大模型服务的流式响应本质上是每次生成一小段内容立刻推给客户端。FastAPI 里最常见的实现是使用StreamingResponse。from fastapi.responses import StreamingResponse router.post(/chat/completions) async def chat_completion(request: ChatRequest): async def event_generator(): async for chunk in llm_service.stream_chat(request.messages): yield fdata: {chunk}\n\n return StreamingResponse( event_generator(), media_typetext/event-stream, headers{ Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no } )这里有一个非常关键的坑如果前面套了 Nginx它默认会开启缓冲把上游的数据攒到一定量再发给客户端这就把流式的意义全毁了。必须在 Nginx 层关闭缓冲同时加响应头X-Accel-Buffering: no双保险。5.2 上游模型服务超时与重试策略大模型服务经常“时好时坏”超时是家常便饭。给任意第三方请求设置超时是我做 AI 应用的最低底线。使用httpx时超时设置要区分连接超时和读取超时import httpx async def llm_request(): timeout httpx.Timeout( connect_timeout10.0, read_timeout120.0, write_timeout30.0, pool_timeout10.0 ) async with httpx.AsyncClient(timeouttimeout) as client: resp await client.post(https://api.example.com/v1/chat/completions, json{...}) resp.raise_for_status() return resp.json()关于超时设置我的经验是connect_timeout不要太长5-10 秒足够交底了read_timeout要留足大模型单次生成可能要几十秒但你也不能放着不管越大的模型推理时间越长这个值需要根据实际模型调试没有统一标准。另外要留意的是如果你需要覆盖“模型无响应但连接还开着”的状态read_timeout就是唯一防线。重试的话优先级从高到低连接失败可以重试超时要看接口是否幂等如果请求可能已经到达模型服务并产生了费用盲目重试就是双重扣费。我通常只在明确返回“429 限流”或“5xx 服务端错误”时才自动重试。5.3 并发控制与限流FastAPI 能扛高并发但大模型服务商并不会让你无限制地并发调用尤其是你用的是同一个 API Key 时服务商那边都有 QPS 限制。我在应用层做了两层保护一层是全局信号量控制同时对上游模型发起的请求数另一层是令牌桶限流控制单个客户端在时间窗口内的请求频率。import asyncio # 全局信号量最多同时 8 个上游请求 llm_semaphore asyncio.Semaphore(8) async def safe_llm_call(messages): async with llm_semaphore: return await llm_service.stream_chat(messages)这样做的好处是即使前端有人手动刷页面或者某个脚本意外陷入死循环也不会轻易把上游 API 打挂。成本上更是直观的上游是按 token 计费的限流就是在保命。5.4 异步任务与后台处理有的 AI 任务不适合同步返回结果比如批量生成摘要、视频内容分析、多轮 Agent 任务。这种我习惯用 FastAPI 的BackgroundTasks配合任务队列来做。from fastapi import BackgroundTasks def run_heavy_task(task_id: str): # 异步执行任务更新状态到数据库 pass router.post(/tasks) async def create_task(request: TaskRequest, background_tasks: BackgroundTasks): task_id generate_task_id() background_tasks.add_task(run_heavy_task, task_id) return {task_id: task_id, status: queued}但注意BackgroundTasks适合短任务如果任务要跑几分钟建议换 Celery 或 arq。一般 AI 场景里做队列我推荐 arq它基于 Redis 和 asyncio与 FastAPI 的异步风格很搭代码写起来也比 Celery 轻多了。6. 从 FastAPI 到 FastMCP把应用变成 AI Agent 的“工具”最近很火的一个方向是 MCPModel Context Protocol目的就是让大模型能够标准化地调用外部工具和数据源。FastMCP 这个库把 FastAPI 开发者熟悉的那套路由写法搬到了 MCP 工具定义上它不是一个和 FastAPI 竞争的东西而是和 FastAPI 生态互补把 FastAPI 应用的能力暴露给 Agent 去调用。6.1 为什么 AI Agent 应用需要 MCP如果你只是做一个聊天机器人直接调大模型 API 就够了。但你要做一个能查天气、能查数据库、能调内部 API 的 Agent问题就来了大模型怎么知道有哪些工具工具参数怎么告诉它返回结果怎么格式化成模型能理解的结构MCP 解决的正是“模型到工具”的最后一公里。FastMCP 让你像写 FastAPI 路由一样声明工具模型可以动态发现工具、校验参数、拿到结构化结果。6.2 一个最简单的 FastMCP 服务from fastmcp import FastMCP mcp FastMCP(My Agent Tools) mcp.tool() def query_knowledge_base(keyword: str, top_k: int 5) - list[dict]: # 查询向量数据库返回 top_k 条结果 return [ {title: FastAPI 入门, score: 0.95}, {title: MCP 协议说明, score: 0.88} ] if __name__ __main__: mcp.run()启动之后MCP 客户端就能动态地发现query_knowledge_base这个工具把用户的自然语言问题解析成keyword和top_k参数再调用背后的向量检索逻辑。6.3 FastAPI 与 FastMCP 的分工协作我现在的项目里FastAPI 和 FastMCP 各司其职组件职责FastAPI对外提供 REST 接口处理 Web 端、移动端等常规客户端请求FastMCP对内或对 Agent 暴露工具接口让大模型能调用检索、数据库操作、业务逻辑等能力共享层Pydantic 模型、业务服务层、鉴权逻辑实践中的做法是把真正复杂的业务逻辑放在services/层FastAPI 路由和 FastMCP 工具都只做一层薄薄的适配。这样既能给普通前端用又不会把 Agent 工具的能力落下。6.4 做 MCP 服务时最值得注意的三件事第一工具命名要非常具体。大模型是靠名字理解工具的search_data这种名字会让模型一头雾水search_user_profiles_by_name就好得多第二参数描述要写清楚。FastMCP 支持通过 docstring 为每个参数生成描述这部分一定要花心思写因为大模型就是靠着这些描述来决定什么时候调用、传什么参数的第三工具返回结果最好是扁平 JSON嵌套太深的结构容易让模型解析出错宁可拆成多条记录也别塞一个巨型 dict。7. 部署、自检与后续迭代的实用路线代码写完只是第一步真正让 AI 应用稳定跑起来部署和验证环节同样值得投入时间。这里把我常用的流程写出来。7.1 上线前的必要检查清单我给自己的每个 FastAPI AI 项目都建了一个 checklist是否锁定了requirements.txt中所有依赖的精确版本环境变量是否通过.env管理且敏感信息没有提交到仓库接口是否区分了生产环境与测试环境的配置是否对上游模型服务设置了超时和重试是否对公网接口开启鉴权流式接口是否设置了X-Accel-Buffering: noNginx 是否关闭缓冲是否配置了请求日志和错误日志是否了解上游模型服务商的价格、限流和并发限额现在每一次部署前我都会走一遍这张表。它能挡住大多数低级事故。7.2 三层日志法AI 接口的排错难度比普通接口高问题可能出在你的代码、模型服务商、网络链路三者的任意一环。我的日志实践分成三层第一层访问日志。记录每个请求的路径、状态码、耗时第二层业务日志。记录每次大模型调用的模型名、输入 token 数、输出 token 数、延迟、是否有截断第三层错误日志。记录异常堆栈以及请求时的上下文参数。有了这三层日志前端反馈“AI 回答有点怪”你能迅速定位是提示词的问题还是模型参数的问题用户投诉“响应好慢”你能通过业务日志看到到底是网络耗时还是推理耗时。7.3 从零至壹的下一步向量检索与多 AgentFastAPI 应用做到能稳定调用大模型之后下一步自然要接知识库和 Agent。向量检索解决的是“模型不知道你私有的数据”的问题常见做法是用 FastAPI 包一个/v1/embeddings接口配合向量数据库做内容召回多 Agent 则是在 FastAPI 里维护多个会话上下文每个 Agent 负责不同职责由主 Agent 做调度。我个人对后续迭代的建议是先把单 Agent 的检索增强做扎实再往上叠加多 Agent。顺序反了调试的复杂度会让你崩溃。7.4 一个压测细节上线前压测别只看 QPS 和平均延迟要盯着 P95 和 P99 延迟。大模型场景下请求耗时长少数几个慢请求就会拖垮用户体验。我压测时发现光把并发从 10 调到 20P99 延迟从 3 秒涨到 15 秒但平均延迟看起来还行。这时候就需要加并发控制或扩容 worker而不是盲目调超时时间。最后说一个我真实踩过的坑在“连接池”上栽过跟头。早期写的服务里每来一个请求就创建一个新的httpx.AsyncClient请求结束后再关闭表面看没问题可一旦并发上来频繁创建和销毁连接导致的性能损耗非常明显还会出现端口资源耗尽。后来改成模块级复用httpx.AsyncClient然后把pool_connections和pool_maxsize调大情况立刻好多了。所以如果你也在用 FastAPI 接大模型建议提前把连接池用好别等压测出问题了再回头改。