搜索与匹配:重排序、逐行搜索、实体对齐
💡 一句话总结:用 Jev 给搜索补上一套「排序—定位—裁决」—— Jev 重排序每对发一道 Noul 按概率重排、逐行搜索用 Choice 指行加一道 Noul 查有无、实体对齐一道 Score 三档定夺。快搜索管召回,Jev 管把对的排到第一。
「搜索」这件事,前半段和后半段是两种活:前半段(BM25、嵌入相似度这类快搜索)够快,能从几千份文档里捞出一个可能对得上前 30 的短名单;后半段——这 30 条里哪条才是对的——它答不上来。官方 Re-ranking cookbook 的实测数据最能说明问题:正确段落 100% 都被 BM25 捞进了 top-30,却只有 5% 排在第一位。
Jev 重排序补的就是后半段:官方在 CLERC 法律查询数据上,对每个「查询-候选」对发一道 Noul,拿到一个 0–1 的可比分数,按它重排短名单——top-1 命中 5% → 18%,top-10 38% → 62%。本课把官方 cookbook 索引里 Search and matching 一组中的三个案例一次讲完:除了重排序,还有逐行搜索(GitHub 服务条款 218 行,一次请求定位答案在哪一行、顺便判断文档里到底有没有答案)和实体对齐(两份啤酒目录 450 个候选对,判断哪两行说的是同一款酒)。三个案例、三套题型组合,但分工原则只有一条:能算的在代码里,要读文本的判断交给 Jev。
学完你能做什么
- 给现有 BM25 / 嵌入检索加一层重排序:每个「查询-候选」对一道 Noul,按 0–1 的概率分数重排短名单——官方实测 top-1 命中率 5% → 18%、top-10 38% → 62%
- 写出
find(query):一份 255 行以内的文档,一次请求返回「每行相关多少」(Choice 概率)加「文档里有没有答案」(Noul),没有答案时敢说没有(官方实测 exists 低到 0.14) - 写出
route(pair):判断两条目录记录是不是同一个东西,三档结局(保持分开 / 送人工 / 合并)由一道 Score 的最近档决定——全程没有任何阈值常数 - 让 Noul 给人工复核点名分歧:同一个请求里逐字段问「名字一样吗、酒厂一样吗、类型一样吗」,curator 打开工单就知道该看哪个字段
为什么用 Jev,而不是只靠快搜索(BM25 / 嵌入相似度)?
快搜索解决「从几千条里捞前 30」,后面的判断它无能为力。
| 能力 | 只靠快搜索 | Jev 重排序与匹配 |
|---|---|---|
| 排序依据 | ❌ 按字面重合 / 向量距离,措辞一换就排错——正确答案 100% 在短名单里,排第一的只有 5% | ✅ 逐对读「这个候选答不答得了这个问题」,官方实测 top-1 5% → 18%、top-10 38% → 62% |
| 「没有答案」的情况 | ❌ 检索永远会给一个「最像」的結果,硬答 | ✅ 逐行搜索配一道 exists Noul,文档没答案时分数掉到 0.14,明说「不在这份文档里」 |
| 中间态怎么办 | ❌ 相似度切一刀,切多少全靠调阈值、换数据就失效 | ✅ 实体对齐用 Score 三档带语义的等级(不同 / 相关 / 同一款),文件里没有任何阈值常数 |
| 分歧在哪说得清吗 | ❌ 只有一个总分,为什么判「不确定」无从查起 | ✅ 每字段一道 Noul:name 0.95、brewery 0.94、style 0.35——分歧点在风格写法,一眼看到 |
| 答案形态 | ❌ 分数是黑盒,换库换分词就漂 | ✅ 类型化答案 + 概率分布,排序键就是 0–1 的 noul,没有一层解析 |
一句话:快搜索管召回(对的必须进来),Jev 管排序与裁决(进来的谁对、像不像一个东西)——两段各干各的,哪段坏了单独修。
你现在的困境
- 「BM25 每次都能把答案捞进前 30,但排第一的基本不是它——用户只看第一条,后面的正确答案等于没有」
- 「文档明明没写答案,搜索还是硬给一个『最像』的行,顺着它下去就是幻觉」
- 「两份商品目录要合并,名字写法不一样、字段各有错漏,阈值定多少都不对:定高了漏合并、定低了错合并——错并一条要人工拆半天」
什么时候用这一招
当你需要:
- 大库检索里,快搜索给出短名单之后把对的排到第一——法律检索、文档问答、企业搜索的重排序层
- 一份中等长度文档(255 行以内)里定位回答问题的那几行,还要能识别「这份文档根本没写」
- 判断两条记录 / 商品 / 条目是不是同一个东西,容许「不确定、送人工」的中间档——错并的代价越高越该用
而且不想:
- 给 rerank 调阈值调到天亮——重排序不设阈值,noul 本身就是排序键
- 解析生成模型吐出来的排名 JSON——那是让它把每条候选重打一遍
- 错并实体之后再人工拆数据
🎒 开始前的准备
- 完成了 Choice 选择题:逐行搜索把 218 个行 ID 当选项,一道 Choice 最多 255 个选项的出处也在这页
- 完成了 Score 打分题:实体对齐靠三档量表裁决,分值可以落在两档之间(比如 1.30)
- 建议读完 Noul 是非题:重排序每对一道 Noul,那个 0–1 的概率就是排序键
- 环境可用:
typesafe-sdk已装、TYPESAFE_API_KEY在环境变量里(安装与第一个请求)。三个官方示例都发布了答案缓存json_cache.json,不设 Key 也能重放官方数字(第 1 步说怎么做) - 一杯水 ☕——本课一次过三个官方案例:同一件事的三种问法
跟我做
三个案例互相独立,每种题型一个;每步的官方数字都来自对应 cookbook 在 jev-1.12 上的录制。先把三套题型组合放在一张表里,动手前心里有地图:
| 案例 | 数据 | 题型组合 | 请求数 | 产出 |
|---|---|---|---|---|
| 重排序 | CLERC:3,565 段判例、40 个查询 | 每对 1 道 Noul | 40 × 30 = 1,200 次 | 重排后的短名单(top-1 5%→18%) |
| 逐行搜索 | GitHub ToS:218 行、43,980 字符 | 1 Choice + 1 Noul,同一请求 | 每个查询 1 次 | 每行相关分 + 有无答案判定 |
| 实体对齐 | 两份啤酒目录:450 个候选对 | 1 Score + 3 Noul,同一请求 | 每对 1 次 | 三档结局:分开 / 人工 / 合并 |
第 1 步:对号入座——三种搜索题、三套题型组合(含环境准备)
为什么:三个案例的共同点是「把真正匹配的那条排到前面」,差别只在问法。先看清哪种活用哪种题型,后面每一步都只是在换数据。
安装依赖(三个 cookbook 的官方安装命令合并;cooksafe 是官方 cookbook 配套工具库,其依赖声明把 typesafe-sdk 钉在 >=0.7.0,<0.8.0,照抄钉住):
pip install 'typesafe-sdk>=0.7.0,<0.8.0' bm25s datasets matplotlib ipython 'cooksafe>=0.2.0,<0.3.0'想零 Key 重放官方数字:从官方示例仓库 typesafe-public-examples 下载对应 cookbook 目录下的 json_cache.json,放进运行目录——cooksafe.JsonCache 会直接重放录制好的答案,一个请求都不发。想真跑就设 TYPESAFE_API_KEY 并删掉缓存文件。
- 依赖装好了;我知道三个案例各用什么题型组合
第 2 步:Jev 重排序——BM25 短名单上每对一道 Noul,按概率重排
为什么:BM25 的问题是排不出「谁真正被引」。官方数据的两个数字放在一起看:正确段落 100% 被 BM25 捞进了 top-30(召回没问题),但只有 5% 排在第一位(排序不行)。重排序做的唯一一件事:拿到这 30 条之后换一个更准的分数重新排。
官方的两步法:
几千份候选
│ 第一步:快搜索(BM25)——整个库上跑,够快
▼
每查询 30 段的短名单
│ 第二步:重排序——每个「查询-候选」对一道 Noul
▼
重排后的短名单(候选还是那 30 个,顺序变了)题目定义(官方原文的简化版):
from typesafe_sdk import Noul, NoulCriteria, TypeSafeClient
question = Noul(
instructions="Is this candidate the cited case?",
criteria=NoulCriteria(
true="The candidate states the specific rule the query cites.",
false="The candidate is only on a similar topic.",
),
)
response = client.system_one(state={...}, questions={"is_cited_source": question})
response.answers["is_cited_source"].noul # -> 0.87三个要点:
- 为什么是 Noul 而不是是非题:官方原话——纯 yes / no 排不了 30 个候选;Noul 返回 0–1 的概率,这个 noul 就是程序排序的那个分数
- criteria 定死两边:
true= 候选陈述了查询引用的那条具体规则;false= 只是话题相近——「相似话题」就是 BM25 误排的根源,要在题里点名排除 - state 里就放这一对:
{"query_excerpt": query, "candidate_passage": candidate},每个请求只见一对
重排就是排序一行代码(官方原文):
nouls = {candidate: ask_typesafe(query, candidate) for candidate in shortlist}
reranked = sorted(shortlist, key=lambda c: nouls[c], reverse=True) # highest noul first官方实现里,40 个查询 × 每个的 30 段短名单 = 1,200 次独立调用,用 ThreadPoolExecutor(max_workers=12) 并发跑完——每对互相不见面,天然可并发。CLERC 语料是 3,565 段美国法院判例(170 行数据汇成共享语料、抽 40 行当查询,每行的查询是去掉引注的判例摘录,gold 是引注指向的那段)。
你应该看到(官方 jev-1.12 录制,快搜索 vs 加 Jev 重排序,40 个查询上正确段落进前 N 的比例):
Top 1: 5% → 18%
Top 5: 15% → 35%
Top 10: 38% → 62%正确段落朝顶部移动了每一个档位——但注意它的上限:重排只重排短名单里已有的 30 段,快搜索没捞进来的救不回来(100% 的召回是 BM25 这次碰巧做到的,不是你的场景必然有,见踩坑提醒)。
💡 这就是 Jev 的独有价值
重排序不设任何阈值——不判「这对匹配 / 不匹配」,只要 40×30 个 0–1 的可比分数,排序交给代码。官方还点了一句:这个演示为清晰每对只问一题,真实应用会在同一个调用里对同一对多问几道——一次请求多题的机制和实测收益见并行提问与投机扇出。
- 我能说出为什么排序键用 Noul 而不是普通是非判断,以及 1,200 次调用怎么来的(40 × 30)
第 3 步:逐行语义搜索——218 行条款一次请求,Choice 指行、Noul 查有无
为什么:重排序处理「一堆候选段落」,这一步处理一份具体文档:GitHub 服务条款(218 行、43,980 字符),用大白话问「我上传的代码归谁」,要把答案定位到行、还要能发现「文档根本没写」。官方的三段结构:先给每行标 ID、再用 Choice 对行 ID 打分、最后同一请求里加一道 Noul 查有无。
先给每行打上 ID、拼回一份文档——模型用这些 ID 指认它的答案(官方原文):
DOCUMENT_URL = (
"https://raw.githubusercontent.com/typesafe-ai/typesafe-public-examples/main/cookbooks/semantic_find/github-tos.txt"
)
LINES = fetch_document(DOCUMENT_URL).splitlines()
def line_id(i: int) -> str:
return f"L{i:03d}"
DOCUMENT = "\n".join(f"{line_id(i)}| {line}" for i, line in enumerate(LINES))拼出来的文档长这样:
L052| You own Your Content. If you post Content you did not create, you are responsible for...
L053| You grant us and other Users the licenses in Sections D.4–D.8. These licenses apply...
L054| 4. License Grant to Us两道题(官方原文):一道 Choice 用行 ID 当选项——选项说明写 None,因为行文本已经在文档里了:
def where_question(query: str) -> Choice:
return Choice(
instructions=f'Which line of the document contains the answer to: "{query}"?',
criteria={line_id(i): None for i in range(len(LINES))},
)另一道 Noul 查有没有答案——必须配它的原因:Choice 的概率加总恒为 1,文档没答案时也总有一行排第一;Noul 的概率不依赖其他选项,没有答案时能掉到接近 0:
def exists_question(query: str) -> Noul:
return Noul(
instructions=f'Does any line of the document address or answer: "{query}"?',
criteria=NoulCriteria(
true="At least one line of the document states or directly implies the answer",
false="No line of the document addresses this",
),
)两道题进同一个请求,文档只发一次:
@json_cache
def _find(model, state, where, exists) -> dict:
response = client.system_one(
state=state,
questions={"where": where, "exists": exists},
model=model,
)
probabilities = response.answers["where"].probabilities
return {
"exists": response.answers["exists"].noul,
"relevance": [probabilities.get(line_id(i), 0.0) for i in range(len(LINES))],
}exists 按三档判——官方注释写着:有答案的通常读数 ≥ 0.9,没有的 ≤ 0.05,阈值用自己的文档调过再上线:
FOUND, ABSENT = 0.7, 0.35 # present answers typically read >=0.9, absent <=0.05
def verdict(exists: float) -> str:
if exists >= FOUND:
return "answered in this document"
return "not in this document" if exists < ABSENT else "partially addressed"你应该看到(官方 jev-1.12 录制,四个查询;前两个有答案、第三个没有、第四个只有半个):
218 lines, 43,980 characters
"who owns the code I upload?"
exists 0.98 -> answered in this document
L052 0.95 You own Your Content. If you post Content you did not crea... ← 就是它
"can GitHub kick me off the platform without warning?"
exists 0.97 -> answered in this document
L168 0.97 GitHub has the right to suspend or terminate your access t...
"do I have to take disputes to arbitration?"
exists 0.14 -> not in this document
L205 0.86 Except to the extent applicable law provides otherwise, th...
"can minors use GitHub with parental permission?"
exists 0.46 -> partially addressed
L029 0.90 You must be age 13 or older. While we are thrilled to see...第三个查询是全套设计里最值钱的一课:排名最高的那行拿了 0.86 的相关分,exists 却只有 0.14——最像的一行不等于答案。官方的点睛句:排名告诉你去哪里看,exists 分数告诉你结果算不算答案。第四个是半个答案:年龄规则那行以 0.90 居首,但「家长同意能不能改规则」没写——partially addressed,人工接着看。
💡 这就是 Jev 的独有价值
Choice 选项 = 行 ID 让「定位到行」变成选择题:218 个选项全列在 criteria 里,模型指认 L052 而不是重新打一遍原文——引用永远指向原文里真实存在的行,幻觉不出 L499 来。配 Noul exists 才敢在「文档没写」的时候拒绝回答,这是纯相似度检索做不到的。
- 我能解释 arbitration 查询里 0.86 与 0.14 各自量的是什么,以及为什么两道题必须同一请求
第 4 步:实体对齐——一道 Score 三档裁决,三道 Noul 点名分歧字段
为什么:合并数据源时,错并是更贵的那种错——官方原话:一旦并错,两边实体的每条事实都描述合并后的实体、所有链接跟着一起过来,撤销时要分辨每条事实来自哪边;漏并只留个重复项。所以判断需要第三个出口:既不敢并、也不敢扔的对,交给人。
数据是 Magellan 数据集里两份啤酒目录的 450 个候选对(一道粗筛已经把值得细看的对挑了出来),每条实体四个字段:name、brewery、style、abv。文本保持原样不做预处理——HTML 实体没还原、撇号被拆成单独的词、个别字符解码错误,全留着。
一道 Score 按三档量裁决,三档描述就是全部决策(官方原文):
LEVELS = [
"They describe two different products.",
"They describe closely related products that may or may not be the same one: "
"a variant, a special edition, or a name that could plausibly refer to either.",
"They describe one and the same product.",
]
OUTCOME = {0: "leave unlinked", 1: "curator queue", 2: "assert sameAs"}
QUESTIONS = {
"link_state": Score(
instructions="How do the two entity descriptions relate as products?",
criteria=LEVELS,
),
"same_name": Noul(instructions="Do the two entities state the same beer name?"),
"same_brewery": Noul(instructions="Are the two entities from the same brewery?"),
"same_style": Noul(instructions="Do the two entities describe the same beer style?"),
}四道题进同一个请求,state 就是这一对:{"entity_a": ..., "entity_b": ...}。三个细节:
- 为什么用 Score 不用 Noul/Choice:官方的选型证词——要把语义标签(三档描述)直接挂到每个结局上、包括中间档;Noul 只能靠输出阈值间接做到,Choice 则丢掉三档的有序关系
- 中间档值得认真写:「变体、特别版、可能指两者任一的名字」——措辞决定了哪些对进 curator、哪些被留在未链接;这就是你的「决策逻辑」,而且在看任何分数之前就能写下来
- abv 不出 Noul:官方原话——比较两个数字是算术,想要就在代码里比。三个文本字段各一道 Noul,给 curator 当分歧明细
结局由「取最近档」一行代码决定——注意整个文件里没有阈值常数(官方原话:There is no threshold constant anywhere in this file):
def route(score_value: float) -> str:
"""The whole decision rule: the nearest level names the outcome."""
return OUTCOME[min(int(score_value + 0.5), len(LEVELS) - 1)]你应该看到(官方 jev-1.12 录制,四对样例——一对同款、一对无关、两对因不同原因落在中间档):
c446 score 1.94 confidence 0.92 -> assert sameAs
Thomas Hooker Old Marley Barleywine | Thomas Hooker Brewing Company
Thomas Hooker Old Marley Barleywine | Thomas Hooker Brewing Company
name 0.97 brewery 0.99 style 0.81 ← 同款,风格写法差异(Barley Wine)
c427 score 0.03 confidence 0.95 -> leave unlinked
Frost Quake Bourbon Barrel Aged Barley Wine | Wellington County Brewery
Lompoc Bourbon Barrel Aged Proletariat Red A | Lompoc Brewing
name 0.02 brewery 0.09 style 0.08 ← 无关
c100 score 1.30 confidence 0.27 -> curator queue
Belle Gueule Rousse | Brasseurs R.J.
Belle Gueule Rousse | Brasseurs RJ
name 0.95 brewery 0.94 style 0.35 ← 只差 style 措辞,confidence 0.27
c428 score 1.10 confidence 0.77 -> curator queue
Ambleside Amber Ale | Bridge Brewing Company
Bridge Ambleside Amber Ale - Pomegranate & Galena Hops | Bridge Brewing Company
name 0.63 brewery 0.98 style 0.74 ← 同厂的石榴增味版本跑完全部 450 对(每对一个请求、ThreadPoolExecutor(max_workers=6) 小池并发——官方注释:公共端点大约 8 个并发以上就限流):
assert sameAs 40 ( 8.9%) ← 自动合并
curator queue 50 (11.1%) ← 送人工
leave unlinked 360 (80.0%) ← 保持分开80% 的对自动了结、每五对里约有一对需要人。值得停一秒的两点:c100 的 name 和 brewery 都到 0.94 以上、只有 style 分歧 0.35——Noul 明细直接告诉 curator 看哪个字段;route() 的两个切点(0.5 和 1.5)不是调出来的,它们完全来自你怎么写三档描述。
- 我能说出「为什么合并错比漏合并贵」、中间档描述是干什么用的,以及 450 对里送人工的比例(50,11.1%)
第 5 步:换成你的数据——短名单、文档、字段三个替换点
为什么:三个案例的替换点加起来只有三个,且互相独立——你的场景大概率是其中之一的变体。
- 换短名单来源:重排序列子里快搜索用的是 BM25,但官方明说这是支线问题——重排序只见到进了短名单的段落,快搜索换嵌入检索、换全文索引都不影响第二层。要改的只是喂进
shortlist的那 30 条 - 换文档:逐行搜索把
DOCUMENT_URL换成你自己的合同 / 政策 / 条款(官方原话:换掉这个 URL,脚本其余部分全部照跑)。255 行以内一段搞定;更长的用官方给的两段式——第一道 Choice 先挑出「答案在哪个窗口」,第二道在窗口内排行 - 换字段:实体对齐要迁移到商品、音乐、机构目录,只需重写
QUESTIONS和LEVELS——数字型字段(价格、数量)别出 Noul,代码比;文本型字段一个一道 Noul。成本跟着候选对数走,与两边目录各多大无关(官方原话)
开发期的省钱与并发,两个习惯:
答案全挂在
JsonCache的json_cache.json上,改代码重跑零请求;想真跑删掉缓存文件即可——缓存键由函数名和参数构成,换文档 / 换问题 / 换候选自动 misslive 跑大量对时用小线程池(官方用 6):速率限制按组织计,超限返回
429,SDK 会自动指数退避重试,但压着线跑没有意义我知道自己的场景该换哪个点:短名单来源 / 文档 / 字段
📋 魔法 Prompt
本页的「Prompt」是给 API 的问题与代码模板——抄走、替换【】占位符(带 ? 的是选填)就能接到你自己的检索或目录上。三件套:重排序打分、逐行定位、对齐裁决。
📝 重排序打分题(Noul 版)
预期效果:任何「快搜索给出候选列表、要按相关度重排」的场景——法律检索、文档问答、企业搜索的 rerank 层——每个查询-候选对套这一个模板拿排序键。
from typesafe_sdk import Noul, NoulCriteria
# 必填项:state 放【查询原文 + 这一个候选】,true/false 分别写清「真匹配」和「最容易混的假匹配」
rerank_question = Noul(
instructions="【候选段回答得了这个问题吗?一句话,例如:这段能不能是被引的那条判例?】",
criteria=NoulCriteria(
true="【候选陈述了查询引用的具体规则/给出了查询要的答案】",
false="【候选只是在相近话题上——把快搜索最典型的误排写在这里】",
),
)
score = client.system_one(
state={"query": 【查询原文】, "candidate": 【这一个候选】},
questions={"is_match": rerank_question},
).answers["is_match"].noul # 0–1,直接当排序键
# 选填项:真实应用在同一个请求里对同一对多问几道——支持度、原创度、时效各一题,一组分数一起排?
# 输出规范:每对一个 0–1 的 noul;reranked = sorted(shortlist, key=noul, reverse=True)
# 约束条件:
# ✅ 不设匹配阈值——重排序只要可比分数,排序在代码
# ✅ false 描述里点名快搜索的典型误排(如「只是话题相近」)
# ❌ 别指望重排召回快搜索漏掉的候选——短名单外的一个都进不来✅ 逐行搜索两连问(Choice + Noul 版)
预期效果:一份 255 行以内的文档,「答案在哪一行」和「有没有答案」一次请求全回来——合同条款定位、政策问答、工单知识库检索。
from typesafe_sdk import Choice, Noul, NoulCriteria
# 必填项:行 ID 先拼进文档("L052| ..."),选项就是这些 ID、说明留 None
questions = {
"where": Choice(
instructions='Which line of the document contains the answer to: "【大白话问题】"?',
criteria=【{行ID: None for 行ID in 全部行}】,
),
"exists": Noul(
instructions='Does any line of the document address or answer: "【同一问题】"?',
criteria=NoulCriteria(
true="At least one line of the document states or directly implies the answer",
false="No line of the document addresses this",
),
),
}
# 选填项:三档判读——exists ≥ 0.7 算有答案、< 0.35 算没有、中间 partially;阈值用自己的文档调?
# 输出规范:每行一个相关分(Choice probabilities)+ 一个 0–1 的 exists;两题同一请求,文档只发一次
# 约束条件:
# ✅ 文档 ≤ 255 行一段搞定;更长先挑窗口、再在窗口内排行(第二道 Choice)
# ✅ exists 必配——Choice 恒有第一名,没答案时它照样硬指一行
# ❌ 别丢掉行 ID 标注这一步——引用必须指回真实存在的行🔧 实体对齐四连问(Score + 3 Noul 版)
预期效果:合并目录 / 去重 / 入库对账——判断两条记录是不是同一个东西,错并代价高的场景尤其该用。
from typesafe_sdk import Noul, Score
# 必填项:三档从左到右由「敢扔」到「敢并」,中间档写清哪类情况该送人工
LEVELS = [
"【两条记录描述两个不同的东西】",
"【描述紧密相关但不一定是同一款:变体 / 特别版 / 可能指两者任一的名字】",
"【描述同一个东西】",
]
QUESTIONS = {
"link_state": Score(
instructions="How do the two entity descriptions relate?",
criteria=LEVELS,
),
【"same_字段1": Noul(instructions="这两条记录的【字段1】说的是同一个吗?")】,
【"same_字段2": Noul(instructions="……同上,每个文本字段一道")】,
}
# 选填项:数字型字段(价格、度数、数量)不出 Noul——两个数字比较是算术,代码一行?
# 输出规范:score 取最近档定结局(不同/人工/同一),三道 Noul 明细供人工复核
# 约束条件:
# ✅ 全程无阈值常数——切点(0.5 / 1.5)由三档措辞决定,看分数之前就能写好
# ✅ 中间档值得最认真写:它决定哪些对进 curator
# ❌ 别为「不放心」再加一层阈值判断——要么改等级措辞,要么接受送人工的比例检查点 ✅
- 我能说出三个案例各自的题型组合:重排序每对 1 Noul、逐行搜索 1 Choice + 1 Noul 同请求、实体对齐 1 Score + 3 Noul 同请求
- 重排序官方数字:top-1 命中 5% → 18%、top-5 15% → 35%、top-10 38% → 62%;正确段落 100% 在短名单里、却只有 5% 排第一
- 我能解释 arbitration 查询的教学点:排名最高的行拿 0.86 相关分、exists 只有 0.14——最像不等于答案
- 实体对齐三档结局取最近档(
route一行),450 对分发 360 / 50 / 40(80.0% / 11.1% / 8.9%),文件里没有任何阈值常数 - 我知道两道硬边界:重排救不了快搜索的漏召;一道 Choice 最多 255 个选项,超了先挑窗口再排行
踩坑提醒
❌ 让重排序救漏召
重排只重排短名单已有的候选——快搜索没选进来的段落,Jev 再准也见不到。官方数据里正确段落 100% 进了 top-30,那是 BM25 在这个数据集上的表现;换你的语料前先看短名单召回,不够就把快搜索调召回、或加候选数——那是第一层的问题,别在第二层修。
❌ 只发 Choice 不问 exists
Choice 概率加总恒为 1,永远有一行排第一——文档没答案时它照样硬指一行,顺着它走就是幻觉。逐行搜索的第二个数字(exists Noul)才是「有没有」的判据:它的概率不依赖其他选项,没答案时能掉到接近 0(实测 0.14)。
❌ 260 行的文档硬塞进一道 Choice
一道 Choice 最多 255 个选项。官方给的超限打法是两段式:第一道 Choice 先挑「答案在哪个窗口的行」,第二道在窗口内排行——每次都在 255 以内。反过来,65 行的文档不用分块,直接上。
❌ 数字字段也扔给 Jev 比对
啤酒目录里 abv(酒精度)8.10 % 对 9.30 %——官方明说比较两个数字是算术,想要就在代码里比。三个文本字段才是 Noul 的活:name / brewery / style 的「像不像」要看写法,0.35 的 style 分数能直接告诉 curator 分歧在哪。和值提取是同一条铁律:模型读文本,算术在代码。
❌ 中文文档不测直接上线
官方语言支持说明:英语是主要训练语言、准确度最好,其他语言(含中文)能用但准确度较低。中文的语义搜索、目录对齐上线前先拿自己的真实数据跑一批,重点看概率分布和 exists 判读(见模型与价格的语言支持一节)。
进阶技巧(可选)
一对多问。 重排序演示里每对只问一道是为了讲清楚;官方原话——真实应用会对同一个「查询-候选」对在一个调用里问多道。一次请求多题几乎不增加响应时间、只多花题的 token,官方实测 13 题合发比分发便宜 12.2 倍、快 10.0 倍、答案不变——机制详见并行提问与投机扇出。
快搜索换嵌入。 官方明说重排序一节的快搜索就是 BM25、别无其他,但那是支线问题——dense embeddings(按语义比较段落)同样能建短名单,第二层重排序不挑食。中文语料上嵌入检索 + Jev 重排的组合常常比纯 BM25 起点更好。
生成 Playground 链接试措辞。 想在浏览器里反复调题目措辞、看概率分布怎么变,用 cooksafe 的 make_playground_link(链接里带完整 state 和问题,别放敏感数据):
from cooksafe import make_playground_link
link = make_playground_link(
{"entity_a": PAIRS[0]["entity_a"], "entity_b": PAIRS[0]["entity_b"]},
QUESTIONS,
)
print(link) # 打开 console.typesafe.ai/playground,改 middle level 措辞看 score 怎么动官方实体对齐 cookbook 实测的那个中间档措辞就是这么值得反复调的:措辞才是你的决策逻辑,调词比调阈值可解释得多。
接到 RAG 里。 检索出来的段落除了「哪条最相关」,还有「该不该进上下文」——那是另一道工序,官方的做法是每段四道 Noul 打分、阈值路由,见 RAG 段落把关。两课拼起来就是完整的检索质量层。
本课小结
✅ Jev 重排序 = 快搜索管召回 + 每对一道 Noul 按概率重排:BM25 短名单上 top-1 命中 5% → 18%、top-10 38% → 62%,不设阈值,排序在代码。
✅ 逐行搜索 = 行 ID 当 Choice 选项(定位、且只能指认真实存在的行)+ 同请求一道 exists Noul(敢说「文档没答案」,实测 0.14 对着 0.86 的最高相关分)。
✅ 实体对齐 = 一道 Score 三档裁决(取最近档定结局)+ 三道 Noul 给 curator 点名分歧字段;450 对分发 360 / 50 / 40,全程无阈值常数——三档措辞就是决策逻辑。
✅ 三条硬边界:重排救不了快搜索的漏召;Choice 最多 255 个选项(超了两段式);数字比较是算术,留给代码。
关键配置字段回顾:
| 字段 | 本页的值 | 说明 |
|---|---|---|
短名单 TOP_K | 30 | 每查询交给重排序的候选数;40 × 30 = 1,200 次调用 |
exists 判读线 | FOUND 0.7 / ABSENT 0.35 | 官方注释:有答案通常 ≥ 0.9、没有 ≤ 0.05;上线前用自己的文档调 |
LEVELS / OUTCOME | different / related / same ↔ 0 / 1 / 2 | 中间档措辞决定哪些对进 curator;切点 0.5 / 1.5 不是调出来的 |
| 每对请求数 | 1(多题同请求) | 重排每对 1 Noul;对齐每对 1 Score + 3 Noul;逐行搜索每查询 1 请求 2 题 |
MAX_WORKERS | 6(官方) | 官方注释:公共端点约 8 并发以上限流;超限 429、SDK 自动退避 |
| 模型 | jev-1.12(官方录制) | live 跑锁你自己的版本 ID,别用会漂移的 jev-latest |
| 选项上限 | Choice 255 | 超限两段式:先挑窗口再排行 |
相关教程:
- RAG 段落把关——同族检索场景:检索出来的段落该不该进上下文,每段四道 Noul 阈值路由
- 值提取:日期、邮箱、电话、金额逐字抽对——同一个「候选收缩」思想的另一面:正则找候选、Jev 挑对的那个
- 并行提问与投机扇出——「多道题一个请求」的底层机制与 12.2x / 10.0x 实测数据
下一课预告
搜索匹配做完了,字段值怎么核对成本最低?下一课:级联提取:便宜模型干活、Jev 核查、贵模型兜底。
你会学到:
- 便宜模型先做提取,Jev 逐字段核查它的产出,只有核查报警才升级到贵的推理模型——成本花在刀刃上
- 「Jev 核查」这一层怎么设计:每字段一道题,报警逻辑全在代码
- 和本页同一分工的延续:模型做判断、代码做路由,只是这次路由的对象是模型