Практычныя нататкі: памяць у LangGraph — поўны практычны гід
Практычны разбор тэмы «кіраванне памяццю ў LangGraph»: кантракты, праверкі і гатовыя фрагменты кода для каманд, якія ўкараняюць гэты падыход.
Крыніцы: LangGraph Memory: The Complete Practical Guide to Managing What Your Agent Remembers
Наступныя прытамлівкі паказваюць практычны падход да кнігі «LangGraph Memory: The Complete Practical Guide to Managing What Your Agent Remembers». Акцэнт ставіцца на контракты, перакананняя і мескі для коду, а не на мотывацыйныя аспекты. Калі працуеце з адглядам, спачатку запісайце контракт: неабяжлівыя даныя, сігнал успеху і тое, што выканаецца у разе частковага нявыполнення. Такі спіс перакананняя дапамагае заліцвачваць будучыя змены ў кодзе. Валідзіце маленькія, тэставаныя елементы замест абмежлівых скрыптов. Калі якісь крок не выйшоў, прычына нявыполнення павінна вказваць на адну конкрэтную адпаведальнасць, а не на заплутаны ланцужок задач.
Проблема памяці, пра якую ніхто не говорыць у туторыялах
Проблема памяці, працэю пра якую ніхто не говорыць у нарадчых матэрыялах, найкрашча працюе, калі яе рассматрываць як вімерную структуру. Запісайце адны ідэальны прыклад, адну справу з бягам і прыметку па поверненню да пачатковага стану, прычым расшырваючы масштабы. Рассматрайце гэты этап як кантракт межа вхіднымі данымі і пераканаленымі выходнымі рэзультатамі. Дайце назвы артыфактам, задаце крытэрыя успеху і адмовіцеся ад тыхоў, частковага завершэння. Зберагачыце стан графа ў простам і тыпаванам фармаце. Вкладзеныя блокі маскуюць інфармацыю пра тое, які вузел запісаў які поле, і спакоююць працэс пасля перарываў.
Ландшафт памяці: чатырнацца рэчаў, якія трэба спачатку зрозумець
«Ландшафт памяці: Чатыры рэчы, якія трэба з’ясавіць спачатку» працюе наўжоўды лепш, калі яго спрыятаць як вімерную паверхню. Запісайце адны «золаты» прыклад, адны прыклад неудачы і прыметку пра вярнэнне да пачатковага стану, перш чым расшырваць масштабы. Запісвайце часы выканання і косты токеноў або запытак праза функцыйнае рэзультат. Відразлівасць костаў з самага пачатку запобегае неспакойным рахункам, калі працэс пераходзіць з дэмаверсіі ў спяльныя асяроддзя. Храніце стан графа ў простам і тыпаваным фармаце. Вкладзеныя блокі маскуюць інфармацыю пра тое, які вузел запісаў які поле, і спакшваюць продажчэй работу пасля перэрываў.
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: Самаўпакоркаванне на аднойчынных мовах (Стандарт для прыменення)
Стратэгія 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, спачатку запісайце умовы вікорыстання: неабходныя даныя, сігнал успеху і тое, што выходзіць на падчасныя неудачы. Такі чарт дапамагае заліцваты змяны ў кодзе.
Спрыятлівае ставленне да гэтага этапу як да угоды між вхіднымі данымі і перакананымі выходнымі рэзультатамі. Дайце назвы элементам, задаць крытэрыя успеху і не падтрымвайце тыхія, непূраныя завершэння.
Зробіце контрольны пункт пасля дорогіх крокаў. Функцыя продакцыі не должна занова ставіць плату за той самы вызыв LLM, калі аператар праказвае пазнейшы вузел.
# ── 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», спачатку запісайце умовы вікорыстоўвання: неабходныя даны, сігнал успеху і тое, што выходзіць пад час частковага абякання. Такі спіс дапамагае заліцварыць будучыя змены ў кодзе. Запісвайце час выканання і кост токенаў або запытаў разам з функцыйнальнымі рэзультатамі. Відразлівасць костаў з самага пачатку запобегае неспакойным рахункам, калі працэс пераходзіць з дэмаверсіі ў спяльныя асяроддзя. Стварайце контрольныя пункты пасля дорогіх крокаў. Продовжэнне працы не должна зноў нарахоўваць косты за той самы вызов LLM, калі аператар прабуе зноў выконаць пазнейшы вузел.
# ============================================================
# 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")
Кансультатывны дапамогаў па выбору стратэгіі каштовання памяці
Калі працуеце з Канала даслужэнняя «Стратэгія памяці», спачатку запісайце умовы працы: неабяжлівыя даныя, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі спіс контроля дапамагае заліцвачыць пазнейшыя змены ў кодзе. Зберагаюце настройкі праза код аплікацыі. Файлы сераўіса, хранільнікі секрэтных дадзеных і флагі функцыйяў должны знаходзіцца ў аднам месцы, куды аператары можаюць адбавіць аўдыт без неабяжлівага чытання всей структуры. Ставяйце контрольныя пункты пасля дорогіх крокаў. Сістэма вярнення працы не должна занова ставіць плату за той самы вызов LLM, калі аператар перапрыяўляе працу да наступнага элемента. Калі працуеце з Канала даслужэнняя «Стратэгія памяці», спачатку запісайце умовы працы: неабяжлівыя даныя, сігнал успеху і тое, што выходзіць у разе частковага нявыпання. Такі спіс контроля дапамагае заліцвачыць пазнейшыя змены ў кодзе. Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшаў, прычына нявыпання должна вказываць на адну конкрэтную адпаведальнасць, а не на заплутаную структуру працы.
Адаптаваны карточкі з рэферэнсамі па ключоўых словах
Адаптаваны карточкі з рэферэнсамі па ключоўых словах работаюць наякша, калі іх спрыяваць як мерыемую структуру. Зберагучы адна ідэальная версія, адзін прыклад неудачы і запіс пра вярнэнне да пачатковага стану, перш чым расшырваць масштаб. Спрыяйце гэтаму этапу як кантракту межа вхіднымі данымі і паверыцельнымі выходамі. Даўце назвы артыфактам, задаць крытэрыя успеху і не падзеўляйцеся частым, непূরным выкананнем задач. Зберагучы стан графа ў простам і тыпаванам фармаце. Вярнутыя структуры маскуюць інфармацію пра тое, які вузел запісаў кожны поле, і спакшваюць продажчэй работу пасля перарываў.
Вывод: Памяць — гэта архітэктура, а не дадатковы элемент
Заключэнне: «Памяц — гэта архітэктура, а не чыста нараджэнне» працюе найкрашча, калі яе расследжваць як вімерную велічыну. Запісайце адны ідэальны прыклад працы, адну сітуацыю неудачы і прыметкі па поверненню да пачатковага стану прычым расшырэнню масштаба. Запісвайце часы выканання і косты токеноў або запытак праза функцыйнае рэзультат. Відразлівасць костаў з самага пачатку запобегае неспакойным рахункам, калі працэс пераходзіць з дэмаверсіі ў спяльныя асяроддзя. Храніце стан графаў у простам і тыпаваным фармаце. Вярнутыя структуры даных маскуюць інфармацыю пра тое, які вузел запісаў кожны поле, і спакшваюць продовжэнне працы пасля перарываў.
Чэк-ліст для эксплуатацыі
Для чэк-ліста для эксплуатацыі неабходна з’явіць вхідныя даны, адпаведальнага за кожны крок і крытэрыя завершэння прычым змене коду. Аператары должны магчымаць перзапуск кроку з вядомай точкі контролю, не падозрэўаючы пра схованы стан.
Документавайце як шлях успеху, так і шлях вяснавання проблем. Перапрыбуткі, людзкія контрольны пункты і обработка некорэктных паведамленняў ёсць часткай продукту, а не дадатковым элементам пасля яго стварэння.
Неабяжна людская згода на тыя элементы, які вітрачаюць грошы або зменяюць даны працэйнага працэсу. Праця ў час складання коду не абавязкова значыць повнайшую рэалізацыю бізнес-функцыйнасці.
Напісце кароткі практычны паведамленні: як зменяць кантрольныя клучы, як спрачысці чэргу заданняў, як анулюваць пярэдніе змены.
Валіце маленькія, тэставаныя елементы замест большых скрыптов. Калі якісь крок не выйшае, прычына нехарактэрызуецца адной відпаведальнасцю, а не заплутаным ланцоўкам заданняў.
Неабяжна людская згода на тыя элементы, які вітрачаюць грошы або зменяюць даны працэйнага працэсу. Праця ў час складання коду не абавязкова значыць повнайшую рэалізацыю бізнес-функцыйнасці.
Перад апранаваннем вэрсый, заморозьце іх, зафіксуйце ідэальны варыянт даных для крытычных траекторый і паказваце способы анулявання змэн. У спадзяльных срэдах неабходны ліміты частоты запуска, перакананні ў належнасці рэсурсаў і чысткі власнік для змены секрэтных клучоў. Валіце простую надзяйнасць замест крэатыўных, але разовых дэманстрацый.
Запіскі для пакета d865d505a59e: не класты ключы прадаўцоў у рэпазітарыю, задаць максымальны ліміт токена на сесію, а таксама зберагчы транскрыпціі праза фіксатуры eval, каб пазнейшыя замены модэляў заставаліся пораўнанымі.