Skip to main content

RAG Pipeline 主链路

Written: 2026.06
第 10 章跟敲代码:codealong/chapters/ch10_rag_pipeline。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的 qa_core/scripts/ 等路径为准。
上一讲QAService 核心编排
下一讲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_answersteps.py 中定义,其内部的信息不足判定委托给 _build_answer_context
_build_answer_context 负责实际的上下文筛选和命中类型判定:

5.2 信息不足的答案

设计意图:信息不足时,系统明确告知用户(而不是让 LLM 即兴发挥),避免 LLM 在没有可靠资料的情况下生成”幻觉”答案。

6. 答案引用增强

6.1 什么是引用增强

LLM 生成的答案可能引用上下文中的信息,但不会自动标注”这个信息来自哪个文档”。引用增强在 LLM 生成完答案后,检查答案是否提到了上下文中的关键信息,如果提到了就补充来源标注。

7. 性能追踪

7.1 阶段计时

追踪信息最终进入 end 事件:
这些数据帮助性能优化:如果文档检索阶段总是很慢,可能需要调整 top_k 或索引参数;如果 LLM 生成阶段很慢,可能需要换更快的模型或调整 max_tokens。

8. 流式事件协议 — 前后端如何协作

8.1 事件驱动的问答模型

一次 RAG 问答不是”前端发请求 → 等 5 秒 → 收到完整答案”。实际的用户体验是:
这就是事件驱动模型。后端通过 WebSocket 持续推送事件,前端根据事件类型更新 UI。

8.2 五种事件类型

8.3 每种事件的字段结构

start 事件 — 请求已被接收:
status 事件 — 阶段性进度通知:
前端通常将 message 显示为一个动态更新的状态栏或加载提示。 token 事件 — 流式答案的片段:
每个 token 是一个或多个中文字符。前端将这些 token 逐个追加到答案区域,实现打字机效果。 end 事件 — 问答完成:
error 事件 — 异常恢复:

8.4 前端如何消费事件

8.5 后端如何推进生成器

关键问题:QAService.stream_query()同步生成器(它内部顺序执行意图识别、Milvus 检索、本地 rerank 和 LLM 流式调用),但 WebSocket 路由是异步函数。如果直接调用 next(iterator),事件循环会被阻塞。 解决方案:asyncio.to_thread 将同步生成器的推进放到独立线程:
为什么需要两个线程? 这是一个 Python 异步编程中很经典的”同步生成器 + 异步 WebSocket”阻抗匹配问题。 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 事件协议的设计原则

  1. 类型安全:每个事件都有 type 字段,前端用 switch 分派处理,不靠字段存在与否判断
  2. 诊断信息附带end 事件携带完整的 retrieval 诊断信息,前端可以用 JS 渲染到页面上,帮助用户理解”系统为什么这样回答”
  3. 错误不崩溃:异常转为 error 事件,不抛到 WebSocket 路由。用户看到错误提示后可以继续下一轮提问
  4. 历史写入在最后end 事件之后才写 MySQL 历史,确保历史记录的是完整答案(含引用增强后的来源)

9. 本讲实践闭环

通过标准:FAQ 快速路径、文档 RAG、信息不足、流式生成都能走到可解释的分支。

9.1 本讲从 0 到 1 实现闭环

本讲是在线问答的主干实现。实现完成后,相关代码结构应该是下面这张图:

9.1.1 :定义流式事件协议

目标:Pipeline 不直接返回字符串,而是持续产出浏览器能理解的事件。 来源:真实代码逻辑压缩版,对应 qa_core/pipeline/events.pyqa_core/pipeline/runtime.pyqa_core/pipeline/rag.py
设计解释:前端要实时展示进度,后端要暴露阶段诊断,所以事件比单个字符串更适合 RAG。

9.1.2 :串起意图、计划和检索

目标:把第 5-8 讲的模块接进同一条主流程。 来源:真实代码逻辑压缩版,对应 qa_core/pipeline/rag.pyqa_core/pipeline/steps.py
设计解释:Pipeline 不自己判断所有规则,而是调用前面章节实现的模块,保持职责清晰。 调试入口补充: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
设计解释:FAQ 高置信直出可以省掉 LLM;文档 RAG 需要足够上下文;没有上下文时必须明确拒绝幻觉。

9.1.4 :构建上下文并流式生成

来源:真实代码逻辑压缩版,对应 qa_core/pipeline/rag.pyqa_core/pipeline/steps.pyqa_core/pipeline/context.pyqa_core/pipeline/citations.py
设计解释:LLM 只看到筛选后的上下文;引用增强在生成后执行,保证答案可追溯。

9.1.5 :保存历史并记录 Trace

来源:真实代码逻辑压缩版,对应 qa_core/pipeline/rag.py::_finish_with_single_answer() 和 Stage 7。
设计解释:历史要保存最终答案,而不是未补引用的中间答案;Trace 要记录命中路径、阶段耗时和检索分数。

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 生成的答案后补充来源标注
  • 阶段计时追踪每个阶段的耗时,帮助定位性能瓶颈
下一讲Prompt 工程与 Profile 系统 — 提示词模板设计、Profile 选择策略、高风险问题安全约束