专栏:AI 全栈开发|05
|-------------------------------------------------------------------------------------------------|
| 技术栈定下来以后,真正打开 IDE 时你面对的不是架构图,而是一个空文件夹。这一篇不追求"生产级目录看起来多专业",而是从 5 个文件开始:先让每个东西有明确归属,再随着项目变复杂逐步扩展。 |
一、目录结构解决的不是"好不好看",而是以后去哪找代码
刚建项目时,所有文件放根目录也能跑。真正的问题通常在几周后才出现:API 路由和模型调用混在一起,迁移脚本不知道放哪,Docker 配置散落,谁都不敢删"final2.py"。
所以目录结构最实用的目标只有两个:
• 看到一个文件名,大致知道它应该属于哪类职责。
• 想修改一类功能时,知道主要应该去哪个目录找。
只要这两点做到了,目录就已经在帮你,而不是在给项目增加仪式感。
二、第一天不要先建 6 个空目录
图 1 项目目录应该跟着真实职责生长,不需要第一天把"目标态"全部建出来
按照前面专栏的路线,第一版只有 Frontend 和 Backend,那么目录完全可以先这样:
app/
├── README.md
├── .gitignore
├── .env.example
├── frontend/
└── backend/
这已经足够建立最重要的边界:浏览器侧代码进 frontend,可信服务端代码进 backend。
等真的有数据库迁移、部署配置、运维脚本和架构文档,再把 scripts、infra、docs 加进来。
三、为什么前端和后端建议分目录
因为它们本来就是两套运行环境:Frontend 依赖 Node / 浏览器生态,Backend 依赖 Python 和服务端库,构建、测试、环境变量也不一样。
分开以后,一个非常简单的规则就成立了:
frontend/ 不直接 import backend/ 的 Python 代码;backend/ 也不去读 frontend/src 里的业务实现。两边通过 HTTP / Streaming API 等契约通信。
如果以后想共享类型,也更推荐通过 OpenAPI 生成客户端、共享 Schema 包等明确机制,而不是相对路径跨目录乱 import。
四、Monorepo 是一种方便的选择,不是"唯一最优"
对一个中小团队、同一个产品、前后端经常一起改的项目,把 frontend 和 backend 放在同一个 Git 仓库里确实很方便:一个 PR 能同时改接口和页面,CI 也能一起跑。
但这不代表 Monorepo 对所有组织都是最优。大型组织、独立发布节奏、不同权限边界、已有多仓库平台,都可能有充分理由拆仓。
|---------------------------------------------------------------|
| 本专栏使用 Monorepo,是为了学习和项目演进更连贯。真正要记的是"职责边界",不是"所有 AI 项目必须一个仓库"。 |
五、项目长大以后,各目录分别应该管什么
图 2 好目录的核心是边界:不同职责各有归属,跨边界通过契约连接
frontend/:浏览器里运行的产品代码
页面路由、组件、Hooks、Streaming 客户端、上传 UI、前端状态都在这里。
frontend/
├── src/
│ ├── app/ # Next.js 页面 / 路由
│ ├── components/ # 可复用 UI
│ ├── hooks/ # useChat 等
│ └── lib/ # API client、stream parser
├── public/
├── package.json
└── tsconfig.json
backend/:可信服务端的业务代码
路由只负责 HTTP 边界,真正的模型调用、RAG、文件服务等放到 services;数据库访问集中到 repositories;配置和安全公共代码放 core。
backend/
├── app/
│ ├── main.py
│ ├── api/ # 路由层
│ ├── services/ # 业务 / AI 编排
│ ├── repositories/ # DB / Cache 数据访问
│ ├── schemas/ # 请求 / 响应模型
│ └── core/ # config / auth / logging
├── tests/
└── pyproject.toml
这里的分层也不是硬性教条。项目只有两三个接口时,services 和 repositories 可以很轻;不要为了目录完整,把一行函数拆成五层。
scripts/:有明确生命周期的"动作"
数据库初始化、一次性数据回填、导入测试数据、迁移辅助脚本等可以放在 scripts。
关键不是"脚本必须一次性",而是它们不是在线请求路径里的业务模块。能重复执行的运维脚本也完全可以放这里,只要命名清楚、行为可控。
infra/:部署与基础设施定义
Docker Compose、Terraform、Kubernetes manifests、反向代理配置等可以放 infra。
但 Dockerfile 放在哪里没有唯一答案。很多项目会把 frontend/Dockerfile、backend/Dockerfile 跟应用源码放在一起,因为构建上下文更直观;也有团队集中管理。选择一种规则并保持一致即可。
docs/:记录代码解释不了的"为什么"
README 解决"怎么跑";docs/ 更适合保存架构总览、复杂流程、API 约定和 ADR。
ADR 最有价值的不是记录"用了 PostgreSQL",而是记录"为什么当时选择 PostgreSQL、考虑过什么替代方案、什么时候重新评估"。
六、根目录应该放什么
根目录是整个仓库的入口,适合放跨前后端都需要知道的契约文件。
|----------------------------|-----------------------|-------------------|
| 文件 | 作用 | 注意 |
| README.md | 启动、测试、目录导航 | 新同事先看这里 |
| .gitignore | 忽略构建产物、虚拟环境、本地 Secret | 不要把 .env 提交 |
| .env.example | 示例配置 Key 与说明 | 只放占位值,不放真实 Secret |
| compose.yaml(按需) | 本地统一启动多个依赖服务 | 项目不需要容器时可暂时没有 |
| Makefile / task runner(按需) | 统一常用开发命令 | 不是必需品 |
`.env.example` 不是生产配置的"唯一真相"。它更像开发者能看到的配置说明和模板;真正的 staging / prod Secret 应由部署平台或 Secret Manager 管理。
七、dev / staging / prod:不要追求"环境长得一模一样" 
图 3 环境应该共享代码和契约,但基础设施规模与实现可以不同
原稿要求三套环境从第一天都用同一套 docker-compose,这个说法太死。更实际的原则是:
• 应用代码尽量同源,不通过改源码切换环境。
• 配置 Key 和接口契约尽量一致,Value 由环境注入。
• 数据库迁移流程、安全规则、关键依赖版本要可重复。
• dev 可以用本地依赖,prod 可以用托管数据库或 Kubernetes;基础设施不必逐字相同。
Staging 的目标是尽可能验证生产关键行为,但是否值得维护完整 staging,也取决于产品规模和团队成本。小项目未必第一天就需要三套完整环境。
八、一个更适合本专栏继续扩展的目录
到真正加入数据库、文件、Worker 和部署后,可以长成下面这样:
app/
├── README.md
├── .gitignore
├── .env.example
├── frontend/
│ ├── src/
│ │ ├── app/
│ │ ├── components/
│ │ ├── hooks/
│ │ └── lib/
│ └── package.json
├── backend/
│ ├── app/
│ │ ├── api/
│ │ ├── services/
│ │ ├── repositories/
│ │ ├── schemas/
│ │ └── core/
│ ├── tests/
│ └── pyproject.toml
├── worker/ # 出现独立长任务进程后再加
├── scripts/
├── infra/
└── docs/
这里特意没有单独建 docker/。Dockerfile 是否跟应用放一起、是否集中到 infra,属于团队约定,不值得在第 05 篇把它说成"正确答案"。
九、几个最容易把目录越设计越复杂的误区
• 误区 1:目录越多越专业。空目录只会增加认知负担,职责真的出现再建。
• 误区 2:所有后端代码都拆 Router / Service / Repository。分层是为了隔离复杂度,不是为了给三行 CRUD 增加模板。
• 误区 3:Monorepo 是所有团队的最优解。它适合本专栏和很多中小团队,但不是组织架构定律。
• 误区 4:dev、staging、prod 必须用完全相同的部署方案。应该一致的是行为和契约,不是机器数量或云产品。
• 误区 5:.env.example 管理全部环境变量值。它只能进仓库做模板;真实 Secret 不应该进入 Git。
• 误区 6:为了共享代码让前端直接 import 后端目录。跨运行时边界最好走 API / Schema 等明确契约。
十、目录什么时候应该重构
目录结构不需要追求"三年不动"。当下面情况开始频繁出现,就说明边界需要调整:
• 一个目录里出现几十个职责完全不同的文件,找代码越来越慢。
• 同一类逻辑在多个目录重复出现。
• 每次改一个模块都必须同时修改很多不相关目录。
• 部署、测试或权限边界已经跟当前目录划分不一致。
好的目录不是永远不变,而是变化有原因、有迁移路径,不是每周凭感觉换一次。
十一、这一篇只需要记住一个原则
目录结构的价值不是提前预测三年后的所有文件,而是让"现在已经存在的职责"有稳定边界,并且给下一阶段的复杂度留出自然生长的位置。
十二、下一篇
下一篇 06《Browser 到 Server,一次请求到底发生了什么?》会从文件结构进入运行时:用户点下发送以后,请求怎样从浏览器到 FastAPI,再怎样把响应送回来。到那时,frontend 和 backend 之间那条"HTTP 线"会真正变得透明。