Главная / Статьи / Практические советы: как исправить систему RAG, которая постоянно извлекает неверный контекст

Практические советы: как исправить систему RAG, которая постоянно извлекает неверный контекст

Пошаговое руководство по практическим советам: как исправить систему RAG, которая постоянно извлекает неверный контекст: контракты, проверки и готовые блоки кода для команд, использующих эту схему.

2332 слов

Используйте это как переработанную версию идей из статьи «Как починить систему 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)),
    }

Затем вы винили разбиение на части

При работе над этапом разбиения на части запишите сначала спецификацию: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список поможет избежать ошибок при последующих изменениях кода. Запишите также время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отслеживание затрат на раннем этапе предотвращает неожиданные расходы при переходе с демо-среды в общедоступные среды. Измерьте точность воспроизведения ответов на фиксированном наборе вопросов перед настройкой подсказок. Частая смена подсказок редко помогает улучшить качество поиска.

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: измерьте время выполнения, класс ошибки и количество использованных токенов для данной записки, затем решите, следует ли сохранять изменение, опираясь на установленный набор критериев, а не на личные наблюдения.