Перейти к содержимому
Главная страница » LangGraph в Docker: Поднятие отказоустойчивого графа состояний для RAG-агента

LangGraph в Docker: Поднятие отказоустойчивого графа состояний для RAG-агента

LangGraph — это фреймворк для создания сложных мультиагентных систем с управлением состояниями. В этом мануале мы создадим отказоустойчивый RAG-агент, работающий в Docker-контейнере с сохранением состояния между запусками.

Почему это важно?

  • Сохранение контекста: Без персистентности агент «забывает» всё после каждого запроса
  • Отказоустойчивость: При падении сервиса состояние восстанавливается из последней контрольной точки
  • Масштабирование: Поддержка множества параллельных сессий (threads) для разных пользователей 

Архитектура решения

КомпонентНазначениеПочему важен
PostgreSQLХранение контрольных точек состоянияПерсистентность, восстановление после сбоев 
RedisОчередь задач и стримингОтказоустойчивость распределённых операций
LangGraph APIВыполнение графа состоянийЦентральный оркестратор агентов
CheckpointerМеханизм снапшотов состоянияПозволяет продолжить выполнение после рестарта

Шаг 1: Структура проекта

Создадим следующую структуру директорий:

text

langgraph-rag-fault-tolerant/
├── docker-compose.yml
├── .env
├── langgraph.json
├── agents/
│   └── rag_agent.py
├── requirements.txt
└── Dockerfile

bash

mkdir -p langgraph-rag-fault-tolerant/agents
cd langgraph-rag-fault-tolerant

Шаг 2: Определение графа состояний RAG-агента

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

agents/rag_agent.py

python

from typing import TypedDict, Annotated, List, Literal
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.checkpoint.postgres import PostgresSaver
from langchain_core.messages import BaseMessage, AIMessage, HumanMessage
from langchain_openai import ChatOpenAI, OpenAIEmbeddings
from langchain_community.vectorstores import Qdrant
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
import os

# === 1. Определение состояния ===
class RAGState(TypedDict):
    """Состояние графа — сохраняется в checkpoint"""
    messages: Annotated[List[BaseMessage], add_messages]
    documents: List[str]
    is_relevant: bool
    retry_count: int

# === 2. Инициализация LLM и Vector Store ===
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
embeddings = OpenAIEmbeddings()

# Подключение к Qdrant (можно заменить на ваш векторный storage)
vector_store = Qdrant.from_existing_collection(
    embeddings=embeddings,
    collection_name="rag_docs",
    url=os.getenv("QDRANT_URL", "http://localhost:6333")
)

# === 3. Определение узлов графа ===
def retrieve(state: RAGState):
    """Узел: поиск релевантных документов"""
    last_message = state["messages"][-1].content
    docs = vector_store.similarity_search(last_message, k=3)
    return {"documents": [doc.page_content for doc in docs]}

def check_relevance(state: RAGState):
    """Узел: проверка релевантности найденных документов"""
    if not state["documents"]:
        return {"is_relevant": False}
    
    prompt = f"""Оцени, отвечают ли найденные документы на вопрос пользователя.
    Документы: {state['documents'][:500]}
    Вопрос: {state['messages'][-1].content}
    Ответь только ДА или НЕТ:"""
    
    response = llm.invoke(prompt)
    is_relevant = "да" in response.content.lower()
    return {"is_relevant": is_relevant}

def generate_answer(state: RAGState):
    """Узел: генерация ответа на основе документов"""
    context = "\n\n".join(state["documents"])
    prompt = f"""На основе контекста ответь на вопрос пользователя.
    
    Контекст: {context}
    Вопрос: {state['messages'][-1].content}
    
    Если в контексте нет информации, скажи об этом честно."""
    
    response = llm.invoke(prompt)
    return {"messages": [AIMessage(content=response.content)]}

def fallback_response(state: RAGState):
    """Узел: ответ без RAG (когда документы не релевантны)"""
    response = llm.invoke(
        f"Ответь на вопрос используя свои знания (документы не найдены): {state['messages'][-1].content}"
    )
    return {"messages": [AIMessage(content=response.content)], "retry_count": state.get("retry_count", 0) + 1}

def should_continue(state: RAGState) -> Literal["generate_answer", "fallback_response", "retrieve"]:
    """Условный переход: маршрутизация на основе релевантности"""
    if state["is_relevant"]:
        return "generate_answer"
    elif state.get("retry_count", 0) < 2:
        # Повторный поиск с переформулировкой (можно добавить query rewriting)
        return "retrieve"
    else:
        return "fallback_response"

# === 4. Сборка графа ===
def create_rag_graph(checkpointer):
    """Создание графа с persistence"""
    graph = StateGraph(RAGState)
    
    # Добавление узлов
    graph.add_node("retrieve", retrieve)
    graph.add_node("check_relevance", check_relevance)
    graph.add_node("generate_answer", generate_answer)
    graph.add_node("fallback_response", fallback_response)
    
    # Определение переходов
    graph.set_entry_point("retrieve")
    graph.add_edge("retrieve", "check_relevance")
    graph.add_conditional_edges(
        "check_relevance",
        should_continue,
        {
            "generate_answer": "generate_answer",
            "fallback_response": "fallback_response",
            "retrieve": "retrieve"
        }
    )
    graph.add_edge("generate_answer", END)
    graph.add_edge("fallback_response", END)
    
    # Компиляция с checkpointer'ом — это и есть отказоустойчивость!
    return graph.compile(checkpointer=checkpointer)

# === 5. Инициализация при запуске ===
def get_app():
    """Фабрика для LangGraph Server"""
    # Подключение к PostgreSQL для чекпоинтов
    conn_string = os.getenv(
        "DATABASE_URL",
        "postgresql://postgres:postgres@postgres:5432/langgraph"
    )
    
    # Инициализация PostgresSaver с таблицей чекпоинтов
    checkpointer = PostgresSaver.from_conn_string(conn_string)
    checkpointer.setup()  # Создание таблиц, если их нет
    
    return create_rag_graph(checkpointer)

# Экспорт для LangGraph Server
app = get_app()

Шаг 3: Конфигурация LangGraph

langgraph.json — конфиг для LangGraph CLI:

json

{
    "dependencies": ["."],
    "graphs": {
        "rag_agent": "./agents/rag_agent.py:app"
    },
    "env": ".env",
    "checkpointer": {
        "type": "postgres",
        "conn_string": "${DATABASE_URL}"
    }
}

Шаг 4: Docker-конфигурация

requirements.txt

text

langgraph>=0.2.0
langgraph-checkpoint-postgres>=0.1.0
langchain-openai>=0.1.0
langchain-community>=0.2.0
qdrant-client>=1.9.0
psycopg2-binary>=2.9.0

Dockerfile

dockerfile

FROM python:3.11-slim

WORKDIR /app

# Установка системных зависимостей
RUN apt-get update && apt-get install -y \
    gcc \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Копирование кода агента
COPY agents/ ./agents/
COPY langgraph.json .

# Установка LangGraph CLI
RUN pip install langgraph-cli

EXPOSE 8123

# Запуск LangGraph сервера
CMD ["langgraph", "serve", "--host", "0.0.0.0", "--port", "8123"]

docker-compose.yml — полный стек с отказоустойчивостью:

yaml

version: '3.8'

services:
  postgres:
    image: postgres:15
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: langgraph
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M

  redis:
    image: redis:7-alpine
    command: redis-server --appendonly yes
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 256M

  qdrant:
    image: qdrant/qdrant:latest
    volumes:
      - qdrant_data:/qdrant/storage
    ports:
      - "6333:6333"
    restart: unless-stopped

  langgraph-api:
    build: .
    environment:
      OPENAI_API_KEY: ${OPENAI_API_KEY}
      DATABASE_URL: postgresql://postgres:postgres@postgres:5432/langgraph
      REDIS_URL: redis://redis:6379
      QDRANT_URL: http://qdrant:6333
    ports:
      - "8123:8123"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      qdrant:
        condition: service_started
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 1G
          cpus: '1.0'
        reservations:
          memory: 512M
          cpus: '0.5'

volumes:
  postgres_data:
  redis_data:
  qdrant_data:

.env

bash

OPENAI_API_KEY=sk-your-key-here
# Опционально для LangSmith
LANGSMITH_API_KEY=your-langsmith-key
LANGSMITH_TRACING=true

Шаг 5: Запуск и тестирование

5.1 Запуск стека

bash

# Поднятие всех сервисов
docker-compose up -d

# Проверка статуса
docker-compose ps

# Просмотр логов API
docker-compose logs -f langgraph-api

5.2 Тестирование через API

bash

# Создание нового потока (thread) — аналог сессии пользователя
curl -X POST http://localhost:8123/threads \
  -H "Content-Type: application/json" \
  -d '{}'

# Отправка запроса с привязкой к thread_id
curl -X POST http://localhost:8123/runs/stream \
  -H "Content-Type: application/json" \
  -d '{
    "assistant_id": "rag_agent",
    "thread_id": "user_123_session_1",
    "input": {
      "messages": [
        {"role": "human", "content": "Что такое reward hacking?"}
      ]
    }
  }'

5.3 Проверка отказоустойчивости

bash

# 1. Отправляем запрос с длительной обработкой
# 2. Перезапускаем API сервис
docker-compose restart langgraph-api

# 3. Отправляем тот же запрос с тем же thread_id
# Система восстановит состояние и продолжит выполнение!

Шаг 6: Мониторинг состояния

Подключитесь к PostgreSQL и проверьте сохранённые контрольные точки:

bash

docker-compose exec postgres psql -U postgres -d langgraph

sql

-- Просмотр всех контрольных точек
SELECT 
    thread_id,
    checkpoint_id,
    parent_checkpoint_id,
    checkpoint ->> 'messages' as messages
FROM checkpoints
ORDER BY checkpoint_id DESC
LIMIT 10;

-- Статистика по потокам
SELECT 
    thread_id,
    COUNT(*) as checkpoint_count,
    MAX(ts) as last_update
FROM checkpoints
GROUP BY thread_id;

Продвинутые сценарии отказоустойчивости

TTL для автоматической очистки чекпоинтов

python

from langgraph.checkpoint.postgres import PostgresSaver
from datetime import timedelta

checkpointer = PostgresSaver.from_conn_string(conn_string)
checkpointer.setup()

# Установка TTL — состояние хранится 7 дней
checkpointer.set_ttl(thread_id="user_123", ttl=timedelta(days=7))

Redis для распределённых блокировок

Добавьте в docker-compose.yml:

yaml

redis:
  image: redis:7-alpine
  command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}

В коде агента используйте Redis для предотвращения конкурентных запусков одного потока:

python

import redis
r = redis.Redis(host='redis', port=6379, decode_responses=True)

def with_lock(thread_id: str):
    """Декоратор для блокировки выполнения"""
    def decorator(func):
        def wrapper(*args, **kwargs):
            lock_key = f"lock:{thread_id}"
            if r.setnx(lock_key, "locked"):
                r.expire(lock_key, 60)  # timeout 60 сек
                try:
                    return func(*args, **kwargs)
                finally:
                    r.delete(lock_key)
            else:
                raise Exception(f"Thread {thread_id} already processing")
        return wrapper
    return decorator

Устранение неполадок

ПроблемаРешение
PostgresSaver не найденУстановите langgraph-checkpoint-postgres
Чекпоинты не сохраняютсяПроверьте DATABASE_URL и права на запись
«No checkpoint found for thread»Убедитесь, что thread_id передан во всех запросах
GIL блокирует параллельные запросыИспользуйте uvicorn --workers 4 для асинхронной обработки

Заключение

Вы создали отказоустойчивый RAG-агент на LangGraph в Docker со следующими характеристиками:

✅ Персистентность состояния — PostgreSQL хранит контрольные точки, позволяя восстановить выполнение после рестарта
✅ Масштабируемость — Redis и thread_id обеспечивают изоляцию сессий пользователей 
✅ Отказоустойчивость — Healthchecks и restart policies в Docker Compose
✅ RAG-логика — Проверка релевантности предотвращает галлюцинации при нерелевантных запросах 

Дальнейшее улучшение

  1. Горизонтальное масштабирование — добавьте langgraph-api реплики с балансировщиком
  2. Асинхронные узлы — перепишите узлы на async для лучшей производительности
  3. Мониторинг — интегрируйте Prometheus + Grafana для метрик чекпоинтов
  4. Шифрование — добавьте шифрование состояния в Postgres для sensitive data 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *