前言

上一篇完成了产品定义和架构设计。今天开始真正动手搭项目——从零初始化整个项目骨架,确保所有服务能一键运行。

我们会创建完整的项目目录、配置 docker-compose 编排所有依赖服务、搭建 FastAPI 后端骨架和 React 前端项目。

1. 项目初始化

1.1 创建项目目录

mkdir know && cd know
git init
mkdir -p backend/app/{models,routers,services,schemas,tasks}
mkdir -p frontend/src/{components,pages,hooks,api,lib}
mkdir -p docs
touch backend/Dockerfile backend/requirements.txt
touch frontend/Dockerfile frontend/package.json
touch docker-compose.yml .env.example .gitignore

1.2 .gitignore

# .gitignore
__pycache__/
*.py[cod]
.env
venv/
node_modules/
dist/
.minio/
.postgres/
.idea/
*.db

2. Docker 服务编排

2.1 docker-compose.yml

# docker-compose.yml
services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: know
      POSTGRES_USER: know
      POSTGRES_PASSWORD: know_secret
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U know"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 5s
      retries: 5

  qdrant:
    image: qdrant/qdrant:latest
    ports:
      - "6333:6333"
      - "6334:6334"
    volumes:
      - qdrant_data:/qdrant/storage

  minio:
    image: minio/minio:latest
    ports:
      - "9000:9000"
      - "9001:9001"
    environment:
      MINIO_ROOT_USER: know_admin
      MINIO_ROOT_PASSWORD: know_secret_2024
      MINIO_DEFAULT_BUCKETS: know-files
    volumes:
      - minio_data:/data
    command: server /data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 10s
      retries: 3

  backend:
    build: ./backend
    ports:
      - "8000:8000"
    env_file: .env
    volumes:
      - ./backend/app:/app/app
      - ./backend/alembic.ini:/app/alembic.ini
      - ./backend/migrations:/app/migrations
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      qdrant:
        condition: service_started
      minio:
        condition: service_started
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload

  celery_worker:
    build: ./backend
    env_file: .env
    volumes:
      - ./backend/app:/app/app
    depends_on:
      - backend
      - redis
    command: celery -A app.tasks worker --loglevel=info

  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    volumes:
      - ./frontend/src:/app/src
    depends_on:
      - backend
    command: npm run dev -- --host 0.0.0.0

volumes:
  postgres_data:
  redis_data:
  qdrant_data:
  minio_data:

2.2 环境变量

# .env.example
# Database
DATABASE_URL=postgresql+asyncpg://know:know_secret@postgres:5432/know

# Redis
REDIS_URL=redis://redis:6379/0

# Qdrant
QDRANT_HOST=qdrant
QDRANT_PORT=6333

# MinIO
MINIO_ENDPOINT=minio:9000
MINIO_ACCESS_KEY=know_admin
MINIO_SECRET_KEY=know_secret_2024
MINIO_BUCKET=know-files

# LLM
LLM_API_KEY=sk-your-key-here
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat

# Embedding
EMBEDDING_MODEL=BAAI/bge-small-zh-v1.5
EMBEDDING_DIMENSION=512

# JWT
JWT_SECRET=your-jwt-secret-change-in-production
JWT_ALGORITHM=HS256
JWT_EXPIRATION_HOURS=72

# App
APP_NAME=KNow
APP_ENV=development
LOG_LEVEL=info

复制一份用于开发:

cp .env.example .env

3. 后端项目搭建

3.1 依赖

# backend/requirements.txt
# Web
fastapi==0.115.0
uvicorn[standard]==0.30.0
python-multipart==0.0.12

# Database
sqlalchemy[asyncio]==2.0.35
asyncpg==0.30.0
alembic==1.13.0

# AI
openai==1.50.0
sentence-transformers==3.1.0
qdrant-client==1.12.0

# Task Queue
celery==5.4.0
redis==5.2.0

# Storage
minio==7.2.0

# Auth
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4
python-multipart==0.0.12

# Utils
pydantic==2.9.0
pydantic-settings==2.5.0
python-dotenv==1.0.0
httpx==0.27.0
PyMuPDF==1.24.0
python-magic==0.4.27

3.2 配置文件

# backend/app/config.py
from pydantic_settings import BaseSettings
from typing import Optional
import os


class Settings(BaseSettings):
    # App
    APP_NAME: str = "KNow"
    APP_ENV: str = "development"
    LOG_LEVEL: str = "info"

    # Database
    DATABASE_URL: str = "postgresql+asyncpg://know:know_secret@postgres:5432/know"

    # Redis
    REDIS_URL: str = "redis://redis:6379/0"

    # Qdrant
    QDRANT_HOST: str = "qdrant"
    QDRANT_PORT: int = 6333

    # MinIO
    MINIO_ENDPOINT: str = "minio:9000"
    MINIO_ACCESS_KEY: str = "know_admin"
    MINIO_SECRET_KEY: str = "know_secret_2024"
    MINIO_BUCKET: str = "know-files"

    # LLM
    LLM_API_KEY: Optional[str] = None
    LLM_BASE_URL: str = "https://api.deepseek.com/v1"
    LLM_MODEL: str = "deepseek-chat"

    # Embedding
    EMBEDDING_MODEL: str = "BAAI/bge-small-zh-v1.5"
    EMBEDDING_DIMENSION: int = 512

    # JWT
    JWT_SECRET: str = "your-jwt-secret"
    JWT_ALGORITHM: str = "HS256"
    JWT_EXPIRATION_HOURS: int = 72

    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"


settings = Settings()

3.3 数据库模型

# backend/app/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
from sqlalchemy.orm import DeclarativeBase

from app.config import settings


engine = create_async_engine(settings.DATABASE_URL, echo=False)
async_session = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)


class Base(DeclarativeBase):
    pass


async def get_db():
    async with async_session() as session:
        try:
            yield session
        finally:
            await session.close()


async def init_db():
    """创建所有表。生产环境应该用 Alembic 迁移。"""
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

3.4 数据模型

# backend/app/models/__init__.py
from app.models.user import User
from app.models.knowledge_base import KnowledgeBase
from app.models.document import Document, DocumentChunk
from app.models.conversation import Conversation, Message, Citation

__all__ = [
    "User", "KnowledgeBase", "Document", "DocumentChunk",
    "Conversation", "Message", "Citation",
]
# backend/app/models/user.py
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Boolean, DateTime
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship

from app.database import Base


class User(Base):
    __tablename__ = "users"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    email = Column(String(255), unique=True, nullable=False, index=True)
    password_hash = Column(String(255), nullable=False)
    nickname = Column(String(100), nullable=False)
    avatar_url = Column(String(500), default="")
    is_active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    knowledge_bases = relationship("KnowledgeBase", back_populates="user")
    conversations = relationship("Conversation", back_populates="user")
# backend/app/models/knowledge_base.py
import uuid
from datetime import datetime
from sqlalchemy import Column, String, Text, Integer, DateTime, ForeignKey
from sqlalchemy.dialects.postgresql import UUID
from sqlalchemy.orm import relationship

from app.database import Base


class KnowledgeBase(Base):
    __tablename__ = "knowledge_bases"

    id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
    user_id = Column(UUID(as_uuid=True), ForeignKey("users.id"), nullable=False)
    name = Column(String(200), nullable=False)
    description = Column(Text, default="")
    embedding_model = Column(String(100), default="BAAI/bge-small-zh-v1.5")
    chunk_size = Column(Integer, default=512)
    chunk_overlap = Column(Integer, default=128)
    document_count = Column(Integer, default=0)
    created_at = Column(DateTime, default=datetime.utcnow)
    updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)

    user = relationship("User", back_populates="knowledge_bases")
    documents = relationship("Document", back_populates="knowledge_base",
                            cascade="all, delete-orphan")

3.5 FastAPI 入口

# backend/app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware

from app.config import settings
from app.database import init_db


@asynccontextmanager
async def lifespan(app: FastAPI):
    """应用生命周期管理。"""
    print(f"Starting {settings.APP_NAME}...")
    await init_db()
    print("Database initialized")
    yield
    print("Shutting down...")


app = FastAPI(
    title=settings.APP_NAME,
    lifespan=lifespan,
)

# CORS
app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost:3000", "http://frontend:3000"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


@app.get("/api/health")
async def health_check():
    return {"status": "ok", "app": settings.APP_NAME, "env": settings.APP_ENV}


# 注册路由(后续添加)
# from app.routers import auth, knowledge_bases, documents, conversations
# app.include_router(auth.router, prefix="/api/auth", tags=["Auth"])
# app.include_router(knowledge_bases.router, prefix="/api/knowledge-bases", tags=["Knowledge Bases"])

3.6 Dockerfile

# backend/Dockerfile
FROM python:3.11-slim

WORKDIR /app

# 安装系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    libmagic1 \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# 下载 Embedding 模型(减少首次请求延迟)
RUN python -c "from sentence_transformers import SentenceTransformer; \
    SentenceTransformer('BAAI/bge-small-zh-v1.5')"

COPY . .

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

4. 前端项目搭建

4.1 初始化 React + Vite 项目

cd know/frontend
npm create vite@latest . -- --template react-ts
npm install

4.2 安装依赖

# UI 框架
npm install tailwindcss @tailwindcss/vite lucide-react
npm install @radix-ui/react-dialog @radix-ui/react-dropdown-menu
npm install @radix-ui/react-tabs @radix-ui/react-toast

# 路由
npm install react-router-dom

# HTTP 客户端
npm install @tanstack/react-query axios

# 表单
npm install react-hook-form @hookform/resolvers zod

# 其他
npm install react-markdown remark-gfm date-fns clsx tailwind-merge

4.3 Vite 配置

// frontend/vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import path from "path";

export default defineConfig({
  plugins: [react(), tailwindcss()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    port: 3000,
    proxy: {
      "/api": {
        target: "http://backend:8000",
        changeOrigin: true,
      },
    },
  },
});

4.4 前端目录结构

frontend/src/
├── main.tsx              # 入口
├── App.tsx               # 路由配置
├── index.css             # 全局样式(Tailwind)
├── lib/
│   ├── utils.ts          # 工具函数
│   └── api.ts            # Axios 实例
├── hooks/
│   └── useAuth.ts        # 认证 hook
├── api/
│   ├── auth.ts           # 认证 API
│   └── knowledgeBase.ts  # 知识库 API
├── components/
│   └── ui/               # UI 组件(shadcn)
│       ├── button.tsx
│       ├── input.tsx
│       ├── dialog.tsx
│       └── ...
├── pages/
│   ├── Login.tsx
│   ├── Register.tsx
│   ├── Dashboard.tsx
│   ├── KnowledgeBase.tsx
│   └── Chat.tsx
└── types/
    └── index.ts           # TypeScript 类型定义

4.5 shadcn/ui 配置

npx shadcn@latest init
npx shadcn@latest add button input card dialog toast

4.6 Dockerfile

# frontend/Dockerfile
FROM node:20-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci

COPY . .

EXPOSE 3000
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

4.7 基础 API 客户端

// frontend/src/lib/api.ts
import axios from "axios";

const api = axios.create({
  baseURL: "/api",
  timeout: 30000,
  headers: {
    "Content-Type": "application/json",
  },
});

// 请求拦截器——自动添加 Token
api.interceptors.request.use((config) => {
  const token = localStorage.getItem("token");
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

// 响应拦截器——处理 401
api.interceptors.response.use(
  (response) => response,
  (error) => {
    if (error.response?.status === 401) {
      localStorage.removeItem("token");
      window.location.href = "/login";
    }
    return Promise.reject(error);
  }
);

export default api;

4.8 路由配置

// frontend/src/App.tsx
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
import Login from "./pages/Login";
import Register from "./pages/Register";
import Dashboard from "./pages/Dashboard";
import KnowledgeBaseDetail from "./pages/KnowledgeBaseDetail";
import Chat from "./pages/Chat";
import Layout from "./components/Layout";

function App() {
  return (


        } />
        } />
        }>
          } />
          } />
          } />
          } />

        } />


  );
}

export default App;

5. 验证项目能跑起来

5.1 一键启动

# 在项目根目录
cp .env.example .env
# 编辑 .env,填入 LLM_API_KEY
docker compose up --build

5.2 验证所有服务

# 后端健康检查
curl http://localhost:8000/api/health
# {"status":"ok","app":"KNow","env":"development"}

# PostgreSQL
docker compose exec postgres pg_isready -U know

# Redis
docker compose exec redis redis-cli ping

# Qdrant
curl http://localhost:6333/collections

# MinIO
curl http://localhost:9000/minio/health/live

# 前端
curl -s -o /dev/null -w "%{http_code}" http://localhost:3000
# 200

5.3 验证成功后看到的

CONTAINER ID   IMAGE          STATUS         PORTS
a1b2c3d4e5f6   know-backend   Up 2 minutes   0.0.0.0:8000->8000/tcp
b2c3d4e5f6a7   know-frontend  Up 2 minutes   0.0.0.0:3000->3000/tcp
c3d4e5f6a7b8   postgres:16    Up 2 minutes   0.0.0.0:5432->5432/tcp
d4e5f6a7b8c9   redis:7        Up 2 minutes   0.0.0.0:6379->6379/tcp
e5f6a7b8c9d0   qdrant:latest  Up 2 minutes   0.0.0.0:6333->6333/tcp
f6a7b8c9d0e1   minio:latest   Up 2 minutes   0.0.0.0:9000->9000/tcp

6. 开发工作流

6.1 日常开发

# 启动所有服务
docker compose up -d

# 查看日志
docker compose logs -f backend
docker compose logs -f frontend

# 进入后端容器
docker compose exec backend bash

# 运行数据库迁移
docker compose exec backend alembic upgrade head

# 停止所有服务
docker compose down

6.2 热重载

开发模式下:
- 后端:FastAPI 的 --reload 自动监听代码变化
- 前端:Vite 的 HMR(热模块替换)毫秒级更新
- 代码修改后只需保存文件,不需要重启容器

6.3 数据库迁移

# 初始化 Alembic
docker compose exec backend alembic init migrations

# 创建新的迁移
docker compose exec backend alembic revision --autogenerate -m "add users table"

# 执行迁移
docker compose exec backend alembic upgrade head

# 回滚
docker compose exec backend alembic downgrade -1

总结

今天完成了:

组件 状态
docker-compose 编排(6 个服务)
FastAPI 后端骨架(配置、数据库、模型)
React 前端骨架(Vite + Tailwind + 路由)
API 客户端与拦截器
Dockerfile(前后端)
开发工作流

现在运行 docker compose up --build 就能看到完整的项目跑起来了。

下一篇我们将实现用户系统——注册、登录、JWT 认证、密码加密。


本文是 《AI 全栈开发实战——做一个真正的产品》 系列的第 2 篇。
系列目录:
1. ✅ 产品定义与架构设计
2. ✅ 技术选型与项目初始化 ← 你在这里
3. 📝 用户系统(注册/登录/JWT)
4. 📝 知识库与文档管理
5. 📝 文档处理 Pipeline
...

本文由 Zyentor(智元界) 原创发布