“明明 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 检索的坑! 🚀
如需完整代码或有疑问,评论区见!