Python 工程化、测试与 FastAPI
# Python 工程化、测试与 FastAPI
把 Python 基础组合成一个最小但完整的后端服务:明确目录、依赖、数据校验、错误边界和测试方式,并理解 FastAPI 在 AI 应用中的位置。
# 先记住一句话
FastAPI 负责把 HTTP 请求转换成经过校验的 Python 数据,再调用业务逻辑并生成响应;模型、数据库和向量检索应放在它后面的独立服务层。
FastAPI 是什么
FastAPI (opens new window) 是一个用 Python 开发 HTTP API 的 Web 框架,不是一套规范。它帮我们处理路由、参数校验、依赖注入、响应生成和接口文档。
FastAPI 底层使用 Starlette 提供 Web 能力,使用 Pydantic 校验数据,通常交给 Uvicorn 运行。它遵循 ASGI,并能根据代码生成 OpenAPI 文档,但 FastAPI 本身不是 ASGI 或 OpenAPI 规范。
- Starlette (opens new window) 是轻量级 ASGI Web 框架,负责路由、请求与响应、中间件和 WebSocket 等底层 Web 功能。
- Pydantic (opens new window) 是数据校验与序列化库,负责按照 Python 类型标注检查、转换输入数据并生成数据模型。
- Uvicorn (opens new window) 是 ASGI 服务器,负责监听端口、接收网络请求,再把请求交给 FastAPI 应用。
- ASGI (opens new window)(Asynchronous Server Gateway Interface,异步服务器网关接口)是 Python Web 服务器与 Web 应用之间的通信规范。可以把它理解成两者之间统一的 “插座” :Uvicorn 按 ASGI 把 HTTP 或 WebSocket 请求交给 FastAPI,FastAPI 再按同一规范返回结果。
# 一个最小完整项目
note-api/
├─ pyproject.toml # Python 版本、依赖和工具配置
├─ src/
│ └─ note_api/
│ ├─ __init__.py # 包入口
│ ├─ main.py # FastAPI 路由与应用对象
│ └─ service.py # 与 HTTP 无关的业务逻辑
└─ tests/
├─ test_api.py # HTTP 边界测试
└─ test_service.py # 纯业务逻辑测试
2
3
4
5
6
7
8
9
10
# 1. 安装依赖
# 文件位置:pyproject.toml
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "note-api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi",
"uvicorn[standard]",
]
[project.optional-dependencies]
test = [
"httpx",
"pytest",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 在已经激活的虚拟环境中安装当前项目及测试依赖。
# -e 表示源码修改后无需重复安装,适合本地开发。
python3 -m pip install -e ".[test]"
2
3
生产项目应把依赖及版本范围写进 pyproject.toml 并提交锁文件,不要只依赖开发者本机已经安装的包。
# 2. 编写业务逻辑
# 文件位置:src/note_api/service.py
from dataclasses import dataclass
# dataclass 自动生成初始化等方法;frozen=True 表示创建后不允许普通属性赋值。
@dataclass(frozen=True)
class Note:
id: str
title: str
content: str
class NoteService:
def __init__(self) -> None:
# 内存数据只用于跑通示例;生产环境在这里注入数据库 Repository。
self._notes = {
"python": Note("python", "Python", "Python 适合快速构建 AI 服务。"),
"go": Note("go", "Go", "Go 适合构建并发网络服务。"),
}
def get(self, note_id: str) -> Note | None:
# dict.get() 在键不存在时返回 None,不会抛出 KeyError。
return self._notes.get(note_id)
def search(self, query: str) -> list[Note]:
normalized = query.strip().casefold()
if not normalized:
return []
# 列表推导式遍历所有笔记,只收集标题或正文命中的对象。
return [
note
for note in self._notes.values()
if normalized in note.title.casefold() or normalized in note.content.casefold()
]
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# 3. 暴露 HTTP API
# 文件位置:src/note_api/main.py
from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel, ConfigDict
from .service import Note, NoteService
class NoteResponse(BaseModel):
"""对外响应结构;不要把数据库对象未经筛选直接返回。"""
# from_attributes=True 允许模型从普通对象属性读取值,而不只接受字典。
model_config = ConfigDict(from_attributes=True)
id: str
title: str
content: str
# FastAPI() 创建 ASGI 应用对象;Uvicorn 启动时会导入这个 app。
app = FastAPI(title="Note API")
service = NoteService()
# 路由装饰器把紧随其后的函数注册为 GET /health 处理器。
@app.get("/health")
def health() -> dict[str, str]:
# 存活检查只证明进程能响应;生产环境还可增加独立的就绪检查。
return {"status": "ok"}
@app.get("/notes/{note_id}", response_model=NoteResponse)
def get_note(note_id: str) -> Note:
note = service.get(note_id)
if note is None:
# HTTPException 由 FastAPI 转换成带状态码的 JSON 响应。
raise HTTPException(status_code=404, detail="note not found")
return note
@app.get("/notes", response_model=list[NoteResponse])
def search_notes(query: str = Query(min_length=1, max_length=100)) -> list[Note]:
# Query 在进入业务层前限制外部输入长度。
return service.search(query)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 从项目根目录启动;--reload 只用于本地开发。
uvicorn note_api.main:app --app-dir src --reload
2
调用链是:
客户端 HTTP 请求
→ FastAPI 路由匹配与输入校验
→ NoteService 执行业务逻辑
→ response_model 过滤并序列化结果
→ HTTP JSON 响应
2
3
4
5
以后接入模型时,新增独立的模型客户端或 Agent 服务,不要把 Prompt 拼接、数据库查询和 HTTP 细节全部堆进路由函数。
# 4. 编写测试
# 文件位置:tests/test_api.py
from fastapi.testclient import TestClient
from note_api.main import app
# TestClient 在当前进程内模拟 HTTP 请求,不必启动真实服务器。
client = TestClient(app)
def test_get_existing_note() -> None:
response = client.get("/notes/python")
# assert 条件为 False 时测试立即失败。
assert response.status_code == 200
assert response.json()["title"] == "Python"
def test_get_missing_note_returns_404() -> None:
response = client.get("/notes/missing")
# 同时验证状态码和稳定错误信息,防止接口只返回 200 加错误文本。
assert response.status_code == 404
assert response.json() == {"detail": "note not found"}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 从项目根目录执行全部测试。
pytest
2
单元测试验证纯业务函数,集成测试验证路由、序列化和依赖协作。外部模型 API 不应在普通测试中真实调用;通过依赖注入替换为可控假实现,另设少量端到端评测验证真实模型效果。
# AI 后端还需要哪些边界
- 模型和外部 API 调用设置超时、并发上限与重试预算;
- 流式响应在客户端断开后取消下游任务;
- 不把 API Key、Prompt 中的敏感字段和完整用户内容写入日志;
- 区分 HTTP 请求成功与 AI 答案质量合格,后者需要独立评测;
- 长任务使用队列和持久化状态,不要无限占用一个 HTTP 请求。
# 项目表达
可以这样描述一个 Python AI 后端:
可直接复述
我使用 FastAPI 承担 HTTP 接入与 Schema 校验,把检索、模型调用和业务规则放在独立服务层。
外部调用设置超时和并发上限,核心逻辑通过单元测试验证。
真实模型效果使用固定评测集回归,因此接口正确性和答案质量可以分开定位。
# 高频面试题与回答
1. FastAPI 中同步路由和异步路由怎样选择?参考答案
如果整条调用链使用异步网络或数据库客户端,可以使用 async def;普通同步库可以使用 def。不能为了看起来异步而在 async def 中直接执行阻塞调用,否则会卡住事件循环。
2. 为什么既要类型标注又要 Pydantic 校验?参考答案
类型标注主要用于开发期静态检查,Pydantic 在运行时验证来自 HTTP 等不可信边界的数据。两者解决的阶段不同:一个减少开发错误,一个阻止无效外部数据进入业务逻辑。
# 接下来学什么
下一篇学习 SQLAlchemy、事务与迁移,把示例中的内存数据替换为数据库,并理解 Session、连接池和事务边界。