
如果你已经阅读了我们的 DeepSearch/DeepResearch 实施指南,让我们深入探讨一些能够大幅提升质量的细节。在这篇文章中,我们将重点关注两个关键挑战:利用 embeddings 从冗长的网页中选择片段以及使用 rerankers 来为爬取 URL 设定优先级。
有些人可能还记得我们之前的结论,即"embeddings 仅对查询去重等 STS 任务(语义文本相似度)有用,而 rerankers 甚至不是我们最初 DeepSearch 实现的一部分。"事实证明,这两者仍然非常有价值 —— 只是不是以传统的方式。我们一直遵循最精简的路径。我们不会为了证明它们的存在价值或我们作为 embedding 和 reranker 提供商的价值而添加组件。我们专注于搜索真正需要的基础功能。
经过数周的实验和迭代,我们发现了这两种技术在 DeepSearch/DeepResearch 系统中不常见但有效的用途。通过应用它们,我们显著提高了 Jina DeepSearch 的质量(欢迎体验)。我们希望与该领域的从业者分享这些见解。
tag从长内容中选择片段
问题是这样的:在使用 Jina Reader 读取网页内容后,我们需要将其作为知识项添加到代理的上下文中以进行推理。虽然将完整内容直接放入 LLM 的上下文窗口是最简单的方法,但考虑到 token 成本和生成速度,这并不是最优的。实践中,我们需要识别内容中与问题最相关的部分,并有选择地只将这些部分作为知识添加到代理的上下文中。
基于 LLM 的过滤存在相同的成本和延迟问题,所以让我们寻找更小型模型的解决方案:我们需要更小更便宜,但仍然支持多语言的模型 —— 这是一个关键因素,因为我们无法保证查询或文档始终是英文的。
我们一边有问题(原始查询或缺口问题),另一边有大量的 markdown 内容,其中大部分内容都是无关的。我们需要为查询选择最相关的片段。这类似于 RAG 社区自 2023 年以来一直在处理的分块问题 —— 使用检索模型仅检索相关块放入上下文窗口进行总结。然而,在我们的情况下有两个关键区别:
- 来自有限文档的有限块。如果每个块大约包含 500 个 tokens,那么一个典型的长网页文档大约有 200,000 个 tokens(p50)到 1,000,000 个 tokens(p99),而我们每步使用 Jina Reader 获取 4-5 个 URL,这将产生大约数百个块 —— 意味着数百个 embedding 向量和数百个余弦相似度。这在 JavaScript 内存中很容易处理,无需向量数据库。
- 我们需要连续的块来形成有效的知识片段。我们不能接受像
[1-2, 6-7, 9, 14, 17, ...]
这样组合散落句子的片段。更有用的知识片段应该遵循像[3-15, 17-24, ...]
这样的模式 —— 始终保持文本连续。这使得 LLM 更容易从知识源复制和引用,并减少幻觉。
剩下的都是从业者抱怨的常见问题:每个块不能太长,因为 embedding 模型无法很好地处理长上下文;分块会导致上下文丢失并使块 embeddings 变得独立同分布;如何找到最佳的边界线索来同时保持可读性和语义?如果你明白我们在说什么,那么你可能在 RAG 实现中也被这些问题困扰过。
但长话短说 —— 采用 late-chunking 和 jina-embeddings-v3 完美解决了这三个问题。Late chunking 为每个块保留了上下文信息,对边界线索不敏感,而 jina-embeddings-v3 本身在非对称多语言检索任务中达到了 SOTA。感兴趣的读者可以查看我们的博客文章或论文了解详情,这里是整体实现。
Conv1D
。该过程首先将长文档分割成固定长度的块,然后使用开启 late-chunking 的 jina-embeddings-v3 进行嵌入。在计算每个块与查询之间的相似度分数后,使用滑动窗口在相似度分数上移动,找到平均值最高的窗口。


function cherryPick(question, longContext, options) {
if (longContext.length < options.snippetLength * options.numSnippets)
return longContext;
const chunks = splitIntoChunks(longContext, options.chunkSize);
const chunkEmbeddings = getEmbeddings(chunks, "retrieval.passage");
const questionEmbedding = getEmbeddings([question], "retrieval.query")[0];
const similarities = chunkEmbeddings.map(embed =>
cosineSimilarity(questionEmbedding, embed));
const chunksPerSnippet = Math.ceil(options.snippetLength / options.chunkSize);
const snippets = [];
const similaritiesCopy = [...similarities];
for (let i = 0; i < options.numSnippets; i++) {
let bestStartIndex = 0;
let bestScore = -Infinity;
for (let j = 0; j <= similarities.length - chunksPerSnippet; j++) {
const windowScores = similaritiesCopy.slice(j, j + chunksPerSnippet);
const windowScore = average(windowScores);
if (windowScore > bestScore) {
bestScore = windowScore;
bestStartIndex = j;
}
}
const startIndex = bestStartIndex * options.chunkSize;
const endIndex = Math.min(startIndex + options.snippetLength, longContext.length);
snippets.push(longContext.substring(startIndex, endIndex));
for (let k = bestStartIndex; k < bestStartIndex + chunksPerSnippet; k++)
similaritiesCopy[k] = -Infinity;
}
return snippets.join("\n\n");
}
使用延迟分块和类似 Conv1D 的平均池化来选择与问题最相关的片段。
确保在调用 Jina Embeddings API 时设置以下 retrieval task
、late_chunking
和 truncate
参数:
await axios.post(
'https://api.jina.ai/v1/embeddings',
{
model: "jina-embeddings-v3",
task: "retrieval.passage",
late_chunking: true,
input: chunks,
truncate: true
},
{ headers });
对于问题嵌入,请确保将 task
改为 retrieval.query
并关闭 late_chunking
完整实现可以在 Github 上找到:
tag对下一步阅读的 URL 进行排序
问题在于:在 DeepSearch 会话期间,你可能会从搜索引擎结果页面(SERP)收集大量 URL,并在阅读单个网页时发现更多链接(页面内链接)。唯一 URL 的总数很容易达到数百个。同样,简单地将所有 URL 直接放入 LLM 的上下文是低效的——这会浪费宝贵的上下文窗口空间,更糟糕的是,我们发现 LLM 基本上是随机选择 URL。引导 LLM 到最有可能包含所需答案的 URL 至关重要。
curl https://r.jina.ai/https://example.com \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "X-Retain-Images: none" \
-H "X-Md-Link-Style: discarded" \
-H "X-Timeout: 20" \
-H "X-With-Links-Summary: all"
在 DeepSearch 中使用 Jina Reader 爬取页面的最佳选项。这将在单独的 links
字段中收集所有页面内链接,并从 content
字段中移除它们。
将这个问题视为一个上下文内的 PageRank,我们需要在会话期间对数百个 URL 进行权重计算。我们基于多个因素对 URL 进行排名,这些因素结合了最后更新时间、域名频率、路径结构,最重要的是与查询的语义相关性,以创建一个综合得分。请记住,我们只能使用实际访问 URL 之前 可用的信息:
频率信号:在不同来源中多次出现的 URL 会获得额外权重。来自在搜索结果中频繁出现的域名的 URL 会获得提升,因为热门域名通常包含权威内容。
路径结构:我们分析 URL 路径以识别内容集群。在常见路径层次结构中的 URL 获得更高分数,对更深路径应用衰减因子。
语义相关性:我们使用 jina-reranker-v2-base-multilingual 来评估问题和每个 URL 的文本信息之间的语义相关性,这是一个经典的重排序问题。每个 URL 的文本信息来自:
- 来自 SERP API 结果的标题和摘要(
https://s.jina.ai/
使用'X-Respond-With': 'no-content'
) - 页面内 URL 的锚文本(
https://r.jina.ai
使用'X-With-Links-Summary': 'all'
)
最后更新时间:某些 DeepSearch 查询对时间敏感,因此最近更新的 URL 比旧的更有价值。在不是像 Google 这样的主要搜索引擎的情况下,可靠地确定最后更新时间具有挑战性。我们实现了一个多层方法,结合以下信号并提供置信度评分的时间戳,在需要时优先考虑较新的内容。
- SERP API 过滤器(如 s.jina.ai 的
tbs
参数用于按时间筛选) - HTTP 头分析(Last-Modified、ETag)
- 元数据提取(meta 标签、Schema.org 时间戳)
- 内容模式识别(HTML 中可见的日期)
- 针对 WordPress、Drupal 和 Ghost 等平台的 CMS 特定指标
受限内容:社交媒体平台上的某些内容是受限的或者需要付费访问,如果没有登录或违反其服务条款,就无法合法获取这些内容。我们应该主动维护一个有问题的 URL 和主机名列表,降低它们的排名,避免浪费时间在无法访问的内容上。
域名多样性:在某些情况下,权重最高的 URL 都来自相同的主机名,这可能会将 DeepSearch 困在局部最优解中,降低最终结果的质量。查看上面的示例,其中所有顶级 URL 都来自 StackOverflow。为了提高多样性,我们可以实现一个探索-利用方法,从每个主机名中选择权重最高的前 k 个 URL。
URL 排序的完整实现可以在我们的 Github 上找到。
<action-visit>
- Crawl and read full content from URLs, you can get the fulltext, last updated datetime etc of any URL.
- Must check URLs mentioned in <question> if any
- Choose and visit relevant URLs below for more knowledge. higher weight suggests more relevant:
<url-list>
+ weight: 0.20 "https://huggingface.co/docs/datasets/en/loading": "Load - Hugging FaceThis saves time because instead of waiting for the Dataset builder download to time out, Datasets will look directly in the cache. Set the environment ...Some datasets may have more than one version based on Git tags, branches, or commits. Use the revision parameter to specify the dataset version you want to load ..."
+ weight: 0.20 "https://huggingface.co/docs/datasets/en/index": "Datasets - Hugging Face🤗 Datasets is a library for easily accessing and sharing datasets for Audio, Computer Vision, and Natural Language Processing (NLP) tasks. Load a dataset in a ..."
+ weight: 0.17 "https://github.com/huggingface/datasets/issues/7175": "[FSTimeoutError] load_dataset · Issue #7175 · huggingface/datasetsWhen using load_dataset to load HuggingFaceM4/VQAv2, I am getting FSTimeoutError. Error TimeoutError: The above exception was the direct cause of the following ..."
+ weight: 0.15 "https://github.com/huggingface/datasets/issues/6465": "`load_dataset` uses out-of-date cache instead of re-downloading a ...When a dataset is updated on the hub, using load_dataset will load the locally cached dataset instead of re-downloading the updated dataset."
+ weight: 0.12 "https://stackoverflow.com/questions/76923802/hugging-face-http-request-on-data-from-parquet-format-when-the-only-way-to-get-i": "Hugging face HTTP request on data from parquet format when the ...I've had to get the data from their data viewer using the parquet option. But when I try to run it, there is some sort of HTTP error. I've tried downloading ..."
</url-list>
</action-visit>
记得将 URL 权重放入 agent 的上下文中,并指示 LLM 遵守这些权重。
tag结论
自从我们的 DeepSearch 系统在 2025 年 2 月 2 日发布以来,我们发现了两个显著改善质量的实现细节。值得注意的是,这两个细节都以"上下文"方式使用多语言嵌入和重排序器——以远小于这些模型通常需要的预计算索引的规模运行。这解释了我们最初的疏忽。
这表明搜索技术的未来呈现出一种有趣的两极分化。考虑一个类似于 Kahneman 双系统理论的框架:
- 快思维(grep、BM25、SQL):快速、基于规则的模式匹配,计算需求最小。
- 慢思维(LLM):具有深度上下文理解的全面推理,需要大量计算。
- 中思维(embeddings、rerankers):处于模棱两可的状态?对于简单的模式匹配来说太"先进"/语义化,但又缺乏真正的推理能力。
我们可能正在见证一种双分架构的流行,其中轻量级、高效的 SQL/BM25 处理初始内容检索,直接输入到强大的 LLM 中进行深度处理。这些 LLM 越来越多地整合了以前需要专门的中层模型的语义功能。中思维模型的剩余角色转向专门的上下文任务:过滤、去重和有限范围的操作,在这些操作中完全推理将是低效的。
尽管如此,选择关键片段和对 URL 进行排名仍然是直接影响 DeepSearch/DeepResearch 系统质量的基本组件。我们希望我们的见解能激发您在自己的实现中做出改进。
查询扩展仍然是另一个关键的质量决定因素。我们正在积极评估多种方法——从基本的基于提示的重写到小型语言模型和基于推理的方法。请期待我们即将发布的这方面的研究成果。敬请关注。