跳转至
RAG基础与环境项目概述与 Docker 环境搭建

第 1 章:项目概述与 Docker 环境搭建

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

本章目标

  • 理解 RAG 系统的基本概念和应用场景
  • 了解本项目的整体架构和技术栈
  • 理解 Docker/Compose 在本项目中的运行底座作用
  • 完成开发环境的搭建和验证

第一部分:前置知识

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

RAG = Retrieval-Augmented Generation,即"检索增强生成"。

在没有 RAG 之前,大语言模型(LLM)存在几个核心问题:

  1. 知识截止日期:模型训练完成后,无法获取训练数据之后的新信息。例如 GPT-4 的知识截止到 2023 年某月,之后发生的事情它不知道。
  2. 幻觉问题:当模型不确定某个答案时,它可能会"编造"一个看起来很合理但实际上是错误的内容。这在企业场景中是不可接受的。
  3. 私有知识无法覆盖:企业内部的制度、流程、业务文档是私有数据,从未进入过公开训练集,模型自然无法回答。

RAG 的解决思路非常简单:

用户提问 → 先从知识库中检索相关文档 → 把检索到的文档和问题一起发给 LLM → LLM 基于文档生成答案

可以把 RAG 理解成"开卷考试":LLM 不再只靠记忆回答,而是可以查阅我们提供的资料后再作答。

RAG 的核心价值: - 答案是可溯源的(每个回答都能追溯到具体的文档片段) - 知识可以实时更新(更新知识库不需要重新训练模型) - 幻觉大幅减少(模型被约束在提供的文档范围内回答)

1.2 RAG 系统的基本组成

一个完整的 RAG 系统包含两条核心链路:

flowchart LR
    subgraph Offline["离线链路(入库)"]
        A1["📄 文档加载<br/>PDF/MD/Word/Excel"] --> A2["✂️ 文档切分<br/>父子块策略"]
        A2 --> A3["🧮 向量化<br/>BGE-M3 Embedding"]
        A3 --> A4["💾 向量存储<br/>Milvus Collection"]
    end

    subgraph Online["在线链路(问答)"]
        B1["❓ 用户提问"] --> B2["🎯 意图识别"]
        B2 --> B3["🔍 语义检索<br/>Dense + Sparse Hybrid"]
        B3 --> B4["📊 重排序<br/>BGE Reranker"]
        B4 --> B5["📝 上下文构建"]
        B5 --> B6["🤖 LLM 生成<br/>DashScope 流式输出"]
    end

    A4 -.->|"检索"| B3
    B6 --> C["✅ 可溯源答案"]

    style Offline fill:#EFF6FF,stroke:#3B82F6,stroke-width:2px
    style Online fill:#ECFDF5,stroke:#059669,stroke-width:2px

对比:传统 LLM vs RAG

flowchart TD
    subgraph Traditional["传统 LLM(仅靠记忆)"]
        T1["❓ 用户提问"] --> T2["🧠 LLM 回忆训练数据"]
        T2 --> T3["⚠️ 可能幻觉<br/>知识有截止日期<br/>无法回答私有知识"]
    end

    subgraph RAG["RAG(开卷考试)"]
        R1["❓ 用户提问"] --> R2["🔍 检索知识库"]
        R2 --> R3["📋 获取相关文档片段"]
        R3 --> R4["🧠 LLM 基于资料回答"]
        R4 --> R5["✅ 有据可查的答案"]
    end

    style Traditional fill:#FEF2F2,stroke:#DC2626,stroke-width:2px
    style RAG fill:#EFF6FF,stroke:#2563EB,stroke-width:2px

离线链路(入库): 1. 文档加载:读取 PDF、Markdown、Word 等各种格式的文档 2. 文档切分:把长文档按语义边界切成小块(chunk),每块通常是几百到一千字 3. 向量化:用 Embedding 模型把每个 chunk 转成一串数字(向量),表达其语义 4. 存储:把向量和原文一起存入向量数据库

在线链路(问答): 1. 意图识别:判断用户问的是什么类型的问题 2. 查询向量化:把用户问题转成向量 3. 语义检索:在向量数据库中找最相似的文档片段 4. 上下文构建:把检索到的片段整理成 LLM 的参考资料 5. 答案生成:LLM 基于资料生成回答

1.3 向量和向量检索是什么

这是理解 RAG 最关键的概念。我们通过一个类比来理解:

类比:图书馆找书

传统关键词搜索(比如 MySQL LIKE 查询)就像你告诉图书管理员"我要找书名里有'Python'的书"。问题在于: - 一本叫《Python 机器学习实战》的书会被找到 - 但一本叫《用编程语言做数据分析》的书不会被找到,虽然它同样涉及 Python

向量检索就像你告诉图书管理员"我要找一本关于'用代码分析数据'的书"。图书管理员理解了这个概念的"语义",然后去书架上找到《Python 机器学习实战》、《R 语言统计分析》、《数据科学入门》等语义相近的书。

技术层面: - Embedding 模型把一段文本(比如一句话、一个段落)转换成一个固定长度的浮点数数组,例如 1024 维的向量 - 语义相近的文本,它们的向量在数学空间中的"距离"也近 - 向量数据库就是专门存储和检索这些向量的系统

# 伪代码示例
文本1 = "Python 是一门编程语言"
文本2 = "Java 也是一门编程语言"
文本3 = "今天天气很好"

向量1 = embedding(文本1)  # [0.12, 0.34, -0.56, ...] (1024个数字)
向量2 = embedding(文本2)  # [0.11, 0.33, -0.54, ...] (与向量1很接近)
向量3 = embedding(文本3)  # [0.89, -0.21, 0.67, ...] (与向量1差异很大)

# 向量1和向量2的余弦相似度 ≈ 0.95(很高)
# 向量1和向量3的余弦相似度 ≈ 0.12(很低)

第二部分:项目架构

2.1 一句话描述

这是一个基于 LangChain + Milvus 2.5 Hybrid Search 的多场景 RAG 平台。不是简单的 Demo,而是补齐了企业级 RAG 完整工程闭环的项目。

2.2 全景架构图

flowchart TD
    User["🖥️ 浏览器用户<br/>static/index.html"] --> Page["GET /api/scenarios"]
    User --> Sources["GET /api/sources"]
    User --> KbList["GET /api/kb_versions"]
    User --> CreateSession["POST /api/create_session"]
    User --> Stream["WebSocket /api/stream"]
    User --> HistoryApi["GET /api/history/{id}"]
    User --> ClearHistoryApi["DELETE /api/history/{id}"]
    User --> FeedbackApi["POST /api/feedback"]

    AdminPage["🖥️ 状态页<br/>static/admin.html"] --> AdminApi["GET /api/admin/*"]

    subgraph FastAPI["FastAPI (app.py)"]
        Router["路由层 qa_core/api"]
        Pages["pages.py<br/>页面/健康检查"]
        Chat["chat.py<br/>问答/历史/反馈"]
        Admin["admin.py<br/>管理接口"]
        KbVersions["kb_versions.py<br/>版本管理"]
    end

    Stream --> Chat
    AdminApi --> Admin

    subgraph Core["qa_core 核心引擎"]
        QAService["QAService<br/>服务编排层"]
        Intent["intent<br/>意图识别"]
        Retrieval["retrieval<br/>Milvus Hybrid Search"]
        Pipeline["pipeline<br/>RAG 主流程"]
        Prompts["prompts<br/>Prompt Profile"]
        Memory["memory<br/>聊天历史"]
        Governance["governance<br/>版本/隔离"]
        Indexing["indexing<br/>文档入库"]
    end

    Chat --> QAService
    QAService --> Intent
    QAService --> Retrieval
    QAService --> Pipeline
    QAService --> Prompts
    QAService --> Memory
    QAService --> Governance

    Retrieval --> Milvus[("Milvus<br/>Dense + Sparse<br/>Hybrid Search")]
    Memory --> MySQL[("MySQL<br/>Chat History<br/>Feedback")]

    Indexing --> Milvus

    Prompts --> LLM["🤖 DashScope LLM<br/>OpenAI 兼容接口"]
    Pipeline --> LLM

    style FastAPI fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
    style Core fill:#F8FAFC,stroke:#64748B,stroke-width:2px
    style Milvus fill:#ECFDF5,stroke:#059669,stroke-width:2px
    style MySQL fill:#FFFBEB,stroke:#D97706,stroke-width:2px
    style LLM fill:#FEF2F2,stroke:#DC2626,stroke-width:2px

2.2.1 架构分层解读

全景架构图从上到下可以划分为 五个层次,每一层各司其职:

层次 名称 包含组件 一句话职责
第一层 用户入口层 浏览器用户(问答页)、状态页(admin 页) 用户看得见、点得到的地方
第二层 路由层 FastAPI qa_core/api(pages / chat / admin / kb_versions) 把 HTTP/WebSocket 请求分发给正确的后端模块
第三层 核心引擎层 qa_core 八大模块(QAService、Intent、Retrieval 等) 所有 RAG 业务逻辑的真正执行者
第四层 外部依赖层 Milvus、MySQL、DashScope LLM 提供向量检索、会话存储、文本生成能力
第五层 入库链路(离线) Indexing → Milvus 文档和 FAQ 怎么进知识库

为什么要分层? 每一层只跟相邻层打交道:浏览器不直接调 Milvus,API 路由不直接拼 Prompt,核心引擎不直接写 HTTP 响应。这保证了任何一层被替换时,其他层不受影响。

2.2.2 第一层:用户入口 — 问答页和状态页各做什么

全景架构图的顶部有两个入口角色:

问答页(static/index.html 是面向普通用户的交互界面,它会发出以下常用请求:

端点 方法 触发时机 作用
GET /api/scenarios HTTP 页面加载时 拉取所有可用业务场景列表(下拉框的数据来源)
GET /api/sources HTTP 切换业务场景时 拉取当前场景可选的 source 过滤项
GET /api/kb_versions HTTP 切换业务场景时 查看当前场景的知识库版本状态
POST /api/create_session HTTP 用户选择场景后 创建新会话,返回 session_id(后续所有问答都绑定这个 ID)
WebSocket /api/stream WebSocket 用户每次发送问题 走完整 RAG 流式问答链路,逐 token 推送答案
GET /api/history/{id} HTTP 用户刷新页面或切换会话 恢复之前的聊天记录
DELETE /api/history/{id} HTTP 用户清空会话时 删除该会话历史和摘要
POST /api/feedback HTTP 用户点赞/点踩 记录用户对某个回答的满意度

关键理解:这些端点里,只有 /api/stream 是 WebSocket 长连接——它承载了最重的 RAG 逻辑。其他端点都是轻量 HTTP 请求,负责页面初始化、历史、反馈、版本或过滤项查询。

状态页(static/admin.html 是面向开发者/管理员的诊断界面,它通过 GET /api/admin/* 访问 LangSmith 状态、active 版本、入库质量报告和回归报告入口。状态页不参与在线问答,只做"事后排查"。

2.2.3 第二层:FastAPI 路由层 — 请求如何分发

app.py 只做四件事:创建 FastAPI 应用、配置 CORS、启动时执行环境校验、注册路由。业务逻辑全在路由模块里:

路由模块 文件 承接的请求
pages.py qa_core/api/pages.py 页面渲染(GET /GET /admin)、健康检查(GET /health)、创建会话
chat.py qa_core/api/chat.py 主链路:WebSocket 流式问答、历史查询、反馈、检索诊断
admin.py qa_core/api/admin.py 管理接口:LangSmith 状态、入库报告、回归报告入口、回归状态
kb_versions.py qa_core/api/kb_versions.py 知识库版本查看、回滚、归档

关键设计决策:在线问答统一走 WebSocket(/api/stream),不再提供额外的 HTTP 问答入口。如果 HTTP 和 WebSocket 两套问答入口并存,容易出现"HTTP 返回的结果和 WebSocket 不一样"的不一致问题。问候、越界、人工客服短句这些无需检索的问题,也在 WebSocket 主链路的意图识别阶段直接返回。

2.2.4 第三层:qa_core 核心引擎 — 八大模块如何协作

这是整个项目的大脑。图表中的八个模块各自承担独立的职责,通过 QAService(服务编排层)统一调度:

① QAService — 服务编排层(总指挥)

QAService 是唯一对外的业务入口。路由层的 chat.py 不直接调 Intent、不直接拼 Prompt,一切通过 QAService 调度。它提供两个问答相关核心方法:

# 流式主链路:完整 RAG,通过 Generator 逐事件产出
service.stream_query(query, source_filter, session_id, ...)

# 检索诊断:只查不生成,用于调试和评测
service.debug_retrieval(query, source_filter, session_id, ...)

QAService 是进程级单例(应用启动时创建一次,全局复用),但不保存任何请求级状态——所有变化的数据都在方法局部变量中,多用户并发不会互相覆盖。

② Intent — 意图识别(检索准备中的业务判断)

在线主链路的第一个决策点是 decide_route():先处理问候、转人工、越界、场景边界和 FAQ 精确命中。只有 route=retrieval 时,才进入 classify_intent() 做检索类意图识别。Intent 模块回答三个问题:

  1. 这是什么类型的问题?(问候 / 标准问答 / 知识咨询 / 追问 / 人工客服 / 越界)
  2. 能不能直接回答?(问候直接回"你好",越界直接拒答)
  3. 如果不能直接回答,后续该怎么处理?(改写成独立问题?FAQ 优先还是文档优先?)

检索类意图识别采用 规则候选 + BERT 模型增强 + 决策网关仲裁 + 知识查询兜底

在线主链路顺序:
1. decide_route() 先收口 direct_answer / faq_exact / retrieval
2. route=retrieval 后加载历史并 classify_intent()
3. 规则层生成 FOLLOW_UP / FAQ_QUERY / KNOWLEDGE_QUERY 候选
4. BERT 模型预测检索类意图及候选分布
5. 决策网关按策略采纳、纠偏或保护规则结果
6. 两侧都不能可靠区分时           →  KNOWLEDGE_QUERY 保守兜底

这一步的决策结果会影响后面的所有步骤:检索计划、查询改写、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 是问答的"流水线车间",包含五个关键环节:

环节 触发条件 做了什么
查询改写(rewrite) 意图为 FOLLOW_UP 时 "那审批呢" → "入职流程中的审批步骤是什么",把省略的主语和背景补全
查询变体(query_variants) 检索计划启用时,主要用于知识查询和追问 原问题 + 等价问法(规则命中本地生成,否则 LLM 生成),提高召回覆盖
上下文构建(context) Doc RAG 时有多个 chunk FAQ 前 2 条 + Doc 得分达标片段,按 [1] 来源 + 内容 格式拼接
事件生成(events) 流式问答全程 产出 start → status → token... → end 事件序列,前端按 type 渲染
引用标注(citations) Doc RAG 时 为每个来源片段标注文件名和 chunk 位置

⑤ 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 / version_seq:FAQ 按 kb_version == active_version 精确过滤;文档 chunk 按 active version_seq 解释 valid_from_seq/valid_to_seq 有效期窗口,支持引用式增量和快速回滚
  • data_scope:每条数据还有 tenant_iddataset_idvisibilityallowed_roles 字段。检索时拼成 Milvus 表达式,实现租户级数据隔离

⑧ Indexing — 文档入库(离线链路)

Indexing 模块不在在线问答链路中执行(太慢),而是通过离线脚本触发:

文档/FAQ → 加载(按后缀选 loader)→ normalize(补 metadata)→ 切分(父子块策略)→ 向量化(BGE-M3)→ 写入 Milvus

入库时还会做增量判断:同版本内通过文件 fingerprint 跳过未变化文件;跨版本增量构建时,未变化文档引用旧 chunk,修改/删除文档通过 valid_to_seq 从目标版本开始失效。

2.2.5 第四层:外部依赖 — 三个独立服务各管什么

全景架构图底部有三个圆柱形节点([(...)]),代表独立部署的外部服务:

Milvus — 向量数据库

职责:存储 Dense 向量 + Sparse 向量,支持 Hybrid Search
部署:Docker Compose 中的 milvus 容器
端口:19530
依赖:etcd(元数据)+ MinIO(索引和日志文件)

每次入库时,BGE-M3 在客户端生成 Dense 语义向量;Milvus 的 BM25 Built-in Function 根据原始文本生成 Sparse 关键词向量。检索时两路向量一起查询,兼顾语义相近和关键词命中。

MySQL — 关系型数据库

职责:保存聊天与反馈、知识库版本控制面、文档清单、缓存命名空间等结构化数据
部署:Docker Compose 中的 MySQL 容器
端口:3306

MySQL 不承担 FAQ/文档语义检索。知识召回在 Milvus 中完成;MySQL 负责事务型控制面和业务元数据,例如版本状态、active 指针、文档清单、聊天历史、反馈与缓存 epoch。这个边界避免了用关系库模糊查询替代向量检索,也让版本治理和数据写入具备事务能力。

DashScope LLM — 大语言模型

职责:查询变体生成(结构化输出)+ 答案生成(流式输出)
接口:OpenAI 兼容 API(通过 LangChain ChatOpenAI 统一调用)
部署:阿里云 DashScope 云端服务

项目主链路采用分层职责:确定性规则先处理直答与安全边界,检索类意图由规则候选、BERT 模型和决策网关共同确定,必要时以知识查询保守兜底;查询变体生成使用结构化输出提升召回覆盖;最终答案生成使用 llm.stream 支持 WebSocket 流式返回。BERT 意图模型是独立的本地分类模型,不经过 get_chat_model();查询变体和答案生成才使用 ChatModel 工厂。

2.2.6 完整问答链路走读:一次用户提问经历了什么

把全景架构图的箭头串起来,就是一次完整问答请求的真实轨迹。以下用"入职流程有哪些步骤"这个提问来走一遍:

sequenceDiagram
    actor User as 用户 / Browser
    participant WS as WebSocket<br/>(/api/stream)
    participant Chat as chat.py
    participant QS as QAService
    participant RT as Route<br/>decide_route()
    participant IC as Intent<br/>Classifier
    participant ML as Milvus<br/>(检索/召回)
    participant PL as Pipeline<br/>(rag.py)
    participant LLM as LLM<br/>(DashScope)
    participant Mem as History<br/>/ Memory

    User->>WS: 输入"入职流程有哪些步骤"并点击发送
    WS->>Chat: {"type":"query","content":"入职流程有哪些步骤",<br/>"session_id":"xxx","scenario_id":"enterprise_knowledge"}
    Chat->>QS: stream_query(query, session_id, scenario_id)

    Note over QS: ① resolve_scenario() → 加载场景 TOML 配置
    Note over QS: ② resolve_active_kb_version() → 查当前 active 版本

    QS-->>WS: ③ {"type":"start","session_id":"xxx"}
    WS-->>User: 问答已开始
    QS-->>WS: ④ {"type":"status","message":"正在进行查询路由"}
    WS-->>User: 进度:正在进行查询路由

    QS->>RT: ⑤ decide_route(query)
    RT-->>QS: route=retrieval

    QS-->>WS: ⑥ {"type":"status","message":"正在识别问题意图"}
    WS-->>User: 进度:正在识别问题意图

    QS->>Mem: ⑦ 读取聊天历史(MySQL chat_messages)
    Mem-->>QS: 历史消息列表

    QS->>IC: ⑧ classify_intent(query, history, scenario)
    IC-->>QS: Intent=KNOWLEDGE_QUERY, source=hr_process

    QS->>PL: ⑨ build_retrieval_plan(intent)
    PL-->>QS: FAQ top_k=3, Doc top_k=8, rerank=True

    QS-->>WS: ⑩ {"type":"status","message":"正在检索业务 FAQ 知识库"}
    WS-->>User: 进度:正在检索

    QS->>ML: ⑪ FAQ Hybrid Search(Dense + Sparse)
    ML-->>QS: 0 条命中(FAQ 未命中)

    Note over QS: ⑫ FAQ 未命中 → 进入 Doc RAG

    QS-->>WS: ⑬ {"type":"status","message":"正在匹配相关业务资料"}
    QS->>ML: ⑭ Doc Hybrid Search(Dense + Sparse)
    ML-->>QS: 召回 8 条相关 chunk

    QS->>ML: ⑮ BGE Reranker CrossEncoder 精排
    ML-->>QS: 保留得分最高的 5 条

    QS->>PL: ⑯ prepare_answer() / build_context()
    PL-->>QS: 按 [文件名] + 内容 拼接

    QS->>PL: ⑰ build_answer_prompt_profile(intent, scenario, query)
    PL-->>QS: 选择 knowledge_answer 模板

    QS-->>WS: ⑱ {"type":"status","message":"正在生成回答"}
    WS-->>User: 进度:正在生成回答

    QS->>LLM: ⑲ llm.stream(SystemMessage(模板), HumanMessage(ctx))
    LLM-->>QS: 逐 token 返回
    QS-->>WS: 每 token → {"type":"token","content":"..."}
    WS-->>User: 逐字打印答案

    QS->>PL: ⑳ enforce_answer_citations(answer, context_docs)
    QS->>PL: ㉑ finalize_generated_answer_confidence() → 合并证据分和生成核验
    QS->>Mem: ㉒ add_turn(question, full_answer) → 写入 MySQL
    Note over QS: ㉓ finish_success() → Trace(含 answer_confidence)

    QS-->>WS: ㉔ {"type":"end","sources":[...],"answer_confidence":{...}, ...}
    WS-->>User: 回答结束,展示来源引用和置信度

耗时分布(典型值):意图识别 ~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 一样。

2.3 部署架构图

flowchart LR
    subgraph Host["宿主机"]
        API["🐍 FastAPI App<br/>127.0.0.1:8000"]
    end

    subgraph Docker["Docker Compose"]
        MilvusSvc["🔍 Milvus Standalone<br/>127.0.0.1:19530"]
        Etcd["⚙️ etcd<br/>元数据存储"]
        MinIO["📦 MinIO<br/>索引/日志存储"]
        MySQLSvc["🗄️ MySQL<br/>127.0.0.1:3306"]
        RedisSvc["⚡ Redis<br/>127.0.0.1:6379"]
    end

    subgraph Local["本地模型"]
        BGE["🧮 BGE-M3<br/>Embedding"]
        Reranker["📊 BGE Reranker<br/>CrossEncoder"]
    end

    subgraph Cloud["云服务"]
        DashScope["☁️ DashScope<br/>LLM API"]
    end

    API --> MilvusSvc
    API --> MySQLSvc
    API --> RedisSvc
    API --> BGE
    API --> Reranker
    API --> DashScope
    MilvusSvc --> Etcd
    MilvusSvc --> MinIO

    style Docker fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
    style Local fill:#ECFDF5,stroke:#059669,stroke-width:2px
    style Cloud fill:#FFFBEB,stroke:#D97706,stroke-width:2px

2.3.1 部署拓扑解读

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

区域 位置 包含组件 部署方式 为什么放这里
宿主机 本地 FastAPI App (127.0.0.1:8000) Python 进程直接运行 应用代码需要频繁修改调试,不适合容器化
Docker 本地容器 Milvus + etcd + MinIO + MySQL + Redis Docker Compose 编排 这些是"基础设施",不需要修改代码,容器化能一键启动、统一管理
本地模型 本地磁盘 BGE-M3 + BGE Reranker Python 进程加载本地文件 Embedding 和 Rerank 延迟敏感,不能走网络;且涉及私有数据不适合上传
云服务 阿里云 DashScope LLM API HTTPS 远程调用 LLM 推理需要 GPU 集群,本地跑不动;API 调用按量付费,成本可控

2.3.2 七类连接线逐一解读

图中的每一条箭头都是一条运行时依赖,按调用频率和延迟敏感度排列:

① FastAPI → Milvus(高频 · 延迟敏感)

协议:gRPC(pymilvus)
端口:127.0.0.1:19530
每次问答调用:1-2 次(FAQ 检索 + Doc 检索)
数据量:Dense 向量(1024维) + Sparse 向量 + metadata

这是调用最频繁的外部依赖。每次用户提问,FastAPI 都要把问题向量化后发给 Milvus 做语义检索。部署在本地 Docker 是因为延迟必须可控——如果 Milvus 在云端,每次检索多 50-100ms 网络延迟,用户体验会明显变差。

② FastAPI → MySQL(中频 · 延迟不敏感)

协议:TCP(pymysql / SQLAlchemy)
端口:127.0.0.1:3306
每次问答调用:2-3 次(读历史 + 写消息 + 刷新摘要)
数据量:每轮对话约 1-5KB 文本

MySQL 主要保存聊天记录、反馈和知识库治理控制面数据,例如版本状态、active 指针、文档清单与缓存命名空间。部署在本地 Docker 同样是为了避免网络延迟累积——虽然单次 MySQL 查询很快,但每次问答和治理操作都可能多次读写,走公网会显著拖慢。

③ FastAPI → Redis(高频 · 延迟敏感)

协议:TCP(redis-py)
端口:127.0.0.1:6379
每次问答调用:按缓存命中情况读取或写入
数据量:query embedding 缓存、FAQ/Doc 检索缓存、缓存命名空间状态

Redis 是 V1 三级缓存里的 L2 共享缓存。更准确地说,这套设计是“L1 进程内 epoch 快照 + L2 Redis 业务缓存 + L3 MySQL 治理层”:L1 只缓存短 TTL 的 cache_epoch 快照,L2 存 query embedding 和 FAQ/Doc 检索结果,L3 MySQL 只存缓存命名空间与 epoch,不存检索结果。Redis 放在 Docker Compose 里,目的是让 API 重启后仍能复用短期缓存,并且便于和 MySQL、Milvus 一起由 Compose 管理。

④ FastAPI → BGE-M3(高频 · 极度延迟敏感)

协议:本地函数调用(transformers / sentence-transformers)
每次问答调用:每个 chunk 入库时 1 次 + 每次查询 1 次
数据量:输入文本 → 输出 1024 维 float32 向量

BGE-M3 是 CPU/GPU 本地推理,不走网络。为什么必须本地?因为 Embedding 调用极其高频——入库时每个 chunk 都要向量化,在线问答时每个 query variant 都要向量化。如果走 API,不仅延迟不可控,API 调用费用也会非常高。而且,私有文档内容发送给第三方 Embedding 服务本身就有数据安全风险。

⑤ FastAPI → BGE Reranker(中频 · 延迟敏感)

协议:本地函数调用(CrossEncoder)
每次问答调用:1 次(对召回结果统一排序)
数据量:输入 query+doc 对 → 输出相关性分数(用于候选排序,不等同于最终答案置信度)

Reranker 用 CrossEncoder 架构对 Milvus 召回的候选文档逐对打分。这个环节对精度影响最大——同样的召回结果,Rerank 前后 MRR 可能差 10-15 个百分点。本地部署保证了延迟和精度都可控。

⑥ FastAPI → DashScope LLM(低频 · 延迟最高)

协议:HTTPS(OpenAI 兼容 API)
每次问答调用:0-2 次(意图分类可能需要 1 次 + 答案生成 1 次)
数据量:输入 System Prompt + 上下文 ~2000-4000 token,输出 ~200-800 token

LLM 是整个链路中唯一部署在云端的组件,也是最慢的环节(首 token 延迟 ~2500ms)。为什么 LLM 可以走云端而 Embedding 不行?

  • LLM 推理需要 GPU 显存(至少 16GB+),本地开发机通常跑不动
  • LLM 调用频率相对低(每次问答 1-2 次),不像 Embedding 那样一次入库就调用上百次
  • API 调用按 token 计费,成本可控
  • LLM 是文本进文本出,不涉及向量数据,传输量小

⑦ Milvus → etcd / MinIO(内部依赖 · 用户无感)

协议:gRPC / S3 兼容 API
触发:Milvus 内部读写元数据和索引文件
用户不需要关心这两条线,Docker Compose 自动管理
  • etcd:存 Milvus 的元数据(Collection Schema、索引配置、段信息)。类比 MySQL 的 information_schema。
  • MinIO:存 Milvus 的索引文件、日志和 binlog。类比 MySQL 的 ibdata 文件。

2.3.3 端口与网络边界

flowchart TB
    subgraph Host["宿主机 Host Machine"]
        direction TB
        API["FastAPI 应用<br/>127.0.0.1:8000"]
        LocalModels["BGE-M3 Embedding<br/>BGE Reranker<br/>(本地文件加载)"]

        subgraph Bridge["Docker 桥接网络"]
            direction TB
            Milvus["Milvus Standalone<br/>127.0.0.1:19530"]
            Etcd["etcd<br/>元数据存储"]
            MinIO["MinIO<br/>索引/日志"]
            MySQL["MySQL<br/>127.0.0.1:3306"]
            Redis["Redis<br/>127.0.0.1:6379"]
        end

        API -->|gRPC| Milvus
        API -->|TCP| MySQL
        API -->|TCP| Redis
        Milvus --> Etcd
        Milvus --> MinIO
        API -->|进程内调用| LocalModels
    end

    Host -->|"HTTPS 出站"| DashScope["☁️ DashScope 云端 LLM"]

    style Bridge fill:#EFF6FF,stroke:#2563EB,stroke-width:2px
    style Host fill:#F9FAFB,stroke:#6B7280,stroke-width:2px
    style DashScope fill:#FFFBEB,stroke:#D97706,stroke-width:2px

所有本地组件都绑定在 127.0.0.1,不暴露到公网,安全性由操作系统网络栈保证。

2.4 技术栈详解

层级 技术 为什么选它
API 框架 FastAPI 原生支持异步、WebSocket、自动生成 OpenAPI 文档
RAG 编排 LangChain 开源生态成熟,封装了 ChatModel、VectorStore、MessageHistory 等
向量数据库 Milvus 2.5.x 支持 BGE-M3 Dense + Milvus BM25 Sparse 混合检索
Embedding BGE-M3 中文语义理解能力强,支持本地部署,生成 1024 维 Dense 向量
Sparse 向量 Milvus BM25BuiltInFunction 服务端内置函数,不需要额外部署分词器
Reranker BGE Reranker Large CrossEncoder 架构,对召回结果做精细排序
LLM DashScope (OpenAI 兼容) 通过 LangChain ChatOpenAI 统一调用
会话存储 MySQL LangChain SQLChatMessageHistory 自动管理表结构
缓存 Redis + 进程内缓存 + MySQL 命名空间 支撑 query embedding、FAQ/Doc 检索和版本激活后的缓存失效闭环
配置 .env.compose / .env + scenario.toml 运行时环境变量 + 场景级 TOML 配置

2.5 八大业务场景

项目内置 8 个行业场景,共享同一套核心引擎:

场景 ID 行业 典型问题
enterprise_knowledge 企业内部知识 "入职流程有哪些步骤"
saas_support SaaS 客服 "API 限流导致接口失败怎么排查"
equipment_ops 设备运维 "日检异常怎么升级"
compliance_qa 合规风控 "供应商尽调需要哪些材料"
cross_border_risk 跨境贸易 "HS 归类争议怎么处理"
tender_contract_risk 招投标合同 "合同变更流程是什么"
insurance_claims 保险理赔 "收款账户不一致可以打款吗"
engineering_project_qa 工程项目 "施工图纸和强制性规范冲突怎么办"

2.6 核心模块一览

qa_core/
├── api/              # FastAPI 路由 — HTTP/WebSocket 请求入口
├── application/      # 服务编排 — QAService 统一业务入口
├── intent/           # 意图识别 — 判断用户想干什么
├── retrieval/        # 检索系统 — Milvus 连接、过滤、重排
├── pipeline/         # RAG 主流程 — 事件生成、上下文构建
├── prompts/          # 提示词 — 模板选择、场景注入
├── indexing/         # 入库 — 文档加载、切分、FAQ 入库
├── governance/       # 治理 — 知识库版本、数据隔离
├── memory/           # 记忆 — 聊天历史、摘要、反馈
├── quality/          # 质量 — 入库质量、冲突检测
├── scenarios/        # 场景 — 多行业配置、source 推断
├── config/           # 配置 — 设置、日志、启动校验
└── observability/    # 可观测 — 追踪、评测、Bad Case

第三部分:环境搭建与首次启动

本章只完成“让项目第一次跑起来”。Dockerfile、Compose 字段、镜像分层、完整交付验收和故障排查统一放到第 20 章。

3.1 运行方式

方式 API 位置 基础设施 适用场景
Docker Compose API、MySQL、Redis、Milvus、etcd、MinIO 都在容器内 Compose 统一管理 首次学习、标准部署、环境复现
本机 API Python API 在宿主机运行 MySQL、Redis、Milvus、etcd、MinIO 仍用 Docker 高频修改 Python 代码

首次运行优先选择 Docker Compose。容器之间通过服务名访问依赖,例如 mysql:3306redis:6379milvus:19530;宿主机调试则使用 localhost

3.2 准备配置和模型

Windows PowerShell:

if (!(Test-Path .env.compose)) { Copy-Item .env.compose.example .env.compose }
New-Item -ItemType Directory -Force models, logs, reports | Out-Null
Test-Path .\models\bge-m3
Test-Path .\models\bge-reranker-large
Test-Path .\models\bert_intent_classifier_v1

Linux Shell:

cp -n .env.compose.example .env.compose
mkdir -p models logs reports
test -d models/bge-m3
test -d models/bge-reranker-large
test -d models/bert_intent_classifier_v1

宿主机模型目录统一使用项目相对路径 ./models,Compose 挂载到容器内 /app/models。因此 Windows 和 Linux 不需要在配置中写各自的绝对路径:

MODEL_VOLUME_HOST_PATH=./models
EMBEDDING_MODEL_PATH=/app/models/bge-m3
RERANKER_MODEL_PATH=/app/models/bge-reranker-large
INTENT_MODEL_PATH=/app/models/bert_intent_classifier_v1

然后在 .env.compose 中填写真实的 DASHSCOPE_API_KEY、数据库密码和管理令牌。

3.3 最小启动流程

# 1. 启动基础设施
docker compose --env-file .env.compose up -d mysql redis etcd minio milvus

# 2. 首次机器若没有项目基础镜像,先构建一次
docker build -f Dockerfile.base -t localhost/knowforge-rag-platform-base:py312 .

# 3. 构建 API
docker compose --env-file .env.compose build api

# 4. 初始化并激活八个业务场景
docker compose --env-file .env.compose run --rm api python scripts/rebuild_scenarios.py --reset-collections --description "docker init all scenarios"

# 5. 启动 API
docker compose --env-file .env.compose up -d api

# 6. 查看状态
docker compose --env-file .env.compose ps
docker compose --env-file .env.compose logs --tail 80 api

Dockerfile.base 只在目标机器没有 localhost/knowforge-rag-platform-base:py312 时需要构建;已有该镜像时可以跳过第 2 步。

3.4 访问与最小检查

环境 地址
Windows 本机 http://127.0.0.1:8000
Linux 服务器 http://服务器IP:8000
健康检查 http://127.0.0.1:8000/health

如果 API 没有启动,按以下顺序检查:

docker compose config 是否通过
 基础服务是否 healthy
 /app/models 下三个模型目录是否存在
 active 知识库版本是否已生成
 api 日志中的 preflight 失败项

启动前置校验的源码和职责放在第 13 章,Docker 构建、部署脚本、验收命令和常见故障放在第 20 章。

3.5 启动前置校验的工作机制

服务不是进程启动后立刻接受请求,而是先执行 FastAPI lifespan -> warmup_runtime()。这里采用 fail-fast(尽早失败):本地模型、场景资料、Milvus、MySQL、active 知识库版本等关键条件任一不满足,进程直接启动失败,而不是等用户第一次提问时才报错。

需要区分两个概念:

  • 启动必需条件:LLM Key、Admin Token、本地模型、场景配置与资料、Milvus、MySQL、active 知识库版本;缺失时阻断启动。
  • LLM 供应商真实连通性:启动时只确认 Key 已配置;真实请求由后台任务探测并写入健康状态,不因供应商短暂抖动阻断 API 进程。

Redis 是条件依赖:只有 CACHE_ENABLED=true 时,启动校验才要求 Redis Python 客户端已安装且 redis_host:redis_port 可连通。

flowchart TD
    Start["🚀 FastAPI lifespan 启动"] --> Config["读取 Settings,加载场景注册表<br/>解析 ACTIVE_SCENARIO_ID"]
    Config --> C1{"1️⃣ LLM API Key<br/>是否为空或占位符?"}
    C1 -- "是" --> Fail1["❌ 启动失败<br/>DASHSCOPE_API_KEY 未配置"]
    C1 -- "否" --> C2{"2️⃣ Admin Token<br/>是否为空或占位符?"}
    C2 -- "是" --> Fail2["❌ 启动失败<br/>ADMIN_API_TOKEN 未配置"]
    C2 -- "否" --> C3{"3️⃣ 本地模型文件<br/>Embedding / Reranker / BERT<br/>目录、标签和权重是否齐全?"}
    C3 -- "否" --> Fail3["❌ 启动失败<br/>本地模型目录、标签或权重缺失"]
    C3 -- "是" --> C4{"4️⃣ 场景资料<br/>配置目录、data_root、FAQ CSV<br/>是否存在且 active 场景可解析?"}
    C4 -- "否" --> Fail4["❌ 启动失败<br/>场景配置或资料缺失"]
    C4 -- "是" --> C5{"5️⃣ Milvus TCP<br/>URI 合法且端口可达?"}
    C5 -- "否" --> Fail5["❌ 启动失败<br/>Milvus 不可连接"]
    C5 -- "是" --> C6{"6️⃣ MySQL TCP<br/>端口可达?"}
    C6 -- "否" --> Fail6["❌ 启动失败<br/>MySQL 不可连接"]
    C6 -- "是" --> Cache{"7️⃣ CACHE_ENABLED<br/>是否为 true?"}
    Cache -- "是" --> Redis{"Redis 客户端已安装<br/>且 TCP 可达?"}
    Redis -- "否" --> Fail7["❌ 启动失败<br/>Redis 依赖或连接不可用"]
    Redis -- "是" --> Schema["8️⃣ MySQL Schema Bootstrap<br/>创建或校验控制面表"]
    Cache -- "否" --> Schema
    Schema --> Active{"9️⃣ 当前 active 场景<br/>是否存在 active KB 版本?"}
    Active -- "否" --> Fail8["❌ 启动失败<br/>请先入库并激活版本"]
    Active -- "是" --> Intent["🔎 预热 BERT 意图决策网关"]
    Intent --> LLMProbe["📡 异步探测 LLM 连通性<br/>只写运行状态,不阻断启动"]
    LLMProbe --> Warmup["🔥 等待检索栈预热完成<br/>BGE Embedding + 8 个场景的 FAQ/Doc Collection<br/>+ CrossEncoder Reranker"]
    Warmup --> Ready["✅ 服务就绪<br/>开始接受请求"]

    style Fail1 fill:#FEF2F2,stroke:#DC2626
    style Fail2 fill:#FEF2F2,stroke:#DC2626
    style Fail3 fill:#FEF2F2,stroke:#DC2626
    style Fail4 fill:#FEF2F2,stroke:#DC2626
    style Fail5 fill:#FEF2F2,stroke:#DC2626
    style Fail6 fill:#FEF2F2,stroke:#DC2626
    style Fail7 fill:#FEF2F2,stroke:#DC2626
    style Fail8 fill:#FEF2F2,stroke:#DC2626
    style Ready fill:#ECFDF5,stroke:#059669,stroke-width:2px
    style Warmup fill:#EFF6FF,stroke:#2563EB

下面的代码是实际启动顺序的压缩版。validate_runtime_environment() 只处理配置、文件系统和 TCP 前置条件;MySQL 表结构、active 版本和模型预热由 app.py 继续完成。

# app.py
@asynccontextmanager
async def lifespan(_: FastAPI):
    await warmup_runtime()
    yield

async def warmup_runtime() -> None:
    # 1-7:Key、Token、模型、场景资料、Milvus、MySQL、可选 Redis
    validate_runtime_environment()

    # 8:控制面表必须先存在,版本状态才能查询
    await asyncio.to_thread(bootstrap_mysql_schema)

    # 9:当前 active 场景必须已有已激活版本
    validate_active_kb_versions(settings.active_scenario_id)

    # 预热 BERT 意图分类模型和规则网关
    warmup_intent_decision_gateway()

    # 后台记录 LLM 可用性;这里不等待,也不以失败阻断启动
    asyncio.create_task(refresh_llm_status_background())

    # 预热 BGE、全部场景的 FAQ/Doc Collection、CrossEncoder,完成前不接收流量
    start_retrieval_warmup_background()
    await asyncio.to_thread(wait_for_retrieval_warmup)

validate_runtime_environment() 内部的校验顺序如下。先做几乎零成本的占位符检查,再检查本地路径,最后才做网络 I/O;因此配置错误通常会比网络超时更早暴露。

# qa_core/config/preflight.py
from pathlib import Path

def validate_runtime_environment() -> dict[str, object]:
    settings = get_settings()
    registry = get_scenario_registry()
    scenario = resolve_scenario(settings.active_scenario_id)

    # 1-2:空值或示例占位符都不允许启动
    if _is_placeholder(settings.llm_api_key):
        raise RuntimeError("DASHSCOPE_API_KEY 未配置")
    if _is_placeholder(settings.admin_api_token):
        raise RuntimeError("ADMIN_API_TOKEN 未配置")

    # 3:三类本地模型都要可用;BERT 还要求标签文件和权重文件
    _require_path("Embedding 模型目录", settings.embedding_model_path)
    _require_path("Reranker 模型目录", settings.reranker_model_path)
    _require_path("BERT 意图模型目录", settings.intent_model_path)
    _require_path("BERT 意图模型标签文件", str(Path(settings.intent_model_path) / "intent_labels.json"))
    if not (Path(settings.intent_model_path) / "model.safetensors").exists() and not (
        Path(settings.intent_model_path) / "pytorch_model.bin"
    ).exists():
        raise RuntimeError("BERT 意图模型权重文件不存在")

    # 4:当前场景配置、资料目录和 FAQ 文件都必须存在
    _require_path("场景配置目录", settings.scenario_config_dir)
    if scenario.scenario_id not in {item.scenario_id for item in registry.list_scenarios()}:
        raise RuntimeError("ACTIVE_SCENARIO_ID 无效")
    _require_path("场景文档目录", scenario.data_root)
    _require_path("场景 FAQ 文件", scenario.faq_csv_path)

    # 5-7:基础设施连通性;Redis 只在启用缓存时成为必需依赖
    _require_milvus_uri()
    _require_tcp("MySQL", settings.mysql_host, settings.mysql_port)
    if settings.cache_enabled:
        _require_redis()

设计意图:允许核心组件缺失时继续启动,会形成“页面能打开,用户提问才报错”的假可用状态。当前设计把故障前移到启动日志:缺模型就补模型,缺 active 版本就先入库,Milvus 或 MySQL 不通就先修基础服务;排查对象明确,不会把后端配置问题误判成前端或网络偶发问题。


重点掌握

优先级 内容 原因
★★★ 必会 RAG 核心概念(检索+生成、开卷考试类比、解决幻觉/私有知识问题) 本项目的根本出发点,所有后续内容建立在此之上
★★★ 必会 离线链路(文档加载→切分→向量化→存储)和在线链路(意图识别→检索→重排→生成)的职责划分 理解两条链路的边界是理解整个项目架构的钥匙
★★★ 必会 全景架构图的五层分层(用户入口→路由→核心引擎→外部依赖→入库) 每一层各司其职,是理解系统边界的基础
★★★ 必会 部署架构:宿主机(FastAPI)、Docker(Milvus/MySQL)、本地模型(BGE-M3/Reranker)、云服务(DashScope)分别部署的原因 理解每类组件的部署选型依据
★★ 理解 向量检索的基本概念(语义相似度 vs 关键词匹配) 为第 2 章深入做铺垫
★★ 理解 20 步问答链路走读(从用户输入到 end 事件的完整路径) 串联全部模块的整体认知
★★ 理解 三条特殊路径(问候/越界快速通道、FAQ 直出通道、追问改写通道) 理解 RAG 的分支逻辑
★ 了解 Compose 最小启动流程和前置校验顺序 实操需要,但非概念重点
★ 了解 8 大业务场景和核心模块一览 了解项目覆盖范围即可

下一章:RAG 核心概念深入 — 向量检索原理、Embedding 模型、混合检索策略

返回笔记开头 ↑