Skip to content
🎯 场景实战第 14 / 16 节

搜索与匹配:重排序、逐行搜索、实体对齐 ​

💡 一句话总结:用 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 道 Noul40 × 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,照抄钉住):

bash
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 条之后换一个更准的分数重新排。

官方的两步法:

text
几千份候选
    │  第一步:快搜索(BM25)——整个库上跑,够快
    ▼
每查询 30 段的短名单
    │  第二步:重排序——每个「查询-候选」对一道 Noul
    ▼
重排后的短名单(候选还是那 30 个,顺序变了)

题目定义(官方原文的简化版):

python
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},每个请求只见一对

重排就是排序一行代码(官方原文):

python
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 的比例):

text
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 指认它的答案(官方原文):

python
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))

拼出来的文档长这样:

text
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,因为行文本已经在文档里了:

python
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:

python
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",
        ),
    )

两道题进同一个请求,文档只发一次:

python
@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,阈值用自己的文档调过再上线:

python
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 录制,四个查询;前两个有答案、第三个没有、第四个只有半个):

text
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 按三档量裁决,三档描述就是全部决策(官方原文):

python
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):

python
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 录制,四对样例——一对同款、一对无关、两对因不同原因落在中间档):

text
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 个并发以上就限流):

text
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 上,改代码重跑零请求;想真跑删掉缓存文件即可——缓存键由函数名和参数构成,换文档 / 换问题 / 换候选自动 miss

  • live 跑大量对时用小线程池(官方用 6):速率限制按组织计,超限返回 429,SDK 会自动指数退避重试,但压着线跑没有意义

  • 我知道自己的场景该换哪个点:短名单来源 / 文档 / 字段

📋 魔法 Prompt ​

本页的「Prompt」是给 API 的问题与代码模板——抄走、替换【】占位符(带 ? 的是选填)就能接到你自己的检索或目录上。三件套:重排序打分、逐行定位、对齐裁决。

📝 重排序打分题(Noul 版) ​

预期效果:任何「快搜索给出候选列表、要按相关度重排」的场景——法律检索、文档问答、企业搜索的 rerank 层——每个查询-候选对套这一个模板拿排序键。

python
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 行以内的文档,「答案在哪一行」和「有没有答案」一次请求全回来——合同条款定位、政策问答、工单知识库检索。

python
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 版) ​

预期效果:合并目录 / 去重 / 入库对账——判断两条记录是不是同一个东西,错并代价高的场景尤其该用。

python
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 和问题,别放敏感数据):

python
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_K30每查询交给重排序的候选数;40 × 30 = 1,200 次调用
exists 判读线FOUND 0.7 / ABSENT 0.35官方注释:有答案通常 ≥ 0.9、没有 ≤ 0.05;上线前用自己的文档调
LEVELS / OUTCOMEdifferent / related / same ↔ 0 / 1 / 2中间档措辞决定哪些对进 curator;切点 0.5 / 1.5 不是调出来的
每对请求数1(多题同请求)重排每对 1 Noul;对齐每对 1 Score + 3 Noul;逐行搜索每查询 1 请求 2 题
MAX_WORKERS6(官方)官方注释:公共端点约 8 并发以上限流;超限 429、SDK 自动退避
模型jev-1.12(官方录制)live 跑锁你自己的版本 ID,别用会漂移的 jev-latest
选项上限Choice 255超限两段式:先挑窗口再排行

相关教程:

下一课预告 ​

搜索匹配做完了,字段值怎么核对成本最低?下一课:级联提取:便宜模型干活、Jev 核查、贵模型兜底。

你会学到:

  • 便宜模型先做提取,Jev 逐字段核查它的产出,只有核查报警才升级到贵的推理模型——成本花在刀刃上
  • 「Jev 核查」这一层怎么设计:每字段一道题,报警逻辑全在代码
  • 和本页同一分工的延续:模型做判断、代码做路由,只是这次路由的对象是模型
搞懂AI关于我们怎么写隐私说明

独立的第三方教程,与文中提到的任何产品及其公司没有隶属或合作关系;产品名称和商标归各自所有者。价格和政策以官方页面为准。