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是什么

1 什么是Docker
Docker 是最流行的 Linux 容器解决方案,把应用和依赖打包成镜像文件。
Docker 就像是轻量级的虚拟机,能够提供虚拟化环境,但是占用的资源更少。
2 Docker与虚拟机的对比
虚拟机:每个 VM 有独立操作系统,资源开销大,启动慢(分钟级)。
容器:多个容器共享内核,资源开销小,启动快(秒级),体积小(MB级)。
结论:Docker 比虚拟机启动更快、体积更小。
1.5 Docker核心组件
1 Docker服务端和客户端
Docker 采用客户端-服务端(C/S)架构:
客户端:发送命令(如
docker run)服务端:执行实际操作并返回结果
客户端和服务端可在同一机器,也可远程连接。
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 怎么部署
部署三步法:
构建镜像:项目代码 + Dockerfile → 镜像文件
推送镜像:推送到私有仓库(或文件拷贝)
生产服务器部署:用 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 save → docker load | 教学 / 内网 |
| 私有仓库推送 | docker push → docker 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 一键部署
部署三步法:
构建镜像:项目代码 + Dockerfile → 镜像文件
推送镜像:推送到私有仓库(或文件拷贝)
生产服务器部署:用 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 # 停止并清除数据(⚠️ 慎用,会删除数据库和向量)







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 总结
核心要点:
企业交付内容:镜像、docker-compose.yml、环境配置、接口文档
RAG调用链路:前端 → 网关 API → RAG 模块
接口文档:先写文档再开发,确保协作顺畅
面试题总结
主题一: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_id和parent_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_size 和 chunk_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 版权协议,转载请附上原文链接及本声明。