标准AI SaaS项目目录结构
好的项目结构,不仅让人看得懂——它让 AI 协作工具也能更好地理解你的代码。当 Claude Code、Cursor 或 Copilot 打开你的仓库时,清晰的目录分层会直接影响它们生成代码的质量。反过来,混乱的项目结构会让 AI 建议的文件归属频繁出错,增加你手动修正的成本。
对于 AI SaaS 产品来说,目录结构还多了一层考量:Prompt 模板、Agent 定义、向量存储配置、模型调用逻辑——这些 AI 特有的代码如果随意散落,会让项目在几个月后变成难以维护的「大杂烩」。本文将以 Next.js App Router 为基础,系统讲解标准 AI SaaS 项目的目录结构应该怎样组织。
Next.js App Router 的标准目录结构
Next.js App Router 有一套约定优于配置的目录规范。官方文档明确列出了顶层文件夹和路由文件的用途,开发者在此之上可以自由扩展。
以下是 Next.js App Router 项目的最小标准结构:
my-nextjs-app/
├── app/ # App Router 路由目录
│ ├── layout.tsx # 根布局(所有页面共享)
│ ├── page.tsx # 首页(/)
│ ├── loading.tsx # 全局加载态
│ ├── error.tsx # 全局错误边界
│ ├── not-found.tsx # 404 页面
│ ├── globals.css # 全局样式
│ └── (routes)/ # 路由分组(不影响 URL)
│ ├── dashboard/
│ │ ├── layout.tsx # 仪表盘布局
│ │ └── page.tsx # 仪表盘页面
│ └── settings/
│ └── page.tsx # 设置页面
├── public/ # 静态资源(直接通过 URL 访问)
│ ├── favicon.ico
│ └── images/
├── src/ # (可选)应用源码根目录
├── next.config.ts # Next.js 配置
├── tsconfig.json # TypeScript 配置
├── package.json # 依赖与脚本
├── .env.local # 本地环境变量
└── .env.production # 生产环境变量这个结构中有几个 Next.js 特有的概念需要理解:
app/ 目录:App Router 的核心。每个文件夹对应 URL 的一个路径段,文件夹里放 page.tsx 就暴露为公开路由。layout.tsx 是该路由段的共享布局,loading.tsx 是 Suspense 加载态,error.tsx 是错误边界。
Route Groups(路由分组):用括号包裹的文件夹名,例如 (routes),不会出现在 URL 中。你可以用它把营销页面和后台页面分开,让它们共享同一路径层级但拥有不同的布局。
Private Folders(私有文件夹):以下划线开头的文件夹名,例如 _components,Next.js 路由系统会忽略它。适合存放当前路由段的内部组件和工具函数。
src/ 目录:可选的应用源码根目录。如果你选择使用 src/,那么 app/ 和其他业务代码都会放在 src/ 下面,与根目录的配置文件分离。
各目录的作用详解
在一个生产级的 AI SaaS 项目中,除了 Next.js 约定的目录之外,你还需要一系列业务目录来组织代码。以下是完整目录清单及其职责:
my-ai-saas/
├── app/ # 路由层:页面定义和数据获取
├── components/ # 组件层:可复用的 UI 组件
│ ├── ui/ # 基础 UI 组件(Button、Input、Modal)
│ └── shared/ # 业务共享组件(Header、Sidebar、DataTable)
├── lib/ # 工具层:通用工具函数和客户端
│ ├── utils.ts # 纯函数工具
│ ├── db.ts # 数据库连接
│ └── auth.ts # 认证工具
├── services/ # 服务层:业务逻辑封装
│ ├── ai/ # AI 相关服务
│ ├── billing/ # 计费服务
│ └── user/ # 用户服务
├── types/ # 类型层:全局 TypeScript 类型
├── config/ # 配置层:环境变量和应用配置
├── hooks/ # Hooks 层:自定义 React Hooks
├── constants/ # 常量层:枚举值、固定配置
├── styles/ # 样式层:全局样式和主题
└── public/ # 静态资源各目录职责对照表
| 目录 | 职责 | 典型文件 | 放置原则 |
|---|---|---|---|
app/ | 路由定义、页面数据获取、Server Components | page.tsx、layout.tsx、route.ts | 只做路由装配,复杂 UI 抽到 _components/ |
components/ui/ | 无业务逻辑的基础 UI 组件 | Button.tsx、Modal.tsx、Input.tsx | 纯展示、接受 props、不依赖业务数据 |
components/shared/ | 跨页面共享的业务组件 | Header.tsx、DataTable.tsx | 可能包含业务逻辑,被多个页面引用 |
lib/ | 通用工具函数、第三方客户端封装 | utils.ts、db.ts、redis.ts | 纯函数优先,可独立测试 |
services/ | 业务逻辑的服务层封装 | ai/completion.ts、billing/stripe.ts | 一个服务对应一个业务域 |
types/ | 全局 TypeScript 类型和接口 | user.ts、ai.ts、api.ts | 被多个模块引用的共享类型 |
config/ | 环境变量读取和应用配置 | env.ts、site.ts、ai-models.ts | 集中管理配置,禁止在业务代码直接读 process.env |
hooks/ | 自定义 React Hooks | use-auth.ts、use-ai-stream.ts | 跨组件共享的状态逻辑 |
constants/ | 枚举、固定值、默认配置 | plans.ts、model-prices.ts | 避免在代码中散落魔法数字 |
为什么需要 services/ 层?
一个常见的错误是直接在 app/ 的 Server Action 或 lib/ 中写业务逻辑。当项目规模增长后,你会发现同一个用户创建逻辑在注册、邀请、OAuth 回调中重复出现。
services/ 层将业务逻辑从路由中抽离,使得:
- 路由文件只负责 HTTP 请求处理、参数校验和响应格式化
- 服务函数可以被多个路由复用
- 服务函数有独立的输入输出类型,易于测试
// ❌ 在路由中直接写业务逻辑
app/api/users/route.ts:
const user = await db.user.create({ ... })
await stripe.customers.create({ ... })
await sendWelcomeEmail(user.email)
// ✅ 服务层封装
services/user/create-user.ts:
export async function createUser(input: CreateUserInput) { ... }
app/api/users/route.ts:
const user = await createUser(requestBody)AI SaaS 特有的目录
AI SaaS 和普通 SaaS 最大的区别在于:你需要管理 Prompt 模板、Agent 定义、向量存储、模型调用链路,以及可能的多步骤工作流。这些代码如果不单独组织,很快就会和业务逻辑纠缠在一起。
AI 特有目录结构
my-ai-saas/
├── ai/ # AI 模块根目录
│ ├── providers/ # 模型提供商适配层
│ │ ├── openai.ts # OpenAI 客户端封装
│ │ ├── anthropic.ts # Anthropic 客户端封装
│ │ └── types.ts # 提供商统一接口
│ ├── prompts/ # Prompt 模板管理
│ │ ├── _registry.ts # Prompt 注册表
│ │ ├── chat/ # 按功能分组
│ │ │ ├── system.ts # 系统 Prompt
│ │ │ └── user-context.ts # 用户上下文 Prompt
│ │ └── generation/
│ │ ├── outline.ts # 大纲生成 Prompt
│ │ └── draft.ts # 草稿生成 Prompt
│ ├── agents/ # Agent 定义
│ │ ├── types.ts # Agent 接口定义
│ │ ├── writer-agent.ts # 写作 Agent
│ │ ├── editor-agent.ts # 编辑 Agent
│ │ └── supervisor-agent.ts # 监督 Agent
│ ├── vectors/ # 向量存储相关
│ │ ├── embeddings.ts # 文本向量化
│ │ ├── store.ts # 向量数据库客户端
│ │ └── retrieval.ts # 检索逻辑
│ ├── tools/ # Agent 可调用的工具
│ │ ├── web-search.ts # 网络搜索工具
│ │ ├── code-executor.ts # 代码执行工具
│ │ └── types.ts # 工具接口定义
│ ├── workflows/ # 多步骤工作流编排
│ │ ├── content-pipeline.ts # 内容生产流水线
│ │ └── research-flow.ts # 研究分析流程
│ └── middleware/ # AI 调用中间件
│ ├── rate-limit.ts # 速率限制
│ ├── token-counter.ts # Token 计数
│ └── retry.ts # 重试策略
├── services/ # 业务服务层
│ └── ai/ # AI 业务逻辑(调用 ai/ 模块)
│ ├── chat.ts # 对话业务
│ └── generation.ts # 生成业务AI 特有目录职责对照表
| 目录 | 职责 | 设计原则 | 典型技术栈 |
|---|---|---|---|
ai/providers/ | 屏蔽不同模型提供商的 API 差异 | 统一接口,切换提供商时只改这一个目录 | openai、@anthropic-ai/sdk |
ai/prompts/ | Prompt 模板集中管理 | Prompt 是内容不是代码,应该能独立迭代和版本管理 | 纯 TypeScript 模板字符串或 .md 文件 |
ai/agents/ | 定义 AI Agent 角色和能力 | 每个 Agent 是一个角色定义,包含 system prompt + 工具列表 | LangChain Agents、自定义 Agent 框架 |
ai/vectors/ | 向量存储和检索 | 向量化逻辑与业务逻辑解耦,更换向量数据库只改这个目录 | Pinecone、Weaviate、pgvector |
ai/tools/ | Agent 可调用的外部工具 | 每个工具是一个独立函数,有明确的输入输出 schema | 自定义函数、MCP 工具 |
ai/workflows/ | 多步骤 AI 流程编排 | 工作流是 Agent 之间的协调层,不包含具体业务逻辑 | LangGraph、Inngest、自定义状态机 |
ai/middleware/ | AI 调用的横切关注点 | Token 计数、速率限制、重试、日志——这些不应该散落在每个调用点 | 自定义中间件链 |
Prompt 为什么要单独管理?
在普通 SaaS 项目中,你可能不觉得 Prompt 需要专门管理。但在 AI 产品中,Prompt 的迭代频率远高于普通代码。一个典型的 AI SaaS 产品,Prompt 可能每周都在调整。
如果 Prompt 硬编码在业务函数里:
// ❌ Prompt 散落在业务代码中
export async function generateOutline(topic: string) {
const response = await openai.chat.completions.create({
messages: [
{ role: "system", content: "你是一个专业的内容策划..." },
{ role: "user", content: `请为「${topic}」生成大纲...` }
]
})
// ...
}当你需要 A/B 测试不同 Prompt、支持多语言 Prompt、或者根据用户反馈调整措辞时,你会发现自己要在几十个文件中搜索和修改字符串。
独立管理 Prompt 之后:
// ✅ Prompt 独立文件
// ai/prompts/generation/outline.ts
export const OUTLINE_SYSTEM_PROMPT = `你是一个专业的内容策划...`
export const OUTLINE_USER_TEMPLATE = (topic: string) =>
`请为「${topic}」生成大纲...`
// services/ai/generation.ts
import { OUTLINE_SYSTEM_PROMPT, OUTLINE_USER_TEMPLATE } from '@/ai/prompts/generation/outline'
export async function generateOutline(topic: string) {
const response = await ai.chat({
system: OUTLINE_SYSTEM_PROMPT,
user: OUTLINE_USER_TEMPLATE(topic)
})
}这样做的好处:Prompt 修改不需要动业务代码;可以在 ai/prompts/_registry.ts 中统一管理版本;方便做 Prompt 的 A/B 测试和灰度发布。
推荐的目录组织原则
原则一:按功能域划分,而非按技术类型
// ❌ 按技术类型划分(容易失控)
components/ → 所有组件混在一起
hooks/ → 所有 hooks 混在一起
utils/ → 所有工具函数混在一起
// ✅ 按功能域划分(高内聚低耦合)
features/
├── auth/ → 认证相关组件、hooks、逻辑
├── billing/ → 计费相关组件、hooks、逻辑
└── ai-chat/ → AI 对话相关组件、hooks、逻辑按技术类型划分在初期看起来很整齐,但随着项目增长,每个目录都会膨胀到难以管理。按功能域划分让相关代码聚在一起,新增功能时只需要在一个目录下操作,删除功能时可以直接删掉整个目录。
原则二:路由文件只做装配
app/ 目录下的文件应该尽量薄——只负责:
- 接收请求参数
- 调用
services/或lib/中的业务逻辑 - 返回响应或渲染页面
// app/(dashboard)/chat/page.tsx
import { ChatView } from './_components/chat-view'
import { getUser } from '@/lib/auth'
import { redirect } from 'next/navigation'
export default async function ChatPage() {
const user = await getUser()
if (!user) redirect('/login')
return <ChatView userId={user.id} />
}页面组件的复杂度应该通过 _components/ 子目录来控制——把具体的 UI 组件拆到当前路由段下的私有目录中。
原则三:AI 代码与业务代码分离
ai/ 目录只关心 AI 能力——如何调用模型、如何管理 Prompt、如何做向量检索。它不应该知道「用户在哪个页面」或「这笔订单多少钱」。
services/ai/ 是两者的桥梁——它调用 ai/ 的能力,结合业务数据(用户信息、订单状态等),完成具体的业务需求。
// ai/providers/openai.ts
// 只知道如何调用 OpenAI API,不知道业务逻辑
export async function chatCompletion(params: ChatParams) { ... }
// services/ai/generation.ts
// 结合业务逻辑调用 AI 能力
export async function generateContent(userId: string, input: ContentInput) {
const user = await getUser(userId)
const plan = await getUserPlan(userId)
const prompt = buildPrompt(input, user.tone)
const result = await chatCompletion({ model: plan.model, prompt })
await saveGeneration(userId, result)
return result
}原则四:类型跟随功能,全局类型集中管理
// ai/agents/types.ts → AI Agent 相关的类型
// services/billing/types.ts → 计费相关的类型
// types/ → 跨多个功能域共享的全局类型如果某个类型只在一个功能域内使用,就放在那个功能域下面。如果被多个功能域引用,就提到 types/ 中。
不同规模项目的结构差异
一个 MVP 阶段的项目和一个成熟产品的目录结构应该有显著差异。过度设计 MVP 的目录结构会拖慢开发速度,而用 MVP 的结构支撑成熟产品则会导致代码腐化。
MVP 阶段(0-3 个月)
MVP 的核心目标是快速验证,目录结构应该尽量简单:
my-ai-mvp/
├── app/ # 路由 + 页面组件
│ ├── (marketing)/ # 营销页面
│ │ ├── page.tsx # 首页
│ │ └── pricing/page.tsx # 定价页
│ ├── (app)/ # 应用页面
│ │ ├── dashboard/page.tsx
│ │ └── chat/page.tsx
│ ├── api/ # API 路由
│ │ └── chat/route.ts
│ └── layout.tsx
├── components/ # 所有组件(暂不分类)
│ ├── ui/ # 基础 UI
│ └── chat/ # 对话相关组件
├── lib/ # 工具函数 + 数据库 + AI 调用
│ ├── db.ts
│ ├── ai.ts # AI 调用(直接封装,不分目录)
│ └── auth.ts
├── types/ # 全局类型
├── config/ # 配置
└── public/ # 静态资源MVP 阶段的 lib/ai.ts 可能只有 100 行左右,直接封装了模型调用和 Prompt。这时候不需要拆出 ai/ 目录。
成长阶段(3-12 个月)
产品开始有多个 AI 功能、多步工作流、向量检索需求。此时需要从 MVP 结构演进:
my-ai-saas/
├── app/ # 路由层(保持简洁)
├── components/ # 组件层
│ ├── ui/
│ ├── shared/
│ └── features/ # 按功能域组织的组件
│ ├── chat/
│ ├── generation/
│ └── billing/
├── services/ # 服务层(从 lib/ 中抽离)
│ ├── ai/
│ ├── billing/
│ └── user/
├── ai/ # AI 能力层(从 lib/ai.ts 拆出)
│ ├── providers/
│ ├── prompts/
│ ├── agents/
│ └── vectors/
├── lib/ # 通用工具
├── types/
├── config/
└── public/成熟阶段(12 个月以上)
产品可能引入 Monorepo、独立的 Prompt 管理平台、向量数据库集群等:
my-ai-platform/ # Monorepo 根目录
├── apps/
│ ├── web/ # 前端 Web 应用
│ ├── api/ # 独立 API 服务
│ └── admin/ # 管理后台
├── packages/
│ ├── ui/ # 共享 UI 组件库
│ ├── ai-core/ # AI 核心能力(providers、prompts、agents)
│ ├── db/ # 数据库 schema 和迁移
│ ├── prompts/ # Prompt 模板包(独立版本管理)
│ ├── vectors/ # 向量存储包
│ ├── config/ # 共享配置
│ └── types/ # 共享类型
├── tools/ # 开发工具
│ ├── prompt-editor/ # Prompt 编辑工具
│ └── eval-runner/ # AI 评测运行器
├── turbo.json
└── pnpm-workspace.yaml不同阶段结构对比
| 维度 | MVP 阶段 | 成长阶段 | 成熟阶段 |
|---|---|---|---|
| 目录策略 | 最小化,少拆目录 | 按职责拆分目录 | Monorepo 多包管理 |
| AI 代码位置 | lib/ai.ts 单文件 | ai/ 独立目录 | packages/ai-core/ 独立包 |
| Prompt 管理 | 内联在 ai.ts 中 | ai/prompts/ 目录 | packages/prompts/ 独立包 |
| 向量存储 | 可能还没有 | ai/vectors/ 目录 | packages/vectors/ 独立包 |
| 服务层 | 路由中直接写 | services/ 按域拆分 | API 服务独立部署 |
| 组件组织 | components/ 扁平 | components/features/ 按域 | packages/ui/ 共享包 |
| 配置管理 | .env + 少量 config | config/ 集中管理 | packages/config/ + 环境变量校验 |
| 典型代码量 | < 20,000 行 | 20,000 - 100,000 行 | > 100,000 行 |
完整案例
案例一:AI 写作 SaaS 项目结构
一个面向海外市场的 AI 写作助手,支持文章生成、改写、翻译,使用 Next.js App Router + Prisma + OpenAI。
ai-writer/
├── app/
│ ├── (marketing)/ # 营销站
│ │ ├── layout.tsx # 营销站布局(导航栏 + 页脚)
│ │ ├── page.tsx # 首页
│ │ ├── pricing/page.tsx # 定价页
│ │ └── blog/
│ │ ├── page.tsx # 博客列表
│ │ └── [slug]/page.tsx # 博客详情
│ ├── (app)/ # 应用主体
│ │ ├── layout.tsx # 应用布局(侧边栏 + 顶栏)
│ │ ├── dashboard/
│ │ │ └── page.tsx # 仪表盘
│ │ ├── documents/
│ │ │ ├── page.tsx # 文档列表
│ │ │ ├── [id]/
│ │ │ │ ├── page.tsx # 文档编辑器
│ │ │ │ └── _components/ # 编辑器内部组件
│ │ │ │ ├── editor.tsx
│ │ │ │ ├── toolbar.tsx
│ │ │ │ └── ai-suggestions.tsx
│ │ │ └── new/page.tsx # 新建文档
│ │ ├── templates/
│ │ │ ├── page.tsx # 模板库
│ │ │ └── [id]/page.tsx # 模板详情
│ │ └── settings/
│ │ ├── page.tsx # 设置首页
│ │ ├── billing/page.tsx # 账单管理
│ │ └── api-keys/page.tsx # API Key 管理
│ ├── api/
│ │ ├── webhooks/stripe/route.ts # Stripe Webhook
│ │ ├── chat/route.ts # AI 对话 API
│ │ └── documents/route.ts # 文档 CRUD API
│ ├── layout.tsx # 根布局
│ ├── globals.css
│ └── not-found.tsx
├── components/
│ ├── ui/ # 基础 UI(shadcn/ui 生成)
│ │ ├── button.tsx
│ │ ├── dialog.tsx
│ │ ├── input.tsx
│ │ └── ...
│ └── shared/ # 共享业务组件
│ ├── header.tsx
│ ├── sidebar.tsx
│ ├── pricing-table.tsx
│ └── ai-streaming-text.tsx
├── services/
│ ├── document/
│ │ ├── create.ts # 创建文档
│ │ ├── update.ts # 更新文档
│ │ ├── list.ts # 文档列表
│ │ └── types.ts
│ ├── billing/
│ │ ├── stripe.ts # Stripe 集成
│ │ ├── plans.ts # 套餐逻辑
│ │ └── usage.ts # 用量统计
│ └── user/
│ ├── profile.ts
│ └── preferences.ts
├── ai/
│ ├── providers/
│ │ ├── openai.ts # OpenAI 封装
│ │ └── types.ts # 统一接口
│ ├── prompts/
│ │ ├── _registry.ts # Prompt 版本注册表
│ │ ├── generate/
│ │ │ ├── article.ts # 文章生成 Prompt
│ │ │ ├── outline.ts # 大纲生成 Prompt
│ │ │ └── title.ts # 标题生成 Prompt
│ │ ├── rewrite/
│ │ │ ├── simplify.ts # 简化改写
│ │ │ ├── expand.ts # 扩写
│ │ │ └── tone.ts # 语气调整
│ │ └── translate/
│ │ └── translate.ts # 翻译 Prompt
│ ├── agents/
│ │ ├── writer.ts # 写作 Agent(生成 + 改写)
│ │ └── editor.ts # 编辑 Agent(校对 + 润色)
│ ├── vectors/
│ │ ├── embeddings.ts # 文档向量化
│ │ ├── store.ts # Pinecone 客户端
│ │ └── search.ts # 语义搜索
│ └── middleware/
│ ├── token-counter.ts # Token 用量统计
│ └── rate-limit.ts # 调用频率限制
├── lib/
│ ├── db.ts # Prisma 客户端
│ ├── auth.ts # NextAuth 配置
│ ├── utils.ts # 通用工具函数
│ └── validators.ts # Zod 校验 schema
├── hooks/
│ ├── use-ai-stream.ts # AI 流式响应 Hook
│ ├── use-document.ts # 文档状态 Hook
│ └── use-billing.ts # 计费状态 Hook
├── types/
│ ├── document.ts
│ ├── user.ts
│ └── api.ts
├── config/
│ ├── env.ts # 环境变量校验(使用 @t3-oss/env-nextjs)
│ ├── site.ts # 站点元数据
│ └── ai-models.ts # 模型配置(名称、价格、上下文长度)
├── constants/
│ ├── plans.ts # 套餐定义
│ └── limits.ts # 各套餐用量限制
├── styles/
│ └── themes/
├── public/
│ ├── favicon.ico
│ └── og/ # Open Graph 图片
├── prisma/
│ └── schema.prisma # 数据库 Schema
├── next.config.ts
├── tsconfig.json
├── tailwind.config.ts
└── package.json这个结构清晰地分离了营销站和应用主体(通过 route groups),AI 能力独立在 ai/ 目录中,业务逻辑封装在 services/ 中,路由层保持简洁。
案例二:AI 数据分析 SaaS 项目结构
一个面向企业客户的 AI 数据分析平台,支持自然语言查询数据库、自动生成图表、多 Agent 协作。使用 Next.js App Router + Drizzle ORM + 多模型提供商。
ai-analytics/
├── app/
│ ├── (marketing)/ # 营销站
│ │ ├── page.tsx
│ │ ├── pricing/page.tsx
│ │ └── docs/ # 文档站
│ │ └── [[...slug]]/page.tsx
│ ├── (app)/ # 应用主体
│ │ ├── layout.tsx # 应用布局
│ │ ├── workspaces/
│ │ │ ├── page.tsx # 工作空间列表
│ │ │ └── [id]/
│ │ │ ├── page.tsx # 工作空间概览
│ │ │ ├── queries/
│ │ │ │ ├── page.tsx # 查询列表
│ │ │ │ ├── [queryId]/
│ │ │ │ │ ├── page.tsx # 查询详情 + 结果
│ │ │ │ │ └── _components/
│ │ │ │ │ ├── query-editor.tsx
│ │ │ │ │ ├── chart-view.tsx
│ │ │ │ │ └── ai-suggestions.tsx
│ │ │ │ └── new/page.tsx # 新建查询
│ │ │ ├── datasources/
│ │ │ │ └── page.tsx # 数据源管理
│ │ │ └── agents/
│ │ │ └── page.tsx # Agent 配置
│ │ └── admin/
│ │ ├── page.tsx # 管理后台
│ │ └── users/page.tsx # 用户管理
│ ├── api/
│ │ ├── query/route.ts # 查询执行 API
│ │ ├── query/stream/route.ts # 流式查询 API
│ │ ├── datasources/route.ts # 数据源 CRUD
│ │ └── webhooks/
│ │ └── stripe/route.ts
│ └── layout.tsx
├── ai/
│ ├── providers/
│ │ ├── openai.ts
│ │ ├── anthropic.ts
│ │ └── types.ts # Provider 统一接口
│ ├── prompts/
│ │ ├── nl2sql/
│ │ │ ├── system.ts # 自然语言转 SQL 系统 Prompt
│ │ │ ├── schema-context.ts # 数据库 Schema 上下文注入
│ │ │ └── few-shot.ts # 示例 Prompt
│ │ ├── chart/
│ │ │ └── suggest.ts # 图表类型推荐 Prompt
│ │ └── insight/
│ │ └── summarize.ts # 数据洞察摘要 Prompt
│ ├── agents/
│ │ ├── types.ts
│ │ ├── query-agent.ts # 查询 Agent(NL → SQL)
│ │ ├── chart-agent.ts # 图表 Agent(数据 → 可视化)
│ │ ├── insight-agent.ts # 洞察 Agent(结果 → 摘要)
│ │ └── supervisor.ts # 监督 Agent(多 Agent 协调)
│ ├── tools/
│ │ ├── sql-executor.ts # SQL 执行工具(带安全检查)
│ │ ├── chart-renderer.ts # 图表渲染工具
│ │ ├── schema-reader.ts # 数据库 Schema 读取工具
│ │ └── types.ts
│ ├── vectors/
│ │ ├── embeddings.ts # 查询历史向量化
│ │ ├── store.ts # 向量存储(pgvector)
│ │ └── retrieval.ts # 相似查询检索
│ ├── workflows/
│ │ ├── query-pipeline.ts # NL → SQL → 执行 → 可视化
│ │ └── insight-pipeline.ts # 数据 → 分析 → 洞察 → 报告
│ └── middleware/
│ ├── sql-safety.ts # SQL 安全检查(防注入、限制操作)
│ ├── token-counter.ts
│ └── retry.ts
├── services/
│ ├── query/
│ │ ├── execute.ts
│ │ ├── history.ts
│ │ └── types.ts
│ ├── datasource/
│ │ ├── connection.ts # 数据源连接管理
│ │ ├── schema.ts # Schema 获取与缓存
│ │ └── types.ts
│ ├── workspace/
│ │ ├── members.ts
│ │ └── settings.ts
│ └── billing/
│ ├── stripe.ts
│ └── usage.ts
├── components/
│ ├── ui/ # 基础 UI
│ ├── shared/ # 共享组件
│ └── features/
│ ├── query-editor/ # 查询编辑器组件
│ ├── chart-builder/ # 图表构建器
│ ├── datasource-wizard/ # 数据源配置向导
│ └── agent-config/ # Agent 配置界面
├── lib/
│ ├── db.ts
│ ├── auth.ts
│ ├── redis.ts # Redis 客户端(缓存、限流)
│ └── utils.ts
├── hooks/
│ ├── use-query-stream.ts
│ ├── use-workspace.ts
│ └── use-agent.ts
├── types/
│ ├── query.ts
│ ├── datasource.ts
│ ├── workspace.ts
│ └── agent.ts
├── config/
│ ├── env.ts
│ ├── ai-models.ts
│ └── supported-databases.ts # 支持的数据库类型
├── constants/
│ ├── sql-limits.ts # SQL 执行限制(超时、行数)
│ └── plans.ts
├── prisma/
│ └── schema.prisma
└── package.json这个案例展示了多 Agent 协作场景下的目录组织——ai/agents/ 中定义了查询、图表、洞察三个 Agent 和一个监督 Agent,ai/workflows/ 负责编排它们的协作流程,ai/tools/ 提供了 Agent 可以调用的工具函数。
项目依赖关系
下面用 Mermaid 图展示 AI SaaS 项目中各目录的依赖关系。核心原则是依赖方向从外向内,内层不依赖外层:
依赖关系的解读:
- 路由层(
app/) 是最外层,它调用组件层、服务层和 Hooks 来组装页面 - 服务层(
services/) 是业务逻辑的核心,它调用 AI 能力层和工具层 - AI 能力层(
ai/) 只关心 AI 技术细节,不关心业务逻辑 - 工具层(
lib/) 和配置层(config/) 是最底层,被所有层引用 - 类型层(
types/) 是纯数据定义,被所有层引用但不依赖任何层
关键约束:ai/ 目录不依赖 services/ 目录。AI 能力层不应该知道业务逻辑的存在。如果 ai/providers/openai.ts 需要知道「当前用户是哪个套餐」,那就说明依赖方向出了问题。
目录结构设计检查清单
在设计或重构你的 AI SaaS 项目目录结构时,可以用以下清单逐项检查:
- 路由文件是否保持简洁——每个
page.tsx和route.ts不超过 100 行,只做请求处理和页面装配 - AI 代码是否集中在
ai/目录——模型调用、Prompt、Agent、向量检索不应该散落在lib/或services/中 - Prompt 是否独立于业务代码——Prompt 模板放在
ai/prompts/中,不在业务函数中硬编码字符串 - 依赖方向是否正确——
ai/不依赖services/,lib/不依赖services/,内层不依赖外层 - 环境变量是否集中管理——使用
config/env.ts统一校验和导出,不在业务代码中直接读process.env - 类型是否有明确的归属——功能域内的类型在功能域目录下,跨域类型在
types/中 - 路由分组是否合理——营销站和应用主体使用 route groups
(marketing)和(app)分离 - 是否有空抽象——仅使用一次的 helper 函数是否内联比创建新文件更合理
- 单文件行数是否受控——没有超过 500 行的源文件,超出部分是否已按职责拆分
- 静态资源是否正确使用
public/——只有需要直接通过 URL 访问的文件才放在public/ - 私有文件夹是否正确使用——路由段内的内部组件放在
_components/中,避免路由冲突 - 是否预留了扩展空间——新增一个 AI 功能时,不需要改动超过 3 个目录
- 新成员能否快速理解——一个不了解项目的开发者,能否通过目录结构判断功能代码在哪里
总结
项目目录结构不是一个可以「先随意后重构」的事情。当代码量超过几万行之后,目录结构的变化成本非常高——涉及大量的 import 路径修改、测试路径调整和 CI 配置更新。
对于 AI SaaS 项目,有三点特别重要:
- AI 能力代码必须独立——
ai/目录让你的 AI 技术栈演进不影响业务代码 - Prompt 必须可管理——Prompt 是你 AI 产品的核心资产,它需要版本控制、A/B 测试和独立迭代
- 依赖方向必须正确——从外向内的单向依赖,让每一层都可以独立测试和替换
好的目录结构不是一套固定的模板,而是一组原则在不同阶段的适当应用。从 MVP 的扁平结构开始,随着项目增长逐步拆分,始终保持依赖方向的清晰和职责边界的明确。
参考资料
- Next.js Official Docs - Project Structure — Next.js 官方文档对项目结构的完整说明,包含所有目录约定和路由文件规范
- Next.js 15 Project Structure: Full-Stack Guide — 涵盖 App Router、Server Actions 和 AI 模式的完整目录组织指南
- Feature-Driven Architecture with Next.js — 按功能域组织 Next.js 项目的实践指南
- Folder Structure for a Scalable SaaS — 面向可扩展 SaaS 应用的目录结构设计指南
- Clean Folder Structure for Real Agent Systems — AI Agent 系统的目录组织最佳实践,涵盖 agents/、tools/、prompts/ 等目录
- 4-Folder Structure for AI Projects — AI 项目的四层目录结构:Prompts、Data、Agents、Evals
- Making Sense of Next.js App Router Folder Structure — 实战经验总结的 Next.js 项目结构方案