# HyDE 假设性文档:实现 doc-doc 对称检索

先生成假设性答案文档,再用文档到文档的相似性提高召回。

# 01. HyDE 混合策略

在前面的课时中,掌握的优化策略都是将对应的 查询 生成 新查询,通过 新查询 来执行相应的检索,但是在数据库中存储的数据一般都是 文档 层面上的,数据会远远比 查询 要大很多,所以 query 和 doc 之间是不对称检索,能找到的相似性文档相对来说也比较少。

例如:**今天回家的路上看到了美丽的风景,非常开心!想掌握 python 该怎么办?**这个请求中,前面的风景、开心等词语均为无关信息。会对真实的请求掌握 python 产生干扰。如果直接搜索用户的请求,可能会产生不正确或无法回答的 LLM 响应。因此,有必要使得用户查询的语义空间与文档的语义空间保持一致。

特别是 query 和对应的相关内容(答案)可能只存在弱相关性,导致难以找到最相关的文档内容。

在这篇论文《Precise Zero-Shot Dense Retrieval without Relevance Labels》中提出了一个 HyDE混合策略 的概念,首先利用 LLM 将问题转换为回答问题的假设性文档/假回答,然后使用嵌入的 假设性文档 去检索真实文档,前提是因为 doc-doc 这个模式执行相似性搜索可以尝试更多的匹配项。

论文地址:https://arxiv.org/pdf/2212.10496

[!IMPORTANT] 假回答和真回答虽然可能存在事实错误,但是会比较像,因此能更容易找到相关内容。

简单来说,就是先根据 query 生成一个 doc,然后根据 doc 生成对应的 embedding,再执行相应的检索,运行流程如下:

hyde-doc-doc-retrieval 图 1

在 LangChain 中,并没有封装基于 HyDE混合策略 的检索器,所以需要自定义一个检索器,并实现 _get_relevant_documents() 方法,具象化代码如下:

from typing import List

import dotenv
import weaviate
from langchain_core.callbacks import CallbackManagerForRetrieverRun
from langchain_core.documents import Document
from langchain_core.language_models import BaseLanguageModel
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.retrievers import BaseRetriever
from langchain_core.runnables import RunnablePassthrough
from langchain_openai import OpenAIEmbeddings, ChatOpenAI
from langchain_weaviate import WeaviateVectorStore
from weaviate.auth import AuthApiKey

dotenv.load_dotenv()

class HyDERetriever(BaseRetriever):
 """HyDE混合策略检索器"""
 retriever: BaseRetriever
 llm: BaseLanguageModel

 def _get_relevant_documents(
 self, query: str, *, run_manager: CallbackManagerForRetrieverRun
 ) -> List[Document]:
 """传递检索query实现HyDE混合策略检索"""
 # 1.构建生成假设性文档的prompt
 prompt = ChatPromptTemplate.from_template(
 "请写一篇科学论文来回答这个问题。\n"
 "问题: {question}\n"
 "文章: "
 )

 # 2.构建链应用
 chain = (
 {"question": RunnablePassthrough()}
 | prompt
 | self.llm
 | StrOutputParser()
 | self.retriever
 )

 return chain.invoke(query)

# 1.构建向量数据库与检索器
db = WeaviateVectorStore(
 client=weaviate.connect_to_wcs(
 cluster_url="https://mbakeruerziae6psyex7ng.c0.us-west3.gcp.weaviate.cloud",
 auth_credentials=AuthApiKey("ZltPVa9ZSOxUcfafelsggGyyH6tnTYQYJvBx"),
 ),
 index_name="DatasetDemo",
 text_key="text",
 embedding=OpenAIEmbeddings(model="text-embedding-3-small"),
)
retriever = db.as_retriever(search_type="mmr")

# 2.创建HyDE检索器
hyde_retriever = HyDERetriever(
 retriever=retriever,
 llm=ChatOpenAI(model="gpt-3.5-turbo-16k", temperature=0),
)

# 3.检索文档
documents = hyde_retriever.invoke("关于LLMOps应用配置的文档有哪些?")
print(documents)
print(len(documents))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

输出内容:

[Document(metadata={'source': './项目API文档.md', 'start_index': 0.0}, page_content='LLMOps 项目 API 文档\n\n应用 API 接口统一以 JSON 格式返回,并且包含 3 个字段:code、data 和 message,分别代表业务状态码、业务数据和接口附加信息。\n\n业务状态码共有 6 种,其中只有 success(成功) 代表业务操作成功,其他 5 种状态均代表失败,并且失败时会附加相关的信息:fail(通用失败)、not_found(未找到)、unauthorized(未授权)、forbidden(无权限)和validate_error(数据验证失败)。\n\n接口示例:\n\njson { "code": "success", "data": { "redirect_url": "https://github.com/login/oauth/authorize?client_id=f69102c6b97d90d69768&redirect_uri=http%3A%2F%2Flocalhost%3A5001%2Foauth%2Fauthorize%2Fgithub&scope=user%3Aemail" }, "message": "" }'), Document(metadata={'source': './项目API文档.md', 'start_index': 1621.0}, page_content='id -> uuid:应用 id,类型为 uuid。\n\nname -> string:应用名称。\n\nicon -> string:应用图标。\n\ndescription -> string:应用描述。\n\npublished_app_config_id -> uuid:已发布应用配置 id,如果不存在则为 null。\n\ndrafted_app_config_id -> uuid:草稿应用配置 id,如果不存在则为 null。\n\ndebug_conversation_id -> uuid:调试会话记录 id,如果不存在则为 null。\n\npublished_app_config/drafted_app_config -> json:应用配置信息,涵盖草稿配置、已发布配置,如果没有则为 null,两个配置的变量信息一致。\n\nid -> uuid:应用配置 id。\n\nmodel_config -> json:模型配置,类型为 json。\n\ndialog_round -> int:携带上下文轮数,类型为非负整型。'), Document(metadata={'source': './项目API文档.md', 'start_index': 5818.0}, page_content='json { "code": "success", "data": { "list": [ { "id": "1550b71a-1444-47ed-a59d-c2f080fbae94", "conversation_id": "2d7d3e3f-95c9-4d9d-ba9c-9daaf09cc8a8", "query": "能详细讲解下LLM是什么吗?", "answer": "LLM 即 Large Language Model,大语言模型,是一种基于深度掌握的自然语言处理模型,具有很高的语言理解和生成能力,能够处理各式各样的自然语言任务,例如文本生成、问答、翻译、摘要等。它通过在大量的文本数据上进行训练,掌握到语言的模式、结构和语义知识'), Document(metadata={'source': './项目API文档.md', 'start_index': 490.0}, page_content='带有分页数据的接口会在 data 内固定传递 list 和 paginator 字段,其中 list 代表分页后的列表数据,paginator 代表分页的数据。\n\npaginator 内存在 4 个字段:current_page(当前页数) 、page_size(每页数据条数)、total_page(总页数)、total_record(总记录条数),示例数据如下:')]
4
1
2

# 02. 局限性与失败案例

对于 doc-doc 类型的检索,虽然在语义空间上保持了一致,但是在 query->doc 的过程中,受限于各种因素,仍然可能产生错误信息。

第一个场景是在 query 没有足够上下文时,HyDE 容易误解对应的词,从而产生错误的信息。

例如提问 Bel是什么?,在没有执行 HyDE 混合策略而是直接查询得到答案如下:

Bel 是由 Paul Graham 在四年的时间里(2015年3月26日至2019年10月12日),用 Arc 语言编写的一种编程语言。它基于 John McCarthy 最初的 Lisp,但添加了额外的功能。它是一个以代码形式表达的规范,旨在成为计算的形式化模型,是图灵机的一种替代方案。
1

但是执行 HyDE 混合策略生成假设性 doc 如下:

Bel 是 Paul Graham 的化名,他是这段信息背后的作者,当时需要种子资金以维持生活,并且参与了一项交易,后来成为 Y Combinator 模式的典范。
1

在这个例子中,HyDE 在没有文档上下文的情况下错误地解释了 Bel,这会导致完全检索不到相关的文档信息。

第二个场景是一些 开放式的查询,HyDE 可能会产生偏见,例如提问 作者会如何评价艺术与工程的区别?,无需转换 query 即可得到正确的响应回答:

作者可能会说,艺术和工程是两种需要不同技能和方法的学科。艺术更注重表达和创造力,而工程更专注于解决问题和技术知识。作者还暗示,艺术学校并不总是提供与工程学校同等水平的严谨性,绘画学生常常被鼓励发展个性化风格,而不是掌握绘画的基础知识。此外,作者可能会指出,工程学相比艺术能提供更多的财务稳定性,正如作者自己创业初期需要种子资金来生活的经历所证明的那样。
1

在使用 HyDE混合策略 转换 query 时,生成的 doc 如下:

作者可能会说,艺术比工程更持久和独立。他们提到,今天编写的软件几十年后就会过时,系统工作也不会长久。相比之下,他们指出绘画可以保留数百年,而且作为艺术家是可以谋生的。他们还提到,作为艺术家,你可以真正独立,不需要老板或研究资金。此外,他们指出艺术可以成为收入来源,适合那些无法接触传统就业形式的人,比如例子中的模特,能够通过为当地古董商建模和制作赝品而谋生。
1

总的来说,HyDE 是一个无监督的方法,可以帮助 RAG 提高效果。但是因为它不完全依赖于 embedding 而是强调问题的答案和查找内容的相似性,也存在一定的局限性。比如如果 LLM 无法理解用户问题,自然不会产生最佳结果,也可能导致错误增加。因此,需要根据场景决定是否选用此方法。

# 最新版 LangChain 用法提示

新项目要优先确认组件所在包名。核心抽象通常在 langchain_core,文档分割在 langchain_text_splitters,OpenAI 相关集成在 langchain_openai,大量社区组件在 langchain_community 或独立集成包中。

涉及 Retriever、Runnable、LCEL、LangGraph 的链路,建议用 invoke() / ainvoke() 作为统一调用入口,并把检索参数、路由条件、重排参数和降级策略显式配置出来。不要只照搬旧导入路径。

# 拓展

HyDE 假设性文档:实现 doc-doc 对称检索 不应该孤立使用。RAG 优化通常要和评测集、召回日志、答案引用、用户反馈、成本统计一起看。只优化某一个环节,可能会让另一个环节退化。

工程上建议把 query、改写后的 query、召回文档、metadata filter、重排得分、最终 Prompt 和模型输出都记录下来。否则问题出现时,很难判断是加载、切分、Embedding、检索、重排、Prompt 还是生成阶段出了问题。

# 常见问题

什么时候需要使用这个策略?

当普通向量检索已经不能稳定命中关键证据,或者复杂问题经常漏召回、召回重复、上下文不完整时,就需要引入该类优化。

它能替代基础 RAG 链路吗?

不能。优化策略依赖基础链路。文档质量、chunk 设计、Embedding 模型、metadata 规范和 Retriever 配置没有打好,后续策略只能缓解,不能根治。

如何判断优化是否有效?

用固定评测集对比优化前后:召回命中率、答案正确率、引用正确率、拒答准确率、延迟和成本。只看单个 demo 很容易误判。

# 面试题

RAG 优化应该从哪里开始?

先定位问题阶段:文档是否解析正确、chunk 是否完整、Embedding 是否适合、检索是否命中、上下文是否可用、模型是否按证据回答。定位后再选择对应策略。

为什么复杂 RAG 系统需要可观测性?

因为答案错误可能来自任意环节。没有检索日志、重排分数、Prompt 和输出记录,就只能靠猜。

如何避免优化策略越加越乱?

每个策略都要有触发条件、输入输出、评测指标和降级方案。能用简单策略解决的问题,不要过早引入复杂链路。

# 生产问题排查

问题 常见原因 处理方式
优化后更慢 多路检索、重排或图分支增加调用次数 增加超时、缓存、并行和候选裁剪
召回更多但更乱 多查询或混合检索没有去重和重排 使用 RRF、rerank、source_id 去重
答案仍然无依据 上下文质量低或 Prompt 没有约束证据 增加引用要求、拒答策略和证据检查
成本上涨明显 过多 LLM 改写、摘要或评估调用 缓存中间结果,限制触发条件
线上效果不稳定 缺少评测集和回归流程 固化 golden set,发布前跑回归