检索与重排

索引做完之后,线上只有一件事:用问题找到该给 LLM 看的块。默认路径是稠密向量 top-k;质量上不去时再加过滤、关键词混合、可选 rerank。

flowchart LR
  Q[question] --> E[embed]
  E --> R[retrieve top-k]
  R --> F[metadata filter]
  F --> RR[optional rerank]
  RR --> L[LLM]

过滤可以在向量库内部做(Chroma where、pgvector WHERE、Qdrant query_filter),不必先取出 1000 条再在 Python 里丢。


top-k 怎么选

k效果
2–4延迟低;容易漏跨段答案
4–8手册 / FAQ 的合理起点
15+ 再 rerank 到 4召回够、提示词仍短

k 不是越大越好:窗口被噪声填满后,模型更容易忽略真正相关的那一段。先固定 k,用 评估 的 hit@k 再改切块。

def format_hits(ids: list[str], docs: list[str], metas: list[dict]) -> str:
    lines = []
    for i, doc, meta in zip(ids, docs, metas):
        src = meta.get("source", "")
        lines.append(f"[{src} #{i}]\n{doc}")
    return "\n\n".join(lines)

元数据过滤

问题「人事部的请假天数」不应检索到工程 Wiki。索引时写入 deptlangsource,查询时带上:

  • Chroma:where={"dept": "hr"}
  • pgvector:WHERE dept = 'hr' ORDER BY embedding <=> %s
  • Qdrant:Filter(must=[FieldCondition(...)])

过滤是权限边界,不是「提分技巧」。租户 ID 必须在服务端强制,不能让模型自己挑。


混合检索(向量 + 关键词)

稠密向量擅长同义改写(「退货」≈「退款」);关键词擅长条款号、SKU、错误码(KH-8842ECONNRESET)。简单融合:各取 top-n,按 Reciprocal Rank Fusion 合并

def rrf(rank_lists: list[list[str]], k: int = 60) -> list[str]:
    scores: dict[str, float] = {}
    for ranks in rank_lists:
        for r, doc_id in enumerate(ranks, start=1):
            scores[doc_id] = scores.get(doc_id, 0.0) + 1.0 / (k + r)
    return sorted(scores, key=scores.get, reverse=True)


dense_ids = ["c3", "c1", "c9"]      # 向量库返回
bm25_ids = ["c1", "c4", "c3"]       # 自己用倒排或 Postgres FTS
print(rrf([dense_ids, bm25_ids])[:4])

真正的 BM25 可用 PostgreSQL 全文检索、或 Qdrant sparse vector(见 Qdrant 预告)。不要在没测量 hit@k 之前上复杂混合。


可选 rerank

粗召回 20 条后,用「问题 + 文档」对重新打分,只把前 4 条送进 LLM。CPU 上可用交叉编码器(不必 GPU):

# pip install sentence-transformers
from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-base")  # CPU 可跑,偏慢


def rerank(question: str, docs: list[str], top: int = 4) -> list[str]:
    scores = reranker.predict([(question, d) for d in docs])
    order = sorted(range(len(docs)), key=lambda i: float(scores[i]), reverse=True)
    return [docs[i] for i in order[:top]]

没有余力装模型时,用词面重叠做教学级 rerank(不能替代交叉编码器):

def overlap_rerank(question: str, docs: list[str], top: int = 4) -> list[str]:
    q = set(question)
    scored = sorted(docs, key=lambda d: len(q & set(d)), reverse=True)
    return scored[:top]

LangChain / LlamaIndex / Dify 里的 Rerank 组件做的是同一件事,见 LangChain RAGDify 知识库


提示词约束

检索再准,提示词若允许「发挥」,模型仍会编。固定三句:

  1. 只根据下列上下文回答。
  2. 上下文没有的事实不要编造(含数字、工号、日期)。
  3. 依据不足时明确说不知道,并列出你参考过的 source

生成质量用忠实度检查,见下一章。


下一步

评论