项目概述与环境搭建
Written: 2026.06🎬 推荐:在学习本讲之前或之后,观看 RAG Pipeline 执行流程动画演示 建立对系统整体执行流程的直观认识。
1. 本讲目标
- 理解 RAG 系统的基本概念和应用场景
- 了解本项目的整体架构和技术栈
- 完成开发环境的搭建和验证
2. 前置知识
2.1 什么是 RAG(检索增强生成)
RAG = Retrieval-Augmented Generation,即”检索增强生成”。 在没有 RAG 之前,大语言模型(LLM)存在几个核心问题:- 知识截止日期:模型训练完成后,无法获取训练数据之后的新信息。例如 GPT-4 的知识截止到 2023 年某月,之后发生的事情它不知道。
- 幻觉问题:当模型不确定某个答案时,它可能会”编造”一个看起来很合理但实际上是错误的内容。这在企业场景中是不可接受的。
- 私有知识无法覆盖:企业内部的制度、流程、业务文档是私有数据,从未进入过公开训练集,模型自然无法回答。
- 答案是可溯源的(每个回答都能追溯到具体的文档片段)
- 知识可以实时更新(更新知识库不需要重新训练模型)
- 幻觉大幅减少(模型被约束在提供的文档范围内回答)
2.2 RAG 系统的基本组成
一个完整的 RAG 系统包含两条核心链路: 对比:传统 LLM vs RAG 离线链路(入库):- 文档加载:读取 PDF、Markdown、Word 等各种格式的文档
- 文档切分:把长文档按语义边界切成小块(chunk),每块通常是几百到一千字
- 向量化:用 Embedding 模型把每个 chunk 转成一串数字(向量),表达其语义
- 存储:把向量和原文一起存入向量数据库
- 意图识别:判断用户问的是什么类型的问题
- 查询向量化:把用户问题转成向量
- 语义检索:在向量数据库中找最相似的文档片段
- 上下文构建:把检索到的片段整理成 LLM 的参考资料
- 答案生成:LLM 基于资料生成回答
2.3 向量和向量检索是什么
这是理解 RAG 最关键的概念。我们通过一个类比来理解: 类比:图书馆找书 传统关键词搜索(比如 MySQL LIKE 查询)就像你告诉图书管理员”我要找书名里有’Python’的书”。问题在于:- 一本叫《Python 机器学习实战》的书会被找到
- 但一本叫《用编程语言做数据分析》的书不会被找到,虽然它也讲 Python
- Embedding 模型把一段文本(比如一句话、一个段落)转换成一个固定长度的浮点数数组,例如 1024 维的向量
- 语义相近的文本,它们的向量在数学空间中的”距离”也近
- 向量数据库就是专门存储和检索这些向量的系统
3. 项目架构
3.1 一句话描述
这是一个基于 LangChain + Milvus 2.5 Hybrid Search 的多场景 RAG 教学平台。不是简单的 Demo,而是补齐了企业级 RAG 完整工程闭环的项目。
3.2 全景架构图
3.2.1 架构分层解读
全景架构图从上到下可以划分为 五个层次,每一层各司其职:
为什么要分层? 每一层只跟相邻层打交道:浏览器不直接调 Milvus,API 路由不直接拼 Prompt,核心引擎不直接写 HTTP 响应。这保证了任何一层被替换时,其他层不受影响。
3.2.2 用户入口 — 问答页和状态页各做什么
全景架构图的顶部有两个入口角色: 问答页(static/index.html) 是面向普通用户的交互界面,它会发出以下常用请求:
关键理解:这些端点里,只有 /api/stream 是 WebSocket 长连接——它承载了最重的 RAG 逻辑。其他端点都是轻量 HTTP 请求,负责页面初始化、历史、反馈、版本或过滤项查询。
状态页(static/admin.html) 是面向开发者/管理员的诊断界面,它通过 GET /api/admin/* 访问 LangSmith 状态、active 版本、入库质量报告和回归报告入口。状态页不参与在线问答,只做”事后排查”。
3.2.3 FastAPI 路由层 — 请求如何分发
app.py 只做四件事:创建 FastAPI 应用、配置 CORS、启动时执行环境校验、注册路由。业务逻辑全在路由模块里:
关键设计决策:在线问答统一走 WebSocket(
/api/stream),不再提供额外的 HTTP 问答入口。如果 HTTP 和 WebSocket 两套问答入口并存,容易出现”HTTP 返回的结果和 WebSocket 不一样”的不一致问题。问候、越界、人工客服短句这些无需检索的问题,也在 WebSocket 主链路的意图识别阶段直接返回。
3.2.4 qa_core 核心引擎 — 八大模块如何协作
这是整个项目的大脑。图表中的八个模块各自承担独立的职责,通过QAService(服务编排层)统一调度:
① QAService — 服务编排层(总指挥)
QAService 是唯一对外的业务入口。路由层的 chat.py 不直接调 Intent、不直接拼 Prompt,一切通过 QAService 调度。它提供两个问答相关核心方法:
decide_route():先处理问候、转人工、越界、场景边界和 FAQ 精确命中。只有 route=retrieval 时,才进入 classify_intent() 做检索类意图识别。Intent 模块回答三个问题:
- 这是什么类型的问题?(问候 / 标准问答 / 知识咨询 / 追问 / 人工客服 / 越界)
- 能不能直接回答?(问候直接回”你好”,越界直接拒答)
- 如果不能直接回答,后续该怎么处理?(改写成独立问题?FAQ 优先还是文档优先?)
- MilvusHybridStore:连接管理、Collection 操作、add_documents / search
- 检索计划(RetrievalPlan):把意图转成具体参数——FAQ 查几条、Doc 查几条、阈值多高、是否 rerank
- 过滤表达式:把
scenario_id、kb_version、tenant_id、dataset_id、visibility、source拼成 Milvus expr,保证不会跨场景、跨版本混查 - 重排序(Reranker):用 BGE Reranker CrossEncoder 对召回结果精排
- 去重:FAQ 和 Doc 各自的去重逻辑
⑤ Prompts — 提示词工程(给 LLM 的指令)
不同的意图和问题类别需要不同的 System Prompt。例如:
faq_answer:严格用标准答案回答,不要自行发挥knowledge_answer:基于提供的文档资料整合回答follow_up:结合对话历史理解用户追问的上下文default:通用问答模板
build_answer_prompt_profile 根据意图和问题类别自动决定,而不是 hardcode 一个通用模板。
⑥ Memory — 聊天记忆(上下文连续性)
Memory 模块管理两件事:
- 聊天历史(HistoryStore):基于 LangChain 的
SQLChatMessageHistory,每次问答后写入 MySQL 的chat_messages表。下次提问时读取最近 N 条消息 + 历史摘要,让模型知道”刚才在聊什么” - 反馈(FeedbackStore):用户点赞/点踩后写入 MySQL,用于后续 bad case 分析和评测集补充
- kb_version:每个 FAQ 和 chunk 都标记了入库版本号。在线检索时自动拼入
kb_version == "xxx"过滤条件,确保只查 active 版本 - data_scope:每条数据还有
tenant_id、dataset_id、visibility、allowed_roles字段。检索时拼成 Milvus 表达式,实现租户级数据隔离
3.2.5 外部依赖 — 三个独立服务各管什么
全景架构图底部有三个圆柱形节点([(...)]),代表独立部署的外部服务:
Milvus — 向量数据库
with_structured_output),以及最终答案的流式生成(llm.stream)。两处共用同一个 get_chat_model() 工厂方法,只是 streaming 参数不同。
3.2.6 完整问答链路走读:一次用户提问经历了什么
把全景架构图的箭头串起来,就是一次完整问答请求的真实轨迹。以下用”入职流程有哪些步骤”这个提问来走一遍:耗时分布(典型值):意图识别 ~50ms | FAQ 检索 ~80ms | Doc 检索 ~120ms | Rerank ~200ms | LLM 生成首 token ~2500ms | 总计 ~3000-4000ms。最慢的环节永远是 LLM 生成——因为需要等待云端模型推理,这是所有 RAG 系统的共性。两条特殊路径(图中箭头覆盖但上面没走到的):
- 问候/越界快速通道:
User → /api/stream → chat.py → QAService.stream_query → decide_route()在主链路内直接产出答案事件,后面的检索准备、检索和 LLM 全部跳过。耗时取决于运行环境和限流/数据库状态,通常远低于完整 RAG。 - FAQ 直出通道:分两种情况:Stage 1 的
route=faq_exact只允许标准问题精确匹配;Stage 3 的 FAQ 标准直出则发生在检索准备之后,允许精确匹配或达到动态阈值。两者都会返回metadata.answer,不进入 Doc 检索和 LLM 生成。 - 追问改写通道:用户说”那审批呢”→ 意图识别为 FOLLOW_UP → Pipeline 读取历史,把”入职流程中的审批步骤”补全 → 后续流程和普通 RAG 一样。
3.3 部署架构图
3.3.1 部署拓扑解读
部署架构图将整个系统划分为四个物理区域,每个区域的部署方式和选型理由各不相同:3.3.2 六条连接线逐一解读
图中的每一条箭头都是一条运行时依赖,按调用频率和延迟敏感度排列: ① FastAPI → Milvus(高频 · 延迟敏感)- LLM 推理需要 GPU 显存(至少 16GB+),本地开发机通常跑不动
- LLM 调用频率相对低(每次问答 1-2 次),不像 Embedding 那样一次入库就调用上百次
- API 调用按 token 计费,成本可控
- LLM 是文本进文本出,不涉及向量数据,传输量小
- etcd:存 Milvus 的元数据(Collection Schema、索引配置、段信息)。类比 MySQL 的 information_schema。
- MinIO:存 Milvus 的索引文件、日志和 binlog。类比 MySQL 的 ibdata 文件。
3.3.3 端口与网络边界
所有本地组件都绑定在127.0.0.1,不暴露到公网,安全性由操作系统网络栈保证。
3.4 技术栈详解
3.5 八大业务场景
项目内置 8 个行业场景,共享同一套核心引擎:3.6 核心模块一览
4. 环境搭建与部署
4.1 前置依赖
项目只保留两个环境模板,先按运行模式选一个,不要混用:
仓库不再保留通用
.env.example,因为这个名字无法表达“容器内视角”还是“宿主机视角”,容易把 Milvus/MySQL 地址和模型路径写错。
4.2 推荐启动路径:全 Docker 验收
第 1 讲优先使用全 Docker 模式:MySQL、Milvus 和 FastAPI API 都由 Docker Compose 管理。这样不需要同时处理“宿主机进程”和“容器网络”两套视角,启动顺序也能交给脚本统一处理。4.2.1 部署拓扑
全 Docker 模式和本机 API 调试模式最容易混淆的是地址和模型路径:
原因很简单:API 如果在宿主机上运行,访问的是宿主机暴露端口;API 如果在 Compose 网络里运行,访问的是容器服务名。
4.2.2 首次启动
进入项目根目录后,先生成 Compose 模式配置。仓库只提交.env.compose.example,真正运行
docker compose --env-file .env.compose ... 前,必须先生成本地 .env.compose:
localhost:
.env.compose,脚本会先从 .env.compose.example 生成文件并退出,提示你填写
DASHSCOPE_API_KEY 和 ADMIN_API_TOKEN。填好后再运行一次同一条命令即可。
如果要一次性初始化 8 个冻结业务场景:
4.2.3 手动等价命令
如果不用部署脚本,也可以按下面的手动顺序执行。第一遍只初始化当前 active 场景:--reset-collections:
--force 只表示忽略文件指纹、强制重新写入数据;它不会修改已经存在的 Milvus
Collection Schema。只有 --reset-collections 才会删除旧 collection,让入库链路按当前
代码重新创建 text + dense + sparse + BM25 Function 的 Hybrid Search 结构。
4.2.4 访问和验收
查看容器状态和 API 日志:Runtime preflight passed 后,说明 API 已经通过启动前置校验。
浏览器访问:
- 问答页:http://127.0.0.1:8000/
- 状态页:http://127.0.0.1:8000/admin
- 文档页:http://127.0.0.1:8000/docs
- 健康检查:http://127.0.0.1:8000/health
./site 挂载到 API 容器的 /app/site。修改 docs/ 或
docs/animation/ 后,在宿主机执行下面命令即可刷新 /docs,不需要为了讲义内容重建 API 镜像:
4.3 可选路径:本机 API 调试
本机 API 调试适合改 Python 代码时使用:MySQL、Milvus 仍由 Docker 管理,FastAPI 在宿主机上用uvicorn 启动。第一遍验收不建议从这条路径开始。
4.3.1 准备本机模式配置
4.3.2 启动依赖并安装依赖
4.3.3 构建知识库并启动 API
4.4 配置变更后如何重启
.env.compose 是 API 容器启动时读取的环境文件。修改 .env.compose 后,不能只执行 docker compose restart api。 restart 通常只重启已有容器,旧容器创建时注入的环境变量可能不会重新加载。
正确做法是重新创建 API 容器:
.env.compose 变更涉及镜像构建相关内容,或者你同时修改了 Dockerfile、requirements.txt、依赖包版本,需要重新构建镜像:
验证
.env.compose 是否已生效:
- 只改
.env.compose不重建 API 容器:页面还在使用旧配置。 - 把生产
.env.compose写成 localhost:API 容器内的localhost指向 API 容器自己,不是宿主机,也不是 Milvus/MySQL 容器。 - 改了
ACTIVE_SCENARIO_ID但没构建该场景知识库:启动前置校验会失败,提示没有 active KB 版本。 - 改了
API_PORT但访问旧端口:API_PORT控制宿主机端口映射,变更后要重新创建容器并访问新端口。
127.0.0.1 替换成虚拟机 IP,并同步调整 .env.compose 中的 CORS_ALLOW_ORIGINS。
如果使用本机 API 调试模式,修改 .env 后直接停止并重新启动 uvicorn 进程;如果修改了 Python 依赖,再执行一次 pip install -r requirements.txt。
4.5 启动前置校验的工作机制
Milvus、MySQL、本地模型、LLM Key、场景配置和 active 知识库版本都是启动必需条件,任一缺失直接启动失败。5. 本讲实践闭环
通过标准:服务容器处于 running/healthy,页面可访问,API 冒烟请求能返回正常响应。
5.1 本讲从 0 到 1 实现闭环
本讲不展开业务算法,重点是把项目运行骨架搭起来。这里先完成“能启动、能访问、能看日志”,不要在第 1 讲就实现完整 RAG。- 先准备
docker-compose.yml,定义 api、mysql、milvus、etcd、minio。 - 再准备
.env.compose,配置 LLM、Milvus、MySQL、active 场景。 - 然后写最小
app.py,提供/health或首页访问。 - 最后挂载静态页面,确认前端能请求 API。
docker-compose.yml。
.env.compose.example 和本地 .env.compose。
app.py。
建议先只做“能启动、能访问、能看日志”,不要在第 1 讲就实现完整 RAG。
6. 重点掌握
7. 本讲小结
- RAG = 检索 + 生成,让 LLM 能够基于外部知识库回答,解决幻觉和私有知识问题
- 向量检索通过语义相似度(而非关键词匹配)找到相关文档
- 本项目采用 FastAPI + LangChain + Milvus + BGE-M3 + DashScope 的技术栈
- 启动前需要完整的环境配置,项目不做技术降级

