Ranking 与 FastTrack
本章讲 NLWeb 真正的「价值核心」:怎么把一堆检索候选变成排好序的结果。先讲打分机制(逐条 LLM 评分 + 边算边发),再讲 FastTrack 怎么把这件事和预检并发起来抢时间。
3.1 逐条 LLM 打分:retrieve-then-rank
要解决的小问题。 向量检索给回 ~50 条候选,但向量相似度只是粗排,未必真切题。怎么精排?
思路。 NLWeb 不写复杂的重排模型(默认),而是让 LLM 当裁判:对每一条候选,把「用户问题 + 该条目的描述」塞进 prompt,让 LLM 输出 0-100 的相关性分 + 一句描述。简单、可解释,且天然能写出「为什么相关」。
默认打分 prompt(ranking.py:96 RANKING_PROMPT)大意:
给这个 {itemType} 打 0-100 分,看它跟用户问题多相关。分数高于 50 就给一句突出相关性的描述(但别提及用户问题本身)……用户问题是 {request.query},条目描述是 {item.description}。
原理演示(核心想法,去掉异步与流式):
# 示意,非源码:逐条打分的本质
for item in candidates:
desc = trim_json(item.schema) # 把 schema.org JSON 裁剪精简
prompt = fill_prompt(RANKING_PROMPT, query, desc)
result = ask_llm(prompt, {"score": int, "description": str})
item.score = result["score"]
ranked = sorted(candidates, key=score, reverse=True)
真实实现 在 Ranking.rankItem(ranking.py:214):它 fill_prompt 后 await ask_llm(...)(ranking.py:226-227),把结果连同 schema_object 打包成一个 ansr 字典。每条候选是并发打分的——do() 给每个 item 起一个 task 再 asyncio.gather(ranking.py:446-452)。
3.2 边算边发:高分项不等全部算完
要解决的小问题。 50 条全打完分再排序再发,用户要干等。能不能先把明显的好结果推出去?
思路。 设一个「早发阈值」EARLY_SEND_THRESHOLD = 59(ranking.py:87)。某条一旦打分超过它,立刻尝试发出去,不等其余条目(ranking.py:247):
# 示意,非源码:rankItem 末尾
if ranking["score"] > self.EARLY_SEND_THRESHOLD: # 59
await self.sendAnswers([ansr]) # 这一条先流式发出
self.rankedAnswers.append(ansr) # 同时仍记账,供最后统一排序
这样用户几乎在第一条好结果算完时就看到东西了。
发多少有节制。 shouldSend 控制别超 max_results,且接近上限时只发「比已发的还好」的(ranking.py:306);sendAnswers 里还有多重「绝不超限」的安全检查(ranking.py:354、ranking.py:387)。
最后兜底排序。 所有条目打完后,do() 用 min_score(默认 51)过滤、按分降序、截到 max_results,把未发出的好结果补发(ranking.py:466-492)。注意这里和早发阈值 59 是两个不同的门槛:59 是「好到可以抢跑」,51 是「及格可以留下」。