Day 002 — Python 工程化 & FastAPI & 数据库速通

📅 2026-07-16 | 🏷️ Python · AI Agent 方向 | 🎯 覆盖后端工程化面试核心


📌 今日知识地图

复制代码
Python 工程化面试全景
│
├── FastAPI(面试出现率 >70%)
│   ├── 路由 & 路径参数 & 查询参数 & 请求体
│   ├── 依赖注入(Depends)--- FastAPI 的灵魂
│   ├── 中间件 vs 依赖 vs 异常处理
│   ├── 生命周期( lifespan / on_event )
│   └── 为什么 FastAPI 这么快?
│
├── 数据库(面试出现率 >60%)
│   ├── SQLAlchemy 2.0 异步 ORM
│   ├── 连接池 & 会话管理
│   ├── Alembic 数据库迁移
│   └── ORM vs 原生 SQL 选型讨论
│
├── Redis(面试出现率 >70%)
│   ├── 五种数据结构 & 底层实现速览
│   ├── 缓存穿透 / 击穿 / 雪崩(必考!)
│   ├── 分布式锁(setnx + Lua + 看门狗)
│   └── 持久化 RDB vs AOF vs 混合
│
├── Docker & 部署(面试出现率 >50%)
│   ├── Dockerfile 多阶段构建
│   ├── docker-compose 编排
│   └── Gunicorn + Uvicorn + Nginx
│
└── 测试(面试出现率 >40%)
    ├── pytest fixture & mock
    ├── FastAPI TestClient
    └── 异步测试

一、FastAPI 深度拆解

1.1 为什么面试官爱问 FastAPI?

FastAPI 是当前 Python 后端面试的绝对热点,原因有三:

  • 它是 AI/Agent 应用的首选框架(异步 + Pydantic 原生集成)
  • 面试官可以用它考察你对 异步编程、类型系统、依赖注入 的理解
  • 字节、腾讯的 Agent 平台(Coze、飞连)底层技术栈跟它高度重合

核心认知:FastAPI = Starlette(高性能异步 Web 核心)+ Pydantic(数据校验)+ OpenAPI(自动文档)。它快不是因为 Python 快,而是因为异步 IO + 数据校验用 Rust 写的(pydantic-core)。

1.2 路由 & 四种参数来源

python 复制代码
from fastapi import FastAPI, Path, Query, Body, Header, Cookie
from pydantic import BaseModel, Field
from typing import Optional, Literal
from enum import Enum

app = FastAPI(title="Agent API", version="1.0.0")

# ═══════════════════════════════════
# 四种参数来源(面试常考:分别从哪取?)
# ═══════════════════════════════════

# ① 路径参数 --- URL 路径的一部分
@app.get("/agents/{agent_id}")
async def get_agent(
    agent_id: str = Path(..., description="Agent ID", min_length=3, max_length=50)
):
    return {"agent_id": agent_id}

# ② 查询参数 --- ?key=value
@app.get("/agents")
async def list_agents(
    page: int = Query(default=1, ge=1),
    size: int = Query(default=20, ge=1, le=100),
    status: Optional[str] = Query(default=None),
):
    return {"page": page, "size": size, "status": status}

# ③ 请求体 --- POST/PUT/PATCH 的 JSON Body
class CreateAgentRequest(BaseModel):
    name: str = Field(..., min_length=1, max_length=100)
    model: Literal["gpt-4", "gpt-3.5", "deepseek"] = "gpt-4"
    temperature: float = Field(default=0.7, ge=0, le=2.0)
    tools: list[str] = Field(default_factory=list, max_length=10)

@app.post("/agents")
async def create_agent(body: CreateAgentRequest):
    # FastAPI 自动校验 → 自动生成 JSON Schema → 自动生成 OpenAPI 文档
    return {"created": body.name}

# ④ Header / Cookie --- 请求头
@app.get("/me")
async def get_me(
    x_request_id: str = Header(..., alias="X-Request-ID"),
    session_token: Optional[str] = Cookie(default=None),
):
    return {"request_id": x_request_id}

面试防坑 :路径参数用 Path(),查询参数用 Query(),请求体用 Body() 或 Pydantic Model。如果不显式声明,FastAPI 会按规则自动推断------但面试时写清楚更显专业。

1.3 依赖注入(Depends)--- FastAPI 的灵魂

依赖注入是 FastAPI 最强大的特性,也是面试中最能体现深度的点。

python 复制代码
from fastapi import Depends, HTTPException, status

# ═══════════════════════════════════
# 场景 1:可复用的认证依赖
# ═══════════════════════════════════
async def get_current_user(
    authorization: str = Header(..., alias="Authorization"),
) -> dict:
    """从 Header 中解析 Token → 查询数据库 → 返回当前用户"""
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="未登录")
    token = authorization.split(" ", 1)[1]
    # 实际项目中这里会验证 JWT、查 Redis Session 等
    if token == "expired":
        raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token 过期")
    return {"user_id": "u123", "name": "Alice"}

@app.get("/protected")
async def protected_route(user: dict = Depends(get_current_user)):
    """这个接口自动需要认证"""
    return {"hello": user["name"]}

# ═══════════════════════════════════
# 场景 2:可传参的依赖(依赖工厂)
# ═══════════════════════════════════
def require_permission(required: str):
    """依赖工厂:返回一个依赖函数"""
    async def check_permission(user: dict = Depends(get_current_user)):
        # 实际查数据库或权限服务
        user_permissions = ["read", "write"]  # 模拟
        if required not in user_permissions:
            raise HTTPException(status_code=403, detail=f"缺少 {required} 权限")
        return user
    return check_permission

@app.post("/agents")
async def create_agent_secure(
    user: dict = Depends(require_permission("write"))
):
    return {"created_by": user["user_id"]}

# ═══════════════════════════════════
# 场景 3:缓存依赖结果(同一个请求内只执行一次)
# ═══════════════════════════════════
async def get_db_session():
    """获取数据库会话(模拟)"""
    print("创建 DB Session")  # 验证是否只执行一次
    session = {"session_id": "s001"}
    try:
        yield session  # yield 后面的代码在请求结束后执行
    finally:
        print("关闭 DB Session")

@app.get("/user-info")
async def user_info(
    db=Depends(get_db_session),
    user=Depends(get_current_user),
):
    # get_db_session 和 get_current_user 共享给此路由下的所有依赖
    # use_cache=True(默认)时,同一个请求内多次 Depends 同一个依赖只执行一次
    return {"db": db, "user": user}

# ═══════════════════════════════════
# 场景 4:全局依赖(所有路由自动应用)
# ═══════════════════════════════════
# app = FastAPI(dependencies=[Depends(rate_limit)])

面试话术:"FastAPI 的依赖注入有四个层次:① 路由级依赖(某个接口需要认证);② 依赖嵌套(权限依赖又依赖认证结果);③ 全局依赖(所有路由都走限流);④ yield 依赖(管理资源生命周期,如数据库连接)。它本质上是把一个函数调用图编译成了执行计划,有缓存、有生命周期管理------类似 pytest fixture 的设计思路。"

1.4 中间件 vs 依赖 vs 异常处理器

这三个东西面试经常被混淆,一表厘清:

机制 作用范围 执行顺序 典型用途 能改响应?
中间件 全局,所有请求 最外层 日志、CORS、限流、请求 ID
依赖(Depends) 路由/路由组 中间件之后 认证、权限、DB会话
异常处理器 全局/路由组 抛异常时 统一错误格式 ✅ 返回错误响应
python 复制代码
from fastapi import Request
from fastapi.responses import JSONResponse
import time, uuid

# ═══ 中间件:包在所有路由外面 ═══
@app.middleware("http")
async def add_request_id(request: Request, call_next):
    """每个请求:生成 Request ID + 记录耗时"""
    request.state.request_id = str(uuid.uuid4())[:8]
    start = time.time()
    response = await call_next(request)  # ← 进入路由
    response.headers["X-Request-ID"] = request.state.request_id
    response.headers["X-Response-Time"] = f"{time.time()-start:.3f}s"
    return response

# ═══ 异常处理器:统一错误格式 ═══
class AppException(Exception):
    def __init__(self, status_code: int, detail: str, error_code: str = ""):
        self.status_code = status_code
        self.detail = detail
        self.error_code = error_code

@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
    return JSONResponse(
        status_code=exc.status_code,
        content={
            "error": exc.error_code or str(exc.status_code),
            "message": exc.detail,
            "request_id": getattr(request.state, "request_id", ""),
        },
    )

# 注册全局异常处理器的方法
# @app.exception_handler(500)  → 捕获未处理的服务器错误
# @app.exception_handler(RequestValidationError)  → 参数校验失败

执行顺序记忆:「中间件先进先出,依赖在上路由在下,异常飞出中间都兜底」

1.5 生命周期:startup/shutdown vs lifespan

python 复制代码
from contextlib import asynccontextmanager

# 推荐写法(FastAPI 官方推荐)
@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动时
    print("初始化连接池...")
    # await init_db_pool()
    # await init_redis_pool()
    yield  # ← 应用运行期间
    # 关闭时
    print("关闭所有连接...")
    # await close_db_pool()
    # await close_redis_pool()

app = FastAPI(lifespan=lifespan)

# 老写法(不推荐但面试可能问)
# @app.on_event("startup")
# @app.on_event("shutdown")

1.6 FastAPI 为什么快?(面试高频)

性能因素 说明
Starlette 异步核心 基于 uvloop(libuv 的 Python 绑定),事件循环性能接近 Node.js
pydantic-core (Rust) JSON 解析和数据校验用 Rust 实现,比纯 Python 快 10-20 倍
异步 IO 单线程高并发,没有线程切换开销,没有 GIL 竞争
uvicorn 服务器 基于 uvloop + httptools(C 库),性能仅次于 C++/Rust/Go

面试话术:"FastAPI 的性能不是靠 Python 本身,而是靠异步 IO + Rust 加速的校验层。实际项目中,瓶颈通常在数据库查询和 LLM API 调用上,框架开销可以忽略不计。选 FastAPI 更多是因为它的类型安全、自动文档、依赖注入让复杂 Agent 项目的可维护性大幅提升。"


二、SQLAlchemy 2.0 异步 ORM

2.1 核心概念

python 复制代码
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy import String, Integer, DateTime, Text, ForeignKey, select
from datetime import datetime

# ═══ ① 声明基类 ═══
class Base(DeclarativeBase):
    pass

# ═══ ② 模型定义 ═══
class Agent(Base):
    __tablename__ = "agents"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(100), nullable=False, comment="Agent名称")
    model: Mapped[str] = mapped_column(String(50), default="gpt-4")
    system_prompt: Mapped[str] = mapped_column(Text, default="")
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    # 一对多:一个 Agent 有多条消息
    messages: Mapped[list["Message"]] = relationship(back_populates="agent", cascade="all, delete-orphan")

class Message(Base):
    __tablename__ = "messages"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    agent_id: Mapped[int] = mapped_column(ForeignKey("agents.id"), nullable=False)
    role: Mapped[str] = mapped_column(String(20), nullable=False)
    content: Mapped[str] = mapped_column(Text, nullable=False)
    created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)

    agent: Mapped["Agent"] = relationship(back_populates="messages")

# ═══ ③ 异步引擎 & 会话工厂 ═══
DATABASE_URL = "postgresql+asyncpg://user:pass@localhost:5432/agent_db"

engine = create_async_engine(
    DATABASE_URL,
    echo=False,              # SQL 日志
    pool_size=20,            # 连接池大小
    max_overflow=10,         # 超出 pool_size 后的最大额外连接
    pool_pre_ping=True,      # 每次使用前检测连接是否存活
)

AsyncSessionLocal = async_sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False,  # commit 后不过期对象(避免异步场景的懒加载问题)
)

2.2 异步 CRUD 最佳实践

python 复制代码
from sqlalchemy import select, update, delete, func
from sqlalchemy.orm import selectinload

# ═══ CREATE ═══
async def create_agent(name: str, model: str = "gpt-4") -> Agent:
    async with AsyncSessionLocal() as session:
        agent = Agent(name=name, model=model)
        session.add(agent)
        await session.commit()
        await session.refresh(agent)  # 获取数据库生成的 id 和 created_at
        return agent

# ═══ READ(单条)═══
async def get_agent(agent_id: int) -> Agent | None:
    async with AsyncSessionLocal() as session:
        return await session.get(Agent, agent_id)

# ═══ READ(列表 + 分页)═══
async def list_agents(page: int = 1, size: int = 20) -> list[Agent]:
    async with AsyncSessionLocal() as session:
        stmt = (
            select(Agent)
            .offset((page - 1) * size)
            .limit(size)
            .order_by(Agent.created_at.desc())
        )
        result = await session.execute(stmt)
        return list(result.scalars().all())

# ═══ READ(联表查询)═══
async def get_agent_with_messages(agent_id: int) -> Agent | None:
    async with AsyncSessionLocal() as session:
        stmt = (
            select(Agent)
            .where(Agent.id == agent_id)
            .options(selectinload(Agent.messages))  # 主动加载关联数据,避免 N+1
        )
        result = await session.execute(stmt)
        return result.scalar_one_or_none()

# ═══ UPDATE ═══
async def update_agent_name(agent_id: int, new_name: str) -> int:
    async with AsyncSessionLocal() as session:
        stmt = (
            update(Agent)
            .where(Agent.id == agent_id)
            .values(name=new_name)
        )
        result = await session.execute(stmt)
        await session.commit()
        return result.rowcount  # 受影响行数

# ═══ DELETE ═══
async def delete_agent(agent_id: int) -> bool:
    async with AsyncSessionLocal() as session:
        agent = await session.get(Agent, agent_id)
        if agent is None:
            return False
        await session.delete(agent)
        await session.commit()
        return True

2.3 FastAPI 集成:用依赖注入管理会话

python 复制代码
from fastapi import Depends

async def get_session() -> AsyncSession:
    """每个请求一个数据库会话,请求结束自动关闭"""
    async with AsyncSessionLocal() as session:
        yield session

@app.get("/api/agents")
async def list_agents_api(
    page: int = 1,
    size: int = 20,
    session: AsyncSession = Depends(get_session),
):
    stmt = select(Agent).offset((page - 1) * size).limit(size).order_by(Agent.created_at.desc())
    result = await session.execute(stmt)
    agents = list(result.scalars().all())
    return {"agents": [{"id": a.id, "name": a.name} for a in agents], "page": page}

@app.post("/api/agents")
async def create_agent_api(
    body: CreateAgentRequest,
    session: AsyncSession = Depends(get_session),
):
    agent = Agent(name=body.name, model=body.model)
    session.add(agent)
    await session.commit()
    await session.refresh(agent)
    return {"id": agent.id, "name": agent.name}

2.4 N+1 问题 & selectinload

python 复制代码
# ❌ N+1 查询:先查 agents(1 次),再逐个查 messages(N 次)
agents = await session.execute(select(Agent))
for agent in agents.scalars():
    print(agent.messages)  # 每次访问都发一次 SQL!

# ✅ 用 selectinload 或 joinedload 一次性加载
stmt = select(Agent).options(selectinload(Agent.messages))
# selectinload: 发两条 SQL(SELECT agents + SELECT messages WHERE agent_id IN (...))
# joinedload: 发一条 SQL(LEFT JOIN),但数据有冗余

面试话术:"N+1 是 ORM 最经典的性能陷阱。SQLAlchemy 用 selectinload(IN 批量加载)或 joinedload(JOIN 加载)解决。Agent 场景中,查 Agent 详情时顺带加载它的消息列表,selectinload 是最常用的------它发两条 SQL,比 JOIN 更可控。"

2.5 Alembic 数据库迁移

bash 复制代码
# 初始化
alembic init migrations

# 生成迁移脚本(自动检测模型变化)
alembic revision --autogenerate -m "add agents and messages tables"

# 执行迁移
alembic upgrade head

# 回滚
alembic downgrade -1

# 查看迁移历史
alembic history
python 复制代码
# migrations/env.py 关键配置
from app.models import Base  # 导入你的模型
target_metadata = Base.metadata  # Alembic 对比这个 metadata 来生成迁移

# 异步引擎配置
from sqlalchemy.ext.asyncio import create_async_engine
connectable = create_async_engine(DATABASE_URL)

三、Redis:从数据结构到缓存策略

3.1 五种数据结构 & 底层实现

数据结构 操作 底层实现 Agent 开发中的用途
String SET/GET/INCR/EXPIRE SDS(简单动态字符串) 缓存、计数器、分布式锁
Hash HSET/HGET/HGETALL listpack → hashtable 存储对象(用户配置、Agent状态)
List LPUSH/RPUSH/LPOP/RPOP quicklist 消息队列、最近 N 条对话
Set SADD/SMEMBERS/SINTER intset → hashtable 去重、标签、共同好友
ZSet ZADD/ZRANGE/ZRANK listpack → skiplist+dict 排行榜、按分数排序的历史记录
python 复制代码
import redis.asyncio as aioredis

# 创建异步 Redis 连接(FastAPI 中必须用异步版!)
redis_client = aioredis.from_url("redis://localhost:6379/0", decode_responses=True)

# ZSet 示例:Agent 对话轮次计数器
await redis_client.zadd("agent:usage:rank", {"agent:chatbot": 1532, "agent:coder": 891})
top3 = await redis_client.zrevrange("agent:usage:rank", 0, 2, withscores=True)
# [('agent:chatbot', 1532.0), ('agent:coder', 891.0)]

# 分布式锁(详见 3.4)

3.2 缓存三大问题 & 解决方案(面试必考!)

这是后端面试出现率最高的 Redis 题,三连问经常连着来。

复制代码
       请求
        │
   ┌────▼────┐
   │  缓存层  │ ← Redis
   └────┬────┘
        │ 未命中(cache miss)
   ┌────▼────┐
   │  数据库  │ ← PostgreSQL / MySQL
   └─────────┘
问题 是什么 典型场景 解决方案
缓存穿透 查一个不存在的数据,缓存没有,数据库也没有 → 每次请求都打到 DB 恶意攻击(用不存在的 ID 刷接口) ① 布隆过滤器(Bloom Filter)② 缓存空值(设置短过期时间)
缓存击穿 一个热点 key 在过期瞬间,大量并发请求同时打到 DB 爆款商品过期瞬间 ① 互斥锁(只有一个线程去查 DB 重建缓存)② 逻辑过期(永不过期 + 异步刷新)
缓存雪崩 大量 key 同时过期,或者 Redis 宕机 → 所有请求打崩 DB 批量导入缓存时设置了相同过期时间 ① 过期时间加随机值(±5 分钟)② Redis Cluster 高可用 ③ 限流+降级
python 复制代码
# ═══ 缓存穿透:缓存空值 ═══
async def get_agent_cached(agent_id: int) -> dict | None:
    cache_key = f"agent:{agent_id}"
    # 1. 查缓存
    cached = await redis_client.get(cache_key)
    if cached is not None:
        if cached == "__NULL__":
            return None  # 缓存了"不存在"
        return json.loads(cached)

    # 2. 查数据库
    agent = await get_agent(agent_id)
    if agent is None:
        # 缓存空值,过期时间短一点
        await redis_client.setex(cache_key, 60, "__NULL__")
        return None

    # 3. 写缓存
    await redis_client.setex(cache_key, 3600, json.dumps({"id": agent.id, "name": agent.name}))
    return {"id": agent.id, "name": agent.name}

# ═══ 缓存击穿:互斥锁 ═══
import asyncio

async def get_hot_agent(agent_id: int) -> dict:
    cache_key = f"agent:{agent_id}"
    lock_key = f"lock:agent:{agent_id}"

    # 1. 先查缓存
    cached = await redis_client.get(cache_key)
    if cached:
        return json.loads(cached)

    # 2. 抢锁去重建缓存
    lock_acquired = await redis_client.setnx(lock_key, "1")
    if lock_acquired:
        await redis_client.expire(lock_key, 10)  # 锁过期防止死锁
        try:
            agent = await get_agent(agent_id)
            if agent:
                await redis_client.setex(cache_key, 3600, json.dumps(agent.__dict__))
            return agent
        finally:
            await redis_client.delete(lock_key)  # 释放锁
    else:
        # 3. 没抢到锁 → 等一会儿再查缓存
        await asyncio.sleep(0.1)
        cached = await redis_client.get(cache_key)
        if cached:
            return json.loads(cached)
        return None

# ═══ 缓存雪崩:随机过期时间 ═══
import random
base_ttl = 3600
actual_ttl = base_ttl + random.randint(-300, 300)  # 3600±300 秒
await redis_client.setex(cache_key, actual_ttl, value)

3.3 持久化策略对比

策略 原理 优点 缺点 适用场景
RDB 定时全量快照 恢复快、文件小 可能丢失最后一次快照后的数据 冷备、灾备
AOF 追加每条写命令 数据安全(最多丢 1s) 文件大、恢复慢 对数据一致性要求高
混合 (4.0+) RDB 快照 + 期间增量 AOF 兼顾恢复速度和安全性 --- 生产推荐

面试话术:"生产环境推荐混合持久化:RDB 做全量快照保证恢复速度,AOF 记录增量保证数据安全。Redis 4.0 的混合持久化把两者优势合并了。"

3.4 分布式锁

复制代码
Redis 分布式锁三要素:
  1. 互斥:setnx(SET if Not eXists)
  2. 防死锁:设置过期时间
  3. 防误删:value 用唯一标识(UUID),释放时对比
python 复制代码
import uuid

class RedisLock:
    def __init__(self, client, key: str, expire: int = 10):
        self.client = client
        self.key = f"lock:{key}"
        self.expire = expire
        self.lock_id = str(uuid.uuid4())  # 唯一标识,防止误删别人的锁

    async def __aenter__(self):
        # Lua 脚本保证 setnx + expire 原子性
        lua = """
        if redis.call('SET', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2]) then
            return 1
        else
            return 0
        end
        """
        acquired = await self.client.eval(lua, 1, self.key, self.lock_id, str(self.expire))
        if not acquired:
            raise RuntimeError(f"获取锁失败: {self.key}")
        return self

    async def __aexit__(self, *args):
        # 释放锁:检查 lock_id 是自己的才删(防止误删)
        lua = """
        if redis.call('GET', KEYS[1]) == ARGV[1] then
            return redis.call('DEL', KEYS[1])
        else
            return 0
        end
        """
        await self.client.eval(lua, 1, self.key, self.lock_id)

# 使用
async with RedisLock(redis_client, "rebuild_cache:agent:123"):
    # 只有获取到锁的进程执行这段代码
    await rebuild_agent_cache(123)

面试加分点:提到 Lua 脚本保证原子性、UUID 防误删、看门狗(Watch Dog)自动续期------这三点说完,面试官就知道你是真用过分布式锁,不是背八股文。


四、Docker 容器化 & 部署

4.1 多阶段构建 Dockerfile

dockerfile 复制代码
# ═══════════════════════════════════
# 阶段 1:构建阶段(编译依赖)
# ═══════════════════════════════════
FROM python:3.12-slim AS builder

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

# ═══════════════════════════════════
# 阶段 2:运行阶段(只复制必要的)
# ═══════════════════════════════════
FROM python:3.12-slim AS runtime

WORKDIR /app

# 从 builder 阶段复制已安装的依赖
COPY --from=builder /root/.local /root/.local

# 复制应用代码
COPY app/ ./app/

# 确保 PATH 包含 pip --user 安装的包
ENV PATH=/root/.local/bin:$PATH

EXPOSE 8000

# 不使用 --reload(生产环境)
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

4.2 docker-compose 编排

yaml 复制代码
# docker-compose.yml
version: "3.9"

services:
  app:
    build: .
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/agent_db
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped
    volumes:
      - ./app:/app/app  # 开发环境热重载

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: user
      POSTGRES_PASSWORD: pass
      POSTGRES_DB: agent_db
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user -d agent_db"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

volumes:
  pgdata:
  redisdata:

4.3 Gunicorn + Uvicorn + Nginx 生产部署

复制代码
Nginx (反向代理, 80/443)
  ├── Uvicorn Worker 1 (处理请求)
  ├── Uvicorn Worker 2
  ├── Uvicorn Worker 3
  └── Uvicorn Worker 4
       │
       └── 共享同一个 PostgreSQL + Redis
bash 复制代码
# Gunicorn 管理多个 Uvicorn Worker
gunicorn app.main:app \
  --workers 4 \
  --worker-class uvicorn.workers.UvicornWorker \
  --bind 0.0.0.0:8000 \
  --timeout 120 \
  --keep-alive 5 \
  --max-requests 10000 \
  --max-requests-jitter 1000 \
  --access-logfile - \
  --error-logfile -
nginx 复制代码
# Nginx 反向代理配置
upstream agent_api {
    server 127.0.0.1:8000;
    # 可添加更多 server 实现负载均衡
}

server {
    listen 80;
    server_name api.example.com;

    # SSE 流式输出配置(Agent 必备!)
    location /api/ {
        proxy_pass http://agent_api;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Request-ID $request_id;

        # 关键:关闭缓冲以支持 SSE 流式响应
        proxy_buffering off;
        proxy_cache off;

        # 超时配置(LLM 调用可能很慢)
        proxy_read_timeout 300s;
    }
}

五、测试:pytest + FastAPI TestClient

5.1 异步测试基础

python 复制代码
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app

@pytest.fixture
async def async_client():
    """创建异步测试客户端"""
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as client:
        yield client

@pytest.mark.anyio  # 支持 async 测试函数
async def test_create_agent(async_client):
    response = await async_client.post(
        "/agents",
        json={"name": "TestAgent", "model": "gpt-4"},
    )
    assert response.status_code == 200
    data = response.json()
    assert data["created"] == "TestAgent"

5.2 Mock 外部依赖

python 复制代码
from unittest.mock import AsyncMock, patch

@pytest.mark.anyio
async def test_agent_with_mock_llm(async_client):
    """Mock LLM API 调用,避免测试依赖外部服务"""
    with patch("app.services.llm.call_openai") as mock_llm:
        mock_llm.return_value = "这是模拟的 LLM 回复"

        response = await async_client.post(
            "/chat",
            json={"message": "你好"},
        )

        assert response.status_code == 200
        assert mock_llm.called  # 确认确实调用了 LLM

@pytest.fixture
async def mock_redis():
    """Mock Redis 客户端"""
    mock = AsyncMock()
    mock.get.return_value = None  # 模拟缓存未命中
    return mock

5.3 pytest fixture 作用域

作用域 声明 生命周期 适用场景
function 默认 每个测试函数创建一次 隔离性最好的测试
class scope="class" 每个测试类创建一次 类内测试共享的 fixture
module scope="module" 每个测试文件创建一次 数据库表结构
session scope="session" 整个测试运行创建一次 数据库连接池

六、今日面试题精选(8 道)

Q1. FastAPI 的依赖注入是怎么实现的?和中间件有什么区别?

标准回答

FastAPI 的 Depends 本质是把函数调用图编译成执行计划。它分析每个依赖函数的参数签名,递归解析所有子依赖,构建 DAG(有向无环图),按拓扑序执行。同一个请求内,同一个依赖(相同参数)默认只执行一次(缓存)。

跟中间件的区别:中间件在最外层,包在所有路由外面,能修改请求和响应;依赖只在路由层内执行,用于认证、权限、资源管理,不能直接修改响应。依赖可以通过 yield 管理资源生命周期(类似 pytest fixture),中间件做不到。

Q2. 缓存穿透、击穿、雪崩分别是什么?怎么解决?

标准回答

  • 穿透:查不存在的数据 → 每次都穿透缓存打到 DB。解决:布隆过滤器预先判断是否存在 + 缓存空值设短过期。
  • 击穿:热点 key 过期瞬间大量并发打 DB。解决:互斥锁(只让一个线程重建缓存)+ 逻辑过期(永不过期 + 后台异步刷新)。
  • 雪崩:大量 key 同时过期或 Redis 宕机。解决:过期时间加随机值 + Redis Cluster 高可用 + 限流降级兜底。

Q3. Redis 分布式锁怎么实现?要注意什么?

标准回答

核心:SET key value NX EX seconds(setnx + expire 原子化)。

三个关键细节:① 过期时间防死锁;② value 用 UUID,释放时比对身份防误删;③ 释放操作用 Lua 脚本保证 get + compare + delete 原子性。

加分:提到 RedLock 算法(多节点投票)及其争议(Martin Kleppmann 的批评);提到看门狗(Watch Dog)自动续期解决"业务执行超时锁过期"的问题。

Q4. SQLAlchemy 的 N+1 问题是什么?怎么解决?

标准回答

N+1 是指查 N 个主对象后,再逐个访问关联对象时各发一条 SQL,总共 N+1 条查询。用 selectinload(IN 批量加载,发 2 条 SQL)或 joinedload(LEFT JOIN 一次加载)解决。selectinload 更常用,因为数据量可控、不会产生笛卡尔积。

Q5. Docker 多阶段构建的好处是什么?

标准回答

分离构建环境和运行环境。构建阶段用完整工具链(编译器、dev 依赖)编译/安装依赖;运行阶段只复制最终产物(编译好的包、二进制),镜像体积大幅缩小,没有编译工具链减少攻击面。Python 项目中,构建阶段 pip install,运行阶段 COPY --from 复制 site-packages。

Q6. RDB 和 AOF 有什么区别?生产环境怎么选?

标准回答

  • RDB:定时全量快照,恢复快,可能丢最后一次快照后的数据
  • AOF:追加每条写命令,最多丢 1s,但文件大、恢复慢
  • 生产推荐混合持久化(Redis 4.0+):RDB 全量 + 期间增量 AOF,兼顾速度和安全

Q7. FastAPI 的生命周期事件怎么管理?

标准回答

推荐用 lifespan(@asynccontextmanager),在 yield 前后分别处理启动和关闭逻辑(初始化连接池、预热缓存、关闭连接)。老版的 @app.on_event("startup")@app.on_event("shutdown") 已不推荐,因为 lifespan 支持异步且语义更清晰。

Q8. pytest fixture 的作用域有哪些?为什么要控制作用域?

标准回答

function(默认,每个测试函数一次)、class(每个测试类一次)、module(每个模块一次)、session(整个测试运行一次)。控制作用域是为了在测试隔离性执行速度之间平衡:function 隔离最好但慢,session 快但测试间可能互相干扰。数据库连接池一般用 session 作用域,测试数据准备用 function 作用域。


七、今日代码实战(4 道)

python 复制代码
# ═══════════════════════════════════════════════
# 练习 1:FastAPI 依赖注入 --- 实现一个速率限制器
# ═══════════════════════════════════════════════
import time
from collections import defaultdict
from fastapi import FastAPI, Request, HTTPException, Depends

app = FastAPI()

# 简单的内存限流器(生产环境用 Redis 滑动窗口)
class RateLimiter:
    def __init__(self, max_requests: int = 60, window: int = 60):
        self.max_requests = max_requests  # 60 次
        self.window = window              # 60 秒
        self.records: dict[str, list[float]] = defaultdict(list)

    async def __call__(self, request: Request):
        now = time.time()
        client_ip = request.client.host
        # 清理过期记录
        self.records[client_ip] = [
            t for t in self.records[client_ip] if now - t < self.window
        ]
        if len(self.records[client_ip]) >= self.max_requests:
            raise HTTPException(status_code=429, detail="请求过于频繁")
        self.records[client_ip].append(now)
        return True

rate_limiter = RateLimiter(max_requests=60)

@app.get("/api/chat", dependencies=[Depends(rate_limiter)])
async def chat():
    return {"reply": "你好"}

# ═══════════════════════════════════════════════
# 练习 2:异步 Redis 缓存装饰器
# ═══════════════════════════════════════════════
import functools, json, hashlib, redis.asyncio as aioredis

redis = aioredis.from_url("redis://localhost:6379/0", decode_responses=True)

def cache(ttl: int = 300):
    """异步函数结果缓存装饰器"""
    def decorator(func):
        @functools.wraps(func)
        async def wrapper(*args, **kwargs):
            # 生成缓存 key
            key_parts = f"{func.__name__}:{args}:{sorted(kwargs.items())}"
            cache_key = f"cache:{hashlib.md5(key_parts.encode()).hexdigest()[:12]}"

            # 查缓存
            cached = await redis.get(cache_key)
            if cached:
                return json.loads(cached)

            # 执行函数并缓存
            result = await func(*args, **kwargs)
            await redis.setex(cache_key, ttl, json.dumps(result, default=str))
            return result
        return wrapper
    return decorator

@cache(ttl=600)
async def expensive_llm_call(prompt: str) -> dict:
    """模拟耗时的 LLM 调用"""
    await asyncio.sleep(1)
    return {"response": f"处理: {prompt[:20]}..."}

# ═══════════════════════════════════════════════
# 练习 3:SQLAlchemy 模型 --- Agent 对话系统
# ═══════════════════════════════════════════════
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
from sqlalchemy import String, Integer, Text, DateTime, ForeignKey, func

class Base(DeclarativeBase):
    pass

class Conversation(Base):
    __tablename__ = "conversations"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    title: Mapped[str] = mapped_column(String(200), default="新对话")
    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
    # cascade: 删除对话时自动删除所有消息
    messages: Mapped[list["ChatMessage"]] = relationship(
        "ChatMessage", back_populates="conversation",
        cascade="all, delete-orphan", lazy="selectin"
    )

class ChatMessage(Base):
    __tablename__ = "chat_messages"
    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    conversation_id: Mapped[int] = mapped_column(ForeignKey("conversations.id"), nullable=False)
    role: Mapped[str] = mapped_column(String(20), nullable=False)  # user/assistant/tool
    content: Mapped[str] = mapped_column(Text, nullable=False)
    token_count: Mapped[int] = mapped_column(Integer, default=0)
    created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
    conversation: Mapped["Conversation"] = relationship(back_populates="messages")

# ═══════════════════════════════════════════════
# 练习 4:pytest 异步测试套件
# ═══════════════════════════════════════════════
import pytest
from httpx import ASGITransport, AsyncClient
from unittest.mock import AsyncMock, patch

@pytest.fixture
async def client():
    transport = ASGITransport(app=app)
    async with AsyncClient(transport=transport, base_url="http://test") as ac:
        yield ac

@pytest.mark.anyio
async def test_list_agents_empty(client):
    """数据库中无 Agent 时返回空列表"""
    response = await client.get("/api/agents")
    assert response.status_code == 200
    assert response.json()["agents"] == []

@pytest.mark.anyio
async def test_create_and_get_agent(client):
    """创建 Agent 后能正确获取"""
    # 创建
    resp = await client.post("/api/agents", json={"name": "测试Agent", "model": "gpt-4"})
    assert resp.status_code == 200
    agent_id = resp.json()["id"]

    # 获取
    resp = await client.get(f"/agents/{agent_id}")
    assert resp.status_code == 200
    assert resp.json()["name"] == "测试Agent"

@pytest.mark.anyio
async def test_validation_error(client):
    """参数校验失败返回 422"""
    response = await client.post("/api/agents", json={"name": ""})  # name 太短
    assert response.status_code == 422
    assert "detail" in response.json()

@pytest.mark.anyio
async def test_rate_limit(client):
    """速率限制测试"""
    # 连续发送超过限制的请求
    statuses = []
    for _ in range(65):
        resp = await client.get("/api/chat")
        statuses.append(resp.status_code)
    assert 429 in statuses  # 应该有请求被限流

📊 今日知识图谱

复制代码
Python 工程化速通 DAY 2
│
├── FastAPI
│   ├── 四种参数:Path / Query / Body / Header
│   ├── 依赖注入:Depends、嵌套、yield、缓存
│   ├── 中间件:@app.middleware("http") → 全局生效
│   ├── 异常处理:@app.exception_handler
│   └── 为什么快:异步IO + pydantic-core(Rust) + uvloop + uvicorn
│
├── 数据库
│   ├── SQLAlchemy 2.0:async session + selectinload 防 N+1
│   ├── Mapped / mapped_column(类型安全 ORM)
│   ├── alembic revision --autogenerate → upgrade head
│   └── 连接池:pool_size + max_overflow + pool_pre_ping
│
├── Redis
│   ├── 五种结构:String / Hash / List / Set / ZSet(skiplist)
│   ├── 缓存三兄弟:穿透(空值/布隆) / 击穿(互斥锁) / 雪崩(随机过期)
│   ├── 分布式锁:setnx + expire(Lua) + UUID防误删
│   └── 持久化:RDB(快照) / AOF(追加) / 混合(4.0+生产推荐)
│
├── Docker & 部署
│   ├── 多阶段构建:builder → runtime,减小镜像
│   ├── docker-compose:app + db + redis 一键启动
│   └── Gunicorn(多worker) + Uvicorn(异步) + Nginx(反向代理+SSE缓冲关闭)
│
└── 测试
    ├── pytest.mark.anyio → 异步测试
    ├── httpx.AsyncClient + ASGITransport → FastAPI 测试
    └── unittest.mock.AsyncMock → Mock 外部依赖

🔜 明日预告

Day 3 --- LLM 大模型基础全通

  • Transformer 完整拆解:Self-Attention 数学推导 → MHA/MQA/GQA → RoPE位置编码
  • 训练全流程:Pre-training → SFT → RLHF(PPO) → DPO → DeepSeek GRPO
  • 推理优化:KV Cache、Flash Attention、量化(GPTQ/AWQ/GGUF)
  • 解码策略:Greedy/Beam Search/Top-K/Top-P/Temperature
  • MoE 混合专家模型(DeepSeek 架构)
  • 10 道 LLM 面试真题

💡 速通心法 :Day 2 的核心是把工程底座搭牢------Agent 最终要部署成 API 服务,FastAPI + SQLAlchemy + Redis + Docker 这一套是你以后每天都在用的东西。今天的关键是理解每一层的职责边界:FastAPI 管请求路由,Pydantic 管数据校验,SQLAlchemy 管数据持久化,Redis 管缓存加速,Docker 管环境一致性。睡前对着知识图谱,能说出每层的核心概念和常见坑,今天就过关了。

相关推荐
半兽先生1 小时前
大模型实现金融文本分类
人工智能·python·金融
Hardworking6661 小时前
第6章 数据工程
运维·服务器·数据库·数据工程
汤姆小白1 小时前
06-LoRA参数高效微调
人工智能·python·机器学习·transformer
Elastic 中国社区官方博客2 小时前
4 个英伟达人工智能任务,1 个 Elasticsearch 接口:嵌入、聊天、completion 和重排序
大数据·数据库·人工智能·elasticsearch·搜索引擎·ai
铅笔侠_小龙虾2 小时前
Rust 学习(6)-所有权规则、移动语义、Clone 与 Copy
python·学习·rust
码云骑士2 小时前
65-AI-Agent基础原理-AutoGPT-四大架构全景解读
人工智能·python·架构
Ulyanov2 小时前
刚体动力学方程——牛顿-欧拉与陀螺效应的奥秘
开发语言·python·目标跟踪·雷达电子对抗·导引头
观察员2 小时前
用 Streamlit + 智谱 AI 打造虚拟伴侣聊天机器人
python
G.O.G.O.G3 小时前
LeetCode SQL 从入门到精通(MySQL)05(下)
数据库·sql·oracle
Cubar3 小时前
[一]开源ERP:odoo19二次开发问题-原库存模块问题
python·开源·odoo·开源erp