Стандартная семичастная структура для каждого агента LangGraph.
Фиксированная структура файлов для агентов LangGraph, включающая импорты и состояние, инструменты, узлы, рёбра, процессы сборки и вызова, шаблоны, а также расширение для работы с несколькими агентами.
Источники: The LangGraph Mental Model: A standardized architecture guide for every agent you’ll ever build
Каждый, кто некоторое время изучал LangGraph, знает это странное ощущение: идея кажется логичной, но код так и не приходит в голову. Граф содержит узлы, узлы выполняют функции, рёбра определяют направление движения, а состояние хранит информацию. Всё это можно набросать на доске за пять минут. Но как только вы открываете пустой файл, ничего не появляется. Вы забываете о необходимых импортах, класс, который вам нужен, не имеет очевидного местоположения, и, кажется, существует пять способов написания одного и того же кода.
Почему код кажется сложнее, чем сама концепция
Основная причина заключается в том, что LangGraph не навязывает своей структуры. Он предоставляет мощные строительные блоки и оставляет выбор их расположения за вами. Каждый учебник и каждый репозиторий описывают всё по-разному, поэтому ваша память так и не фиксируется на какой-то одной схеме.
Предложенное здесь решение заключается в выборе одного стандартного формата и использовании его для каждого агента, будь то крошечный чат-бот или система с множеством взаимодействующих агентов. Можно представить себе это как шаблон, который можно запомнить и без усилий вспомнить, подобно диаграммам, показывающим, что уже сделал агент, только здесь речь идет о исходном коде, а не о истории выполнения.
Весь файл сразу
Каждое приложение LangGraph может быть написано в виде одного файла с семью разделами в фиксированном порядке, сверху вниз:
langgraph_agent.py
│
├── MODULE 1: IMPORTS & CONFIGURATION
│ └── All your libraries, API keys, model setup
│
├── MODULE 2: STATE
│ └── The TypedDict that defines your agent's memory
│
├── MODULE 3: TOOLS (optional, but common)
│ └── Functions decorated with @tool that the LLM can call
│
├── MODULE 4: NODES
│ └── Functions that do the actual work at each graph step
│
├── MODULE 5: EDGES & ROUTING
│ └── Functions that decide what happens next
│
├── MODULE 6: GRAPH ASSEMBLY
│ └── Where you build, wire, and compile the graph
│
└── MODULE 7: ENTRYPOINT
└── The __main__ block or invoke() call that runs everything
Это скелет структуры. Когда вы начинаете проект, откройте файл и введите семь заголовков разделов в виде комментариев, затем заполняйте их по одному. Далее в этом тексте рассматривается каждый раздел: для чего он предназначен, какие имена необходимо запомнить, а также предоставляется шаблон для копирования.
Раздел 1: импорты и настройки
Для чего он предназначен
Считайте этот раздел набором инструментов. Плотник сначала раскладывает все инструменты перед началом работы, а здесь вы указываете Python, какие внешние возможности вам нужны, и настраиваете модель, которую будет использовать агент.
Имена, которые следует знать
ChatOpenAIилиChatAnthropic— это класс языковой модели, «мозг» агента. Создайте его здесь один раз и используйте повторно везде.StateGraph— это основной класс, который формирует граф, подобно чертежу, который вы рисуете.END— это специальная константа, обозначающая конечную точку. Ребро, ведущее к ней, прерывает выполнение.MemorySaver— это указатель проверки, который сохраняет данные в памяти на протяжении нескольких этапов диалога.
Шаблон
# --- Standard Library ---
import os
from typing import TypedDict, Annotated, Literal
# --- LangChain Core ---
from langchain_openai import ChatOpenAI # or ChatAnthropic, ChatGroq, etc.
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
# --- LangGraph Core ---
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages # The message reducer
from langgraph.prebuilt import ToolNode # Pre-built node for tool execution
from langgraph.checkpoint.memory import MemorySaver
# --- Configuration ---
# Always name your model variable 'llm' - it's the standard in every node
llm = ChatOpenAI(
model="gpt-4o", # or "claude-3-5-sonnet-20241022", etc.
temperature=0, # 0 = deterministic; raise for creativity
api_key=os.environ.get("OPENAI_API_KEY")
)
Почему структура такая
Импорты разделены на четыре группы: стандартная библиотека, LangChain (слой моделей), LangGraph (слой графов) и настройки. Это соответствует структуре самих библиотек, где LangChain предоставляет примитивы ИИ, а LangGraph отвечает за оркестрацию. Разделение групп позволяет быстро понять, что находится где, когда возникают проблемы.
Самой полезной привычкой здесь является использование имени модели llm. Все функции узлов впоследствии будут ссылаться на эту переменную, и если в одном проекте используется имя model, а в другом — chat, код теряет единообразие и становится сложнее для просмотра.
Раздел 2: состояние
Для чего оно нужно
State представляет собой рабочую память агента — единственный источник истины, который распространяется по всему графу. Каждый узел может читать из неё и записывать в неё данные, в то время как движок управляет её жизненным циклом. Из всего, что существует в LangGraph, правильная реализация State, вероятно, является самой важной.
Хорошей аналогией может служить доска объявлений в офисе: кто бы ни вошёл в комнату, то есть каждый узел, может прочитать написанное там и что-то добавить, причём доска остаётся нетронутой на протяжении всей встречи, что соответствует выполнению всего графа.
Имена, которые стоит знать
TypedDict— это класс на Python, описывающий структуру состояния, похожий на схему. Ваш класс State наследуется от него.Annotated— это обёртка для указателей типов. LangGraph использует её для привязки редьюсера к полю, то есть функции, определяющей, как новое значение объединяется с уже существующим.
add_messages — встроенный редьюсер для поля messages. Вместо замены списка каждый раз, когда узел добавляет сообщение, он добавляет его в конец. Именно этот редьюсер вы будете использовать чаще всего.operator.add — ещё один распространённый редьюсер, предназначенный для полей, которые должны накапливаться путём сложения, таких как список результатов или счётчик.Шаблон
# ============================================================
# MODULE 2: STATE
# ============================================================
class AgentState(TypedDict):
# 'messages' is the heartbeat of almost every LangGraph agent.
# Annotated[list, add_messages] means: "this is a list, and when
# a node writes to it, append - don't replace."
messages: Annotated[list[BaseMessage], add_messages]
# Add custom fields below for your specific agent's needs.
# Fields without a reducer are REPLACED each time a node writes to them.
# Example: a simple string field (gets replaced each write)
current_task: str
# Example: a list you want to accumulate (use operator.add as reducer)
# results: Annotated[list[str], operator.add]
# Example: a counter
# iteration_count: int
О редьюсерах
Редьюсеры сбивают с толку многих начинающих, поэтому вот аналогия. Представьте состояние как бумажный формуляр, который передают по офису. Поле messages — это блок листочков с наклейками: каждый, кто работает с формуляром, добавляет наклейку, и никто не удаляет предыдущие. Это и есть add_messages.
Поле без редьюсера — это отдельное текстовое поле. Каждый человек перезаписывает то, что написал предыдущий, что является стандартным поведением LangGraph: побеждает последняя запись.
Существует простое правило: всегда указывайте для поля messages Annotated[list, add_messages], для каждого дополнительного списка, который должен продолжать расти, выбирайте Annotated[list, operator.add], а все остальные поля оставляйте без изменений.
Раздел 3: инструменты
Для чего он нужен
Инструменты — это руки агента. LLM является мозгом, который рассуждает и принимает решения, но сам по себе не может действовать в реальном мире. Благодаря инструментам он может просматривать веб, обращаться к API, выполнять код, искать информацию в базе данных или запускать любую функцию на Python, которую вы определите.
Этот раздел является необязательным. Агент, который создаёт только текст и никогда не взаимодействует с внешними системами, может его пропустить, хотя большинству реальных агентов нужны инструменты.
Имена, которые стоит знать
@tool— это декоратор LangChain, преобразующий обычную функцию на Python в инструмент, понятный LangGraph. Документация функции становится описанием, которое читает LLM при принятии решения о её использовании.ToolNode— это готовый узел, доступный вlanggraph.prebuilt, который запускает те инструменты, которые выбрал LLM, так что вам не нужно писать логику выполнения самостоятельно.
bind_tools — это метод в вашем экземпляре LLM, который привязывает к нему инструменты. После этого модель знает, какие инструменты доступны, и может запросить их использование, отправляя специальное сообщение tool_calls вместо обычного текста.Шаблон
# ============================================================
# MODULE 3: TOOLS
# ============================================================
@tool
def search_web(query: str) -> str:
"""Search the web for current information about a topic.
Use this when you need real-time information that is not in
your training data, such as recent news or live prices.
Args:
query: The search query string.
Returns:
A string containing search results.
"""
# Your actual implementation here (e.g., Tavily, SerpAPI, etc.)
# Placeholder for illustration:
return f"Search results for: {query}"
@tool
def calculate(expression: str) -> str:
"""Evaluate a mathematical expression and return the result.
Use this for any arithmetic, algebra, or numerical computation.
Args:
expression: A valid Python math expression as a string, e.g. '2 + 2 * 10'
Returns:
The computed result as a string.
"""
try:
return str(eval(expression))
except Exception as e:
return f"Error: {e}"
# Collect all tools into a list - this is the pattern you always follow
tools = [search_web, calculate]
# Bind tools to the LLM so it knows they exist and can choose to call them
llm_with_tools = llm.bind_tools(tools)
# Create the pre-built ToolNode that will execute tool calls automatically
tool_node = ToolNode(tools)
Документация имеет большее значение, чем кажется
Новички часто упускают это из виду: модель опирается на вашу документацию для выбора инструмента. Она читает текст внутри тройных кавычек. Если документация нечеткая или отсутствует, большая языковая модель не будет знать, является ли ваш инструмент подходящим, поэтому пишите четкие, конкретные описания, в которых указано что делает инструмент и когда его использовать.
Раздел 4: узлы
Для чего они нужны
Узлы — это исполнители задач. Каждый из них представляет собой функцию на Python, которая принимает текущее состояние, выполняет какие-либо действия, такие как вызов большой языковой модели, обработка данных или использование инструмента, и возвращает словарь с обновлениями состояния. Это и есть полное описание задачи.
Узел никогда не решает, что произойдет дальше. Он выполняет свою работу и обновляет состояние, а дальнейшая обработка происходит в разделе 5.
Имена, которые стоит знать
state— это параметр, который получает каждый узел, представляющий собой словарь, построенный по образцуAgentState. В нем можно читать такие значения, какstate["messages"]илиstate["current_task"].HumanMessage,AIMessageиSystemMessage— это три типа сообщений, которые вы будете использовать постоянно. Первый — это то, что сказал пользователь, второй — ответ модели, а третий содержит ваши инструкции для модели, то есть системный промпт.invoke— это способ вызова LLM илиllm_with_toolsдля получения ответа. Вы передаете список сообщений и получаетеAIMessage.
AgentState. Его значения — это обновления, а не полное состояние, и LangGraph объединяет их с помощью редукторов, которые вы определили.Шаблон
# ============================================================
# MODULE 4: NODES
# ============================================================
# ── Node: Agent (the reasoning brain) ───────────────────────
def agent_node(state: AgentState) -> dict:
"""The central reasoning node. Calls the LLM and decides
whether to respond or call a tool."""
# Build the message list to send to the LLM.
# Always include a system message to set behavior.
system_prompt = SystemMessage(content=(
"You are a helpful assistant. Use the available tools "
"when you need real-time information or computation. "
"Respond clearly and concisely."
))
# The LLM receives the system prompt + all previous messages in state
messages_to_send = [system_prompt] + state["messages"]
# Call the LLM. Use llm_with_tools if you have tools; plain llm if not.
response = llm_with_tools.invoke(messages_to_send)
# Return the LLM's response as a state update.
# add_messages will APPEND this AIMessage to state["messages"].
return {"messages": [response]}
# ── Node: Summarizer (example of a non-LLM processing node) ─
def summarize_node(state: AgentState) -> dict:
"""Summarizes the conversation so far to keep context short.
This shows that nodes don't have to call an LLM - they can
do any Python processing."""
all_messages = state["messages"]
# Summarize with the LLM (a different prompt, same LLM)
summary_prompt = [
SystemMessage(content="Summarize the following conversation in 2-3 sentences."),
HumanMessage(content=str(all_messages))
]
summary_response = llm.invoke(summary_prompt)
# Replace messages with a fresh start containing just the summary
return {
"messages": [AIMessage(content=f"[Summary] {summary_response.content}")]
}
Изображение узлов
Представьте узел как бегуна-передатчика. Он берёт эстафету, которая представляет собой состояние, пробегает свой отрезок дистанции, то есть выполняет работу, и передаёт эстафету с обновлёнными данными. Он не выбирает следующего бегуна — это задача того, кто управляет гонкой, а именно рёбер.
Формат каждой функции узла одинаков: def my_node(state: AgentState) -> dict. Входным параметром является полное состояние, а выходным — словарь, содержащий только те поля, которые необходимо изменить.
Раздел 5: рёбра и маршрутизация
Для чего это нужно
Рёбра представляют собой логику принятия решений графа. Они соединяют узлы между собой и определяют порядок выполнения операций. В LangGraph существует два вида рёбер.
Статические рёбра соединяют один узел с другим без каких-либо условий: «после сумматора — к агенту». Никаких решений, просто прямая связь.
Условные рёбра анализируют текущее состояние и возвращают строку с именем следующего узла. Именно здесь заключается интеллект агента относительно своего собственного потока: следует ли ему вызвать инструмент, завершить действие или вернуться для повторной попытки?
Имена, которые стоит знать
add_edge(A, B)создаёт статическое ребро, то есть управление всегда идёт от A к B.add_conditional_edges(source, routing_function, mapping)создаёт ветвь, исходящую изsource. Функция маршрутизации принимает текущее состояние и возвращает строку, а словарьmappingпреобразует эту строку в реальное имя следующего узла.
tools_condition — это готовая функция маршрутизации из langgraph.prebuilt. Она проверяет последнее сообщение в состоянии на наличие вызовов инструментов. Если таковые имеются, она направляет поток к "tools", а в противном случае — к END. Это стандартный шаблон для агентов, использующих инструменты, и его стоит запомнить.START — это константа, обозначающая начало графа. Установка точки входа означает добавление ребра от START к первому узлу.Шаблон
# ============================================================
# MODULE 5: EDGES & ROUTING
# ============================================================
# Import the pre-built tool routing function
from langgraph.prebuilt import tools_condition
# ── Custom Routing Function Example ─────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "summarize", "__end__"]:
"""Custom router for the agent node.
Routing functions always:
1. Receive the current state as input
2. Return a string that maps to the next node (or END)
The return values must match the keys in add_conditional_edges' mapping.
"""
last_message = state["messages"][-1] # Look at what the LLM just said
# Case 1: The LLM decided to call a tool
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
# Case 2: The conversation is getting long - summarize before continuing
if len(state["messages"]) > 20:
return "summarize"
# Case 3: The LLM gave a direct answer - we're done
return "__end__" # LangGraph's internal name for END
Изображение для маршрутизации
Представьте функцию маршрутизации как диспетчера железнодорожных сигналов. Поезд прибывает на станцию, когда завершается работа узла. Диспетчер проверяет информацию о текущем состоянии и переключает соответствующий сигнал, чтобы поезд поехал по правильному пути. Диспетчер никогда не перемещает поезд; это делает LangGraph.
Одно правило, которое следует помнить: функции маршрутизации никогда не изменяют состояние. Они только его читают. Любые изменения должны происходить в узле, поэтому если вы хотите обновить состояние во время маршрутизации, перенесите эту логику в узел.
Раздел 6: сборка графа
Для чего он нужен
Здесь все соединяется воедино. Рассматривайте этот раздел как этап сборки. Разделы с 1 по 5 были посвящены подготовке: определению материалов, их нарезке и раскладке, а теперь вы собираете структуру, соединяя элементы и компилируя результат.
Сборка всегда проходит по тем же пяти шагам, которые стоит запомнить: инициализация, регистрация узлов, установка точки входа, соединение рёбер, компиляция.
Названия, которые нужно знать
StateGraph(AgentState)создаёт инструмент сборки на основе вашего класса State в качестве схемы, поэтому каждый узел должен возвращать обновления, совместимые с ней.add_node(name, function)регистрирует узел. Имя — это строка, которая будет использоваться в рёбрах, а функция — это узел из раздела 4.set_entry_point(name)указывает, какой узел будет выполняться первым при вызове графа.add_edge(A, B)создаёт прямую, безусловную связь от A к B.add_conditional_edges(source, router, mapping)добавляет условную ветвь от исходного узла с использованием функции маршрутизации и сопоставления возвращаемых строк с именами пунктов назначения.compile(checkpointer=...)преобразует граф в объект, готовый к выполнению (CompiledGraph). После этого добавлять узлы и рёбра невозможно. Опциональный параметр checkpointer включает режим постоянной памяти.
Шаблон
# ============================================================
# MODULE 6: GRAPH ASSEMBLY
# ============================================================
# ── Step 1: Initialize ──────────────────────────────────────
# Always pass your State class to StateGraph
graph_builder = StateGraph(AgentState)
# ── Step 2: Register All Nodes ──────────────────────────────
# Format: add_node("node_name_as_string", node_function)
# The string name is what you use in ALL edge definitions
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("tools", tool_node) # The pre-built ToolNode from Module 3
graph_builder.add_node("summarize", summarize_node)
# ── Step 3: Set Entry Point ─────────────────────────────────
# Which node runs first when we invoke the graph?
graph_builder.set_entry_point("agent")
# ── Step 4: Wire the Edges ──────────────────────────────────
# Conditional edge from agent: check if we need tools, a summary, or we're done
graph_builder.add_conditional_edges(
"agent", # Source node
should_continue, # Routing function from Module 5
{
"tools": "tools", # If router returns "tools" → go to tools node
"summarize": "summarize", # If router returns "summarize" → go to summarize node
"__end__": END, # If router returns "__end__" → stop the graph
}
)
# Static edge: after tools run, always go back to agent (the ReAct loop)
graph_builder.add_edge("tools", "agent")
# Static edge: after summarization, always return to agent
graph_builder.add_edge("summarize", "agent")
# ── Step 5: Compile ─────────────────────────────────────────
# Without checkpointer: no persistent memory (stateless per invocation)
# With checkpointer: memory persists across turns (stateful conversations)
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
Иллюстрация процесса сборки
Лучшим примером для сравнения служит печатная плата. Каждый вызов add_node добавляет компонент. Функции add_edge и add_conditional_edges соединяют провода между компонентами. Функция compile() выполняет окончательную проверку и готовит плату к использованию. После этого она готова к подаче питания, что осуществляется с помощью функции invoke, и к выполнению своих функций.
Шаблон включает в себя основной цикл ReAct: агент → инструменты → агент → инструменты и так далее, пока агент не решит, что работа завершена, и не перейдёт к точке END. Этот цикл «анализ → действие» лежит в основе практически всех агентов LangGraph, использующих инструменты, и понимание его помогает разобраться в большинстве графов, с которыми вы столкнётесь.
Раздел 7: точка входа и вызов
Для чего это нужно
Это площадка запуска. Всё, что было ранее, — это определение, а этот раздел представляет собой выполнение: код, который запускает граф, подаёт входные данные и собирает выходные.
Существует два способа его запуска: invoke для однократного обработки и потоковый режим для постепенного вывода результата. Также требуется объект config — словарь с параметрами выполнения, в частности thread_id, используемый для управления памятью.
Имена, которые стоит знать
graph.invoke(input, config)выполняет весь граф и возвращает конечное состояние. Поскольку это синхронная операция, результат будет получен только после завершения всех шагов.graph.stream(input, config)также выполняет граф, но возвращает частичные обновления состояния по мере их появления, что идеально подходит для приложений, предназначенных для пользователей, которым необходимо отображать текущий прогресс.
config — это словарь, передаваемый при любом вызове. Его ключевая запись имеет вид {"configurable": {"thread_id": "some_id"}}. ID потока функционирует как ID сессии, указывающий компоненту проверки, какую память восстанавливать и сохранять. Два пользователя с разными ID потоков никогда не делят память.HumanMessage(content=...) — это способ обертки ввода пользователя перед передачей его в граф.Шаблон
# ============================================================
# MODULE 7: ENTRYPOINT & INVOCATION
# ============================================================
if __name__ == "__main__":
# ── Config: defines this conversation's memory session ──
# Change thread_id to start a fresh conversation.
# Keep the same thread_id to continue an existing one.
config = {"configurable": {"thread_id": "user-session-001"}}
# ── Single Invocation (synchronous) ─────────────────────
user_input = "What is the current price of Bitcoin?"
result = graph.invoke(
input={"messages": [HumanMessage(content=user_input)]},
config=config
)
# The result is the final state dictionary.
# Access the last message to get the agent's final answer.
final_answer = result["messages"][-1].content
print(f"Agent: {final_answer}")
# ── Streaming Invocation (for real-time output) ──────────
for chunk in graph.stream(
input={"messages": [HumanMessage(content=user_input)]},
config=config,
stream_mode="values" # Yields the full state after each node runs
):
# Each chunk is a state snapshot. The last message shows progress.
latest = chunk["messages"][-1]
if hasattr(latest, "content") and latest.content:
print(f"[Streaming] {latest.content}")
# ── Multi-turn Conversation Loop ─────────────────────────
print("\n--- Starting Interactive Session ---")
while True:
user_text = input("You: ").strip()
if user_text.lower() in ("exit", "quit", "bye"):
break
response = graph.invoke(
input={"messages": [HumanMessage(content=user_text)]},
config=config # Same config = same memory thread
)
print(f"Agent: {response['messages'][-1].content}\n")
Полная шаблонная структура
Ниже приведена полная, минимальная структура, которую можно скопировать в новый проект. Это единственный файл, который стоит сохранить в закладках: замените примеры реализации на собственную логику, и у вас будет основа любого агента LangGraph.
# ============================================================
# LANGGRAPH CANONICAL AGENT TEMPLATE
# Modules: Imports → State → Tools → Nodes → Edges → Assembly → Entrypoint
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
from typing import TypedDict, Annotated, Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, AIMessage, SystemMessage, BaseMessage
from langchain_core.tools import tool
from langgraph.graph import StateGraph, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ── MODULE 2: STATE ─────────────────────────────────────────
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
# Add your custom fields here
# ── MODULE 3: TOOLS ─────────────────────────────────────────
@tool
def my_tool(input: str) -> str:
"""Describe clearly what this tool does and when the LLM should use it."""
return f"Result for: {input}"
tools = [my_tool]
llm_with_tools = llm.bind_tools(tools)
tool_node = ToolNode(tools)
# ── MODULE 4: NODES ─────────────────────────────────────────
def agent_node(state: AgentState) -> dict:
"""The reasoning node. Calls the LLM, optionally triggers tool calls."""
messages = [SystemMessage(content="You are a helpful assistant.")] + state["messages"]
response = llm_with_tools.invoke(messages)
return {"messages": [response]}
# ── MODULE 5: EDGES & ROUTING ───────────────────────────────
def should_continue(state: AgentState) -> Literal["tools", "__end__"]:
"""Decide: did the LLM call a tool, or did it give a final answer?"""
last_message = state["messages"][-1]
if hasattr(last_message, "tool_calls") and last_message.tool_calls:
return "tools"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ────────────────────────────────
graph_builder = StateGraph(AgentState)
graph_builder.add_node("agent", agent_node)
graph_builder.add_node("tools", tool_node)
graph_builder.set_entry_point("agent")
graph_builder.add_conditional_edges(
"agent",
should_continue,
{"tools": "tools", "__end__": END}
)
graph_builder.add_edge("tools", "agent")
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
# ── MODULE 7: ENTRYPOINT ────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "session-001"}}
while True:
user_text = input("You: ").strip()
if not user_text or user_text.lower() in ("exit", "quit"):
break
response = graph.invoke(
{"messages": [HumanMessage(content=user_text)]},
config=config
)
print(f"Agent: {response['messages'][-1].content}\n")
Дальнейшее изучение: многопроцессорные системы
Когда подход с одним агентом кажется естественным, многопроцессорные системы уже не пугают. Основная идея заключается в том, что каждый агент представляет собой просто скомпилированную графовую структуру, а такая структура может сама стать узлом в более крупной графовой структуре-контроллере.
Термины, важные для работы с многопроцессорными системами
- Узел-контроллер — это агент, задача которого не заключается в ответе пользователю, а в выборе какого специалиста следует привлечь к выполнению задачи. Он осуществляет маршрутизацию между подагентами.
- Подграф — это скомпилированная графовая структура, зарегистрированная в качестве узла в другой графовой структуре. Он принимает и возвращает состояние, как и любой обычный узел.
Command — это более новый паттерн, доступный с версии LangGraph 0.2, при котором один узел возвращает как обновление состояния, так и инструкцию маршрутизации в одном значении. В некоторых конфигурациях с несколькими агентами это устраняет необходимость в отдельных условных ребрах.Структурный шаблон
# ── MULTI-AGENT PATTERN ─────────────────────────────────────
# Each specialist is a compiled graph (a subgraph)
# Sub-agent 1: A researcher
researcher_graph = StateGraph(AgentState)
# ... (built with its own nodes, edges, and tools)
researcher = researcher_graph.compile()
# Sub-agent 2: A writer
writer_graph = StateGraph(AgentState)
# ... (built with its own nodes, edges, and tools)
writer = writer_graph.compile()
# ── SUPERVISOR NODE ─────────────────────────────────────────
def supervisor_node(state: AgentState) -> dict:
"""Decides which sub-agent should handle the current task."""
# The supervisor LLM decides: "researcher" or "writer" or "FINISH"
response = supervisor_llm.invoke(state["messages"])
return {"messages": [response], "next_agent": response.content}
def route_to_agent(state: AgentState) -> Literal["researcher", "writer", "__end__"]:
"""Routes to the appropriate sub-agent based on supervisor's decision."""
return state.get("next_agent", "__end__")
# ── SUPERVISOR GRAPH ────────────────────────────────────────
supervisor_builder = StateGraph(AgentState)
supervisor_builder.add_node("supervisor", supervisor_node)
supervisor_builder.add_node("researcher", researcher) # Subgraph as a node!
supervisor_builder.add_node("writer", writer) # Subgraph as a node!
supervisor_builder.set_entry_point("supervisor")
supervisor_builder.add_conditional_edges(
"supervisor",
route_to_agent,
{"researcher": "researcher", "writer": "writer", "__end__": END}
)
supervisor_builder.add_edge("researcher", "supervisor")
supervisor_builder.add_edge("writer", "supervisor")
supervisor_graph = supervisor_builder.compile(checkpointer=MemorySaver())
Версия с несколькими агентами имеет ту же структуру, что и версия с одним агентом, и применяются те же семь разделов. Единственное отличие заключается в том, что некоторые из «узлов» представляют собой полностью скомпилированные графы. Ментальная модель масштабируется без проблем.
Краткий справочник
Здесь представлена краткая таблица, которую можно распечатать или прикрепить, чтобы собрать основные идеи в одном месте.
- Состояние:
TypedDictопределяет его структуру.Annotated[T, reducer]привязывает функцию-редуктор к определенному полю.add_messages— это стандартный редуктор для списка сообщений, который добавляет элементы в конец, а не заменяет их.operator.addиспользуется для суммирования списков или чисел. - LLM:
ChatOpenAIилиChatAnthropic— это класс, всегда связанный с именемllm. Функцияbind_tools(tools)сообщает модели о доступных инструментах. Функцияinvoke(messages)вызывает модель и возвращает ответ. - Messages:
HumanMessageсодержит ввод пользователя,AIMessage— вывод модели, аSystemMessage— ваши инструкции. Все они всегда передаются в функциюinvokeв виде списка. - Tools: Аннотация
@toolделает функцию на Python доступной для вызова моделью, причем документация служит её описанием. ФункцияToolNode(tools)автоматически выполняет вызовы инструментов. Функцияtools_conditionпроверяет, запросила ли модель использование инструмента. - Граф:
StateGraph(State)создаёт конструктор;add_node(name, fn)регистрирует узел,set_entry_point(name)выбирает первый узел,add_edge(A, B)создаёт безусловную связь,add_conditional_edges(source, router, mapping)создаёт связь, выбранную по значению возврата маршрутизатора,compile(checkpointer=...)завершает формирование графа, аENDобозначает конечную точку. - Вызов:
graph.invoke(input, config)возвращает итоговое состояние,graph.stream(input, config)постепенно передаёт обновления,config = {"configurable": {"thread_id": "..."}}идентифицирует сессию памяти, аMemorySaver()представляет собой контрольный указатель в памяти.
Превращение в автоматическую привычку
Цель не в том, чтобы обучать LangGraph с нуля. Речь идет о создании для вашего мозга стабильной основы, которую он может автоматически использовать, так же как музыкант берет в руки знакомую последовательность аккордов или повар раскладывает ингредиенты перед началом работы.
Семь разделов — импорты, состояние, инструменты, узлы, ребра, сборка и точка входа — образуют эту структуру. Названия внутри каждого раздела соответствуют нотам, а полная схема представляет собой нотную запись.
Как простой чат-бот, так и конвейер с десятками узлов могут использовать одну и ту же структуру. Сложность возрастает, но структура остается прежней, и именно эта стабильность делает ее стандартом.
Разумный способ начала — начать с малого, используя минимальный цикл ReAct: один узел агента, один узел инструмента и одно условное ребро с использованием tools_condition. Сначала сделайте это работающим, затем добавьте еще один узел, а позже — еще одного агента. Структура сохраняется при любом размере.
Чтобы узнать больше о бэкендах для хранения данных, схемах вовлечения человека в процесс и вариантах развертывания, обратитесь к официальной документации LangGraph.