RAG Pipeline 主链路
Written: 2026.06第 10 章跟敲代码:上一讲:QAService 核心编排codealong/chapters/ch10_rag_pipeline。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的qa_core/、scripts/等路径为准。
下一讲:Prompt 工程与 Profile 系统
1. 本讲目标
- 理解 RAG Pipeline 的 8 个 Stage(0-7)事件生成模型
- 掌握 FAQ 快速路径和 FAQ 标准直出的区别
- 理解上下文构建中的筛选、去重、截断策略
- 理解答案引用增强的实现
本讲边界 本讲关注一次在线问答的完整主流程:问题如何进入 Pipeline、如何检索、如何构建上下文、如何流式返回。Prompt 选择和模板细节在本讲只作为“生成阶段的一部分”理解,完整展开放到第 11 讲。
2. 前置知识 — Pipeline 设计模式
2.1 Pipeline vs Chain
Chain(链):固定的步骤序列,A → B → C → D,没有分支。 Pipeline(管道):有分支、有快慢路径的流程。每一步可以提前结束(如 FAQ 命中时跳过文档检索),也可以根据上一步的结果调整下一步的参数。 本项目的 RAG 流程是 Pipeline 而非 Chain:2.2 Pipeline 的模块化拆分
项目将 Pipeline 拆分为多个职责单一的文件:3. 8 个 Stage 主流程
3.1 Stage 0-7 可视化总览
3.2 完整流程代码(简化)
3.3 Stage 1:查询路由
这是在线问答进入检索准备之前的低成本路由层。它统一处理三类结果:
这里的关键点是:intent 描述用户想做什么,route 描述系统下一步怎么处理。FAQ 精确命中不是新的用户意图,而是
route=faq_exact,同时携带 intent=FAQ_QUERY。
你好、转人工、彩票怎么买 这类问题不应该先进入 FAQ 快速路径,而应该在这一阶段直接收口。
3.4 FAQ 精确命中为什么放在路由层
FAQ 精确命中依赖知识库内容、版本、tenant、source_filter 和标准问题文本,它不是“用户意图类型”。所以更准确的表达是:查询路由层可以产出route=faq_exact,并把 intent 标记为 FAQ_QUERY。
3.5 _exact_faq_answer() — 精确匹配实现
- 减少首 token 延迟。标准 FAQ 的精确命中不需要经过历史加载、检索类意图识别、改写、检索计划等步骤。
- FAQ 快速路径仍然访问 Milvus(带版本和数据隔离过滤),不是本地缓存。
- 它在同一个
decide_route()中排在 direct_answer 之后,避免问候、转人工、越界问题先触发知识库查询。
- 还没做意图识别,不知道这是 FAQ_QUERY 还是 KNOWLEDGE_QUERY
- 如果是知识咨询但 FAQ 相似分数高,可能误答。所以只允许用户问题和 FAQ 标准问题完全一致时才直出。
3.6 FAQ 标准直出 vs FAQ 精确路由
这是两个容易混淆的概念:4. 上下文构建
4.1 select_context_docs() 的筛选策略
4.2 build_context() 的格式化输出
5. 信息不足处理
5.1 什么情况判定为信息不足
prepare_answer 在 steps.py 中定义,其内部的信息不足判定委托给 _build_answer_context:
_build_answer_context 负责实际的上下文筛选和命中类型判定:
5.2 信息不足的答案
6. 答案引用增强
6.1 什么是引用增强
LLM 生成的答案可能引用上下文中的信息,但不会自动标注”这个信息来自哪个文档”。引用增强在 LLM 生成完答案后,检查答案是否提到了上下文中的关键信息,如果提到了就补充来源标注。7. 性能追踪
7.1 阶段计时
end 事件:
8. 流式事件协议 — 前后端如何协作
8.1 事件驱动的问答模型
一次 RAG 问答不是”前端发请求 → 等 5 秒 → 收到完整答案”。实际的用户体验是:8.2 五种事件类型
8.3 每种事件的字段结构
start 事件 — 请求已被接收:message 显示为一个动态更新的状态栏或加载提示。
token 事件 — 流式答案的片段:
8.4 前端如何消费事件
8.5 后端如何推进生成器
关键问题:QAService.stream_query() 是同步生成器(它内部顺序执行意图识别、Milvus 检索、本地 rerank 和 LLM 流式调用),但 WebSocket 路由是异步函数。如果直接调用 next(iterator),事件循环会被阻塞。
解决方案:asyncio.to_thread 将同步生成器的推进放到独立线程:
QAService.stream_query() 是一个同步生成器——它内部顺序执行意图识别、Milvus 检索(gRPC 阻塞调用)、本地 Rerank(CPU 密集计算)、LLM 流式调用(HTTP 阻塞读取)。如果把这段逻辑直接放在主线程的事件循环中调用 next(iterator),整个事件循环会在每次推进生成器时被阻塞,导致其他 WebSocket 连接、HTTP 请求全部卡住。
解决方案:asyncio.to_thread 作为桥梁。 主线程通过 asyncio.to_thread 把同步生成器的推进操作丢给线程池中的工作线程,自己立即返回并继续处理事件循环中的其他任务。工作线程推进完成后,结果通过 Future 传回主线程,主线程再 await websocket.send_json(event) 发给浏览器。
图中两条线程的分工:
事件如何跨线程:工作线程每产出一个 token 或状态事件,生成器 yield 一次;主线程的
_next_stream_event(stream) 捕获这个值并通过 asyncio.to_thread 的返回值传回;主线程拿到事件后立即 send_json 给浏览器。这个过程对用户透明——浏览器看到的是连续的 status → token... → end 事件流。
为什么不在 LLM 流式阶段回到主线程? LLM 的 llm.stream() 本身返回一个迭代器,每次迭代都是阻塞的 HTTP 读取操作。如果回到主线程逐 token 读取,同样会阻塞事件循环。所以整个生成器——从意图识别到最后一个 token——全部留在工作线程中执行。
8.6 事件协议的设计原则
- 类型安全:每个事件都有
type字段,前端用switch分派处理,不靠字段存在与否判断 - 诊断信息附带:
end事件携带完整的 retrieval 诊断信息,前端可以用 JS 渲染到页面上,帮助用户理解”系统为什么这样回答” - 错误不崩溃:异常转为
error事件,不抛到 WebSocket 路由。用户看到错误提示后可以继续下一轮提问 - 历史写入在最后:
end事件之后才写 MySQL 历史,确保历史记录的是完整答案(含引用增强后的来源)
9. 本讲实践闭环
通过标准:FAQ 快速路径、文档 RAG、信息不足、流式生成都能走到可解释的分支。
9.1 本讲从 0 到 1 实现闭环
本讲是在线问答的主干实现。实现完成后,相关代码结构应该是下面这张图:9.1.1 :定义流式事件协议
目标:Pipeline 不直接返回字符串,而是持续产出浏览器能理解的事件。 来源:真实代码逻辑压缩版,对应qa_core/pipeline/events.py、qa_core/pipeline/runtime.py 与 qa_core/pipeline/rag.py。
9.1.2 :串起意图、计划和检索
目标:把第 5-8 讲的模块接进同一条主流程。 来源:真实代码逻辑压缩版,对应qa_core/pipeline/rag.py 和 qa_core/pipeline/steps.py。
debug_retrieval() 只用于检索诊断,故意从 prepare_retrieval() 开始跑半链路,方便观察检索类意图、source 推断、按需改写和 FAQ/Doc 召回;线上问答入口仍然是 stream_query(),并且一定先经过 decide_route()。
9.1.3 :实现 FAQ 快速路径和文档 RAG 分支
来源:真实代码逻辑压缩版,对应qa_core/pipeline/rag.py::_search_and_generate() 与 qa_core/pipeline/retrieval_steps.py。
9.1.4 :构建上下文并流式生成
来源:真实代码逻辑压缩版,对应qa_core/pipeline/rag.py、qa_core/pipeline/steps.py、qa_core/pipeline/context.py、qa_core/pipeline/citations.py。
9.1.5 :保存历史并记录 Trace
来源:真实代码逻辑压缩版,对应qa_core/pipeline/rag.py::_finish_with_single_answer() 和 Stage 7。
9.1.6 :验收完整链路
验收方式: 来源:命令行验收,对应scripts/api_e2e_smoke.py。
通过标准:
- 能看到
start/status/token/end事件。 - FAQ 快速路径、文档 RAG、信息不足至少各有可解释分支。
- 最终答案带引用来源。
- 右侧诊断或 Trace 能看到命中路径、阶段耗时和 top score。
10. 重点掌握
11. 本讲小结
- Pipeline > Chain:RAG 是有分支、有快慢路径的管道,不是固定步骤的链
- 查询路由统一处理 direct_answer、faq_exact、retrieval,避免拆成两套“意图识别”
- FAQ 精确命中是
route=faq_exact,不是新的 intent;它命中时仍携带intent=FAQ_QUERY - 上下文构建依次执行:FAQ 补充 → 分数过滤 → 去重 → 优先表格 → 截断 → 格式化
- 信息不足时明确告知用户,而不是让 LLM 在没有资料的情况下生成幻觉
- 引用增强在 LLM 生成的答案后补充来源标注
- 阶段计时追踪每个阶段的耗时,帮助定位性能瓶颈

