Конечные Автоматы и Агенты: Построение Надёжных Рабочих Процессов с LangGraph
Большинство туториалов по агентам показывают простой цикл: спросить Claude, разобрать ответ, вызвать инструмент, повторить. Для демонстраций это работает. В продакшене нужны детерминизм, восстановление после ошибок, точки человеческого одобрения и возможность аудита.
LangGraph привносит конечные автоматы в рабочие процессы агентов. Вместо специального цикла из if-операторов вы получаете явный граф: именованные узлы (логические единицы), типизированные рёбра (переходы) и общую схему состояния, которая проходит через всё выполнение.
Почему Конечные Автоматы для Агентов?
Проблема Специального Цикла
Типичный цикл агента выглядит так:
messages = []while True: response = claude.messages.create(model=..., messages=messages) if response.stop_reason == "end_turn": break for tool_call in get_tool_calls(response): result = execute_tool(tool_call) messages.append(tool_result(tool_call.id, result))Для двух-трёх инструментов это читаемо. Добавьте пять инструментов, условные пути, шаг одобрения человеком и логику повторных попыток — и получите сотни строк запутанного потока управления.
Глубинная проблема — неявное состояние. На каком этапе находится агент? Какие данные он собрал? Всё живёт в messages — нетипизированном блобе без принудительной схемы.
Конечные Автоматы как Решение
Конечный автомат делает неявное явным. Вы определяете:
- Узлы — дискретные логические единицы, получающие текущее состояние, выполняющие одну задачу и возвращающие обновления состояния.
- Рёбра — переходы между узлами, безусловные (
A → B всегда) или условные. - Состояние — типизированный словарь, проходящий через весь граф. Схема валидируется на каждом шаге.
Когда НЕ Использовать LangGraph
Для простых однократных задач прямой API-вызов быстрее и нагляднее. Используйте LangGraph, когда рабочий процесс включает:
- Несколько различных этапов, выполняемых последовательно
- Условное ветвление на основе промежуточных результатов
- Шаги с участием человека
- Логику восстановления после ошибок или повторных попыток
- Требования аудитируемости
Основы LangGraph
Определение Состояния
from typing import TypedDict
class ResearchState(TypedDict): query: str research_notes: str draft: str review_feedback: str is_approved: boolУзлы
import anthropic
client = anthropic.Anthropic()
def research_node(state: ResearchState) -> dict: response = client.messages.create( model="claude-opus-4-6", max_tokens=2048, messages=[{ "role": "user", "content": f"Исследуй эту тему детально: {state['query']}" }] ) return {"research_notes": response.content[0].text}
def draft_node(state: ResearchState) -> dict: response = client.messages.create( model="claude-opus-4-6", max_tokens=2048, messages=[{ "role": "user", "content": f"Напиши черновик на основе этих заметок:\n{state['research_notes']}" }] ) return {"draft": response.content[0].text}Рёбра и Условная Маршрутизация
from typing import Literal
def route_after_check(state: ResearchState) -> Literal["human_review", "draft"]: if state.get("review_feedback"): return "human_review" return "draft"
workflow.add_conditional_edges( "research", route_after_check, {"human_review": "review_node", "draft": "draft_node"})Создание и Запуск Графа
from langgraph.graph import StateGraph, END
workflow = StateGraph(ResearchState)workflow.add_node("research", research_node)workflow.add_node("draft", draft_node)workflow.set_entry_point("research")workflow.add_edge("research", "draft")workflow.add_edge("draft", END)
graph = workflow.compile()result = graph.invoke({"query": "Что такое протокол MCP?"})print(result["draft"])Построение Рабочего Процесса Проверки Документов
Проектирование Состояния
class DocumentState(TypedDict): document: str key_terms: list[str] compliance_issues: list[str] summary: str human_feedback: str is_approved: boolРеализация Узлов
import json
def extract_terms_node(state: DocumentState) -> dict: response = client.messages.create( model="claude-opus-4-6", max_tokens=512, messages=[{ "role": "user", "content": ( "Извлеки ровно 5 ключевых терминов из этого документа. " "Верни их как JSON-массив строк.\n\n" f"Документ: {state['document']}" ) }] ) try: raw = response.content[0].text.strip() if raw.startswith("```"): raw = raw.split("```")[1] if raw.startswith("json"): raw = raw[4:] terms = json.loads(raw.strip()) except (json.JSONDecodeError, IndexError): terms = [t.strip() for t in response.content[0].text.split("\n") if t.strip()][:5] return {"key_terms": terms}
def check_compliance_node(state: DocumentState) -> dict: response = client.messages.create( model="claude-opus-4-6", max_tokens=1024, messages=[{ "role": "user", "content": ( "Проверь этот документ на проблемы соответствия. " "Ищи: персональные данные (имена, СНИЛС, email, телефоны), " "конфиденциальные пометки, непроверенные утверждения.\n\n" "Верни JSON-массив описаний проблем. " "Верни пустой массив [], если проблем не обнаружено.\n\n" f"Документ: {state['document']}" ) }] ) try: raw = response.content[0].text.strip() if raw.startswith("```"): raw = raw.split("```")[1] if raw.startswith("json"): raw = raw[4:] issues = json.loads(raw.strip()) except (json.JSONDecodeError, IndexError): issues = [] return {"compliance_issues": issues}
def human_review_node(state: DocumentState) -> dict: print("\n=== ТРЕБУЕТСЯ ПРОВЕРКА ЧЕЛОВЕКОМ ===") for issue in state["compliance_issues"]: print(f" - {issue}") return { "human_feedback": "Проверено и одобрено после редактирования персональных данных", "is_approved": True }
def summarize_node(state: DocumentState) -> dict: response = client.messages.create( model="claude-opus-4-6", max_tokens=256, messages=[{ "role": "user", "content": f"Кратко изложи этот документ ровно в двух предложениях:\n\n{state['document']}" }] ) return {"summary": response.content[0].text, "is_approved": True}Сборка Графа
def route_after_compliance(state: DocumentState) -> Literal["human_review", "summarize"]: if state.get("compliance_issues"): return "human_review" return "summarize"
workflow = StateGraph(DocumentState)workflow.add_node("extract", extract_terms_node)workflow.add_node("check", check_compliance_node)workflow.add_node("review", human_review_node)workflow.add_node("summarize", summarize_node)
workflow.set_entry_point("extract")workflow.add_edge("extract", "check")workflow.add_conditional_edges( "check", route_after_compliance, {"human_review": "review", "summarize": "summarize"})workflow.add_edge("review", "summarize")workflow.add_edge("summarize", END)
graph = workflow.compile()
result = graph.invoke({ "document": "Пациент Иван Петров (СНИЛС: 123-456-789 00) страдает гипертонией.", "key_terms": [], "compliance_issues": [], "summary": "", "human_feedback": "", "is_approved": False,})print(result["summary"])Контрольные Точки Человека и Прерывания
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()graph = workflow.compile( checkpointer=checkpointer, interrupt_before=["review"])
thread_config = {"configurable": {"thread_id": "doc-review-001"}}result = graph.invoke(initial_state, config=thread_config)
current_state = graph.get_state(thread_config)print("Проблемы:", current_state.values["compliance_issues"])
graph.update_state( thread_config, {"human_feedback": "Одобрено после ручного редактирования", "is_approved": True})
final_result = graph.invoke(None, config=thread_config)Персистентность и Продакшн-развёртывание
from langgraph.checkpoint.postgres import PostgresSaver
with PostgresSaver.from_conn_string("postgresql://...") as checkpointer: graph = workflow.compile(checkpointer=checkpointer)
import asyncio
async def batch_process(documents: list[str]) -> list[dict]: return await asyncio.gather(*[ graph.ainvoke({"document": doc, "key_terms": [], "compliance_issues": [], "summary": "", "human_feedback": "", "is_approved": False}) for doc in documents ])Распространённые Паттерны и Ошибки
- Разветвление / Объединение: Параллельный запуск нескольких узлов через
Sendс объединением результатов. - Чрезмерно сложная маршрутизация: При слишком многих ветвях — разбить на подграфы.
- Раздутое состояние: Хранить ссылки (ID базы данных, ключи S3), а не большие артефакты.
- Изолированное тестирование узлов: Поскольку узлы — обычные функции, их можно юнит-тестировать напрямую.
def test_extract_terms_node(): state = { "document": "Искусственный интеллект быстро меняет современное общество.", "key_terms": [], "compliance_issues": [], "summary": "", "human_feedback": "", "is_approved": False, } result = extract_terms_node(state) assert "key_terms" in result assert isinstance(result["key_terms"], list)LangGraph заменяет специальные циклы явными, отлаживаемыми и возобновляемыми рабочими процессами. Начните с линейного графа (A → B → C → КОНЕЦ). Добавьте одно условное ребро, когда нужно ветвление. Добавьте контрольную точку человека, когда нужно одобрение. Большинству продакшн-агентов нужны именно эти три паттерна.
Связанные статьи
- Восстановление Агентов После Ошибок: 5 Паттернов для Продакшн-Надёжности
- Мультиагентные паттерны: оркестраторы, воркеры и конвейеры
- Отладка и Наблюдаемость в Системах Автономных Агентов
- Введение в агентную разработку