第 1 章:项目概述与 Docker 环境搭建¶
🎬 推荐:在学习本章之前或之后,观看 RAG Pipeline 执行流程动画 建立对系统整体执行流程的直观认识。
本章目标¶
- 理解 RAG 系统的基本概念和应用场景
- 了解本项目的整体架构和技术栈
- 理解 Docker/Compose 在本项目中的运行底座作用
- 完成开发环境的搭建和验证
第一部分:前置知识¶
1.1 什么是 RAG(检索增强生成)¶
RAG = Retrieval-Augmented Generation,即"检索增强生成"。
在没有 RAG 之前,大语言模型(LLM)存在几个核心问题:
- 知识截止日期:模型训练完成后,无法获取训练数据之后的新信息。例如 GPT-4 的知识截止到 2023 年某月,之后发生的事情它不知道。
- 幻觉问题:当模型不确定某个答案时,它可能会"编造"一个看起来很合理但实际上是错误的内容。这在企业场景中是不可接受的。
- 私有知识无法覆盖:企业内部的制度、流程、业务文档是私有数据,从未进入过公开训练集,模型自然无法回答。
RAG 的解决思路非常简单:
可以把 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 模块回答三个问题:
- 这是什么类型的问题?(问候 / 标准问答 / 知识咨询 / 追问 / 人工客服 / 越界)
- 能不能直接回答?(问候直接回"你好",越界直接拒答)
- 如果不能直接回答,后续该怎么处理?(改写成独立问题?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_id、kb_version、tenant_id、dataset_id、visibility、source拼成 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 按 activeversion_seq解释valid_from_seq/valid_to_seq有效期窗口,支持引用式增量和快速回滚 - data_scope:每条数据还有
tenant_id、dataset_id、visibility、allowed_roles字段。检索时拼成 Milvus 表达式,实现租户级数据隔离
⑧ Indexing — 文档入库(离线链路)
Indexing 模块不在在线问答链路中执行(太慢),而是通过离线脚本触发:
入库时还会做增量判断:同版本内通过文件 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 — 关系型数据库
MySQL 不承担 FAQ/文档语义检索。知识召回在 Milvus 中完成;MySQL 负责事务型控制面和业务元数据,例如版本状态、active 指针、文档清单、聊天历史、反馈与缓存 epoch。这个边界避免了用关系库模糊查询替代向量检索,也让版本治理和数据写入具备事务能力。
DashScope LLM — 大语言模型
项目主链路采用分层职责:确定性规则先处理直答与安全边界,检索类意图由规则候选、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 系统的共性。
两条特殊路径(图中箭头覆盖但上面没走到的):
- 问候/越界快速通道:
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 一样。
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(中频 · 延迟不敏感)
MySQL 主要保存聊天记录、反馈和知识库治理控制面数据,例如版本状态、active 指针、文档清单与缓存命名空间。部署在本地 Docker 同样是为了避免网络延迟累积——虽然单次 MySQL 查询很快,但每次问答和治理操作都可能多次读写,走公网会显著拖慢。
③ FastAPI → Redis(高频 · 延迟敏感)
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(中频 · 延迟敏感)
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(内部依赖 · 用户无感)
- 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:3306、redis:6379、milvus: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 模型、混合检索策略