前端转 AI 第 21 天:FastAPI 入门 - 从命令行到 HTTP 服务
(明天国庆,假期期间不学习,博主要出去玩!!!假期后继续学习)
昨日回顾
Day 20 把助手打包成了 CLI 工具,能 pip install、能发布。
但有个问题------它只能在本地跑,别人用不了。
想分享给朋友?让他装 Python、装依赖、跑命令?门槛太高。
该上后端了。 今天开始 FastAPI,把助手变成一个 HTTP 服务,任何能发请求的地方都能用------网页、App、其他脚本、Postman。
Day 21 目标:搞懂 FastAPI 的核心,能写接口,把 Day 20 的助手改造成 HTTP 服务。
一、为什么选 FastAPI
Python 的 Web 框架不少,为什么 FastAPI 是AI 应用的首选?
1. 主流框架对比
| 框架 | 特点 | 类型 | 适合 |
|---|---|---|---|
| Flask | 极简、灵活 | 同步 | 小项目、传统 Web |
| Django | 全家桶、重 | 同步 | 大型 CMS、后台 |
| FastAPI | 现代、异步、类型驱动 | 异步 | API、AI 应用 |
| Sanic | 纯异步、快 | 异步 | 高并发 |
| Tornado | 老牌异步 | 异步 | WebSocket |
2. FastAPI 的三大优势
优势一:基于 Pydantic(Day 11 的成果直接用)
python
from pydantic import BaseModel
class ChatRequest(BaseModel):
message: str
temperature: float = 0.7
@app.post("/chat")
def chat(req: ChatRequest):
# req 自动校验、自动解析、自动补默认值
...
你只写数据模型,FastAPI 帮你做校验 + 解析 + 错误处理。
优势二:自动生成 API 文档
FastAPI 启动后自带两个文档界面:
/docs:Swagger UI(可交互)/redoc:ReDoc(更清爽)
你一行文档都没写,它自动根据 Pydantic 模型和类型注解生成。 前端联调时直接给这个链接就行。
优势三:原生异步
python
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await db.get_user(user_id) # 直接 await
return user
Agent 里大量 async 调用(LLM、向量库、HTTP),FastAPI 天生匹配。 Flask / Django 都是同步的,想做异步要额外折腾。
结论:做 AI 应用,没有理由不用 FastAPI。
3. 安装
bash
pip install "fastapi[standard]"
[standard] 会连 uvicorn 等常用工具一起装上。
二、第一个 FastAPI 应用
1. Hello World
新建 day21/first_api/main.py:
python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello, FastAPI"}
@app.get("/health")
def health_check():
return {"status": "ok"}
2. 启动
bash
cd day21/first_api
uvicorn main:app --reload
参数解释:
main:app:main.py文件里的app对象--reload:改代码自动重启(开发模式)
输出:
vbnet
INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO: Started reloader process [12345]
INFO: Started server process [12346]
INFO: Application startup complete.
3. 测试
打开浏览器:
- http://127.0.0.1:8000/ →
{"message": "Hello, FastAPI"} - http://127.0.0.1:8000/health →
{"status": "ok"}
或者用 curl:
bash
curl http://127.0.0.1:8000/
或前端习惯的写法:
js
fetch("http://127.0.0.1:8000/").then(r => r.json()).then(console.log)
4. 见证奇迹:打开 /docs
你会看到一个完整的、可交互的 Swagger UI:
- 列出了所有接口
- 每个接口都有"Try it out"按钮(可以直接在浏览器里测试)
- 参数说明、返回结构、错误码全有
你一行文档都没写! 这就是 FastAPI 最爽的地方。
**再访问 http://127.0.0.1:8000/redoc**------另一个风格的文档界面。
三、路由与参数
1. 路径参数
python
@app.get("/users/{user_id}")
def get_user(user_id: int): # ← 声明 int,自动转换
return {"user_id": user_id, "type": type(user_id).__name__}
访问 /users/123:
json
{"user_id": 123, "type": "int"}
访问 /users/abc:
json
{
"detail": [
{
"loc": ["path", "user_id"],
"msg": "Input should be a valid integer...",
"type": "int_parsing"
}
]
}
看到没?int 类型不匹配,FastAPI 自动返回 422 错误,还告诉你哪里错了。
前端视角: 相当于给每个参数都配了 zod 校验,还自动生成错误信息。
2. 查询参数
python
@app.get("/search")
def search(
q: str,
limit: int = 10,
sort: str = "desc",
):
return {"q": q, "limit": limit, "sort": sort}
访问 /search?q=python&limit=5:
json
{"q": "python", "limit": 5, "sort": "desc"}
规则:
- 函数参数没在路径里 → FastAPI 自动识别为查询参数
- 有默认值 → 可选参数
- 没默认值 → 必填参数(不传报错)
3. 用 Query 加约束
python
from fastapi import Query
@app.get("/items")
def list_items(
q: str | None = Query(default=None, max_length=50, description="搜索关键词"),
skip: int = Query(default=0, ge=0, description="跳过多少条"),
limit: int = Query(default=10, ge=1, le=100, description="返回多少条"),
):
return {"q": q, "skip": skip, "limit": limit}
ge / le / max_length 和 Day 11 的 Pydantic Field 一样------因为 FastAPI 就是用 Pydantic 做的。
description 会显示在 /docs 里。 前端联调时一目了然。
4. 路径参数 with 约束
python
from fastapi import Path
@app.get("/users/{user_id}")
def get_user(
user_id: int = Path(ge=1, description="用户 ID,正整数"),
):
return {"user_id": user_id}
四、请求体:POST 的标配
这是 FastAPI 最强的地方------用 Pydantic 模型定义请求体。
python
from pydantic import BaseModel, Field
class ChatMessage(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
messages: list[ChatMessage]
model: str = "gpt-4o-mini"
temperature: float = Field(default=0.7, ge=0, le=2)
max_tokens: int = Field(default=1024, ge=1, le=32000)
@app.post("/chat")
def chat(req: ChatRequest):
# 到这里 req 已经被完整校验
last = req.messages[-1]
return {
"reply": f"你说的是:{last.content}",
"model": req.model,
"tokens": 42,
}
前端发请求:
js
fetch("http://127.0.0.1:8000/chat", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
messages: [{ role: "user", content: "你好" }],
temperature: 0.7,
}),
});
你什么都没写,FastAPI 就做到了:
- ✅ 校验
messages非空 - ✅ 校验
temperature在 0-2 之间 - ✅ 校验
max_tokens是正整数 - ✅ 自动补默认值
- ✅ 类型转换(
"0.7"→0.7) - ✅ 生成错误信息
- ✅ 在
/docs里展示结构
这就是 Day 11 说的"Pydantic 是 Agent 底牌"的最好体现。
1. 混合使用:路径 + 查询 + 请求体
python
@app.put("/users/{user_id}")
def update_user(
user_id: int, # 路径参数
send_email: bool = False, # 查询参数
user: UserUpdate = ..., # 请求体(Pydantic 模型)
):
return {"user_id": user_id, "send_email": send_email, "user": user}
FastAPI 自动识别三种参数的位置------靠类型注解区分。
2. 单值请求体
如果请求体就是一个简单值(比如一个字符串),用 Body:
python
from fastapi import Body
@app.post("/echo")
def echo(text: str = Body(embed=True)):
return {"text": text}
请求体:
json
{"text": "hello"}
embed=True表示"包一层对象"。 不写的话请求体就是裸字符串"hello"。
五、响应:response_model 和状态码
1. response_model:自动过滤敏感字段
场景: 用户对象有 password_hash,返回时不能带上。
python
class UserInDB(BaseModel):
id: int
name: str
password_hash: str # 敏感
class UserOut(BaseModel):
id: int
name: str
@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int) -> UserInDB:
return UserInDB(id=1, name="张三", password_hash="xxx")
返回给客户端的只有 {"id": 1, "name": "张三"},password_hash 自动被过滤。
这个特性非常重要------防止不小心泄露字段。
2. 直接返回 Pydantic 模型
python
@app.get("/user", response_model=UserOut)
def get_user():
return UserOut(id=1, name="张三") # FastAPI 自动转 JSON
和返回 dict 一样,但类型更明确。
3. 状态码
python
from fastapi import status
@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: UserIn):
...
return {"id": 1}
常用状态码:
| 状态码 | 含义 |
|---|---|
200 |
成功(默认) |
201 |
创建成功 |
204 |
成功但无内容(删除) |
400 |
客户端错误 |
401 |
未认证 |
403 |
无权限 |
404 |
找不到 |
422 |
参数校验失败(FastAPI 自动用) |
500 |
服务器错误 |
4. 抛异常:HTTPException
python
from fastapi import HTTPException
@app.get("/users/{user_id}")
def get_user(user_id: int):
user = db.get(user_id)
if not user:
raise HTTPException(
status_code=404,
detail=f"用户 {user_id} 不存在",
)
return user
客户端收到:
json
{"detail": "用户 123 不存在"}
HTTPException 会自动转成 JSON 响应 + 状态码。
六、中间件和生命周期
1. CORS:前端联调必配
前端在 localhost:5173 起服务,后端在 localhost:8000------浏览器会拦。
python
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"], # 前端地址
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
开发阶段可以先 allow_origins=["*"] 方便调试,生产环境再收紧。
这个问题前端同学最熟------浏览器跨域,后端加 CORS 头就能解决。
2. 启动和关闭:lifespan
FastAPI 推荐用 lifespan 管理资源(数据库连接、LLM 客户端):
python
from contextlib import asynccontextmanager
from fastapi import FastAPI
@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动时执行
print("启动:初始化数据库")
app.state.db = init_database()
yield # ← 应用运行期间
# 关闭时执行
print("关闭:清理资源")
app.state.db.close()
app = FastAPI(lifespan=lifespan)
@app.get("/")
def root():
db = app.state.db # 从 app.state 拿
return {"status": "ok"}
好处:
- 一次性初始化:不用每个请求都建连接
- 优雅关闭:进程退出前能清理资源
- 可测试 :可以在测试里替换
app.state.db
Day 23 会详细讲这套模式。
七、实战:把助手改造成 HTTP 服务
把 Day 20 的命令行助手改造成 HTTP API。
1. 项目结构
bash
cp -r day20/ai-assistant-v7 day21/ai-assistant-api
cd day21/ai-assistant-api
目录调整:
bash
src/ai_assistant/
├── __init__.py
├── main.py # CLI 入口(保留)
├── api/
│ ├── __init__.py
│ ├── app.py # ← FastAPI 实例(新增)
│ ├── routes.py # ← 路由定义(新增)
│ ├── schemas.py # ← API 请求/响应模型(新增)
│ └── deps.py # ← 依赖注入(新增)
├── config.py
├── models.py
├── orm_models.py
├── db_orm.py
├── storage_orm.py
├── llm.py
├── memory.py
└── ...
新增 api/ 子包,与 CLI 层解耦。
2. api/schemas.py:API 请求响应模型
python
"""API 请求/响应模型"""
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Literal
Role = Literal["system", "user", "assistant"]
class MessageSchema(BaseModel):
role: Role
content: str = Field(min_length=1, max_length=10000)
class ChatRequest(BaseModel):
"""对话请求"""
message: str = Field(min_length=1, max_length=10000, description="用户消息")
session_id: str | None = Field(default=None, description="会话 ID,不传则创建新会话")
class ChatResponse(BaseModel):
"""对话响应"""
session_id: str
reply: str
message_count: int
class SessionInfo(BaseModel):
"""会话信息"""
id: str
title: str
updated_at: datetime
message_count: int
class MessageInfo(BaseModel):
"""消息信息"""
role: Role
content: str
timestamp: datetime
class SessionDetail(BaseModel):
"""会话详情"""
id: str
title: str
messages: list[MessageInfo]
created_at: datetime
updated_at: datetime
class SessionListResponse(BaseModel):
"""会话列表响应"""
sessions: list[SessionInfo]
total: int
关键点:这些模型和 Day 14 的业务模型不同。
- 业务模型 (
models.py):内部用 - API 模型 (
schemas.py):对外用
为什么要分开?
- 业务模型可能变化:内部字段调整不影响 API
- API 模型有约束 :
max_length/description是给前端看的 - 可以隐藏字段:内部字段不对 API 暴露
这是"API 契约"的思想。 前端联调时看 /docs 就够了,不用读你源码。
3. api/deps.py:依赖注入
FastAPI 的"依赖注入"是一大特色------把"每次请求都需要的东西"抽出来复用。
python
"""依赖注入"""
from fastapi import Depends
from functools import lru_cache
from ..config import config
from ..db_orm import DatabaseORM
from ..storage_orm import SessionStorageORM
from ..llm import MockLLMClient
from ..memory import MemoryManager
@lru_cache
def get_db() -> DatabaseORM:
"""全局唯一的数据库连接"""
return DatabaseORM(config.DATA_DIR / "assistant.db")
@lru_cache
def get_storage() -> SessionStorageORM:
return SessionStorageORM(get_db())
def get_llm() -> MockLLMClient:
return MockLLMClient()
def get_memory() -> MemoryManager:
return MemoryManager(
max_context_tokens=8000,
recent_window=10,
summary_threshold=20,
)
lru_cache 保证 get_db() 只执行一次------第一次调用建连接,后续直接返回同一个对象。
用法:
python
@app.get("/sessions")
def list_sessions(storage: SessionStorageORM = Depends(get_storage)):
return storage.list_sessions()
Depends(get_storage) 告诉 FastAPI:
这个接口需要
storage,帮我调用get_storage()拿一个。
好处:
- 不用手动传参
- 可测试 :测试时替换
get_storage就行 - 可复用:多个接口共享同一个依赖
前端类比: 有点像 React 的 Context,或者 NestJS 的 @Injectable()。
4. api/routes.py:路由定义
python
"""API 路由"""
from fastapi import APIRouter, HTTPException, Depends
from ..models import Session as SessionModel, Message
from ..llm import LLMError
from ..storage_orm import SessionStorageORM
from .schemas import (
ChatRequest, ChatResponse,
SessionInfo, SessionListResponse,
SessionDetail, MessageInfo,
)
from .deps import get_storage, get_llm, get_memory
router = APIRouter(prefix="/api", tags=["assistant"])
# ============ 健康检查 ============
@router.get("/health")
def health():
return {"status": "ok"}
# ============ 对话 ============
@router.post("/chat", response_model=ChatResponse)
async def chat(
req: ChatRequest,
storage: SessionStorageORM = Depends(get_storage),
llm = Depends(get_llm),
memory = Depends(get_memory),
):
"""
发送一条消息,返回助手的回复
- 如果传了 session_id,就在该会话里继续对话
- 如果不传,会创建一个新会话
"""
# 1. 加载或创建会话
if req.session_id:
session = storage.load(req.session_id)
if not session:
raise HTTPException(status_code=404, detail=f"会话不存在: {req.session_id}")
else:
import uuid
session = SessionModel(session_id=uuid.uuid4().hex[:8])
# 2. 加用户消息
session.add_message("user", req.message)
# 3. 组装上下文
context = memory.get_context(session.messages)
messages = [{"role": "system", "content": "你是一个友好的 AI 助手"}]
messages.extend(m.to_llm_format() for m in context)
# 4. 调 LLM
try:
reply = await llm.chat(messages)
except LLMError as e:
raise HTTPException(status_code=503, detail=f"模型调用失败: {e}")
# 5. 加助手回复 + 保存
session.add_message("assistant", reply)
storage.save(session)
return ChatResponse(
session_id=session.session_id,
reply=reply,
message_count=session.message_count,
)
# ============ 会话管理 ============
@router.get("/sessions", response_model=SessionListResponse)
def list_sessions(storage: SessionStorageORM = Depends(get_storage)):
"""列出所有会话"""
sessions = storage.list_sessions()
return SessionListResponse(
sessions=[SessionInfo(**s) for s in sessions],
total=len(sessions),
)
@router.get("/sessions/{session_id}", response_model=SessionDetail)
def get_session(session_id: str, storage: SessionStorageORM = Depends(get_storage)):
"""获取会话详情"""
session = storage.load(session_id)
if not session:
raise HTTPException(status_code=404, detail="会话不存在")
return SessionDetail(
id=session.session_id,
title=session.title,
messages=[
MessageInfo(role=m.role, content=m.content, timestamp=m.timestamp)
for m in session.messages
],
created_at=session.created_at,
updated_at=session.updated_at,
)
@router.delete("/sessions/{session_id}", status_code=204)
def delete_session(session_id: str, storage: SessionStorageORM = Depends(get_storage)):
"""删除会话"""
if not storage.delete(session_id):
raise HTTPException(status_code=404, detail="会话不存在")
@router.get("/search")
def search_sessions(
q: str,
limit: int = 20,
storage: SessionStorageORM = Depends(get_storage),
):
"""搜索包含关键字的会话"""
results = storage.search(q, limit=limit)
return {"keyword": q, "results": results, "count": len(results)}
看几个关键设计:
1. APIRouter 统一前缀和标签
python
router = APIRouter(prefix="/api", tags=["assistant"])
所有路由自动加 /api 前缀 ,/docs 里分组显示。
2. Depends 拿依赖
python
storage: SessionStorageORM = Depends(get_storage)
不用手动 storage = get_storage(),FastAPI 帮你注入。 而且同一个请求里多次 Depends(get_storage) 会拿到同一个实例(有缓存机制)。
3. 业务异常转 HTTP 异常
python
if not session:
raise HTTPException(status_code=404, detail="会话不存在")
内部业务逻辑不关心 HTTP 状态码,路由层负责转换。
这是"分层架构"的体现------业务层不知道 HTTP 的存在。
4. 异步 + 同步混用
python
@router.post("/chat", ...)
async def chat(...): # 因为内部有 await llm.chat,所以用 async
...
@router.get("/sessions", ...)
def list_sessions(...): # 纯数据库操作,用 def 就行(FastAPI 会丢到线程池)
...
规则:
- 内部有
await→ 用async def - 纯同步代码 (数据库、CPU 计算) → 用
def,FastAPI 会自动放线程池
别把所有接口都写成 async ------同步代码在 async def 里会阻塞事件循环。
5. api/app.py:FastAPI 实例
python
"""FastAPI 应用"""
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from ..logger import get_logger
from .routes import router
from .deps import get_db
logger = get_logger(__name__)
@asynccontextmanager
async def lifespan(app: FastAPI):
"""启动/关闭钩子"""
logger.info("API 启动")
db = get_db()
# 数据库已在 init 时建表,这里不需要额外操作
yield
logger.info("API 关闭")
db.close()
def create_app() -> FastAPI:
"""应用工厂"""
app = FastAPI(
title="AI 助手 API",
description="前端转 AI 学习项目的后端服务",
version="0.1.0",
lifespan=lifespan,
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=["*"], # 开发阶段放开,生产收紧
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 挂路由
app.include_router(router)
return app
app = create_app()
关键点:
1. lifespan 管理资源
启动时 :初始化数据库;关闭时:清理资源。
2. create_app() 应用工厂
为什么用工厂函数,而不是直接 app = FastAPI()?
- 可测试:测试时可以传不同配置
- 可多实例:某些场景需要创建多个 app
- 更规范:社区推荐模式
3. app.include_router()
把路由挂到 app 上。 后续加新模块(比如 /api/tools)就再写一个 router,include 进来就行。
6. 启动 API
在 pyproject.toml 里加一个命令:
toml
[project.scripts]
ai-assistant = "ai_assistant.main:main"
ai-assistant-api = "ai_assistant.api.run:main"
创建 api/run.py:
python
"""API 启动入口"""
import uvicorn
from ..logger import setup_logging
def main():
setup_logging("INFO")
uvicorn.run(
"ai_assistant.api.app:app",
host="0.0.0.0",
port=8000,
reload=True, # 开发模式
)
if __name__ == "__main__":
main()
运行:
bash
pip install -e .
ai-assistant-api
或直接:
bash
uvicorn ai_assistant.api.app:app --reload
访问 http://localhost:8000/docs,看到完整文档。
7. 测试整个流程
用 curl 或 Postman 测试:
1. 发一条消息(不传 session_id):
bash
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "你好"}'
响应:
json
{
"session_id": "a3f9b2e1",
"reply": "你好!我是一个模拟的 AI 助手...",
"message_count": 2
}
2. 继续这个会话:
bash
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "Python 是什么", "session_id": "a3f9b2e1"}'
3. 列出所有会话:
bash
curl http://localhost:8000/api/sessions
4. 获取会话详情:
bash
curl http://localhost:8000/api/sessions/a3f9b2e1
5. 删除会话:
bash
curl -X DELETE http://localhost:8000/api/sessions/a3f9b2e1
6. 搜索:
bash
curl "http://localhost:8000/api/search?q=Python"
8. 前端调用示范
前端拿到 API 后,fetch 就好:
js
const API_BASE = "http://localhost:8000/api";
async function chat(message, sessionId = null) {
const res = await fetch(`${API_BASE}/chat`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message, session_id: sessionId }),
});
if (!res.ok) {
const err = await res.json();
throw new Error(err.detail);
}
return res.json();
}
// 使用
const result = await chat("你好");
console.log(result.reply);
你前端的 fetch 代码,一行不用改------FastAPI 返回的就是标准 JSON + 状态码。
9. 从 curl 到前端,全链路打通
你现在有了一个完整的 AI 助手服务:
bash
┌─────────────┐
│ 前端页面 │ ← fetch / axios
│ (React) │
└──────┬──────┘
│ HTTP
▼
┌─────────────┐
│ FastAPI │ ← /api/chat /api/sessions
└──────┬──────┘
│
▼
┌─────────────┐
│ 业务层 │ ← SessionStorageORM / MemoryManager
└──────┬──────┘
│
▼
┌─────────────┐
│ SQLite │
└─────────────┘
Day 46 接真 LLM 时,只需要改 llm.py。 API 层、业务层、存储层全不动。
这就是 Day 13 说的"架构好,改一处不影响全局"。
八、前端视角:FastAPI vs Express/Koa
做过前端的你,会觉得 FastAPI 极其顺手。
| 概念 | Express | FastAPI |
|---|---|---|
| 应用实例 | const app = express() |
app = FastAPI() |
| 定义路由 | app.get("/", handler) |
@app.get("/") 装饰器 |
| 路径参数 | req.params.id |
函数参数 user_id: int |
| 查询参数 | req.query.q |
函数参数 q: str |
| 请求体 | req.body(要 body-parser) |
Pydantic 模型(自动) |
| 校验 | 手写或 zod | Pydantic(内置) |
| 错误处理 | res.status(404).json(...) |
raise HTTPException(404, ...) |
| 中间件 | app.use(...) |
app.add_middleware(...) |
| 自动文档 | 装 swagger-jsdoc | 内置 |
| 类型安全 | TypeScript | Python 类型注解 |
核心差异:
1. FastAPI 是"类型驱动"的
Express 里你得手写校验:
js
app.post("/chat", (req, res) => {
const { message } = req.body;
if (!message || typeof message !== "string") {
return res.status(400).json({ error: "message required" });
}
// ...
});
FastAPI 里:
python
@app.post("/chat")
def chat(req: ChatRequest):
# 校验已经做完了
...
2. FastAPI 自带文档
Express 要装 swagger-jsdoc 或 tsoa 才能有文档。 FastAPI 天然有。
3. 异步语法几乎一样
Express:
js
app.get("/users/:id", async (req, res) => {
const user = await db.getUser(req.params.id);
res.json(user);
});
FastAPI:
python
@app.get("/users/{user_id}")
async def get_user(user_id: int):
user = await db.get_user(user_id)
return user
一一对应,几乎无痛迁移。
九、今日踩坑
uvicorn main:app找不到模块 → 要在项目根目录执行,或者装成包pip install -e .@app.get忘了写 → 路由没注册,404- 所有接口都写
async def→ 同步代码在async def里会阻塞事件循环,纯同步用def - Pydantic 模型和业务模型混用 → 分开定义,API 契约更清晰
Depends里用了@lru_cache但对象不该全局 → 有状态的(如请求级 Session)不能用lru_cache- CORS 没配 → 前端联调一堆跨域错误
response_model忘了加 → 敏感字段泄露,比如密码 hashraise HTTPException写成了return→ 状态码不对async def里调同步库 (requests/time.sleep) → 阻塞事件循环,要用asyncio.to_thread或换异步库--reload上了生产 → 生产环境要关掉,性能和安全都受影响
十、今日总结
今天核心收获三件事:
- FastAPI = 类型驱动 + Pydantic + 异步,是 AI 应用后端的最优选择
@app.get/@app.post+ Pydantic 模型,接口定义清晰,自动校验、自动文档Depends依赖注入 +lifespan生命周期,是 FastAPI 的"高级特性",也是工程化关键
一个感受:
前端的"接口对接"是一个边界,FastAPI 把这条边界变得极其清晰。
你写 Pydantic 模型,前端看
/docs,双方都不需要看对方源码。这就是 FastAPI 的最大价值------它把"API 契约"从文档约定变成了代码约束。
你的项目现在是一个真正的"AI 助手服务"了。
CLI 能跑,HTTP 能调,前端能连,测试覆盖,可分发包。
从"脚本"到"工具"到"服务",一个完整的软件进化链走完了。
Day 21 完成度:✅
明日预告(Day 22)
- FastAPI 进阶 :
- 依赖注入的更多用法(带参数、类依赖)
- 中间件:请求日志、耗时统计
- 异常处理器:统一错误格式
- 路由分组与嵌套
- 认证入门:API Key 校验
- 给 API 加一套完整的中间件栈
最后
如果这篇对你有用,点个赞让我知道有人在看。
评论区互动: 你之前写后端用什么?Express / Koa / Flask / Django?和 FastAPI 比怎么样?
有问题评论区见,我们国庆假期结束后继续。
100 天,国庆假期结束后见。
附录:系列目录
已完成 ✅
| 阶段 | 天数 | 内容 |
|---|---|---|
| 基础 | 01-06 | Python 语法、数据结构、函数、OOP、文件、依赖管理 |
| 网络 | 07-08 | HTTP 请求、异步 asyncio |
| 工程 | 09-12 | 正则、装饰器、Pydantic、日志调试 |
| 项目 | 13-14 | 命令行 AI 助手 v1 / v2 |
| 复盘 | 15 | 阶段总结 |
| 数据 | 16-17 | SQLite 数据库、SQLAlchemy ORM |
| 测试 | 18-19 | pytest 入门 + 进阶 |
| 工具 | 20 | CLI 打包与发布 |
| 后端 | 21 | FastAPI 入门 ✅ |
进行中 🎯
| 阶段 | 天数 | 内容 |
|---|---|---|
| 后端 | 22-30 | FastAPI 进阶 + 完整服务端 |
| 大模型 | 31-45 | LLM API + Prompt 工程 |
| RAG | 46-60 | 向量数据库 + 知识库 |
| Agent | 61-80 | LangChain / 工具调用 |
| 实战 | 81-100 | 完整 Agent 项目上线 |
100 天后见,一起从切图仔变成 Agent 开发者。