06-RAG系统的可控性设计-从HyDE到联网搜索的按需启停架构

6 阅读11分钟

RAG 系统的可控性设计:从 HyDE 到联网搜索的按需启停架构

本文从工程成本、延迟控制和准确率三个维度,剖析为什么一个生产级 RAG 系统必须为每个检索组件配备"开关",以及如何在 LangGraph 状态机中实现优雅的按需启停。


📚 知识库项目实战系列:

  1. RAG 系统架构设计:基于 LangGraph 的多源异构 RAG 系统
  2. 多路召回与融合排序:RRF + Rerank + 动态 TopK 工程实践
  3. 跨库链路检索:Neo4j 图数据库桥接文档与代码
  4. 知识库项目实战 | RAG 检索链路实战:从 ItemName 过滤陷阱到引用去重的工程修复
  5. HyDE 假设性文档检索:原理、陷阱与工程化反思
  6. 知识库项目实战 | RAG 系统的可控性设计:从 HyDE 到联网搜索的按需启停架构 ← 本篇
  7. 从知识库到信息流产品AI助手:多源数据血缘构建与全链路溯源
  8. 代码仓库 RAG 全链路:从 Gitee 抓取、AST 分块到跨库检索

一、引言:RAG 系统不是"全都要",而是"按需给"

很多 RAG 系统的入门教程都会画一张"全链路"架构图:

Query → Query重写 → HyDE生成 → Embedding检索 → HyDE检索 → Web搜索 → KG查询 → RRF融合 → Rerank → LLM生成

这张图传达了一个隐含假设:所有组件同时运行,效果才是最好的。

但在生产环境中,这个假设存在三个致命问题:

  1. 成本失控:一次查询调用 4 次 LLM(ItemName确认 + 查询重写 + HyDE生成 + 答案生成),再加上 Tavily Web API,单条查询成本可能超过 ¥0.5
  2. 延迟爆炸:HyDE 生成一次假设文档需要 1-3 秒,Web 搜索 API 往返需要 1-2 秒,总延迟轻松突破 5 秒
  3. 准确率反噬:HyDE 的术语漂移、Web 搜索的主题扩散,都会污染检索结果,导致"检索到了很多,但正确的被挤掉了"

核心洞察:RAG 链路中的每个组件都不是免费的,它们有自己的能力边界和适用场景。把不适合的组件强行接入链路,不仅浪费资源,还会降低整体效果。

本系统的解决方案很简单——给每个可选组件加一个开关,让用户(或系统)按需启停。


二、问题背景:一条查询的真实成本

以本系统为例,拆解一次完整查询的调用链:

步骤组件调用类型预估延迟预估成本
1ItemName 确认LLM (JSON模式)0.8s¥0.05
2查询重写LLM0.5s¥0.03
3HyDE 生成LLM1.5s¥0.08
4Embedding 检索Qdrant 向量搜索0.2s¥0.00
5HyDE 检索Qdrant 向量搜索0.2s¥0.00
6Web 搜索Tavily API1.5s¥0.02
7RRF + Rerank本地计算0.5s¥0.00
8答案生成LLM (流式/非流式)2.0s¥0.15
合计~7.2s~¥0.33

HyDE 和 Web 搜索合计占用了 42% 的延迟和 30% 的成本,但它们对最终答案的贡献却高度依赖查询类型。

对于"修行前后的反应模式"这类个人知识库查询:

  • HyDE 生成的假设文档引入了"性欲"等无关术语,贡献为负
  • Web 搜索返回的互联网内容和个人修行笔记无关,贡献为零

这意味着用户为这两次调用支付的 ¥0.10 和 3 秒延迟,完全是浪费


三、HyDE 开关:假设性检索的性价比反思

3.1 HyDE 的价值曲线

HyDE 不是一无是处。它的价值可以用一条"查询复杂度-收益"曲线来描述:

收益
 ^
 │      ╭────── 短查询/模糊查询收益高
 │     ╱
 │    ╱
 │   ╱
 │  ╱
 │ ╱
 │╱
 └──────────────────→ 查询复杂度(长度+明确性)
      短/模糊      长/明确
  • 短查询(< 10 字) :如"什么是 RRF",用户意图模糊,HyDE 的语义补全价值大
  • 长查询(> 20 字) :如"修行前后的反应模式与情绪内化的关系",用户意图已明确,HyDE 的"帮忙理解"反而可能理解偏了
  • 主观性/笔记类查询:知识库内容是非标准化术语,HyDE 的"专家视角"与原文术语不对齐,收益为负

3.2 开关实现

在 node_search_embedding_hyde_qdrant.py 中:

def node_search_embedding_hyde(state):
    # 用户未开启 HyDE 开关,直接跳过
    if not state.get("enable_hyde", False):
        logger.info("用户未开启 HyDE 假设性检索,跳过 HyDE 节点")
        return {"hyde_embedding_chunks": []}
    
    # 正常 HyDE 流程...
    hyde_doc = step_1_create_hyde_doc(rewritten_query)
    resp = step_2_search_embedding_hyde(...)
    return {"hyde_embedding_chunks": resp}

这个设计的精妙之处在于:开关在节点入口拦截,不改动 LangGraph 的拓扑结构。

为什么不在 main_graph.py 中用条件边控制?

# 方案 A:条件边(复杂,改动大)
builder.add_conditional_edges(
    "node_item_name_confirm",
    lambda state: "hyde" if state.get("enable_hyde") else "skip",
    {"hyde": "node_search_embedding_hyde", "skip": "node_rrf"}
)

# 方案 B:节点内部短路(简单,可扩展)
def node_search_embedding_hyde(state):
    if not state.get("enable_hyde"):
        return {"hyde_embedding_chunks": []}
    # ... 正常逻辑

方案 B 的优势:

  1. 图结构稳定:不需要为每个开关改条件边,降低图的复杂度
  2. 状态兼容:即使开关关闭,下游 RRF 节点接收到的 hyde_embedding_chunks 仍然是合法的空列表,不会报错
  3. 调试友好:日志中可以看到"跳过 HyDE 节点"的明确记录

3.3 开关关闭时的链路变化

# 开启 HyDE
RRF 输入: Embedding(权重1.0) + HyDE(权重0.9) + CrossKB(权重1.2)
         → 多路结果融合

# 关闭 HyDE
RRF 输入: Embedding(权重1.0) + CrossKB(权重1.2)
         → 两路结果融合(HyDE 这一路为零贡献)

关闭 HyDE 后,RRF 的权重分布自动调整:

  • Embedding 的相对权重从 1.0 / (1.0 + 0.9 + 1.2) = 32% 提升到 1.0 / (1.0 + 1.2) = 45%
  • 这意味着普通向量检索的话语权更大,系统更忠实于用户的原始查询语义

四、联网搜索开关:外部知识补充的可控性

4.1 Web 搜索的双刃剑效应

Web 搜索的加入本意是弥补知识库的覆盖盲区,但它带来了三个副作用:

副作用一:来源冲突

知识库说"HAK 180 烫金机的建议温度是 110℃",Web 搜索可能返回一篇博客说"HAK 180 建议 120℃"。Rerank 时如果 Web 结果分数更高,LLM 可能会引用错误信息。

副作用二:主题污染

用户问"修行前后的反应模式",Web 搜索可能返回心理学文章、佛教科普、甚至瑜伽教程。这些内容虽然相关,但术语体系和知识库原文完全不同,会把检索向量拉偏。

副作用三:实时性陷阱

Web 搜索返回的信息是最新的,但不一定最准确。对于产品手册、技术文档这类"稳定性知识",知识库的版本才是权威来源,Web 搜索的"最新信息"反而可能是未经验证的谣言。

4.2 开关实现

在 node_web_search_tavily.py 中:

def node_web_search_tavily(state):
    # 用户未开启联网搜索开关,直接跳过
    if not state.get("enable_web_search", False):
        logger.info("用户未开启联网搜索,跳过 Web 搜索节点")
        return {"web_search_docs": []}
    
    # 正常 Tavily 搜索流程...
    response = requests.post(TAVILY_API_URL, ...)
    return {"web_search_docs": web_documents}

4.3 开关关闭时的链路变化

# node_rerank.py 中的 merge 逻辑
def step_1_merge_rrf_mcp(state):
    rrf_chunks = state.get("rrf_chunks", [])      # 来自 Embedding/HyDE/KG
    web_search_docs = state.get("web_search_docs", [])  # 来自 Web 搜索
    
    # 本地结果 + Web 结果合并后统一 Rerank
    for chunk in rrf_chunks:
        chunks_list.append({..., "source": "local"})
    for doc in web_search_docs:
        chunks_list.append({..., "source": "web"})

关闭 Web 搜索后,web_search_docs 为空列表,chunks_list 中只有 source="local" 的文档。这意味着:

  1. Rerank 的候选池更小:只从知识库中挑选,避免了 Web 来源的干扰
  2. Local Source Boost 效果更纯粹:之前加的 +0.08 本地优先级加分,在只有本地文档时自然失效(大家都是 local,等于没加),不会产生偏置
  3. 引用卡片更干净:不会出现"一半来自知识库,一半来自网页"的混乱引用列表

五、架构设计:LangGraph 中的条件执行哲学

5.1 两种条件执行模式

本系统采用了**"节点内部短路"**模式,而不是传统的条件边模式。这背后有一个架构层面的权衡:

维度条件边模式节点内部短路模式
图复杂度高(每增加一个开关,增加一条条件边和分支)低(图结构不变,节点内部判断)
可视化清晰度中(图上有分支,但运行时只走一条)高(图结构稳定,运行时日志说明跳过)
状态兼容性中(跳过节点时,下游节点可能期望字段不存在)高(跳过节点返回空列表,下游无感知)
扩展成本高(新增开关需要改图结构)低(新增开关只需改节点内部)
调试难度中(需要看条件边判断逻辑)低(日志直接打印"跳过 XXX 节点")

对于本系统这种组件较多、开关频繁调整的场景,节点内部短路模式显然更合适。

5.2 状态机设计

两个开关都作为 QueryGraphState 的字段,从 API 层一路透传到节点层:

class QueryGraphState(TypedDict):
    # ... 其他字段
    enable_web_search: bool   # 是否开启联网搜索,默认 False
    enable_hyde: bool         # 是否开启 HyDE 假设性检索,默认 False

API 层(query_server.py):

class QueryRequest(BaseModel):
    # ... 其他字段
    enable_web_search: bool = Field(False, title="是否开启联网搜索,默认关闭")
    enable_hyde: bool = Field(False, title="是否开启 HyDE 假设性检索,默认关闭")

def run_query_graph(..., enable_web_search: bool = False, enable_hyde: bool = False):
    state = create_query_default_state(
        # ... 其他字段
        enable_web_search=enable_web_search,
        enable_hyde=enable_hyde
    )
    query_app.invoke(state)

这种设计的优点是全链路无状态透传:从 HTTP 请求 → Pydantic 模型 → LangGraph State → 节点函数,开关值始终保持一致,不存在某一层丢失的情况。


六、成本与延迟的量化分析

6.1 四种配置组合的成本对比

假设单次 LLM 调用平均 ¥0.05,Tavily API 单次 ¥0.02,Qdrant 检索免费:

配置HyDEWebLLM 调用次数API 调用单次成本延迟
最小链路3 次0¥0.23~3.5s
仅 HyDE4 次0¥0.31~5.0s
仅 Web3 次1¥0.25~5.0s
全量链路4 次1¥0.33~7.2s

最小链路(默认配置)相比全量链路,成本降低 30%,延迟降低 51%

6.2 什么时候值得开启?

场景推荐配置理由
查询个人知识库/笔记HyDE❌ Web❌知识库已覆盖,外部内容会污染结果
查询产品手册,但知识库版本较旧HyDE❌ Web✅需要最新信息补充
短查询/模糊查询,知识库覆盖不全HyDE✅ Web❌HyDE 的语义补全有价值
研究性探索,需要多方观点HyDE✅ Web✅知识库 + HyDE + Web 三路互补

七、产品层面的决策框架

7.1 给用户选择权,还是系统自动决策?

本系统目前选择了给用户显式开关。另一种方案是系统自动判断:

def auto_decide(query: str, kb_coverage_score: float) -> dict:
    """自动决定是否启用 HyDE 和 Web 搜索"""
    result = {"enable_hyde": False, "enable_web_search": False}
    
    # 查询很短或模糊时启用 HyDE
    if len(query) < 15 or is_vague_query(query):
        result["enable_hyde"] = True
    
    # 知识库覆盖度低时启用 Web 搜索
    if kb_coverage_score < 0.3:
        result["enable_web_search"] = True
    
    return result

给用户开关的优势:

  • 透明度高,用户知道系统在做什么
  • 适合"专家用户",他们清楚自己的需求
  • 调试方便,可以 A/B 测试不同配置的效果

自动决策的优势:

  • 零学习成本,普通用户无需理解 HyDE 是什么
  • 可以根据历史数据持续优化决策策略

本系统的选择是给用户开关,因为当前用户群体是开发者/高级用户,他们有能力也有意愿控制检索链路的行为。未来可以在开关基础上叠加"智能推荐"——系统根据查询特征推荐是否开启,但最终决策权交给用户。

7.2 开关的默认状态哲学

两个开关都默认关闭,这体现了一个产品设计原则:

默认配置应该是"最可靠"的,而不是"最强大"的。

  • 关闭 HyDE:避免术语漂移,保证检索忠实于用户原意
  • 关闭 Web 搜索:避免外部信息污染知识库的权威性
  • 用户明确开启后,系统才承担对应的成本和风险

这与很多 SaaS 产品"默认开启所有功能"的做法相反,但更符合 RAG 系统的本质——知识库的自有内容才是最可信的,外部增强只是补充。


八、总结

本系统通过为 HyDE 和 Web 搜索增加显式开关,实现了三个层面的可控性:

层面可控性体现
成本默认配置单次查询 ¥0.23,相比全量链路节省 30%
延迟默认配置 3.5s 响应,相比全量链路缩短 51%
准确率避免 HyDE 术语漂移和 Web 搜索主题污染,知识库查询准确率提升

关键工程决策:

  1. 节点内部短路优于条件边,保持图结构稳定
  2. 全链路状态透传,从 HTTP API 到 LangGraph State 到节点函数,开关值不丢失
  3. 默认关闭优于默认开启,把"最可靠"作为默认配置

RAG 系统的可控性设计不是"做减法",而是给每个组件划定清晰的能力边界,让用户在正确的时间调用正确的工具。

正如一位前辈所说:

"一个优秀的系统不是因为它有什么都能做的能力,而是因为它知道什么时候不该做。"


作者正在寻找 AI 工程方向的机会,欢迎交流。