文章目录
- 一、为什么是 FastAPI?
- 1.1 FastAPI 是什么
- 1.2 核心组成:Starlette + Pydantic
- 二、环境安装与启动命令
- 2.1 安装
- 2.2 启动命令逐段解析
- 三、Demo 01:第一个接口 —— Hello World
- 3.1 完整代码
- 3.2 逐行拆解
- 3.3 路径参数与类型注解
- 四、Demo 02:请求体与 Pydantic —— 对接大模型接口
- 4.1 为什么需要 BaseModel
- 4.2 完整代码(FastAPI + LangChain 调用 DeepSeek)
- 4.3 关键步骤说明
- 4.4 校验效果
- 五、Demo 03:参数校验 —— Annotated + Path / Query
- 5.1 Annotated 是什么
- 5.2 Path:给路径参数加校验
- 5.3 Query:查询参数校验
- 5.4 三种参数工具对比
- 六、Demo 04:请求体字段校验 —— Field 与 model_dump
- 6.1 完整代码
- 6.2 可选字段的读法
- 6.3 Field:给字段加规则
- 6.4 model_dump 与字典展开
- 6.5 校验效果
- 七、Demo 05:综合实战 —— Todo 待办接口
- 7.1 完整代码(修正版)
- 7.2 数据设计思路
- 7.3 response_model:声明返回值结构
- 7.4 查单条与 404
- 八、async 异步:FastAPI 高性能的秘密
- 8.1 通俗理解
- 8.2 两种写法怎么选
- 九、全文总结
- 十、核心知识点复盘
本文基于一套循序渐进的实战 Demo(01~05),从第一个 Hello World 接口开始,经过请求体模型、参数校验、字段规则,最终完成一个 Todo 待办应用,并讲清楚 async 异步的原理。每个 Demo 的代码都完整可运行,坑点均来自真实调试过程。
一、为什么是 FastAPI?
1.1 FastAPI 是什么
FastAPI 是 Python 生态中的高性能 Web 接口框架,性能比肩 Go 和 Node.js。它的定位非常专一:只做后端 API,不用折腾繁琐配置,开发效率极高,特别适合配合 LangChain / LangGraph 开发大模型后端服务。
它打动人的三个特性:
| 特性 | 说明 |
|---|---|
| 高性能 | 基于异步无阻塞模型,天然适合高并发场景 |
| 省代码 | 写少量代码即可完成接口,自带类型提示 |
| 自动文档 | 自动生成交互式接口文档(Swagger),是前后端 API 约定的利器 |
1.2 核心组成:Starlette + Pydantic
【核心原理】FastAPI 本身不直接处理网络,它是站在两个库肩膀上的"组装框架":
- Starlette:Web 底层基座。负责接收 HTTP 请求、路由匹配、返回响应、处理网络,自带异步能力。
- Pydantic:类型校验库。功能类似前端 JS 生态里的 zod,负责校验用户输入(路径参数、查询参数、请求体),底层依赖 Python 类型注解。
请求的完整流转路径:
浏览器请求 ↓ Starlette(接收 HTTP / 路由匹配 / 异步调度) ↓ FastAPI(调度框架,串联一切) ↓ Pydantic(校验参数,不合格直接返回 422) ↓ 你写的业务函数(只关心业务逻辑) ↓ JSON 响应一个类比帮助记忆:Starlette 是发动机,Pydantic 是安检仪,FastAPI 是把二者组装好、让你只写业务逻辑的整车厂。
二、环境安装与启动命令
2.1 安装
pipinstall"fastapi[standard]"# 安装 fastapi 全家桶(含常用依赖)pipinstall"uvicorn[standard]"# uvicorn:异步 Web 服务器,用来运行 FastAPI 项目【重点】FastAPI 只是一个 Python 包,自己不会监听端口。必须由 uvicorn 这类 ASGI 服务器来托管运行。可以理解为:FastAPI 是应用,uvicorn 是跑应用的容器,二者缺一不可。
2.2 启动命令逐段解析
python-muvicorn main:app--reload--port8080| 片段 | 含义 |
|---|---|
python -m uvicorn | 用当前 Python 环境启动 uvicorn(比裸敲uvicorn更不容易用错解释器) |
main:app | 在main.py文件里找app这个变量(冒号读作"在……里面") |
--reload | 热重载:代码保存后自动重启服务,仅限开发环境 |
--port 8080 | 监听 8080 端口 |
【易错点】启动日志里常见的0.0.0.0不是用来在浏览器访问的地址。host="0.0.0.0"的含义是"监听本机所有网卡",是服务端的绑定配置。浏览器测试请访问http://127.0.0.1:8000或http://localhost:8000。
三、Demo 01:第一个接口 —— Hello World
3.1 完整代码
fromfastapiimportFastAPI# 1. 创建 FastAPI 应用实例,整个服务的入口对象app=FastAPI()# 2. 装饰器:把下面的函数注册为 "/" 路径的 GET 接口@app.get("/")asyncdefroot():# 返回字典,FastAPI 自动转成 JSON 响应return{"message":"World"}# 3. 路径参数:{name} 是占位符,请求 /hello/tom 时 name = "tom"@app.get("/hello/{name}")asyncdefsay_hello(name:str):return{"message":f"Hello,{name}!"}if__name__=="__main__":importuvicorn uvicorn.run("main:app",host="127.0.0.1",port=8000,reload=True)3.2 逐行拆解
app = FastAPI():创建应用实例,路由、文档都挂在它身上。@app.get("/"):装饰器是 Python 的语法糖,等价于root = app.get("/")(root),作用是把函数注册进路由表。当GET /请求到来时,Starlette 查路由表,找到root函数并执行。return {"message": "World"}:返回字典会被自动序列化为 JSON,并设置响应头Content-Type: application/json,你不用手动json.dumps。
【重点】装饰器路由是 FastAPI 的核心写法,本质就是"函数注册":@app.get("/路径")声明"什么请求路径、什么 HTTP 方法,交给哪个函数处理"。
3.3 路径参数与类型注解
/hello/{name}中的{name}是占位符。类型注解name: str有两个作用:
- 自动接收:访问
/hello/FastAPI,函数里name就是字符串"FastAPI"; - 自动转换:如果注解是
int,FastAPI 会把 URL 里的字符串转成整数,转换失败直接返回 422 错误。
四、Demo 02:请求体与 Pydantic —— 对接大模型接口
4.1 为什么需要 BaseModel
GET 请求的参数很简单,但 POST 通常传结构化的 JSON。手写校验长这样:
data=awaitrequest.json()prompt=data.get("prompt")ifnotisinstance(prompt,str):return{"error":"prompt 必须是字符串"}用BaseModel一行类型注解就能替代上面全部代码。
4.2 完整代码(FastAPI + LangChain 调用 DeepSeek)
fromdotenvimportload_dotenv# 读取 .env 文件的工具importosfromfastapiimportFastAPIfrompydanticimportBaseModel# 数据校验基类fromlangchain_openaiimportChatOpenAI# 把 .env 里的键值对加载进环境变量load_dotenv()app=FastAPI(title="LangChain & FastAPI")# 初始化大模型客户端(DeepSeek 兼容 OpenAI 协议)llm=ChatOpenAI(api_key=os.getenv("DEEPSEEK_API_KEY"),# 从环境变量读密钥base_url=os.getenv("DEEPSEEK_BASE_URL"),# 接口地址model=os.getenv("DEEPSEEK_MODEL"),# 模型名temperature=0.7,# 随机性,越大回答越发散)# 定义请求体模型:要求 JSON 里必须有 prompt 字段,且为字符串classChatReq(BaseModel):prompt:str# POST 接口:请求体经过 ChatReq 校验@app.post("/chat")asyncdefchat(req:ChatReq):resp=llm.invoke(req.prompt)# 调用大模型return{"input":req.prompt,"reply":resp.content}if__name__=="__main__":importuvicorn uvicorn.run("main:app",host="127.0.0.1",port=8000,reload=True)4.3 关键步骤说明
load_dotenv():把项目下.env文件的键值对注入环境变量,再用os.getenv()读取。密钥不写死在代码里,避免提交到 Git 泄露。ChatOpenAI(...):初始化一个"大模型客户端",之后llm.invoke("问题")就能拿到模型回答。class ChatReq(BaseModel):继承BaseModel后,ChatReq就是一个自带校验能力的请求模型,prompt: str表示必填且必须是字符串。req: ChatReq:FastAPI 看到参数类型是 Pydantic 模型,会自动完成"解析 JSON → 校验 → 实例化"三步。
4.4 校验效果
| 请求体 | 结果 |
|---|---|
{"prompt": "你好"} | 通过,req.prompt = "你好" |
{} | 422,缺少必填字段 prompt |
{"prompt": 123} | 422,数字不能当作字符串 |
{"prompt": "hi", "extra": 1} | 通过,多余字段默认忽略 |
【核心原理】校验发生在你的函数执行之前。不合法的请求直接被挡在门外返回 422,你的函数拿到的req一定是干净可用的数据——这就是"类型即校验"的含义。
五、Demo 03:参数校验 —— Annotated + Path / Query
5.1 Annotated 是什么
Annotated是 Python 标准库typing提供的类型注解工具,作用是给类型附加"元数据"(额外信息):
Annotated[真实类型,元数据1,元数据2,...]第一个参数是真正的数据类型,后面的元数据是附加说明。FastAPI 会读取这些元数据,把Path()、Query()的校验规则应用上去。
5.2 Path:给路径参数加校验
fromfastapiimportFastAPI,Path,QueryfromtypingimportAnnotated app=FastAPI()@app.get("/p/{article_id}")asyncdefarticle_detail(article_id:Annotated[int,Path(ge=2)]):# int:article_id 必须是整数(自动转换)# Path(ge=2):值必须 >= 2return{"article_id":article_id}比较运算符的含义:
| 写法 | 规则 | 缩写含义 |
|---|---|---|
Path(ge=2) | >= 2 | greater than or equal |
Path(le=100) | <= 100 | less than or equal |
Path(gt=0) | > 0 | greater than |
Path(lt=100) | < 100 | less than |
Path(ge=2, le=100) | 2 ~ 100 之间 | 组合使用 |
实际访问效果:
| 请求 | 结果 |
|---|---|
/p/5 | 通过,返回{"article_id": 5} |
/p/1 | 422,小于 2 |
/p/abc | 422,无法转成整数 |
【重点】为什么用Annotated而不是旧写法article_id: int = Path(ge=2)?因为新写法把"类型"和"规则"分开,语义清晰;必填参数也不会被误认为有默认值。
5.3 Query:查询参数校验
查询参数是 URL 问号后面的部分,多个参数用&连接:/article/list?page=2&size=5。
@app.get("/article/list")asyncdefarticle_list(page:Annotated[int,Query(1,ge=1)]=1,# 默认第 1 页,最小为 1size:Annotated[int,Query(10,ge=1)]=10,# 默认每页 10 条,最小为 1):return{"page":page,"size":size}Query(1, ge=1):第一个参数1是默认值,ge=1是校验规则。不传page时自动用默认值。- 【易错点】原 Demo 里
size写的是Query(10, ge=10),规则与默认值冲突——它意味着size最小只能是 10,传 5 会报错。如果业务本意是"每页最少 1 条",这是典型手误。校验规则必须和业务语义对齐。
5.4 三种参数工具对比
| 工具 | 用在哪 | 示例 |
|---|---|---|
Path() | URL 路径参数/p/{id} | Annotated[int, Path(ge=2)] |
Query() | 查询参数?page=1 | Annotated[int, Query(1, ge=1)] |
Field() | 请求体模型的字段 | Field(..., min_length=6) |
一句话记忆:URL 上的用 Path / Query,JSON 请求体里的用 Field。
六、Demo 04:请求体字段校验 —— Field 与 model_dump
6.1 完整代码
fromfastapiimportFastAPIfrompydanticimportBaseModel,FieldfromtypingimportAnnotated app=FastAPI()# 商品模型:description / tax 是可选字段classItem(BaseModel):name:str# 必填description:str|None=None# 可选:str 或 None,默认 Noneprice:float# 必填,自动转浮点数tax:float|None=None# 可选@app.put("/items/{item_id}")asyncdefupdate_item(item_id:int,item:Item):# model_dump() 把模型实例转成普通字典,** 再展开合并进结果result={"item_id":item_id,**item.model_dump()}returnresult# 登录模型:Field 给字段加校验规则classLogin(BaseModel):# ... 表示必填email:Annotated[str,Field(...,description="邮箱地址")]password:Annotated[str,Field(...,min_length=6,max_length=20,description="密码,6~20位")]@app.post("/login")asyncdeflogin(data:Login):return{"email":data.email,"password":data.password}if__name__=="__main__":importuvicorn uvicorn.run("main:app",host="127.0.0.1",port=8000,reload=True)6.2 可选字段的读法
description: str | None = None三段式读法:类型是str或None,默认值是None,即可以不传。而name: str没有默认值,就是必填。
6.3 Field:给字段加规则
Field是 Pydantic 提供的函数,给模型字段添加校验规则、默认值和说明。其中...是 Python 的省略号对象(Ellipsis),在Field里表示必填、无默认值。
Field(...)# 必填Field(None)# 可空,默认 NoneField(18,ge=0,le=150)# 默认 18,范围 0~150常用参数:
| 参数 | 作用 |
|---|---|
... | 必填 |
min_length/max_length | 字符串长度限制 |
gt/lt/ge/le | 数值范围 |
pattern | 正则表达式校验格式 |
description | 字段说明,显示在 /docs 文档里 |
6.4 model_dump 与字典展开
item.model_dump():把 Pydantic 模型实例转成普通字典(Pydantic v2 的方法,v1 时代叫.dict())。{"item_id": item_id, **item.model_dump()}:**把字典展开,与前面的键值对合并成一个新字典。
【重点】model_dump在很多场景都会遇到,比如把大模型返回的消息对象转成字典存进对话历史——"对象变字典"是高频操作。
6.5 校验效果
| 请求体 | 结果 |
|---|---|
{"email": "a@b.com", "password": "123456"} | 通过 |
{"password": "123456"} | 422,缺少必填的 email |
{"email": "a@b.com", "password": "123"} | 422,密码少于 6 位 |
七、Demo 05:综合实战 —— Todo 待办接口
7.1 完整代码(修正版)
原 Demo 存在几个小问题(第十一节逐条分析),下面是修正后的完整可运行版本:
fromfastapiimportFastAPI,HTTPException,PathfrompydanticimportBaseModel,FieldfromtypingimportAnnotated,List app=FastAPI(title="Todo增删改查")todos=[]# 内存列表当"数据库"next_id=1# 自增 ID# 用户提交用的模型:没有 id,防止用户伪造编号classTodoCreate(BaseModel):title:Annotated[str,Field(min_length=1,max_length=100,description="待办标题,1~100个字符")]done:bool=False# 可选,默认未完成# 存储和返回用的模型:继承 TodoCreate,服务端补上 idclassTodo(TodoCreate):id:int# 创建待办@app.post("/todos",summary="创建待办",response_model=Todo)asyncdefcreate_todo(data:TodoCreate):globalnext_id# 声明修改模块级变量todo=Todo(id=next_id,**data.model_dump())# 服务端分配 idnext_id+=1todos.append(todo)returntodo# 查询所有待办@app.get("/todos",summary="查询所有待办",response_model=List[Todo])asyncdefget_all_todos():returntodos# 查询单个待办@app.get("/todos/{todo_id}",summary="查询指定ID待办",response_model=Todo)asyncdefget_todo(todo_id:Annotated[int,Path(gt=0,description="ID必须大于0")]):foritemintodos:ifitem.id==todo_id:returnitem# 没找到:返回标准的 404 错误,而不是 raise 一个字典raiseHTTPException(status_code=404,detail="Todo not found")if__name__=="__main__":importuvicorn uvicorn.run("main:app",host="127.0.0.1",port=8000,reload=True)7.2 数据设计思路
todos列表充当"内存数据库",服务重启就清空——学习够用,生产环境要换成真正的数据库。TodoCreate(用户提交,无 id)与Todo(服务端存储与返回,带 id)分成两个模型:id 由服务端分配,避免用户提交伪造的编号。Todo继承TodoCreate获得全部字段,再加一个id。- 模型里没写
id却在路径参数上用Field校验是常见误用——路径参数应该用Path(对比表见 5.4 节)。
7.3 response_model:声明返回值结构
response_model=List[Todo]声明"这个接口返回的是 Todo 数组",有三大作用:
- 校验返回数据:函数返回后,FastAPI 按模型检查每一项,不符合直接报错,把问题拦在后端;
- 过滤多余字段:返回数据里模型之外的字段会被自动删掉,内部字段、敏感信息不会漏给前端;
- 生成响应文档:/docs 页面自动展示响应的 JSON 结构,前端不用问就知道格式。
summary="查询所有待办"则是纯文档用途,只显示在 /docs 的接口标题上,不影响任何逻辑。
7.4 查单条与 404
查单个待办用for循环匹配 id。找不到时必须返回 404 语义,标准做法是抛出HTTPException:
raiseHTTPException(status_code=404,detail="Todo not found")【易错点】raise后面只能跟异常对象,不能 raise 一个字典(如raise {"detail": "..."}),那会直接抛TypeError。
八、async 异步:FastAPI 高性能的秘密
8.1 通俗理解
把服务器想象成奶茶店的服务员:
- 同步(没有 async):接一单 → 站着等奶茶做完 → 才接下一单。等待期间什么都干不了,其他客人全在排队。
- 异步(async):接一单 → 把单子递给后台 →转头继续接下一单。奶茶好了再回来取。
同步与异步处理请求的时间线对比:
同步:一个线程死等,其余请求全部排队 线程 ├──请求A──等待A(卡住)──完成A──请求B──等待B(卡住)──完成B──▶ ↑ A 在等待时,B 只能干等 异步:等待时让出控制权,一个线程穿插处理多个请求 线程 ├──请求A──让出去→处理B──返回A──完成A──返回B──完成B──▶ ↑ A 等待的时间被用来处理 B,总耗时大幅缩短async def定义的函数就是"会安排事情的服务员":遇到耗时操作(数据库查询、文件读写、网络请求),先让出控制权去处理别的请求,结果好了再回来继续。
8.2 两种写法怎么选
| 函数写法 | FastAPI 怎么处理 | 适用场景 |
|---|---|---|
async def | 跑在事件循环里,函数内可用await | 内部使用异步库(异步数据库驱动、httpx 等) |
def(普通) | 自动丢到线程池执行,不会阻塞服务 | 内部是同步阻塞调用(如 requests、同步 ORM) |
选型原则一句话:函数里要await就写async def;全是同步阻塞调用就写普通def。两种 FastAPI 都能正确调度,不必强求全 async。
九、全文总结
本文沿一条主线走完了 FastAPI 的入门路径:Hello World(路由注册、路径参数)→ 请求体模型(BaseModel 对接大模型)→ 参数校验(Annotated + Path / Query)→ 字段规则(Field、model_dump)→ 综合实战(Todo 接口 + response_model)→ 异步原理(async / await)。
贯穿始终的核心思想只有一个:FastAPI 把路由、参数解析、数据校验、接口文档全部自动化,你只需要写业务函数本身。类型注解不再是"装饰",而是被框架真正消费的配置——写对类型,校验、转换、文档全部免费获得。
十、核心知识点复盘
| 知识点 | 一句话要点 |
|---|---|
| FastAPI 组成 | Starlette 负责 Web 底层与异步,Pydantic 负责类型校验 |
| uvicorn | ASGI 服务器,FastAPI 应用必须由它托管运行 |
| 装饰器路由 | @app.get("/路径")把函数注册进路由表 |
| 路径参数 | {name}占位符 + 类型注解自动转换 |
| 请求体模型 | 继承BaseModel,字段类型即校验规则 |
| Annotated | 给类型附加元数据,Annotated[int, Path(ge=2)] |
| Path / Query / Field | URL 路径参数 / 查询参数 / 请求体字段,各管一摊 |
... | Field 里的省略号,表示必填 |
| model_dump | 模型实例转字典(对象变字典) |
| response_model | 校验返回值、过滤多余字段、生成响应文档 |
| HTTPException | 标准错误返回方式,如 404 |
| async / await | 有异步库用async def,纯同步代码用def |
祝你在 FastAPI 的路上一路畅通。