Задача: хотим «агента», который делает несколько шагов, помнит контекст, принимает решения и останавливается по условию. Это не один вызов модели — это цикл: подумай → действуй → обнови память → повтори до цели. В этой статье начнём именно с агентной постановки, а затем покажем, как LangGraph помогает это собрать прозрачно и тестопригодно.
План статьи:
- Что такое агент простыми словами и зачем он нужен
- LangGraph в 1 минуту: узлы, рёбра, состояние
- Мини-агент: Plan → Act с критерием остановки (код)
- Базовая механика без агент��ости: граф A→B (код)
- Подводные камни и куда двигаться дальше
Что такое агент и зачем он нужен
Агент — это цикл действий с памятью:
- есть цель (goal),
- есть память о шагах (мысли, действия, результаты),
- на каждом шаге он решает «что дальше» и при необходимости вызывает инструменты/функции/модели,
- он умеет останавливаться, когда цель достигнута или превышен лимит.
Почему это важно:
- Многошаговые задачи (сбор данных, планирование задач, интеграции) редко решаются одним вызовом LLM;
- Нужна прозрачность и контроль: где мы находимся, почему агент принял такое решение, когда нужно остановиться.
LangGraph в 1 минуту
LangGraph — это способ собирать таких агентов из простых кусочков кода:
- Состояние (state) — явная «память» агента (goal, thoughts, actions, result, done…).
- Узлы (nodes) — функции, которые читают и изменяют состояние (например, Plan, Act).
- Рёбра (edges) — порядок переходов между узлами (включая циклы и развилки).
- Точки входа/выхода — где стартуем и где заканчиваем.
В итоге вы получаете исполняемый workflow с методами вроде invoke/stream. Никакой «магии»: это обычные функции и прозрачные переходы.
Мини-агент: Plan → Act → (повтор) с критерием остановки
Соберём минимальную петлю. В реальности Plan может вызывать LLM, а Act — инструменты и API. Здесь используем имитацию, чтобы пример был самодостаточным и сразу работал.
Схема:
- state: goal, thoughts, actions, result, done
- Plan: решает следующий шаг; если цель уже достигнута — done=True
- Act: «выполняет» шаг (заглушка), обновляет result и журнал действий
- Цикл: Plan → Act ��� Plan, пока не done или не исчерпан лимит итераций
from typing import TypedDict, List
from langgraph.graph import StateGraph
class SimpleAgentState(TypedDict, total=False):
goal: str
thoughts: List[str]
actions: List[str]
result: str
done: bool
def node_plan(state: SimpleAgentState) -> SimpleAgentState:
new = dict(state)
new["thoughts"] = list(new.get("thoughts") or [])
new["actions"] = list(new.get("actions") or [])
new["result"] = new.get("result") or ""
goal = new.get("goal") or ""
result = new.get("result") or ""
# Наивная проверка достижения цели: если goal встречается в result
if goal and goal.lower() in result.lower():
new["thoughts"].append("Цель достигнута — останавливаемся.")
new["done"] = True
return new # Без постановки нового шага
# Иначе запланируем простое действие: 'append_goal'
new["thoughts"].append("Похоже, цель не достигнута. Действуем: append_goal")
new["next_action"] = "append_goal" # служебное поле для act
new["done"] = False
return new # type: ignore[return-value]
def node_act(state: SimpleAgentState) -> SimpleAgentState:
new = dict(state)
new["actions"] = list(new.get("actions") or [])
new["result"] = new.get("result") or ""
action = new.pop("next_action", None)
if action == "append_goal":
goal = new.get("goal") or ""
new["result"] = (new["result"] or "") + f" {goal}".strip()
new["actions"].append("append_goal")
else:
new["actions"].append("noop")
return new # type: ignore[return-value]
def build_agent_graph():
g = StateGraph(SimpleAgentState)
g.add_node("Plan", node_plan)
g.add_node("Act", node_act)
# Простой цикл: Plan -> Act -> Plan. Остановку контролируем флагом done на уровне вызова.
g.add_edge("Plan", "Act")
g.add_edge("Act", "Plan")
g.set_entry_point("Plan")
return g.compile()
def run_agent_once(goal: str):
workflow = build_agent_graph()
state: SimpleAgentState = {
"goal": goal,
"thoughts": [],
"actions": [],
"result": "",
"done": False,
}
# Ограничим 5 итерациями, чтобы избежать бесконечного цикла
for _ in range(5):
state = workflow.invoke(state) # Plan -> Act -> Plan
if state.get("done"):
break
return state
if __name__ == "__main__":
final = run_agent_once("hello")
print(final)
Идея: за 1–2 итерации в result появится цель (hello), Plan увидит совпадение и выставит done=True. Это и есть «агентность»: последовательное планирование и действие с явной памятью и контролем остановки.
Под реальный LLM: замените логику внутри node_plan на вызов модели (через LangChain/клиенты LLM), а в node_act подключите настоящие инструменты. Прелесть LangGraph — вы управляете потоком (ветвления, повторы, остановки) и храните память в открытом виде, что удобно тестировать и дебажить.
Базовая механика LangGraph без агентной логики: A → B
Теперь, когда идея агентной петли понятна, посмотрим на самую простую механику графа: два узла, которые п��следовательно обновляют состояние.
from typing import TypedDict
from langgraph.graph import StateGraph
from rich import print as rprint
class SimpleState(TypedDict, total=False):
message: str
count: int
def node_a(state: SimpleState) -> SimpleState:
new_state: SimpleState = dict(state)
new_state["count"] = (new_state.get("count") or 0) + 1
new_state["message"] = (new_state.get("message") or "") + " -> from A"
rprint({"node": "A", "state": new_state})
return new_state
def node_b(state: SimpleState) -> SimpleState:
new_state: SimpleState = dict(state)
new_state["message"] = (new_state.get("message") or "") + " -> from B"
rprint({"node": "B", "state": new_state})
return new_state
def build_graph():
g = StateGraph(SimpleState)
g.add_node("A", node_a)
g.add_node("B", node_b)
g.add_edge("A", "B")
g.set_entry_point("A")
g.set_finish_point("B")
return g.compile()
def main():
workflow = build_graph()
initial: SimpleState = {"message": "hello", "count": 0}
result = workflow.invoke(initial)
rprint({"result": result})
if __name__ == "__main__":
main()
Ожидаемый результат: {"message": "hello -> from A -> from B", "count": 1}. Это показательная «скелетная» версия без планирования и остановок — чистая механика «узлы + рёбра + состояние».
Подводные камни и практические советы
- Память (state) — это контракт. Сначала продумайте поля: цель, мысли, действия, результаты, флаги. Так проще расширять агента.
- Иммутабельный стиль обновления состояния упрощает отладку и тесты.
- Всегда задавайте критерии остановки и/или лимиты итераций, особенно в циклах Plan/Act.
- Для реальных проектов используйте условные рёбра и явный finish-маршрут (router), чтобы останавливать граф без «внешнего» цикла.
- Логируйте промежуточные состояния (print/rich/logging) — это эконо��ит часы.
Что дальше (Часть 2)
- Условная маршрутизация (router): выбор следующего узла по значению из состояния и явное завершение через finish-ребро.
- Интеграция LLM и инструментов: реально мыслящий Plan и исполняющий Act.
- Ветвления и циклы: ограничители, счётчики, тайм-ауты, стратегии остановки.
- Наблюдаемость:
stream()для пошагового контроля и трейсинга.
Итого: мы начали с агентной задачи и показали, как LangGraph помогает собрать прозрачного, детерминируемого многошагового агента — из узлов (мини-агентов/инструментов), рёбер (оркестрации) и состояния (памяти). Дальше углубим маршрутизацию, подключим LLM к планировщику и сделаем цикл Plan/Act по-настоящему умным и наблюдаемым.