Dartboard RAG: Що робити, коли Top-K повертає один і той самий фрагмент тричі
Простий метод збору даних top-k може повертати майже ідентичні фрагменти. Dartboard RAG обирає дані з урахуванням як релевантності, так і різноманітності; ось алгоритм, код та ситуації, коли його використовувати.
Джерела: Dartboard RAG: When Top-K Returns Three Versions of the Same Chunk
Простий метод знаходження топ-к елементів працює неефективно. Коли в корпусі тексту одне й те саме описано у кількох місцях, k найближчих фрагментів до запиту можуть виявитися одним фрагментом, у якому це описано тричі різними словами.
Уявіть 100-сторінковий звіт про зміну клімату, у якому п’ять розділів присвятовані парниковим газам; кожен з них розглядає цю тему трохи по-різному, але доходить до одного й того самого висновку. Хтось запитує: «Що спричиняє зміну клімату?», і індекс FAISS повертає наступне:
Top 3:
1. "Greenhouse gases trap heat in the atmosphere, causing warming..." (sim 0.91)
2. "Atmospheric greenhouse gases are the primary driver of climate change..." (sim 0.89)
3. "The trapping of heat by greenhouse gases leads to rising temperatures..." (sim 0.88)
Це три фрагменти, які по суті є одним фактом. Користувач запитав про причини у множині, а отримав одну й ту саму причину. Фрагменти про вирубку лісів, промислове сільське господарство та зміни використання землі знаходилися на 8, 12 та 17 місцях, тож жоден з них не потрапив до контексту ШІ. Відповідь моделі відображає її вхідні дані: послідовний абзац про парникові гази без згадки про інші причини.
Проблемою не є векторний пошук. Він робив те, що йому було сказано — повертав найбільш схожі результати. Проблема в тому, що поняття схожості не враховує редундантність. Три фрагменти можуть бути максимально схожими на запит та водночас майже ідентичними один одному, і звичайний алгоритм top-k поверне всі три, виключивши інші релевантні матеріали.
Dartboard RAG змінює правило вибору. Якщо стандартний метод пошуку шукає k фрагментів, найближчих до запиту, то цей підхід шукає k фрагментів, кожен з яких є дуже релевантним та водночас знаходиться на значній відстані один від одного. У результаті отримуються релевантні частини корпусу, а не кілька копій найкращого знахідки.
Уявіть дартборд
Розглядайте запит як центральну точку, а кожен фрагмент — як дарт, вже встромлений у дошку. Чим ближче фрагмент до центру, тим він релевантніший.
Chunks plotted by relevance to query
(closer to center = higher cosine sim)
●●● ← cluster of near-duplicate chunks
●●● (all about "greenhouse gases")
● bull's-eye = QUERY
● ← chunk about deforestation
(relevant but different topic)
● ● ← chunks about agriculture, ocean carbon
● ● ← chunks about historical climate
STANDARD TOP-3 picks: DARTBOARD TOP-3 picks:
3 closest darts 1 closest, then darts that are also
= 3 darts in the same good but spread across the board
spot near bull's-eye = better coverage of relevant content
Звичайний метод пошуку збирає три найближчі дротики. Метод дошки для дротиків збирає три дротики, які є близькими один до одного, але водночас розташовані й на відстані.
Загальна схема
З архітектурної точки зору це стандартний метод пошуку з додатковим етапом розумнішого вибору. Сховище векторів та ембеддинги залишаються такими ж, як були. Єдиним новим елементом є крок жадібного вибору, який вирішує, які з 15 кандидатів потраплять до фінальних 5.
Крок 1: отримати більше, ніж потрібно
Алгоритм не може обирати між кандидатами, яких у нього немає, тому йому потрібен набір, більший за кінцеву кількість k. У базовій реалізації цей набір множиться на коефіцієнт перепробування 3:
fetch_k = self.k * self.oversampling # default: 5 × 3 = 15
candidates = vector_store.search(query_embedding, k=fetch_k)
Отже, ви берете 15 кандидатів, а не 5. FAISS повертає їх у порядку зростання косинусової схожості, що розміщує кандидата 0 найближче до запиту, а кандидата 14 — найдалі. На наступному етапі вибору обирається найкращий набір з п’яти кандидатів шляхом поєднання оцінок релевантності та різноманітності.
Наступний етап вимагає ембеддингів кожного кандидата, а не лише його тексту, тому набір даних завантажується у вигляді повних об’єктів Document із приєднаними ембеддингами.
Крок 2: створення двох матриц відстаней
На основі ембеддингів кандидатів обчислюються дві матриці:
# Normalize all vectors so dot product = cosine similarity
query_norm = query_vec / np.linalg.norm(query_vec)
cand_norm = candidate_matrix / np.linalg.norm(candidate_matrix, axis=1, keepdims=True)
# Distance = 1 - cosine_similarity
query_distances = 1.0 - np.dot(query_norm, cand_norm.T) # (1, N)
document_distances = 1.0 - np.dot(cand_norm, cand_norm.T) # (N, N)
query_distancesмістить відстань від запиту до кожного кандидата. Низьке значення означає високу релевантність. Його форма — (1, N).document_distancesмістить парну відстань між кожними двома кандидатами. Низьке значення означає, що ці кандидати схожі один на одного. Його форма — (N, N).
Релевантність визначається за допомогою першої матриці, а різноманітність — за допомогою другої.
Крок 3: перетворення відстаней на лог-ймовірності
Чисті відстані погано поєднуються між собою. Різниця між 0,3 та 0,4 може здаватися значущою, проте просте віднімання дає 0,1 та недооцінює її. Тому алгоритм перетворює відстані на лог-ймовірності за допомогою гауссівського ядра:
def lognorm(dist, sigma):
return -np.log(sigma) - 0.5 * np.log(2 * np.pi) - dist**2 / (2 * sigma**2)
Це лог-графік гауссівської щільності. Інтуїтивно кажучи, невелика відстань дає високу ймовірність (близькість означає, що об’єкти, ймовірно, є релевантними або схожими), велика відстань — низьку, а sigma визначає наскільки гострою є крива. Малий значення sigma, за замовчуванням 0,1, створює щільні піки, які сильно реагують на невеликі різниці відстаней. Більше значення, наприклад 0,5, розплющує криву, тож схожі відстані обробляються більш однаково.
Робота у логарифмічному просторі є важливою для числової стабільності. При невеликих відстанях та малому значенні sigma первинні ймовірності зменшуються до дуже малих чисел, які можуть стати рівними нулю через занадто малі значення, тоді як логарифмічні ймовірності залишаються в межах та порівнянними.
Крок 4: цикл жадібності
Саме тут реалізується логіка цього алгоритму. Процедура виглядає так:
- Спочатку беріть найбільш релевантний фрагмент. Поки що не потрібен тест на різноманітність, оскільки ще не обрано нічого для порівняння.
- Зберігайте значення
max_distances, яке фіксує для кожного кандидата найбільшу відстань у логарифмічних ймовірностях до будь-якого обраного документа. Чим вище це значення, тим більша різниця з поточним вибором. - Для кожної залишеної позиції оцінюйте кожного невибраного кандидата, поєднуючи показник релевантності (наскільки він близький до запиту, з
query_probs) з показником різноманітності (наскільки він далекий від найближчого вже обраного кандидата, зmax_distances). Сума дорівнюєdiversity_weight × diversity + relevance_weight × relevance. - Об’єднуйте отримані оцінки за допомогою функції logsumexp, яка забезпечує числову стабільність.
- Оберіть кандидата з найвищою сумарною оцінкою, додайте його та оновіть значення
max_distances.
У коді:
# Step 1: pick most relevant first
most_relevant_idx = np.argmax(query_probs)
selected_indices = [most_relevant_idx]
max_distances = doc_probs[most_relevant_idx].copy() # diversity tracker
# Step 2-6: iteratively add diverse + relevant chunks
while len(selected_indices) < num_results:
# For each candidate, compute "diversity from any selected"
updated_distances = np.maximum(max_distances, doc_probs)
# Combine relevance + diversity
combined = (diversity_weight * updated_distances
+ relevance_weight * query_probs[np.newaxis, :])
# Aggregate per candidate (logsumexp for numerical stability)
normalized = logsumexp(combined, axis=1)
# Mask already-selected
for idx in selected_indices:
normalized[idx] = -np.inf
# Pick the best
best_idx = np.argmax(normalized)
max_distances = updated_distances[best_idx]
selected_indices.append(best_idx)
Основна ідея — це комбінований бал. Кожен вибір оцінюється за критеріями «Чи є це релевантним?» та «Чи відрізняється це від того, що вже маємо?». Перший вибір максимізує чисту релевантність, а кожен наступний вибір знаходить баланс між цими двома факторами.
Що неформально роблять математичні формули
На кожному кроці алгоритм фактично запитує: серед кандидатів, які ще не обрані, який з них найкраще покращує як свій бал релевантності, так і охоплення відповідної області?
Уявіть собі 2D-діаграму розсіювання, де по осі X відображається релевантність, а по осі Y — відстань від обраного набору. Перший вибір — це точка, розташована найдалі праворуч. У всіх наступних кандидатів є значення Y, яке відповідає їхній відстані від цього першого вибору.
Для другого вибору потрібно щось, що знаходиться далеко праворуч (релевантне), а також вище (на відміну від першого). Оскільки обидві ваги дорівнюють 1,0, баланс є рівним, тож цикл вибере фрагмент, який є трохи менш релевантним, але значно більш відмінним.
Relevance →
Low ●◯
● ● ← Standard top-k picks these 3
(highest relevance, regardless of diversity)
● ●
●●●●●● ● ● ● ← Many similar high-relevance chunks
High (cluster)
Diversity ↓
from
selected
↓
↓ ↓ ← Dartboard picks 1 from cluster,
then far-away ones with high relevance still
Простий підхід top-k обирає найправіші точки, незалежно від того, наскільки вони згруповані. Підхід Dartboard спочатку обирає найправішу точку, а потім навмисно розширює пошук на інші відповідні області.
Два ваги
relevance_weight = 1.0 # how much we care about chunks being close to query
diversity_weight = 1.0 # how much we care about chunks being different from each other
Обидва параметри за замовчуванням дорівнюють 1,0, що означає однакову вагу. Зміна їхніх значень змінює поведінку алгоритму: підвищуйте параметр різноманітності, якщо продовжують з’являтися дублікати, та підвищуйте параметр релевантності, якщо у результати пошуку потрапляють неактуальні фрагменти. За замовчуванням ці значення підходять для більшості корпусів.
Приклад роботи з корпусом, насиченим дублікатами
У базовій реалізації є параметр duplicate_factor, який повторює кожен фрагмент N разів — це корисний спосіб перевірки різноманітності на штучно насиченому корпусі. Встановіть його на 5 та поставте запитання „Що спричиняє зміну клімату?“
Стандартний пошук топ-5 дає наступний результат:
1. "Greenhouse gases cause warming..." (sim 0.91)
2. "Greenhouse gases cause warming..." (sim 0.91) ← DUPLICATE
3. "Greenhouse gases cause warming..." (sim 0.91) ← DUPLICATE
4. "Greenhouse gases cause warming..." (sim 0.91) ← DUPLICATE
5. "Greenhouse gases cause warming..." (sim 0.91) ← DUPLICATE
Unique results: 1/5
П’ять результатів є буквально одним цілим, усі вони однаково близькі до запиту, тож ШІ отримує один факт, переформульований п’ятьма способами.
Рейтинг топ-5 від Dartboard дає наступне:
1. "Greenhouse gases cause warming..." (highest relevance — wins first pick)
2. "Deforestation reduces the carbon sink..." (different chunk, still relevant)
3. "Industrial agriculture emits methane..." (third unique cause)
4. "Land-use changes alter surface albedo..." (fourth unique cause)
5. "Fossil fuel combustion is the largest CO₂ source..." (related to #1 but different angle)
Unique results: 5/5
Як тільки обирається перший фрагмент, показник різноманітності карає кожну його копію, оскільки кожна з них має нульову різноманітність порівняно з обраним фрагментом. Тому цикл шукає унікальний контент у слотах від двох до п’яти, і ШІ бачить п’ять різних фактів про причини. Це найкращий варіант для системи оцінки. У реальних корпусах даних рідко трапляються точні дублікати, тож ефект є менш вираженим, хоча та сама логіка застосовується й до майже ідентичних записів.
Усе це приблизно у двадцяти рядках
def dartboard_select(query_emb, candidate_embs, k=5, sigma=0.1):
# 1. Compute distance matrices
query_dist = 1 - cosine(query_emb, candidate_embs) # query→each
doc_dist = 1 - cosine(candidate_embs, candidate_embs) # each→each
# 2. Convert distances to log-probabilities
query_probs = lognorm(query_dist, sigma)
doc_probs = lognorm(doc_dist, sigma)
# 3. Pick most relevant first
selected = [np.argmax(query_probs)]
max_distances = doc_probs[selected[0]].copy()
# 4. Iteratively add diverse + relevant
while len(selected) < k:
updated = np.maximum(max_distances, doc_probs)
combined = updated + query_probs[np.newaxis, :] # equal weights = sum
scores = logsumexp(combined, axis=1)
for idx in selected:
scores[idx] = -np.inf # don't re-select
best = np.argmax(scores)
max_distances = updated[best]
selected.append(best)
return selected
Довжина є помірною. Складність полягає у тому, що означають математичні формули, а не у кількості рядків. Як тільки правило стає зрозумілим (вибрати найбільш релевантний фрагмент, а потім віддати перевагу кандидатам, які є релевантними та відрізняються від вже обраного), код стає легким для розуміння.
Параметри, які варто налаштувати
Два параметри заслуговують уваги.
oversamplingвизначає розмір набору кандидатів. Значення за замовчуванням 3 означає 15 кандидатів для отримання 5 фінальних. Підвищення цього значення до 5 або 10 дає алгоритму більше можливостей для роботи, але уповільнює обчислення взаємних відстаней. У корпусі з сильним дублюванням параметр oversampling 10× дозволяє алгоритму знаходити більш унікальні фрагменти на більших позиціях рейтингу.diversity_weightмає зростати, якщо під час оцінювання все ще знаходяться дублікати, та знижуватися, якщо результати надто розсіяні, а фрагменти поза темою обираються лише через те, що вони „інші“. Значення 1.0 є збалансованим, а значення 1.5 чи 2.0 є доцільними для дуже щільних корпусів.
sigma є більш делікатним параметром та рідко потребує змін. За замовчуванням значення 0.1 підходить для косинусових відстаней у ембеддингах OpenAI. Якщо ви перейдете на модель ембеддингів з іншими масштабами відстаней, можливо, доведеться його переналаштувати, хоча більшість користувачів можуть залишити його без змін.
Де це допомагає, а де — ні
Dartboard підходить для кількох ситуацій.
- Корпуси з майже однаковими даними. Багато джерел, що охоплюють одну тему, повторювані документи з політики, архіви новин із перекриваючимися матеріалами, індексовані повідомлення з форумів. Стандартний підхід top-k тут зазвичай не ефективний.
- Обсяг інформації настільки ж важливий, як і точність. У разі узагальнень досліджень, детальних запитань та відповідей чи порівняльних аналізів користувач хоче отримати всю сукупність релевантної інформації, а не лише один найкращий фрагмент.
- Додаткові обчислювальні ресурси. Додаткові витрати пов’язані з арифметикою матриц над набором кандидатів, а не з додатковими викликами ШІ. Це майже непомітно для k=5 з 15 кандидатами, але помітно для k=20 з 60 чи більше кандидатами.
У таких випадках це допомагає менше.
- Природно різноманітні корпуси даних. Якщо фрагменти вже є тематично різними завдяки семантичному розділенню чи добре структурованим документам, метод top-k все одно поверне різноманітні фрагменти, а інструмент dartboard лише додасть надлишкову складність, не змінюючи ранжування.
- Дуже конкретні запитання. На запитання „Яка температура кипіння ртуті?“ існує одна правильна відповідь. Логіка різноманітності може обрати менш якісні фрагменти заради різноманітності, що погіршить якість відповіді.
- Дуже мале значення k. При k, дорівнювальному 1 або 2, майже немає можливостей для різноманітності, тому звичайний метод top-k підходить ідеально.
- Конвеєри обробки даних, які вже поєднують фузію, переранжування та інші операції. Кожен з цих елементів вирішує пов’язані проблеми по-своєму. Додавання інструменту dartboard до такої складної системи рідко допомагає та лише збільшує складність.
Ідея, яку варто запам’ятати
Цікавим аспектом Dartboard RAG є не математика, а усвідомлення того, що сама лише релевантність — це неправильна мета для методу отримання даних top-k.
Стандартний підхід top-k шукає ті k елементів, які є найбільш релевантними окремо. Це означає, що кожен елемент розглядається окремо, а те, що вже є у наборі результатів, ігнорується; тому, коли існує кілька однаково релевантних елементів, вони всі потрапляють у результат, навіть якщо вони говорять про одне й те саме.
Краща мета полягає у тому, щоб визначити, які k елементів разом найкраще відповідають потребам запиту. Це інша форма оптимізації, у якій оцінка елемента k залежить від елементів від 1 до k-1, а не лише від його власної релевантності. Такі жадібні методи, як Dartboard, є прийнятними наближеннями до цього підходу.
Ця концепція знаходить застосування не лише у процесі отримання даних:
- Вибірка навчальних даних. Техніки активного навчання використовують вибірку, що враховує різноманітність, з тієї ж причини.
Загальний урок полягає у тому, що при виборі набору елементів слід оцінювати самі набори, а не окремі елементи. Dartboard — це версія принципу RAG, і як тільки ви його засвоїте, побачите ту саму помилку в багатьох інших схемах типу «ранжуй, потім візьми топ-k».
Є своя ціна: додаткові обчислення та складність пояснення того, чому один фрагмент кращий за інший. Винагородою є краще охоплення, що часто безпосередньо призводить до кращих відповідей, особливо коли корпус даних не впорядкований так, щоб кожен окремий факт був у своєму фрагменті.