Практичні поради: як виправити систему RAG, яка постійно отримує неправильний контекст
Покрокова інструкція з практичних порад: як виправити систему RAG, яка постійно отримує неправильний контекст: контракти, перевірки та готові блоки коду для команд, які використовують цю схему.
Використовуйте цей документ як оновлену версію ідей з матеріалу «Як виправити систему RAG, яка постійно отримує неправильний контекст» для спеціалістів: чіткі етапи, впорядковані блоки коду та примітки з відновлення, які залишаються при передачі обов’язків. Етап Огляду найкраще функціонує, якщо його розглядати як вимірювану основу. Збережіть один ідеальний запис, один випадок збою та примітки щодо скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій.
Вам потрібен випадок збою, який можна відтворити
Якщо вам потрібен етап перевірки на помилки, спочатку визначте вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок, починаючи з відомої точки контролю, без необхідності здогадуватися про прихований стан. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Призначте назви елементів, визначте критерії успіху та не допускайте мовчазного часткового завершення. Наводьте конкретні уривки, які лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинацію від проблем із індексуванням.
chunks = [
{
"id": "audit_03",
"source": "audit-logs",
"text": (
"Enterprise audit logs are retained for 365 days "
"before automatic deletion."
),
},
{
"id": "errors_07",
"source": "api-errors",
"text": (
"NX-204 means the requested resource exists but is not "
"available in the caller's current region."
),
},
{
"id": "exports_01",
"source": "csv-exports",
"text": (
"CSV exports run asynchronously and appear in the exports "
"panel when processing completes."
),
},
{
"id": "exports_04",
"source": "csv-exports",
"text": (
"A completed CSV download link remains active for seven days."
),
},
]
eval_cases = [
{
"query": "How long are enterprise audit logs kept?",
"relevant": {"audit_03"},
},
{
"query": "What does error NX-204 mean?",
"relevant": {"errors_07"},
},
{
"query": "How long is a CSV export link usable?",
"relevant": {"exports_04"},
},
]
Ви почали з навмисно простого механізму пошуку
Якщо ви починаєте з певної стадії, необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Записуйте час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-середовища до спільних. Наводьте конкретні уривки тексту, які лягли в основу відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.
import numpy as np
from sklearn.decomposition import TruncatedSVD
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.preprocessing import normalize
class LsaRetriever:
def __init__(self, chunks, dims=16):
self.chunks = chunks
self.tfidf = TfidfVectorizer(
stop_words="english",
ngram_range=(1, 2),
sublinear_tf=True,
)
term_matrix = self.tfidf.fit_transform(
chunk["text"] for chunk in chunks
)
# The corpus is tiny. SVD doesn't need dimensions it cannot use.
dims = min(
dims,
term_matrix.shape[0] - 1,
term_matrix.shape[1] - 1,
)
if dims < 1:
raise ValueError("Need more text to build the LSA index.")
self.svd = TruncatedSVD(
n_components=dims,
random_state=0,
)
self.index = normalize(
self.svd.fit_transform(term_matrix)
)
def search(self, query, limit=None):
query_vec = self.tfidf.transform([query])
query_vec = normalize(self.svd.transform(query_vec))
similarity = cosine_similarity(
query_vec,
self.index,
)[0]
ranked = np.argsort(similarity)[::-1]
if limit is not None:
ranked = ranked[:limit]
return [
(self.chunks[i], float(similarity[i]))
for i in ranked
]
1. 0.879 exports_01
CSV exports run asynchronously and appear in the exports panel...
2. 0.843 exports_02
Large exports are split into multiple compressed files.
3. 0.830 exports_03
Users can cancel an export while it is still queued...
4. 0.772 exports_04
A completed CSV download link remains active for seven days.
Найпростіший інструмент для виправлення помилок був найкориснішим
На найпростішій стадії відлагодження необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок з відомої точки контролю, не здогадуючись про прихований стан. Зберігайте конфігурацію окремо від коду програми. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь код. Наводьте ті уривки, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинацію від проблем з індексуванням. На найпростішій стадії відлагодження необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість знову виконати крок з відомої точки контролю, не здогадуючись про прихований стан. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на багато факторів одночасно.
це краще, ніж заплутана система трубопроводів.def show_hits(retriever, query, limit=5):
print(f"\n{query}\n")
for position, (chunk, score) in enumerate(
retriever.search(query, limit),
start=1,
):
print(
f"{position:>2}. {score:.3f} "
f"{chunk['id']} ({chunk['source']})"
)
print(f" {chunk['text']}\n")
Ви не хотіли, щоб оцінка залежала від точних формулювань
Під час роботи на етапі, який ви не хотіли використовувати, спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Цей перелік допомагає зберігати чесність пізніших змін у коді. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Призначте назви елементам, визначте критерії успіху та не допускайте беззвучного часткового виконання завдань. Вимірюйте рівень відтворення інформації на фіксованому наборі запитань перед налаштуванням запрошень до виконання завдань. Часта зміна запрошень рідко вирішує проблеми слабкого пошуку інформації.
if answer_hint in chunk["text"]:
...
def evaluate_retriever(retriever, cases, k=3):
recall_scores = []
reciprocal_ranks = []
for case in cases:
hits = retriever.search(case["query"])
relevant = case["relevant"]
relevant_positions = [
position
for position, (chunk, _) in enumerate(hits, start=1)
if chunk["id"] in relevant
]
found_in_top_k = sum(
position <= k
for position in relevant_positions
)
recall_scores.append(
found_in_top_k / len(relevant)
)
reciprocal_ranks.append(
1 / relevant_positions[0]
if relevant_positions
else 0.0
)
return {
f"recall@{k}": float(np.mean(recall_scores)),
"mrr": float(np.mean(reciprocal_ranks)),
}
Потім ви звинуватили розділення на частини
Під час роботи над етапом «Then you blamed chunking», спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Запишіть час виконання та витрати на токени або запити поруч із функціональними результатами. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-версії до спільних середовищ. Перевірте рівень відтворення інформації на фіксованому наборі запитань перед налаштуванням підказок. Часта зміна підказок рідко допомагає покращити ефективність пошуку.
chunk_size = 500
chunk_overlap = 50
BM25 зробив експеримент трохи ніяковим
Під час роботи з етапом експериментування за алгоритмом BM25 спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та те, що відбувається при частковій невдачі. Такий перелік допомагає зберігати чесність подальших змін у коді. Зберігайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код. Вимірюйте рівень відтворення інформації на фіксованому наборі запитань перед налаштуванням підказок. Зміни підказок рідко вирішують проблеми слабкої системи пошуку. Під час роботи з етапом експериментування за алгоритмом BM25 спочатку запишіть умови використання: необхідні вхідні дані, сигнал про успіх та те, що відбувається при частковій невдачі. Такий перелік допомагає зберігати чесність подальших змін у коді. Віддавайте перевагу невеликим, тестованим одиницям коду перед об’ємними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану послідовність операцій.
Ви все одно хотіли обидва сигнали
Обидві етапні роботи працюють найкраще, коли їх розглядають як вимірювану поверхню. Запишіть один ідеальний результат, один випадок невдачі та примітку про скасування змін перед розширенням обсягу роботи. Розглядайте цей етап як контракт між вхідними даними та перевіреними результатами. Позначте всі елементи, визначте критерії успіху та не погоджуйтесь на мовчазне часткове виконання завдань. Розділіть політику часткового оброблення даних від політики їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості.
from collections import defaultdict
def fuse_rankings(vector_hits, bm25_hits, rrf_k=60):
fused = defaultdict(float)
chunks_by_id = {}
for hits in (vector_hits, bm25_hits):
for rank, (chunk, _) in enumerate(hits, start=1):
chunk_id = chunk["id"]
chunks_by_id[chunk_id] = chunk
fused[chunk_id] += 1 / (rrf_k + rank)
ranked_ids = sorted(
fused,
key=fused.get,
reverse=True,
)
return [
(chunks_by_id[chunk_id], fused[chunk_id])
for chunk_id in ranked_ids
]
def hybrid_search(query, lsa, bm25, candidate_k=20):
vector_hits = lsa.search(query, limit=candidate_k)
bm25_hits = bm25.search(query, limit=candidate_k)
return fuse_rankings(vector_hits, bm25_hits)
Поновне ранжування було останнім елементом, який ви тестували
Процес переранкінгу є останнім етапом, і він працює найкраще, якщо розглядати його як вимірювану характеристику. Збережіть один ідеальний приклад роботи, один випадок невдачі та запис про скасування змін перед розширенням обсягу роботи. Записуйте час виконання та витрати на токени чи запити поруч із функціональними результатами. Чітке бачення витрат заздалегідь запобігає несподіваним рахункам, коли процес переходить від демо-середовища до спільних середовищ. Розділяйте політику часткової обробки даних та політику їх пошуку. Зміна однієї з них не повинна змушувати переписувати іншу, коли змінюються показники якості.
def cheap_local_rerank(query, candidates, limit=5):
"""
Good enough for this experiment.
I'd use a learned reranker for a real deployment.
"""
candidate_text = [
chunk["text"]
for chunk, _ in candidates
]
tfidf = TfidfVectorizer(
analyzer="char_wb",
ngram_range=(3, 5),
min_df=1,
)
matrix = tfidf.fit_transform(
[query, *candidate_text]
)
relevance = cosine_similarity(
matrix[0],
matrix[1:],
)[0]
reranked = sorted(
zip(candidates, relevance),
key=lambda row: row[1],
reverse=True,
)
return [
(chunk, float(score))
for ((chunk, _), score) in reranked[:limit]
]
Кінцеві цифри виявилися менш цікавими, ніж ви очікували
Остаточні показники найкраще розглядати як вимірювану поверхню на етапі розробки. Збережіть один ідеальний зразок виконання, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища конфіденційних даних та флаги функцій мають знаходитися в одному місці, щоб оператори могли їх перевіряти, не читаючи весь код. Розділяйте політику часткової обробки даних та політику їх отримання. Зміна однієї з них не повинна змушувати переписувати іншу при зміні показників якості. Остаточні показники найкраще розглядати як вимірювану поверхню на етапі розробки. Збережіть один ідеальний зразок виконання, один випадок збою та примітку про скасування змін перед розширенням обсягу роботи. Віддавайте перевагу невеликим, тестованим одиницям коду перед величезними скриптами. Коли якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на складну послідовність операцій.
Повернутися до запиту CSV
На етапі „Повернення до CSV“ необхідно визначити вхідні дані, відповідальну особу за крок та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан. Розглядайте цей етап як угоду між вхідними даними та перевіреними результатами. Призначте назви документів, визначте критерії успіху та не допускайте беззвучного часткового завершення. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без цитат оператори не зможуть відрізнити галюцинацію від проблем із індексуванням.
How long is a CSV export link usable?
1. CSV exports run asynchronously...
2. Large exports are split...
3. Users can cancel an export...
4. A completed CSV download link remains active for seven days.
1. A completed CSV download link remains active for seven days.
2. Users can cancel an export while it is still queued...
3. CSV exports run asynchronously...
Порядок виправлення помилок тепер набагато простіший
Для належного порядку відладки спочатку необхідно визначити вхідні дані, виконавця кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не намагаючись вгадати прихований стан системи. Необхідно фіксувати час виконання та витрати на обробку даних поруч із функціональними результатами. Чітке відображення витрат заздалегідь запобігає несподіваним рахункам під час переходу з демо-середовища у спільні середовища. Слід посилатися на конкретні уривки тексту, які лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинації від проблем з індексуванням.
Остаточні міркування та висновки
На етапі заключних міркувань та висновків необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан. Конфігурацію слід тримати окремо від коду додатку. Файли середовища, сховища секретних даних та флаги функцій мають знаходитися в одному місці, яке оператори можуть перевірити, не читаючи весь код. Наводьте уривки тексту, які фактично лежать в основі відповіді. Без посилань оператори не зможуть відрізнити галюцинацію від проблем із індексуванням. На етапі заключних міркувань та висновків необхідно визначити вхідні дані, власника кроку та критерії завершення перед зміною коду. Оператори повинні мати можливість перезапустити крок з відомої точки контролю, не здогадуючись про прихований стан. Віддавайте перевагу невеликим, тестованим одиницям перед об’ємними скриптами. Коли крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на...
заплутана система обробки даних.Чек-лист для експлуатації
Під час роботи над етапом чек-листу для експлуатації спочатку запишіть умови контракту: необхідні вхідні дані, сигнал про успіх та те, що відбувається при частковій невдачі. Цей чек-лист допомагає зберігати чесність пізніших змін у коді.
Одночасно задокументуйте шлях успішної роботи та шлях відновлення. Повторні спроби, людський контроль та обробка некоректних повідомлень є частиною продукту, а не етапом подальшої оптимізації.
Перед налаштуванням запитів вимірюйте ефективність пошуку за фіксованим набором запитань. Зміна запитів рідко допомагає вирішити проблеми слабкої системи пошуку.
Фіксуйте версії залежностей та записуйте ідентифікатор зображення, яке використовувалося під час демонстрації. Відтворюваність краща за індивідуальні знання команди.
Віддавайте перевагу невеликим, тестованим одиницям коду перед складними скриптами. Якщо якийсь крок зазнає невдачі, причина має вказувати на конкретну відповідальність, а не на заплутану систему обробки даних.
Вимірюйте рівень відтворення інформації на фіксованому наборі запитань перед налаштуванням підказок. Часта зміна підказок рідко вирішує проблеми слабкого пошуку даних.
Перед тим, як підвищувати рівень функціональності системи, заморозьте поточні версії, збережіть ідеальний запис для критичного шляху виконання та переконайтеся у наявності кроків для відкату. У спільних середовищах необхідні обмеження на частоту використання, перевірки прав доступу та чіткий власник для зміни секретних даних. Віддавайте перевагу надійності перед креативними одноразовими демонстраціями.
Примітка для 4527c294eba8: не включайте ключі постачальника до репозиторію, встановіть ліміт токенів на сеанс та зберігайте записи поруч із елементами для оцінки, щоб подальша заміна моделей залишалася порівнянною.
Під час роботи над етапом 0 документації з посилення безпеки спочатку запишіть умови виконання: необхідні вхідні дані, сигнал про успіх та те, що відбувається у разі часткової невдачі. Такий перелік допомагає зберігати чесність пізніших змін у коді. Тримайте конфігурацію окремо від коду додатку. Файли середовища, сховища секретних даних та флаги функцій мають знаходитися в одному місці, де оператори можуть їх перевіряти, не читаючи весь код.
Деталь посилення безпеки 0/820: вимірюйте час виконання, клас помилки та кількість витрачених токенів для цього пункту, а потім вирішуйте, чи залишити зміни, ґрунтуючись на фіксованому наборі критеріїв, а не на індивідуальних спостереженнях.