用 Jev 做 RAG 重排:答案更准、成本更低
TL;DR: 召回负责找到”看起来相关”的 chunk,重排(rerank)负责判断”哪个真的能回答问题”。用 Jev 的打分题(scoring)给每个候选 chunk 的相关性打 1-10 分,排序、按阈值截断,只让幸存者进入生成模型。用几次廉价的打分调用换来更少、更干净的上下文 token——答案更准,生成成本更低。文末附 50 行以内的最小实现。
这里的”重排”指什么
- 输入:用户问题 + 第一阶段召回器(向量、BM25 或混合检索)返回的 N 个候选 chunk。
- 任务类型:对每个 chunk 做一次
scoring——在固定分制上评估相关性。 - 输出:每个 chunk 一个结构化分数。示例数据(example fixture):
{"answer":8,"scale":[1,10],"confidence":0.86,"rationale":"直接定义了问题所问的重试策略。"}
answer 是相关性分,confidence 表示模型对自己这个分数有多大把握,rationale 在你回头排查检索质量时非常有用。
流程图
用户问题
|
v
第一阶段召回(top-50 候选)
|
v
逐 chunk 调 Jev scoring(相关性,1-10 分制)
|
v
按分数降序排序
|
+--> 分数 >= 7 且置信度可接受 --> 保留
|
+--> 其余 --> 丢弃
|
v
取前 k 个幸存 chunk 作为生成上下文
重活仍然在召回器,Jev 只负责判断”已经找到的东西够不够好”。
最小实现
import os, json, requests
URL = "https://openrouter.ai/api/v1/chat/completions"
# Confirm the exact model slug on the OpenRouter model page.
MODEL = "typesafe/jev-1.13"
def score_chunk(query: str, chunk: str) -> dict:
resp = requests.post(
URL,
headers={"Authorization": f"Bearer {os.environ['OPENROUTER_API_KEY']}"},
json={
"model": MODEL,
"messages": [
{"role": "system", "content":
"Score how well this passage answers the query, 1-10. "
"1 = irrelevant, 10 = directly and completely answers it."},
{"role": "user", "content": f"Query: {query}\n\nPassage: {chunk[:2000]}"},
],
"temperature": 0,
},
timeout=30,
)
resp.raise_for_status()
# 示例数据(example fixture):
# {"answer":8,"scale":[1,10],"confidence":0.86,"rationale":"..."}
return json.loads(resp.json()["choices"][0]["message"]["content"])
def rerank(query, chunks, min_score=7, top_k=5):
scored = [(score_chunk(query, c), c) for c in chunks]
scored.sort(key=lambda x: x[0]["answer"], reverse=True)
return [c for s, c in scored if s["answer"] >= min_score][:top_k]
rerank() 返回清洗后的上下文列表,RAG 栈其余部分原样不动。官方 API 的端点与字段以 typesafe.ai 官方文档为准。
阈值建议
| 配置项 | 建议 | 理由 |
|---|---|---|
| 分制 | [1, 10] | 足以区分”沾边”和”能回答” |
| 收录线 | 分数 >= 7 | 低于此线的 chunk 很少改变最终答案 |
| top-k 上限 | 5(在 3-8 之间调) | 即使大量 chunk 过线,生成 token 也有上限 |
| 打分低置信度 | 丢弃,或触发更宽召回重试 | 模型自己都没把握的分数,本身就是弱证据 |
| 批量说明 | 串行或小并发打分 | 批量重排案例讨论了吞吐与限流的平衡 |
本场景依赖 Jev 的三原语(three primitives)——scoring 是三种任务类型之一;成本与延迟(cost and latency)指南解释了为什么”廉价判断调用放在昂贵生成模型前面”通常在总花费上更划算。RAG chunk 批量重排的案例里给出了实测数据。
本文适用版本:Jev 1.13。
重排上线后的评估方法
离线回测
上线前先在历史问题集上构造一份离线基准:把”未重排”与”重排后”的上下文分别喂给生成模型,用同一批参考答案评估质量差异。常用的三个衡量维度:
| 指标 | 衡量方式 | 说明 |
|---|---|---|
| 召回命中率 | top-k 中含参考答案片段的比例 | 用人工标注集或 LLM 裁判均可 |
| 上下文 token 用量 | 喂给生成模型的总 token 数 | 直接影响成本,详见成本与延迟指南 |
| 人工相关分 | 1-5 分主观评估 | 每次抽取 20-50 条做基准对比 |
示例数据(example fixture)——一次离线对比的结果片段,字段含义仅供参考,具体数字以自己项目实测为准:
{
"without_rerank": {"avg_context_tokens": 3200, "hit_rate": 0.61},
"with_rerank": {"avg_context_tokens": 1100, "hit_rate": 0.74}
}
定期抽检
上线后每周从生产日志随机抽取若干问题,将重排前的候选集与最终留存 chunk 放在一起,人工判断截断是否合理。需要关注两类错误:
- 误截(高相关 chunk 被丢弃):通常说明阈值设得过高,或特定表达风格导致打分偏低。发现后先降低
min_score,而不是立即换模型。 - 漏截(低相关 chunk 仍然进入上下文):往往是
confidence阈值未生效,或召回集本身质量不足。抽检时把rationale字段一并打印出来,方便定位具体原因。
定期抽检的频率无需很高;每周抽 10-20 条并记录趋势,比偶发性大规模人工评估更可持续。
与向量检索、BM25 混合时的取舍
现代 RAG 常把向量检索(语义近邻)与 BM25(关键词词频)做混合来覆盖两类失配:语义相近但词面不同、以及词面一致但语义偏移。混合检索后接入重排时,以下取舍值得关注:
| 场景 | 建议 |
|---|---|
| 混合检索候选集较大(> 30 条) | 重排收益更明显;可先用较低阈值(如 6 分)保留更多候选,再用 top-k 截断 |
| 候选集本身很小(< 10 条) | 阈值适当放宽,防止全部被截断导致上下文为空,参考常见坑第一条 |
| 实时问答、延迟敏感 | 将候选集上限控制在 20 条以内,打分调用可小并发但注意限流 |
| 批量文档处理 | 串行或小并发均可,关注 成本与延迟指南 的限流建议 |
混合检索提升了召回覆盖率,同时也把更多杂质候选带进来;重排负责”在更宽的召回结果里找真正有用的部分”。两者职责分明,接口层面互不干扰,可以独立迭代。
BM25 分支对精确短语匹配表现更好,向量分支对语义改写更鲁棒,但两路结果的分数量纲不同,融合时通常做归一化再相加。重排在融合之后统一对候选集打相关性分,不需要感知上游是哪路检索,这也是它在混合架构里接入代价低的原因之一。
常见坑
1. 候选集被全部截断:当所有 chunk 得分都低于 min_score 时,rerank() 返回空列表,下游生成模型会收到空上下文并可能产生幻觉。解决方式:返回空列表前回退到分数最高的 1 条,或者触发一次更宽的召回重试。具体降级策略可参考 置信度与回退指南。
2. 串行打分延迟叠加:50 个候选 × 每次打分约 300ms ≈ 15 秒端到端延迟。批量场景建议用线程池(4-8 并发),单条实时问答建议先把候选集压缩到 20 条以内再进重排。
3. confidence 字段未纳入决策:rationale 是可读解释,便于排查;但判断打分可信度应看 confidence。两个字段各司其职——只记录 answer 而忽略 confidence,相当于丢掉了模型对自身不确定性的表达。
4. 分制与 prompt 不一致:在调用里把 scale 设为 [0, 100] 却在 system prompt 里仍写”1-10 分制”,会让模型打分无参照,结果混乱。始终保持 scale 参数与 system prompt 中描述的分制完全一致。
5. 忽略召回质量退化:重排只能在已召回的候选里选优,无法弥补召回覆盖率本身的下降。如果发现即使放宽阈值命中率仍然持续下滑,应先排查向量索引或 BM25 分词是否有问题,而不是继续调整重排参数。
FAQ
- Jev 在 RAG 重排里做什么? 召回之后,对每个候选 chunk 调一次 scoring 打相关性分,按分排序、按阈值截断,只把留下的 chunk 交给生成模型。
- 分数阈值怎么设? 1-10 分制上以 7 分为收录线比较合理,同时加 top-k 上限,避免生成模型拿到过多上下文。
- 比只用向量检索更贵吗? 每个候选多一次廉价打分调用,但喂给生成模型的 chunk 更少更准;批量重排案例里有具体成本对比。