前端转 AI 100 天 Day 21:FastAPI 入门——从命令行到 HTTP 服务

0 阅读17分钟

前端转 AI 第 21 天:FastAPI 入门 - 从命令行到 HTTP 服务

(明天国庆,假期期间不学习,博主要出去玩!!!假期后继续学习)

昨日回顾

Day 20 把助手打包成了 CLI 工具,能 pip install、能发布。

但有个问题——它只能在本地跑,别人用不了。

想分享给朋友?让他装 Python、装依赖、跑命令?门槛太高。

该上后端了。 今天开始 FastAPI,把助手变成一个 HTTP 服务,任何能发请求的地方都能用——网页、App、其他脚本、Postman。

Day 21 目标:搞懂 FastAPI 的核心,能写接口,把 Day 20 的助手改造成 HTTP 服务。


一、为什么选 FastAPI

Python 的 Web 框架不少,为什么 FastAPI 是AI 应用的首选?

1. 主流框架对比

框架特点类型适合
Flask极简、灵活同步小项目、传统 Web
Django全家桶、重同步大型 CMS、后台
FastAPI现代、异步、类型驱动异步API、AI 应用
Sanic纯异步、快异步高并发
Tornado老牌异步异步WebSocket

2. FastAPI 的三大优势

优势一:基于 Pydantic(Day 11 的成果直接用)

from pydantic import BaseModel

class ChatRequest(BaseModel):
    message: str
    temperature: float = 0.7

@app.post("/chat")
def chat(req: ChatRequest):
    # req 自动校验、自动解析、自动补默认值
    ...

你只写数据模型,FastAPI 帮你做校验 + 解析 + 错误处理。

优势二:自动生成 API 文档

FastAPI 启动后自带两个文档界面:

  • /docs:Swagger UI(可交互)
  • /redoc:ReDoc(更清爽)

你一行文档都没写,它自动根据 Pydantic 模型和类型注解生成。 前端联调时直接给这个链接就行。

优势三:原生异步

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await db.get_user(user_id)   # 直接 await
    return user

Agent 里大量 async 调用(LLM、向量库、HTTP),FastAPI 天生匹配。 Flask / Django 都是同步的,想做异步要额外折腾。

结论:做 AI 应用,没有理由不用 FastAPI。

3. 安装

pip install "fastapi[standard]"

[standard] 会连 uvicorn 等常用工具一起装上。


二、第一个 FastAPI 应用

1. Hello World

新建 day21/first_api/main.py:

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
def read_root():
    return {"message": "Hello, FastAPI"}


@app.get("/health")
def health_check():
    return {"status": "ok"}

2. 启动

cd day21/first_api
uvicorn main:app --reload

参数解释:

  • main:app:main.py 文件里的 app 对象
  • --reload:改代码自动重启(开发模式)

输出:

INFO:     Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)
INFO:     Started reloader process [12345]
INFO:     Started server process [12346]
INFO:     Application startup complete.

3. 测试

打开浏览器:

或者用 curl:

curl http://127.0.0.1:8000/

或前端习惯的写法:

fetch("http://127.0.0.1:8000/").then(r => r.json()).then(console.log)

4. 见证奇迹:打开 /docs

访问 http://127.0.0.1:8000/docs

你会看到一个完整的、可交互的 Swagger UI:

  • 列出了所有接口
  • 每个接口都有"Try it out"按钮(可以直接在浏览器里测试)
  • 参数说明、返回结构、错误码全有

你一行文档都没写! 这就是 FastAPI 最爽的地方。

**再访问 http://127.0.0.1:8000/redoc**——另一个风格的文档界面。


三、路由与参数

1. 路径参数

@app.get("/users/{user_id}")
def get_user(user_id: int):     # ← 声明 int,自动转换
    return {"user_id": user_id, "type": type(user_id).__name__}

访问 /users/123:

{"user_id": 123, "type": "int"}

访问 /users/abc:

{
  "detail": [
    {
      "loc": ["path", "user_id"],
      "msg": "Input should be a valid integer...",
      "type": "int_parsing"
    }
  ]
}

看到没?int 类型不匹配,FastAPI 自动返回 422 错误,还告诉你哪里错了。

前端视角: 相当于给每个参数都配了 zod 校验,还自动生成错误信息。

2. 查询参数

@app.get("/search")
def search(
    q: str,
    limit: int = 10,
    sort: str = "desc",
):
    return {"q": q, "limit": limit, "sort": sort}

访问 /search?q=python&limit=5:

{"q": "python", "limit": 5, "sort": "desc"}

规则:

  • 函数参数没在路径里 → FastAPI 自动识别为查询参数
  • 有默认值 → 可选参数
  • 没默认值 → 必填参数(不传报错)

3. 用 Query 加约束

from fastapi import Query

@app.get("/items")
def list_items(
    q: str | None = Query(default=None, max_length=50, description="搜索关键词"),
    skip: int = Query(default=0, ge=0, description="跳过多少条"),
    limit: int = Query(default=10, ge=1, le=100, description="返回多少条"),
):
    return {"q": q, "skip": skip, "limit": limit}

ge / le / max_length 和 Day 11 的 Pydantic Field 一样——因为 FastAPI 就是用 Pydantic 做的。

description 会显示在 /docs 里。 前端联调时一目了然。

4. 路径参数 with 约束

from fastapi import Path

@app.get("/users/{user_id}")
def get_user(
    user_id: int = Path(ge=1, description="用户 ID,正整数"),
):
    return {"user_id": user_id}

四、请求体:POST 的标配

这是 FastAPI 最强的地方——用 Pydantic 模型定义请求体。

from pydantic import BaseModel, Field

class ChatMessage(BaseModel):
    role: str
    content: str

class ChatRequest(BaseModel):
    messages: list[ChatMessage]
    model: str = "gpt-4o-mini"
    temperature: float = Field(default=0.7, ge=0, le=2)
    max_tokens: int = Field(default=1024, ge=1, le=32000)


@app.post("/chat")
def chat(req: ChatRequest):
    # 到这里 req 已经被完整校验
    last = req.messages[-1]
    return {
        "reply": f"你说的是:{last.content}",
        "model": req.model,
        "tokens": 42,
    }

前端发请求:

fetch("http://127.0.0.1:8000/chat", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    messages: [{ role: "user", content: "你好" }],
    temperature: 0.7,
  }),
});

你什么都没写,FastAPI 就做到了:

  • ✅ 校验 messages 非空
  • ✅ 校验 temperature 在 0-2 之间
  • ✅ 校验 max_tokens 是正整数
  • ✅ 自动补默认值
  • ✅ 类型转换("0.7" → 0.7)
  • ✅ 生成错误信息
  • ✅ 在 /docs 里展示结构

这就是 Day 11 说的"Pydantic 是 Agent 底牌"的最好体现。

1. 混合使用:路径 + 查询 + 请求体

@app.put("/users/{user_id}")
def update_user(
    user_id: int,                        # 路径参数
    send_email: bool = False,            # 查询参数
    user: UserUpdate = ...,              # 请求体(Pydantic 模型)
):
    return {"user_id": user_id, "send_email": send_email, "user": user}

FastAPI 自动识别三种参数的位置——靠类型注解区分。

2. 单值请求体

如果请求体就是一个简单值(比如一个字符串),用 Body:

from fastapi import Body

@app.post("/echo")
def echo(text: str = Body(embed=True)):
    return {"text": text}

请求体:

{"text": "hello"}

embed=True 表示"包一层对象"。 不写的话请求体就是裸字符串 "hello"。


五、响应:response_model 和状态码

1. response_model:自动过滤敏感字段

场景: 用户对象有 password_hash,返回时不能带上。

class UserInDB(BaseModel):
    id: int
    name: str
    password_hash: str    # 敏感


class UserOut(BaseModel):
    id: int
    name: str


@app.get("/users/{user_id}", response_model=UserOut)
def get_user(user_id: int) -> UserInDB:
    return UserInDB(id=1, name="张三", password_hash="xxx")

返回给客户端的只有 {"id": 1, "name": "张三"},password_hash 自动被过滤。

这个特性非常重要——防止不小心泄露字段。

2. 直接返回 Pydantic 模型

@app.get("/user", response_model=UserOut)
def get_user():
    return UserOut(id=1, name="张三")   # FastAPI 自动转 JSON

和返回 dict 一样,但类型更明确。

3. 状态码

from fastapi import status

@app.post("/users", status_code=status.HTTP_201_CREATED)
def create_user(user: UserIn):
    ...
    return {"id": 1}

常用状态码:

状态码含义
200成功(默认)
201创建成功
204成功但无内容(删除)
400客户端错误
401未认证
403无权限
404找不到
422参数校验失败(FastAPI 自动用)
500服务器错误

4. 抛异常:HTTPException

from fastapi import HTTPException

@app.get("/users/{user_id}")
def get_user(user_id: int):
    user = db.get(user_id)
    if not user:
        raise HTTPException(
            status_code=404,
            detail=f"用户 {user_id} 不存在",
        )
    return user

客户端收到:

{"detail": "用户 123 不存在"}

HTTPException 会自动转成 JSON 响应 + 状态码。


六、中间件和生命周期

1. CORS:前端联调必配

前端在 localhost:5173 起服务,后端在 localhost:8000——浏览器会拦。

from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:5173"],   # 前端地址
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)

开发阶段可以先 allow_origins=["*"] 方便调试,生产环境再收紧。

这个问题前端同学最熟——浏览器跨域,后端加 CORS 头就能解决。

2. 启动和关闭:lifespan

FastAPI 推荐用 lifespan 管理资源(数据库连接、LLM 客户端):

from contextlib import asynccontextmanager
from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    # 启动时执行
    print("启动:初始化数据库")
    app.state.db = init_database()

    yield    # ← 应用运行期间

    # 关闭时执行
    print("关闭:清理资源")
    app.state.db.close()


app = FastAPI(lifespan=lifespan)


@app.get("/")
def root():
    db = app.state.db   # 从 app.state 拿
    return {"status": "ok"}

好处:

  • 一次性初始化:不用每个请求都建连接
  • 优雅关闭:进程退出前能清理资源
  • 可测试:可以在测试里替换 app.state.db

Day 23 会详细讲这套模式。


七、实战:把助手改造成 HTTP 服务

把 Day 20 的命令行助手改造成 HTTP API。

1. 项目结构

cp -r day20/ai-assistant-v7 day21/ai-assistant-api
cd day21/ai-assistant-api

目录调整:

src/ai_assistant/
├── __init__.py
├── main.py               # CLI 入口(保留)
├── api/
│   ├── __init__.py
│   ├── app.py            # ← FastAPI 实例(新增)
│   ├── routes.py         # ← 路由定义(新增)
│   ├── schemas.py        # ← API 请求/响应模型(新增)
│   └── deps.py           # ← 依赖注入(新增)
├── config.py
├── models.py
├── orm_models.py
├── db_orm.py
├── storage_orm.py
├── llm.py
├── memory.py
└── ...

新增 api/ 子包,与 CLI 层解耦。

2. api/schemas.py:API 请求响应模型

"""API 请求/响应模型"""
from pydantic import BaseModel, Field
from datetime import datetime
from typing import Literal


Role = Literal["system", "user", "assistant"]


class MessageSchema(BaseModel):
    role: Role
    content: str = Field(min_length=1, max_length=10000)


class ChatRequest(BaseModel):
    """对话请求"""
    message: str = Field(min_length=1, max_length=10000, description="用户消息")
    session_id: str | None = Field(default=None, description="会话 ID,不传则创建新会话")


class ChatResponse(BaseModel):
    """对话响应"""
    session_id: str
    reply: str
    message_count: int


class SessionInfo(BaseModel):
    """会话信息"""
    id: str
    title: str
    updated_at: datetime
    message_count: int


class MessageInfo(BaseModel):
    """消息信息"""
    role: Role
    content: str
    timestamp: datetime


class SessionDetail(BaseModel):
    """会话详情"""
    id: str
    title: str
    messages: list[MessageInfo]
    created_at: datetime
    updated_at: datetime


class SessionListResponse(BaseModel):
    """会话列表响应"""
    sessions: list[SessionInfo]
    total: int

关键点:这些模型和 Day 14 的业务模型不同。

  • 业务模型(models.py):内部用
  • API 模型(schemas.py):对外用

为什么要分开?

  • 业务模型可能变化:内部字段调整不影响 API
  • API 模型有约束:max_length / description 是给前端看的
  • 可以隐藏字段:内部字段不对 API 暴露

这是"API 契约"的思想。 前端联调时看 /docs 就够了,不用读你源码。

3. api/deps.py:依赖注入

FastAPI 的"依赖注入"是一大特色——把"每次请求都需要的东西"抽出来复用。

"""依赖注入"""
from fastapi import Depends
from functools import lru_cache

from ..config import config
from ..db_orm import DatabaseORM
from ..storage_orm import SessionStorageORM
from ..llm import MockLLMClient
from ..memory import MemoryManager


@lru_cache
def get_db() -> DatabaseORM:
    """全局唯一的数据库连接"""
    return DatabaseORM(config.DATA_DIR / "assistant.db")


@lru_cache
def get_storage() -> SessionStorageORM:
    return SessionStorageORM(get_db())


def get_llm() -> MockLLMClient:
    return MockLLMClient()


def get_memory() -> MemoryManager:
    return MemoryManager(
        max_context_tokens=8000,
        recent_window=10,
        summary_threshold=20,
    )

lru_cache 保证 get_db() 只执行一次——第一次调用建连接,后续直接返回同一个对象。

用法:

@app.get("/sessions")
def list_sessions(storage: SessionStorageORM = Depends(get_storage)):
    return storage.list_sessions()

Depends(get_storage) 告诉 FastAPI:

这个接口需要 storage,帮我调用 get_storage() 拿一个。

好处:

  • 不用手动传参
  • 可测试:测试时替换 get_storage 就行
  • 可复用:多个接口共享同一个依赖

前端类比: 有点像 React 的 Context,或者 NestJS 的 @Injectable()。

4. api/routes.py:路由定义

"""API 路由"""
from fastapi import APIRouter, HTTPException, Depends

from ..models import Session as SessionModel, Message
from ..llm import LLMError
from ..storage_orm import SessionStorageORM
from .schemas import (
    ChatRequest, ChatResponse,
    SessionInfo, SessionListResponse,
    SessionDetail, MessageInfo,
)
from .deps import get_storage, get_llm, get_memory


router = APIRouter(prefix="/api", tags=["assistant"])


# ============ 健康检查 ============

@router.get("/health")
def health():
    return {"status": "ok"}


# ============ 对话 ============

@router.post("/chat", response_model=ChatResponse)
async def chat(
    req: ChatRequest,
    storage: SessionStorageORM = Depends(get_storage),
    llm = Depends(get_llm),
    memory = Depends(get_memory),
):
    """
    发送一条消息,返回助手的回复
    
    - 如果传了 session_id,就在该会话里继续对话
    - 如果不传,会创建一个新会话
    """
    # 1. 加载或创建会话
    if req.session_id:
        session = storage.load(req.session_id)
        if not session:
            raise HTTPException(status_code=404, detail=f"会话不存在: {req.session_id}")
    else:
        import uuid
        session = SessionModel(session_id=uuid.uuid4().hex[:8])

    # 2. 加用户消息
    session.add_message("user", req.message)

    # 3. 组装上下文
    context = memory.get_context(session.messages)
    messages = [{"role": "system", "content": "你是一个友好的 AI 助手"}]
    messages.extend(m.to_llm_format() for m in context)

    # 4. 调 LLM
    try:
        reply = await llm.chat(messages)
    except LLMError as e:
        raise HTTPException(status_code=503, detail=f"模型调用失败: {e}")

    # 5. 加助手回复 + 保存
    session.add_message("assistant", reply)
    storage.save(session)

    return ChatResponse(
        session_id=session.session_id,
        reply=reply,
        message_count=session.message_count,
    )


# ============ 会话管理 ============

@router.get("/sessions", response_model=SessionListResponse)
def list_sessions(storage: SessionStorageORM = Depends(get_storage)):
    """列出所有会话"""
    sessions = storage.list_sessions()
    return SessionListResponse(
        sessions=[SessionInfo(**s) for s in sessions],
        total=len(sessions),
    )


@router.get("/sessions/{session_id}", response_model=SessionDetail)
def get_session(session_id: str, storage: SessionStorageORM = Depends(get_storage)):
    """获取会话详情"""
    session = storage.load(session_id)
    if not session:
        raise HTTPException(status_code=404, detail="会话不存在")

    return SessionDetail(
        id=session.session_id,
        title=session.title,
        messages=[
            MessageInfo(role=m.role, content=m.content, timestamp=m.timestamp)
            for m in session.messages
        ],
        created_at=session.created_at,
        updated_at=session.updated_at,
    )


@router.delete("/sessions/{session_id}", status_code=204)
def delete_session(session_id: str, storage: SessionStorageORM = Depends(get_storage)):
    """删除会话"""
    if not storage.delete(session_id):
        raise HTTPException(status_code=404, detail="会话不存在")


@router.get("/search")
def search_sessions(
    q: str,
    limit: int = 20,
    storage: SessionStorageORM = Depends(get_storage),
):
    """搜索包含关键字的会话"""
    results = storage.search(q, limit=limit)
    return {"keyword": q, "results": results, "count": len(results)}

看几个关键设计:

1. APIRouter 统一前缀和标签

router = APIRouter(prefix="/api", tags=["assistant"])

所有路由自动加 /api 前缀,/docs 里分组显示。

2. Depends 拿依赖

storage: SessionStorageORM = Depends(get_storage)

不用手动 storage = get_storage(),FastAPI 帮你注入。 而且同一个请求里多次 Depends(get_storage) 会拿到同一个实例(有缓存机制)。

3. 业务异常转 HTTP 异常

if not session:
    raise HTTPException(status_code=404, detail="会话不存在")

内部业务逻辑不关心 HTTP 状态码,路由层负责转换。

这是"分层架构"的体现——业务层不知道 HTTP 的存在。

4. 异步 + 同步混用

@router.post("/chat", ...)
async def chat(...):   # 因为内部有 await llm.chat,所以用 async
    ...

@router.get("/sessions", ...)
def list_sessions(...):   # 纯数据库操作,用 def 就行(FastAPI 会丢到线程池)
    ...

规则:

  • 内部有 await → 用 async def
  • 纯同步代码(数据库、CPU 计算) → 用 def,FastAPI 会自动放线程池

别把所有接口都写成 async——同步代码在 async def 里会阻塞事件循环。

5. api/app.py:FastAPI 实例

"""FastAPI 应用"""
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from ..logger import get_logger
from .routes import router
from .deps import get_db


logger = get_logger(__name__)


@asynccontextmanager
async def lifespan(app: FastAPI):
    """启动/关闭钩子"""
    logger.info("API 启动")
    db = get_db()
    # 数据库已在 init 时建表,这里不需要额外操作
    yield
    logger.info("API 关闭")
    db.close()


def create_app() -> FastAPI:
    """应用工厂"""
    app = FastAPI(
        title="AI 助手 API",
        description="前端转 AI 学习项目的后端服务",
        version="0.1.0",
        lifespan=lifespan,
    )

    # CORS
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],   # 开发阶段放开,生产收紧
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )

    # 挂路由
    app.include_router(router)

    return app


app = create_app()

关键点:

1. lifespan 管理资源

启动时:初始化数据库;关闭时:清理资源。

2. create_app() 应用工厂

为什么用工厂函数,而不是直接 app = FastAPI()?

  • 可测试:测试时可以传不同配置
  • 可多实例:某些场景需要创建多个 app
  • 更规范:社区推荐模式

3. app.include_router()

把路由挂到 app 上。 后续加新模块(比如 /api/tools)就再写一个 router,include 进来就行。

6. 启动 API

在 pyproject.toml 里加一个命令:

[project.scripts]
ai-assistant = "ai_assistant.main:main"
ai-assistant-api = "ai_assistant.api.run:main"

创建 api/run.py:

"""API 启动入口"""
import uvicorn
from ..logger import setup_logging


def main():
    setup_logging("INFO")
    uvicorn.run(
        "ai_assistant.api.app:app",
        host="0.0.0.0",
        port=8000,
        reload=True,     # 开发模式
    )


if __name__ == "__main__":
    main()

运行:

pip install -e .
ai-assistant-api

或直接:

uvicorn ai_assistant.api.app:app --reload

访问 http://localhost:8000/docs,看到完整文档。

7. 测试整个流程

用 curl 或 Postman 测试:

1. 发一条消息(不传 session_id):

curl -X POST http://localhost:8000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "你好"}'

响应:

{
  "session_id": "a3f9b2e1",
  "reply": "你好!我是一个模拟的 AI 助手...",
  "message_count": 2
}

2. 继续这个会话:

curl -X POST http://localhost:8000/api/chat \
  -H "Content-Type: application/json" \
  -d '{"message": "Python 是什么", "session_id": "a3f9b2e1"}'

3. 列出所有会话:

curl http://localhost:8000/api/sessions

4. 获取会话详情:

curl http://localhost:8000/api/sessions/a3f9b2e1

5. 删除会话:

curl -X DELETE http://localhost:8000/api/sessions/a3f9b2e1

6. 搜索:

curl "http://localhost:8000/api/search?q=Python"

8. 前端调用示范

前端拿到 API 后,fetch 就好:

const API_BASE = "http://localhost:8000/api";

async function chat(message, sessionId = null) {
  const res = await fetch(`${API_BASE}/chat`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ message, session_id: sessionId }),
  });

  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.detail);
  }

  return res.json();
}

// 使用
const result = await chat("你好");
console.log(result.reply);

你前端的 fetch 代码,一行不用改——FastAPI 返回的就是标准 JSON + 状态码。

9. 从 curl 到前端,全链路打通

你现在有了一个完整的 AI 助手服务:

┌─────────────┐
│  前端页面   │  ← fetch / axios
│  (React)    │
└──────┬──────┘
       │ HTTP
       ▼
┌─────────────┐
│  FastAPI    │  ← /api/chat /api/sessions
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  业务层     │  ← SessionStorageORM / MemoryManager
└──────┬──────┘
       │
       ▼
┌─────────────┐
│  SQLite     │
└─────────────┘

Day 46 接真 LLM 时,只需要改 llm.py。 API 层、业务层、存储层全不动。

这就是 Day 13 说的"架构好,改一处不影响全局"。


八、前端视角:FastAPI vs Express/Koa

做过前端的你,会觉得 FastAPI 极其顺手。

概念ExpressFastAPI
应用实例const app = express()app = FastAPI()
定义路由app.get("/", handler)@app.get("/") 装饰器
路径参数req.params.id函数参数 user_id: int
查询参数req.query.q函数参数 q: str
请求体req.body(要 body-parser)Pydantic 模型(自动)
校验手写或 zodPydantic(内置)
错误处理res.status(404).json(...)raise HTTPException(404, ...)
中间件app.use(...)app.add_middleware(...)
自动文档装 swagger-jsdoc内置
类型安全TypeScriptPython 类型注解

核心差异:

1. FastAPI 是"类型驱动"的

Express 里你得手写校验:

app.post("/chat", (req, res) => {
  const { message } = req.body;
  if (!message || typeof message !== "string") {
    return res.status(400).json({ error: "message required" });
  }
  // ...
});

FastAPI 里:

@app.post("/chat")
def chat(req: ChatRequest):
    # 校验已经做完了
    ...

2. FastAPI 自带文档

Express 要装 swagger-jsdoc 或 tsoa 才能有文档。 FastAPI 天然有。

3. 异步语法几乎一样

Express:

app.get("/users/:id", async (req, res) => {
  const user = await db.getUser(req.params.id);
  res.json(user);
});

FastAPI:

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    user = await db.get_user(user_id)
    return user

一一对应,几乎无痛迁移。


九、今日踩坑

  1. uvicorn main:app 找不到模块 → 要在项目根目录执行,或者装成包 pip install -e .
  2. @app.get 忘了写 → 路由没注册,404
  3. 所有接口都写 async def → 同步代码在 async def 里会阻塞事件循环,纯同步用 def
  4. Pydantic 模型和业务模型混用 → 分开定义,API 契约更清晰
  5. Depends 里用了 @lru_cache 但对象不该全局 → 有状态的(如请求级 Session)不能用 lru_cache
  6. CORS 没配 → 前端联调一堆跨域错误
  7. response_model 忘了加 → 敏感字段泄露,比如密码 hash
  8. raise HTTPException 写成了 return → 状态码不对
  9. async def 里调同步库(requests / time.sleep) → 阻塞事件循环,要用 asyncio.to_thread 或换异步库
  10. --reload 上了生产 → 生产环境要关掉,性能和安全都受影响

十、今日总结

今天核心收获三件事:

  1. FastAPI = 类型驱动 + Pydantic + 异步,是 AI 应用后端的最优选择
  2. @app.get / @app.post + Pydantic 模型,接口定义清晰,自动校验、自动文档
  3. Depends 依赖注入 + lifespan 生命周期,是 FastAPI 的"高级特性",也是工程化关键

一个感受:

前端的"接口对接"是一个边界,FastAPI 把这条边界变得极其清晰。
你写 Pydantic 模型,前端看 /docs,双方都不需要看对方源码。
这就是 FastAPI 的最大价值——它把"API 契约"从文档约定变成了代码约束。

你的项目现在是一个真正的"AI 助手服务"了。
CLI 能跑,HTTP 能调,前端能连,测试覆盖,可分发包。

从"脚本"到"工具"到"服务",一个完整的软件进化链走完了。

Day 21 完成度:✅

明日预告(Day 22)

  • FastAPI 进阶:
    • 依赖注入的更多用法(带参数、类依赖)
    • 中间件:请求日志、耗时统计
    • 异常处理器:统一错误格式
    • 路由分组与嵌套
  • 认证入门:API Key 校验
  • 给 API 加一套完整的中间件栈

最后

如果这篇对你有用,点个赞让我知道有人在看。

评论区互动: 你之前写后端用什么?Express / Koa / Flask / Django?和 FastAPI 比怎么样?

有问题评论区见,我们国庆假期结束后继续。

100 天,国庆假期结束后见。


附录:系列目录

已完成 ✅

阶段天数内容
基础01-06Python 语法、数据结构、函数、OOP、文件、依赖管理
网络07-08HTTP 请求、异步 asyncio
工程09-12正则、装饰器、Pydantic、日志调试
项目13-14命令行 AI 助手 v1 / v2
复盘15阶段总结
数据16-17SQLite 数据库、SQLAlchemy ORM
测试18-19pytest 入门 + 进阶
工具20CLI 打包与发布
后端21FastAPI 入门 ✅

进行中 🎯

阶段天数内容
后端22-30FastAPI 进阶 + 完整服务端
大模型31-45LLM API + Prompt 工程
RAG46-60向量数据库 + 知识库
Agent61-80LangChain / 工具调用
实战81-100完整 Agent 项目上线

100 天后见,一起从切图仔变成 Agent 开发者。