进阶:FastAPI 业务架构详解
🎯 本篇目标:搞懂一个生产级 FastAPI 项目的目录结构——每个文件夹是干什么的、代码该放在哪、一次请求是怎么在各层之间流转的。
Week 2-3 里我们写的 demo 所有逻辑都塞在
routes/里,业务一复杂就会失控。本篇教你正确的分层方式。
1. 为什么需要分层?
反面教材:所有逻辑塞在路由里
# ❌ 反例:routes/trading.py —— 路由 + 业务逻辑 + 数据库操作混在一起
@router.post("/orders")
async def place_order(req: PlaceOrderRequest):
# 校验逻辑(40 行)
# 查询数据库余额(30 行)
# 调用交易所 API(40 行)
# 写入订单表(20 行)
# 发送通知(15 行)
return order
问题:
| 问题 | 后果 |
|---|---|
| 无法复用 | 下单逻辑在"网页下单"和"Agent 下单"两处要用 → 复制粘贴 |
| 无法测试 | 想测业务逻辑必须先启动整个 HTTP 服务 |
| 无法替换 | 今天用 SQLite,明天换 PostgreSQL → 改到吐血 |
| 无法协作 | 三个人改同一个文件,天天冲突 |
TS 对照:你其实已经懂了
如果你用过 NestJS,Python 的分层和它几乎一一对应:
| NestJS (TS) | FastAPI (Python) | 职责 |
|---|---|---|
Controller | routes/ | 接收 HTTP 请求、参数校验、返回响应 |
Service | services/ | 业务逻辑(该不该下单、手续费怎么算) |
Repository / TypeORM Entity | dao/ + models/db.py | 数据库读写 |
DTO + class-validator | schemas.py (Pydantic) | 数据传输对象 + 校验 |
Module | 无直接对应,靠目录约定 | 依赖组装 |
Guard / Middleware | dependencies / middleware | 鉴权、日志等横切逻辑 |
ConfigModule | config.py | 配置管理 |
💡 核心原则
每一层只认识它的下一层,不跨层调用。
routes(路由层)→ services(业务层)→ dao(数据层)→ 数据库
路由层不应该直接写 SQL;数据层不应该知道 HTTP 的存在。
2. 生产级目录结构总览
electric-trading-ai/
├── app/
│ ├── main.py # ① 应用入口:创建 FastAPI 实例、注册路由和中间件
│ ├── config.py # ② 配置中心:所有环境变量和常量
│ │
│ ├── models/ # ③ 数据模型层(只有定义,没有逻辑)
│ │ ├── __init__.py
│ │ ├── schemas.py # - Pydantic 模型:API 请求/响应的数据结构
│ │ └── db.py # - ORM 模型:数据库表的结构定义
│ │
│ ├── routes/ # ④ 路由层(薄!只做"接电话")
│ │ ├── __init__.py
│ │ ├── deps.py # - 依赖注入:get_db、get_current_user 等
│ │ ├── trading.py # - 交易相关路由
│ │ ├── agent.py # - Agent 对话路由(含流式)
│ │ └── health.py # - 健康检查
│ │
│ ├── services/ # ⑤ 业务逻辑层(核心!所有"脑子"在这里)
│ │ ├── __init__.py
│ │ ├── trading_service.py # - 下单、撤单、查仓等业务规则
│ │ ├── agent_service.py # - 组织 Agent 执行、管理会话
│ │ └── market_service.py # - 行情获取与缓存
│ │
│ ├── dao/ # ⑥ 数据访问层(只管读写数据库)
│ │ ├── __init__.py
│ │ ├── order_dao.py # - 订单表的增删改查
│ │ └── session_dao.py # - 会话表的增删改查
│ │
│ ├── agent/ # ⑦ AI 模块(Agent 专用,不掺 HTTP 概念)
│ │ ├── __init__.py
│ │ ├── graph.py # - LangGraph 图定义
│ │ ├── nodes.py # - 各节点处理函数
│ │ ├── tools.py # - Tool 定义
│ │ └── prompts.py # - Prompt 模板
│ │
│ ├── rag/ # ⑧ RAG 模块
│ │ ├── __init__.py
│ │ ├── pipeline.py # - 文档加载/分块/向量化
│ │ └── retriever.py # - 检索器封装
│ │
│ ├── llm/ # ⑨ LLM 适配层(统一不同厂商 API)
│ │ ├── __init__.py
│ │ └── client.py # - 统一的 LLM 客户端
│ │
│ ├── core/ # ⑩ 横切关注点(全应用共用的基础设施)
│ │ ├── __init__.py
│ │ ├── exceptions.py # - 自定义异常 + 全局异常处理器
│ │ ├── middleware.py # - 中间件:日志、耗时统计、CORS
│ │ └── security.py # - API Key 校验、JWT
│ │
│ └── utils/ # ⑪ 纯工具函数(无业务含义)
│ ├── __init__.py
│ └── helpers.py # - 时间格式化、ID 生成等
│
├── tests/ # ⑫ 测试(和 app/ 结构镜像)
│ ├── test_trading_service.py # - 业务层单测(不需要启动服务)
│ └── test_api_trading.py # - 接口测试(用 TestClient)
│
├── data/ # 运行时数据(ChromaDB、SQLite)
├── .env # 环境变量(gitignore!)
├── .env.example # 环境变量模板
└── pyproject.toml
3. 每一层详细说明
① main.py — 应用入口
只做组装,不写业务。相当于 NestJS 的 AppModule。
# app/main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from app.config import settings
from app.core.middleware import log_requests
from app.core.exceptions import register_exception_handlers
from app.routes import trading, agent, health
app = FastAPI(title="Electric Trading AI", version="0.1.0")
# 中间件
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"])
# 全局异常处理器
register_exception_handlers(app)
# 注册路由
app.include_router(health.router)
app.include_router(trading.router)
app.include_router(agent.router)
判断标准
如果 main.py 里出现了 SQL、LLM 调用、业务判断,一定放错地方了。
② config.py — 配置中心
# app/config.py
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
llm_api_key: str = ""
llm_base_url: str = "https://api.siliconflow.cn/v1"
llm_model: str = "Qwen/Qwen2.5-7B-Instruct"
app_port: int = 8000
debug: bool = True
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
settings = Settings() # 全局单例
规则:任何文件需要配置,都 from app.config import settings,禁止直接在业务代码里 os.getenv()。
③ models/ — 数据模型层
只有数据结构定义,零逻辑。分两类:
# app/models/schemas.py —— API 层的数据结构(对外的"合同")
from pydantic import BaseModel, Field
from typing import Literal, Optional
class PlaceOrderRequest(BaseModel):
"""前端/调用方发给我们的数据格式"""
symbol: str
side: Literal["buy", "sell"]
price: float = Field(gt=0)
quantity: int = Field(gt=0)
class OrderResponse(BaseModel):
"""我们返回给调用方的数据格式"""
order_id: str
status: Literal["pending", "filled", "failed"]
# app/models/db.py —— 数据库表的结构(对内的"仓库货架")
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
class Base(DeclarativeBase): pass
class OrderRecord(Base):
"""orders 表 —— 注意:和 OrderResponse 是两个东西!"""
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
symbol: Mapped[str]
side: Mapped[str]
quantity: Mapped[int]
created_at: Mapped[str]
💡 为什么要两套模型?
TS 里你也这么干过:interface ApiOrder(给前端的)≠ interface DbOrderRow(数据库行的)。API 模型和 DB 模型解耦后,加字段、改表结构都不会直接暴露给调用方。
④ routes/ — 路由层(薄!)
只做三件事:参数校验(Pydantic 自动做了)、调 service、返回响应。
# app/routes/trading.py
from fastapi import APIRouter, Depends
from app.models.schemas import PlaceOrderRequest, OrderResponse
from app.services.trading_service import TradingService
from app.routes.deps import get_trading_service
router = APIRouter(prefix="/api/trading", tags=["交易"])
@router.post("/orders", response_model=OrderResponse)
async def place_order(
req: PlaceOrderRequest,
service: TradingService = Depends(get_trading_service), # 依赖注入
):
return await service.place_order(req) # 一行业务逻辑都不写
# app/routes/deps.py — 依赖注入(相当于 NestJS 的 Provider 注入)
from functools import lru_cache
@lru_cache
def get_trading_service() -> TradingService:
return TradingService(order_dao=OrderDao(), market_service=MarketService())
⑤ services/ — 业务逻辑层(核心!)
所有"该不该、怎么算、按什么规则"的判断都在这。不 import 任何 FastAPI 的东西——这就是它能被单独测试、被 Agent 复用的关键。
# app/services/trading_service.py
from app.models.schemas import PlaceOrderRequest, OrderResponse
from app.dao.order_dao import OrderDao
from app.services.market_service import MarketService
class TradingService:
def __init__(self, order_dao: OrderDao, market_service: MarketService):
self.order_dao = order_dao # 依赖通过构造函数传入
self.market_service = market_service
async def place_order(self, req: PlaceOrderRequest) -> OrderResponse:
# 业务规则:查最新价、校验余额、风控……
price = await self.market_service.get_price(req.symbol)
if req.side == "buy" and req.price > price * 1.1:
raise BusinessError("买单价格超出市价 10%,疑似误操作")
record = await self.order_dao.create(req, price)
return OrderResponse(order_id=record.id, status="pending")
💡 关键收益:Agent 直接复用业务层
Month 2 的 Agent Tool 不用重写下单逻辑,直接调同一个 service:
@tool
def place_order(symbol: str, side: str, quantity: int) -> dict:
"""Agent 下单工具 —— 复用业务层!"""
req = PlaceOrderRequest(symbol=symbol, side=side, quantity=quantity, price=0)
resp = await trading_service.place_order(req)
return {"order_id": resp.order_id, "status": resp.status}
这就是分层的最大价值:HTTP 和 Agent 是两个"入口",共享同一套"业务大脑"。
⑥ dao/ — 数据访问层
只管数据库读写,没有任何业务判断。
# app/dao/order_dao.py
from app.models.db import OrderRecord
class OrderDao:
async def create(self, req, price) -> OrderRecord:
"""插入一条订单记录 —— 没有规则判断,就是纯读写"""
...
async def get_by_id(self, order_id: str) -> OrderRecord | None:
...
⑦-⑨ agent/ rag/ llm/ — AI 模块
和 Month 2/3 的内容对应,记住分层规则:
| 目录 | 里有什么 | 里不该有什么 |
|---|---|---|
agent/ | LangGraph 图、节点、Tool、Prompt | FastAPI 路由、SQL |
rag/ | 分块、向量化、检索器 | HTTP 处理 |
llm/ | 统一的 LLM 客户端封装 | 业务规则 |
services/agent_service.py 是它们和 HTTP 世界的桥梁:路由 → agent_service → agent/graph。
⑩ core/ — 横切关注点
所有"每个接口都需要"的基础设施:
# app/core/exceptions.py — 自定义异常 + 全局处理
from fastapi import Request
from fastapi.responses import JSONResponse
class BusinessError(Exception):
"""业务规则不满足(如余额不足)"""
def __init__(self, message: str, code: str = "BUSINESS_ERROR"):
self.message, self.code = message, code
def register_exception_handlers(app):
@app.exception_handler(BusinessError)
async def business_error_handler(request: Request, exc: BusinessError):
return JSONResponse(status_code=400, content={"code": exc.code, "message": exc.message})
💡 TS 对照:相当于 NestJS 的
ExceptionFilter。业务代码只管raise BusinessError("..."),HTTP 错误格式全局统一。
⑪ utils/ — 纯工具函数
判断标准:把函数复制到一个没有任何业务的项目里,依然成立(时间格式化、ID 生成)。如果有业务含义,放 services/。
⑫ tests/ — 测试
和 app/ 结构镜像。分层的直接红利:
# tests/test_trading_service.py — 单测业务层,不用启动 HTTP 服务!
async def test_place_order_price_too_high():
service = TradingService(order_dao=MockDao(), market_service=MockMarket(price=100))
with pytest.raises(BusinessError):
await service.place_order(PlaceOrderRequest(symbol="BTC", side="buy", price=200, quantity=1))
4. 一次请求的完整流转
以 POST /api/trading/orders 为例:
sequenceDiagram
participant C as 前端/Agent
participant R as routes/trading.py<br/>(路由层)
participant S as services/trading_service.py<br/>(业务层)
participant M as services/market_service.py<br/>(行情)
participant D as dao/order_dao.py<br/>(数据层)
participant DB as SQLite
C->>R: POST /orders {symbol, side, quantity}
Note over R: Pydantic 自动校验参数
R->>S: await service.place_order(req)
S->>M: get_price("BTC")
M-->>S: 99999.99
Note over S: 业务规则:价格校验、风控
S->>D: create(req, price)
D->>DB: INSERT INTO orders
DB-->>D: order record
D-->>S: OrderRecord
S-->>R: OrderResponse
R-->>C: 200 {order_id, status}
数据变形链(TS 里叫 DTO mapping):
PlaceOrderRequest (API 模型)
→ TradingService 内部处理
→ OrderRecord (DB 模型)
→ OrderResponse (API 模型) 返回
5. 「代码该放哪」决策表
| 你要写的代码 | 放哪 | 反例 |
|---|---|---|
| 接口路径、HTTP 状态码 | routes/ | ❌ 在 service 里 return JSONResponse |
| "余额不足不能下单"这种规则 | services/ | ❌ 在路由里 if 判断 |
| INSERT / SELECT 语句 | dao/ | ❌ 在 service 里拼 SQL |
| 表结构、API 字段定义 | models/ | ❌ 在路由里手写 dict |
| LLM 调用封装 | llm/ | ❌ 在路由里直接 httpx 调 API |
| LangGraph 图和 Tool | agent/ | ❌ 在 main.py 里建图 |
| API Key、端口 | config.py + .env | ❌ 硬编码在代码里 |
| 全局错误格式 | core/exceptions.py | ❌ 每个路由自己 try/except 拼错误 |
| 时间格式化等纯函数 | utils/ | ❌ 放 services 里混淆 |
6. 本篇作业
| # | 任务 | 验收标准 |
|---|---|---|
| 1 | 把 Week 2-3 写的 FastAPI demo 重构成上面的分层结构 | main.py 里没有任何业务代码 |
| 2 | 抽出 TradingService,路由层每行只剩"调 service" | 删掉 service 文件后路由报 import 错误(说明解耦成功) |
| 3 | 给 place_order 写单测(Mock 掉 dao) | pytest 不启动服务就能跑通 |
| 4 | 加一个 BusinessError + 全局异常处理器 | 业务错误统一返回 {"code": ..., "message": ...} |
| 5 | 对照决策表自查一遍现有代码 | 没有跨层调用 |
7. 常见疑问
Q: 小项目也要分这么多层吗? A: 不用。Demo 阶段 routes/ + models/ 两层够了。当你发现同一段业务逻辑想被两处复用,或想给接口写单测时,就是引入 services/ 的时机。分层是为了解决问题,不是仪式感。
Q: service 和 dao 一定要分开吗? A: 单表 CRUD 的简单场景,service 直接操作 ORM 也能接受。但只要出现"多表联动"或"可能换数据库",就拆出 dao。
Q: FastAPI 的 Depends 和 NestJS 的 DI 一样吗? A: 思路一致(依赖从外部注入而不是内部 new),但 FastAPI 的 Depends 更轻量——本质是函数调用 + 缓存。上面的 get_trading_service 用 @lru_cache 就是手工实现的"单例 Provider"。