Главная / Статьи

Агентный RAG: предоставление ЯИИ возможности самостоятельно решать, когда и где запрашивать информацию.

Почему статический RAG не справляется с вопросами, требующими нескольких шагов обработки или оперативного ответа, и как агент LangGraph осуществляет маршрутизацию между Qdrant и веб-поиском с использованием ограничений и предостережений.

Издатель: REACTAPP.TOP9 мин чтения2167 слов

Источники: Your RAG Pipeline Is Lying to You. The Problem Is Not the Embeddings — Agentic RAG

«RAG — это в основном уже решённая проблема». Эту фразу часто говорят на собраниях по анализу работы команд, и честный ответ заключается в том, что всё зависит от того, что вы подразумеваете под поиском информации. Любой, кто уже давно использует системы RAG в производстве для обработки кратких сводок медицинских записей, рабочих процессов преобразования естественного языка в SQL или многопроцессных исследовательских систем, обычно сталкивается с одними и теми же ограничениями.

Проблема не в эмбеддингах. Она заключается в предположении, что поиск информации происходит один раз, на определённом этапе, в одном источнике.

Статический RAG (встраивание документов, извлечение топ-k результатов и их передача в LLM) хорошо справляется с простыми, самодостаточными вопросами, основанными на стабильных внутренних знаниях. Однако когда вопрос становится многоэтапным, временно чувствительным или требует как внутренних документов, так и актуальных данных, статическая схема начинает генерировать ошибочные ответы. Такая схема может порекомендовать клиническую процедуру на основе устаревших внутренних рекомендаций, хотя быстрая поисковая операция в интернете за полминуты позволила бы узнать о внесенных изменениях.

Лучшие эмбеддинги не решат эту проблему. Помогает то, что LLM может сам решать, когда и откуда брать информацию.

Проблема подхода «извлечь один раз, отвечать всегда»

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

Задайте вопрос «Каковы побочные эффекты метформина?», и система сразу даст ответ. Теперь спросите что-то более сложное: какие взаимодействия с лекарствами недавно были отмечены FDA для метформина на фоне новых препаратов класса GLP-1, и охватываются ли они текущим протоколом лечения пациента? Для этого требуются внутренние документы и актуальные регуляторные данные, а также анализ различий между ними. Статический RAG берет данные один раз из одного источника, а большие языковые модели лишь кратко обрабатывают полученную информацию.

Это не гипотетический сценарий. Во внутренней оценке медицинской системы [1] статический RAG показал 89% соответствия ответов по стабильным вопросам, но его показатель снизился до 61%, когда требовалось сопоставление внутренних клинических документов с внешними рекомендациями по лечению. Простые оценки не выявили этой проблемы, потому что ответы казались правильными; на самом деле они были устаревшими.

Формулировка решения проста, но его реализация сложна: поиск информации превращается в решение, а не в отдельный шаг.

Что означает агентный RAG

В агентном RAG большая языковая модель не выполняет поиск за вас в заранее определенный момент. Она рассматривает поиск как инструмент, подобный калькулятору или исполнителю кода, и применяет его тогда, когда считает доказательства недостаточными. Это приводит к трём формам поведения, которые невозможно получить с помощью статической системы.

  • Условный поиск. Если контекст уже содержит ответ на вопрос, агент полностью пропускает поиск.
  • Многократный поиск. Агент получает информацию, читает её, замечает отсутствующую часть и возвращается за дополнительными данными; каждый этап определяется результатами предыдущего. Именно так люди проводят исследования.
  • Маршрутизация источников. Агент выбирает между хранилищем векторов, поиском в Интернете или структурированной базой данных в зависимости от вопроса. Вопрос «Что гласит наша политика?» направляется в хранилище векторов, тогда как вопрос «Что объявило FDA на прошлой неделе?» — в Интернет.
  • Сама идея не является чем-то новым. В статье ReAct, опубликованной Яо и его коллегами в 2022 году, была описана схема чередования шагов рассуждения с вызовами инструментов, причем агентный RAG представляет собой применение подхода ReAct к процессу поиска информации [2].

    Недавнее изменение касается инструментов. LangGraph наконец позволяет использовать циклы с ветвлением, имеющие состояние. Tavily предлагает API для поиска в Интернете, разработанное специально для агентов, который возвращает контент, готовый к обработке большими языковыми моделями; по данным компании, он обрабатывает более 100 миллионов запросов в месяц при задержке поиска p50 в 180 мс [3]. Подробности представлены в посте Tavily 101.

    Три ключевых компонента

    Подумайте о детективе, а не о библиотекаре. Библиотекарь доставляет информацию по команде. Детектив читает имеющиеся материалы, определяет, чего не хватает, идет за этим, снова читает и продолжает до тех пор, пока не сможет обосновать свой вывод. Этот цикл состоит из трех компонентов.

    1. Роутер определяет, какого типа поиск требуется для ответа на запрос. Языковая модель рассматривает вопрос и решает, достаточен ли существующий контекст, нужны ли документы, нужен ли интернет, или все трое вещи одновременно.
    2. Инструменты поиска являются источниками доказательств. Минимумом считаются два инструмента: один для поиска в векторном хранилище внутренних документов, другой — для поиска актуальной информации в интернете. Каждый из них представляет собой обычную функцию, которую языковая модель вызывает с строкой запроса.
  • Проверка обоснованности заключается в определении того, достаточно ли имеющихся доказательств. Перед составлением окончательного ответа агент оценивает, является ли полученная информация достаточной и релевантной; если нет, он запрашивает её снова. Статические пайплайны не могут реализовать такой цикл.
  • Рабочая реализация

    Вот гибридный агент в LangGraph, который переключается между векторным хранилищем Qdrant и веб-поиском Tavily. Он сам выбирает инструмент, может вызывать их поочередно и запрашивать информацию снова, если первый результат недостаточен для обоснования ответа. Часть, связанная с Qdrant, реализована в соответствии с руководством по интеграции LangChain.

    from typing import Annotated, Literal
    from langgraph.graph import StateGraph, START, END
    from langgraph.prebuilt import ToolNode
    from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
    from langchain_core.tools import tool
    from langchain_openai import ChatOpenAI
    from langchain_qdrant import QdrantVectorStore
    from langchain_openai import OpenAIEmbeddings
    from tavily import TavilyClient
    from qdrant_client import QdrantClient
    import operator
    from typing import TypedDict, Sequence
    
    # --- State definition ---
    class AgentState(TypedDict):
        messages: Annotated[Sequence, operator.add]
        retrieval_count: int  # guard against infinite loops
    
    # --- Tool 1: Vector store retrieval (internal docs) ---
    qdrant_client = QdrantClient(url="http://localhost:6333")
    embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
    vector_store = QdrantVectorStore(
        client=qdrant_client,
        collection_name="internal_docs",
        embedding=embeddings,
    )
    @tool
    def retrieve_from_docs(query: str) -> str:
        """
        Search internal document knowledge base.
        Use for questions about internal policies, protocols, or proprietary knowledge.
        """
        results = vector_store.similarity_search(query, k=4)
        if not results:
            return "No relevant internal documents found for this query."
        return "\n\n---\n\n".join([
            f"[Source: {doc.metadata.get('source', 'internal')}]\n{doc.page_content}"
            for doc in results
        ])
    
    # --- Tool 2: Web retrieval (live data) ---
    tavily = TavilyClient(api_key="YOUR_TAVILY_API_KEY")
    @tool
    def retrieve_from_web(query: str) -> str:
        """
        Search the live web for current information.
        Use for recent news, regulatory updates, or anything that may have changed
        since the internal knowledge base was last updated.
        """
        results = tavily.search(
            query=query,
            max_results=5,
            search_depth="advanced",  # deep retrieval for better accuracy
        )
        if not results.get("results"):
            return "No relevant web results found."
        return "\n\n---\n\n".join([
            f"[Source: {r['url']}]\n{r['content']}"
            for r in results["results"]
        ])
    
    # --- Agent node ---
    tools = [retrieve_from_docs, retrieve_from_web]
    tool_node = ToolNode(tools)
    llm = ChatOpenAI(model="gpt-4o", temperature=0).bind_tools(tools)
    SYSTEM_PROMPT = """You are a research assistant with access to two retrieval tools:
    - retrieve_from_docs: internal document knowledge base (policies, protocols, proprietary data)
    - retrieve_from_web: live web search (recent news, regulatory updates, real-time information)
    For any question:
    1. Decide whether you need to retrieve information or can answer from context.
    2. If retrieval is needed, choose the right source - or call both if the question spans internal and live knowledge.
    3. After retrieving, check if the evidence is sufficient. If not, retrieve again with a refined query.
    4. Always cite your sources in the final answer.
    Do not hallucinate. If you cannot find sufficient evidence, say so explicitly."""
    def call_agent(state: AgentState) -> AgentState:
        messages = [SystemMessage(content=SYSTEM_PROMPT)] + list(state["messages"])
        response = llm.invoke(messages)
        return {
            "messages": [response],
            "retrieval_count": state.get("retrieval_count", 0)
        }
    def update_retrieval_count(state: AgentState) -> AgentState:
        return {
            "messages": [],
            "retrieval_count": state.get("retrieval_count", 0) + 1
        }
    
    # --- Routing logic ---
    MAX_RETRIEVALS = 3  # prevent infinite loops
    def should_continue(state: AgentState) -> Literal["tools", "end"]:
        last_message = state["messages"][-1]
        # If the LLM called a tool and we haven't hit the loop guard, continue
        if hasattr(last_message, "tool_calls") and last_message.tool_calls:
            if state.get("retrieval_count", 0) < MAX_RETRIEVALS:
                return "tools"
        return "end"
    
    # --- Build the graph ---
    graph_builder = StateGraph(AgentState)
    graph_builder.add_node("agent", call_agent)
    graph_builder.add_node("tools", tool_node)
    graph_builder.add_node("count_retrieval", update_retrieval_count)
    graph_builder.add_edge(START, "agent")
    graph_builder.add_conditional_edges(
        "agent",
        should_continue,
        {"tools": "tools", "end": END}
    )
    graph_builder.add_edge("tools", "count_retrieval")
    graph_builder.add_edge("count_retrieval", "agent")
    agent = graph_builder.compile()
    
    # --- Run it ---
    if __name__ == "__main__":
        result = agent.invoke({
            "messages": [
                HumanMessage(content=(
                    "What does our current patient protocol say about GLP-1 usage, "
                    "and are there any FDA safety updates issued in 2025 we should be aware of?"
                ))
            ],
            "retrieval_count": 0,
        })
        print(result["messages"][-1].content)
    

    Некоторые детали заслуживают внимания.

    Защитный механизм MAX_RETRIEVALS в логике маршрутизации — это не просто декоративный элемент. Без него агенты могут выполнять до 15 запросов к источникам информации по одному неоднозначному вопросу, тратя токены и терпение. Обычно трех запросов достаточно. Если этого недостаточно, значит, вопрос слишком общий или инструменты возвращают бесполезные данные.

    Поле retrieval_count в состоянии позволяет прервать цикл. Структура StateGraph от LangGraph передаёт его при каждом переходе между узлами, поэтому оно сохраняется на протяжении всего диалога, а не только одного цикла.

    Системный промпт также играет важную роль. Указание модели, когда использовать каждый инструмент, а не просто информация о существовании инструментов, позволяет отличить агента, который разумно маршрутизирует действия, от того, кто из лени прибегает к функции retrieve_from_web для всего.

    Когда агентный подход RAG усугубляет ситуацию

    Он не заменяет статический RAG. Он используется вместо него для определённых типов задач, но приводит к негативным результатам в четырёх случаях.

    • Основным ограничением является задержка. Каждый проход по циклу требует дополнительного вызова модели плюс вызов инструмента. Для простого ассистента для вопросов и ответов, у которого задержка p95 должна быть менее секунды, агент, способный выполнять два-три цикла, не подходит. Хорошо настроенный статический поисковик работает быстрее и достаточен.
    • База знаний должна быть стабильной и самодостаточной. Если на все вопросы пользователей можно уверенно ответить с помощью векторного хранилища, добавление сложности бесполезно. Сначала измерьте эффективность статической системы с использованием RAGAS (статья). Если точность воспроизведения контекста превышает 85%, а точность ответов — 90%, то использование агентного обёртки является избыточным.
    • Нельзя доверять выбору инструментов модели. Благодаря тщательно составленному системному промпту GPT-4o и Claude Sonnet примерно в 88% случаев правильно определяют, следует ли запрашивать информацию из документов или из Интернета, согласно внутренним измерениям. В оставшихся 12% случаях возникают ошибки источника, которые сложнее отладить, чем обычные галлюцинации, поскольку ответ имеет основания, но находится в неверном месте. При использовании менее совершенных моделей необходимо заранее настроить выбор инструментов.
    • Данным в реальном времени нельзя доверять. Получение информации из Интернета создаёт дополнительную поверхность для атак. Tavily фильтрует попытки внедрения промптов в свой API, но ни один фильтр не является идеальным, поэтому любой агент, использующий контент из Интернета, должен рассматривать его как потенциально враждебный.

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

    С чего начать

    Самый маленький шаг — менее пяти минут: добавьте один инструмент Tavily в существующего агента LangChain с помощью from langchain_community.tools.tavily_search import TavilySearchResults. LangGraph пока не нужен. Обеспечьте доступ к инструменту, напишите системное указание о том, когда его вызывать, выполните пять тестовых запросов и посмотрите, что будет выбрано.

    Следующий уровень — примерно один день: настройте приведённый выше пример LangGraph на ваш собственный хранилище векторов, оставив оба инструмента. Сначала исключите счётчик поиска, чтобы увидеть, как часто агент будет повторять действия, затем добавьте его. Разница поможет многому научиться.

    Продвинутый режим: вставка этапа оценки после результата каждого инструмента и перед следующим ходом агента. Более мелкая и дешевая вызов LLM отвечает «да» или «нет» на вопрос о том, достаточен ли полученный контекст, и только затем основная модель формирует свой ответ. Этот шаг повышает уровень надежности воспроизведения контекста с «достаточно хорошего в тестах» до «надежного в производстве».

    Ошибки маршрутизации инструментов, составляющие 12% от общего числа ошибок, занимают большую часть времени на отладку в производственных условиях. Необходимо рано внедрять средства для их отслеживания, чтобы узел оценки обнаруживал их раньше, чем это сделают пользователи.

    Источники

    [1] Внутренние тесты на платформе RAG в сфере здравоохранения с использованием RAGAS 0.1.x на наборе из 200 клинических запросов. Это не опубликованное исследование.

    [2] Yao и др. (2022), ReAct: Synergizing Reasoning and Acting in Language Models, arXiv:2210.03629.

    [3] Блог Tavily, Tavily 101, с показателями задержки и объема данных, указанными на странице продукта в мае 2026 года.

    Связанные статьи

  • grep Beat Vector Search in Agent Memory Tests, and the Harness Mattered as Much — Исследование 116 вопросов LongMemEval показало, что grep превосходит векторный поиск, но способ предоставления результатов так же сильно влияет на точность, как и сам механизм поиска. Что можно извлечь из этого.
  • Expert RAG: HyDE, Compression, Self-Querying and Agentic Workflows — Четыре передовых метода поиска для промышленного использования в RAG в Haystack: HyDE, контекстуальное сжатие, самопоиск и агентные рабочие процессы, а также когда использовать каждый из них.
  • Шаг за шагом: проектирование реальной системы поиска данных RAG для ИИ-агентов — Планирование системы RAG с использованием контрактов SourceDocument и DocumentChunk, разбиения данных на части, генерации векторных представлений и использования ChromaDB, всё это через RetrievalService, обеспечивающий стабильность работы агента.
  • Проект RAG — это не один огромный файл на Python: структура папок для масштабирования — Разделение демонстрационного приложения RAG на компоненты: ввод данных, разбиение на части, генерация векторных представлений, хранилище векторов, поиск, формулировка запросов, клиент LLM, API, тесты и логи, что позволяет проекту развиваться за пределы начальной стадии.
  • Практические заметки: почему вашему RAG-пайплайну нужен ри-рэнкер (и как это сделать) — пошаговое руководство к Практическим заметкам: почему вашему RAG-пайплайну нужен ри-рэнкер (и как это сделать), включающее контракты, проверки и готовые блоки кода для команд, использующих эту схему.