前言
大模型应用火了一年多,很多人都在用 Python + LangChain 写脚本,但真要做成产品,就必须有后端 API。这个时候问题来了:用 Flask?太老了;用 Django?太重了;用 Node?还得再学一套。
FastAPI 就是那个"刚刚好"的答案------性能比肩 Go/Node,类型提示自带文档,代码量少到离谱,而且跟 LangChain 家族天生一对。
这篇文章,我会带你从零开始,把 FastAPI 的核心用法 + 与 LangChain 集成的实战经验,一次性讲透。
一、FastAPI 到底强在哪?
先给不熟悉的朋友一句大白话总结:
FastAPI = Pydantic(类型校验)+ Starlette(异步 Web 内核)
拆开看:
| 组件 | 角色 | 类比 |
|---|---|---|
| Starlette | Web 底层:接收 HTTP、路由匹配、返回响应、处理异步 | 相当于 Node 的 Koa |
| Pydantic | 数据校验:请求参数、请求体、响应模型 | 相当于前端的 Zod |
这两个组合起来,带来几个杀手级特性:
- 自动生成交互式文档:Swagger UI 和 ReDoc 白送,前后端 API 约定再也不用扯皮
- 原生异步无阻塞 :
async/await一把梭,等数据库、等文件 IO、等 LLM 响应都不卡线程 - 类型提示驱动:写少量代码就能拿到强类型校验,编辑器补全也舒服
- 专为 API 而生:不用折腾繁琐配置,特别适合给 LangChain/LangGraph 做后端服务
一句话总结:FastAPI 让写接口这件事,回归到"写函数"本身。
二、环境搭建:两步走
日常开发用标准版就够了:
# FastAPI 全家桶(包含 uvicorn、httpx 等)
pip install "fastapi[standard]"
# 如果想要更完整的 uvicorn 特性(比如 websocket、watchfiles 热重载)
pip install "uvicorn[standard]"
验证一下:
pip show fastapi
启动服务有两种方式:
# 方式一:命令行启动(推荐开发用,--reload 热重载)
python -m uvicorn main:app --reload --port 8080
# 方式二:代码里启动(见下方完整示例)
三、从 Hello World 开始
不多说,先跑通再说:
from fastapi import FastAPI
# 初始化 FastAPI 应用实例
app = FastAPI()
# 装饰器模式注册路由
@app.get("/")
async def root():
return {"message": "Hello World"}
# 路径参数:Pydantic 会自动把 name 转成 str
@app.get("/hello/{name}")
async def hello(name: str):
return {"message": f"你好 {name}"}
跑起来后访问 http://127.0.0.1:8000/docs,你会看到 自动生成的 Swagger 文档,直接就能测试接口。
这就是 FastAPI 的第一个爽点:接口即文档,文档即测试。
四、参数校验:Annotated 的艺术
FastAPI 的参数校验,靠的是 Annotated 这个神器。语法是:
Annotated[类型, 校验规则]
1. 路径参数(Path)
from fastapi import FastAPI, Path
from typing import Annotated
app = FastAPI()
@app.get("/p/{article_id}")
async def article_detail(
article_id: Annotated[int, Path(ge=2, description="文章ID,必须≥2")]
):
return {"article_id": article_id}
⚠️ 踩坑提醒 :
Path(ge=2)这种数值约束只能用在int/float上。如果你写成Annotated[str, Path(ge=2)],虽然启动不报错,但校验逻辑是错的(字符串不该比大小)。字符串要用min_length/max_length。
2. 查询参数(Query)
from fastapi import Query
# http://0.0.0.0:8000/article/list?page=2&size=5
@app.get("/article/list")
async def article_list(
page: Annotated[int, Query(ge=1)] = 1,
size: Annotated[int, Query(ge=1, le=100)] = 10,
):
return {"page": page, "size": size}
看到了吗?类型提示 + 默认值 + 校验规则,三个东西一行搞定,FastAPI 会自动做类型转换和校验,参数错了直接返回 422 错误,你一行校验代码都不用写。
五、请求体校验:Pydantic Model 才是主场
GET 请求用 Path/Query 就够了,但 POST/PUT 的请求体,得靠 Pydantic 的 BaseModel:
from fastapi import FastAPI
from pydantic import BaseModel, Field
from typing import Annotated
app = FastAPI()
class Item(BaseModel):
name: str
description: str | None = None
price: float
tax: float | None = None
@app.put("/items/{item_id}")
async def update_item(item_id: int, item: Item):
# model_dump() 把实例转成 dict
# ** 展开字典合并
result = {"item_id": item_id, **item.model_dump()}
return result
Field:给字段加规则
class LoginIn(BaseModel):
email: Annotated[str, Field(..., description="邮箱地址")]
password: Annotated[str, Field(
...,
min_length=6,
max_length=20,
description="密码,6-20位"
)]
@app.post("/login")
async def login(data: LoginIn):
return {"email": data.email, "password": data.password}
这里的 ...(Ellipsis)表示必填 。配合 Field 可以加 min_length、max_length、gt、ge、description 等一堆约束。
金句来了:写 FastAPI,你的代码就是你的文档。 类型定义即校验规则,Field 里的 description 直接进 Swagger,前端看了直呼内行。
六、实战:接入 LangChain,5 分钟搞定一个 Chat API
好戏开始了。我们把 DeepSeek 接进来,做一个最简版的 Chat 接口:
import os
from dotenv import load_dotenv
from fastapi import FastAPI
from pydantic import BaseModel
from langchain_openai import ChatOpenAI
load_dotenv()
app = FastAPI(title="Langchain & FastAPI")
# 初始化 LLM
llm = ChatOpenAI(
api_key=os.getenv("DEEPSEEK_API_KEY"),
base_url=os.getenv("DEEPSEEK_BASE_URL"),
model=os.getenv("DEEPSEEK_MODEL"),
temperature=0.7,
)
class ChatReq(BaseModel):
prompt: str
@app.post("/chat")
async def chat(req: ChatReq):
resp = llm.invoke(req.prompt)
return {
"input": req.prompt,
"reply": resp.content, # 注意:不是 replay 😂
}
if __name__ == "__main__":
import uvicorn
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
.env 文件长这样:
DEEPSEEK_API_KEY=sk-xxxxxxxx
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
DEEPSEEK_MODEL=deepseek-chat
启动后,在 /docs 里直接试一发:
{
"prompt": "用一句话解释什么是 FastAPI"
}
为什么这么配?
因为 ChatOpenAI 兼容 OpenAI 协议,DeepSeek、通义千问、Kimi 之类只要提供 OpenAI 兼容接口,都能这么接。换个 base_url 就换模型,非常丝滑。
进阶提示
llm.invoke() 是同步阻塞调用,如果并发量大,建议改成异步:
resp = await llm.ainvoke(req.prompt)
配合 FastAPI 的异步能力,单机轻松扛住几十上百并发。
七、实战:写一个完整的 Todo CRUD
光说不练假把式,我们来写个 Todo 的增删查:
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel, Field
from typing import Annotated, Optional, List
app = FastAPI(title="Todo 增删改查")
todos: List[dict] = []
next_id = 1
class Todo(BaseModel):
title: Annotated[str, Field(
min_length=1,
max_length=100,
description="待办事项的标题,1-100 字符"
)]
done: Annotated[Optional[bool], Field(default=False, description="是否完成")]
@app.get("/todos", summary="查询所有待办", response_model=List[Todo])
def read_todos():
return todos
@app.get("/todos/{todo_id}", summary="查询单个待办", response_model=Todo)
def get_todo(
todo_id: Annotated[int, Field(..., gt=0, description="Todo ID, 必须大于 0")]
):
for item in todos:
if item["id"] == todo_id:
return item
# ✅ 注意:return 要写在 for 循环外面
raise HTTPException(status_code=404, detail="找不到这条 todo")
原笔记里的两个坑,一起聊聊
坑 1:return 写在 for 循环里面
原代码是这样:
for item in todos:
if item["id"] == todo_id:
return item
return {"msg": "找不到这条todo"} # ❌ 第一次循环就返回了
这会导致一个问题:只要第一次循环的 id 不匹配,就直接返回"找不到",后面的 todo 永远查不到 。正确写法是 return 挪到循环外面,配合 raise HTTPException 更规范。
坑 2:response_model 与 dict 混用
response_model=Todo 表示返回结果会被 Pydantic 按 Todo 模型过滤和序列化。如果你 return {"msg": "..."} 这种不含 title 的字典,FastAPI 会直接报响应校验错误。所以找不到的时候,用 HTTPException 更符合语义。
八、总结:为什么 FastAPI 值得你投入
最后收个尾,把本文几个关键点串一下:
- 架构清晰:Starlette 管 Web 通信,Pydantic 管数据校验,各司其职
- 写少做多 :
Annotated+BaseModel+Field三件套,校验 + 文档全自动 - 异步友好 :原生
async/await,接 LLM、数据库、文件 IO 都能无阻塞 - AI 后端首选 :和 LangChain 集成就是几行代码的事,换模型只需改
base_url - 自动文档:Swagger UI 白送,前后端对接效率翻倍
FastAPI 让你把"接口校验"和"接口文档"这两件最烦的事,变成了写代码时的副产品。