Agentic RAG локально: сборка без API на LangGraph, Ollama, Qdrant | AiManual
AiManual Logo Ai / Manual.
29 Дек 2025 Гайд

Полное руководство: как собрать Agentic RAG систему полностью локально — без API и облаков

Пошаговый гайд по сборке полностью локальной Agentic RAG системы с использованием LangGraph, Ollama, Qdrant и Gradio. Никаких платных API — только open-source р

Почему локальная Agentic RAG — это будущее AI-разработки

В мире, где каждый запрос к GPT-4 стоит денег, а конфиденциальность данных становится приоритетом номер один, локальные AI-системы перестали быть нишевым решением. Agentic RAG (Retrieval-Augmented Generation) — это следующая ступень эволюции: не просто поиск и генерация, а интеллектуальный агент, который сам решает, когда искать информацию, когда использовать инструменты, и как планировать выполнение сложных задач.

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

Если вам интересна архитектура современных AI-агентов, рекомендую прочитать нашу статью «Как спроектировать современного AI-агента: от planner/executor до stateful memory», где мы подробно разбираем архитектурные подходы.

Архитектура нашей локальной системы

Прежде чем погружаться в код, давайте посмотрим на общую архитектуру системы, которую мы будем строить:

Компонент Технология Назначение
LLM (языковая модель) Ollama + Llama 3.2 / Mistral Мозг агента, обработка запросов, планирование
Оркестратор LangGraph / LangChain Управление workflow агента, состояние, графы
Векторная БД Qdrant (локальная) Хранение и поиск контекста
Embeddings Sentence Transformers Векторизация текста
Интерфейс Gradio / Streamlit Веб-интерфейс для взаимодействия
Документы Unstructured / LlamaParse Парсинг PDF, DOCX, HTML

1 Подготовка окружения и установка зависимостей

Начнем с создания чистого Python окружения. Я рекомендую использовать Python 3.10 или выше:

# Создаем виртуальное окружение
python -m venv agentic_rag_env
source agentic_rag_env/bin/activate  # Для Windows: agentic_rag_env\Scripts\activate

# Устанавливаем базовые зависимости
pip install --upgrade pip
pip install langgraph langchain langchain-community
pip install sentence-transformers qdrant-client
pip install unstructured[all] pypdf
pip install gradio fastapi uvicorn
pip install ollama

Внимание: Для работы с различными форматами документов (PDF, DOCX, HTML) библиотеке Unstructured могут потребоваться системные зависимости. На Ubuntu/Debian установите: sudo apt-get install poppler-utils tesseract-ocr libreoffice

2 Запуск локальных сервисов: Ollama и Qdrant

Ollama — это самый простой способ запускать локальные LLM. Установите и запустите модель:

# Установка Ollama (Linux/Mac)
curl -fsSL https://ollama.com/install.sh | sh

# Запускаем сервер Ollama
ollama serve &

# Скачиваем и запускаем модель (рекомендую Llama 3.2 3B или Mistral 7B)
ollama pull llama3.2:3b
# или
ollama pull mistral:7b

Теперь запустим Qdrant — векторную базу данных, которая будет хранить наши эмбеддинги:

# Способ 1: Через Docker (рекомендуется)
docker run -p 6333:6333 -p 6334:6334 \
    -v $(pwd)/qdrant_storage:/qdrant/storage:z \
    qdrant/qdrant

# Способ 2: Через Python клиент (полностью в памяти)
pip install qdrant-client[http]
# В коде создадим in-memory коллекцию
💡
Выбор модели: Для локального запуска на CPU или слабой GPU лучше всего подходят Llama 3.2 3B (3 миллиарда параметров) или Phi-3 Mini (3.8B). Для мощных видеокарт (RTX 3090/4090) можно использовать Llama 3.1 8B или Mixtral 8x7B. Подробнее о выборе моделей читайте в нашей статье «Агентные workflow на практике».

3 Создание ядра Agentic RAG системы

Теперь напишем основной код нашего агента. Мы создадим агента с ReAct архитектурой (Reasoning + Acting), который умеет:

  1. Анализировать запрос пользователя
  2. Решать, нужен ли поиск в базе знаний
  3. Использовать инструменты (калькулятор, поиск в интернете, etc.)
  4. Планировать выполнение сложных многошаговых задач
  5. Запоминать контекст разговора
from typing import TypedDict, List, Optional, Annotated
import operator
from langchain_community.llms import Ollama
from langchain_community.embeddings import OllamaEmbeddings
from langchain_community.vectorstores import Qdrant
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import DirectoryLoader, PyPDFLoader
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage
from langchain.tools import tool
from langchain.agents import AgentExecutor, create_react_agent
import numpy as np

# Определяем состояние агента
class AgentState(TypedDict):
    messages: Annotated[List, add_messages]  # История сообщений
    knowledge_base: Optional[str]  # Релевантные документы
    current_step: str  # Текущий шаг выполнения
    needs_search: bool  # Нужен ли поиск в RAG
    final_answer: Optional[str]  # Финальный ответ

# Инициализация LLM через Ollama
llm = Ollama(
    model="llama3.2:3b",
    temperature=0.1,  # Низкая температура для консистентности
    num_predict=1024,  # Максимальная длина ответа
)

# Инициализация эмбеддингов
embeddings = OllamaEmbeddings(
    model="nomic-embed-text",  # Хорошие локальные эмбеддинги
)

# Создаем инструменты для агента
@tool
def search_knowledge_base(query: str) -> str:
    """Поиск информации в локальной базе знаний"""
    # Здесь будет подключение к Qdrant
    return "Найденная информация из базы знаний"

@tool
def calculate(expression: str) -> str:
    """Выполнение математических вычислений"""
    try:
        result = eval(expression)
        return f"Результат: {result}"
    except:
        return "Ошибка в выражении"

@tool
def web_search(query: str) -> str:
    """Поиск в интернете (если нужно)"""
    # Можно подключить локальный поиск через DuckDuckGo
    return "Результаты поиска из интернета"

# Создаем граф агента
def create_agent_graph():
    workflow = StateGraph(AgentState)
    
    # Узел: анализ запроса
    def analyze_query(state: AgentState):
        messages = state["messages"]
        last_message = messages[-1].content if messages else ""
        
        # Простой анализ: проверяем, нужен ли поиск
        search_keywords = ["информация", "документ", "найди", "ищи", "база знаний"]
        needs_search = any(keyword in last_message.lower() for keyword in search_keywords)
        
        return {
            "needs_search": needs_search,
            "current_step": "analyzing_query"
        }
    
    # Узел: поиск в RAG
    def rag_search(state: AgentState):
        if not state["needs_search"]:
            return {"knowledge_base": None, "current_step": "generating_answer"}
        
        # Получаем последний запрос
        query = state["messages"][-1].content
        
        # Здесь должен быть реальный поиск в Qdrant
        # Пока заглушка
        results = ["Документ 1: Информация о...", "Документ 2: Данные по..."]
        
        return {
            "knowledge_base": "\n".join(results),
            "current_step": "generating_answer"
        }
    
    # Узел: генерация ответа
    def generate_answer(state: AgentState):
        messages = state["messages"]
        knowledge = state.get("knowledge_base", "")
        
        # Формируем промпт с контекстом
        prompt = f"""Ты — интеллектуальный ассистент. Используй следующую информацию если она релевантна:
        
        Контекст из базы знаний:
        {knowledge}
        
        История разговора:
        {messages[-5:] if len(messages) > 5 else messages}
        
        Текущий запрос: {messages[-1].content if messages else ''}
        
        Ответь максимально полезно и точно:"""
        
        response = llm.invoke(prompt)
        
        return {
            "final_answer": response,
            "current_step": "completed",
            "messages": messages + [AIMessage(content=response)]
        }
    
    # Добавляем узлы в граф
    workflow.add_node("analyze", analyze_query)
    workflow.add_node("search", rag_search)
    workflow.add_node("generate", generate_answer)
    
    # Определяем edges (переходы)
    workflow.set_entry_point("analyze")
    workflow.add_edge("analyze", "search")
    workflow.add_edge("search", "generate")
    workflow.add_edge("generate", END)
    
    return workflow.compile()

# Создаем и компилируем граф
agent_graph = create_agent_graph()

4 Настройка векторной базы знаний с Qdrant

Теперь создадим полноценную базу знаний. Сначала загрузим документы, затем создадим эмбеддинги и сохраним в Qdrant:

import os
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct

class LocalKnowledgeBase:
    def __init__(self, collection_name="documents", persist_dir="./qdrant_data"):
        # Инициализируем клиент Qdrant
        self.client = QdrantClient(
            path=persist_dir,  # Локальное хранение
            prefer_grpc=True
        )
        
        self.collection_name = collection_name
        self.embeddings = embeddings
        
        # Создаем коллекцию если её нет
        self._create_collection()
    
    def _create_collection(self):
        try:
            self.client.get_collection(self.collection_name)
            print(f"Коллекция {self.collection_name} уже существует")
        except:
            # Создаем новую коллекцию
            self.client.create_collection(
                collection_name=self.collection_name,
                vectors_config=VectorParams(
                    size=768,  # Размерность эмбеддингов nomic-embed-text
                    distance=Distance.COSINE
                )
            )
            print(f"Создана коллекция {self.collection_name}")
    
    def load_documents(self, directory_path: str):
        """Загрузка документов из директории"""
        loaders = {
            '.pdf': PyPDFLoader,
            '.txt': lambda path: DirectoryLoader(path, glob="**/*.txt"),
            '.docx': lambda path: DirectoryLoader(path, glob="**/*.docx"),
        }
        
        all_documents = []
        
        for ext, loader_class in loaders.items():
            for file_path in os.listdir(directory_path):
                if file_path.endswith(ext):
                    full_path = os.path.join(directory_path, file_path)
                    try:
                        loader = loader_class(full_path)
                        documents = loader.load()
                        all_documents.extend(documents)
                        print(f"Загружен {file_path}: {len(documents)} страниц")
                    except Exception as e:
                        print(f"Ошибка загрузки {file_path}: {e}")
        
        # Разбиваем на чанки
        text_splitter = RecursiveCharacterTextSplitter(
            chunk_size=1000,
            chunk_overlap=200,
            length_function=len
        )
        
        chunks = text_splitter.split_documents(all_documents)
        print(f"Всего чанков: {len(chunks)}")
        
        # Создаем эмбеддинги и сохраняем в Qdrant
        self._index_documents(chunks)
        
        return chunks
    
    def _index_documents(self, documents):
        """Индексация документов в Qdrant"""
        points = []
        
        for i, doc in enumerate(documents):
            # Создаем эмбеддинг для каждого чанка
            embedding = self.embeddings.embed_query(doc.page_content)
            
            point = PointStruct(
                id=i,
                vector=embedding,
                payload={
                    "text": doc.page_content,
                    "source": doc.metadata.get("source", "unknown"),
                    "page": doc.metadata.get("page", 0)
                }
            )
            points.append(point)
            
            # Пакетная загрузка каждые 100 точек
            if len(points) >= 100:
                self.client.upsert(
                    collection_name=self.collection_name,
                    points=points
                )
                points = []
                print(f"Индексировано {i+1} документов")
        
        # Загружаем оставшиеся
        if points:
            self.client.upsert(
                collection_name=self.collection_name,
                points=points
            )
        
        print(f"Индексация завершена. Всего документов: {len(documents)}")
    
    def search(self, query: str, top_k: int = 5):
        """Поиск в базе знаний"""
        # Создаем эмбеддинг запроса
        query_embedding = self.embeddings.embed_query(query)
        
        # Ищем в Qdrant
        search_result = self.client.search(
            collection_name=self.collection_name,
            query_vector=query_embedding,
            limit=top_k
        )
        
        # Форматируем результаты
        results = []
        for hit in search_result:
            results.append({
                "text": hit.payload["text"],
                "score": hit.score,
                "source": hit.payload.get("source", "unknown")
            })
        
        return results

# Инициализируем базу знаний
kb = LocalKnowledgeBase()

# Загружаем документы (если есть)
if os.path.exists("./documents"):
    kb.load_documents("./documents")

5 Создание веб-интерфейса с Gradio

Теперь создадим простой веб-интерфейс для взаимодействия с нашим агентом:

import gradio as gr
import asyncio
from typing import List

class AgenticRAGInterface:
    def __init__(self, agent_graph, knowledge_base):
        self.agent = agent_graph
        self.kb = knowledge_base
        self.conversation_history = []
    
    def process_query(self, query: str, use_rag: bool):
        """Обработка запроса пользователя"""
        
        # Добавляем сообщение в историю
        self.conversation_history.append({"role": "user", "content": query})
        
        # Если нужен RAG, ищем в базе знаний
        context = ""
        if use_rag:
            search_results = self.kb.search(query)
            if search_results:
                context = "\n".join([f"[{i+1}] {res['text'][:200]}..." 
                                    for i, res in enumerate(search_results)])
        
        # Подготавливаем состояние
        initial_state = {
            "messages": [HumanMessage(content=query)],
            "knowledge_base": context,
            "needs_search": use_rag,
            "current_step": "start",
            "final_answer": None
        }
        
        # Запускаем агента
        try:
            result = self.agent.invoke(initial_state)
            answer = result.get("final_answer", "Не удалось получить ответ")
            
            # Добавляем ответ в историю
            self.conversation_history.append({"role": "assistant", "content": answer})
            
            # Форматируем историю для отображения
            history_text = self._format_history()
            
            return answer, history_text
            
        except Exception as e:
            error_msg = f"Ошибка: {str(e)}"
            return error_msg, self._format_history()
    
    def _format_history(self):
        """Форматирование истории разговора"""
        formatted = []
        for msg in self.conversation_history[-10:]:  # Последние 10 сообщений
            role = "👤 Пользователь" if msg["role"] == "user" else "🤖 Ассистент"
            formatted.append(f"{role}: {msg['content']}")
        return "\n\n".join(formatted)
    
    def clear_history(self):
        """Очистка истории"""
        self.conversation_history = []
        return "История очищена", ""

# Создаем интерфейс
interface = AgenticRAGInterface(agent_graph, kb)

# Создаем Gradio интерфейс
def create_gradio_interface():
    with gr.Blocks(title="Локальный Agentic RAG", theme=gr.themes.Soft()) as demo:
        gr.Markdown("""
        # 🤖 Локальный Agentic RAG Система
        Полностью автономный AI-агент с базой знаний. Работает без интернета!
        """)
        
        with gr.Row():
            with gr.Column(scale=2):
                query_input = gr.Textbox(
                    label="Ваш запрос",
                    placeholder="Задайте вопрос или дайте задание...",
                    lines=3
                )
                
                rag_toggle = gr.Checkbox(
                    label="Использовать базу знаний (RAG)",
                    value=True
                )
                
                submit_btn = gr.Button("Отправить", variant="primary")
                clear_btn = gr.Button("Очистить историю")
                
            with gr.Column(scale=3):
                answer_output = gr.Textbox(
                    label="Ответ агента",
                    lines=8,
                    interactive=False
                )
                
                history_output = gr.Textbox(
                    label="История разговора",
                    lines=12,
                    interactive=False
                )
        
        # Примеры запросов
        gr.Examples(
            examples=[
                ["Объясни концепцию machine learning простыми словами", True],
                ["Посчитай: (15 * 4) + (120 / 3)", False],
                ["Найди информацию о нейронных сетях в базе знаний", True],
                ["Спланируй изучение Python на месяц", False]
            ],
            inputs=[query_input, rag_toggle],
            label="Примеры запросов"
        )
        
        # Обработчики событий
        submit_btn.click(
            fn=interface.process_query,
            inputs=[query_input, rag_toggle],
            outputs=[answer_output, history_output]
        )
        
        clear_btn.click(
            fn=interface.clear_history,
            inputs=[],
            outputs=[answer_output, history_output]
        )
        
        query_input.submit(
            fn=interface.process_query,
            inputs=[query_input, rag_toggle],
            outputs=[answer_output, history_output]
        )
        
        return demo

# Запускаем интерфейс
if __name__ == "__main__":
    demo = create_gradio_interface()
    demo.launch(
        server_name="0.0.0.0",
        server_port=7860,
        share=False  # Не создавать публичную ссылку
    )

Расширенные возможности и оптимизация

Добавление инструментов и навыков

Настоящая сила Agentic RAG проявляется, когда агент умеет использовать различные инструменты. Давайте расширим наш агента:

from datetime import datetime
import json

# Дополнительные инструменты для агента
@tool
def get_current_time() -> str:
    """Возвращает текущее время и дату"""
    return datetime.now().strftime("%Y-%m-%d %H:%M:%S")

@tool
def analyze_sentiment(text: str) -> dict:
    """Анализ тональности текста"""
    # Можно использовать локальную модель для анализа тональности
    prompt = f"Проанализируй тональность текста: '{text}'. Ответь одним словом: позитивный, негативный, нейтральный."
    
    response = llm.invoke(prompt)
    return {
        "text": text,
        "sentiment": response.strip(),
        "analysis_time": get_current_time()
    }

@tool
def summarize_text(text: str, max_length: int = 200) -> str:
    """Суммаризация текста"""
    prompt = f"Суммаризуй следующий текст в {max_length} символов:\n\n{text}"
    return llm.invoke(prompt)

# Инструмент для работы с файлами
@tool
def read_file(file_path: str) -> str:
    """Чтение текстового файла"""
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            return f.read()
    except Exception as e:
        return f"Ошибка чтения файла: {str(e)}"

# Создаем агента с расширенным набором инструментов
tools = [
    search_knowledge_base,
    calculate,
    web_search,
    get_current_time,
    analyze_sentiment,
    summarize_text,
    read_file
]

# Можно создать специализированного агента для конкретной задачи
class ResearchAgent:
    def __init__(self):
        self.tools = [search_knowledge_base, web_search, summarize_text]
        
    def research_topic(self, topic: str, depth: str = "basic"):
        """Исследование темы с разной глубиной"""
        steps = []
        
        # Планирование исследования
        plan_prompt = f"""Спланируй исследование темы '{topic}'.
        Глубина: {depth}
        Верни план в формате JSON со списком шагов."""
        
        plan = llm.invoke(plan_prompt)
        steps.append(f"План исследования: {plan}")
        
        # Выполнение плана
        # Здесь можно реализовать автоматическое выполнение шагов
        
        return steps

Оптимизация производительности

Проблема Решение Эффект
Медленная инференция LLM Кэширование промптов, использование более легких моделей (Phi-3, Llama 3.2 3B) Ускорение в 2-3 раза
Большие эмбеддинги Использование бинарных эмбеддингов, квантование Экономия 4-8x памяти
Медленный поиск в Qdrant Индексы HNSW, сегментирование коллекций Ускорение поиска в 10-100 раз
Потребление памяти Использование GGUF квантованных моделей, offloading слоев на CPU Работа на 8-16GB RAM

Типичные ошибки и как их избежать

Ошибка 1: Слишком большие чанки документов. Решение: Используйте чанки 500-1000 токенов с overlap 10-20%.

Ошибка 2: LLM игнорирует контекст из RAG. Решение: Используйте prompt engineering: явно указывайте модель использовать контекст, добавьте few-shot примеры.

Ошибка 3: Агент зацикливается. Решение: Добавьте максимальное количество шагов в LangGraph, используйте проверку на повторяющиеся действия.

Ошибка 4: Плохое качество эмбеддингов. Решение: Используйте специализированные модели для эмбеддингов (nomic-embed-text, bge-small-en-v1.5).

FAQ: Частые вопросы

Какое железо нужно для запуска?

Минимально: 8GB RAM, 4 ядра CPU. Рекомендуется: 16GB RAM, 8+ ядер CPU, видеокарта с 8GB+ VRAM (для более крупных моделей).

Можно ли использовать эту систему в production?

Да, но с некоторыми доработками: добавьте логирование, мониторинг, обработку ошибок и механизмы fallback. Для production-ready решений читайте нашу статью «Production-ready AI-агенты: как превратить хайп в работающую систему для бизнеса».

Как добавить мультимодальность (изображения, аудио)?

Используйте локальные мультимодальные модели (LLaVA, Bakllava) через Ollama. Для голосового интерфейса можно добавить Whisper для STT (распознавание речи) и Coqui TTS для синтеза. Подробнее в статье «Как собрать голосового ассистента на одной видеокарте».

Как обеспечить безопасность?

Локальный запуск уже обеспечивает конфиденциальность. Для защиты от prompt injection используйте валидацию входных данных и промпт-шейпинг. Подробнее в «Как защититься от атаки Man-in-the-Prompt».

Заключение

Локальные Agentic RAG системы — это не будущее, а настоящее. С текущим развитием open-source моделей и инструментов, каждый разработчик может собрать мощного AI-агента, который работает полностью автономно, не отправляет данные в облако и не требует ежемесячных платежей.

Ключевые преимущества нашей системы:

  • Полная приватность: Все данные остаются на вашем компьютере
  • Нулевая стоимость использования: После начальной настройки — никаких платежей
  • Гибкость: Можно дообучать модели, добавлять свои инструменты, модифицировать архитектуру
  • Производительность: Современные оптимизации позволяют запускать на consumer железе

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

💡
Профессиональный совет: Для production систем обязательно добавьте мониторинг качества ответов, A/B тестирование разных моделей и механизм человеческого интера (human-in-the-loop) для сложных случаев. Помните — даже лучшие AI-агенты иногда ошибаются, и важно иметь возможность корректировать их работу.