Как проверять корректность решения на 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
- Двигается от симптома к гипотезам и проверкам
- Отличает баг инструмента от ошибки использования или окружения