Головна / Статті / Практичні нотатки: Серія AI: Найскладніша частина RAG, частина 2 — Чому ідеально

Практичні нотатки: Серія AI: Найскладніша частина RAG, частина 2 — Чому ідеально

Покрокове пояснення до практичних нотаток: Серія про ШІ: Найскладніша частина RAG, частина 2 — Чому ідеально: контракти, перевірки та слоти для коду для команд, які використовують цю схему.

1548 слів

У цьому посібнику детально описано шлях від сировини до функціональної системи для проекту: The AI Series: Найскладніша частина RAG, Частина 2 — Чому навіть ідеальні фрагменти все одно призводять до поганого отримання інформації. Основна увага приділяється конкретним крокам виконання, чітким перевіркам та коду, який можна просто додати до репозиторію, не здогадуючись про його призначення. На етапі огляду необхідно визначити вхідні дані, виконавця кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан системи. Необхідно фіксувати час виконання та витрати на токени чи запити разом із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-версії до спільних середовищ.

По-перше, що насправді таке ембеддинг?

Під час роботи над першим етапом «Що це?» спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища секретних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Вимірюйте рівень відтворення інформації за фіксованим набором запитань перед налаштуванням промптів. Зміна промптів рідко вирішує проблеми слабкого пошуку інформації.

"How do I reset my password?"     → [0.81, 0.12, -0.44, 0.09]
"Steps to change your password"   → [0.79, 0.15, -0.41, 0.11]
"How do I cancel my subscription?"→ [0.22, 0.68, 0.05, -0.39]
from numpy import dot
from numpy.linalg import norm

def cosine_similarity(a, b):
    return dot(a, b) / (norm(a) * norm(b))

query = [0.80, 0.13, -0.42, 0.10]  # "reset my password"

print(cosine_similarity(query, [0.81, 0.12, -0.44, 0.09]))  # 0.998 - very close
print(cosine_similarity(query, [0.22, 0.68, 0.05, -0.39]))  # 0.310 - far apart

Чому пошук насправді складний

Під час роботи над етапом «Чому отримання даних є справжнім», спочатку запишіть умови виконання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Одночасно задокументуйте шлях успішного виконання та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Вимірюйте рівень відтворення інформації за фіксованим набором запитань перед налаштуванням підказок. Зміна підказок рідко допомагає вирішити проблеми з недостатньою ефективністю отримання даних.

Як виглядає справжня система отримання даних

Під час роботи над етапом «Що таке справжнє отримання даних», спочатку запишіть умови взаємодії: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на складну послідовність операцій. Вимірюйте рівень відтворення інформації за фіксованим набором запитань перед налаштуванням запрошень до виконання завдань. Часті зміни запрошень рідко допомагають покращити якість отримання даних. Під час роботи над етапом «Що таке справжнє отримання даних», спочатку запишіть умови взаємодії: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-середовищ до спільних середовищ.

Гібридний пошук: чому сам пошук за векторами недостатній

Гібридний пошук за векторами працює найкраще, коли його розглядають як вимірювану поверхню. Зафіксуйте один ідеальний приклад, один випадок невдачі та примітки щодо скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію поза кодом додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь граф. Розділіть політику часткового оброблення даних від політики їх пошуку. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

from collections import defaultdict

def reciprocal_rank_fusion(result_lists, k=60):
    scores = defaultdict(float)
    for result_list in result_lists:
        for rank, doc_id in enumerate(result_list):
            scores[doc_id] += 1 / (k + rank + 1)
    return sorted(scores, key=scores.get, reverse=True)

vector_ids = [d.metadata["chunk_id"] for d in vector_store.similarity_search(query, k=20)]
bm25_ids = [d.metadata["chunk_id"] for d in bm25_index.search(query, k=20)]

final_ranking = reciprocal_rank_fusion([vector_ids, bm25_ids])

Переранжування — це крок, який ніхто не пропускає двічі

Етап переранкінгу працює найкраще, якщо його розглядати як вимірювану поверхню. Зафіксуйте один ідеальний варіант виконання, один випадок невдачі та примітку про скасування змін перед розширенням обсягу роботи. Документуйте як успішний, так і відновлювальний сценарії роботи. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Розділіть політику часткової обробки даних від політики їх пошуку. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

from sentence_transformers import CrossEncoder

reranker = CrossEncoder("BAAI/bge-reranker-base")

def rerank(query, candidates, top_n=5):
    pairs = [(query, c["text"]) for c in candidates]
    scores = reranker.predict(pairs)
    for c, s in zip(candidates, scores):
        c["rerank_score"] = float(s)
    return sorted(candidates, key=lambda c: c["rerank_score"], reverse=True)[:top_n]

Виправлення проблеми, а не лише індексу

Етап «Виправлення запитання „Не“» функціонує найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед складними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність дій. Розділяйте політику часткового оброблення даних та політику їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості. Етап «Виправлення запитання „Не“» функціонує найкраще, коли його розглядають як вимірювану поверхню. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Візуалізація витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-середовищ до спільних середовищ.

def hyde_retrieve(query, llm, vector_store, k=5):
    hypothetical = llm.invoke(f"Write a short passage answering: {query}")
    return vector_store.similarity_search(hypothetical, k=k)

Вимірювання замість оцінки на око

Для етапу «Вимірювання замість оцінки» необхідно визначити вхідні дані, відповідального за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись визначити прихований стан. Конфігурацію слід тримати окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функціоналу мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Наводьте ті уривки, які фактично лягли в основу відповіді. Без посилань оператори не зможуть відрізнити галюцинацію від проблем з індексуванням.

def recall_at_k(retrieved_ids, relevant_ids, k):
    top_k = set(retrieved_ids[:k])
    return len(top_k & relevant_ids) / len(relevant_ids) if relevant_ids else 0.0

Що б ви порадили тому, хто починає працювати над цим сьогодні

На етапі «Що ви скажете» необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Необхідно документувати як оптимальний, так і альтернативний сценарії виконання. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації. Наводьте конкретні уривки тексту, які лягли в основу відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.

Неприємна правда про RAG

Для етапу «The Uncomfortable Truth About» необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок з відомої точки контролю, не здогадуючись про прихований стан. Краще використовувати невеликі, перевірювані одиниці коду замість об’ємних скриптів. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану структуру обробки даних. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням. Для етапу «The Uncomfortable Truth About» необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок з відомої точки контролю, не здогадуючись про прихований стан. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу від демо-версії до публікації.

середовищах.

Чек-лист операцій

Етап чек-листу операцій працює найкраще, якщо його розглядати як вимірювану основу. Збережіть один ідеальний запис, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи.

Розглядайте цей етап як угоду між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не допускайте мовчазного часткового виконання завдань.

Розділіть політику поділу на частини від політики отримання даних. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.

Оцінюйте відповіді, надані за один крок, та послідовність дій у кілька кроків окремо. Агрегація оцінок чату приховує проблеми, пов’язані з циклами обробки даних.

Напишіть короткий посібник: як змінювати ключі, як спорожнювати чергу, як скасовувати останнє завантаження даних.

Документуйте одночасно шлях успішної роботи та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації.

Перш ніж піднімати стек на новий рівень, заморозьте версії, зафіксуйте ідеальний запис для критичного шляху та підтвердьте кроки відкату. У спільних середовищах необхідні обмеження на частоту запитів, перевірки прав доступу та чіткий власник для зміни секретів. Віддавайте перевагу надійності перед креативними одноразовими демонстраціями.

Примітка для da3fc7392f6d: не включайте ключі постачальника до репозиторію, встановіть ліміт токенів на сеанс та зберігайте записи поруч із фіксами для оцінки, щоб подальша заміна моделей залишалася порівнянною.