
Se hai già letto la nostra guida all'implementazione di DeepSearch/DeepResearch, approfondiamo alcuni dettagli che possono migliorare notevolmente la qualità. In questo post, ci concentreremo su due sfide chiave: sfruttare gli embedding per la selezione di frammenti da pagine web lunghe e utilizzare i reranker per dare priorità agli URL durante il crawling.
Alcuni potrebbero ricordare la nostra precedente conclusione secondo cui "gli embedding erano utili solo per la deduplicazione delle query come i task STS (semantic textual similarity), mentre i reranker non facevano nemmeno parte della nostra implementazione originale di DeepSearch". Bene, si scopre che entrambi sono ancora molto utili - solo non nel modo convenzionale che ci si potrebbe aspettare. Abbiamo sempre seguito il percorso più snello possibile. Non aggiungiamo componenti solo per giustificare la loro esistenza o il nostro valore come fornitore di embedding e reranker. Siamo concentrati su ciò di cui la ricerca ha davvero bisogno come fondamento.
Quindi dopo settimane di esperimenti e iterazioni, abbiamo scoperto utilizzi insoliti ma efficaci per entrambi nei sistemi DeepSearch/DeepResearch. Applicandoli, abbiamo migliorato significativamente la qualità di Jina DeepSearch (sentiti libero di provarlo). Vorremmo condividere queste intuizioni con altri professionisti che lavorano in questo ambito.
tagSelezionare Frammenti da Contenuti Lunghi
Il problema è questo: dopo aver utilizzato Jina Reader per leggere il contenuto della pagina web, dobbiamo aggiungerlo come elemento di conoscenza al contesto dell'agente per il ragionamento. Mentre inserire l'intero contenuto nella finestra di contesto del LLM è il modo più semplice, non è ottimale considerando i costi dei token e la velocità di generazione. In pratica, dobbiamo identificare quali parti del contenuto sono più rilevanti per la domanda e aggiungere selettivamente solo quelle parti come conoscenza al contesto dell'agente.
Il filtraggio basato su LLM ha gli stessi problemi di costo e latenza, quindi cerchiamo soluzioni con modelli più piccoli: abbiamo bisogno di modelli più piccoli ed economici, ma ancora multilingue – un fattore cruciale poiché non possiamo garantire che sia la query che i documenti saranno sempre in inglese.
Abbiamo una domanda da un lato (sia la query originale che una domanda di gap) e un grande contenuto markdown dall'altro, dove la maggior parte del contenuto è irrilevante. Dobbiamo selezionare i frammenti più rilevanti per la query. Questo assomiglia al problema di chunking con cui la comunità RAG si è confrontata dal 2023 - recuperare solo i chunk rilevanti utilizzando modelli di retrieval da inserire nella finestra di contesto per la sintesi. Tuttavia, ci sono due differenze chiave nel nostro caso:
- Chunks limitati da un numero limitato di documenti. Se ogni chunk contiene circa 500 token, allora un tipico documento web lungo ha circa 200.000 token (p50) fino a 1.000.000 di token (p99), e noi usiamo Jina Reader per recuperare 4-5 URL ad ogni passo, questo produrrebbe approssimativamente centinaia di chunk - ovvero centinaia di vettori di embedding e centinaia di similarità del coseno. Questo è facilmente gestibile con JavaScript in memoria senza un database vettoriale.
- Abbiamo bisogno di chunk consecutivi per formare frammenti di conoscenza efficaci. Non possiamo accettare frammenti che combinano frasi sparse come
[1-2, 6-7, 9, 14, 17, ...].
Un frammento di conoscenza più utile seguirebbe pattern come[3-15, 17-24, ...]
- mantenendo sempre testo consecutivo. Questo rende più facile per il LLM copiare e citare dalla fonte di conoscenza e riduce le allucinazioni.
Il resto sono tutte le problematiche di cui si lamentano i professionisti: ogni chunk non può essere troppo lungo poiché i modelli di embedding non possono gestire bene contesti lunghi; il chunking introduce perdita di contesto e rende gli embedding dei chunk i.i.d; e come trovare i migliori indizi di confine che mantengano sia la leggibilità che la semantica? Se sai di cosa stiamo parlando, allora probabilmente sei stato perseguitato da questi problemi nelle tue implementazioni RAG.
Ma in breve - il late-chunking con jina-embeddings-v3 risolve magnificamente tutti e tre i problemi. Il late chunking mantiene le informazioni di contesto per ogni chunk, è insensibile agli indizi di confine, e jina-embeddings-v3 stesso è SOTA nei task di retrieval multilingue asimmetrico. I lettori interessati possono seguire i nostri post del blog o i paper per i dettagli, ma ecco l'implementazione complessiva.
Conv1D
. Il processo inizia dividendo un documento lungo in chunk di lunghezza fissa, che vengono poi incorporati con jina-embeddings-v3 con l'opzione late-chunking attivata. Dopo aver calcolato i punteggi di similarità tra ogni chunk e la query, una finestra scorrevole si muove attraverso i punteggi di similarità per trovare la finestra con il valore medio più alto.


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");
}
Utilizzo del late chunking e del pooling medio tipo Conv1D per selezionare il miglior snippet rispetto alla domanda.
Assicurati di chiamare l'API Jina Embeddings con i parametri task
, late_chunking
e truncate
impostati come segue:
await axios.post(
'https://api.jina.ai/v1/embeddings',
{
model: "jina-embeddings-v3",
task: "retrieval.passage",
late_chunking: true,
input: chunks,
truncate: true
},
{ headers });
Per l'embedding della domanda, assicurati di cambiare task
in retrieval.query
e disattivare late_chunking
L'implementazione completa può essere trovata su Github:
tagClassificazione degli URL per la prossima lettura
Il problema è questo: durante una sessione DeepSearch, probabilmente raccoglierai molti URL dalle pagine dei risultati del motore di ricerca (SERP) e ne scoprirai ancora di più ogni volta che leggi singole pagine web (quei link nelle pagine). Il conteggio totale degli URL unici può facilmente raggiungere le centinaia. Ancora una volta, inserire semplicemente tutti gli URL direttamente nel contesto dell'LLM è inefficiente - spreca prezioso spazio nella finestra di contesto e, più problematicamente, abbiamo scoperto che gli LLM essenzialmente selezionano gli URL in modo casuale. È fondamentale guidare l'LLM verso gli URL che hanno la più alta probabilità di contenere la risposta necessaria.
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"
Migliore opzione per utilizzare Jina Reader per crawlare una pagina in DeepSearch. Questo raccoglierà tutti i link della pagina in un campo links
separato, rimuovendoli dal campo content
.
Pensa a questo problema come a un PageRank contestuale dove dobbiamo pesare centinaia di URL durante una sessione. Classifichiamo gli URL basandoci su molteplici fattori che combinano l'ultimo tempo di aggiornamento, la frequenza del dominio, la struttura del percorso e, più importante, la rilevanza semantica rispetto alla query per creare un punteggio composito. Ricorda che possiamo utilizzare solo le informazioni disponibili prima di visitare effettivamente l'URL:
Segnali di Frequenza: Gli URL che appaiono più volte attraverso diverse fonti ricevono un peso aggiuntivo. Gli URL da domini che appaiono frequentemente nei risultati di ricerca ricevono un boost, poiché i domini popolari spesso contengono contenuti autorevoli.
Struttura del Percorso: Analizziamo i percorsi degli URL per identificare cluster di contenuti. Gli URL all'interno di gerarchie di percorsi comuni ricevono punteggi più alti, con un fattore di decadimento applicato ai percorsi più profondi.
Rilevanza Semantica: Utilizziamo jina-reranker-v2-base-multilingual per valutare la rilevanza semantica tra la domanda e le informazioni testuali di ciascun URL, che è un classico problema di riordinamento. Le informazioni testuali di ciascun URL provengono da:
- Titolo e snippet dai risultati delle API SERP (
https://s.jina.ai/
con'X-Respond-With': 'no-content'
) - Testo ancora degli URL nella pagina (
https://r.jina.ai
con'X-With-Links-Summary': 'all'
)
Ultimo Tempo di Aggiornamento: Alcune query DeepSearch sono sensibili al tempo, quindi gli URL aggiornati recentemente sono più preziosi di quelli più vecchi. Senza essere un motore di ricerca importante come Google, determinare in modo affidabile l'ultimo tempo di aggiornamento è una sfida. Abbiamo implementato un approccio multi-livello che combina i seguenti segnali e fornisce un timestamp con punteggio di confidenza che dà priorità ai contenuti più recenti quando necessario.
- Filtri API SERP (come il parametro
tbs
di s.jina.ai per filtrare per recenza) - Analisi dell'header HTTP (Last-Modified, ETag)
- Estrazione dei metadati (meta tag, timestamp Schema.org)
- Riconoscimento dei pattern di contenuto (date visibili in HTML)
- Indicatori specifici per CMS per piattaforme come WordPress, Drupal e Ghost
Contenuto Protetto: Alcuni contenuti sulle piattaforme di social media sono protetti o semplicemente dietro paywall, e senza effettuare l'accesso o violare i loro ToS, non c'è modo legittimo di recuperare questi contenuti. Dovremmo mantenere attivamente una lista di URL e nomi host problematici per abbassare i loro ranking, evitando di perdere tempo su contenuti inaccessibili.
Diversità di Dominio: In alcuni casi, gli URL con il peso più alto provengono tutti dagli stessi nomi host, il che può intrappolare DeepSearch in un ottimo locale e ridurre la qualità finale dei risultati. Controlla gli esempi sopra dove tutti gli URL principali provengono da StackOverflow. Per migliorare la diversità, possiamo implementare un approccio di esplorazione-sfruttamento selezionando i k URL con il ranking più alto da ciascun nome host.
L'implementazione completa della classificazione degli URL può essere trovata sul nostro 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>
Ricorda di inserire i pesi degli URL nel contesto dell'agente e di istruire i LLM a rispettare i pesi.
tagConclusione
Dal rilascio del nostro sistema DeepSearch il 2 febbraio 2025, abbiamo scoperto due dettagli implementativi che hanno migliorato sostanzialmente la qualità. Sorprendentemente, entrambi utilizzano embedding e reranker multilingue in modo "in-context" - operando su una scala molto più piccola rispetto agli indici pre-calcolati che questi modelli tipicamente richiedono. Questo spiega la nostra iniziale svista.
Ciò indica un'affascinante polarizzazione nel futuro della tecnologia di ricerca. Consideriamo un framework analogo alla teoria del doppio processo di Kahneman:
- Fast-think (grep, BM25, SQL): Corrispondenza di pattern rapida e governata da regole con minime esigenze computazionali.
- Slow-think (LLM): Ragionamento completo con profonda comprensione contestuale, che richiede una computazione significativa.
- Mid-think (embedding, reranker): Intrappolati nel limbo? Troppo "avanzati"/semantici per la semplice corrispondenza di pattern ma privi di vere capacità di ragionamento.
Potremmo star assistendo alla popolarità di un'architettura biforcata dove SQL/BM25 leggeri ed efficienti gestiscono il recupero iniziale dei contenuti, alimentando direttamente potenti LLM per l'elaborazione profonda. Questi LLM incorporano sempre più le funzioni semantiche che prima richiedevano modelli specializzati di livello intermedio. Il ruolo rimanente per i modelli mid-think si sposta verso attività in-context specializzate: filtraggio, deduplicazione e operazioni di portata limitata dove un ragionamento completo sarebbe inefficiente.
Tuttavia, la selezione di snippet critici e il ranking degli URL rimangono componenti fondamentali con un impatto diretto sulla qualità dei sistemi DeepSearch/DeepResearch. Speriamo che le nostre intuizioni stimolino miglioramenti nelle vostre implementazioni.
L'espansione delle query continua a essere un altro determinante cruciale della qualità. Stiamo valutando attivamente molteplici approcci—dai basic prompt-based rewrite ai small language model e metodi basati sul ragionamento. Cercate i nostri prossimi risultati su questo fronte presto. Restate sintonizzati.