LangGraphSeniorExperience

Как проверять корректность решения на LangGraph, а не только успешное выполнение кода?

Корректность в LangGraph проверяется на четырёх уровнях: unit-тесты узлов с мок-LLM, изолированные тесты routing-функций, интеграционные тесты графа с MemorySaver и LLM evaluation harness (LangSmith/RAGAS) для нетерминированных ответов.

Разница между «код выполнился» и «результат корректен»

В LangGraph граф может завершиться без исключений, но вернуть неверный результат: LLM галлюцинировал, routing-функция выбрала не ту ветку, reducer перетёр часть истории. Проверка корректности требует отдельного слоя валидации поверх happy-path тестов.

Уровень 1: детерминированные unit-тесты узлов

Каждый узел — чистая функция над State. Мокируем LLM-вызовы, проверяем трансформацию State:

from unittest.mock import patch, MagicMock
from my_agent.nodes import evaluate_node
from my_agent.state import State

def test_evaluate_sets_score():
    state: State = {
        "messages": [],
        "draft": "Paris is the capital of France.",
        "score": 0.0,
        "iterations": 1,
    }
    mock_response = MagicMock()
    mock_response.content = '{"score": 0.92, "feedback": "accurate"}'

    with patch("my_agent.nodes.llm.invoke", return_value=mock_response):
        result = evaluate_node(state)

    assert result["score"] == 0.92
    assert "feedback" in result
    # граф не упал, но и результат проверен

Уровень 2: тесты routing-логики

Routing-функции должны быть детерминированы и тестироваться изолированно. Особенно важно проверить граничные случаи:

from my_agent.routing import should_continue

def test_routing_ends_after_max_iterations():
    state = {"score": 0.5, "iterations": 3}
    assert should_continue(state) == "__end__"

def test_routing_retries_on_low_score():
    state = {"score": 0.5, "iterations": 1}
    assert should_continue(state) == "generate"

def test_routing_passes_on_high_score():
    state = {"score": 0.9, "iterations": 1}
    assert should_continue(state) == "__end__"

Уровень 3: интеграционные тесты всего графа

Запускаем граф с фиксированными моками LLM и проверяем финальный State. Используем MemorySaver вместо реального checkpointer:

from langgraph.checkpoint.memory import MemorySaver
from my_agent.graph import build_graph

def test_graph_terminates_and_produces_valid_state():
    memory = MemorySaver()
    graph = build_graph(checkpointer=memory)
    config = {"configurable": {"thread_id": "test-001"}}

    with patch("my_agent.nodes.llm.invoke") as mock_llm:
        # Первый вызов: низкий score → retry
        # Второй вызов: высокий score → завершение
        mock_llm.side_effect = [
            MagicMock(content='{"score": 0.5}'),
            MagicMock(content='{"score": 0.95}'),
        ]
        final = graph.invoke({"draft": "", "score": 0.0, "iterations": 0}, config)

    assert final["score"] >= 0.8
    assert final["iterations"] <= 3  # не зашёл в бесконечный цикл

Уровень 4: проверка State-инвариантов через get_state

После выполнения граф можно остановить через interrupt_before и инспектировать промежуточный State:

graph_with_interrupt = builder.compile(
    checkpointer=memory,
    interrupt_before=["evaluate"]
)
result = graph_with_interrupt.invoke(initial_state, config)
snapshot = graph_with_interrupt.get_state(config)

# Проверяем, что узел generate отработал корректно
assert snapshot.values["draft"] != ""
assert snapshot.next == ("evaluate",)

Уровень 5: LLM evaluation для нетерминированных ответов

Для финальных ответов LLM используйте evaluation harness (LangSmith Dataset + evaluate() или RAGAS для RAG-пайплайнов):

  • Фиксируйте тестовые датасеты (input → expected output) в langsmith.create_dataset().
  • Запускайте langsmith.evaluate(graph.invoke, data=dataset, evaluators=[accuracy_evaluator]) в CI.
  • Следите за дрейфом метрик при обновлении модели или промпта.

Подводные камни

  • Тестирование только через assert result is not None — граф завершился, но вернул пустой или некорректный State.
  • Отсутствие проверки числа итераций — тест проходит, но в production граф делает 10 лишних LLM-вызовов.
  • Использование реального LLM в unit-тестах — нестабильные ответы делают тесты flaky и дорогими.
  • Не тестировать routing-функции изолированно — ошибка в routing обнаруживается только в интеграционном тесте.
  • Игнорирование reducer-поведения — тест на одном сообщении проходит, но с несколькими сообщениями список перетирается.
  • Отсутствие seed для random в промптах с temperature > 0 — тесты проходят иногда, а не всегда.
  • Не проверять snapshot.next при interrupt-тестах — непонятно, на каком узле граф ожидает.

What hurts your answer

  • Сразу обвинять LangGraph, не проверив соседние слои системы
  • Чинить симптом без минимального воспроизведения и evidence
  • Не учитывать версии, конфигурацию, окружение и recent changes

What they're listening for

  • Умеет локализовать проблему вокруг LangGraph
  • Двигается от симптома к гипотезам и проверкам
  • Отличает баг инструмента от ошибки использования или окружения

Related topics