AI Agent Python 学习路线
首页
  • Month 1 概览
  • Week 1 · Python 语法速成
  • Week 2-3 · FastAPI 实战
  • 进阶 · FastAPI 业务架构详解
  • 实战 · 会员管理系统(商业级标准)
  • Week 4 · AI / LLM 基础
  • Month 2 概览
  • Week 5-6 · LangChain / LangGraph
  • Week 7-8 · RAG 检索增强生成
  • Month 3 概览
  • Week 9-10 · 架构设计
  • Week 11 · 实现 + 调试
  • Week 12 · 部署 + 复盘
  • 基建 · 选型指南(SQLite/PG/MySQL/Redis/Nginx)
📚 资源 & 避坑
首页
  • Month 1 概览
  • Week 1 · Python 语法速成
  • Week 2-3 · FastAPI 实战
  • 进阶 · FastAPI 业务架构详解
  • 实战 · 会员管理系统(商业级标准)
  • Week 4 · AI / LLM 基础
  • Month 2 概览
  • Week 5-6 · LangChain / LangGraph
  • Week 7-8 · RAG 检索增强生成
  • Month 3 概览
  • Week 9-10 · 架构设计
  • Week 11 · 实现 + 调试
  • Week 12 · 部署 + 复盘
  • 基建 · 选型指南(SQLite/PG/MySQL/Redis/Nginx)
📚 资源 & 避坑

进阶: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)职责
Controllerroutes/接收 HTTP 请求、参数校验、返回响应
Serviceservices/业务逻辑(该不该下单、手续费怎么算)
Repository / TypeORM Entitydao/ + models/db.py数据库读写
DTO + class-validatorschemas.py (Pydantic)数据传输对象 + 校验
Module无直接对应,靠目录约定依赖组装
Guard / Middlewaredependencies / middleware鉴权、日志等横切逻辑
ConfigModuleconfig.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、PromptFastAPI 路由、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 图和 Toolagent/❌ 在 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"。