Головна / Статті / П'ять стратегій пам'яті в LangChain: від буфера до пам'яті сутностей

П'ять стратегій пам'яті в LangChain: від буфера до пам'яті сутностей

Чому чат-моделі все забувають між викликами та п'ять патернів пам'яті для LangChain і LangGraph: буфер, вікно, підсумок, буфер із підсумком і сутності, з їхніми плюсами й мінусами.

1667 слів

Багато хто вважає, що чат-модель пам'ятає, що ви їй сказали. Це не так. Викличте GPT-5, GPT-4o, Claude чи Gemini, і результат однаковий: кожен запит починається з нуля, і якщо ви знову не надішлете попередні репліки, модель гадки не має, що вони були. Ось найкоротша демонстрація:

from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o-mini")llm.invoke("Hi, my name is Asha.")
llm.invoke("What's my name?")

Другий виклик нічого не знає про перший. Ім'я Аші так і не надіслали, тож модель не може його назвати.

Що таке пам'ять у LangChain

Жодної магії тут немає. Пам'ять — це політика, що відповідає на одне питання: які частини попередньої розмови надсилаються разом із наступним запитом? Від вашої відповіді залежить, наскільки розумним, масштабованим і дешевим вийде застосунок. Нижче п'ять політик, які використовують із LangChain і LangGraph, у чому кожна добра й погана та де вона доречна.

Головна суперечність

Припустімо, користувачка представляється Ашею, отримує привітну відповідь, а згодом питає, як її звати. Щоб відповісти правильно, те перше повідомлення має потрапити в новий промпт.

Проблеми починаються, коли чати розростаються до 10, 100 чи 1000 повідомлень. Якщо щоразу надсилати все наново, зростають витрати токенів, рахунки за API, затримка й навантаження на контекстне вікно. Кожен вибір міняє одне на інше:

  • Пам'ятаєте більше — зростає вартість.
  • Пам'ятаєте менше — вартість падає, але страждає точність.
  • Надсилаєте більше контексту — відповіді сповільнюються.
  • Надсилаєте менше контексту — відповіді пришвидшуються, але губиться інформація.

Стратегія 1: буферна пам'ять

Буферна пам'ять зберігає весь діалог повністю й надсилає його під час кожного виклику. Уявіть чат, який ніколи нічого не видаляє. Код короткий:

from langchain_core.messages import HumanMessage
history = []def chat(user_input):
    history.append(HumanMessage(user_input))    response = llm.invoke(history)    history.append(response)    return response.content

Приклад розмови:

User: I live in Chennai.
Assistant: That's great.

Трохи згодом:

User: Which city do I live in?
Assistant: Chennai

Модель правильно називає Ченнаї, бо той рядок досі є в історії, яку вона отримує.

Плюси: ідеальна пам'ять, будується елементарно, налагоджувати приємно, бо все на виду. Мінуси: токени ростуть без обмежень, витрати збільшуються, відповіді з часом сповільнюються, і зрештою ви впираєтеся в стелю контексту. Підходить для коротких сесій, внутрішніх інструментів, середовищ розробки й тестування.

Стратегія 2: віконна пам'ять

Тут зберігаються лише останні K повідомлень. Усе, що старіше, просто видаляється — як на дошці, де кожен новий запис стирає найстаріший.

from langchain_core.messages import trim_messages
trimmed = trim_messages(
    history,
    strategy="last",
    max_tokens=10,
    token_counter=len
)

Спробуйте такий обмін:

User: My name is Asha.
Assistant: Nice to meet you.
User: Message 1
User: Message 2
User: Message 3
User: Message 4User: What's my name?

Якщо повідомлення з ім'ям уже випало з вікна, моделі доведеться визнати поразку:

Assistant: I don't know your name.

Цей факт втрачено назавжди. Зате витрата токенів передбачувана, відповіді швидкі, а рахунок лишається невеликим. Мінус у тому, що стара інформація зникає, тому довгі розмови не працюють. Це FAQ-боти, балакучі чат-боти, помічники служби підтримки й будь-які бюджетні застосунки.

Стратегія 3: пам'ять на підсумках

Припустімо, ви не хочете накопичувати все підряд, але й забувати все теж не хочете. За пам'яті на підсумках модель час від часу пише стислий переказ старих реплік, і цей переказ замінює вихідний текст.

Люди роблять так природно. Після двогодинної зустрічі ви не пам'ятаєте кожного речення. Ви пам'ятаєте, що обговорили бюджет, схвалили пропозицію й призначили перевірку прогресу на наступний тиждень.

Після довгого чату збережений стан може виглядати так:

Summary:
User is building a shipment tracking application.
The discussion focused on Azure deployment and performance optimization.

Замість 100 повідомлень ви надсилаєте це. Приблизна реалізація починається з порожнього поточного підсумку:

running_summary = ""

Потім, коли історія перевищує поріг, старі повідомлення стискаються:

if len(history) > 6:
    old_messages = history[:-2]    running_summary = llm.invoke(
        f"Summarize this conversation:\n{old_messages}"
    ).content    history[:] = history[-2:]

Сильні сторони: зростання токенів вирівнюється, довгі розмови працюють, витрати падають. Слабкі: точні формулювання зникають, важливі деталі можуть загубитися, а результат добрий рівно настільки, наскільки добра модель, що пише підсумок. Підходить для довгих сесій підтримки, віртуальних асистентів, коучингових застосунків і багатогодинних чатів.

Стратегія 4: буфер із підсумком

Якщо відкрити код справжнього продакшен-чат-бота, зазвичай знайдеться якась версія саме цього підходу. Він поєднує два попередні: свіжі повідомлення зберігаються дослівно, а старі згортаються в підсумок. Ви отримуєте чіткий недавній контекст і доступні за ціною довгі розмови.

Добрий асистент керівника працює так само. Учорашнє прохання він пам'ятає точно:

You asked for the deployment status yesterday.

А загальне тло тримає в стислому вигляді:

This project started in January and focuses on Azure migration.

Свіжі деталі лишаються чіткими, а старі перетворюються на стисле знання. Мінімальна версія задає ліміт на кількість збережених останніх повідомлень:

RECENT_LIMIT = 4
running_summary = ""def chat(user_input):
    global running_summary    history.append(HumanMessage(user_input))    if len(history) > RECENT_LIMIT:        older_messages = history[:-RECENT_LIMIT]        running_summary = llm.invoke(
            f"""
            Update this summary:
            {running_summary}            Using:
            {older_messages}
            """
        ).content        history[:] = history[-RECENT_LIMIT:]    context = [
        SystemMessage(
            content=f"Summary: {running_summary}"
        )
    ] + history    response = llm.invoke(context)    history.append(response)    return response.content

Сильні сторони: добрий баланс ціни й точності, підтримка довгих чатів, збережені недавні деталі й гарна поведінка в продакшені. Слабкі: трохи більше складності плюс періодичні виклики для підсумку. Типові застосування — копілоти, корпоративні чат-боти, платформи підтримки й асистенти продакшен-рівня.

Стратегія 5: пам'ять сутностей

Іноді зберігати треба не сам діалог, а факти. Візьмімо такі твердження:

User: My sister's birthday is in March.
User: I work at Contoso.
User: My manager is Priya.

За кілька місяців ці факти все ще потрібні. Пам'ять сутностей веде структуровані записи про людей, місця, проєкти й компанії, що згадувалися, тож вихідний чат може зникнути, а корисне лишиться. Сховище сутностей може містити:

{
    "Priya": [
        "User's manager"
    ],
    "Contoso": [
        "User works here"
    ]
}

Видобування починається з порожнього сховища:

entity_store = {}

а функція просить модель витягти сутності й факти:

def extract_entities(text):    prompt = f"""
    Extract entities and facts:
    {text}
    """    return llm.invoke(prompt).content

Сильні сторони: чудово підходить для довгострокової пам'яті, дуже ощадлива щодо токенів, дані структуровані. Слабкі: архітектура важча, потрібна логіка видобування, і точну розмову відтворити не можна. Підходить для особистих асистентів, CRM-систем, помічників для працівників та інструментів керування відносинами з клієнтами.

Як це робиться в LangGraph

У старих туторіалах трапляються знайомі класи:

ConversationBufferMemory
ConversationSummaryMemory
ConversationBufferWindowMemory

Тепер це застарілі патерни, і сучасна робота з LangChain спирається на LangGraph. Раніше створювали об'єкт пам'яті:

memory = ConversationBufferMemory()

У новому стилі використовують чекпойнтер, наприклад

MemorySaver()

або

PostgresSaver()

для зберігання стану розмови. Обрізання, підсумки й обробка цього стану — вже ваша справа. Налаштування графа починається з таких імпортів:

from langgraph.graph import (
    StateGraph,
    MessagesState,
    START
)

а повне з'єднання разом з обрізанням виглядає так:

from langgraph.checkpoint.memory import (
    MemorySaver
)from langchain_core.messages import (
    trim_messages
)def call_model(state: MessagesState):    trimmed = trim_messages(
        state["messages"],
        strategy="last",
        max_tokens=10,
        token_counter=len
    )    response = llm.invoke(trimmed)    return {
        "messages": [response]
    }graph = StateGraph(MessagesState)graph.add_node("model", call_model)graph.add_edge(START, "model")app = graph.compile(
    checkpointer=MemorySaver()
)

Один момент часто збиває з пантелику: чекпойнтер не вирішує, що запам'ятовувати. Він вирішує лише, де живе пам'ять: у процесі, у базі даних чи в іншому сховищі. Вибір між буфером, вікном, підсумком, буфером із підсумком і сутностями так само за вами.

Чи можна зберігати пам'ять в Azure Blob Storage?

Так. Там можна тримати історію чатів, підсумки, дані сутностей і чекпойнти сесій. Але для пам'яті розмови в реальному часі він зазвичай підходить погано: читання повільніше, ніж у Redis, сховище не розраховане на часті читання й записи, і знадобиться додаткова логіка індексування. Типова корпоративна схема виглядає інакше:

Redis
    ↓
PostgreSQL
    ↓
Azure Blob Storage

У такій схемі Redis зберігає активні розмови, PostgreSQL — історію сесій, а Blob Storage архівує довгострокові дані.

Як обрати стратегію

Добирайте політику під продукт. Короткі чи внутрішні чати можуть жити на буферній пам'яті. Дешевим балакучим ботам вистачить вікна. Довгим сесіям, де формулювання мало важать, пасуватимуть підсумки. Усьому, що працює з клієнтами й довго, зазвичай потрібен буфер із підсумком, а всьому, що має пам'ятати сталі факти про людей чи організації, — пам'ять сутностей.

Наостанок

Устрій пам'яті — одне з найважливіших рішень в ШІ-продукті. Бот без пам'яті здається забудькуватим, а бот із безмежною пам'яттю стає дорогим і повільним. Мета не в тому, щоб пам'ятати все, а в тому, щоб пам'ятати потрібне.

Для більшості продакшен-застосунків нині найкращий баланс масштабу, вартості й користувацького досвіду дає поєднання буфера з підсумком, пам'яті сутностей і чекпойнтів LangGraph. Якщо ви будуєте асистентів на LangChain чи LangGraph, правильно влаштована пам'ять покращить якість і продуктивність більше, ніж майже будь-який трюк із промптами. Найрозумніші системи — не ті, що пам'ятають усе, а ті, що знають, що зберегти.