第9讲:QAService 核心编排¶
上一讲:Milvus 混合检索深度解析 下一讲:RAG Pipeline 主流程深度解析
本讲目标¶
- 理解 QAService 作为服务编排层(Orchestration Layer)的设计理念
- 掌握"服务门面"模式在 RAG 系统中的应用
- 理解两个核心方法的职责分工
- 理解 Generator(生成器)在流式问答中的角色
本讲地图¶
本图对应本讲功能闭环,展示从输入到本讲交付物的主干路径。节点与主项目代码文件和函数保持一致,后续章节消费的能力只作为交付边界出现。
图 1:第 09 讲功能闭环地图¶
flowchart TD
C09_FACTORY["服务工厂<br/>get_qa_service()"]
C09_HISTORY["历史存储<br/>ChatHistoryStore"]
C09_SERVICE["服务门面<br/>QAService"]
C09_VALIDATE["source 校验<br/>validate_source()"]
C09_STREAM["流式问答<br/>stream_query()"]
C09_DEBUG["检索诊断<br/>debug_retrieval()"]
C09_OUT{{"章节输出<br/>QAService.stream_query() / debug_retrieval()"}}
C09_FACTORY --> C09_SERVICE
C09_HISTORY --> C09_SERVICE
C09_SERVICE --> C09_VALIDATE
C09_SERVICE --> C09_STREAM
C09_SERVICE --> C09_DEBUG
C09_STREAM --> C09_OUT
C09_DEBUG --> C09_OUT
style C09_FACTORY fill:#F8FAFC,stroke:#64748B,stroke-width:2px
style C09_HISTORY fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C09_SERVICE fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C09_VALIDATE fill:#FEF3C7,stroke:#D97706,stroke-width:2px
style C09_STREAM fill:#DBEAFE,stroke:#2563EB,stroke-width:2px
style C09_DEBUG fill:#F5F3FF,stroke:#7C3AED,stroke-width:2px
style C09_OUT fill:#DCFCE7,stroke:#16A34A,stroke-width:2px
节点与代码对齐¶
| 节点 | 对齐文件 | 函数/对象 | 本章职责 |
|---|---|---|---|
| 服务工厂 | qa_core/application/factory.py |
get_qa_service() |
集中创建并缓存 QAService。 |
| 历史存储 | qa_core/memory/history.py |
ChatHistoryStore |
提供 recent_queries、get_context_messages 和 add_turn。 |
| 服务门面 | qa_core/application/service.py |
QAService |
API 层只调用服务方法,不关心 RAG 内部细节。 |
| source 校验 | qa_core/application/service.py |
validate_source() |
委托 retrieval.filters 校验 source_filter。 |
| 流式问答 | qa_core/application/service.py |
stream_query() |
委托 rag_stream_query() 产出事件流。 |
| 检索诊断 | qa_core/application/service.py |
debug_retrieval() |
复用主链路的检索准备与召回,但不生成最终答案。 |
| 章节输出 | qa_core/application/service.py |
QAService.stream_query() / debug_retrieval() |
为第 10 章 Pipeline 下沉和第 12 章 API 入口提供稳定服务接口。 |
第一部分:前置知识 — 服务编排模式¶
1.1 什么是服务编排¶
服务编排(Service Orchestration) 是软件架构中的一种模式:用一个中心化的"编排器"来协调多个子服务的调用顺序和数据流转。
打个比方: - 没有编排:每个厨师自己决定做什么菜、用什么食材、先炒哪个后炒哪个 → 混乱 - 有编排:主厨(编排器)决定菜单、分配任务、协调出菜顺序 → 有序
在 RAG 系统中,"子服务"包括: - 意图识别 → 判断用户想干什么 - 历史读取 → 获取会话上下文 - 查询改写 → 补全追问 - 检索计划 → 决定如何检索 - FAQ 检索 → 查标准答案 - 文档检索 → 查业务资料 - 上下文构建 → 组织参考资料 - LLM 生成 → 产生答案 - 历史写入 → 保存对话
QAService 就是协调这些子服务的"主厨"。
1.2 QAService 不做什么(边界)¶
第二部分:QAService 的两个核心方法¶
2.1 方法职责对照¶
flowchart TD
Client["🖥️ 浏览器"] --> WS["WebSocket /api/stream<br/>在线问答唯一入口"]
Client --> Debug["POST /api/retrieval/debug<br/>检索半链路"]
WS --> Stream["stream_query()<br/>唯一主干链路"]
Stream --> Intent{"意图识别结果"}
Intent -->|"问候/越界/转人工"| DirectReturn["直接答案事件<br/>跳过检索和 LLM"]
Intent -->|"FAQ/知识问答/追问"| Rag["FAQ/文档检索<br/>重排/生成"]
Stream --> Generator["Generator 持续产出事件"]
Generator --> Events["start → status → token* → end/error"]
Debug --> DebugFn["debug_retrieval()<br/>不调用 LLM 生成"]
DebugFn --> Diag["返回诊断 JSON<br/>intent/plan/sources"]
style Stream fill:#ECFDF5,stroke:#059669,stroke-width:2px
style DebugFn fill:#FFFBEB,stroke:#D97706,stroke-width:2px
2.2 stream_query() — 唯一主干链路¶
yield from 是 Python 的委托语法。rag_stream_query 是 qa_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 不需要自己维护生成循环。
2.3 debug_retrieval() — 检索诊断半链路¶
这个方法服务于状态页、评测脚本和排障。它可以告诉开发者"意图是什么、检索计划是什么、FAQ/Doc 命中了什么",但不会生成面向用户的最终答案。
注意:debug_retrieval() 是检索诊断半链路,它故意从 prepare_retrieval() 开始,以便观察检索类意图、source 推断、按需改写、检索计划和召回质量;线上用户问答仍从 stream_query() 进入,并先执行 decide_route()。因此,调试接口没有走 route=direct_answer / faq_exact / retrieval,并不代表在线主链路跳过查询路由。
2.4 source 白名单校验¶
这个校验放在 QAService 层(而非 API 层或 retrieval 层)是经过考虑的: - API 层:不应该知道 Milvus 过滤规则(它只管 HTTP 参数校验) - Retrieval 层:不应该承担业务白名单判断(它只管执行检索) - QAService(编排层):最清楚"前端筛选项 + 意图推断分类"如何进入主链路
第三部分:Generator 模式在 RAG 中的应用¶
3.1 什么是 Generator¶
Generator(生成器)是 Python 的一个核心特性,使用 yield 关键字:
Generator 的特点是惰性求值:每次只产生一个值,调用方可以在每个值之间做其他事情。
3.2 为什么 RAG 适合用 Generator¶
RAG 的问答过程不是一个"输入→等待→输出"的单步操作,而是一个多阶段持续产出的过程:
每个 yield 都是一个可以立即推送给前端的事件。用户不需要等全部流程跑完才能看到任何东西。
3.3 前端接收到的体验¶
如果不用 Generator 而是一次性返回:
第四部分:应用工厂模式¶
4.1 get_qa_service() 工厂函数¶
为什么用单例:
- settings 是只读配置,加载一次即可
- history 是历史存储适配器,本身负责按 session_id 隔离会话
- 每次请求创建新的 QAService 会重复加载配置,但没有好处
为什么不担心并发:
- QAService 只保存 settings(只读)和 history(线程安全适配器)
- 请求级变量(query、intent、plan、sources 等)都不在 QAService 上,而在方法局部变量中
4.2 在 API 中使用¶
第五部分:错误处理与事件协议¶
5.1 异常不抛给 WebSocket 路由¶
设计意图:如果抛出异常到 WebSocket 路由,前端收到的就是一个 WebSocket 协议级别的错误,页面无法优雅地展示错误信息。以事件形式返回错误,前端可以按同一套 UI 渲染错误信息,并允许用户继续下一轮提问。
5.2 事件类型汇总¶
| 事件类型 | 含义 | 前端处理 |
|---|---|---|
start |
请求已接收 | 创建答案区域,显示加载状态 |
status |
当前进行到哪个阶段 | 更新进度提示文字 |
token |
LLM 生成的一个 token | 追加到答案文本末尾 |
end |
问答完成 | 显示来源引用、诊断信息、耗时 |
error |
可恢复的错误 | 显示错误信息,允许继续提问 |
本讲实践闭环¶
| 项目 | 内容 |
|---|---|
| 本讲类型 | 系统集成 |
| 实践产物 | QAService、WebSocket stream、检索诊断的服务编排 |
| 是否进入最终项目 | 是 |
| 验收方式 | WebSocket 能持续输出事件,检索诊断能返回命中明细 |
| 后续落点 | 第 10 讲接入完整 Pipeline 事件,第 12 讲纳入 FastAPI 路由 |
通过标准:在线问答只有 WebSocket 主入口;同一个业务服务能支撑流式回答和检索诊断,但不把 RAG 细节泄露给 API 层。
本讲从 0 到 1 实现闭环¶
这一讲的目标不是再写一个 RAG 算法,而是把前面已经具备的能力包装成一个稳定的应用服务层。实现时按这个顺序推进:
flowchart LR
API["WebSocket / HTTP Debug<br/>协议层"] --> Service["QAService<br/>应用服务门面"]
Service --> Stream["stream_query<br/>进入 RAG Pipeline"]
Stream --> Direct{"意图是否可直答?"}
Direct -->|"是"| DirectAnswer["token/end 事件<br/>问候/越界/转人工"]
Direct -->|"否"| Rag["FAQ/Doc 检索<br/>LLM 流式生成"]
Stream --> Events["start / status / token / end / error"]
Service --> Debug["debug_retrieval<br/>检索诊断"]
Service --> History["ChatHistory<br/>保存会话"]
Service --> Trace["LangSmith Trace<br/>记录诊断信息"]
- 先定义
QAService,让 API 层以后只调用 service,不直接碰 Milvus、Prompt、Pipeline。 - 实现
stream_query(),把完整 RAG Pipeline 产出的事件原样透传给 WebSocket;问候、越界、转人工等直答也在这条链路内完成。 - 实现
debug_retrieval(),复用检索准备逻辑但不调用最终回答 LLM。 - 最后补一个工厂函数
get_qa_service(),避免每个请求重复初始化 settings、history、retriever。
实现完成后,相关代码结构应该是下面这张图:
flowchart LR
subgraph Core["qa_core"]
subgraph Application["application"]
Service["service.py<br/>QAService<br/>stream_query / debug_retrieval"]
end
subgraph API["api"]
Chat["chat.py<br/>WebSocket stream<br/>retrieval debug"]
end
subgraph Pipeline["pipeline"]
Rag["rag.py<br/>完整 RAG 事件流"]
end
subgraph Memory["memory"]
History["history.py<br/>会话历史"]
end
end
subgraph Scripts["scripts"]
Smoke["api_e2e_smoke.py<br/>HTTP 管理接口 / WS 冒烟"]
end
Chat --> Service
Service --> Rag
Service --> History
Smoke -. 验证 .-> Chat
来源:真实代码逻辑压缩版,对应 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。
闭环验证重点:
| 验证项 | 验证方式 | 期望结果 |
|---|---|---|
| WebSocket stream | 连接 /api/stream |
持续收到事件 |
| 检索诊断 | 请求 /api/retrieval/debug |
返回意图、计划、FAQ/Doc 命中 |
| 业务分层 | 查看 chat.py |
路由只调用 service,不写检索细节 |
| 历史保存 | 连续追问 | 下一轮能读取上下文 |
| Trace 记录 | 查看诊断字段 | 有 hit_type、intent、kb_version、data_scope、耗时等信息 |
| 调试入口 | 调用 debug_retrieval() |
只跑检索半链路,不调用最终 LLM |
验收重点:API 层只负责协议和连接,业务编排集中在 service;service 再把请求交给 Pipeline,而不是把每个模块揉在路由函数里。
重点掌握¶
| 优先级 | 内容 | 原因 |
|---|---|---|
| ★★★ 必会 | QAService 服务编排层的定位:协调意图识别、历史、检索、生成、存储,不直接处理 HTTP 或 Milvus 细节 | 理解"编排层"在分层架构中的角色 |
| ★★★ 必会 | 两个核心方法:stream_query(唯一在线问答主干链路,yield from 透传事件)、debug_retrieval(只查不生成) | QAService 对外的完整接口 |
| ★★★ 必会 | Generator(生成器)模式在流式问答中的应用:惰性求值,每个 yield 产生一个可立即推送给前端的事件 | 理解 RAG 流式体验的技术实现 |
| ★★ 理解 | 直答分流放在 Pipeline 主链路中:问候、越界、转人工也通过 WebSocket 事件返回 | 避免多套在线入口导致口径和状态不一致 |
| ★★ 理解 | 单例工厂 get_qa_service() + @lru_cache:只缓存 settings 和 history,请求级状态在局部变量中 | 并发安全的保证 |
| ★★ 理解 | 错误以事件(error 类型)形式返回给前端,不抛异常到 WebSocket 路由 | 用户体验和安全设计 |
| ★ 了解 | source 白名单校验放在 QAService 层的理由 | 理解分层职责的划分依据 |
| ★ 了解 | 事件类型汇总:start / status / token / end / error | 回顾第 11 讲的 WebSocket 事件协议 |
本讲小结¶
- QAService 是服务编排层,协调意图、历史、检索、生成、存储,但不直接处理 HTTP 或 Milvus 细节
- stream_query 是唯一在线问答主干链路,通过 Generator 持续产出事件
- debug_retrieval 是检索诊断半链路,只查不生成
- yield from 将 RAG Pipeline 的事件透传给调用方
- 单例工厂确保 settings 和 history 只加载一次,请求级状态全在局部变量中
- 错误以事件形式返回,前端可以优雅展示并允许继续提问
下一讲:RAG Pipeline 主流程深度解析 — Stage 0-7 事件生成、上下文构建、答案引用增强