Skip to main content

项目概述与环境搭建

Written: 2026.06
🎬 推荐:在学习本讲之前或之后,观看 RAG Pipeline 执行流程动画演示 建立对系统整体执行流程的直观认识。

1. 本讲目标

  • 理解 RAG 系统的基本概念和应用场景
  • 了解本项目的整体架构和技术栈
  • 完成开发环境的搭建和验证

2. 前置知识

2.1 什么是 RAG(检索增强生成)

RAG = Retrieval-Augmented Generation,即”检索增强生成”。 在没有 RAG 之前,大语言模型(LLM)存在几个核心问题:
  1. 知识截止日期:模型训练完成后,无法获取训练数据之后的新信息。例如 GPT-4 的知识截止到 2023 年某月,之后发生的事情它不知道。
  2. 幻觉问题:当模型不确定某个答案时,它可能会”编造”一个看起来很合理但实际上是错误的内容。这在企业场景中是不可接受的。
  3. 私有知识无法覆盖:企业内部的制度、流程、业务文档是私有数据,从未进入过公开训练集,模型自然无法回答。
RAG 的解决思路非常简单:
可以把 RAG 理解成”开卷考试”:LLM 不再只靠记忆回答,而是可以查阅我们提供的资料后再作答。 RAG 的核心价值
  • 答案是可溯源的(每个回答都能追溯到具体的文档片段)
  • 知识可以实时更新(更新知识库不需要重新训练模型)
  • 幻觉大幅减少(模型被约束在提供的文档范围内回答)

2.2 RAG 系统的基本组成

一个完整的 RAG 系统包含两条核心链路: 对比:传统 LLM vs RAG 离线链路(入库)
  1. 文档加载:读取 PDF、Markdown、Word 等各种格式的文档
  2. 文档切分:把长文档按语义边界切成小块(chunk),每块通常是几百到一千字
  3. 向量化:用 Embedding 模型把每个 chunk 转成一串数字(向量),表达其语义
  4. 存储:把向量和原文一起存入向量数据库
在线链路(问答)
  1. 意图识别:判断用户问的是什么类型的问题
  2. 查询向量化:把用户问题转成向量
  3. 语义检索:在向量数据库中找最相似的文档片段
  4. 上下文构建:把检索到的片段整理成 LLM 的参考资料
  5. 答案生成:LLM 基于资料生成回答

2.3 向量和向量检索是什么

这是理解 RAG 最关键的概念。我们通过一个类比来理解: 类比:图书馆找书 传统关键词搜索(比如 MySQL LIKE 查询)就像你告诉图书管理员”我要找书名里有’Python’的书”。问题在于:
  • 一本叫《Python 机器学习实战》的书会被找到
  • 但一本叫《用编程语言做数据分析》的书不会被找到,虽然它也讲 Python
向量检索就像你告诉图书管理员”我要找一本关于’用代码分析数据’的书”。图书管理员理解了这个概念的”语义”,然后去书架上找到《Python 机器学习实战》、《R 语言统计分析》、《数据科学入门》等语义相近的书。 技术层面
  • 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 调度。它提供两个问答相关核心方法:
QAService 是进程级单例(应用启动时创建一次,全局复用),但不保存任何请求级状态——所有变化的数据都在方法局部变量中,多用户并发不会互相覆盖。 ② Intent — 意图识别(检索准备中的业务判断) 在线主链路的第一个决策点是 decide_route():先处理问候、转人工、越界、场景边界和 FAQ 精确命中。只有 route=retrieval 时,才进入 classify_intent() 做检索类意图识别。Intent 模块回答三个问题:
  1. 这是什么类型的问题?(问候 / 标准问答 / 知识咨询 / 追问 / 人工客服 / 越界)
  2. 能不能直接回答?(问候直接回”你好”,越界直接拒答)
  3. 如果不能直接回答,后续该怎么处理?(改写成独立问题?FAQ 优先还是文档优先?)
检索类意图识别的核心策略是 规则优先 + LLM 补充
这一步的决策结果会影响后面的所有步骤:检索计划、查询改写、Prompt 模板选择、FAQ 直出阈值。 ③ Retrieval — 检索系统(查什么、怎么查) Retrieval 模块负责和 Milvus 打交道,它封装了:
  • MilvusHybridStore:连接管理、Collection 操作、add_documents / search
  • 检索计划(RetrievalPlan):把意图转成具体参数——FAQ 查几条、Doc 查几条、阈值多高、是否 rerank
  • 过滤表达式:把 scenario_idkb_versiontenant_iddataset_idvisibilitysource 拼成 Milvus expr,保证不会跨场景、跨版本混查
  • 重排序(Reranker):用 BGE Reranker CrossEncoder 对召回结果精排
  • 去重:FAQ 和 Doc 各自的去重逻辑
检索不是”一把梭”,而是分层策略:先查 FAQ(高频确定答案),FAQ 不命中再查文档(需要整合资料),两者分开决策、分开返回。 ④ Pipeline — RAG 主流程(流水线) Pipeline 是问答的”流水线车间”,包含五个关键环节: ⑤ Prompts — 提示词工程(给 LLM 的指令) 不同的意图和问题类别需要不同的 System Prompt。例如:
  • faq_answer:严格用标准答案回答,不要自行发挥
  • knowledge_answer:基于提供的文档资料整合回答
  • follow_up:结合对话历史理解用户追问的上下文
  • default:通用问答模板
Prompt 选择由 build_answer_prompt_profile 根据意图和问题类别自动决定,而不是 hardcode 一个通用模板。 ⑥ Memory — 聊天记忆(上下文连续性) Memory 模块管理两件事:
  • 聊天历史(HistoryStore):基于 LangChain 的 SQLChatMessageHistory,每次问答后写入 MySQL 的 chat_messages 表。下次提问时读取最近 N 条消息 + 历史摘要,让模型知道”刚才在聊什么”
  • 反馈(FeedbackStore):用户点赞/点踩后写入 MySQL,用于后续 bad case 分析和评测集补充
⑦ Governance — 知识库治理(版本与隔离) Governance 模块保证不同场景、不同版本、不同权限的数据不会”串门”:
  • kb_version:每个 FAQ 和 chunk 都标记了入库版本号。在线检索时自动拼入 kb_version == "xxx" 过滤条件,确保只查 active 版本
  • data_scope:每条数据还有 tenant_iddataset_idvisibilityallowed_roles 字段。检索时拼成 Milvus 表达式,实现租户级数据隔离
⑧ Indexing — 文档入库(离线链路) Indexing 模块不在在线问答链路中执行(太慢),而是通过离线脚本触发:
入库时还会做增量判断:通过文件 fingerprint(哈希指纹)检查内容是否变化,没变化的跳过,有变化的先删旧 chunk 再写新的。

3.2.5 外部依赖 — 三个独立服务各管什么

全景架构图底部有三个圆柱形节点([(...)]),代表独立部署的外部服务: Milvus — 向量数据库
每次入库时,BGE-M3 为每个 chunk 同时生成两种向量——Dense(语义向量,1024 维浮点数)和 Sparse(关键词权重,由 Milvus 内置 BM25 函数生成)。检索时两种向量一起查,兼顾语义相近和关键词命中。 MySQL — 关系型数据库
MySQL 不承担知识检索。FAQ 和文档的检索全在 Milvus 中进行。MySQL 的角色非常单一——“聊天记录存储”。这个边界很重要,避免了”用 MySQL LIKE 模糊查询冒充语义检索”的情况。 DashScope LLM — 大语言模型
项目中有两处调用 LLM:意图识别不确定时的结构化输出(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 系统的共性。
两条特殊路径(图中箭头覆盖但上面没走到的):
  1. 问候/越界快速通道User → /api/stream → chat.py → QAService.stream_query → decide_route() 在主链路内直接产出答案事件,后面的检索准备、检索和 LLM 全部跳过。耗时取决于运行环境和限流/数据库状态,通常远低于完整 RAG。
  2. FAQ 直出通道:分两种情况:Stage 1 的 route=faq_exact 只允许标准问题精确匹配;Stage 3 的 FAQ 标准直出则发生在检索准备之后,允许精确匹配或达到动态阈值。两者都会返回 metadata.answer,不进入 Doc 检索和 LLM 生成。
  3. 追问改写通道:用户说”那审批呢”→ 意图识别为 FOLLOW_UP → Pipeline 读取历史,把”入职流程中的审批步骤”补全 → 后续流程和普通 RAG 一样。

3.3 部署架构图

3.3.1 部署拓扑解读

部署架构图将整个系统划分为四个物理区域,每个区域的部署方式和选型理由各不相同:

3.3.2 六条连接线逐一解读

图中的每一条箭头都是一条运行时依赖,按调用频率和延迟敏感度排列: ① FastAPI → Milvus(高频 · 延迟敏感)
这是调用最频繁的外部依赖。每次用户提问,FastAPI 都要把问题向量化后发给 Milvus 做语义检索。部署在本地 Docker 是因为延迟必须可控——如果 Milvus 在云端,每次检索多 50-100ms 网络延迟,用户体验会明显变差。 ② FastAPI → MySQL(中频 · 延迟不敏感)
MySQL 只存聊天记录和反馈。部署在本地 Docker 同样是为了避免网络延迟累积——虽然单次 MySQL 查询很快(<5ms),但每次问答要读写多次,走公网会显著拖慢。 ③ FastAPI → BGE-M3(高频 · 极度延迟敏感)
BGE-M3 是 CPU/GPU 本地推理,不走网络。为什么必须本地?因为 Embedding 调用极其高频——入库时每个 chunk 都要向量化,在线问答时每个 query variant 都要向量化。如果走 API,不仅延迟不可控,API 调用费用也会非常高。而且,私有文档内容发送给第三方 Embedding 服务本身就有数据安全风险。 ④ FastAPI → BGE Reranker(中频 · 延迟敏感)
Reranker 用 CrossEncoder 架构对 Milvus 召回的候选文档逐对打分。这个环节对精度影响最大——同样的召回结果,Rerank 前后 MRR 可能差 10-15 个百分点。本地部署保证了延迟和精度都可控。 ⑤ FastAPI → DashScope LLM(低频 · 延迟最高)
LLM 是整个链路中唯一部署在云端的组件,也是最慢的环节(首 token 延迟 ~2500ms)。为什么 LLM 可以走云端而 Embedding 不行?
  • LLM 推理需要 GPU 显存(至少 16GB+),本地开发机通常跑不动
  • LLM 调用频率相对低(每次问答 1-2 次),不像 Embedding 那样一次入库就调用上百次
  • API 调用按 token 计费,成本可控
  • LLM 是文本进文本出,不涉及向量数据,传输量小
⑥⑦ Milvus → etcd / MinIO(内部依赖 · 用户无感)
  • 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
至少确认这些项已经改成真实值:
同时确认 Compose 模式地址保持为容器服务名,不要改成 localhost
确认模型目录存在:
推荐直接使用项目提供的部署脚本。它会按顺序启动 MySQL/Milvus、构建 API 镜像、重建并激活知识库,然后启动 API:
如果你没有提前创建 .env.compose,脚本会先从 .env.compose.example 生成文件并退出,提示你填写 DASHSCOPE_API_KEYADMIN_API_TOKEN。填好后再运行一次同一条命令即可。 如果要一次性初始化 8 个冻结业务场景:
这个顺序不能反过来,因为 API 启动前会检查 active KB 版本;空库直接启动 API 会被 preflight 拒绝。

4.2.3 手动等价命令

如果不用部署脚本,也可以按下面的手动顺序执行。第一遍只初始化当前 active 场景:
新环境首次部署,如果要一次性初始化全部 8 个冻结业务场景,把单场景命令替换为:
如果之前已经存在知识库,只是资料内容变化,批量刷新全部 8 个场景时不要删除 collection:
如果只重建一个已有场景,例如企业知识场景:
只有在新环境全量初始化、旧 collection schema 不兼容,或明确要清空重建单场景 collection 时,才追加 --reset-collections
注意:--force 只表示忽略文件指纹、强制重新写入数据;它不会修改已经存在的 Milvus Collection Schema。只有 --reset-collections 才会删除旧 collection,让入库链路按当前 代码重新创建 text + dense + sparse + BM25 Function 的 Hybrid Search 结构。

4.2.4 访问和验收

查看容器状态和 API 日志:
看到 Runtime preflight passed 后,说明 API 已经通过启动前置校验。 浏览器访问: 执行接口冒烟测试:
讲义站点由宿主机 ./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

启动成功后会看到类似:
另开一个 PowerShell 窗口执行验收:

4.4 配置变更后如何重启

.env.compose 是 API 容器启动时读取的环境文件。修改 .env.compose 后,不能只执行 docker compose restart api restart 通常只重启已有容器,旧容器创建时注入的环境变量可能不会重新加载。 正确做法是重新创建 API 容器:
如果 .env.compose 变更涉及镜像构建相关内容,或者你同时修改了 Dockerfilerequirements.txt、依赖包版本,需要重新构建镜像:
如果只修改了这些内容,通常不需要重建镜像,只需要重新创建 API 容器: 验证 .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。
  1. 先准备 docker-compose.yml,定义 api、mysql、milvus、etcd、minio。
  2. 再准备 .env.compose,配置 LLM、Milvus、MySQL、active 场景。
  3. 然后写最小 app.py,提供 /health 或首页访问。
  4. 最后挂载静态页面,确认前端能请求 API。
实现完成后,相关运行底座结构应该是下面这张图: 来源:真实配置文件,见 docker-compose.yml
来源:真实配置文件,见 .env.compose.example 和本地 .env.compose
来源:真实代码调用点,见 app.py
来源:命令行验收,对应 Docker Compose 启动流程。
闭环验证重点: 建议先只做“能启动、能访问、能看日志”,不要在第 1 讲就实现完整 RAG。

6. 重点掌握

7. 本讲小结

  • RAG = 检索 + 生成,让 LLM 能够基于外部知识库回答,解决幻觉和私有知识问题
  • 向量检索通过语义相似度(而非关键词匹配)找到相关文档
  • 本项目采用 FastAPI + LangChain + Milvus + BGE-M3 + DashScope 的技术栈
  • 启动前需要完整的环境配置,项目不做技术降级
下一讲RAG 核心概念深入 — 向量检索原理、Embedding 模型、混合检索策略