Files

106 lines
4.3 KiB
Python
Raw Permalink Normal View History

2026-07-02 22:03:39 +08:00
"""Implement HyDE (Hypothetical Document Embeddings) query expansion.
HyDE improves dense retrieval by addressing the vocabulary gap between
short user queries and longer document passages:
User query → [LLM generates hypothetical answer]
embed hypothetical answer (not original query)
retrieve similar real passages from Milvus
The hypothetical answer uses the same vocabulary and phrasing as documents,
so its embedding is much closer to relevant chunks than a terse query embedding.
Usage:
expander = HyDEExpander()
retrieval_query = expander.expand(query, provider=..., model=...)
chunks = retrieval_service.retrieve(query=retrieval_query, ...)
When the LLM call fails, expand() falls back to the original query so the
retrieval pipeline degrades gracefully.
References:
Gao et al. (2022), "Precise Zero-Shot Dense Retrieval without Relevance Labels"
https://arxiv.org/abs/2212.10496
"""
from __future__ import annotations
from loguru import logger
from app.config.settings import settings
from app.services.llm.llm_factory import get_llm_client
# Maximum chars to trim from the hypothetical answer to avoid token overrun.
_MAX_HYPOTHESIS_CHARS = 600
# System prompt that instructs the LLM to write a passage *as if* it were
# from a regulatory document, not a conversation answer.
_HYDE_SYSTEM = (
"你是一位法规知识库专家。用户提出了一个问题,"
"请用50-120字写一段话,模拟如果相关法规文档中存在完美答案,"
"该段落会是什么内容。\n\n"
"要求:\n"
"- 使用与法规文档相同的正式书面语气\n"
"- 包含可能的条款编号、标准名称等关键术语\n"
"- 不要解释你在做什么,直接输出假设性段落\n"
"- 如问题过于模糊,写一段合理的通用法规说明"
)
class HyDEExpander:
"""Generate a hypothetical document passage to improve dense retrieval.
The expander is stateless — instantiate once and call expand() per query.
It requires no external dependencies beyond the project's existing LLM
client infrastructure.
"""
def expand(self, query: str) -> str:
"""Return a combined retrieval query: original query + hypothetical passage.
The combination ensures:
- Dense retrieval uses the enriched hypothetical text (semantic match).
- BM25 retrieval still benefits from the original query keywords.
The model used is ``settings.hyde_llm_model`` (dedicated lightweight model)
falling back to the main ``settings.llm_model`` when not configured.
If the LLM call fails for any reason, returns the original query unchanged.
"""
if not settings.hyde_enabled:
return query
# Use the dedicated HyDE model when configured; fall back to main LLM.
2026-08-06 11:08:46 +08:00
# A lightweight model (e.g. qwen3.6-flash) is sufficient for generating
2026-07-02 22:03:39 +08:00
# a short hypothetical passage and significantly reduces cost + latency.
provider = settings.hyde_llm_provider or settings.llm_provider
model = settings.hyde_llm_model or settings.llm_model
try:
client = get_llm_client(provider=provider, model=model)
resp = client.chat(
messages=[
{"role": "system", "content": _HYDE_SYSTEM},
{"role": "user", "content": f"问题:{query}"},
],
max_tokens=settings.hyde_max_tokens,
# Low temperature: we want a plausible, deterministic passage.
temperature=0.3,
)
if not resp.is_success or not resp.content:
logger.debug("HyDE LLM call failed or empty — using original query")
return query
hypothesis = resp.content.strip()[:_MAX_HYPOTHESIS_CHARS]
logger.debug("HyDE expanded query ({}{} chars)", len(query), len(hypothesis))
# Concatenate: the embedding model will see the full combined text,
# so the resulting vector leans toward the hypothetical document style.
return f"{query}\n\n{hypothesis}"
except Exception as exc: # noqa: BLE001 — intentional broad catch for graceful fallback
logger.warning("HyDE expansion failed: {} — using original query", exc)
return query