投研智库 RAG Agent(二):环境底座------Docker Compose 一键拉起向量库全家桶
「投研智库 RAG Agent」专栏第 2 篇。上一篇定了架构和路线,这一篇把地基打好:本机一条命令拉起全部中间件,连通性自检全绿,才有资格写第一行业务代码。
一、这一步要达到的目标状态
| 项 | 期望 |
|---|---|
| Python 环境 | 项目目录下 .venv(uv 管理),uv sync 一键复现依赖 |
| 中间件 | etcd / Milvus / MinIO / MongoDB / Attu 全部健康 |
| 本地模型 | bge-m3、bge-reranker-large 下载到本地路径 |
| 配置 | 所有连接信息收敛到根目录 .env |
| 验收 | 连通性自检脚本四项全绿 |
二、Python 环境:为什么用 uv 而不是 conda
依赖定义在 pyproject.toml,锁定在 uv.lock------换机只需 uv sync 就能逐字节复现依赖 ,这是 conda 环境导出/导入做不到的确定性。对应 Java 里的心智模型:pyproject.toml ≈ pom.xml,uv.lock ≈ 依赖树锁定,.venv ≈ 项目私有的运行时。
项目的真实依赖清单(pyproject.toml,Python ≥ 3.11):
ini
dependencies = [
"fastapi>=0.135.1", # Web 框架,SSE 用 StreamingResponse
"langgraph>=1.1.2", # StateGraph 编排
"langchain>=1.2.12", # RecursiveCharacterTextSplitter 等
"langchain-openai>=1.1.11",# 云端 LLM 的 OpenAI 兼容客户端
"flagembedding>=1.3.5", # BGE-M3 本地推理(dense + sparse)
"pymilvus[model]>=2.6.10", # Milvus SDK(含 sparse 支持)
"magic-pdf>=1.3.12", # MinerU 解析
"minio>=7.2.20", # 对象存储 SDK
"pymongo>=4.16.0", # 会话历史、任务状态
"modelscope>=1.35.0", # 模型下载
"torch>=2.10.0", # 本地模型推理后端
"loguru>=0.7.3", # 统一日志
"python-dotenv>=1.2.2", # .env 加载
...
]
几个依赖的版本约束是实际踩坑后锁的:flagembedding 对 transformers 版本敏感,pymilvus 的 sparse API 在 2.6 之前行为不一致------锁在 uv.lock 里,避免"同事机器上装出另一个版本"这种最难查的问题。
bash
cd rag_agent_pro
uv sync # 依赖装进 .venv,锁定版本
提交规则:pyproject.toml / uv.lock 进 git;.venv、.env、docker-data/ 一律忽略。
三、Docker Compose:五个服务一键起
docker-compose.yml 定义了主线需要的全部中间件:
| 服务 | 容器 | 镜像版本 | 端口 | 作用 | 内存上限 |
|---|---|---|---|---|---|
| etcd | rag-etcd | etcd:v3.5.18 | 容器内 | Milvus 的元数据存储 | 512m |
| MinIO | rag-minio | minio:2024-12-18 | 9000/9001 | 对象存储(一套两用,见下) | 512m |
| Milvus | rag-milvus | milvus:v2.5.4 | 19530/9091 | 向量库(standalone 模式) | 2g |
| MongoDB | rag-mongo | mongo:7 | 27017 | 会话历史、任务状态 | 512m |
| Attu | rag-attu | attu:v2.5 | 8002 | Milvus 可视化管理 | 512m |
| Neo4j | rag-neo4j | neo4j:5-community | 7474/7687 | 知识图谱(可选 profile) | 1g |
bash
docker compose up -d # 默认四件套 + Attu
docker compose --profile graph up -d # 需要图谱时才启 Neo4j
docker compose ps # 全部 Up / healthy 即通过
编排文件里的关键细节
拉起五个容器谁都会,这份 compose 文件真正值得讲的是这几处:
1)依赖顺序用 service_healthy,不是裸 depends_on
yaml
milvus:
depends_on:
etcd:
condition: service_healthy # 等 etcd 健康检查通过才启动
minio:
condition: service_healthy
裸 depends_on 只保证"容器启动顺序",不保证"服务真的可用"。Milvus 启动时如果 etcd 还没就绪会直接崩溃退出。每个服务都配了 healthcheck(etcd 用 etcdctl endpoint health,MinIO 用 /minio/health/live,Milvus 用 :9091/healthz,Mongo 用 mongosh ping),Milvus 额外给了 start_period: 90s------它冷启动确实慢,不给宽限期健康检查会误判。
2)每个服务显式 mem_limit
本机开发 Docker 不加限制会把内存吃穿------这是被 Milvus 吃掉 8G 后补的课。此外 Mongo 还加了一层引擎级限制:
bash
mongo:
mem_limit: 512m
command: ["--wiredTigerCacheSizeGB", "0.25"] # WiredTiger 缓存也要限,否则容器内 OOM
只限容器不限引擎,Mongo 的存储引擎仍会按"宿主机内存的 50%"计算缓存,然后在容器内被 OOM killer 干掉------两层都要限。
3)etcd 的自动压缩参数
yaml
etcd:
environment:
ETCD_AUTO_COMPACTION_MODE: revision
ETCD_AUTO_COMPACTION_RETENTION: "1000"
ETCD_QUOTA_BACKEND_BYTES: "2147483648" # 2GB 上限
etcd 默认不压缩历史版本,长期跑会把后端存储撑到配额上限,然后 Milvus 的元数据操作集体报错。这三行是 Milvus 官方推荐配置,照抄即可,但要知道它防的是什么。
4)数据卷全部落 ./docker-data/
所有容器数据映射到项目目录下的 docker-data/(已 gitignore)------docker compose down 不丢数据,想彻底重置就删目录,环境状态一目了然。
三个架构层面的决策:
- MinIO 一套两用 :Milvus 内部存储用
a-bucket,业务图片用knowledge-base-files------省一个容器,本机内存有限时很实在; - Neo4j 走可选 profile :知识图谱不是主线(查询图里 KG 节点是占位),默认不启,1G 内存省下来给 Milvus;Neo4j 自身的堆内存也压到了 512m(
NEO4J_server_memory_heap_max__size); - Attu 默认启动:它是后面每一篇验收的眼睛------集合建没建、数据进没进、向量维度对不对,浏览器里直接看,比写查询脚本快。
四、本地模型:Embedding / Rerank 为什么不走 API
导入一份几百页的手册要向量化几百个 chunk,查询侧每次要精排几十个候选------这类高频批量调用走 API 又贵又慢,所以 BGE-M3 和 BGE-Reranker 都下载到本地跑。
项目里有现成的下载脚本(app/tool/download_bgem3.py / download_reranker.py),核心就是 modelscope 一行:
css
uv run modelscope download --model BAAI/bge-m3 --local_dir <本地路径>
uv run modelscope download --model BAAI/bge-reranker-large --local_dir <本地路径>
两个模型的实际占用(决定了你的硬件预算):
| 模型 | 磁盘 | 加载后内存(CPU/FP32) | 用途 |
|---|---|---|---|
| bge-m3 | ~2.3GB | ~3GB | dense 1024 维 + sparse 双向量 |
| bge-reranker-large | ~2.2GB | ~2.5GB | query-chunk 成对精排 |
无 NVIDIA 显卡时 .env 里设 BGE_DEVICE=cpu、BGE_FP16=0(CPU 上开 FP16 会直接报错,不是变慢)。首次加载模型要几十秒,属正常现象------工程上的对策是模型单例 :进程内只加载一次,服务启动时预热,而不是每次请求加载(这个封装在 app/lm/embedding_utils.py,第 5 篇细讲)。
而生成、改写、图片摘要这类低频高价值调用走云端 OpenAI 兼容接口------这就是上一篇说的"高频本地、低频云端"原则的落地。
五、配置收敛:一个 .env 管所有连接
所有密钥和连接信息集中在根目录 .env(不进 git),由 app/conf/ 下的配置模块统一读取------业务代码不直接碰 os.environ:
ini
# 大模型
OPENAI_API_KEY=sk-xxx
OPENAI_BASE_URL=https://.../compatible-mode/v1
LLM_DEFAULT_MODEL=qwen-flash
# 本地模型
BGE_M3_PATH=D:\ai_models...\bge-m3
RERANKER_MODEL_PATH=D:\ai_models...\bge-reranker-large
BGE_DEVICE=cpu
BGE_FP16=0
# 中间件(本机 Docker 全指向 localhost)
MILVUS_URL=http://localhost:19530
MONGO_URL=mongodb://localhost:27017
MINIO_ENDPOINT=localhost:9000 # 注意:不带 http:// 前缀
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
配置分层对应关系:.env 管"值",app/conf/*.py(milvus_config / minio_config / embedding_config / lm_config 等)管"读取和默认值",docker-compose.yml 里用 ${MINIO_ACCESS_KEY:-minioadmin} 语法与 .env 共享同一份凭证------一处改,处处生效。
一个踩过的坑:MinIO 的 endpoint 不能带 http:// 前缀 (SDK 自己拼协议),而 Milvus 的 URL 要带------两个 SDK 约定不一致,配置错了报的却是含糊的连接错误,排查了半天。类似约定不一致的地方,最好的办法就是像上面那样直接把注释写进 .env 模板里。
六、里程碑验收:连通性自检
写业务代码前先跑一段自检脚本,四项全绿才算地基完工:
python
from pymilvus import MilvusClient
from pymongo import MongoClient
from minio import Minio
print('Milvus', MilvusClient(uri=MILVUS_URL).list_collections()) # ① 向量库通
print('Mongo', MongoClient(MONGO_URL).admin.command('ping')) # ② 文档库通
print('MinIO', [b.name for b in minio_client.list_buckets()]) # ③ 对象存储通
# ④ LLM 探测:让模型回一个字,验证 Key 和 base_url
这一步看似多余,实际挡掉了大量"以为是代码 bug、其实是环境问题"的排查成本------四项自检把"环境错"和"代码错"提前切开了。
Attu(http://localhost:8002)此时能连上 Milvus 但集合为空------这是正常的,集合由导入流程首次运行时创建。
七、本篇小结
- uv 管依赖(版本锁定,逐字节复现),Docker Compose 管中间件(healthcheck +
service_healthy依赖 + 双层内存限制); - 编排细节有讲究:etcd 自动压缩防撑爆、Mongo 容器/引擎两层限内存、Milvus 启动给 90s 宽限期;
- 高频调用(Embedding/Rerank)本地模型 + 进程内单例,低频调用(生成)云端 API;
- 配置全收敛
.env+conf/模块,换机只改一个文件; - 验收动作:连通性自检四项全绿。
地基打好了,下一篇进入正题:导入链路的 LangGraph 图设计------先定义 State 契约和图骨架,再逐个实现节点。
上一篇:《项目背景与总体架构》 · 下一篇:《导入链路(上):LangGraph 图设计与前置解析节点》