Практические советы: LangGraph Memory: Полное практическое руководство по управлению
Пошаговое руководство по практическим заметкам: LangGraph Memory — полное практическое руководство по управлению контрактами, проверками и слотами для вставки кода для команд, использующих эту модель.
Источники: LangGraph Memory: The Complete Practical Guide to Managing What Your Agent Remembers
В следующих заметках описывается практический подход к использованию «LangGraph Memory: Полное практическое руководство по управлению тем, что запоминает ваш агент». Основное внимание уделяется контрактам, проверкам и местам для вставки кода, а не мотивирующим формулировкам. При изучении обзора сначала запишите контракт: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список поможет сохранять честность при последующих изменениях кода. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. Если какой-то шаг не сработает, причина неудачи должна указывать на конкретную ответственность, а не на запутанную цепочку операций.
Проблема памяти, о которой никто не говорит в учебниках
Проблема памяти, о которой никто не говорит в учебниках, наилучшим образом решается, если рассматривать её как измеримую структуру. Соберите один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объёма работ. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия всем элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Сохраняйте структуру графа простой и типизированной. Вложенные структуры данных скрывают информацию о том, какой узел заполнил тот или иной поле, что приводит к нарушению возобновления работы после перерывов.
Ландшафт памяти: четыре вещи, которые нужно понять в первую очередь
«Ландшафт памяти: четыре вещи, которые нужно понять в первую очередь» наилучшим образом работает, если рассматривать его как измеримую поверхность. Сначала зафиксируйте один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию, прежде чем расширять объём исследования. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости на ранних этапах предотвращает неожиданные счёты при переходе от демо-среды к общедоступным средам. Сохраняйте структуру графа простой и типизированной; вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и могут нарушить возобновление работы после прерываний.
User sends message
↓
Graph loads State from Checkpointer (using thread_id)
↓
Memory Management runs (trim / filter / summarize)
↓
LLM receives cleaned messages
↓
LLM response added to State
↓
Checkpointer saves updated State (to memory / SQLite / Postgres)
Расширенный модуль 2: Состояние, учитывающее память
Модуль 2 Расширенный: режим, учитывающий состояние памяти, работает наилучшим образом, когда рассматривается как измеримая структура. Соберите один эталонный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работы. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф. Сохраняйте структуру графа простой и типизированной. Вложенные структуры данных скрывают информацию о том, какой узел записал тот или иной поле, и мешают возобновлению работы после прерываний. Модуль 2 Расширенный: режим, учитывающий состояние памяти, работает наилучшим образом, когда рассматривается как измеримая структура. Соберите один эталонный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию перед расширением объема работы. Предпочитайте небольшие, тестируемые единицы кода большим развернутым скриптам. Когда какой-либо шаг срабатывает некорректно, причина сбоя должна указывать на конкретную ответственность, а не на запутанную цепочку операций.
class AgentState(TypedDict):
messages: Annotated[list[BaseMessage], add_messages]
Стандартный режим, учитывающий состояние памяти
Для режима работы, учитывающего состояние памяти, необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия результатам работы, определите критерии успеха и не допускайте молчаливого частичного завершения задачи. Внедряйте проверку человеком для операций, связанных с расходами или изменением производственных данных. Настройки, сделанные во время компиляции, не гарантируют полноты выполнения бизнес-задач.
# ── MODULE 2: STATE (Memory-Aware Version) ──────────────────
from langgraph.graph import MessagesState
# Option A: The shorthand (recommended for most agents)
# MessagesState is a pre-built TypedDict with just the messages field.
# Use it when you don't need custom fields.
class State(MessagesState):
summary: str # The only addition needed for memory management
Три стратегии управления длиной разговора
В разделе «Три стратегии управления длительностью разговора» необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рядом с функциональными результатами следует записывать время выполнения и стоимость токенов или запросов. Отображение затрат на раннем этапе предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Для операций, связанных с тратой денег или изменением производственных данных, необходимо предусмотреть утверждение человеком. Подключение компонентов во время компиляции не гарантирует полноты реализации бизнес-логики.
Стратегия 1: Фильтрация сообщений
Для стратегии 1: фильтрация сообщений, перед изменением кода необходимо определить входные данные, ответственного за выполнение шага и критерии завершения. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь кодовый граф. Вводите ручное утверждение для операций, связанных с тратой денег или изменением производственных данных. Подключение компонентов во время компиляции не гарантирует полноты обработки бизнес-логики. Для стратегии 1: фильтрация сообщений, перед изменением кода необходимо определить входные данные, ответственного за выполнение шага и критерии завершения. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Предпочитайте небольшие, тестируемые единицы кода большим скриптам. При сбое шага он должен указывать на конкретную причину, а не на запутанную структуру обработки данных.
Шаблон А: Фильтровать только то, что видит LLM (состояние не меняется)
При работе по шаблону А: Фильтровать только то, что видит LLM (состояние не меняется), сначала запишите контракт: требуемые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список помогает сохранять честность при последующих изменениях кода. Рассматривайте этот этап как контракт между входными данными и проверенными выходными данными. Дайте названия результатам обработки, определите критерии успеха и не допускайте безусловного частичного завершения работы. Храните в кэше стабильные инструкции системы и схемы инструментов. Пересылка одинакового вступительного текста — частая причина избыточных расходов.
# ── MODULE 4: NODES ──────────────────────────────────────────
def chat_model_node(state: State) -> dict:
"""Calls the LLM with only the last 2 messages, but state keeps the full history."""
# Filter: only send last 2 messages to the LLM
# State is NOT modified - full history stays in the checkpoint
recent_messages = state["messages"][-2:]
response = llm.invoke(recent_messages)
return {"messages": [response]}
Шаблон Б: RemoveMessage — полное удаление из состояния
При работе над шаблоном B: RemoveMessage — Полное удаление из состояния, сначала запишите контракт: необходимые входные данные, сигнал о успехе и что происходит при частичной неудаче. Такой список контроля помогает сохранять честность при последующих изменениях кода. Записывайте время выполнения и стоимость токенов или запросов рядом с функциональными результатами. Отображение затрат заранее предотвращает неожиданные счета при переходе с демо-среды в общедоступные среды. Создавайте контрольные точки после дорогостоящих операций. Функция возобновления работы не должна снова взимать плату за один и тот же вызов LLM, когда оператор пытается выполнить последующий узел.
# ── IMPORTS needed ──────────────────────────────────────────
from langchain_core.messages import RemoveMessage
# ── MODULE 4: NODES ──────────────────────────────────────────
def filter_messages_node(state: State) -> dict:
"""A dedicated node that removes all but the last 2 messages from state.
RemoveMessage works by ID: you pass it the ID of a message you want deleted,
and the add_messages reducer handles the actual deletion. This is NOT the same
as just slicing the list - it permanently removes from the checkpoint.
"""
# Build a list of RemoveMessage objects for every message we want to delete
# state["messages"][:-2] = all messages EXCEPT the last 2
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {"messages": delete_messages} # Reducer sees these and deletes them
def chat_model_node(state: State) -> dict:
"""After filtering, the state already has a short message list."""
response = llm.invoke(state["messages"])
return {"messages": [response]}
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
graph_builder = StateGraph(State)
graph_builder.add_node("filter", filter_messages_node)
graph_builder.add_node("chat_model", chat_model_node)
# Filter runs FIRST, then chat_model sees the cleaned state
graph_builder.add_edge(START, "filter")
graph_builder.add_edge("filter", "chat_model")
graph_builder.add_edge("chat_model", END)
graph = graph_builder.compile(checkpointer=MemorySaver())
Стратегия 2: Отбор данных на основе токенов с использованием trim_messages
При работе над Стратегией 2: Обрезка на основе токенов с использованием trim_messages сначала запишите контракт: необходимые входные данные, сигнал успешного выполнения и последствия частичной неудачи. Такой чек-лист помогает сохранять честность последующих изменений в коде.
Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код.
Храните в кэше стабильные инструкции системы и схемы инструментов. Пересылка одинаковых заголовков — распространенная причина ресурсозатрат.
При работе над Стратегией 2: Обрезка на основе токенов с использованием trim_messages сначала запишите контракт: необходимые входные данные, сигнал успешного выполнения и последствия частичной неудачи. Такой чек-лист помогает сохранять честность последующих изменений в коде.
Предпочитайте небольшие, тестируемые единицы кода большим скриптам. Когда какой-то шаг терпит неудачу, ошибка должна указывать на конкретную ответственность, а не на запутанную структуру.
цикл обработки.
# ── IMPORTS needed ──────────────────────────────────────────
from langchain_core.messages import trim_messages
# ── MODULE 4: NODES ──────────────────────────────────────────
def chat_model_node(state: State) -> dict:
"""Uses token-based trimming to control exactly how much history the LLM sees."""
trimmed = trim_messages(
state["messages"],
# Maximum number of tokens to allow (tune this to your model and budget)
max_tokens=1000,
# "last" = keep the MOST RECENT messages (the ones that fit within max_tokens)
# This is almost always what you want
strategy="last",
# The LLM used to count tokens. Use the same model you're chatting with
# for accurate counting. This does NOT make an API call - just counts.
token_counter=ChatOpenAI(model="gpt-4o"),
# False = never cut a message in half. Either include it fully or drop it.
# Always set this to False in production to avoid feeding partial tool results.
allow_partial=False,
# True = always include the most recent HumanMessage even if it alone
# exceeds max_tokens. Prevents the agent from receiving an empty context.
include_system=True,
)
response = llm.invoke(trimmed)
return {"messages": [response]}
Краткий справочник ключевых параметров
Краткий справочник ключевых параметров наиболее эффективен, если рассматривать его как измеримую структуру. Соберите один эталонный пример вывода, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работы. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успешности и не соглашайтесь на молчаливое частичное выполнение задачи. Сохраняйте структуру графа простой и типизированной. Вложенные структуры данных скрывают информацию о том, какой узел заполнил тот или иной поле, и мешают возобновлению работы после прерываний.
Стратегия 3: Суммаризация с использованием LLM (стандарт в производстве)
Стратегия 3: Суммаризация с использованием LLM (стандарт производства) работает наилучшим образом, когда рассматривается как измеримая величина. Сначала необходимо зафиксировать один идеальный пример обработки, один случай сбоя и записку о возврате к предыдущему состоянию, прежде чем расширять объем работ. Рядом с функциональными результатами следует записывать время выполнения и стоимость токенов или запросов. Отслеживание затрат с самого начала помогает избежать неожиданных счетов при переходе от демо-версии к общедоступным средам. Определите лимит токенов на один ход и на всю сессию. Инструменты типа агентов активно расширяют контекст; жесткие ограничения предотвращают появление неожиданных счетов при использовании демо-версий.
Шаг 1: Описание (Модуль 2)
Шаг 1: Состояние (Модуль 2) лучше всего рассматривать как измеримую поверхность. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работы. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь граф. Сохраняйте структуру графа простой и типизированной. Вложенные структуры данных маскируют информацию о том, какой узел записал какое поле, и приводят к нарушению возобновления работы после перерывов.
class State(MessagesState):
summary: str # Stores the running summary. Empty string by default.
Шаг 2: Два узла (Модуль 4)
Шаг 2: Подход «Два узла» (Модуль 4) наилучшим образом работает, когда его рассматривают как измеримую поверхность. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма исследования. Документируйте одновременно путь успешной работы и путь восстановления. Повторные попытки, проверки человеком и обработка неработоспособных сообщений являются частью продукта, а не этапом последующей доработки. Сохраняйте структуру графа простой и типизированной; вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и мешают возобновлению работы после прерываний.
def call_model(state: State) -> dict:
"""The main agent node. Uses summary as context if it exists."""
summary = state.get("summary", "")
if summary:
# If we have a summary, prepend it as a system message
# This gives the LLM the compressed history without the raw messages
system_message = SystemMessage(
content=f"Summary of the conversation so far:\n\n{summary}"
)
messages = [system_message] + state["messages"]
else:
# No summary yet — just use the raw messages
messages = state["messages"]
response = llm.invoke(messages)
return {"messages": [response]}
def summarize_conversation(state: State) -> dict:
"""Triggered when conversation gets long. Compresses history into a summary,
then deletes the old raw messages to keep state lean."""
existing_summary = state.get("summary", "")
# Build the summarization prompt.
# If a summary already exists, extend it with new messages.
# This is the "rolling summary" pattern - it never reads all messages from scratch.
if existing_summary:
summary_message = (
f"This is the summary of our conversation so far:\n{existing_summary}\n\n"
f"Extend this summary with the new messages above. "
f"Keep it concise - focus on facts and decisions that matter for future turns."
)
else:
summary_message = (
"Create a concise summary of the conversation above. "
"Focus on key facts, user intent, and any decisions made. "
"This summary will replace the full message history."
)
# Call the LLM with all current messages + the summarization instruction
messages = state["messages"] + [HumanMessage(content=summary_message)]
response = llm.invoke(messages)
# After summarizing, delete all messages except the last 2.
# Why keep 2? So the LLM has immediate conversational context
# (it can see what was just said) even after summarization runs.
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {
"summary": response.content, # Updated summary stored in state
"messages": delete_messages # Old messages removed from state
}
Шаг 3: Логика маршрутизации (Модуль 5)
Шаг 3: Логика маршрутизации (модуль 5) работает наилучшим образом, когда рассматривается как измеримая структура. Соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работы. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. При сбое какого-либо шага причина должна указывать на конкретную ответственность, а не на запутанную цепочку операций. Сохраняйте структуру графа простой и типизированной. Вложенные структуры данных маскируют информацию о том, какой узел заполнил тот или иной поле, что приводит к нарушению работоспособности после перерывов.
from typing import Literal
def should_continue(state: State) -> Literal["summarize_conversation", "__end__"]:
"""After the main agent responds, decide: keep going or trigger summarization?
The threshold (6 messages here) is your tuning knob.
Too low = you summarize frequently, losing some nuance.
Too high = you let the context grow before compressing.
For most production agents, 6–10 messages is a good starting range.
"""
if len(state["messages"]) > 6:
return "summarize_conversation"
return "__end__"
Шаг 4: Сборка графа (модуль 6)
Шаг 4: Сборка графа (модуль 6) наилучшим образом работает, если рассматривать его как измеримую поверхность. Сохраните один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работ. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Дайте названия создаваемым объектам, определите критерии успешности и не соглашайтесь на молчаливое частичное выполнение задачи. Сохраняйте структуру графа простой и типизированной. Вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и мешают возобновлению работы после прерываний.
graph_builder = StateGraph(State)
graph_builder.add_node("call_model", call_model)
graph_builder.add_node("summarize_conversation", summarize_conversation)
graph_builder.add_edge(START, "call_model")
graph_builder.add_conditional_edges(
"call_model",
should_continue,
{
"summarize_conversation": "summarize_conversation",
"__end__": END
}
)
# After summarization, the conversation ends for this turn
graph_builder.add_edge("summarize_conversation", END)
memory = MemorySaver()
graph = graph_builder.compile(checkpointer=memory)
Как это работает на практике
Метод «Как это работает на практике» дает наилучшие результаты, если рассматривать его как измеримую структуру. Сначала соберите один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию, прежде чем расширять объём исследования. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Отображение стоимости на ранних этапах предотвращает неожиданные счёты при переходе от демо-среды к общедоступным средам. Сохраняйте структуру графа простой и типизированной; вложенные структуры данных маскируют информацию о том, какой узел заполнил тот или иной поле, и приводят к нарушению целостности данных после перерывов.
Turn 1–3: Messages accumulate. should_continue → "__end__" each time.
Turn 4: call_model runs. len(messages) > 6 → should_continue → "summarize_conversation"
summarize_conversation:
- LLM reads all 7 messages, generates a 2-sentence summary
- state["summary"] = "User is troubleshooting a Python import error..."
- Deletes messages[:-2] (keeps only last 2 raw messages)
Turn 5: call_model prepends summary as SystemMessage + 2 recent messages
LLM sees: [SystemMessage(summary), HumanMessage, AIMessage, HumanMessage(latest)]
Token cost: 4 messages instead of potentially 20+
Сделать память по-настоящему постоянной: внешние точки контроля
Создание по-настоящему устойчивой памяти: внешние чекпоинтеры работают наилучшим образом, когда их рассматривают как измеримую структуру. Сохраняйте один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию до расширения объёма работы. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Сохраняйте состояние структуры простым и типизированным. Вложенные данные маскируют информацию о том, какой узел записал какое поле, и мешают возобновлению работы после прерываний. Создание по-настоящему устойчивой памяти: внешние чекпоинтеры работают наилучшим образом, когда их рассматривают как измеримую структуру. Сохраняйте один идеальный пример работы, один случай сбоя и записку о возврате к предыдущему состоянию до расширения объёма работы. Предпочитайте небольшие, тестируемые единицы кода большим скриптам. Когда какой-то шаг сбивается, причина сбоя должна указывать на конкретную ответственность, а не на запутанную цепочку операций.
Интерфейс чекпоинтера
Для интерфейса Checkpointer необходимо определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Рассматривайте этот этап как контракт между входными данными и проверенными выходными результатами. Укажите названия элементов, определите критерии успеха и не допускайте молчаливого частичного завершения работы. Внедряйте утверждение человека для операций, связанных с тратой денег или изменением производственных данных. Подключение компонентов во время компиляции не гарантирует полноты выполнения бизнес-задач.
# Development: everything in RAM (lost on restart)
from langgraph.checkpoint.memory import MemorySaver
checkpointer = MemorySaver()
# Local / Single-machine production: persists to a .db file on disk
from langgraph_checkpoint_sqlite import SqliteSaver
import sqlite3
conn = sqlite3.connect("chat_memory.db", check_same_thread=False)
checkpointer = SqliteSaver(conn)
# Cloud / Distributed production: persists to a Postgres database
from langgraph_checkpoint_postgres import PostgresSaver
checkpointer = PostgresSaver.from_conn_string(
"postgresql://user:password@host:5432/dbname"
)
# This one line is the ONLY thing that changes between environments:
graph = graph_builder.compile(checkpointer=checkpointer)
SqliteSaver: подробный обзор
Для детального использования SqliteSaver необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Регистрируйте время выполнения, а также стоимость токенов или запросов вместе с функциональными результатами. Отображение стоимости заранее помогает избежать неожиданных счетов при переходе от демо-среды к общедоступным средам. Включайте утверждение человека для операций, связанных с расходами или изменением производственных данных. Подключение на этапе компиляции не гарантирует полноты решения с точки зрения бизнес-требований.
# ── Full SqliteSaver Setup ───────────────────────────────────
import sqlite3
from langgraph_checkpoint_sqlite import SqliteSaver
# File-based: state persists across restarts
conn = sqlite3.connect(
"agent_memory.db",
check_same_thread=False # Required for LangGraph's multi-threaded execution
)
checkpointer = SqliteSaver(conn)
graph = graph_builder.compile(checkpointer=checkpointer)
# Usage is IDENTICAL to MemorySaver:
config = {"configurable": {"thread_id": "user-123"}}
response = graph.invoke({"messages": [HumanMessage(content="Hello")]}, config)
# Stop and restart your app. Run this again with the SAME thread_id:
# The agent picks up exactly where it left off.
response = graph.invoke({"messages": [HumanMessage(content="What did I just say?")]}, config)
# Agent: "You said 'Hello'."
Когда использовать SqliteSaver вместо PostgresSaver
Чтобы определить, когда использовать SqliteSaver вместо PostgresSaver, необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Храните конфигурацию вне кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Вводите утверждение человека для операций, связанных с тратой денег или изменением производственных данных. Подключение на этапе компиляции не гарантирует полноты обработки бизнес-логики. Чтобы определить, когда использовать SqliteSaver вместо PostgresSaver, необходимо заранее определить входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг с известной точки контроля, не догадываясь о скрытом состоянии. Предпочитайте небольшие, тестируемые единицы кода вместо обширных скриптов. При сбое шага он должен указывать на конкретную причину, а не на несколько одновременных.
запутанная цепочка операций.Проверка памяти: get_state и get_state_history
При работе с функциями Проверка памяти: get_state и get_state_history сначала запишите условия использования: необходимые входные данные, сигнал о успешном выполнении и последствия частичной неудачи. Такой список поможет сохранять честность при последующих изменениях кода.
Рассматривайте этот этап как договор между входными данными и проверенными выходными результатами. Дайте названия элементам, определите критерии успешности и не допускайте безответственного частичного выполнения задач.
Создавайте контрольные точки после дорогостоящих операций. Функция возобновления выполнения не должна повторно оплачивать один и тот же вызов большой языковой модели, когда оператор пытается выполнить следующий элемент.
# ── Inspecting Current State ─────────────────────────────────
config = {"configurable": {"thread_id": "user-123"}}
# Get the current state snapshot for this thread
state_snapshot = graph.get_state(config)
# What's in the snapshot?
print(state_snapshot.values["messages"]) # Current message list
print(state_snapshot.values.get("summary", "")) # Summary, if any
print(state_snapshot.next) # Which nodes would run next (empty list = graph is at END)
# ── Inspecting State History ─────────────────────────────────
# get_state_history gives you every checkpoint ever saved for this thread.
# Each checkpoint is a moment in time after a node ran.
for state in graph.get_state_history(config):
print(f"Step: {state.metadata['step']}")
print(f"Messages: {len(state.values['messages'])}")
print(f"Next: {state.next}")
print("---")
Собираем всё вместе: полностью управляемый агент с поддержкой памяти
При работе над книгой «Putting It All Together: The Complete Memory-Managed Agent» сначала запишите условия работы агента: необходимые входные данные, сигнал о успешном выполнении и действия при частичной неудаче. Такой список поможет избежать ошибок при последующих изменениях кода. Рядом с функциональными результатами записывайте время выполнения операций, а также стоимость токенов или запросов. Отслеживание затрат с самого начала предотвращает неожиданные расходы при переходе с демо-среды в общедоступные среды. Выполняйте контрольные точки после дорогостоящих операций — система не должна снова взимать плату за один и тот же вызов большой языковой модели при повторной попытке обработки последующего элемента.
# ============================================================
# LANGGRAPH MEMORY-MANAGED AGENT — COMPLETE TEMPLATE
# Combines: Summarization + External Persistence (SqliteSaver)
# ============================================================
# ── MODULE 1: IMPORTS & CONFIGURATION ───────────────────────
import os
import sqlite3
from typing import Literal
from langchain_openai import ChatOpenAI
from langchain_core.messages import (
HumanMessage, AIMessage, SystemMessage,
BaseMessage, RemoveMessage
)
from langgraph.graph import StateGraph, START, END
from langgraph.graph import MessagesState
from langgraph.checkpoint.memory import MemorySaver
# For production, swap MemorySaver for one of these:
# from langgraph_checkpoint_sqlite import SqliteSaver
# from langgraph_checkpoint_postgres import PostgresSaver
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# ── MODULE 2: STATE ─────────────────────────────────────────
class State(MessagesState):
# summary stores the rolling compressed history.
# It starts empty and gets written by summarize_conversation.
summary: str
# ── MODULE 4: NODES ─────────────────────────────────────────
def call_model(state: State) -> dict:
"""Main reasoning node. Uses rolling summary for long-context efficiency."""
summary = state.get("summary", "")
if summary:
system_message = SystemMessage(
content=f"Summary of the conversation so far:\n\n{summary}"
)
messages = [system_message] + state["messages"]
else:
messages = state["messages"]
response = llm.invoke(messages)
return {"messages": [response]}
def summarize_conversation(state: State) -> dict:
"""Compression node. Summarizes history and prunes old messages from state."""
existing_summary = state.get("summary", "")
if existing_summary:
summary_prompt = (
f"Current conversation summary:\n{existing_summary}\n\n"
f"Extend this summary with the new messages. "
f"Be concise. Preserve facts, user intent, and key decisions."
)
else:
summary_prompt = (
"Summarize this conversation concisely. "
"Focus on facts, user intent, and decisions made."
)
messages = state["messages"] + [HumanMessage(content=summary_prompt)]
response = llm.invoke(messages)
# Keep only the 2 most recent raw messages in state.
# Everything older is now captured in the summary.
delete_messages = [RemoveMessage(id=m.id) for m in state["messages"][:-2]]
return {
"summary": response.content,
"messages": delete_messages
}
# ── MODULE 5: ROUTING ────────────────────────────────────────
def should_continue(state: State) -> Literal["summarize_conversation", "__end__"]:
"""Trigger summarization when conversation exceeds 6 messages.
Tune this threshold based on your average message length and token budget.
For GPT-4o at ~$0.005/1K tokens, 6 messages is a practical starting point.
"""
if len(state["messages"]) > 6:
return "summarize_conversation"
return "__end__"
# ── MODULE 6: GRAPH ASSEMBLY ─────────────────────────────────
graph_builder = StateGraph(State)
graph_builder.add_node("call_model", call_model)
graph_builder.add_node("summarize_conversation", summarize_conversation)
graph_builder.add_edge(START, "call_model")
graph_builder.add_conditional_edges(
"call_model",
should_continue,
{
"summarize_conversation": "summarize_conversation",
"__end__": END
}
)
graph_builder.add_edge("summarize_conversation", END)
# ── CHECKPOINTER SELECTION ───────────────────────────────────
# Development (in-memory, lost on restart):
checkpointer = MemorySaver()
# Uncomment for local persistence (survives restarts):
# conn = sqlite3.connect("agent_memory.db", check_same_thread=False)
# checkpointer = SqliteSaver(conn)
# Uncomment for cloud/production (requires PostgreSQL):
# checkpointer = PostgresSaver.from_conn_string(os.environ["DATABASE_URL"])
graph = graph_builder.compile(checkpointer=checkpointer)
# ── MODULE 7: ENTRYPOINT ─────────────────────────────────────
if __name__ == "__main__":
config = {"configurable": {"thread_id": "user-001"}}
print("Memory-managed agent ready. Type 'exit' to quit, 'state' to inspect memory.\n")
while True:
user_text = input("You: ").strip()
if not user_text or user_text.lower() == "exit":
break
# Debug command: inspect what the agent currently remembers
if user_text.lower() == "state":
snapshot = graph.get_state(config)
print(f"\n[DEBUG] Messages in state: {len(snapshot.values['messages'])}")
print(f"[DEBUG] Summary: {snapshot.values.get('summary', '(none)')}\n")
continue
response = graph.invoke(
{"messages": [HumanMessage(content=user_text)]},
config=config
)
print(f"Agent: {response['messages'][-1].content}\n")
Руководство по принятию решений относительно стратегии использования памяти
При работе с Руководством по принятию решений в области стратегии памяти сначала запишите условия работы: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список помогает сохранять честность последующих изменений в коде. Храните конфигурацию отдельно от кода приложения. Файлы среды, хранилища секретов и флаги функций должны находиться в одном месте, чтобы операторы могли их проверять, не читая весь код. Создавайте контрольные точки после дорогостоящих операций. Система возобновления работы не должна снова взимать плату за один и тот же вызов большой языковой модели, когда оператор пытается выполнить следующий шаг. При работе с Руководством по принятию решений в области стратегии памяти сначала запишите условия работы: необходимые входные данные, сигнал успешного выполнения и действия при частичной неудаче. Такой список помогает сохранять честность последующих изменений в коде. Предпочитайте небольшие, тестируемые единицы кода большим скриптам. При сбое какого-либо шага причина неудачи должна указывать на конкретную ответственность, а не на запутанную структуру обработки данных.
Обновлённая карточка справки по ключевым словам
Обновлённая карточка справки по ключевым словам наилучшим образом работает, если рассматривать её как измеримую основу. Соберите один идеальный пример, один случай сбоя и запись о возврате к предыдущему состоянию перед расширением объёма работы. Рассматривайте этот этап как контракт между входными данными и проверенными результатами. Дайте названия элементам, определите критерии успеха и не соглашайтесь на молчаливое частичное выполнение задачи. Сохраняйте структуру графа простой и типизированной. Вложенные структуры скрывают информацию о том, какой узел заполнил тот или иной поле, и нарушают возможность продолжения работы после прерываний.
Заключение: память — это часть архитектуры, а не дополнение к ней
Заключение: «Память — это архитектура, а не последующая деталь» наилучшим образом работает, когда её рассматривают как измеримую величину. Сначала зафиксируйте один идеальный пример работы, один случай сбоя и запись о возврате к предыдущему состоянию, прежде чем расширять объём работ. Записывайте временные показатели, а также стоимость токенов или запросов рядом с функциональными результатами. Очевидность затрат с самого начала предотвращает неожиданные расходы при переходе с демо-среды в общедоступные среды. Сохраняйте структуру графа простой и типизированной; вложенные структуры скрывают информацию о том, какой узел записал какое поле, и могут нарушить возобновление работы после прерываний.
Чек-лист операционной работы
Для чек-листа операционной работы определите входные данные, ответственного за выполнение шага и критерии завершения перед изменением кода. Операторы должны иметь возможность перезапустить шаг, исходя из известной точки контроля, без необходимости угадывать скрытое состояние. Документируйте как успешный путь выполнения, так и пути восстановления. Повторные попытки, проверки человеком и обработка неработоспособных сообщений являются частью продукта, а не последующими доработками.
Необходимо получать одобрение человека для операций, связанных с тратой денег или изменением производственных данных. Подключение компонентов во время компиляции не гарантирует полноты функционала бизнес-процесса.
Напишите краткий руководство: как обновлять ключи, как опустошать очередь, как откатывать последнюю операцию ввода данных.
Лучше использовать небольшие, тестируемые модули вместо обширных скриптов. При сбое какого-либо шага причина должна быть связана с конкретной функцией, а не с запутанной цепочкой операций.
Необходимо получать одобрение человека для операций, связанных с тратой денег или изменением производственных данных. Подключение компонентов во время компиляции не гарантирует полноты функционала бизнес-процесса.
Перед внедрением новой стековой архитектуры заморозьте версии, сохраните эталонный вариант данных для критически важных процессов и убедитесь, что существуют шаги для отката. В общедоступных средах необходимы ограничения на частоту запросов, проверки принадлежности ресурсов и четко определенный ответственный за обновление секретов. Лучше выбирать надежность, чем красивые, но единоразовые демонстрации.
Примечание к пакету d865d505a59e: не включайте ключи поставщиков в репозиторий, установите лимит токенов на сессию и храните транскрипции рядом с фикстчерами оценки, чтобы последующие замены моделей оставались сопоставимыми.