📅 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 管环境一致性。睡前对着知识图谱,能说出每层的核心概念和常见坑,今天就过关了。