Впровадження HyDE (Hypothetical Document Embeddings) для RAG
HyDE — це техніка поліпшення retrieval, запропонована Gao et al. (2022). Замість пошуку документів за embedding запиту безпосередньо, LLM генерує гіпотетичну відповідь на питання, а пошук виконується за embedding цієї відповіді. Гіпотетична відповідь розташована у просторі "документів", а не "запитів", тому її embedding краще збігається з реальними документами.
Чому HyDE працює
Асиметрія embedding простору: запитання та відповіді — різні розподіли у векторному просторі. Embedding запиту "який строк позивної давності для трудових спорів" падає в область запитів, а не документів з відповідями. HyDE генерує текст, подібний до документів у корпусі.
Стандартний RAG:
Запит → Embedding(запит) → пошук → документи
HyDE:
Запит → LLM → Гіпотетична_відповідь → Embedding(відповідь) → пошук → документи
Впровадження HyDE
from langchain.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain.retrievers import ParentDocumentRetriever
import asyncio
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
hyde_prompt = ChatPromptTemplate.from_template("""
Напиши абзац для підтримки/відповіді на питання
Питання: {query}
Абзац:""")
async def hyde_retriever(query: str, base_retriever, top_k: int = 5):
# Крок 1: Генеруємо гіпотетичний документ
hypothetical_docs = await asyncio.gather(
*[asyncio.to_thread(
lambda: llm.invoke(hyde_prompt.format(query=query)).content
) for _ in range(3)]
)
# Крок 2: Вбудовуємо кожний гіпотетичний документ та витягуємо
all_docs = []
for hypo_doc in hypothetical_docs:
docs = await asyncio.to_thread(
lambda: base_retriever.invoke(hypo_doc)
)
all_docs.extend(docs)
# Крок 3: Дедублюємо та повертаємо top-k
seen = set()
unique_docs = []
for doc in all_docs:
if doc.metadata.get("id") not in seen:
unique_docs.append(doc)
seen.add(doc.metadata.get("id"))
return unique_docs[:top_k]
Multi-Query HyDE
Комбінуємо HyDE з підходом multi-query для кращого охоплення:
class MultiQueryHyDE:
def __init__(self, llm, embeddings, base_retriever):
self.llm = llm
self.embeddings = embeddings
self.retriever = base_retriever
def generate_hypothetical_answers(self, query: str, num=3) -> list:
"""Генеруємо кілька гіпотетичних відповідей"""
prompt = f"""Генеруй {num} правдоподібних гіпотетичних відповідей на: {query}
Кожна відповідь повинна бути повним, самостійним абзацом."""
response = self.llm.invoke(prompt)
answers = response.content.split("\n\n")
return [a.strip() for a in answers if a.strip()]
def retrieve(self, query: str, top_k: int = 5) -> list:
hypothetical_answers = self.generate_hypothetical_answers(query)
all_docs = []
for answer in hypothetical_answers:
docs = self.retriever.invoke(answer)
all_docs.extend(docs)
# Дедублюємо та повертаємо
seen = set()
result = []
for doc in all_docs:
doc_id = doc.metadata.get("id")
if doc_id not in seen:
result.append(doc)
seen.add(doc_id)
return result[:top_k]
Практичний кейс: пошук технічної документації
Завдання: асистент для 50 000 технічних статей (середня довжина 2500 слів).
Результати:
| Метрика | Стандартний RAG | HyDE |
|---|---|---|
| Context Recall | 0.62 | 0.81 |
| MRR@5 | 0.58 | 0.74 |
| P@1 | 0.34 | 0.52 |
| Latency | 800ms | 1200ms (+HyDE генерація) |
HyDE поліпшив recall на 31% з прийнятним збільшенням latency.
Терміни
- Впровадження HyDE retriever: 2–3 дні
- Тестування та підбір промпту: 2–3 дні
- Порівняння з baseline: 1–2 дні
- Всього: 1 тиждень







