面试 · 2025年7月4日 0

用滑动窗口批量推理,拯救你的向量检索慢——一场“块”上加速的实战

“明明 GPU 很强,为什么检索还是像蜗牛?”
——一位被 embedding 拖慢的开发者

前言

在大模型+向量数据库的检索场景下,事实验证经常需要“在一堆文档块里找出和事实最相关的那几块”。
一开始你可能很天真:

“直接 embedding 检索+reranker,不就完事了吗?”

很快你会发现,准确率还行,但有些事实是文档块的子句,embedding 检索容易漏掉。于是你祭出了“滑动窗口”大法:

  • 把每个 chunk 再切成多个窗口,和 fact 做 embedding 相似度,窗口命中就算相关。

结果——准确率提升了,速度却慢到怀疑人生


1. 朴素滑动窗口:一刀切,慢得优雅

最初的代码大概长这样:

for chunk in all_chunks:
    for i in range(0, len(chunk) - window_size + 1, step):
        window = chunk[i:i+window_size]
        window_emb = embedding_model.encode([window])
        score = cosine_similarity(fact_emb, window_emb)
        if score > threshold:
            # 命中

问题:

  • 每个 chunk 都滑窗,窗口数量爆炸。
  • embedding 推理一窗一推,GPU/CPU 资源浪费。
  • 结果:准确率高,速度感人。

2. 优化一:只对 Top-N Chunk 滑窗

思路:
先用 embedding 检索+reranker,挑出最相关的前 N 个 chunk,再滑窗。

semantic_results = retriever.hybrid_search_db(query=fact, top_k_embedding=5)
top_chunks = [r["document"] for r in semantic_results[:2]]  # 只滑前2个

效果:

  • 只对最有希望的块滑窗,窗口数骤减。
  • 速度提升,准确率基本不变。

3. 优化二:滑窗批量推理,GPU 吃饱饭

思路:
把所有窗口文本收集起来,一次性 batch 推理 embedding。

all_windows = []
window_info = []
for chunk in top_chunks:
    for i in range(0, len(chunk) - window_size + 1, window_size):
        window = chunk[i:i+window_size]
        all_windows.append(window)
        window_info.append((chunk, i, i+window_size))

window_embs = embedding_model.encode(all_windows)  # 一次性推理

效果:

  • embedding 推理速度提升数倍。
  • 资源利用率高,响应时间大幅缩短。

4. 优化三:只输出每个 Chunk 的最佳窗口

思路:
同一个 chunk 可能多个窗口命中,但你只关心“最佳命中”即可。

chunk_best = {}
for idx, (chunk, start, end) in enumerate(window_info):
    score = cosine_similarity(fact_emb, window_embs[idx])
    if score >= threshold:
        if chunk not in chunk_best or score > chunk_best[chunk]["window_similarity"]:
            chunk_best[chunk] = {
                "chunk": chunk,
                "window_similarity": score,
                "window_start": start,
                "window_end": end,
            }
# 只输出最佳
for best in chunk_best.values():
    print(f"[滑动窗口最佳匹配] fact: '{fact}' 匹配到 chunk,window_similarity={best['window_similarity']:.4f}")

效果:

  • 日志简洁,分析方便。
  • 结果更聚焦,避免重复。

5. 终极合并:相关块去重,分数优先

思路:
无论是 semantic、contains 还是 window_embedding,每个 chunk 只保留分数最高的那种类型,最终只返回 top_k 个。

all_relevant = {}
for c in semantic_chunks + contains_chunks + window_chunks:
    key = c["chunk"]
    new_score = c.get("reranker_score", c.get("window_similarity", -1))
    old_score = all_relevant[key].get("reranker_score", all_relevant[key].get("window_similarity", -1)) if key in all_relevant else -1
    if key not in all_relevant or new_score > old_score:
        all_relevant[key] = c
sorted_chunks = sorted(
    all_relevant.values(),
    key=lambda x: x.get("reranker_score", x.get("window_similarity", -1)),
    reverse=True
)[:top_k]

效果:

  • 结果更精炼,相关性更高。
  • 用户体验 up up up!

6. 总结

  • 滑动窗口让事实验证更精准,但要用得巧,别让它拖慢系统。
  • 批量推理+只滑 Top-N+只保留最佳窗口+最终去重合并,让你的检索又快又准。
  • 代码优化不是玄学,是一场“块”上加速的理性革命!

“优化不是一蹴而就,而是每一行代码的精雕细琢。”
——你,优化完滑动窗口后


欢迎点赞、收藏、转发,让更多人少踩 embedding 检索的坑! 🚀


如需完整代码或有疑问,评论区见!