Skip to main content

QAService 编排

Written: 2026.06
第 09 章跟敲代码:codealong/chapters/ch09_qaservice_orchestration。 这部分代码是本章跟敲版,用来先跑通核心闭环;完整项目源码仍以本讲后文标注的 qa_core/scripts/ 等路径为准。
上一讲Milvus 混合检索深度解析
下一讲RAG Pipeline 主流程深度解析

1. 本讲目标

  • 理解 QAService 作为服务编排层(Orchestration Layer)的设计理念
  • 掌握”服务门面”模式在 RAG 系统中的应用
  • 理解两个核心方法的职责分工
  • 理解 Generator(生成器)在流式问答中的角色

2. 前置知识 — 服务编排模式

2.1 什么是服务编排

服务编排(Service Orchestration) 是软件架构中的一种模式:用一个中心化的”编排器”来协调多个子服务的调用顺序和数据流转。 打个比方:
  • 没有编排:每个厨师自己决定做什么菜、用什么食材、先炒哪个后炒哪个 → 混乱
  • 有编排:主厨(编排器)决定菜单、分配任务、协调出菜顺序 → 有序
在 RAG 系统中,“子服务”包括:
  • 意图识别 → 判断用户想干什么
  • 历史读取 → 获取会话上下文
  • 查询改写 → 补全追问
  • 检索计划 → 决定如何检索
  • FAQ 检索 → 查标准答案
  • 文档检索 → 查业务资料
  • 上下文构建 → 组织参考资料
  • LLM 生成 → 产生答案
  • 历史写入 → 保存对话
QAService 就是协调这些子服务的”主厨”。

2.2 QAService 不做什么(边界)


3. QAService 的两个核心方法

3.1 方法职责对照

3.2 stream_query() — 唯一主干链路

yield from 是 Python 的委托语法。rag_stream_queryqa_core.pipeline.rag 模块中 stream_query 函数的 import alias(from qa_core.pipeline.rag import stream_query as rag_stream_query),它是一个生成器函数,每次 yield 产生一个事件。yield from 把这些事件”透传”给调用方(FastAPI WebSocket 路由),所以 QAService 不需要自己维护生成循环。

3.3 debug_retrieval() — 检索诊断半链路

这个方法服务于状态页、评测脚本和排障。它可以告诉开发者”意图是什么、检索计划是什么、FAQ/Doc 命中了什么”,但不会生成面向用户的最终答案。 注意:debug_retrieval()检索诊断半链路,它故意从 prepare_retrieval() 开始,以便观察检索类意图、source 推断、按需改写、检索计划和召回质量;线上用户问答仍从 stream_query() 进入,并先执行 decide_route()。因此,调试接口没有走 route=direct_answer / faq_exact / retrieval,并不代表在线主链路跳过查询路由。

3.4 source 白名单校验

这个校验放在 QAService 层(而非 API 层或 retrieval 层)是经过考虑的:
  • API 层:不应该知道 Milvus 过滤规则(它只管 HTTP 参数校验)
  • Retrieval 层:不应该承担业务白名单判断(它只管执行检索)
  • QAService(编排层):最清楚”前端筛选项 + 意图推断分类”如何进入主链路

4. Generator 模式在 RAG 中的应用

4.1 什么是 Generator

Generator(生成器)是 Python 的一个核心特性,使用 yield 关键字:
Generator 的特点是惰性求值:每次只产生一个值,调用方可以在每个值之间做其他事情。

4.2 为什么 RAG 适合用 Generator

RAG 的问答过程不是一个”输入→等待→输出”的单步操作,而是一个多阶段持续产出的过程:
每个 yield 都是一个可以立即推送给前端的事件。用户不需要等全部流程跑完才能看到任何东西。

4.3 前端接收到的体验

如果不用 Generator 而是一次性返回:

5. 应用工厂模式

5.1 get_qa_service() 工厂函数

为什么用单例
  • settings 是只读配置,加载一次即可
  • history 是历史存储适配器,本身负责按 session_id 隔离会话
  • 每次请求创建新的 QAService 会重复加载配置,但没有好处
为什么不担心并发
  • QAService 只保存 settings(只读)和 history(线程安全适配器)
  • 请求级变量(query、intent、plan、sources 等)都不在 QAService 上,而在方法局部变量中

5.2 在 API 中使用


6. 错误处理与事件协议

6.1 异常不抛给 WebSocket 路由

设计意图:如果抛出异常到 WebSocket 路由,前端收到的就是一个 WebSocket 协议级别的错误,页面无法优雅地展示错误信息。以事件形式返回错误,前端可以按同一套 UI 渲染错误信息,并允许用户继续下一轮提问。

6.2 事件类型汇总


7. 本讲实践闭环

通过标准:在线问答只有 WebSocket 主入口;同一个业务服务能支撑流式回答和检索诊断,但不把 RAG 细节泄露给 API 层。

7.1 本讲从 0 到 1 实现闭环

这一讲的目标不是再写一个 RAG 算法,而是把前面已经具备的能力包装成一个稳定的应用服务层。实现时按这个顺序推进:
  1. 先定义 QAService,让 API 层以后只调用 service,不直接碰 Milvus、Prompt、Pipeline。
  2. 实现 stream_query(),把完整 RAG Pipeline 产出的事件原样透传给 WebSocket;问候、越界、转人工等直答也在这条链路内完成。
  3. 实现 debug_retrieval(),复用检索准备逻辑但不调用最终回答 LLM。
  4. 最后补一个工厂函数 get_qa_service(),避免每个请求重复初始化 settings、history、retriever。
实现完成后,相关代码结构应该是下面这张图: 来源:真实代码逻辑压缩版,对应 qa_core/application/service.py::QAService
直答场景不再放在额外 API 入口处理,而是在 Pipeline 的意图识别阶段处理。这样历史保存、Trace、限流、事件协议都只走一套实现,问候、越界、转人工和复杂 RAG 问题不会分裂成两条在线链路。 WebSocket 不应该知道 RAG 内部有多少阶段,它只消费 service 产出的事件。这样以后 Pipeline 从 7 阶段变成 9 阶段,API 层也不需要跟着大改。 来源:真实代码调用点,见 qa_core/api/chat.py
验收时重点看两件事:WebSocket 能持续收到 start/status/token/end/error 事件;/api/retrieval/debug 能返回检索诊断信息但不生成最终答案。 来源:命令行验收,对应 scripts/api_e2e_smoke.py
闭环验证重点: 验收重点:API 层只负责协议和连接,业务编排集中在 service;service 再把请求交给 Pipeline,而不是把每个模块揉在路由函数里。

8. 重点掌握

9. 本讲小结

  • QAService 是服务编排层,协调意图、历史、检索、生成、存储,但不直接处理 HTTP 或 Milvus 细节
  • stream_query 是唯一在线问答主干链路,通过 Generator 持续产出事件
  • debug_retrieval 是检索诊断半链路,只查不生成
  • yield from 将 RAG Pipeline 的事件透传给调用方
  • 单例工厂确保 settings 和 history 只加载一次,请求级状态全在局部变量中
  • 错误以事件形式返回,前端可以优雅展示并允许继续提问
下一讲RAG Pipeline 主流程深度解析 — Stage 0-7 事件生成、上下文构建、答案引用增强