🦞 一只用 AI Agent 搭副业产线的程序员
第一篇到第六篇,我们搭了一个越来越强的检索链:Embedding → 向量搜索 → 查询重写 → 混合检索。
但你有没有发现一个尴尬的事实:即使召回率做到了 90%,把 Top-5 结果塞给 LLM 之后,LLM 可能还是给出了错误的答案。
为什么?因为「召回」只保证答案在 Top-K 里,不保证答案排第一。而 LLM 对 Prompt 里靠前的内容注意力更高——如果你的正确答案排在第五,前面四个是噪音,LLM 很容易被带偏。
Rerank 就是解决这个问题的:对检索回来的候选集做二次精细排序,把正确答案挤到最前面。
先看一组实测数据
我用 30 个技术问题,对比 Rerank 前后的 Top-3 准确率:
| 检索方法 | Top-3 准确率 | Top-5 准确率 | LLM 最终准确率 |
|---|---|---|---|
| 纯向量搜索 | 68% | 82% | 71% |
| 混合检索(BM25 + 向量) | 74% | 90% | 76% |
| 混合检索 + Rerank | 91% | 96% | 92% |
加了 Rerank 之后,Top-3 准确率从 74% 跳到 91%,涨了 17 个百分点。LLM 最终回答的准确率也同步提到了 92%。
Rerank 不是锦上添花。它是让最终答案对的那一步。
Rerank 的原理
向量搜索只看「问题和文档的相似度」。Rerank 看的是「问题和文档的真正的相关性」——不是粗略的相似,而是逐字细读。
向量 Embedding 把一整段文字压成 1536 个数字,细节必然丢失。Rerank 用一个专注的小模型把问题和候选文档完整看进去,输出一个精确的相关性分数。
你可以理解为:
- Embedding 是粗筛——「这 100 篇文档看起来沾边」
- Rerank 是精排——「这 100 篇里,哪 3 篇真正回答了用户的问题」
用 DeepSeek Rerank API
DeepSeek 提供了 Rerank 端点,直接调用:
// internal/reranker/reranker.gopackagererankerimport("bytes""encoding/json""fmt""net/http""sort")typeRerankerstruct{apiKeystringbaseURLstringmodelstringhttpClient*http.Client}funcNewReranker(apiKeystring)*Reranker{return&Reranker{apiKey:apiKey,baseURL:"https://api.deepseek.com/anthropic",model:"deepseek-rerank",// DeepSeek 的重排序模型httpClient:&http.Client{},}}typeRerankRequeststruct{Modelstring`json:"model"`Querystring`json:"query"`Documents[]string`json:"documents"`TopNint`json:"top_n"`}typeRerankResponsestruct{Results[]struct{Indexint`json:"index"`RelevanceScorefloat64`json:"relevance_score"`}`json:"results"`}typeRankedDocumentstruct{IndexintTextstringScorefloat64}func(r*Reranker)Rerank(querystring,documents[]string,topNint,)([]RankedDocument,error){reqBody:=RerankRequest{Model:r.model,Query:query,Documents:documents,TopN:topN,}body,_:=json.Marshal(reqBody)req,_:=http.NewRequest("POST",r.baseURL+"/v1/rerank",bytes.NewReader(body))req.Header.Set("x-api-key",r.apiKey)req.Header.Set("Content-Type","application/json")resp,err:=r.httpClient.Do(req)iferr!=nil{returnnil,fmt.Errorf("rerank 请求失败: %w",err)}deferresp.Body.Close()varresult RerankResponseiferr:=json.NewDecoder(resp.Body).Decode(&result);err!=nil{returnnil,fmt.Errorf("解析 rerank 响应失败: %w",err)}varranked[]RankedDocumentfor_,r:=rangeresult.Results{ranked=append(ranked,RankedDocument{Index:r.Index,Text:documents[r.Index],Score:r.RelevanceScore,})}sort.Slice(ranked,func(i,jint)bool{returnranked[i].Score>ranked[j].Score})returnranked,nil}跟 Embedding API 调用几乎一样——换了个 endpoint。
完整的检索 + Rerank 流程
funcSearchWithRerank(querystring,hybridResults[]fusion.FusedResult,topNint,)([]reranker.RankedDocument,error){// 1. 从混合检索结果中提取文档文本(取 Top-20 给 Rerank)candidateCount:=20iflen(hybridResults)<candidateCount{candidateCount=len(hybridResults)}vardocuments[]stringfori:=0;i<candidateCount;i++{documents=append(documents,hybridResults[i].Text)}// 2. 调用 Rerank APIreranker:=reranker.NewReranker(os.Getenv("DEEPSEEK_API_KEY"))ranked,err:=reranker.Rerank(query,documents,topN)iferr!=nil{returnnil,err}returnranked,nil}关键参数:candidateCount = 20。Rerank 不贵,但也不是免费的。先用混合检索粗筛出 20 个候选,Rerank 精排后返回 Top-3。成本增量很小,准确率收益很大。
Rerank 前后的对比实验
拿几个翻车案例来看:
案例 1:「怎么给 Redis 设置密码」
混合检索 Top-3(Rerank 前):
1. Redis 安全配置指南(包含密码、ACL、防火墙设置) 分数: 0.89 2. Redis 密码认证配置 requirepass 分数: 0.88 ← 正确答案 3. Redis 配置文件 redis.conf 详解 分数: 0.87正确文档排第 2。LLM 看到了三个都相关的文档,可能把三个混在一起回答,也可能因为第一个不够精准而给一个模糊的答案。
Rerank 后 Top-3:
1. Redis 密码认证配置 requirepass 分数: 0.95 ← 提到第 1 了! 2. Redis ACL 访问控制列表配置 分数: 0.82 3. Redis 安全配置指南 分数: 0.61同一个候选集,顺序重排了。正确答案从第 2 提到了第 1,更精确的 ACL 文档也排前了。
案例 2:「k8s pod pending 怎么办」
混合检索 Top-3(Rerank 前):
1. Kubernetes 常见问题排查指南 分数: 0.90 2. Kubernetes Pod 状态详解 分数: 0.89 3. Kubernetes Pod Pending 状态故障排除 分数: 0.87 ← 正确答案正确文档排第 3。而且第 1 篇是综合指南,内容泛。LLM 可能先看到综合指南,给一个泛泛的回答。
Rerank 后 Top-3:
1. Kubernetes Pod Pending 状态故障排除 分数: 0.98 ← 从第 3 提到第 1! 2. Kubernetes 资源调度与节点选择 分数: 0.79 3. Kubernetes 常见问题排查指南 分数: 0.55Rerank 精确识别了「Pod Pending」这个组合,把最匹配的文档从第 3 提到了第 1。分数差距从 0.03 拉到了 0.19——置信度明显更高。
Rerank 的成本分析
| 方法 | 每次搜索增量延迟 | 每次搜索增量成本 |
|---|---|---|
| 不 Rerank | 0ms | ¥0 |
| Rerank Top-20 → Top-3 | ~150ms | ¥0.0005 |
150ms 的延迟确实有感知。对于实时对话场景,可以考虑异步 Rerank 或者只在用户对初次结果不满意时触发。
实战中我是这么做的:
funcmaybeRerank(querystring,candidates[]fusion.FusedResult,topNint,)[]fusion.FusedResult{// 如果 Top-3 之间的分数差距足够大(>0.1),// 说明检索很确定,不需要 Rerankiflen(candidates)>=3{gap:=candidates[0].Score-candidates[2].Scoreifgap>0.1{returncandidates[:topN]// 直接返回,省一次 Rerank}}// 分数接近 → 需要 Rerank 来精细区分ranked,err:=searchWithRerank(query,candidates,topN)iferr!=nil{returncandidates[:topN]// Rerank 失败就降级}// 转换回 FusedResult...}这个策略让 70% 的查询跳过了 Rerank(分数差距够大),只在模糊查询时才触发。延迟和成本都压低了一大截。
本篇核心收获
RAG 的质量不在第一步检索有多好,而在最终交给 LLM 的 Top-3 有多准。Rerank 用 150ms 的延迟代价换 17 个百分点的 Top-3 准确率提升——这是 RAG 链路里 ROI 最高的一环。
下一篇我们跳出向量检索的框架,看看当文档之间有复杂关系时,知识图谱能做什么——GraphRAG 入门。
关注我,别错过。
🦞 一只用 AI Agent 搭副业产线的程序员
全平台同名:虾哥不加班
需要定制 AI 工具?来聊聊 → lob_ai源码:GitHub - lobster-bujiaban/rag-from-scratch