04-模块一-全景认知 第04讲-CodeSentinel 项目启动 - Python + FastAPI 工程环境与项目骨架搭建

2 阅读28分钟

模块一-全景认知 | 第04讲:CodeSentinel 项目启动 - Python + FastAPI 工程环境与项目骨架搭建

开场:从“想法”到“可运行的骨架”

欢迎来到《AI 架构师与代码审核实战》模块一的第四讲。前三讲我们讨论了 AI 时代架构师的职责边界、技术栈全景,以及如何把大模型能力“产品化”为可治理的工程能力。从这一讲开始,贯穿全课的实战项目 CodeSentinel 将真正落地:我们要搭建一个 AI 驱动的代码审核与架构治理平台 的工程底座。

很多团队在“AI 项目”里犯的第一个错误,不是模型选错,而是 工程起点选错:一上来就把 LangChain、向量库、提示词堆进一个 main.py,几天后就陷入“改一处崩一片”的泥潭。架构师的职责之一,是在不确定性(模型的概率输出)之外,先铺好 确定性的工程外壳——清晰的分层、可测试的结构、可配置的环境、可复制的容器化开发体验。

本讲目标非常具体:你将得到一套 Clean Architecture 风格的目录骨架、一份 可直接 uv syncpyproject.toml、一个 FastAPI 应用工厂、基于 pydantic-settings 的配置管理、健康检查接口,以及 Docker / docker-compose 的开发环境。完成后,你可以用 uv run uvicorn 本地启动,也可以用 docker compose up 一键拉起服务。

这一讲内容偏“基建”,但它是后续所有领域模型、用例服务、LLM 编排的承载面。把骨架搭对,后面的 AI 能力才有地方“长”,而不是到处打补丁。

为了让“开场”真正达到课堂口径的篇幅,这里把本讲的学习路径再展开一层:你会先理解 为什么启动期要刻意克制(不把 LLM 写进路由),再理解 为什么配置必须类型化(避免线上才爆炸),最后理解 为什么容器化是团队契约(不是运维专属)。这三件事合在一起,构成 CodeSentinel 后续所有模块的“工程宪法”。当你在后面看到我们把 LangChain 的链路与工具调用接进来时,请始终记得:它们应当像插件一样插入,而不是像胶水一样涂满全局。


全局视角:CodeSentinel 启动期的系统切片

在完整平台出现之前,启动期我们只需要看清三件事:入口如何装配配置如何注入运行环境如何一致。下图描述了本讲交付物的逻辑关系(表现层只依赖应用层接口,基础设施细节延后实现,用虚线表示后续模块会填充)。

flowchart TB
  subgraph Dev["开发体验"]
    UV["uv / pyproject.toml"]
    DC["docker-compose.yml"]
    DK["Dockerfile"]
  end

  subgraph Runtime["运行时"]
    UVI["Uvicorn ASGI"]
    APP["FastAPI App Factory"]
    CFG["pydantic-settings"]
    HC["/health 健康检查"]
  end

  subgraph Layers["分层骨架(本讲先占位)"]
    PRE["presentation/"]
    APP_L["application/"]
    DOM["domain/"]
    INF["infrastructure/"]
  end

  UV --> UVI
  DC --> UVI
  DK --> UVI
  UVI --> APP
  APP --> CFG
  APP --> HC
  APP --> PRE
  PRE -.-> APP_L
  APP_L -.-> DOM
  APP_L -.-> INF

第二张图从 部署视角 说明开发容器与宿主机挂载的常见模式(便于热重载与依赖缓存)。

flowchart LR
  subgraph Host["宿主机工作区 codesentinel/"]
    SRC["src/"]
    TST["tests/"]
    PY["pyproject.toml"]
  end

  subgraph Container["dev 容器"]
    WDIR["/app"]
    UV_RUN["uv run uvicorn ... --reload"]
  end

  SRC --> WDIR
  PY --> WDIR
  TST --> WDIR
  WDIR --> UV_RUN

核心原理:为什么用应用工厂、为什么 settings 要独立

1. FastAPI 应用工厂(Application Factory)

应用工厂模式的核心思想是:create_app() 负责装配main 或 ASGI 入口只负责 选择环境并调用工厂。好处包括:

  • 测试友好:测试里可以 create_app() 注入测试配置、替换依赖。
  • 多实例/多配置:同一套代码在不同环境(dev/staging/prod)用不同 settings 启动。
  • 延迟导入副作用:避免在模块 import 阶段就做网络、读文件等操作。

2. pydantic-settings 与 12-Factor 配置

pydantic-settings 把环境变量、.env 文件、默认值统一成 强类型配置对象。对 CodeSentinel 这种要接多种外部系统(LLM、向量库、Git 托管、消息队列)的项目,配置即契约:字段缺失应在启动期失败,而不是运行到一半才抛莫名其妙的 KeyError

本讲先定义最小集合:APP_NAMEENVDEBUGHOSTPORTLOG_LEVEL

3. Clean Architecture 目录骨架的“现在时”与“将来时”

本讲在 domain/application/infrastructure/ 中放置 最小可导入包__init__.py),避免空目录在部分工具链下不被识别;真正的领域模型与用例将在后续讲次填充。表现层(presentation/)先挂一个 health 路由,验证 ASGI 栈路与依赖注入路径通畅。

4. 容器化的目标不是“能跑”,而是“团队同构”

docker-compose.yml 的价值在于:新同学不需要在本机装对版本的 Python、系统库、编译链,也能获得一致体验。开发镜像建议:

  • 使用官方 slim 镜像减小体积;
  • 安装 uv 加速依赖解析;
  • 挂载源码目录实现 reload;
  • 暴露端口与设置环境变量与本地一致。

5. pyproject.toml:把“项目意图”写成机器可读合同

PEP 621 之后的 Python 项目更推荐把元数据与依赖声明集中在 pyproject.toml。对架构师而言,这份文件不是“安装说明”,而是 对外边界:哪些库属于运行时必需、哪些属于开发期工具、测试如何发现包、构建系统如何选择,都会在这里固化。CodeSentinel 后续会在依赖上同时面对“数据面(SQLAlchemy)”“向量面(Chroma)”“智能编排面(LangChain)”,如果不提前把版本区间与可选依赖分层写清楚,很容易出现“某人本机升级了一个间接依赖导致向量客户端不兼容”的集体返工。本讲把 LangChain 与 Chroma 先声明进来,是为了让后续讲次不再反复改项目骨架;如果你希望更极致的冷启动速度,也可以在评审中把部分依赖挪到 optional,但务必在 ADR 里记录取舍。

6. ASGI、Uvicorn 与异步边界:现在简单,未来不翻车

FastAPI 运行在 ASGI 服务器之上(本讲用 Uvicorn)。很多团队在早期只写同步路由,后期突然引入阻塞式调用或重计算,导致事件循环被拖慢。CodeSentinel 的审核链路迟早会包含 I/O 密集(拉取仓库、调用模型、写入向量索引)与 CPU 密集(部分静态分析)混合负载。启动期就要在团队共识里写清楚:慢路径默认异步,CPU 密集任务考虑线程池或独立 worker,而不是在请求线程里“顺便算算”。这属于性能架构的伏笔,但最好从第一天就避免“同步写满、后期无处下手”。

7. 测试先行:健康检查不是摆设,是契约

/healthtests/test_health.py 看起来微不足道,但它建立了两个关键契约:第一,路由注册路径正确;第二,应用工厂在测试里可注入配置。后续当你把鉴权、中间件、全局异常处理加进来时,这个测试会第一时间告诉你“装配是否仍然可控”。对 AI 团队尤其重要的是:模型生成的路由代码很容易漏注册或漏前缀,最小测试是成本最低的回归锚点。

8. 日志级别、结构化字段与可关联追踪

启动期就要约定日志格式:至少包含时间、级别、logger 名、消息,并预留 request_idtrace_id 字段(即使现在还未接入 OpenTelemetry)。CodeSentinel 后续会在一次审核链路里串联 Web 请求、规则执行、模型调用与向量检索;如果日志不可关联,你会在事故复盘时失去时间线。settings.log_level 的存在不是摆设,而是为了让 staging 与 production 默认更安静、开发默认更啰嗦。请避免在启动路径打印敏感配置,哪怕只是“调试方便”。

9. OpenAPI 与类型提示:把“接口契约”前移到开发阶段

FastAPI 会自动生成 OpenAPI schema,这对前后端协作与后续生成客户端非常有用。更关键的是:路由函数返回值使用明确类型(本讲 dict[str, Any] 是起步,后续会换成 Pydantic 模型),能让静态检查与 IDE 补全更早发现问题。对 AI 辅助编码而言,类型提示相当于“第二套提示词”,能显著减少胡编字段名的情况。架构师要在团队规范里写清楚:新增路由必须带类型与标签,禁止长期停留在 Any

10. 多环境矩阵:用一张表消灭口头约定

建议维护一张简单矩阵:dev 允许 DEBUG=true,staging 接近生产但数据可脱敏,production 强制关闭调试并收紧日志。环境变量名保持一致,只改变量值;不要在代码里写 if env == "prod" 的魔法字符串散落各处,而是集中在 settings 与少数策略模块。CodeSentinel 未来会接多家模型供应商,更要把“环境差异”限制在配置层,否则你会在代码里看到一堆硬编码分支。

11. 与 CI 对齐:本地命令即流水线命令

团队最常见的摩擦之一是:本地能跑、CI 不能跑。解决办法是强制 uv run ... 作为唯一入口,并在 CI 使用相同命令。Dockerfile 里同样使用 uv sync,保证容器与本地依赖解析一致。若 CI 需要加速,可以缓存 uv 的下载目录,但不要换用另一套安装工具,否则“锁定语义”会被悄悄破坏。

12. 安全默认值:从第一天就拒绝“调试后门”

不要在骨架阶段引入未鉴权的管理接口、不要在生产配置打开任意 CORS、不要把 reload 带进生产镜像。CodeSentinel 处理的是代码与评审数据,属于高敏感场景;安全基线应当与功能同步增长,而不是等功能完成后再补洞。启动期的“克制”,是未来安全审计时最少的解释成本。


代码实战:完整项目骨架(可直接复制落地)

代码走读:从目录树到 docker compose up 的完整链路(建议你按顺序对照)

这一节用中文把下面每个代码块的“意图”讲透,确保你不只是复制粘贴,而是知道每一块在系统里承担什么责任。整体链路是:依赖声明 → 配置模型 → 路由聚合 → 应用工厂 → 测试验证 → 容器编排。这条链路未来会被复制到更复杂的模块里,只是代码变多,骨架不变。

首先看目录结构src/codesentinel 是安装后可导入的包名空间;config 放 settings;presentation 放 FastAPI 路由与 APIRouter 聚合;domain/application/infrastructure 先占位,是为了让 import 路径、依赖方向检查与未来拆分一次到位。testspyproject.toml 中的 pythonpath = ["src"] 配合,保证测试能找到包。不要把测试写成依赖“当前工作目录碰巧正确”的脆弱脚本。

接着看 pyproject.tomldependencies 列出运行时最小集合,其中 LangChain、SQLAlchemy、Chroma、httpx 是 CodeSentinel 后续能力的“预埋”,避免每讲都改依赖导致合并冲突。optional-dependencies.dev 把测试与静态检查工具隔离,生产镜像构建时可以选择不安装 dev,从而减小攻击面与镜像体积。hatchling 作为 build backend 负责打包 wheel;tool.pytest.ini_options 把 asyncio 模式与 pythonpath 固化,减少新人本地“pytest 找不到模块”的困惑。ruff 段落是风格与常见 bug 规则的底线,建议与 CI 共享。

再看 settings.pyenv_prefix 让所有环境变量有统一前缀,避免与操作系统或其他服务撞名;env_file 让本地开发可用 .env,但生产仍应以注入环境变量为主;extra="ignore" 提高在云平台部署时的兼容性;get_settings 使用 lru_cache 避免重复解析,但要注意测试时如需变更配置应使用工厂注入或清缓存策略(后续模块会展开)。字段默认值给出“本地可跑”的最低配置,不要把敏感默认值写进仓库。

routes_health.py 保持极薄:只负责 HTTP 语义与返回结构,不做业务判断。后续如果你把健康检查升级为 readiness,应在这里调用端口(由应用层或基础设施实现具体探测),而不是把 SQL 语句写进路由函数。api.py 的职责是把多个 Router 组合起来,让 main.py 保持干净;当模块变多时,你会感谢这个聚合点存在。

main.py 的应用工厂 是关键:create_app 接收可选 Settings,为测试打开口子;lifespan 预留资源初始化与释放;include_router 统一挂载 API。模块末尾 app = create_app() 是为了兼容 uvicorn codesentinel.main:app 的惯用法。若你担心重复创建 settings,记住测试路径应显式传参,生产路径走默认缓存即可。

tests/test_health.py 示范了“如何不用启动外部依赖就验证装配”:本地构造 Settings,调用 create_app,再用 TestClient 发请求。未来你可以用同样模式替换数据库为内存 fake、替换 LLM 为 stub,保持测试快速。失败时优先怀疑:路由前缀、include_router 顺序、或 lifespan 中的异常。

Dockerfile 分层意图是:先复制依赖描述文件并安装,再复制源码,利用缓存;EXPOSE 只是文档性提示,真正端口映射在 compose;CMD 使用 uv run 与本地一致。docker-compose.yml 把开发体验固化:挂载源码实现 reload、环境变量与本地对齐、命令显式写出 host/port。若你在 Windows 遇到权限或路径问题,优先检查卷挂载目录是否与容器内工作目录一致。

把以上走读串起来,你应该能回答三个问题:我为什么能在测试里替换配置?我为什么敢先不加数据库?我为什么要把路由写得这么薄?答得上,模块一的工程骨架才算真正学会。

目录结构

codesentinel/
├── src/
│   └── codesentinel/
│       ├── __init__.py
│       ├── main.py                 # ASGI 入口:装配 settings 并启动工厂
│       ├── config/
│       │   ├── __init__.py
│       │   └── settings.py         # pydantic-settings
│       ├── domain/
│       │   └── __init__.py
│       ├── application/
│       │   └── __init__.py
│       ├── infrastructure/
│       │   └── __init__.py
│       └── presentation/
│           ├── __init__.py
│           ├── api.py              # APIRouter 聚合
│           └── routes_health.py    # /health
├── tests/
│   ├── __init__.py
│   └── test_health.py
├── pyproject.toml
├── Dockerfile
├── docker-compose.yml
└── README.md(可选,本讲正文已含运行说明)

说明:把可导入包放在 src/codesentinel 下,可避免“意外导入仓库根目录同名模块”的经典坑,也与 pytest 的 pythonpath 配置天然契合。

pyproject.toml(完整依赖与工具配置)

[project]
name = "codesentinel"
version = "0.1.0"
description = "AI-driven code review and architecture governance platform"
readme = "README.md"
requires-python = ">=3.11"
dependencies = [
  "fastapi>=0.115.0",
  "uvicorn[standard]>=0.30.0",
  "pydantic>=2.9.0",
  "pydantic-settings>=2.6.0",
  "langchain>=0.3.0",
  "langchain-core>=0.3.0",
  "sqlalchemy>=2.0.0",
  "chromadb>=0.5.0",
  "httpx>=0.27.0",
]

[project.optional-dependencies]
dev = [
  "pytest>=8.3.0",
  "pytest-asyncio>=0.24.0",
  "ruff>=0.7.0",
]

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/codesentinel"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
pythonpath = ["src"]

[tool.ruff]
line-length = 100
target-version = "py311"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]

src/codesentinel/config/settings.py

from functools import lru_cache

from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="CODESENTINEL_",
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    app_name: str = Field(default="CodeSentinel")
    env: str = Field(default="dev", description="运行环境:dev/staging/prod")
    debug: bool = Field(default=False)
    host: str = Field(default="0.0.0.0")
    port: int = Field(default=8000)
    log_level: str = Field(default="info")


@lru_cache
def get_settings() -> Settings:
    return Settings()

补充走读(settings)app_name 会映射到 OpenAPI 文档标题与部分日志标识;env 建议只用于分支策略(例如决定是否启用调试路由),不要把它当作权限判断的唯一依据;debug 直连 FastAPI 的调试特性,staging/prod 通常应为 falsehost/port 在容器里常见组合是 0.0.0.0:8000,但生产入口往往还有反向代理与 TLS 终止;log_level 建议与 uvicorn--log-level 保持一致,否则会出现“应用日志很吵但访问日志很安静”的错位。若你后续引入按租户切换模型供应商,不要把租户密钥塞进 settings 的全局字段,而应走请求级上下文或密钥服务。

补充走读(包初始化文件)config/__init__.py 里导出 get_settings 是为了让表现层与其他层在少数场景下显式获取配置,但不要把“到处调用 get_settings()”当作依赖注入的替代品;长期仍推荐在组装点注入。domain/application/infrastructure 的 docstring 不是装饰,而是给新同学的方向牌:看到 import 出现在错误层级时,立刻警觉。__version__ 便于在 /health 或未来 /version 暴露构建信息,支撑排障。

src/codesentinel/presentation/routes_health.py

from datetime import datetime, timezone
from typing import Any

from fastapi import APIRouter

router = APIRouter(tags=["health"])


@router.get("/health")
def health() -> dict[str, Any]:
    return {
        "status": "ok",
        "service": "codesentinel",
        "ts": datetime.now(timezone.utc).isoformat(),
    }

src/codesentinel/presentation/api.py

from fastapi import APIRouter

from codesentinel.presentation.routes_health import router as health_router

api_router = APIRouter()
api_router.include_router(health_router)

补充走读(路由聚合)api.py 的价值在于把“有哪些路由集合”集中管理。后续你会增加 routes_review.pyroutes_rules.py 等文件,如果每个都在 main.pyinclude_router,很快会失去全局视野。另一个细节是前缀:APIRouter(prefix="/api/v1") 这类策略应在聚合层统一决定,避免每个路由文件各自为政导致 OpenAPI 路径混乱。tags 用于文档分组,建议与模块边界一致,方便前端与测试人员检索。

src/codesentinel/__init__.py

__all__ = ["__version__"]

__version__ = "0.1.0"

src/codesentinel/presentation/__init__.py

"""表现层:路由、依赖注入装配(随课程推进而增长)。"""

其他 __init__.py(domain/application/infrastructure/config)

# src/codesentinel/domain/__init__.py
"""领域层占位:实体、值对象、领域事件将在后续讲次实现。"""

# src/codesentinel/application/__init__.py
"""应用层占位:用例服务、端口接口将在后续讲次实现。"""

# src/codesentinel/infrastructure/__init__.py
"""基础设施层占位:数据库、向量库、外部 API 适配器将实现于此。"""

# src/codesentinel/config/__init__.py
from codesentinel.config.settings import Settings, get_settings

__all__ = ["Settings", "get_settings"]

应用工厂 src/codesentinel/main.py

from contextlib import asynccontextmanager
from typing import AsyncIterator

from fastapi import FastAPI

from codesentinel.config.settings import Settings, get_settings
from codesentinel.presentation.api import api_router


def create_app(settings: Settings | None = None) -> FastAPI:
    settings = settings or get_settings()

    @asynccontextmanager
    async def lifespan(_: FastAPI) -> AsyncIterator[None]:
        # 后续在此初始化数据库连接池、向量客户端、LangChain 缓存等
        yield

    app = FastAPI(
        title=settings.app_name,
        debug=settings.debug,
        lifespan=lifespan,
    )
    app.include_router(api_router)
    return app


app = create_app()

补充走读(应用工厂与 lifespan)lifespan 是资源生命周期的“正统入口”。当你加入数据库时,典型模式是在 yield 之前创建连接池,在 yield 之后关闭。不要把连接池绑在全局变量上,否则测试之间会互相污染。create_appsettings 参数让你可以在测试注入“假配置”,而不触碰环境变量;这与 get_settings() 的缓存策略互补:生产默认缓存,测试走显式对象。模块级 app 与工厂并存,是 FastAPI 生态的常见折中:既满足 uvicorn module:app,又满足 TestClient(create_app(...))

ASGI 启动说明

uvicorn 默认查找 app 对象,因此可用:

uv run uvicorn codesentinel.main:app --host 0.0.0.0 --port 8000 --reload

tests/test_health.py

from fastapi.testclient import TestClient

from codesentinel.config.settings import Settings
from codesentinel.main import create_app


def test_health_ok():
    settings = Settings(app_name="CodeSentinel-Test", env="test", debug=True)
    app = create_app(settings)
    client = TestClient(app)
    resp = client.get("/health")
    assert resp.status_code == 200
    body = resp.json()
    assert body["status"] == "ok"
    assert body["service"] == "codesentinel"
    assert "ts" in body

补充走读(测试与容器)TestClient 会在同进程里调用 ASGI 应用,适合作为快速回归;但它不是真实网络栈,某些与 HTTP/2、TLS、代理相关的问题仍需要更高层测试补齐。测试里显式构造 Settings 的关键意义在于:你可以把 app_name 改成带标识的值,避免与生产日志混淆。Docker 构建阶段复制 tests 目录,是为了让镜像内也能运行 pytest(在 CI 很有用);开发 compose 挂载 tests 则让你改测试即生效。uv sync --frozen 强调可复现;一旦团队成熟,应把失败从“回退到非 frozen”改为“修复 lock”,否则复现性会被悄悄破坏。compose 的 command 多行写法注意 YAML 缩进,避免把参数拆坏导致容器启动失败。

Dockerfile

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1 \
    UV_LINK_MODE=copy

WORKDIR /app

RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
    && rm -rf /var/lib/apt/lists/*

RUN curl -LsSf https://astral.sh/uv/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"

COPY pyproject.toml /app/pyproject.toml
COPY src /app/src
COPY tests /app/tests

RUN uv sync --frozen || uv sync

EXPOSE 8000

CMD ["uv", "run", "uvicorn", "codesentinel.main:app", "--host", "0.0.0.0", "--port", "8000"]

若你尚未生成 lock 文件,首次构建时 uv sync --frozen 可能失败,Dockerfile 中已用 || uv sync 兼容;团队化后建议提交 uv.lock 并只使用 uv sync --frozen

docker-compose.yml

services:
  api:
    build: .
    ports:
      - "8000:8000"
    environment:
      CODESENTINEL_ENV: dev
      CODESENTINEL_DEBUG: "true"
      CODESENTINEL_LOG_LEVEL: debug
    volumes:
      - ./src:/app/src
      - ./tests:/app/tests
    command: >
      uv run uvicorn codesentinel.main:app
      --host 0.0.0.0
      --port 8000
      --reload

本地 .env 示例(不要提交密钥)

CODESENTINEL_APP_NAME=CodeSentinel
CODESENTINEL_ENV=dev
CODESENTINEL_DEBUG=true
CODESENTINEL_HOST=0.0.0.0
CODESENTINEL_PORT=8000
CODESENTINEL_LOG_LEVEL=info

补充走读(.env 与本地机密).env 适合开发者个人机器,不建议在团队共享聊天里传递;更推荐每人本地私有配置,并配合 .gitignore 固化忽略规则。若你需要演示环境,使用单独的 .env.demo 并明确“无密钥”。当 CodeSentinel 接入真实模型与 Git 凭证后,要把“哪些变量属于 Secret、哪些属于 Config”分开管理,避免把 Secret 写进 compose 明文(开发环境可用,但要限制传播范围)。这一条在未来接入企业 SSO 与审计时会被反复验证。


生产环境实战:从“能跑”到“像线上”

开发环境的成功标准,是 新同事 10 分钟跑起来;生产环境的标准,是 可观测、可回滚、可扩容。本讲虽未接入完整观测链路,但建议你在启动期就养成三个习惯:

  1. 配置分层.env 仅供本地;线上使用编排系统注入环境变量(K8s Secret、云平台参数),避免把敏感信息 bake 进镜像。
  2. 健康检查语义/health 未来可拆为 liveness(进程活着)与 readiness(依赖可用)。当接入数据库与向量库后,readiness 应真实探测连接,而不是永远返回 200。
  3. 镜像分层缓存:Dockerfile 中先复制 pyproject.tomluv sync,后复制源码,能最大化利用构建缓存;CI 中配合 registry cache 进一步提速。

下面用一张 生产演进 示意图总结本讲底座在后续模块中的落点(LangChain、Chroma、SQLAlchemy 将在基础设施层实现适配器,而不是散落在路由函数里)。

flowchart TB
  subgraph Now["本讲交付"]
    A1["FastAPI 工厂"]
    A2["settings"]
    A3["health"]
  end

  subgraph Next["后续模块典型增量"]
    B1["infrastructure/db"]
    B2["infrastructure/llm"]
    B3["infrastructure/vector"]
    B4["application/use_cases"]
    B5["domain/model"]
  end

  A1 --> B4
  A2 --> B1
  A2 --> B2
  A2 --> B3
  A3 --> B1
  B5 --> B4
  B4 --> A1

深度延展:工程化启动的Checklist、常见坑与团队协作策略

这一节是把“能跑”推进到“能长期维护”的实战笔记。你可以在评审新人提交的启动方案时逐条对照使用,也可以把它改写成团队内部的 Onboarding 清单。

第一,包布局与可导入性。把业务代码放在 src/codesentinel 下面,并不是为了好看,而是为了修复一个长期存在的 Python 生态问题:当你在仓库根目录执行脚本或安装可编辑包时,如果项目里没有 src 隔离,容易出现“同名模块遮蔽”“本地改动不生效但运行却通过”的诡异状态。团队应在文档里明确:唯一的导入入口是安装后的包名 codesentinel,避免把仓库根目录当作 PYTHONPATH 使用。对于 Windows 开发者,还应注意大小写不敏感文件系统偶尔会掩盖导入错误,因此 CI 最好在 Linux 上跑一次最小编译与测试,保证跨平台一致性不被“本机侥幸”欺骗。

第二,依赖管理策略:为什么推荐 uvuv 在解析速度、锁定依赖、复现构建方面显著降低摩擦。CodeSentinel 后续会引入大量二进制依赖与原生扩展(向量库、HTTP 客户端等),锁定文件的价值会随着时间指数级上升。建议团队从第一天就养成“提交 lock 文件或至少在 CI 强制可复现安装”的习惯,避免“我本地能装、流水线不能装”的经典时间浪费。若你暂时不使用 lock,也要在评审里明确风险承担者,否则架构决策会悄悄变成个人习惯。

第三,FastAPI 应用工厂与测试双入口。模块级 app = create_app() 方便 uvicorn codesentinel.main:app 这种标准启动方式,而测试应显式调用 create_app(settings=...)。这样做的原因是:测试要控制配置、替换依赖、禁用真实网络。你们可以在后续模块引入依赖覆盖时,把 get_settings 通过 app.dependency_overrides 或工厂参数统一处理,避免在测试里改全局环境变量导致并发测试互相污染。对 AI 辅助编码而言,“双入口”也能减少模型把副作用写进 import 阶段的概率。

第四,pydantic-settingsextra="ignore"。本讲示例里使用 extra="ignore",是为了让环境变量中夹杂编排平台注入的其他键时应用仍能启动。如果你们希望“未知配置键直接失败”,可以改为 forbid,但这在云平台环境常常不现实。折中方案是:业务域敏感配置严格校验,平台域变量允许忽略;同时在日志里输出“启动时生效的关键配置摘要”(注意不要打印密钥)。

第五,lifespan 钩子的资源节奏。现在 lifespan 只是 yield,未来你们会在这里建立数据库连接池、初始化 LangChain 的缓存后端、预热向量索引等。原则是:进来的资源必须能在 shutdown 阶段被关闭。对于 FastAPI,异步 lifespan 特别适合管理异步客户端(例如 httpx.AsyncClient)。不要在 import 阶段创建昂贵连接,那会让工具链的静态检查、OpenAPI 生成、测试收集都变得缓慢且不稳定。CodeSentinel 未来接入多模型供应商时,这一点会决定你是否能在不重启进程的情况下切换配置。

第六,健康检查的演进路线。现在的 /health 返回 ok 只是证明 ASGI 链路通。生产上建议拆 /live/readylive 只关心进程是否还能响应;ready 关心关键依赖是否可用。否则你会遇到 Kubernetes 把流量打给一个“活着但不可用”的 Pod,造成业务层面错误率突增。对于 CodeSentinel,后续 ready 很可能会检查数据库迁移版本、向量库集合是否存在、以及外部模型网关的连通性(但要小心:过于重的 readiness 探针可能拖垮弹性伸缩)。架构师要在 SLO 与探针成本之间做显式权衡,并把结论写进运行手册。

第七,Docker 开发挂载与热重载docker-compose 挂载 srctests 时,要确认容器内工作目录、文件权限与换行符不会破坏可执行脚本。Windows 下的路径与绑定挂载偶尔会带来性能问题,必要时可在 WSL2 中放置源码目录。开发镜像不要追求与生产镜像完全一致,但要保持 Python 版本关键系统依赖 一致,否则会出现“容器里能编、本机不能编”的错位。若团队混合 Mac 与 Windows,更建议把“唯一真值运行环境”先定义成 Linux 容器,减少扯皮。

第八,安全基线:即便现在没有密钥.env 不要提交到 git,应该用 .env.example 给出模板。未来引入 API Key、数据库密码、向量库凭证时,要明确:哪些进入 Secret Manager,哪些进入 CI 变量,哪些可以被开发者本地持有。镜像里不要 ENV 写入真实生产密钥。对 CodeSentinel 来说,LLM 调用的密钥泄露是高频事故源,因此从工程骨架阶段就要把“配置来自环境”刻进肌肉记忆。顺便提醒:日志里打印 Authorization 头或完整提示词在生产上通常属于高风险行为,后续要讲“可观测性”而不是“可复制隐私”。

第九,CI 最小闭环。建议在合并请求阶段运行:静态检查、pytest、以及 docker build 的缓存友好构建。你不需要一上来就上完整安全扫描,但至少要防止明显的依赖漂移与格式化混乱。对 AI 团队而言,CI 也是对“模型生成代码”的第一道确定性约束:没有 CI,就没有架构边界;没有边界,智能只会加速混乱。

第十,文档化运行路径。本讲附录给出命令,但团队仍应在 README 中写明:Python 版本、uv 安装方式、常见报错(端口占用、WSL 权限、代理设置)。架构师的价值之一,是把“隐知识”变成“可复制流程”,否则每个人都会向同事重复解释为什么 uvicorn 找不到包。更进一步,你可以把“推荐的 IDE 设置(格式化、类型检查)”也写进去,让代码风格争议在入口就被消灭。

最后补一组排障思路:当 uvicorn 提示导入失败时,先判断是 PYTHONPATH 还是可编辑安装问题;当 Docker 启动成功但浏览器访问失败时,先核对端口映射与 host 绑定是否为 0.0.0.0;当 settings 读取不到环境变量时,先确认前缀 CODESENTINEL_ 是否与 compose 对齐。把这三类问题写成团队 Wiki,能显著降低新人上手成本。再补一条 AI 时代特供:当 Copilot 建议你“直接在 main.py 里启动全局客户端”时,把它当成触发器,回头检查你是否违背了工厂模式与 lifespan 原则。

案例推演:三种“看起来快、后面必痛”的启动方式

第一种,把所有依赖写进 requirements.txt 但不锁版本,然后让每个人自己 pip install。短期似乎更快,但 CodeSentinel 这种涉及向量库与二进制依赖的项目,会在两周内出现“同一代码不同环境”的漂移。你会把大量时间花在解释为什么同事电脑报错而你没事。第二种,把 FastAPI 应用写成全局单例并在 import 时连接外部服务。演示时很炫,一旦 CI 做静态检查或收集测试,就会频繁触发外部依赖,流水线不稳定,本地离线开发也困难。第三种,为了省事把业务逻辑写进路由函数,并提前把 LangChain 链塞进去。第一天需求能交,但第二周你要加审计、加重试、加权限时,会发现逻辑散落成一团,测试只能写端到端,反馈周期从秒级变分钟级。架构师的价值,是在第一天拒绝这三种捷径。

与 CodeSentinel 后续模块的接口契约(提前公示)

虽然本讲不写具体用例,但建议在团队内提前约定:presentation 路由函数的依赖注入入口、application 用例服务的构造参数、infrastructure 适配器的生命周期。未来接入向量检索时,不要把客户端藏在全局变量里;未来接入异步任务队列时,不要把任务发布散落在路由里。把契约写成一页纸,比开十次会更有效。

常见问答:新手最常问的五个问题

问:为什么不用 Django? 答:选型取决于团队与产品形态。CodeSentinel 以 API 与异步 I/O 为主,FastAPI 更轻;若你需要强管理后台与 ORM 生态,Django 也合理,但仍需分层。问:为什么 health 不做鉴权? 答:探针通常需要匿名访问,但要避免泄露敏感信息;若担心扫描,可在网关层限制来源 IP。问:开发环境要不要挂载整个仓库? 答:通常挂载源码目录即可;不要把 .venv 从宿主机挂进 Linux 容器,极易出现二进制不兼容。问:要不要上 poetry? 答:本讲用 uv;工具链选择以团队熟练度为准,关键是锁依赖与可复现。问:模型密钥放哪? 答:只放环境变量/Secret Manager,绝不写进代码与镜像层。

术语精读:本讲出现的工程关键词(面向团队统一口径)

应用工厂:用函数创建并配置应用实例,而不是在导入副作用里完成装配。ASGI:异步服务网关接口,连接 Web 服务器与应用代码。热重载:开发模式下监测文件变化并重启 worker,提高迭代效率,但生产禁用。** slim 镜像**:精简操作系统层的 Docker 基础镜像,减小攻击面与拉取时间。工作目录:容器内 WORKDIR,决定相对路径解析与默认命令执行位置。可编辑安装:以开发模式安装本地包,使 import 指向源码树。探针:编排系统用于判断容器是否存活/就绪的 HTTP/TCP 检查。环境变量前缀:避免与系统或其他服务冲突的配置命名空间(本讲 CODESENTINEL_)。把这些术语写进团队词典,可以减少会议里的“你说的健康检查是我说的 readiness 吗”之类摩擦。


本讲小结(思维导图)

mindmap
  root((第04讲小结))
    工程起点
      先确定性外壳
      后概率性智能
    结构
      src布局
      Clean Architecture骨架
    FastAPI
      应用工厂
      lifespan钩子
    配置
      pydantic-settings
      环境变量契约
    容器
      Dockerfile分层
      compose热重载
    验证
      pytest健康检查
      uvicorn本地启动

思考题

  1. 为什么把 create_app() 与模块级 app = create_app() 同时保留?测试代码应优先使用哪一种方式,为什么?
  2. 如果未来 /health 需要检查 Chroma 与 PostgreSQL,你应该把探测逻辑放在表现层、应用层还是基础设施层?依据是什么?
  3. pyproject.toml 里同时声明 langchainchromadb 可能带来体积与启动时间成本,你会如何在架构评审里权衡“先集成”与“延迟依赖”?

下一讲预告

下一讲我们将进入 Clean Architecture 速成:用依赖规则解释“为什么分层不是文件夹美学”,并把 依赖倒置 落到 Python 的 Protocol 与抽象基类上。你会看到 ReviewService 如何只依赖仓储协议,而不是 ORM 或向量库实现——这正是 CodeSentinel 在 AI 时代保持可控性的关键。


附录:一键命令备忘

# 安装依赖并运行(需在 codesentinel 项目根目录)
uv sync
uv run uvicorn codesentinel.main:app --host 0.0.0.0 --port 8000 --reload

# 测试
uv run pytest -q

# 容器
docker compose up --build