Что получится в результате
Соберем ИИ-агента на LangGraph как управляемый граф состояний. Агент будет принимать запрос, хранить state, вызывать LLM, выбирать tools, выполнять RAG-поиск, требовать approval для рискованных действий, сохранять checkpoints, писать логи и проходить тестовый сценарий.
В первой версии агент не должен быть бесконечным циклом "модель решила сама". Нужно явно описать state schema, nodes, edges, условия переходов, лимиты шагов, tool policy, обработку ошибок, human review и критерии завершения.
В результате будет рабочий MVP:
- проект лежит в `langgraph_agent_project`;
- настройки лежат в `.env`;
- состояние агента описано в `AgentState`;
- узлы графа описаны в `graph_nodes`;
- переходы описаны в `graph_edges`;
- tools описаны в `tool_registry`;
- права tools лежат в `tool_policy`;
- память сессии работает через checkpointer;
- долговременные факты описаны в `memory_store`;
- RAG-контекст приходит из `retrieval_node`;
- approval идет через `human_approval_node`;
- ошибки идут через `error_handler_node`;
- запуски пишутся в `agent_runs`;
- вызовы tools пишутся в `tool_call_log`;
- checkpoints пишутся в `checkpoint_store`;
- тесты лежат в `langgraph_test_cases`.
Финальная проверка: агент получает вопрос, ищет в базе знаний, выбирает безопасный tool, останавливается на approval для рискованного действия и возвращает ответ с понятным trace.
Что понадобится
Подготовьте:
- Python 3.11 или 3.12;
- виртуальное окружение;
- LangGraph;
- LangChain OpenAI или другой provider;
- LLM API key;
- embedding/vector store, если нужен RAG;
- список tools;
- список запрещенных действий;
- тестовые вопросы;
- отдельный staging API key;
- LangSmith или другой tracing, если нужен production-debug;
- базу данных или checkpointer для persistence.
Для первого запуска можно использовать in-memory checkpointer и простые tools, но структуру проекта сразу делайте такой, чтобы потом заменить память и store на production-хранилища.
Шаг 1. Опишите сценарий агента
Для MVP возьмем агента поддержки по базе знаний.
Он должен уметь:
- принять вопрос пользователя;
- классифицировать запрос;
- найти контекст в базе знаний;
- ответить по источникам;
- вызвать безопасный read-only tool;
- остановиться, если нужен write-tool;
- передать случай человеку;
- сохранить summary сессии;
- записать trace.
Не включайте в первый сценарий:
- платежи;
- удаление данных;
- массовые рассылки;
- изменение CRM-сделок;
- произвольный web browsing;
- shell-команды;
- долгие автономные циклы.
Проверка: сценарий можно нарисовать как граф из 6-10 узлов.
Шаг 2. Создайте проект
Команды:
mkdir langgraph_agent_project
cd langgraph_agent_project
python -m venv .venv
source .venv/bin/activate
pip install -U langgraph langchain-openai langchain-core python-dotenv pydantic
Для Windows:
python -m venv .venv
.\.venv\Scripts\Activate.ps1
pip install -U langgraph langchain-openai langchain-core python-dotenv pydantic
Структура:
langgraph_agent_project
app
graph.py
state.py
nodes.py
tools.py
policy.py
memory.py
prompts.py
tests.py
.env
requirements.txt
Проверка: `python -c "import langgraph"` завершается без ошибки.
Шаг 3. Создайте `.env`
Файл `.env`:
OPENAI_API_KEY=replace_me
AGENT_MODEL=gpt-4.1-mini
AGENT_MAX_STEPS=8
AGENT_ENABLE_WRITE_TOOLS=false
AGENT_ENV=staging
Правила:
- `.env` не попадает в Git;
- API key не передается в prompt;
- модель фиксируется в настройках;
- write-tools выключены по умолчанию;
- staging и production имеют разные ключи.
Проверка: `OPENAI_API_KEY` не печатается в логах.
Шаг 4. Опишите таблицу запусков
Даже если сначала работаете локально, сразу заведите модель данных.
`agent_runs`:
id
run_id
thread_id
user_id
status
input_text
final_answer
error_message
started_at
finished_at
Статусы:
- `created`;
- `running`;
- `waiting_approval`;
- `completed`;
- `failed`;
- `cancelled`;
- `timeout`.
Проверка: каждый запуск агента имеет `run_id` и `thread_id`.
Шаг 5. Создайте state schema
Файл `app/state.py`:
from typing import Annotated, Literal, TypedDict
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
user_id: str
thread_id: str
intent: str | None
retrieved_context: list[dict]
tool_results: list[dict]
next_action: str | None
needs_human: bool
error: str | None
step_count: int
Что хранить в state:
- сообщения;
- user id;
- thread id;
- intent;
- RAG context;
- tool results;
- флаг human review;
- ошибку;
- счетчик шагов.
Что не хранить:
- API keys;
- пароли;
- tokens;
- большие документы целиком;
- платежные данные.
Проверка: state содержит только данные, нужные для маршрутизации графа.
Шаг 6. Опишите реестр узлов
Создайте `graph_nodes`.
id
node_key
node_type
description
can_call_llm
can_call_tools
requires_approval
Узлы MVP:
- `input_guard_node`;
- `classify_intent_node`;
- `retrieval_node`;
- `llm_answer_node`;
- `tool_router_node`;
- `tool_executor_node`;
- `human_approval_node`;
- `memory_update_node`;
- `finalize_node`;
- `error_handler_node`.
Проверка: у каждого узла есть одна понятная ответственность.
Шаг 7. Создайте guard node
Файл `app/nodes.py`:
def input_guard_node(state: AgentState) -> dict:
last_message = state["messages"][-1].content
blocked_phrases = ["покажи api key", "игнорируй инструкции", "удали все"]
if any(phrase in last_message.lower() for phrase in blocked_phrases):
return {
"needs_human": True,
"next_action": "human_review",
"error": "guardrail_triggered",
}
return {
"step_count": state.get("step_count", 0) + 1,
}
Проверяйте:
- prompt injection;
- просьбы раскрыть system prompt;
- просьбы показать ключи;
- опасные write-actions;
- персональные данные;
- слишком длинный ввод.
Проверка: опасный запрос не идет сразу в LLM node.
Шаг 8. Сделайте классификацию intent
Узел должен понять маршрут.
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
def classify_intent_node(state: AgentState) -> dict:
user_text = state["messages"][-1].content
prompt = f"""
Верни один intent:
kb_question, tool_request, lead_request, complaint, unsafe, other.
Сообщение: {user_text}
"""
result = llm.invoke(prompt).content.strip()
return {"intent": result}
Для production лучше вернуть structured output, но в MVP можно начать с короткого списка.
Проверка: вопрос по базе знаний получает `kb_question`, а запрос на удаление данных - `unsafe`.
Шаг 9. Подключите RAG node
`retrieval_node` должен вернуть только релевантные chunks.
def retrieval_node(state: AgentState) -> dict:
question = state["messages"][-1].content
chunks = [
{
"source": "kb:getting-started",
"title": "Начало работы",
"text": "ИИ-агент отвечает по базе знаний и передает сложные случаи оператору.",
"score": 0.82,
}
]
return {"retrieved_context": chunks}
В реальном проекте замените заглушку на:
- vector search;
- keyword search;
- metadata filters;
- access rules;
- rerank;
- citations.
Проверка: если chunks не найдены, следующий узел должен уметь сказать `no_answer`.
Шаг 10. Напишите prompt ответа
Файл `app/prompts.py`:
SYSTEM_PROMPT = """
Ты ИИ-агент поддержки.
Отвечай только на основе переданного контекста.
Если ответа нет в контексте, скажи, что нужен человек.
Не выдумывай цены, сроки, условия и ссылки.
Не проси пароли, коды из SMS и токены.
Если нужен write-tool, поставь needs_human = true.
"""
Правила:
- RAG context отделен от memory context;
- sources передаются явно;
- отсутствие источника не маскируется;
- опасные темы уходят на handoff;
- ответ короткий.
Проверка: модель не отвечает по внутренним знаниям, если context пустой.
Шаг 11. Сделайте LLM node
from langchain_core.messages import AIMessage, SystemMessage
from app.prompts import SYSTEM_PROMPT
def llm_answer_node(state: AgentState) -> dict:
context = "\n\n".join(
f"[{i + 1}] {chunk['title']}\n{chunk['text']}\nSource: {chunk['source']}"
for i, chunk in enumerate(state.get("retrieved_context", []))
)
messages = [
SystemMessage(content=SYSTEM_PROMPT),
SystemMessage(content=f"Контекст:\n{context or 'Контекст не найден'}"),
*state["messages"],
]
response = llm.invoke(messages)
return {"messages": [AIMessage(content=response.content)]}
Проверка: ответ добавляется в `messages`, а не заменяет историю.
Шаг 12. Создайте tool registry
Файл `app/tools.py`:
from langchain_core.tools import tool
@tool
def search_order_status(order_id: str) -> str:
"""Read-only проверка статуса заказа."""
return f"Заказ {order_id}: тестовый статус processing"
@tool
def create_crm_task(summary: str) -> str:
"""Создает задачу менеджеру. Требует approval в MVP."""
return f"CRM task draft: {summary}"
TOOLS = {
"search_order_status": search_order_status,
"create_crm_task": create_crm_task,
}
Создайте `tool_registry`:
id
tool_name
description
is_readonly
requires_approval
timeout_seconds
is_active
Проверка: write-tool явно помечен как `requires_approval`.
Шаг 13. Добавьте tool policy
Файл `app/policy.py`:
TOOL_POLICY = {
"search_order_status": {
"enabled": True,
"requires_approval": False,
"max_calls_per_run": 3,
},
"create_crm_task": {
"enabled": True,
"requires_approval": True,
"max_calls_per_run": 1,
},
}
def can_call_tool(tool_name: str) -> bool:
return TOOL_POLICY.get(tool_name, {}).get("enabled", False)
def requires_approval(tool_name: str) -> bool:
return TOOL_POLICY.get(tool_name, {}).get("requires_approval", True)
Проверяйте:
- tool включен;
- не превышен лимит вызовов;
- write-tool требует approval;
- аргументы валидны;
- tool имеет timeout.
Проверка: модель не может вызвать tool, которого нет в registry.
Шаг 14. Сделайте router node для tools
Для MVP можно маршрутизировать по intent.
def tool_router_node(state: AgentState) -> dict:
if state.get("intent") == "tool_request":
return {"next_action": "call_tool"}
if state.get("intent") in ["complaint", "unsafe"]:
return {"next_action": "human_review", "needs_human": True}
return {"next_action": "answer"}
Проверка: complaint не идет в tool executor.
Шаг 15. Сделайте tool executor node
from app.policy import can_call_tool, requires_approval
from app.tools import TOOLS
def tool_executor_node(state: AgentState) -> dict:
tool_name = "search_order_status"
if not can_call_tool(tool_name):
return {"error": "tool_disabled", "needs_human": True}
if requires_approval(tool_name):
return {"next_action": "human_review", "needs_human": True}
result = TOOLS[tool_name].invoke({"order_id": "123"})
return {"tool_results": [{"tool": tool_name, "result": result}]}
В production:
- tool name берется из structured tool call;
- аргументы валидируются;
- timeout обязателен;
- результат логируется;
- ошибка не ломает весь graph.
Проверка: tool result попадает в state и виден следующему узлу.
Шаг 16. Создайте human approval node
from langgraph.types import Command, interrupt
def human_approval_node(state: AgentState) -> dict:
approval = interrupt({
"type": "approval_request",
"tool_name": state.get("tool_name"),
"tool_args": state.get("tool_args"),
"question": "Подтвердить выполнение write-tool?",
})
if approval.get("decision") != "approve":
return {
"needs_human": False,
"next_action": "finalize",
"messages": state["messages"],
"errors": state["errors"] + ["Write-tool отклонен человеком"],
}
return {
"needs_human": False,
"next_action": "tool_executor",
"messages": state["messages"],
}
Чтобы продолжить graph после паузы, вызывайте его с тем же `thread_id` и `Command(resume=...)`:
config = {"configurable": {"thread_id": "thread-123"}}
paused = graph.invoke(initial_state, config=config)
resumed = graph.invoke(
Command(resume={"decision": "approve"}),
config=config,
)
В реальном проекте этот узел:
- создает approval task;
- отправляет оператору summary;
- сохраняет proposed action;
- ставит run в `waiting_approval`;
- ждет resume;
- продолжает graph после решения.
Проверка: write-action не выполняется до подтверждения человека.
Шаг 17. Опишите graph edges
Создайте `graph_edges`.
id
from_node
to_node
condition_key
description
Переходы:
- `START -> input_guard_node`;
- `input_guard_node -> classify_intent_node`;
- `classify_intent_node -> retrieval_node`;
- `retrieval_node -> llm_answer_node`;
- `llm_answer_node -> tool_router_node`;
- `tool_router_node -> tool_executor_node`;
- `tool_router_node -> human_approval_node`;
- `tool_executor_node -> llm_answer_node`;
- `llm_answer_node -> memory_update_node`;
- `memory_update_node -> finalize_node`;
- `error_handler_node -> finalize_node`.
Проверка: в графе есть путь к завершению и нет бесконечного цикла без лимита.
Шаг 18. Соберите StateGraph
Файл `app/graph.py`:
from langgraph.graph import END, START, StateGraph
from app.state import AgentState
from app.nodes import (
input_guard_node,
classify_intent_node,
retrieval_node,
llm_answer_node,
tool_router_node,
tool_executor_node,
human_approval_node,
)
def route_after_router(state: AgentState) -> str:
action = state.get("next_action")
if action == "call_tool":
return "tool_executor"
if action == "human_review":
return "human_approval"
return "finalize"
builder = StateGraph(AgentState)
builder.add_node("input_guard", input_guard_node)
builder.add_node("classify_intent", classify_intent_node)
builder.add_node("retrieval", retrieval_node)
builder.add_node("llm_answer", llm_answer_node)
builder.add_node("tool_router", tool_router_node)
builder.add_node("tool_executor", tool_executor_node)
builder.add_node("human_approval", human_approval_node)
builder.add_node("finalize", lambda state: state)
builder.add_edge(START, "input_guard")
builder.add_edge("input_guard", "classify_intent")
builder.add_edge("classify_intent", "retrieval")
builder.add_edge("retrieval", "llm_answer")
builder.add_edge("llm_answer", "tool_router")
builder.add_conditional_edges("tool_router", route_after_router)
builder.add_edge("tool_executor", "llm_answer")
builder.add_edge("human_approval", END)
builder.add_edge("finalize", END)
graph = builder.compile()
Проверка: `graph` компилируется без ошибки.
Шаг 19. Запустите первый invoke
Создайте `app/main.py`:
from langchain_core.messages import HumanMessage
from app.graph import graph
result = graph.invoke({
"messages": [HumanMessage(content="Как работает агент поддержки?")],
"user_id": "u_1",
"thread_id": "thread_1",
"intent": None,
"retrieved_context": [],
"tool_results": [],
"next_action": None,
"needs_human": False,
"error": None,
"step_count": 0,
})
print(result["messages"][-1].content)
Команда:
python -m app.main
Проверка: агент возвращает ответ, а не падает на state schema.
Шаг 20. Добавьте checkpointer
Для short-term memory используйте checkpointer.
from langgraph.checkpoint.memory import InMemorySaver
checkpointer = InMemorySaver()
graph = builder.compile(checkpointer=checkpointer)
Запуск с thread:
config = {"configurable": {"thread_id": "thread_1"}}
graph.invoke(input_state, config=config)
В production замените in-memory вариант на постоянное хранилище.
Проверка: второй invoke с тем же `thread_id` видит историю checkpoint.
Шаг 21. Добавьте memory store
Для долговременной памяти используйте отдельный store, а не state.
`memory_store`:
id
user_id
memory_key
memory_value
memory_type
expires_at
created_at
Правила:
- preferences сохраняются после consent;
- секреты не сохраняются;
- state не раздувается;
- retrieval памяти ограничен top 3-5;
- пользователь может удалить память.
Проверка: память пользователя не смешивается между thread_id.
Шаг 22. Добавьте лимит шагов
Цикл tool executor -> LLM может стать бесконечным.
Добавьте проверку:
def max_steps_guard(state: AgentState) -> dict:
step_count = state.get("step_count", 0) + 1
if step_count > 8:
return {
"step_count": step_count,
"needs_human": True,
"error": "max_steps_exceeded",
"next_action": "human_review",
}
return {"step_count": step_count}
Проверяйте:
- max steps;
- max tool calls;
- max tokens;
- max cost;
- timeout.
Проверка: ошибка tool не заставляет агента бесконечно повторять вызов.
Шаг 23. Добавьте обработку ошибок
Создайте `error_handler_node`.
def error_handler_node(state: AgentState) -> dict:
return {
"needs_human": True,
"next_action": "human_review",
"messages": state["messages"],
}
Ошибки:
- LLM timeout;
- invalid JSON;
- disabled tool;
- tool timeout;
- retrieval unavailable;
- checkpoint error;
- rate limit;
- max steps.
Проверка: пользователь получает fallback, а trace содержит причину.
Шаг 24. Добавьте structured output
Для решений graph лучше использовать строгий формат.
from pydantic import BaseModel
class AgentDecision(BaseModel):
answer: str
next_action: str
needs_human: bool
confidence_score: float
used_sources: list[str]
Проверяйте:
- JSON валидный;
- `confidence_score` от 0 до 1;
- `next_action` из списка;
- sources не пустые для RAG;
- `needs_human` true для risky cases.
Проверка: свободный текст вместо JSON не управляет графом.
Шаг 25. Добавьте streaming
Streaming нужен для UI, но не должен ломать state.
Используйте streaming только для ответа пользователю:
- graph state обновляется после завершения шага;
- partial tokens не пишутся как final answer;
- tool calls не показываются пользователю сырыми;
- error закрывает stream fallback-сообщением;
- trace сохраняет полный результат.
Проверка: при обрыве stream run получает понятный статус.
Шаг 26. Добавьте tracing
Создайте `agent_trace_log`.
id
run_id
node_key
input_hash
output_hash
duration_ms
status
created_at
Логируйте:
- вход в узел;
- выход из узла;
- routing decision;
- tool call;
- retrieval;
- approval wait;
- errors;
- final answer.
Можно подключить LangSmith, если нужен визуальный trace.
Проверка: по `run_id` видно, через какие nodes прошел агент.
Шаг 27. Добавьте eval cases
Создайте `langgraph_test_cases`.
id
case_key
input_text
expected_route_json
expected_answer_contains_json
forbidden_tools_json
is_critical
Минимальные тесты:
- вопрос по базе знаний;
- отсутствие ответа;
- read-only tool;
- write-tool требует approval;
- prompt injection;
- max steps;
- tool timeout;
- retrieval unavailable;
- memory preference;
- complaint handoff.
Проверка: critical tests проходят перед deploy.
Шаг 28. Прогоните smoke test
Сценарий:
- запустить graph;
- задать вопрос по базе знаний;
- проверить `retrieval_node`;
- проверить `llm_answer_node`;
- запросить read-only tool;
- проверить `tool_executor_node`;
- запросить write-tool;
- проверить `human_approval_node`;
- проверить checkpoint;
- проверить trace.
Проверка: smoke test проходит без ручной правки state.
Шаг 29. Подготовьте API wrapper
Для production заверните graph в API.
Endpoint:
POST /api/agent/run
Вход:
{
"thread_id": "thread_1",
"user_id": "u_1",
"message": "Как подключить агента к CRM?"
}
Ответ:
{
"run_id": "run_1",
"status": "completed",
"answer": "..."
}
Правила:
- API проверяет auth;
- thread_id валидируется;
- graph вызывается с config;
- errors не раскрывают stack trace;
- run пишется в базу.
Проверка: API может вызвать graph несколько раз в одном thread.
Шаг 30. Подготовьте production persistence
Для production нужны постоянные хранилища.
Замените:
- in-memory checkpointer на Postgres или другое хранилище;
- in-memory store на базу;
- локальные заглушки tools на backend services;
- print logs на structured logs;
- ручные tests на regression suite.
Создайте `checkpoint_store`:
id
thread_id
checkpoint_id
state_json
created_at
Проверка: после перезапуска процесса thread можно продолжить.
Шаг 31. Минимальный результат для запуска
MVP готов, если выполнены условия:
- проект создан;
- `.env` настроен;
- `AgentState` описан;
- nodes имеют одну ответственность;
- edges компилируются;
- graph запускается;
- RAG node возвращает context;
- LLM node отвечает по context;
- tool registry есть;
- tool policy работает;
- write-tool требует approval;
- checkpointer работает;
- memory store не смешивает пользователей;
- max steps защищает от циклов;
- errors уходят в handler;
- structured output валидируется;
- trace пишется;
- eval cases есть;
- smoke test пройден;
- API wrapper готов.
Финальная проверка: запустите три сценария: обычный вопрос, read-only tool и write-tool. Первый должен завершиться ответом, второй - tool result, третий - human approval без выполнения опасного действия.
Что нельзя автоматизировать в первой версии
В первой версии не автоматизируйте:
- shell-команды;
- произвольный web browsing;
- платежи;
- возвраты;
- изменение CRM без approval;
- удаление данных;
- бесконечные циклы tools;
- запись секретов в state;
- хранение полного transcript без TTL;
- автоматическое изменение tool policy;
- auto-deploy graph без evals;
- выполнение write-tools из prompt;
- раскрытие trace пользователю;
- запуск без max steps;
- production без persistent checkpoint.
Сначала LangGraph должен дать контроль над маршрутом, состоянием и безопасными tools. Уже потом добавляйте multi-agent, параллельные ветки, сложную память и долгие автономные процессы.
Частые вопросы
LangGraph подходит новичкам?
LangGraph требует базового Python и понимания stateful workflow. Если нужен быстрый no-code прототип, проще начать с n8n или Flowise. Если нужен контроль над состоянием, ветвлениями и тестами, LangGraph подходит лучше.
Чем LangGraph отличается от LangChain?
LangChain дает компоненты для моделей, prompts, tools и интеграций. LangGraph фокусируется на orchestration: state, nodes, edges, циклы, persistence, human-in-the-loop и управляемые agent workflows.
Можно ли использовать LangGraph без RAG?
Да. RAG нужен только если агент должен отвечать по документам или базе знаний. LangGraph можно использовать и для tool workflows, классификации, multi-step обработки заявок и approval-сценариев.
Где хранить память LangGraph-агента?
Краткосрочный контекст храните через checkpointer по `thread_id`. Долговременную память храните отдельно в `memory_store` с consent, TTL, safety checks и удалением.
Когда нужен human approval?
Approval нужен для write-tools, CRM-изменений, платежей, удаления данных, жалоб, персональных данных и любых действий, где ошибка агента может повлиять на пользователя или бизнес.