Как стать автором
Обновить
10.73

Self-RAG: LLM сама выбирает, когда ей нужен контекст

Уровень сложностиПростой
Время на прочтение7 мин
Количество просмотров3.8K

Привет, чемпионы! Сегодня как никогда актуальны методы улучшения LLM-ассистентов, особенно для бизнес-интеграции. Поделюсь опытом внедрения технологии Self-RAG, её плюсами, минусами и реализацией кастомного модуля на локальной инфраструктуре.

Как мы пришли к этому

Мы разворачивали у себя LLM с подходом RAG для ответов на вопросы по документации и столкнулись с проблемами:

  1. Нужно было снизить количество галлюцинаций при обработке внутренних и внешних данных

  2. При работе иногда появляются вопросы у юзеров приложения, которые в целом не связаны с внешними ресурсами, но мы лезем туда все-равно - возникали лишние траты

  3. Экономия ресурсов: первоначально казалось, что Self-RAG потребует больше затрат, хоть это и даст выше качество, но на практике при ванильном RAG чисто перезапросов было такое, что перекрывало цену инференса по нагрузке.

Решая эти проблемы, мы узнали о Self-RAG и решили попробовать!

Что такое Self-RAG

Изначально предложенная архитектура выглядела так:

  • Сам вопрос к LLM идет сначала, через модуль маршрутизатора, где маршрутизатор определяет нужен-ли контекст модели или нет.

  • Далее все это заходит в модуль self-rag, где для каждого из документов формируется ответ модели.

  • После чего все это идет в модель критик, которая дает свою оценку насколько релевантно то или иное решение по выставлению оценки метками.

После чего выбираются лучшие документы, на основе которых вы получаете ваш ответ.

Данная система выглядит интересно, однако нам нужно было сделать более кастомный модуль self-rag после чего, поизучав мы нашли решение, которое можно было легко отлаживать кастомизировать и использовать локально, поэтому мы взяли реализацию на графах модуля от langchain, которую они увидели так:

В данной системе алгоритм у модуля следующий:

Как только мы понимаем, что нам необходим RAG мы идем в модуль, где заданный вопрос, который инициирует запуск извлечения документов:

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import Chroma
from langchain_ollama import OllamaEmbeddings

urls = [
    "https://docs.pytorch.org/docs/stable/cuda.html",
    "https://docs.pytorch.org/docs/stable/torch_cuda_memory.html",
    "https://docs.pytorch.org/docs/stable/distributed.html",
]

docs = [WebBaseLoader(url).load() for url in urls]
docs_list = [item for sublist in docs for item in sublist]

text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder(
    chunk_size=250, chunk_overlap=0
)
doc_splits = text_splitter.split_documents(docs_list)

embeddings = OllamaEmbeddings(model="nomic-embed-text")

vectorstore = Chroma.from_documents(
    documents=doc_splits,
    collection_name="rag-chroma",
    embedding=embeddings,
)


retriever = vectorstore.as_retriever()

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

from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama 
from pydantic import BaseModel, Field
from retriver import retriever


class GradeDocuments(BaseModel):
    """Binary score for relevance check on retrieved documents."""

    binary_score: str = Field(
        description="Documents are relevant to the question, 'yes' or 'no'"
    )

llm = ChatOllama(model="mistral:instruct", options={"temperature": 0})
structured_llm_grader = llm.with_structured_output(GradeDocuments)

system = """You are a grader assessing relevance of a retrieved document to a user question. \n 
    It does not need to be a stringent test. The goal is to filter out erroneous retrievals. \n
    If the document contains keyword(s) or semantic meaning related to the user question, grade it as relevant. \n
    Give a binary score 'yes' or 'no' score to indicate whether the document is relevant to the question."""
grade_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("human", "Retrieved document: \n\n {document} \n\n User question: {question}"),
    ]
)

retrieval_grader = grade_prompt | structured_llm_grader

question = "agent memory"
docs = retriever.invoke(question)

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

from langchain import hub
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import ChatOllama
from retrivial_greader import docs


prompt = hub.pull("rlm/rag-prompt")

llm = ChatOllama(model="qwen3:1.7b", options={"temperature": 0})

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

rag_chain = prompt | llm | StrOutputParser()
question = "agent memory"

generation = rag_chain.invoke({"context": docs, "question": question})

И как раз на случай того, что модели стало все-равно на нашу нулевую меру энтропии, мы делаем модуль для оценки ответов на галлюцинации:

from pydantic import BaseModel, Field
from langchain_ollama import ChatOllama
from langchain_core.prompts import ChatPromptTemplate
from generator import generation
from retrivial_greader import docs

class GradeHallucinations(BaseModel):
    """Binary score for hallucination present in generation answer."""

    binary_score: str = Field(
        description="Answer is grounded in the facts, 'yes' or 'no'"
    )


llm = ChatOllama(model="qwen3:1.7B", options={"temperature": 0})
structured_llm_grader = llm.with_structured_output(GradeHallucinations)

system = """You are a strict grader. 
You must answer ONLY with 'yes' or 'no' and NOTHING else.

Rule:
- Answer 'yes' if the generation is grounded in or supported by the documents.
- Answer 'no' if the generation contains hallucinations or is not grounded.

Do not explain. Do not output any extra text.
"""


hallucination_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", system),
        ("human", "Set of facts: \n\n {documents} \n\n LLM generation: {generation}. Is the generation grounded in the documents? Answer only with 'yes' or 'no'."),
    ]
)



question = "agent memory"

hallucination_grader = hallucination_prompt | structured_llm_grader
score = hallucination_grader.invoke({"documents": docs, "generation": generation})

После чего мы оцениваем итоговый ответ, а именно насколько он подходит как ответ на вопрос:

from pydantic import BaseModel, Field
from langchain_core.prompts import ChatPromptTemplate
from langchain_ollama import ChatOllama
from retriver import retriever
from generator import rag_chain


class GradeAnswer(BaseModel):
    """Binary score to assess answer addresses question."""
    binary_score: str = Field(
        description="Answer addresses the question, 'yes' or 'no'"
    )

llm = ChatOllama(model="qwen3:1.7b", options={"temperature": 0})
structured_llm_grader = llm.with_structured_output(GradeAnswer)

system = """You are a grader assessing whether an answer addresses / resolves a question.
Respond only with a JSON object containing the field 'binary_score', either "yes" or "no".
Do not include any explanations or additional text."""
answer_prompt = ChatPromptTemplate.from_messages([
    ("system", system),
    ("human", "User question: \n\n {question} \n\n LLM generation: {generation}"),
])


answer_grader = answer_prompt | structured_llm_grader

question = "agent memory"
docs = retriever.invoke(question)
generation = rag_chain.invoke({"context": docs, "question": question})

result = answer_grader.invoke({"question": question, "generation": generation})

В результате, мы объединяем все эти узлы по логике, что описана выше в граф, который принимает решение на ответы на наши вопросы.

Модельки в данном случае взяты для вашего простого развертывания данного прототипа, промты также подстраивались под достижения нужных ответов.

Гладко было на бумаге...

Будь уверен, но в меру

Первое это конечно модуль того, как мы заставляем модель решать нужен ли ей контекст или нет (спойлер-просто дать в промт не поможет). Если статья наберет неплохую обратную связь, я расскажу, как мы реализовали решение, которую объединяли с таким модулем для определения RETRIEVE активации.

В случае обычной работы же возникали следующие проблемы - это:

  • лишняя самоуверенность модели

  • лишняя неуверенность

Логично, правда? Как раз в этом случае возникала либо проблема, в которой мы невзначай превращали self-RAG в ванильный раз посредством того, что даже для вопроса

Сколько будет 2+2?

Модель лезла в контекст и запускала все это ради такого вопроса

Либо при лишней уверенности она при просьбе описать утилиту из нашей внутренней документации решала, что она знает, что это итак и просто не инициировала вызов посредством флага RETRIEVE в ответе.

Распределите ресурсы разумно

При построении self-RAG важно учитывать нагрузку на железо (особенно если вы работаете на локальной машине или сервере компании). Вот несколько практических рекомендаций:

  1. Объединяйте логически близкие задачи в один узел.
    Например, решение задачи «нужен ли доступ к внешним данным?» и «что ответить пользователю?» может обрабатываться одной и той же LLM. Это снижает количество вызовов модели и экономит ресурсы. При грамотной обработки вызовов конечно же.

  2. Разделяйте ресурсоёмкие этапы и lightweight-оценки.
    Используйте компактные модели (например, Mistral, Qwen-1.5-1.8B) для узлов оценки релевантности, галлюцинаций или «ответа по теме». Эти задачи часто можно делегировать более лёгким LLM.

  3. Тестируйте узлы оценки.
    Узлы типа «оценка релевантности» или «есть ли галлюцинация» можно заменять на разные версии моделей или даже эвристики — и тестировать, как это влияет на итоговое качество. Это простой, но мощный способ адаптировать систему под ваши данные. Ведь ваша документация или источники могут не подойти под ту или иную.

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

Код выше использовался для прототипирования, так что использованные модели и там можно подкорректировать.

А в чем же выигрыш?

В данном случае мы получили отсев запросов, что требовали внешних источников данных, что убрало часть юзеров, что просто задают вопросы из тренировочного корпуса модели.

А в случае использования такой системы мы ожидали больше нагрузки в связи с увеличением вычислений в узлах, однако при запуске выяснилось, что нагрузка на метриках уменьшилась на 6%, что было вызвано тем, что пользователи сразу получали то, что им нужно, если получали ответ из "базы знаний". Приятный бонус так сказать.

Про api и зависимости

Если же мы использовали бы платный api каких-то моделей, то в нашем случае мы получили бы повышение качества ответов, при этом совмещая это с потенциально снижением стоимости (в зависимости от уровня ваших пользователей). При этом никто не запрещает менять узлы на более маленькие модели, что потянет ваша внутренняя инфраструктура.

Итоги

Как итог можно сказать, что данная технология очень помогла нам, надеюсь, что поможет и вам. Даже если использовать и внедрять вы это не будете, то хотя бы узнали, как это работает и какие методы для этого используются!

Как я сказал выше, то если мы наберем большой фидбек, то я расскажу про то, как мы реализовали модуль для самооценки и соединили это все в крутого франкенштейна!

🔥 Ставьте лайк и пишите, какие темы разобрать дальше! Главное — пробуйте и экспериментируйте!


✔️ Присоединяйтесь к нашему Telegram-сообществу @datafeeling, где мы делимся новыми инструментами, кейсами, инсайтами и рассказываем, как всё это применимо к реальным задачам

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
+4
Комментарии1

Публикации

Информация

Сайт
t.me
Дата регистрации
Дата основания
Численность
2–10 человек
Местоположение
Россия