第 9 章:检索与知识图谱——Search and retrieval / Graphs and knowledge graphs
第 9 章:检索与知识图谱——Search and retrieval / Graphs and knowledge graphs
检索系统的瓶颈很少是"找不到",而是"找到一堆之后,不知道该信哪一条"。向量相似度只回答"看起来像不像",回答不了"这一段到底有没有答到我的问题"。本章讲 Jev 在 RAG 与知识图谱里的位置:把重排、相关性判定、答案与来源的对齐,收敛成一次次类型化判定。
RAG 的短板在召回之后的每一步
先摆出标准 RAG 的流水线:切块 → 向量化 → 向量召回 → 重排 → 拼上下文 → 生成。
其中"向量召回"是一次模糊的相似度检索:便宜、快、覆盖面广。但它衡量的是语义上的接近,不是逻辑上的相关。一个片段可能因为用词接近而被召回,却根本没有回答 query;一篇文档可能整体相关,但真正有用的只是其中一段。到了"该不该把这段放进上下文"这个位置,靠余弦相似度排序就力不从心了。
传统做法有两种。一是上一个 cross-encoder 重排模型——训练和部署成本高,换个领域还要重训。二是再叫一次大模型"给这几段排个序"——于是又回到了生成模型的老路:你要它输出一个分数或次序,它却给你一段解释,你还得写解析器把它捞出来。
Jev 的位置正在这里。重排、相关性筛选、答案与来源对齐,本质上都是封闭的判定:这段和相关吗?这组关系该不该走?这个结论有没有来源支撑?它们都能写成类型化问题,交给决策层,让 Python 负责排序、过滤、路由和停止。
Jev 在检索里出现的三个位置
一条 RAG 管道里,Jev 通常出现在三处。用一张图理清它们的位置与形态:
flowchart LR
Q["query"] --> R["向量召回
粗召,便宜"]
R --> J1["① 相关性判定
Noul 逐候选"]
J1 --> RK["② 重排
按概率排序"]
RK --> G["生成模型
拼上下文作答"]
G --> J2["③ 答案-来源对齐
Noul 逐条 claim"]
J2 --> OUT["放行 / 拦截"]- 相关性判定:对每个候选问一个
Noul——"这段能回答 query 吗?",取概率高于阈值的那批。这是Search / Retrieval形态的标配。 - 重排:不需要二值答案、只要相对次序时,就用同一批
Noul的概率当分数,由代码排序。这是Ranking,底层是 Scoring。 - 答案-来源对齐:生成之后,把回答拆成若干条 claim,逐条问
Noul——"这个说法能不能在来源里找到支撑?"。这是后置的Verification。
三段判定都不生成文本,各返回一个带概率的布尔。代码拿着这些概率决定阈值、次序与是否放行。一次把几十个候选打成一批送进去,成本远低于让大模型把上下文重读一遍。
def rerank(query: str, candidates: list[str], threshold: float = 0.6) -> list[str]:
questions = {
f"c{i}": {"type": "noul",
"question": f"这段文字是否直接回答了这个问题:{query}?"}
for i in range(len(candidates))
}
d = client.systemone(
state="\n\n".join(f"[{i}] {c}" for i, c in enumerate(candidates)),
questions=questions,
instructions="只看这段话本身能否支撑答案,不要因为它和问题用词相近就判相关。",
)
scored = [(candidates[i], d[f"c{i}"].probability) for i in range(len(candidates))]
scored = [s for s in scored if s[1] >= threshold]
scored.sort(key=lambda x: x[1], reverse=True)
return [c for c, _ in scored]这段代码里,threshold 和 sort 都在 Python 里,是百分之百确定的行为;模型只回答"相不相关"。这就是"代码拥有控制流"在检索里的具体样子。
实例一:LlamaIndex Jev——把重排变成一次带分数的判定
WiktorB2004/llama-index-jev 是 LlamaIndex 的非官方适配器,做法很典型:对每一段召回的 passage 用 Jev Score 打分,再用 Choice/Noul 在几个 query engine 之间做选择。
它的实测落在 NFCorpus 检索基准上:nDCG@5 从 0.340 提升到 0.396,代价约 $0.0003/query。
两个细节值得留意。第一,nDCG@5 是次序敏感的指标,看的是前 5 条排得对不对——光"找到"不够,还要"排对"。检索里权重最高的位置就那么几个,重排把该靠前的顶上去,指标就动了。第二,单次查询约三分之一厘,这个量级说明重排是"每查必做"也负担得起的一步,不是奢侈品。
这里的取舍是把 passage 的相关程度交给 Score 的连续值,而不是一个二值的相关/不相关。连续分数更适合排序,也和 Ranking 形态的整套评估方法(nDCG、Recall@k)对得上。
实例二:Search with Jev and Milvus——召回与策略彻底分家
milvus-io/bootcamp 的 search_with_jev 是一组 9 个可运行的 notebook。它们把整条链路拼得很完整:用 Gemini 做 embedding、用 Milvus 做向量检索,把召回的候选交给 Jev 的 Noul 和 Choice 判定,再由 Python 负责排序、过滤、路由和停止。
这组 notebook 的价值不在某个单点指标,而在它示范了一条清晰的分工线:
flowchart LR
subgraph 模型与向量库
E["Gemini embedding"] --> M["Milvus 检索"]
end
M --> J["Jev:Noul / Choice 判定"]
subgraph 代码
P["排序 / 过滤 / 路由 / 停止条件"]
end
J --> P
P --> ANS["构造上下文"]同一个组织还把重排单独抽成了 milvus-io/milvus-model:它把候选文档的 Noul 问题批量送进 Jev,再通过一个 Python reranker 适配器,返回按分数排好序、并保留原始索引的结果。保留原始索引这个细节很重要——重排之后你还要把候选映射回它原来的位置才能取用,而这件纯工程的事由 Python 精确完成,不需要模型参与。
所以这组项目的核心是一句话:向量库负责召回,Jev 负责判定,Python 负责策略。 停止条件(召回够了没有、要不要继续检索)也属于策略,写在代码里,可测可改。
实例三:OpenViking——没有原生 rerank 接口时,用概率当分数
volcengine/OpenViking 是 Volcengine 的 agent context database。它内置了一个 Jev rerank client:对每个候选文档,用 jev-latest 打向 api.typesafe.ai,把返回的概率直接当作相关性分数。
它这么做的理由很明确——TypeSafe 并没有提供一个原生的 rerank 接口。既然没有现成的批量重排端点,那就退一步:把重排拆成"对每个候选问一个 Noul",再拿概率当分数。这个做法看着朴素,却很能说明 Jev 的通用性——当系统缺一个专用信号时,一次类型化判定就能把信号补上,不必等一个专门的 rerank API。
这条经验对自建检索管道的团队尤其有用:你不需要先拥有一个重排模型,才能拥有重排能力。先把相关性判定跑通、用概率排序,就拿到一个可用的起点;要提指标,再回来调阈值、换问题问法。
知识图谱:把"关系该不该走"也交给判定
图谱检索和向量检索的差别在于"多跳"。向量检索一次取回相似片段,图谱检索要在实体和关系上一步步走,每一跳都面临一个判定:这条关系对当前这个问题相不相关,值不值得走下去?
图谱链路上 Jev 能出力有两个阶段。一是构建阶段的抽取——从文本里抽出实体和关系,本质是 Structured Data Extraction,把非结构化文本转成结构化三元组,下一章会细讲。二是查询阶段的判定——在图上遍历时,对每条候选关系问一个 Noul:沿着它走,能不能更接近答案?走哪条、走几跳、什么时候停,仍然是代码掌握的控制流。
这也回答了一个常被问到的问题:Graph RAG 为什么能提升 Recall? 纯向量检索只能召回"和 query 长得像"的片段,而多跳问题("A 的创始人后来创办的那家公司叫什么")需要的中间实体,往往和 query 本身的用词毫无相似度,向量召回根本碰不到。图谱提供了显式的路径,而 Jev 在每一跳上做相关性判定——把"哪条边值得走"从死板的规则变成带概率的语义判断,于是更多真正相关的证据被拉进了上下文。
实例四:Vector Graph RAG——图遍历与向量检索并行,各自判定
zilliztech/vector-graph-rag 是这套思路的一个干净实现:它用 Jev Noul 判定来挑选要走的图关系,同时对完整的源 passage 做重排,与向量检索的候选并行比较。
它在两组多跳基准上的合并结果是 86.07% 平均 Recall@5,评测覆盖 1,000 条 MuSiQue 查询和 1,000 条 2Wiki 查询。
MuSiQue 与 2Wiki 都是为多跳问答设计的基准——一个问题要串联两跳甚至更多跳的信息才能回答。能在这类基准上把 Recall@5 做到八十六个百分点,靠的正是两条腿走路:向量检索覆盖"语义相近"的候选,图关系判定补上"逻辑上需要但用词不相近"的那部分证据。两者在同一个召回预算里争位置,而谁更相关由 Noul 的概率说话。
顺带一提:缓存的新鲜度也是判定
检索链路上还有一个不起眼却高频的判断:这条缓存还能不能用? 语义缓存按语义命中,但"命中"不等于"答案仍然有效"——时间敏感的问题,昨天的答案今天可能就错了。
zilliztech/GPTCache 的做法是接入 TypeSafe Jev 的 Noul 检查,用来评估缓存命中的新鲜度和与时间相关的查询是否仍然有效。这类判断没有规则可写("过期"没有固定边界),只能靠语义判定,正好落在 Noul 的射程里。
同样属于"给检索结果把关"的还有 Cribrix:它先用 Jev Score 加 Noul 检查过滤召回的 chunk 里有没有答案依据、有没有 prompt injection,再对草稿逐条 claim 做批量 Noul,凡有 claim 找不到来源、或引用了来源里没有的数字,就扣下不发。在它重放的 62 题 golden set 上,22 道无法回答的问题一道都没答(0/22),而朴素的 top-5 RAG 答了 4/22。这说明落在生成前后两端的判定,直接决定了一个 RAG 系统"会不会一本正经地胡说"。
官方 use-case-map 把这一章的两大场景分别归为 "Search and retrieval" 与 "Graphs and knowledge graphs"。从上到下看,两者的结构其实是同构的:召回是机器的活,"这一步该不该用"是判定的活,而阈值、次序、停止条件回到代码里。
小结
- 检索的难点在召回之后:向量相似度回答"像不像",回答不了"相不相关";重排、相关性筛选、答案-来源对齐都是封闭判定,适合交给 Jev。
- Jev 在检索里有三个位置:候选的相关性判定(
Noul)、按概率重排(Ranking)、生成后的答案-来源对齐(Verification)。 - LlamaIndex Jev 用
Score给每段 passage 打分,NFCorpus 的nDCG@5从0.340提升到0.396,约$0.0003/query。 - Search with Jev and Milvus 分工清晰:Gemini embedding + Milvus 召回、Jev 判定、Python 负责排序/过滤/路由/停止;
milvus-model批量跑Noul并保留原始索引。 - OpenViking 在 TypeSafe 没有原生 rerank 接口时,用
jev-latest的概率当相关性分数——缺专用信号时,一次判定就能补上。 - Vector Graph RAG 用
Noul挑图关系并对源 passage 重排,在 1,000 MuSiQue + 1,000 2Wiki 上取得86.07%平均Recall@5;Graph RAG 提 Recall 的关键,是补上向量召回碰不到的中间证据。
下一章换个视角,从"找到信息"转向"调度与把关":怎么用一次便宜的判定决定把请求交给哪个模型,又怎么给输入和输出各加一道逐条核对的护栏。