Агентный RAG в LangGraph: оценщики, условное маршрутизирование и резервный вариант с поиском в интернете
Перенесите линейную пайплайн LangChain RAG в StateGraph LangGraph с инструментами оценки LLM, условными входами и ребрами, тестами, а также концом FastAPI для отчета о происхождении данных.
Источники: Agentic AI with LangChain — Part 2: Orchestrating Workflow by LangGraph
Семантический пайплайн RAG, созданный на основе ключевых компонентов LangChain — бэкенда FastAPI и фронтенда-чат-бота Streamlit, обрабатывает загрузку PDF-файлов, преобразует документы в эмбеддинги и отвечает на вопросы через интерфейс чата. У него также есть определенные ограничения: примитивы LangChain сами по себе обеспечивают линейные цепочки обработки, то есть прямую последовательность от запроса через нейросеть до парсера результата. Любые сценарии, требующие условных ветвлений, циклов, динамического маршрутизирования или принятия решений нейросетью, выходят за рамки этой структуры, а простые операторы if/else обеспечивают лишь поверхностный, заранее определенный алгоритм работы.
LangGraph решает эту проблему. Результатом является агентный рабочий процесс с следующими характеристиками:
- условные ветвления и циклы, встроенные непосредственно в пайплайн
- нейросеть, выбирающая между поиском в интернете и извлечением информации с помощью RAG без жестко заданных правил
- переходы между этапами, управляемые с помощью узлов и ребер LangGraph
В итоге у вас получается система RAG с агентным подходом, способная отвечать как из векторного хранилища, так и из Интернета.
Термины LangGraph
Весь словарь терминов сводится к четырем основным концепциям. StateGraph координирует выполнение операций. Состояние — это типизированный словарь, в котором хранятся и обновляются данные пайплайна. Узлы представляют собой функции на Python, изменяющие состояние и выполняющие вычисления в рамках графа. Ребра обозначают переходы между узлами, а условные ребра позволяют создавать ветвления и циклы.
Конфигурация
Положите ключи API в файл .env, находящийся в корне проекта:
OPENAI_API_KEY="<Your OpenAI API Key>"
VECTOR_DB=pinecone
Hugging_Face_TOKEN=<your huggingface token>
PINECONE_API_KEY=<your Pinecone API Key>
PINECONE_ENVIRONMENT=gcp-starter
LANGSMITH_TRACING=true
LANGSMITH_ENDPOINT="https://api.smith.langchain.com"
LANGSMITH_API_KEY="<Your LangSmith API Key"
LANGSMITH_PROJECT="agent-ai"
TAVILY_API_KEY=<Your TAVILY API Key>
Затем задайте их в классе Pydantic BaseSettings:
class BaseConfig(BaseSettings):
OPENAI_API_KEY: Optional[str]
PINECONE_API_KEY: Optional[str]
TAVILY_API_KEY: Optional[str]
"""Loads the dotenv file. Including this is necessary to get
pydantic to load a .env file."""
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
Структура проекта
Структура папок выглядит следующим образом:
project/
├── rag_process/
│ ├── __init__.py
│ ├── extractor.py
| └── service.py
│
├── graphs
│ │── chains.py # llm chains
│ ├── nodes.py # graph nodes. Defined as Python functions
│ ├── graph.py # graph workflow
│ ├── consts.py # define node names as constants
│ └── state.py # typed dictionary to store graph states
|── schemas.py # pydantic data models
|── upload.py # python functions to upload files asynchronously
├── main.py # fastAPI app
├── config.py # pydantic BaseConfig
│── dependencies.py # dependency inject to fastAPI endpoint function
├── test_chains.py # pytests
├── .env # environment variable definition
├── client.py # streamlit client
├── .gitignore
└── requirements.txt # package requirements
Цепочки получения данных, сервис FastAPI и клиент Streamlit переняты из предыдущей системы. Новыми элементами являются граф, его класс состояния и инструменты оценки, которые проверяют полученные документы на релевантность, выявляют галлюцинации и определяют, соответствует ли ответ вопросу.
Цепочки LLM
Эти цепочки находятся в файле graphs/chains.py и используют язык выражений LangChain (LCEL). Определено семь таких цепочек:
retrieve_documentsизвлекает K наиболее похожих документов из Pineconegenerate_answerформирует ответ на вопрос с учетом контекстаretrival_graderопределяет, являются ли полученные документы релевантнымиweb_searchвыполняет поиск в Интернете и возвращает контекстanswer_graderпроверяет, отвечает ли ответ на вопрос
hallucination_grader проверяет, что ответ основан на контекстеselect_routes отправляет вопросы о агентах, инжиниринге промптов и адверсарских атаках в RAG, а все остальное — в поисковикиЕдиничные тесты для цепочек
В системах с агентами тестирование имеет ещё большее значение, чем обычно, поскольку выводы LLM недетерминированы и могут меняться при обновлении моделей, настройке поставщиками или изменениях эмбеддингов. Тесты в файле test_chains.py используют pytest с маркером pytest.mark.asyncio для асинхронных цепочек. Положительный тест на извлечение данных с помощью RAG выглядит так:
@pytest.mark.asyncio
async def test_rag_retrieval_yes() -> None:
prompt = "what is prompt engineer ?"
searched_documents = await retrieve_documents(prompt)
searched_document = searched_documents[0]
# logger.info(f"searched_content: {searched_document.page_content}")
retrieval_grade = await retrieval_grader(searched_document, prompt)
assert retrieval_grade
В нем задаётся вопрос о инжиниринге промптов, и первый полученный документ передаётся в retrieval_grader. Поскольку эту тему уже охватывают документы в Pinecone, оценщик должен вернуть True. Этот тест защищает от сбоев при изменениях эмбеддингов или поведения модели, рано выявляет регрессии после обновлений модели и проверяет логику маршрутизации между RAG и поиском в интернете. Соответствующий отрицательный тест подтверждает, что оценщик возвращает False, и процесс переходит к поиску в интернете.
Состояние и узлы
Состояние — это типизированный словарь, хранящийся в graphs/state.py:
class GraphState(TypedDict):
question: str
generation: str
web_search: bool
documents: list[str]
Узлы считывают и обновляют этот состояние. Они определены в файле graphs/nodes.py и представляют собой обычные функции на Python, которые считывают состояние, вызывают цепочки LLM и возвращают обновленные поля. Узел, используемый в условном ребре, возвращает имя следующего узла для выполнения. Их существует семь.
generateвызываетgenerate_answerи возвращает ответ.retrieveвызываетretrieve_documentsи возвращает документы.search_webвызываетweb_searchи добавляет результаты к документам.grade_documentsсохраняет соответствующие документы и при необходимости устанавливаетweb_search=True.grade_generationзапускает инструменты оценки галлюцинаций и ответов и возвращает значение «полезно», «неполезно» или «не поддерживается».
decide_to_generate выбирает между WEBSEARCH и GENERATE в зависимости от значения web_search.route_question отправляет вопросы, связанные с агентом, в RAG, а остальные — на поиск в интернете.Создание StateGraph
Сам граф формируется в файле graphs/graph.py, причем готовый рабочий процесс представляет собой разветвлённую структуру, включающую этапы извлечения информации, оценки, генерации и поиска. Его создание включает несколько шагов.
Сначала каждому узлу присваивается строковый идентификатор. Хранение этих названий в виде констант в файле graphs/consts.py облегчает чтение графа и предотвращает опечатки:
RETRIEVE = "retrieve"
GRADE_DOCUMENTS = "grade_documents"
GENERATE = "generate"
WEBSEARCH = "websearch"
Далее создайте StateGraph и зарегистрируйте каждый узел вместе с его функцией:
workflow = StateGraph(GraphState)
workflow.add_node(RETRIEVE, retrieve)
workflow.add_node(GRADE_DOCUMENTS, grade_documents)
workflow.add_node(GENERATE, generate)
workflow.add_node(WEBSEARCH, search_web)
Каждый вызов add_node связывает имя с функцией, и такое разделение имён и поведения облегчает поддержку, визуализацию и расширение рабочего процесса.
В-третьих, необходимо установить условный точку входа. Вместо жёсткой привязки начального узла LangGraph позволяет функции проанализировать вопрос и выбрать первый маршрут. Здесь route_question определяет, следует ли начинать с поиска в Интернете или с извлечения информации с помощью RAG:
workflow.set_conditional_entry_point(
route_question,
{
WEBSEARCH: WEBSEARCH,
RETRIEVE: RETRIEVE,
}
)
Эта функция выполняется в первую очередь и возвращает либо "WEBSEARCH", либо "RETRIEVE" в зависимости от темы, причем LangGraph использует её результат для выбора начального узла. Вопросы, связанные с агентами, направляются в RAG, а вопросы общего характера начинаются с поиска в интернете.
В-четвертых, соединяют узлы рёбрами. Статические рёбра всегда следуют одному пути, тогда как условные рёбра выбирают следующий узел на основе решения большой языковой модели:
workflow.add_edge(RETRIEVE, GRADE_DOCUMENTS)
workflow.add_conditional_edges(
GRADE_DOCUMENTS,
decide_to_generate,
{
WEBSEARCH: WEBSEARCH,
GENERATE: GENERATE,
},
)
workflow.add_conditional_edges(
GENERATE,
grade_generation,
{
"not supported": GENERATE,
"useful": END,
"not useful": WEBSEARCH,
}
)
workflow.add_edge(WEBSEARCH, GENERATE)
workflow.add_edge(GENERATE, END)
Вместе эти элементы формируют динамический рабочий процесс, в котором большая языковая модель выбирает начальный путь, релевантность документа определяет использование метода RAG или поиска в интернете, а качество ответа решает, следует ли его улучшать, перейти на альтернативный вариант или завершить процесс. Такая структура превращает простую схему RAG в более надежную агентскую систему.
Наконец, соберите граф и при необходимости нарисуйте его:
app = workflow.compile()
app.get_graph().draw_mermaid_png(output_file_path="graphs.png")
Распространение через FastAPI
В файле dependencies.py функция импортирует скомпилированную структуру графа и вызывает её асинхронно:
from graphs.graph import app
async def get_generation(body: RAGRequest=Body(...)) -> dict:
try:
generation = await app.ainvoke(body.dict())
return {
"answer": generation.get("generation", ""),
"web_search": generation.get("web_search", False)
}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
FastAPI вводит его как зависимость, и он возвращает сгенерированный ответ вместе с флагом, указывающим, пришел ли он от RAG или из результатов веб-поиска.
Попробуем
Фронтенд Streamlit из предыдущей системы можно использовать без изменений. Запрос о том, как стать миллионером, корректно направляет на веб-поиск, тогда как запрос о том, что такое инжиниринг промптов, направляет на семантический путь RAG. Это демонстрирует динамическое маршрутизирование вместе с четким указанием источника каждого ответа.
Чтобы подвести итог: LangGraph StateGraph теперь управляет состоянием, рабочий процесс включает динамические переходы с циклами и разветвлениями через условные ребра, инструменты оценки LLM определяют релевантность документов, выявляют галлюцинации и оценивают качество ответов, а API сообщает пользователю, пришел ли ответ от RAG или из Интернета. Следующим шагом в этой работе является использование инструментов и фреймворка ReAct для еще более агентного поведения.