标准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 Componentspage.tsxlayout.tsxroute.ts只做路由装配,复杂 UI 抽到 _components/
components/ui/无业务逻辑的基础 UI 组件Button.tsxModal.tsxInput.tsx纯展示、接受 props、不依赖业务数据
components/shared/跨页面共享的业务组件Header.tsxDataTable.tsx可能包含业务逻辑,被多个页面引用
lib/通用工具函数、第三方客户端封装utils.tsdb.tsredis.ts纯函数优先,可独立测试
services/业务逻辑的服务层封装ai/completion.tsbilling/stripe.ts一个服务对应一个业务域
types/全局 TypeScript 类型和接口user.tsai.tsapi.ts被多个模块引用的共享类型
config/环境变量读取和应用配置env.tssite.tsai-models.ts集中管理配置,禁止在业务代码直接读 process.env
hooks/自定义 React Hooksuse-auth.tsuse-ai-stream.ts跨组件共享的状态逻辑
constants/枚举、固定值、默认配置plans.tsmodel-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/ 目录下的文件应该尽量薄——只负责:

  1. 接收请求参数
  2. 调用 services/lib/ 中的业务逻辑
  3. 返回响应或渲染页面
// 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.tsai/prompts/ 目录packages/prompts/ 独立包
向量存储可能还没有ai/vectors/ 目录packages/vectors/ 独立包
服务层路由中直接写services/ 按域拆分API 服务独立部署
组件组织components/ 扁平components/features/ 按域packages/ui/ 共享包
配置管理.env + 少量 configconfig/ 集中管理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 项目中各目录的依赖关系。核心原则是依赖方向从外向内,内层不依赖外层

流程图画布 · 115%
Mermaid 流程图加载中...

依赖关系的解读:

  • 路由层(app/ 是最外层,它调用组件层、服务层和 Hooks 来组装页面
  • 服务层(services/ 是业务逻辑的核心,它调用 AI 能力层和工具层
  • AI 能力层(ai/ 只关心 AI 技术细节,不关心业务逻辑
  • 工具层(lib/配置层(config/ 是最底层,被所有层引用
  • 类型层(types/ 是纯数据定义,被所有层引用但不依赖任何层

关键约束:ai/ 目录不依赖 services/ 目录。AI 能力层不应该知道业务逻辑的存在。如果 ai/providers/openai.ts 需要知道「当前用户是哪个套餐」,那就说明依赖方向出了问题。

流程图画布 · 115%
Mermaid 流程图加载中...

目录结构设计检查清单

在设计或重构你的 AI SaaS 项目目录结构时,可以用以下清单逐项检查:

  • 路由文件是否保持简洁——每个 page.tsxroute.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 项目,有三点特别重要:

  1. AI 能力代码必须独立——ai/ 目录让你的 AI 技术栈演进不影响业务代码
  2. Prompt 必须可管理——Prompt 是你 AI 产品的核心资产,它需要版本控制、A/B 测试和独立迭代
  3. 依赖方向必须正确——从外向内的单向依赖,让每一层都可以独立测试和替换

好的目录结构不是一套固定的模板,而是一组原则在不同阶段的适当应用。从 MVP 的扁平结构开始,随着项目增长逐步拆分,始终保持依赖方向的清晰和职责边界的明确。

参考资料

  1. Next.js Official Docs - Project Structure — Next.js 官方文档对项目结构的完整说明,包含所有目录约定和路由文件规范
  2. Next.js 15 Project Structure: Full-Stack Guide — 涵盖 App Router、Server Actions 和 AI 模式的完整目录组织指南
  3. Feature-Driven Architecture with Next.js — 按功能域组织 Next.js 项目的实践指南
  4. Folder Structure for a Scalable SaaS — 面向可扩展 SaaS 应用的目录结构设计指南
  5. Clean Folder Structure for Real Agent Systems — AI Agent 系统的目录组织最佳实践,涵盖 agents/、tools/、prompts/ 等目录
  6. 4-Folder Structure for AI Projects — AI 项目的四层目录结构:Prompts、Data、Agents、Evals
  7. Making Sense of Next.js App Router Folder Structure — 实战经验总结的 Next.js 项目结构方案