RAG07-企业级生产环境部署

企业级生产环境部署

1 Docker的原理和基本使用

1.1 学习目标

  • 了解虚拟机和Docker的区别

  • 掌握Docker的原理

  • 掌握Docker的基本命令

  • 掌握Docker镜像构建

1.2 环境配置的难题

软件开发中,环境配置一致性是个大问题。不同机器的操作系统、依赖库版本不同,常常出现”在我机器上能跑”的尴尬情况。

理想方案是:把原始环境完整复制过来,实现”开箱即用”。

1.3 虚拟机

虚拟机是最早的”带环境安装”方案,把操作系统、依赖库、配置文件打包成独立盒子,在任何支持的机器上”开箱即用”。

1 什么是虚拟机

虚拟机(Virtual Machine)是在物理机上模拟出的独立计算机系统,拥有完整的操作系统。

物理机 vs 虚拟机:

  • 物理机:一台机器装一个系统,独占资源

  • 虚拟机:一台机器装多个系统,共享硬件但内部隔离

2 虚拟机案例

VMware 可以在 Windows 中同时运行多个虚拟机(如 Windows XP、Windows 8.1)。

关键概念:

  • 物理机:运行虚拟机软件的真实机器

  • 镜像文件:未运行的虚拟机状态

  • 虚拟机:运行中的镜像

3 虚拟机的缺点

  • 资源占用多:即使程序只用 1MB 内存,虚拟机也要占用几百 MB

  • 冗余步骤多:无法跳过用户登录等系统操作

  • 启动慢:启动虚拟机等于启动完整系统,需几分钟

4 Linux 容器

容器不对整个操作系统虚拟化,而是对进程进行隔离。

虚拟化是将物理资源抽象化,把一台物理机虚拟成多台逻辑机,充分利用硬件资源。

优势:

  • 启动快:几秒启动(容器是系统进程)

  • 资源少:只占用需要的资源,多个容器可共享

  • 体积小:只需打包应用和依赖

1.4 Docker是什么

image

1 什么是Docker

Docker 是最流行的 Linux 容器解决方案,把应用和依赖打包成镜像文件。

Docker 就像是轻量级的虚拟机,能够提供虚拟化环境,但是占用的资源更少

2 Docker与虚拟机的对比

虚拟机:每个 VM 有独立操作系统,资源开销大,启动慢(分钟级)。

容器:多个容器共享内核,资源开销小,启动快(秒级),体积小(MB级)。

结论:Docker 比虚拟机启动更快、体积更小。

1.5 Docker核心组件

1 Docker服务端和客户端

Docker 采用客户端-服务端(C/S)架构:

  • 客户端:发送命令(如 docker run

  • 服务端:执行实际操作并返回结果

客户端和服务端可在同一机器,也可远程连接。
Docker的客户端和服务端.png

2 Docker镜像

镜像(Image)是只读模板,包含运行应用所需的所有内容:

  • 完整的操作系统

  • 应用程序

  • 依赖库和配置

一个镜像可创建多个容器。

3 Docker容器

容器(Container)是从镜像创建的运行实例,可启动、停止、删除,相互隔离。

镜像 vs 容器

  • 镜像:静态模板(类)

  • 容器:运行实例(对象)

1.6 Docker的基本使用

Hello World案例

# 下载镜像
docker pull hello-world

# 查看本地镜像
docker images

# 运行容器
docker run hello-world

docker run 会自动下载不存在的镜像。

其他常用命令

docker ps              # 列出运行中的容器
docker ps --all        # 列出所有容器
docker logs [ID]       # 查看容器日志
docker exec -it [ID] /bin/bash  # 进入容器
docker stop [ID]       # 停止容器
docker rm [ID]         # 删除容器
docker rmi [ID]        # 删除镜像

2 服务部署

2.1 学习目标

  • 理解生产环境的服务部署

  • 完成镜像构建实操

  • 了解接口文档编写方式

2.2 生产环境部署

把系统部署到生产服务器(IDC或云服务),主流方式是 Docker 镜像部署。

1 怎么部署

部署三步法

  1. 构建镜像:项目代码 + Dockerfile → 镜像文件

  2. 推送镜像:推送到私有仓库(或文件拷贝)

  3. 生产服务器部署:用 Docker 命令启动容器


flowchart LR

subgraph DEV["开发机"]

Code["项目代码<br/>Dockerfile"]

Image["Docker Image"]

Code -->|docker build| Image

end

Registry["Registry<br/>Harbor / Docker Hub / ACR"]

Tar["Image.tar"]

Image -->|docker push| Registry
Image -->|docker save| Tar

subgraph PROD["生产服务器"]

Image2["Docker Image"]

Container["Docker Container"]

Image2 -->|docker compose up<br/>docker run| Container

end

Registry -->|docker pull| Image2
Tar -->|docker load| Image2

  style DEV fill:none,stroke:#000000
  style PROD fill:none,stroke:#000000
  

大型公司用 Kubernetes(K8s)集群部署,个人项目直接用 Docker 即可。

2 部署以后怎么使用

RAG 系统通常作为内部模块使用。典型架构:前端 → 网关 API → RAG 模块。

flowchart TD; 
a[前端] --> b[后端 API<br/>调度中心] --> c[RAG 模块] --> c1[milvus];
b --> d[订单模块] --> d1[mysql];
b --> e[用户模块] --> e1[mysql];

常用远程调用方式:

  • HTTP/HTTPS(最常用):基于请求-响应模式通信,大多数 AI 服务通过 HTTP 提供 RESTful API
  • WebSocket:建立长连接,支持双向通信,适合聊天、Token 流式输出等实时场景。

说明:

  • HTTP/HTTPS 是网络通信协议。
  • RESTful API 是基于 HTTP 设计 Web API 的一种规范(设计风格),两者不是同一概念。
  • 在大模型项目中,普通接口(如聊天、Embedding、Rerank)通常使用 HTTP + RESTful API;需要实时流式推送时,则常使用 WebSocket(或 HTTP Streaming/SSE)。

2.3 构建项目的Docker镜像

1 编写Dockerfile配置文件

Dockerfile 定义镜像构建逻辑,常用指令:

  • FROM:指定基础镜像

  • RUN:构建时执行命令

  • CMD:容器启动时执行

  • COPY:复制本地文件

  • WORKDIR:设置工作目录

  • ENV:设置环境变量

  • EXPOSE:声明端口

编写 Dockerfile

# ============================================================
# EduRAG 智能问答系统 (edu-rag:gz8)
# 单阶段构建: 避免跨阶段 COPY,大幅提升构建速度
# 锁定 bookworm (Debian 12 稳定版),避免 trixie 镜像源不稳定
# ============================================================

FROM python:3.10-slim-bookworm

# 环境变量
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

# 运行时系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    g++ \
    curl \
    zlib1g-dev \
    libgl1 \
    libglib2.0-0 \
    libmagic1 \
    && rm -rf /var/lib/apt/lists/*

# 复制依赖文件(利用 Docker 层缓存)
COPY requirements.txt .

# torch 从 requirements.txt 中移除,在此单独安装 CPU 版(~200MB)
# 如果从 PyPI 安装,会拉取 GPU 版 torch(~2.4GB) + CUDA 依赖(~3GB)
RUN pip install --no-cache-dir --upgrade "pip<24.1" && \
    pip install --no-cache-dir torch==2.10.0 \
        --index-url https://download.pytorch.org/whl/cpu && \
    pip install --no-cache-dir -r requirements.txt

# 复制项目文件(含 ML 模型 ~5.9GB)
COPY . .

# 非 root 用户
RUN useradd --create-home --shell /bin/bash app && \
    chown -R app:app /app
USER app

# 健康检查(首次部署 init_data.py 约 3-5 分钟)
HEALTHCHECK --interval=30s --timeout=10s --retries=5 --start-period=600s \
    CMD curl -f http://localhost:8080/health || exit 1

EXPOSE 8080

# 启动流程: 初始化数据(FQA导入+向量索引) → 启动应用服务
CMD ["sh", "-c", "python init_data.py && python app.py"]

关键设计说明:

设计点说明
单阶段构建避免多阶段的 COPY --from=builder,Docker Desktop 上跨阶段复制 2GB 需要数小时
slim-bookworm锁定 Debian 12 稳定版,避免 trixie 镜像源 502
CPU-only torch单独安装 CPU 版,避免拉取 ~5GB 的 CUDA 依赖
torch 移出 requirements.txt防止 pip install -r 覆盖为 GPU 版
HEALTHCHECK start-period=600s首次部署 init_data.py 需 3-5 分钟构建向量索引

编写 .dockerignore 文件

.dockerignore 控制哪些文件不打入镜像,避免无用文件增大体积:

# Python 缓存和构建产物
__pycache__/
*.pyc
*.pyo
*.egg-info/
dist/
build/

# 运行时日志(通过 volume 挂载)
logs/

# 敏感配置(通过 .env + docker-compose 注入)
.env

# 未使用的 ML 模型(节省 ~389MB)
rag_qa/models/nlp_bert_document-segmentation_chinese-base/

# 训练和评估数据(仅开发用,不影响运行)
data/model_generic.json
rag_qa/rag_assesment/

# IDE 和编辑器
.vscode/
.idea/

# Docker 自身文件
Dockerfile
docker-compose*.yml
.dockerignore

# 未使用的旧文件
old_main.py

# 系统文件
*.DS_Store
Thumbs.db

# 虚拟环境
venv/

编写数据初始化脚本 init_data.py

容器启动时自动执行,完成 MySQL 和 Milvus 的数据初始化。幂等设计:已有数据时自动跳过,重启秒过。

"""
容器启动初始化脚本 (幂等设计,每次启动安全执行)
在 app.py 启动前运行,确保所有数据就绪。
"""
import sys, os, time

PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, PROJECT_ROOT)

def log(msg):
    print(f"[init_data] {msg}")

def wait_for_service(name, connect_func, max_retries=12, delay=5):
    """通用服务等待函数"""
    for i in range(max_retries):
        try:
            connect_func()
            log(f"✅ {name} 连接成功")
            return True
        except Exception as e:
            log(f"⏳ {name} 未就绪,{delay}s 后重试... ({i+1}/{max_retries})")
            time.sleep(delay)
    log(f"❌ {name} 连接超时")
    return False

# ---- MySQL 初始化 ----
def check_mysql():
    from base.config import config
    import pymysql
    conn = pymysql.connect(host=config.MYSQL_HOST, port=config.MYSQL_PORT,
        user=config.MYSQL_USER, password=config.MYSQL_PASSWORD, connect_timeout=5)
    conn.close()

def init_fqa_data():
    """创建 jpkb 表 + 导入 CSV 数据(幂等)"""
    from mysql_qa.db.mysql_client import MysqlClient
    mysql_client = MysqlClient()           # 自动创建 database
    mysql_client.create_table()             # 创建 jpkb 表
    questions = mysql_client.fetch_questions()
    if questions and len(questions) > 0:    # 已有数据,跳过
        log(f"✅ FQA 数据已存在 ({len(questions)} 条),跳过导入")
        mysql_client.close()
        return
    csv_path = os.path.join(PROJECT_ROOT, "mysql_qa", "data", "JP学科知识问答.csv")
    mysql_client.insert_data(csv_path)     # 导入 CSV
    log(f"✅ FQA 数据导入完成")
    mysql_client.close()

# ---- Milvus 初始化 ----
def check_milvus():
    from base.config import config
    from pymilvus import MilvusClient
    client = MilvusClient(uri=f"http://{config.MILVUS_HOST}:{config.MILVUS_PORT}")
    client.close()

def milvus_collection_has_data():
    """轻量检查:不加载 ML 模型,只查 row_count"""
    from base.config import config
    from pymilvus import MilvusClient
    client = MilvusClient(uri=f"http://{config.MILVUS_HOST}:{config.MILVUS_PORT}")
    try:
        if config.MILVUS_DATABASE_NAME not in client.list_databases():
            return False
        client.use_database(config.MILVUS_DATABASE_NAME)
        if not client.has_collection(config.MILVUS_COLLECTION_NAME):
            return False
        stats = client.get_collection_stats(config.MILVUS_COLLECTION_NAME)
        return int(stats.get("row_count", 0)) > 0
    finally:
        client.close()

def init_milvus_vectors():
    """构建 Milvus 向量索引(仅首次部署执行,约 2-5 分钟)"""
    from base.config import config
    from rag_qa.core.vector_store import VectorStore
    from rag_qa.core.document_processor import process_documents
    log("正在加载 ML 模型 (bge-m3, bge-reranker-large)...")
    vector_store = VectorStore(collection_name=config.MILVUS_COLLECTION_NAME,
        host=config.MILVUS_HOST, port=config.MILVUS_PORT,
        database=config.MILVUS_DATABASE_NAME)
    data_dir = os.path.join(PROJECT_ROOT, "rag_qa", "ai_data")
    chunks = process_documents(data_dir, config.PARENT_CHUNK_SIZE,
        config.CHILD_CHUNK_SIZE, config.CHUNK_OVERLAP)
    if chunks:
        vector_store.add_documents(chunks)
        log(f"✅ Milvus 向量索引构建完成,写入 {len(chunks)} 个文档块")

# ---- 主流程 ----
def main():
    log("=" * 50)
    log("EduRAG 数据初始化开始")
    log("=" * 50)
    if wait_for_service("MySQL", check_mysql):
        try:
            init_fqa_data()
        except Exception as e:
            log(f"❌ FQA 初始化失败: {e}")
    if wait_for_service("Milvus", check_milvus, max_retries=15, delay=5):
        try:
            if not milvus_collection_has_data():
                init_milvus_vectors()
        except Exception as e:
            log(f"❌ Milvus 索引构建失败: {e}")
    log("数据初始化完成,即将启动应用...")

if __name__ == "__main__":
    main()

初始化流程图:

容器启动 → init_data.py
  │
  ├─ MySQL 检查(秒级)
  │   ├─ 有数据 → 跳过 ✓
  │   └─ 无数据 → 建表 + 导入 CSV
  │
  ├─ Milvus 轻量检查(秒级,不加载模型)
  │   ├─ 有向量 → 跳过 ✓
  │   └─ 无向量 → 加载 bge-m3 → 处理文档 → 向量化 → 写入 Milvus
  │
  └─ python app.py → 启动 FastAPI 服务

首次部署: 约 3-5 分钟
后续重启: 约 3-5 秒(全部跳过)

2 配置 .env 文件(敏感信息)

.env 文件存放密码和 API 密钥等敏感信息,不要提交到 Git 仓库

docker compose 启动时自动读取此文件进行变量替换:

# ---- 应用端口 ----
APP_PORT=8080

# ---- MySQL 认证 ----
MYSQL_USER=root
MYSQL_PASSWORD=123456
MYSQL_DATABASE=subjects_kg

# ---- Redis 认证 ----
REDIS_PASSWORD=1234
REDIS_DB=0

# ---- Milvus ----
MILVUS_DATABASE_NAME=itcast
MILVUS_COLLECTION_NAME=edurag_gz8

# ---- LLM API 密钥 ----
DASHSCOPE_API_KEY=你的API密钥
LLM_MODEL=qwen-plus
DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1

3 构建 Docker 镜像

# 在项目根目录执行(Dockerfile 所在目录)
docker build -t edu-rag:gz8 .

# 查看构建结果
docker images

4 传输镜像到服务器

镜像构建完成后,需要传输到生产服务器。两种方式:

方式命令适用场景
tar 文件传输docker savedocker load教学 / 内网
私有仓库推送docker pushdocker pull企业环境
# === 方式一:tar 文件(适合内网传输)===
docker save -o edu-rag-gz8.tar edu-rag:gz8      # 开发机:导出镜像
# scp edu-rag-gz8.tar user@server:/opt/edurag/   # 传输到服务器
docker load -i edu-rag-gz8.tar                   # 服务器:加载镜像

# === 方式二:私有仓库(适合企业环境)===
docker tag edu-rag:gz8 <仓库地址>/edu-rag:gz8     # 开发机:打标签
docker push <仓库地址>/edu-rag:gz8                # 开发机:推送
docker pull <仓库地址>/edu-rag:gz8                # 服务器:拉取

5 编写 docker-compose.yml(全套服务编排)

docker-compose.yml 定义全部服务的启动、网络和依赖关系。

本方案采用全套容器化:App + MySQL + Redis + Milvus 全部在 Docker 中运行,容器间通过服务名通信,不需要 host.docker.internal

# ============================================================
# EduRAG 智能问答系统 - 全套 Docker Compose 编排
# 架构: 每个服务一个容器,全部自包含,一键启动
#
# 【生产环境】先加载镜像,再启动:
#   docker load -i edu-rag-gz8.tar        ← 加载应用镜像
#   docker compose up -d                   ← 启动全部容器
#
# 【开发环境】从源码构建并启动:
#   docker compose up -d --build           ← 构建镜像 + 启动容器
#
# 停止: docker compose down
# 日志: docker compose logs -f edurag-app
#
# 首次启动时自动完成(无需手动操作):
#   - MySQL jpkb 表创建 + FQA 数据导入
#   - Milvus 向量索引构建(处理 rag_qa/ai_data/ 中的文档)
#   后续重启自动跳过(幂等设计,秒过)
# ============================================================

version: '3.8'

services:
  # ============ EduRAG 应用 ============
  edurag-app:
    build: .              # 开发环境: docker compose up -d --build 时从 Dockerfile 构建
    image: edu-rag:gz8    # 生产环境: docker compose up -d 时直接使用已有镜像
    container_name: edurag-container
    ports:
      - "${APP_PORT:-8080}:8080"
    environment:
      - HOST=0.0.0.0
      - PORT=8080
      # MySQL (容器间通信,直接用服务名)
      - MYSQL_HOST=mysql
      - MYSQL_PORT=3306
      - MYSQL_USER=${MYSQL_USER:-root}
      - MYSQL_PASSWORD=${MYSQL_PASSWORD:-123456}
      - MYSQL_DATABASE=${MYSQL_DATABASE:-subjects_kg}
      # Redis
      - REDIS_HOST=redis
      - REDIS_PORT=6379
      - REDIS_PASSWORD=${REDIS_PASSWORD:-1234}
      - REDIS_DB=${REDIS_DB:-0}
      # Milvus
      - MILVUS_HOST=milvus
      - MILVUS_PORT=19530
      - MILVUS_DATABASE_NAME=${MILVUS_DATABASE_NAME:-itcast}
      - MILVUS_COLLECTION_NAME=${MILVUS_COLLECTION_NAME:-edurag_gz8}
      # LLM
      - DASHSCOPE_API_KEY=${DASHSCOPE_API_KEY}
      - LLM_MODEL=${LLM_MODEL:-qwen-plus}
      - DASHSCOPE_BASE_URL=${DASHSCOPE_BASE_URL:-https://dashscope.aliyuncs.com/compatible-mode/v1}
    volumes:
      - ./logs:/app/logs
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_healthy
      milvus:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - edu-rag-net

  # ============ MySQL ============
  mysql:
    image: mysql:8.0
    container_name: edurag-mysql
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_PASSWORD:-123456}
      MYSQL_DATABASE: ${MYSQL_DATABASE:-subjects_kg}
    ports:
      - "3307:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped
    networks:
      - edu-rag-net

  # ============ Redis ============
  redis:
    image: redis:7-alpine
    container_name: edurag-redis
    command: redis-server --requirepass ${REDIS_PASSWORD:-1234}
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-1234}", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    networks:
      - edu-rag-net

  # ============ Milvus (向量数据库) ============
  milvus:
    image: milvusdb/milvus:v2.4.0
    container_name: edurag-milvus
    command: ["milvus", "run", "standalone"]
    environment:
      ETCD_USE_EMBED: "true"
      ETCD_DATA_DIR: /var/lib/milvus/etcd
      COMMON_STORAGETYPE: local
    ports:
      - "19530:19530"
      - "9091:9091"
    volumes:
      - milvus_data:/var/lib/milvus
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 15s
      timeout: 10s
      retries: 10
      start_period: 90s
    restart: unless-stopped
    networks:
      - edu-rag-net

volumes:
  mysql_data:
  redis_data:
  milvus_data:

networks:
  edu-rag-net:
    driver: bridge

⚠️ 生产服务器上没有源码和 Dockerfile,必须用 docker compose up -d(不加 --build),否则会报错。

架构说明:

flowchart LR;
    subgraph Docker Compose 网络
        app[edurag-app<br>:8080] -->|MYSQL_HOST=mysql| mysql[MySQL<br>:3306]
        app -->|REDIS_HOST=redis| redis[Redis<br>:6379]
        app -->|MILVUS_HOST=milvus| milvus[Milvus<br>:19530]
    end
    user[用户浏览器] -->|http://IP:8080| app

depends_on + condition: service_healthy 的作用: 确保 MySQL、Redis、Milvus 全部就绪后,App 容器才启动。避免 App 启动时连不上数据库。

6 一键部署

部署三步法

  1. 构建镜像:项目代码 + Dockerfile → 镜像文件

  2. 推送镜像:推送到私有仓库(或文件拷贝)

  3. 生产服务器部署:用 Docker 命令启动容器


flowchart LR

subgraph DEV["开发机"]

Code["项目代码<br/>Dockerfile"]

Image["Docker Image"]

Code -->|docker build| Image

end

Registry["Registry<br/>Harbor / Docker Hub / ACR"]

Tar["Image.tar"]

Image -->|docker push| Registry
Image -->|docker save| Tar

subgraph PROD["生产服务器"]

Image2["Docker Image"]

Container["Docker Container"]

Image2 -->|docker compose up<br/>docker run| Container

end

Registry -->|docker pull| Image2
Tar -->|docker load| Image2

style DEV fill:none,stroke:#000000
style PROD fill:none,stroke:#000000

核心原则:开发机构建镜像,生产机只启动容器。生产服务器不需要源码和 Dockerfile。

生产服务器只需 3 个文件

文件用途
edu-rag-gz8.tar应用镜像(代码 + 依赖 + ML 模型,已全部打包)
docker-compose.yml服务编排配置(定义 4 个容器的启动顺序和网络)
.env敏感配置(数据库密码、API 密钥)
# 生产服务器上执行
docker load -i edu-rag-gz8.tar     # 加载应用镜像
docker compose up -d               # 启动全部 4 个容器(App + MySQL + Redis + Milvus)

首次启动时 init_data.py 自动完成数据初始化(MySQL 建表导数据 + Milvus 构建向量索引),约 3-5 分钟。
后续重启秒过(幂等设计,已有数据自动跳过)。

部署后验证

docker compose logs -f edurag-app    # 查看启动日志
docker compose ps                    # 确认容器状态为 healthy
curl http://localhost:8080/health    # 返回 {"status": "healthy"}

正常启动日志:

[init_data] ✅ MySQL 连接成功
[init_data] ✅ FQA 数据导入完成
[init_data] ✅ Milvus 向量数据已存在 (xxx 条),跳过索引构建
[init_data] 数据初始化完成,即将启动应用...
INFO:     Uvicorn running on http://0.0.0.0:8080

常用运维命令

docker compose logs -f edurag-app    # 查看应用日志
docker compose exec edurag-app bash  # 进入容器调试
docker compose restart               # 重启服务(不重建镜像)
docker compose down                  # 停止全部服务
docker compose down -v               # 停止并清除数据(⚠️ 慎用,会删除数据库和向量)

image.png

image.png

image.png

image.png

image.png

image.png

image-2.png

2.4 接口文档

1 什么是接口文档

接口文档是 API 的使用说明书,告诉开发者怎么调用接口、传什么参数、返回什么结果。

必须包含的信息

  • 接口路径

  • 功能描述

  • 请求参数(必填、类型)

  • 响应结果

2 edu-rag问答系统 API 接口文档

项目概述

  • 基础框架:FastAPI

  • 默认端口:8080

接口列表

首页访问

  • 路径:GET /

  • 描述:返回前端页面

创建会话

  • 路径:POST /api/create_session

  • 返回:{"session_id": "string"}

查询历史消息

  • 路径:GET /api/history/{session_id}

  • 返回:{"session_id": "string", "history": []}

清除历史消息

  • 路径:DELETE /api/history/{session_id}

非流式查询

  • 路径:POST /api/query

  • 请求体:{"query": "问题", "source_filter": "可选", "session_id": "可选"}

  • 返回:{"answer": "答案", "is_streaming": false, "session_id": "string", "processing_time": 0.5}

流式查询(WebSocket)

  • 路径:WS /api/stream

  • 发送:{"query": "问题", "source_filter": "可选", "session_id": "可选"}

  • 接收:

    • {"type": "start", "session_id": "..."} - 开始

    • {"type": "token", "token": "..."} - 逐字返回

    • {"type": "end", "session_id": "...", "is_complete": true} - 结束

健康检查

  • 路径:GET /health

  • 返回:{"status": "healthy"}

获取学科类别

  • 路径:GET /api/sources

  • 返回:{"sources": ["ai", "java", "test", "ops", "bigdata"]}

2.5 总结

核心要点

  1. 企业交付内容:镜像、docker-compose.yml、环境配置、接口文档

  2. RAG调用链路:前端 → 网关 API → RAG 模块

  3. 接口文档:先写文档再开发,确保协作顺畅

面试题总结

主题一:FQA、BM25 与检索策略

Q1 如何减少 BM25 FQA 的错误命中(语义不相关但分数过线)?

答:

  • 本质原因:BM25 是关键词匹配,不理解深层语义。
  • 线上缓解:
    • 双阈值:归一化分数(相对阈值,如 0.85)+ 原始分数(绝对阈值,如 10.0)同时达标。
    • 持续维护问答对:补齐同义表达,减少误召回。
    • 查询改写:LLM 改写 query 后再检索(效果可提升,但延迟较高)。
  • 升级方案:
    • 用 embedding 语义检索替代 BM25(向量+余弦/IP,相似度阈值如 0.9)。
    • 混合检索:BM25 + embedding,使用 RRFRanker 融合。
    • BM25 粗召回 + Cross-Encoder 精排,过滤误匹配。

Q2 BM25 检索与 embedding 稀疏向量 IP 匹配有什么区别?

答:

  • BM25:规则检索,关键词匹配,速度快、实现简单,适合 FQA 快速召回。
  • embedding 稀疏向量 IP:向量匹配,可实现更强关键词语义表达,效果通常更好,但计算更重、模型更大。

Q3 FQA 为什么同时使用 MySQL 和 Redis?

答:

  • MySQL:持久化主存储,保证数据不丢失,支持完整 CRUD。
  • Redis:高频问答缓存,内存读写,延迟低。
  • 查询链路:Redis -> MySQL -> 回写 Redis。
  • 价值:同时兼顾数据可靠性与高并发性能。

Q4 BM25 的核心思想是什么?

答:

  • BM25 是 TF-IDF 的改进排序算法,核心由三部分构成:
    • IDF:词越稀有,区分度越高,权重越大。
    • TF 饱和:出现次数越多分越高,但不会无限线性增长。
    • 文档长度归一化:短文中命中同样词通常更“重要”。
  • 本项目中:rank_bm25.BM25Okapi 打分后,通过 softmax 归一化到 [0,1] 便于阈值判断。

主题二:文档处理与分块

Q1 文档加载和文档分块的作用分别是什么?

答:

  • 文档加载:把 PDF/Word/PPT/图片/Markdown 等统一成 Document(正文+元数据)。
  • 文档分块:把长文切成可检索片段,提高召回精度并降低 LLM 上下文成本。

Q2 为什么做父子分块(Parent-Child Chunking)?

答:

  • 核心:小块检索更准,大块上下文更完整(small-to-big)。
  • 子块(如 300):用于精准向量检索。
  • 父块(如 1200):用于回答生成,减少语义缺失。
  • 实现关键:子块 metadata 记录 parent_idparent_content,命中子块后可回溯父块。

Q3 为什么自定义加载模块,而不是只用 LangChain 内置加载器?

答:

  • 内置加载器对“文本层”友好,但对扫描版 PDF、图中文字、复杂表格/图形处理不足。
  • 课件场景含大量图片与图表,不补 OCR 会形成检索盲区。
  • 工程做法:按格式选专用解析库 + OCR,最后统一封装为 Document

Q4 为什么要做 OCR?

答:

  • 大量知识存在于截图、公式、示意图中,不 OCR 无法进入知识库。
  • 取舍策略:不是全量 OCR;例如 PDF 只对大图(宽高占比阈值)做 OCR,提高效率。

Q5 如何提取 PDF 文本和图片?

答:

  • 使用 PyMuPDF(fitz)逐页处理:
    • page.get_text("text") 提取文本层。
    • page.get_image_info(xrefs=True) 获取图片并按尺寸阈值过滤。
    • fitz.Pixmap 提取像素,必要时做旋转校正,再交给 RapidOCR 识别。
  • 页内文本与 OCR 结果拼接,最终形成统一 Document

Q6 Word、PPT、图片的加载原理?

答:

  • Word:python-docx 解析段落/表格,内嵌图片提取后二次 OCR。
  • PPT:python-pptx 按阅读顺序遍历 shape,文本/表格直提,图片与组合图递归 OCR。
  • 图片:RapidOCR 直接识别并拼接文本。

Q7 为什么自定义中文分割器?

答:

  • 默认分割器偏英文,中文句边界不自然,容易“半句截断”。
  • 自定义策略:按“段落 -> 句号/问号/感叹号 -> 分号 -> 逗号”递归切分,并保留分隔符,语义更完整。

Q8 chunk_sizechunk_overlap 怎么设?

答:

  • chunk_size:越大上下文更完整,但检索粒度变粗;越小更精确,但上下文可能断裂。
  • chunk_overlap:减少边界信息丢失,常取 chunk_size 的 10%~20%。
  • 本项目示例:父块 1200、子块 300、重叠 50(约 17%)。

主题三:Milvus、向量检索与精排

Q1 什么是向量数据库?为什么用 Milvus?

答:

  • 向量数据库用于存储向量并做相似度检索。
  • Milvus 适合大规模向量场景:检索快、索引类型丰富、生态成熟。

Q2 为什么嵌入模型用 bge-m3?

答:

  • 可同时生成 dense + sparse 向量:
    • dense:语义相似召回。
    • sparse:关键词匹配(可替代/增强 BM25)。
  • 一个模型覆盖两类召回信号,工程上更高效。

Q3 嵌入模型和精排模型的区别?

答:

  • 嵌入模型:负责召回候选。
  • 精排模型(Cross-Encoder):负责候选重排序。
  • 两者目标不同,通常不建议一个模型“一把梭”。

Q4 dense 向量和 sparse 向量的区别?

答:

  • dense:每维通常非零,擅长语义相近匹配。
  • sparse:大部分维度为零,擅长关键词精确命中。

Q5 为什么做混合检索?

答:

  • 用户问题有时偏语义、有时偏关键词。
  • dense + sparse 融合,召回更全面,稳定性更好。

Q6 IVF_FLAT、nlist、nprobe 是什么?

答:

  • IVF_FLAT:先聚类后检索的常见向量索引。
  • nlist:簇数量,越大越细。
  • nprobe:查询时探测簇数量,越大越准但更慢。

Q7 为什么 sparse 常用 SPARSE_INVERTED_INDEX + IP?

答:

  • sparse 向量本质是“词项-权重”,倒排结构天然适配。
  • IP(内积)与权重匹配逻辑一致,实践中效果稳定。

Q8 WeightedRanker 与 RRFRanker 怎么选?

答:

  • WeightedRanker:可控性强,需人工调权重。
  • RRFRanker:对不同分数尺度更鲁棒,工程上更稳。

Q9 为什么先混合检索再 Cross-Encoder 精排?

答:

  • 直接全量精排成本过高。
  • 两阶段(召回 -> 精排)是性能与效果的折中最优解。

Q10 Cross-Encoder 的精排逻辑是什么?

答:

  • 输入 (query, doc) 成对文本,模型直接输出相关性分数。
  • 分数越高,文档越适合作为最终回答依据。

Q11 hashlib 在向量入库中的作用?

答:

  • 用 MD5 为文本生成稳定 ID,便于 upsert 和去重控制。
  • 避免重复写入导致检索结果污染。

Q12 为什么先连 Milvus default 库再建业务库?

答:

  • 业务库不存在时直接连接可能失败。
  • 先连接 default 再创建并切换,流程更稳妥。

Q13 一句话概括检索流程?

答:

  • 文档向量化(dense+sparse)写入 Milvus,查询时混合检索召回候选,再经 Cross-Encoder 精排,返回最相关父文档作为上下文。

主题四:系统架构与在线服务

Q1 为什么采用“规则 + FQA + RAG”三层架构?

答:

  • 按成本和准确率分层:规则最快、FQA 处理高频、RAG 处理开放问题。
  • 目标是兼顾时延、成本和回答质量。

Q2 FQA 与 RAG 的核心区别?

答:

  • FQA:从已有标准问答中匹配答案。
  • RAG:先检索资料,再让 LLM 基于资料生成答案。

Q3 为什么 WebSocket 更适合流式输出?

答:

  • 长连接可持续推送 token,天然适配实时对话体验。

Q4 为什么要做问候语短路?

答:

  • 问候类问题无需检索与生成,直接返回可显著降延迟和降成本。

Q5 session_id 的作用是什么?

答:

  • 标识一次会话,关联多轮上下文与历史记录,保证对话连续性。

Q6 为什么用“队列 + 线程池”桥接流式生成?

答:

  • 许多检索链/模型接口是同步生成器。
  • 子线程生产、主协程异步发送,可避免阻塞事件循环。

Q7 如何保证回答可追溯性?

答:

  • 先检索证据再生成回答,尽量基于检索上下文,降低“无依据生成”。

Q8 阈值(如 BM25 threshold)的作用?

答:

  • 用于决定“直接走 FQA”还是“升级到 RAG”。
  • 本质是在精度、召回、成本、体验之间做平衡。

本文为 程序员青阳 原创文章,遵循 CC BY-NC-SA 4.0 版权协议,转载请附上原文链接及本声明。

原文链接:https://heliufang.github.io/posts/78377242/index.html