Агентный RAG: предоставление ЯИИ возможности самостоятельно решать, когда и где запрашивать информацию.
Почему статический RAG не справляется с вопросами, требующими нескольких шагов обработки или оперативного ответа, и как агент LangGraph осуществляет маршрутизацию между Qdrant и веб-поиском с использованием ограничений и предостережений.
Источники: 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 большая языковая модель не выполняет поиск за вас в заранее определенный момент. Она рассматривает поиск как инструмент, подобный калькулятору или исполнителю кода, и применяет его тогда, когда считает доказательства недостаточными. Это приводит к трём формам поведения, которые невозможно получить с помощью статической системы.
- Условный поиск. Если контекст уже содержит ответ на вопрос, агент полностью пропускает поиск.
- Многократный поиск. Агент получает информацию, читает её, замечает отсутствующую часть и возвращается за дополнительными данными; каждый этап определяется результатами предыдущего. Именно так люди проводят исследования.
Сама идея не является чем-то новым. В статье ReAct, опубликованной Яо и его коллегами в 2022 году, была описана схема чередования шагов рассуждения с вызовами инструментов, причем агентный RAG представляет собой применение подхода ReAct к процессу поиска информации [2].
Недавнее изменение касается инструментов. LangGraph наконец позволяет использовать циклы с ветвлением, имеющие состояние. Tavily предлагает API для поиска в Интернете, разработанное специально для агентов, который возвращает контент, готовый к обработке большими языковыми моделями; по данным компании, он обрабатывает более 100 миллионов запросов в месяц при задержке поиска p50 в 180 мс [3]. Подробности представлены в посте Tavily 101.
Три ключевых компонента
Подумайте о детективе, а не о библиотекаре. Библиотекарь доставляет информацию по команде. Детектив читает имеющиеся материалы, определяет, чего не хватает, идет за этим, снова читает и продолжает до тех пор, пока не сможет обосновать свой вывод. Этот цикл состоит из трех компонентов.
- Роутер определяет, какого типа поиск требуется для ответа на запрос. Языковая модель рассматривает вопрос и решает, достаточен ли существующий контекст, нужны ли документы, нужен ли интернет, или все трое вещи одновременно.
- Инструменты поиска являются источниками доказательств. Минимумом считаются два инструмента: один для поиска в векторном хранилище внутренних документов, другой — для поиска актуальной информации в интернете. Каждый из них представляет собой обычную функцию, которую языковая модель вызывает с строкой запроса.
Рабочая реализация
Вот гибридный агент в 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 года.
Связанные статьи
- Агентный поиск и достаточный контекст: когда RAG недостаточно осведомлен — Стандартный RAG часто отвечает слишком рано. Узнайте, как планировать поиски, направлять источники, оценивать доказательства и повторно выполнять поиск до тех пор, пока контекст не станет достаточным, на примере Acme.
- Chroma Context-1: 20B-модель-подагент для более дешевого и эффективного агентного RAG — Как Chroma Context-1 действует в качестве подагента, предназначенного исключительно для поиска, который сокращает собственный контекст, как он обучался, что не хватает без его использования и в чем его недостатки.