模块一-全景认知 | 第04讲:CodeSentinel 项目启动 - Python + FastAPI 工程环境与项目骨架搭建
开场:从“想法”到“可运行的骨架”
欢迎来到《AI 架构师与代码审核实战》模块一的第四讲。前三讲我们讨论了 AI 时代架构师的职责边界、技术栈全景,以及如何把大模型能力“产品化”为可治理的工程能力。从这一讲开始,贯穿全课的实战项目 CodeSentinel 将真正落地:我们要搭建一个 AI 驱动的代码审核与架构治理平台 的工程底座。
很多团队在“AI 项目”里犯的第一个错误,不是模型选错,而是 工程起点选错:一上来就把 LangChain、向量库、提示词堆进一个 main.py,几天后就陷入“改一处崩一片”的泥潭。架构师的职责之一,是在不确定性(模型的概率输出)之外,先铺好 确定性的工程外壳——清晰的分层、可测试的结构、可配置的环境、可复制的容器化开发体验。
本讲目标非常具体:你将得到一套 Clean Architecture 风格的目录骨架、一份 可直接 uv sync 的 pyproject.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_NAME、ENV、DEBUG、HOST、PORT、LOG_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. 测试先行:健康检查不是摆设,是契约
/health 与 tests/test_health.py 看起来微不足道,但它建立了两个关键契约:第一,路由注册路径正确;第二,应用工厂在测试里可注入配置。后续当你把鉴权、中间件、全局异常处理加进来时,这个测试会第一时间告诉你“装配是否仍然可控”。对 AI 团队尤其重要的是:模型生成的路由代码很容易漏注册或漏前缀,最小测试是成本最低的回归锚点。
8. 日志级别、结构化字段与可关联追踪
启动期就要约定日志格式:至少包含时间、级别、logger 名、消息,并预留 request_id 与 trace_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 路径、依赖方向检查与未来拆分一次到位。tests 与 pyproject.toml 中的 pythonpath = ["src"] 配合,保证测试能找到包。不要把测试写成依赖“当前工作目录碰巧正确”的脆弱脚本。
接着看 pyproject.toml:dependencies 列出运行时最小集合,其中 LangChain、SQLAlchemy、Chroma、httpx 是 CodeSentinel 后续能力的“预埋”,避免每讲都改依赖导致合并冲突。optional-dependencies.dev 把测试与静态检查工具隔离,生产镜像构建时可以选择不安装 dev,从而减小攻击面与镜像体积。hatchling 作为 build backend 负责打包 wheel;tool.pytest.ini_options 把 asyncio 模式与 pythonpath 固化,减少新人本地“pytest 找不到模块”的困惑。ruff 段落是风格与常见 bug 规则的底线,建议与 CI 共享。
再看 settings.py:env_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 通常应为 false;host/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.py、routes_rules.py 等文件,如果每个都在 main.py 里 include_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_app 的 settings 参数让你可以在测试注入“假配置”,而不触碰环境变量;这与 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 分钟跑起来;生产环境的标准,是 可观测、可回滚、可扩容。本讲虽未接入完整观测链路,但建议你在启动期就养成三个习惯:
- 配置分层:
.env仅供本地;线上使用编排系统注入环境变量(K8s Secret、云平台参数),避免把敏感信息 bake 进镜像。 - 健康检查语义:
/health未来可拆为 liveness(进程活着)与 readiness(依赖可用)。当接入数据库与向量库后,readiness 应真实探测连接,而不是永远返回 200。 - 镜像分层缓存:Dockerfile 中先复制
pyproject.toml再uv 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 上跑一次最小编译与测试,保证跨平台一致性不被“本机侥幸”欺骗。
第二,依赖管理策略:为什么推荐 uv。uv 在解析速度、锁定依赖、复现构建方面显著降低摩擦。CodeSentinel 后续会引入大量二进制依赖与原生扩展(向量库、HTTP 客户端等),锁定文件的价值会随着时间指数级上升。建议团队从第一天就养成“提交 lock 文件或至少在 CI 强制可复现安装”的习惯,避免“我本地能装、流水线不能装”的经典时间浪费。若你暂时不使用 lock,也要在评审里明确风险承担者,否则架构决策会悄悄变成个人习惯。
第三,FastAPI 应用工厂与测试双入口。模块级 app = create_app() 方便 uvicorn codesentinel.main:app 这种标准启动方式,而测试应显式调用 create_app(settings=...)。这样做的原因是:测试要控制配置、替换依赖、禁用真实网络。你们可以在后续模块引入依赖覆盖时,把 get_settings 通过 app.dependency_overrides 或工厂参数统一处理,避免在测试里改全局环境变量导致并发测试互相污染。对 AI 辅助编码而言,“双入口”也能减少模型把副作用写进 import 阶段的概率。
第四,pydantic-settings 与 extra="ignore"。本讲示例里使用 extra="ignore",是为了让环境变量中夹杂编排平台注入的其他键时应用仍能启动。如果你们希望“未知配置键直接失败”,可以改为 forbid,但这在云平台环境常常不现实。折中方案是:业务域敏感配置严格校验,平台域变量允许忽略;同时在日志里输出“启动时生效的关键配置摘要”(注意不要打印密钥)。
第五,lifespan 钩子的资源节奏。现在 lifespan 只是 yield,未来你们会在这里建立数据库连接池、初始化 LangChain 的缓存后端、预热向量索引等。原则是:进来的资源必须能在 shutdown 阶段被关闭。对于 FastAPI,异步 lifespan 特别适合管理异步客户端(例如 httpx.AsyncClient)。不要在 import 阶段创建昂贵连接,那会让工具链的静态检查、OpenAPI 生成、测试收集都变得缓慢且不稳定。CodeSentinel 未来接入多模型供应商时,这一点会决定你是否能在不重启进程的情况下切换配置。
第六,健康检查的演进路线。现在的 /health 返回 ok 只是证明 ASGI 链路通。生产上建议拆 /live 与 /ready:live 只关心进程是否还能响应;ready 关心关键依赖是否可用。否则你会遇到 Kubernetes 把流量打给一个“活着但不可用”的 Pod,造成业务层面错误率突增。对于 CodeSentinel,后续 ready 很可能会检查数据库迁移版本、向量库集合是否存在、以及外部模型网关的连通性(但要小心:过于重的 readiness 探针可能拖垮弹性伸缩)。架构师要在 SLO 与探针成本之间做显式权衡,并把结论写进运行手册。
第七,Docker 开发挂载与热重载。docker-compose 挂载 src 与 tests 时,要确认容器内工作目录、文件权限与换行符不会破坏可执行脚本。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本地启动
思考题
- 为什么把
create_app()与模块级app = create_app()同时保留?测试代码应优先使用哪一种方式,为什么? - 如果未来
/health需要检查 Chroma 与 PostgreSQL,你应该把探测逻辑放在表现层、应用层还是基础设施层?依据是什么? pyproject.toml里同时声明langchain与chromadb可能带来体积与启动时间成本,你会如何在架构评审里权衡“先集成”与“延迟依赖”?
下一讲预告
下一讲我们将进入 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