Python 工程化、测试与 FastAPI

# Python 工程化、测试与 FastAPI

本篇目标

把 Python 基础组合成一个最小但完整的后端服务:明确目录、依赖、数据校验、错误边界和测试方式,并理解 FastAPI 在 AI 应用中的位置。

组织项目目录编写 API验证外部输入建立自动化测试

# 先记住一句话

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     # 纯业务逻辑测试
1
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"]
1
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]"
1
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()
        ]
1
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)
1
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
1
2

调用链是:

客户端 HTTP 请求
  → FastAPI 路由匹配与输入校验
  → NoteService 执行业务逻辑
  → response_model 过滤并序列化结果
  → HTTP JSON 响应
1
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"}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 从项目根目录执行全部测试。
pytest
1
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、连接池和事务边界。

# 参考资料

上次更新时间: 2026年09月10日 00:03:52