Engineering the Agentic Stack · Część 2

Architektura pamięci agentów AI: checkpointy i magazyny wektorowe

Tłumaczenie automatyczne Ten artykuł został automatycznie przetłumaczony z angielskiego oryginału.

Aktualizacja artykułu

Pierwotnie opublikowano 14 lutego 2026 r. Zaktualizowano i poddano przeglądowi 6 września 2026 r. Aktualizacja obejmuje kompaktowanie kontekstu, benchmarki pamięci oraz API magazynów, a także wyjaśnia różnice między stanem roboczym, checkpointami i pamięcią długoterminową.

Pętla rozumowania przetrwa tylko jedno żądanie, chyba że jej stan zostanie zapisany poza workerem. Bez pamięci agenta agent nie może wznowić wstrzymanego planu, odzyskać działania po awarii ani przypomnieć sobie preferencji z wcześniejszej sesji. Część 1 omawiała przepływ sterowania. W tym wpisie identyfikuję, jakiego stanu potrzebuje każda kolejna tura i gdzie ten stan powinien się znajdować.

Jako punkt odniesienia dla omówienia hot checkpointów wykorzystam Market Analyst Agent — niewielkiego agenta LangGraph, który pobiera dane rynkowe i tworzy raport analityczny. Sekcje dotyczące cold vectorów i surowego Markdownu to niezależne, ilustracyjne projekty pokazujące rozszerzenia, których obecny projekt jeszcze nie implementuje. Następnie omówię, kiedy warto użyć PostgreSQL, Redis, Qdrant, magazynów klucz-wartość i zwykłych plików Markdown.

Każdy z opisanych niżej magazynów jest odczytywany przez harness — kod sterujący pętlą wokół modelu. To harness decyduje, które elementy ich zawartości trafią do context window; magazyny same o tym nie decydują. Ten artykuł dotyczy miejsca przechowywania stanu, zanim harness po niego sięgnie. Część 3 i Część 4 omawiają, co harness robi następnie z promptem.


Czym jest pamięć agenta AI?

Pamięć agenta AI to warstwa stanu, która pozwala agentowi zachowywać postęp zadania, pobierać wcześniejszą wiedzę i aktualizować to, co wie, między kolejnymi uruchomieniami. Projekt może wykorzystywać checkpointy, magazyny semantyczne lub ustrukturyzowane oraz dokumenty czytelne dla człowieka. Wybieraj tylko te magazyny, które są potrzebne z punktu widzenia wymagań produktu dotyczących odtwarzania i odzyskiwania stanu.

PotrzebaNajlepszy domyślny wybórDlaczego
Wstrzymanie i wznowienie jednego uruchomieniaMagazyn checkpointów PostgreSQLTrwały, odpytywalny i łatwy w obsłudze wraz z danymi aplikacji
Nietrwały stan o niskich opóźnieniachMagazyn checkpointów RedisSzybkie wznowienie i krótkotrwały stan, z kompromisami dotyczącymi trwałości
Semantyczne przypominanie między wątkamiQdrant lub pgvectorPobiera wspomnienia według znaczenia, a nie tylko dokładnych kluczy
Ustrukturyzowane fakty o użytkownikuPostgreSQL lub magazyn klucz-wartośćDeterministyczne aktualizacje są lepsze niż rozmyte wyszukiwanie preferencji i ID
Konwencje projektowe i poznane proceduryPliki Markdown lub JSONCzytelne dla człowieka, obsługują diffy i łatwe do aktualizowania przez agentów
Pamięć relacji między wieloma encjamiGraf wiedzyPrzydatny, gdy relacje są ważniejsze niż pojedyncze fakty

Nie zaczynaj od pamięci tylko dlatego, że brzmi inteligentnie. Zacznij od widocznej dla użytkownika awarii: utraty postępu, zapomnienia preferencji, powtarzania badań lub braku możliwości ponownego wykorzystania konwencji projektowej.

Awarie wymagające pamięci

Agent bez stanu może odpowiedzieć na izolowane pytanie, ale zapomina żądanie natychmiast po zakończeniu wywołania. Taki projekt zawodzi, gdy produkt musi obsługiwać którekolwiek z poniższych zachowań:

  • Wstrzymywanie i wznawianie: użytkownik rozpoczyna zadanie badawcze, zamyka laptopa i wraca następnego dnia. Bez stanu zapisanego w checkpointach agent zacznie od początku.
  • Spójność w wielu turach: podczas długiej rozmowy agent musi pamiętać, z jakich narzędzi korzystał, jakie dane zebrał i które kroki planu ukończył.
  • Personalizacja: powracający użytkownik oczekuje, że agent będzie znał jego tolerancję ryzyka, preferowaną głębokość analizy i wcześniejsze interakcje.
  • Human-in-the-loop (HITL): agent zbiera dowody i czeka na zgodę człowieka przed wykonaniem kolejnego kroku. Stan oczekiwania musi przetrwać restart procesu.

W Market Analyst Agent z Części 1 żądanie „Analyze NVDA” generuje plan, pięć wywołań narzędzi, zebrane dane i szkic raportu. Gdy użytkownik odpowiada „wygląda dobrze, ale dodaj analizę konkurencji”, checkpoint odtwarza plan i badania z ostatniego ukończonego kroku. Dodanie kroku dotyczącego konkurencji wymagałoby interpretacji kolejnej wiadomości i ponownego planowania; towarzyszący projekt nie implementuje tego zachowania. Checkpoint dostarcza wcześniejszy stan, ale aplikacja musi zdecydować, jak nowe żądanie zmienia plan.

Pamięć długoterminowa obsługuje inny przypadek. Jeśli użytkownik wróci tydzień później i poprosi: „Update my NVDA analysis”, agent może potrzebować przypomnienia sobie preferencji dotyczącej konserwatywnych ocen ryzyka oraz zainteresowania akcjami spółek półprzewodnikowych. Magazyn pamięci oparty na wektorach może pobrać te fakty między sesjami bez ponownego pytania o nie.

Przykłady implementacji poniżej używają LangGraph, biblioteki open source LangChain do budowania agentów jako jawnych grafów stanu; wyznaczone przez nią granice magazynowania można uogólnić na dowolny framework. Pomyśl o trwającej rozmowie użytkownika „Analyze NVDA” jako o jednym wątku. Każde uruchomienie grafu w celu udzielenia odpowiedzi lub kontynuowania rozmowy jest jednym uruchomieniem w tym wątku. Gdy uruchomienie jest aktywne, kontekst modelu i lokalne zmienne programu stanowią jego pamięć roboczą; znikają, gdy praca zostaje zatrzymana. LangGraph nazywa stan zapisany dla tego jednego wątku pamięcią krótkoterminową, a fakty dostępne dla innych wątków — pamięcią długoterminową. Dalej „wątek” i „rozmowa” oznaczają to samo. Część 5 używa terminu „sesja” dla trwałego dziennika jednego uruchomienia, dlatego w tym artykule unikam go w odniesieniu do rozmowy.

Sześć typów pamięci agenta i trzy warstwy magazynowania, do których się sprowadzająSześć typów pamięci agenta i trzy warstwy magazynowania, do których się sprowadzają


Taksonomia pamięci agentów AI

Przed przejściem do implementacji warto sklasyfikować to, co agenci muszą pamiętać. Framework CoALA — Cognitive Architectures for Language Agents (Sumers, Yao i in., 2023) — to szeroko cytowana taksonomia oparta na kognitywistyce. Zakres pamięci wprowadziłem w moim wpisie o inżynierii kontekstu; tutaj rozwijam go do sześciu kategorii:

Typ pamięciZakresCzas życiaPrzykładWzorzec magazynowania
RoboczaBieżący krokMilisekundyArgumenty wywołania narzędzia, bieżąca odpowiedź LLMW procesie (słownik Python)
KrótkoterminowaBieżący wątekMinuty–godzinyHistoria rozmowy, postęp planu, zebrane daneMagazyn checkpointów
EpizodycznaMiędzy wątkamiDni–miesiące„W zeszłym tygodniu użytkownik pytał o wyniki NVDA”Magazyn wektorowy / magazyn KV
SemantycznaMiędzy wątkamiMiesiące–bezterminowo„Użytkownik preferuje konserwatywne inwestycje”Magazyn wektorowy / magazyn KV
DokumentowaMiędzy wątkamiDni–bezterminowoNotatki projektowe, podsumowania badań, poznane wzorceMagazyn plików (Markdown/JSON)
ProceduralnaCały systemBezterminowo„Podczas analizy akcji zawsze sprawdzaj dokumenty SEC”Konfiguracja / prompt systemowy

Pamięć robocza przechowuje bieżące obserwacje, pobrane fakty i wyniki pośrednie używane przez aktywne uruchomienie. Część z nich znajduje się w zmiennych aplikacji; wybrane wiadomości oraz wyniki narzędzi tworzą wejście modelu. To wejście musi zmieścić się w context window modelu, natomiast stan aplikacji może być większy i przetrwać wiele kroków. Pamięć procesu jest tracona podczas awarii, chyba że zostanie jawnie zapisana. Pozostałe warstwy dostarczają informacji do tego stanu roboczego.

Pamięć krótkoterminowa to checkpoint zapisywany przez LangGraph po każdej jednostce wykonania grafu — super-kroku, zdefiniowanym w następnej sekcji. Pamięć epizodyczna i semantyczna utrzymują się między wątkami. Pamięć dokumentowa przechowuje notatki projektowe, podsumowania badań i poznane konwencje w plikach, które mogą przeglądać ludzie i agenci. Pamięć proceduralna obejmuje instrukcje systemowe, definicje narzędzi i procedury wielokrotnego użytku, które można pobierać do zadania. Czasy życia w tabeli są przykładowe; retencja wynika z polityki aplikacji, a stan roboczy może trwać przez całe aktywne uruchomienie.

Na potrzeby implementacji pięć z sześciu typów sprowadza się do trzech warstw magazynowania. Pamięć krótkoterminowa staje się pamięcią hot — checkpointem bieżącego wątku. Pamięć epizodyczna i semantyczna stają się pamięcią cold — przypominaniem informacji między wątkami. Pamięć dokumentowa zachowuje zgromadzoną wiedzę projektową w formie czytelnej i bezpośrednio edytowalnej. Pamięć robocza jest grupowana z warstwą hot, ponieważ checkpointy mogą przechowywać stan potrzebny do odtworzenia aktywnego uruchomienia. Checkpoint nie jest pełnym wewnętrznym procesem obliczeniowym modelu. Procedury mogą być dostarczane wraz z agentem albo przechowywane i pobierane z plików lub innego magazynu. Warstwy te opisują wybory implementacyjne tego artykułu, a nie wzajemnie wykluczające się typy pamięci.

CoALA klasyfikuje pamięć roboczą, epizodyczną, semantyczną i proceduralną. Przegląd Memory in the Age of AI Agents organizuje natomiast pamięć według formy, funkcji i dynamiki, uwzględniając dokumenty, bazy kodu i przepływy pracy wielokrotnego użytku. Pliki mogą implementować kilka z tych kategorii. Ten artykuł wyróżnia pamięć dokumentową, aby uwidocznić jej odpowiedzialność za przechowywanie i utrzymanie danych.

Ten sam wzorzec magazynowania występuje w innych domenach. Agent Minecraft (Voyager) przechowuje wielokrotnego użytku umiejętności gry jako biblioteki kodu, a agenci webowi wyprowadzają wielokrotnego użytku przepływy przeglądania z udanych uruchomień. Do obu przypadków wrócę później. Pliki, które można przeglądać, i indeksowane wyszukiwanie mogą współistnieć: Voyager pobiera programy na podstawie embeddingów ich opisów.

Pamięć zarządzana przez agenta różni się również od stałego pipeline’u RAG tym, kto wykonuje zapis. Agent lub jego harness wybiera, co przechowywać, aktualizować i usuwać, a później decyduje, kiedy to pobrać.

Artykuł Generative Agents (Park i in., 2023) pokazał, jak daleko można posunąć ten model: symulowani agenci przechowywali, analizowali i pobierali własne wspomnienia. Jego strumień pamięci porządkował kandydatów według świeżości, ważności i trafności — to projekt, który nadal stanowi użyteczny punkt odniesienia dla wyszukiwania pamięci agenta.


Kompaktowanie utrzymuje użyteczność rozmowy

Większe context window nie eliminuje potrzeby wyboru tego, co ma przetrwać. Obecne API mogą podsumować starszą część rozmowy, zanim wypełni ona okno. Kompaktowanie po stronie serwera w Claude, nadal będące funkcją beta na dzień 6 września 2026 r., zwraca blok compaction, którego kolejne żądania używają zamiast wcześniejszej zawartości. Może to ograniczyć pracę związaną z podsumowywaniem po stronie klienta, ale podsumowanie może pominąć fakt potrzebny później.

Autorytatywny stan zadania przechowuj poza tym podsumowaniem: ukończone efekty, zgody, odwołania do źródeł i dokładne ograniczenia użytkownika. Checkpoint odtwarza wykonanie; kompaktowanie skraca kontekst modelu; pamięć długoterminowa wybiera wiedzę dla innej rozmowy. Testuj te trzy zachowania oddzielnie. Wymuś kompaktowanie w połowie testu i sprawdź, czy następna akcja nadal respektuje wcześniejsze ograniczenie. Nie używaj skompaktowanego transkryptu jako jedynego zapisu tego, co zostało zatwierdzone.

Pamięć krótkoterminowa agenta: magazyn checkpointów

LangGraph zapisuje checkpointy na granicach super-kroków — pojedynczego węzła lub partii węzłów uruchomionych równolegle. Przy domyślnym durability="async" następny krok może rozpocząć się, gdy zapis jest jeszcze wykonywany; durability="sync" czeka na utrwalenie danych przed kontynuacją, zwiększając opóźnienie zapisu. Odzyskiwanie po awarii korzysta z ostatniego utrwalonego checkpointu, niekoniecznie z ostatniego ukończonego kroku. To podstawa wstrzymywania i wznawiania, debugowania z cofaniem w czasie oraz przepływów HITL.

Pamięć hot: checkpoint zapisywany po każdym super-kroku oraz ścieżka odzyskiwania, która go ponownie ładujePamięć hot: checkpoint zapisywany po każdym super-kroku oraz ścieżka odzyskiwania, która go ponownie ładuje

Checkpoint zawiera stan grafu potrzebny do wznowienia: AgentState z Części 1 — wiadomości, tożsamość, profil użytkownika, kroki planu, dane badawcze i tryb wykonania. Po przerwaniu HITL lub restarcie procesu LangGraph przywraca ostatni zapisany stan i korzysta z metadanych harmonogramowania, aby wybrać następny węzeł. Wznawia działanie na granicy ukończonego węzła, a nie w dowolnej linii kodu Python. Zapisane szczegóły obejmują ID checkpointu i znacznik czasu, wersję każdego kanału (tak LangGraph nazywa klucz stanu) oraz wersje kanałów, które każdy węzeł już widział. Numer kroku jest metadanymi tego checkpointu. Checkpoint różni się również od dołączanego tylko dziennika zdarzeń lub trace’a; Część 5 wyraźnie rozdziela te powierzchnie obserwowalności środowiska uruchomieniowego.

Jak działa checkpointing w LangGraph

BaseCheckpointSaver LangGraph to prosty interfejs: put() zapisuje checkpoint, get_tuple() odczytuje najnowszy checkpoint wątku, a list() zwraca historię. Każdy checkpoint jest identyfikowany przez (thread_id, checkpoint_ns, checkpoint_id), gdzie thread_id identyfikuje rozmowę, checkpoint_ns obsługuje przestrzeń nazw podgrafów, a checkpoint_id jest unikatową wersją.

Najważniejsza decyzja dotyczy backendu, który znajdzie się za tym interfejsem. PostgreSQL i Redis to dwa popularne wybory produkcyjne.

PostgreSQL a Redis

Redis i PostgreSQL jako backendy checkpointów — porównanie opóźnienia, trwałości i modelu zapytańRedis i PostgreSQL jako backendy checkpointów — porównanie opóźnienia, trwałości i modelu zapytań

WymiarPostgreSQL (langgraph-checkpoint-postgres)Redis (langgraph-checkpoint-redis)
Model trwałościTransakcje ACID, WAL i replikacjaKonfigurowalna trwałość: dziennik poleceń tylko do dołączania (AOF) lub okresowe snapshoty (RDB)
Historia checkpointówTrwała historia na potrzeby wznowienia i debugowaniaRetencja zależy od savera i ustawień eksmisji
Główne ograniczenieOpóźnienie zapisu do bazy i wzrost tabelZużycie RAM, eksmisja i konfiguracja trwałości
Dopasowanie operacyjneZespoły, które już obsługują relacyjne bazy danychZespoły, które już obsługują Redis przy dużej przepustowości
Najlepszy wybór domyślnyTrwałe wznowienie i powtarzalne debugowanieStan sesji wrażliwy na opóźnienia, ale możliwy do odzyskania

Ogólne benchmarki baz danych nie przewidują wydajności checkpointów. Zmierz rozmiar serializowanego stanu, częstotliwość zapisów, ustawienia trwałości i współbieżność własnego grafu.

PostgreSQL: trwały wybór domyślny

PostgreSQL jest bezpieczniejszym wyborem domyślnym dla większości zespołów. Checkpointy przetrwają awarie, otrzymujesz pełną semantykę transakcji, a historia checkpointów upraszcza debugowanie z cofaniem w czasie.

Uproszczona wersja konfiguracji checkpointów z memory/hot.py. Jeśli atakujący mógłby zapisywać checkpointy, ustaw LANGGRAPH_STRICT_MSGPACK=true lub skonfiguruj allowed_msgpack_modules. Ogranicza to deserializację do bezpiecznych lub jawnie zadeklarowanych typów; domyślna, liberalna konfiguracja ostrzega o niezarejestrowanych typach, ale nadal na nie pozwala.

import asyncio
from contextlib import asynccontextmanager

from langchain_core.messages import HumanMessage
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver

@asynccontextmanager
async def postgres_checkpointer(connection_string: str):
    """Yield a PostgreSQL-backed checkpoint store.

    PostgreSQL gives us ACID guarantees — if a checkpoint write succeeds,
    the state is durable even if the process crashes immediately after.
    `from_conn_string` is itself an async context manager: it owns the
    connection and closes it on exit, so the graph has to run inside it.
    """
    async with AsyncPostgresSaver.from_conn_string(connection_string) as checkpointer:
        # Create the checkpoint tables if they don't exist.
        # This is idempotent — safe to call on every startup.
        await checkpointer.setup()
        yield checkpointer

async def main(authenticated_user_id: str) -> None:
    # The graph lives inside the context manager's scope.
    async with postgres_checkpointer(
        "postgresql://user:pass@localhost:5432/agent_memory"
    ) as checkpointer:
        graph = create_graph(checkpointer=checkpointer)

        # Every invoke/stream call now persists state automatically.
        config = {"configurable": {"thread_id": "user-123-session-1"}}
        result = await graph.ainvoke(
            {"user_id": authenticated_user_id,
             "messages": [HumanMessage(content="Analyze NVDA")]}, config
        )

        # After the server authenticates the approver and validates approval
        # of this exact draft, update the companion's approval field.
        await graph.aupdate_state(config, {"report_approved": True})
        # Continue the static interrupt_before pause; new input starts a new run.
        result = await graph.ainvoke(None, config)

# Local fixture identity. A server supplies this only after authentication.
asyncio.run(main(authenticated_user_id="user-123"))

user_id w wejściu grafu pochodzi z uwierzytelnionego kontekstu serwera; thread_id jedynie lokalizuje checkpointy i nie ustanawia tożsamości ani nie autoryzuje dostępu do wątku. AsyncPostgresSaver korzysta z pakietu langgraph-checkpoint-postgres, który tworzy cztery tabele: checkpoints (serializowany stan), checkpoint_blobs (duże dane binarne), checkpoint_writes (oczekujące zapisy na potrzeby odzyskiwania po awarii) oraz checkpoint_migrations (wersja schematu). Współbieżni writerzy są rozdzielani przez klucz główny (thread_id, checkpoint_ns, checkpoint_id) i upserty, a nie przez blokady — dwóch workerów działających na tym samym wątku nie uszkodzi wzajemnie danych, ale też nie będzie się ze sobą koordynować.

Redis: gdy wąskim gardłem jest opóźnienie

Gdy opóźnienie checkpointów jest wąskim gardłem, Redis jest opcją dla stanu możliwego do odzyskania. Przed wyborem Redisa zamiast PostgreSQL zmierz rozmiar serializowanego stanu, ustawienia trwałości i współbieżność.

Uproszczona wersja konfiguracji checkpointów z memory/hot.py:

import asyncio
from contextlib import asynccontextmanager

from langgraph.checkpoint.redis.aio import AsyncRedisSaver

@asynccontextmanager
async def redis_checkpointer(redis_url: str):
    """Yield a Redis-backed checkpoint store.

    Redis keeps checkpoints in memory for low-latency access.
    Durability depends on RDB snapshots, AOF fsync policy, and replication.
    AOF with appendfsync everysec can still lose about one second of writes
    after a crash; enabling AOF alone is not a no-loss guarantee.
    """
    async with AsyncRedisSaver.from_conn_string(redis_url) as checkpointer:
        # Initialize Redis data structures
        await checkpointer.asetup()
        yield checkpointer

async def main() -> None:
    # Same graph API, different backend.
    async with redis_checkpointer("redis://localhost:6379") as checkpointer:
        graph = create_graph(checkpointer=checkpointer)

asyncio.run(main())

AsyncRedisSaver z langgraph-checkpoint-redis przechowuje każdy checkpoint jako osobny dokument RedisJSON pod tym samym kluczem (thread_id, checkpoint_ns, checkpoint_id) co saver Postgresa. Przeprojektowanie w wersji v0.1.0 umieściło wartości checkpointów bezpośrednio w dokumencie i zastąpiło pobieranie poszczególnych kanałów ścieżką JSON.GET. Ta zmiana dotyczy pobierania wartości, a nie każdej operacji trwałego zapisu; pomiary opóźnień producenta zależą od obciążenia. Redis 8.0+ zawiera RedisJSON i RediSearch domyślnie — nie trzeba instalować dodatkowych modułów.

Wybierz politykę trwałości i fsync w Redisie zgodnie z akceptowanym oknem utraty danych. RDB może utracić zapisy od ostatniego snapshota; typowa polityka AOF appendfsync everysec może utracić około jednej sekundy danych. always zamienia opóźnienie zapisu na silniejszą trwałość, natomiast no pozostawia opróżnianie buforów systemowi operacyjnemu. Testuj odzyskiwanie przy rzeczywistych ustawieniach dysku i replikacji.

W przypadku wdrożeń z ograniczoną pamięcią ShallowRedisSaver przechowuje tylko najnowszy checkpoint każdego wątku — bez historii, ale przy minimalnym zużyciu RAM. Użyj tego rozwiązania, gdy potrzebujesz wstrzymywania i wznawiania, ale nie potrzebujesz debugowania z cofaniem w czasie.

Kiedy użyć którego rozwiązania

Użyj PostgreSQL, gdy:

  • Potrzebujesz pełnej historii checkpointów do debugowania z cofaniem w czasie lub powtarzalnego wznawiania
  • Trwałość danych jest niepodlegająca negocjacji (usługi finansowe, ochrona zdrowia)
  • PostgreSQL już działa w Twoim stacku
  • Agent wykonuje długie zadania, w których utrata stanu oznacza godziny ponownych obliczeń
  • Chcesz mieć ujednolicony magazyn danych — PostgreSQL z pgvector może być jednym backendem dla checkpointów, pamięci długoterminowej i wyszukiwania wektorowego

Użyj Redis, gdy:

  • Opóźnienie checkpointów jest Twoim wąskim gardłem (czat w czasie rzeczywistym, streamingowy UX)
  • Budujesz boty głosowe lub doświadczenia streamingowe, w których dostęp do checkpointów znajduje się na ścieżce krytycznej pod względem opóźnień
  • Potrzebujesz skalowania horyzontalnego między wieloma niezależnymi wątkami. Jeśli kilku agentów modyfikuje współdzielony stan, przypisz właściciela temu stanowi i koordynuj działania poza saverem checkpointów.
  • Obsługujesz krótkotrwałe sesje, w których utrata checkpointu jest możliwa do naprawienia
  • Chcesz używać cache’owania semantycznego do ograniczenia zbędnych wywołań LLM (Redis LangCache przechowuje semantycznie podobne zapytania w cache, aby unikać powtarzających się wywołań LLM)

Inne opcje: langgraph-checkpoint-sqlite działa w lokalnym środowisku programistycznym i wdrożeniach jednoprokesowych. W stackach natywnych dla AWS langgraph-checkpoint-aws udostępnia DynamoDBSaver z automatycznym przenoszeniem payloadów — udokumentowany saver przenosi dane powyżej progu 350 KB, gdy skonfigurowany jest bucket S3. Ten próg wynika z polityki implementacji, a nie z limitu 400 KB elementu DynamoDB. Ceny serverless i brak infrastruktury do zarządzania sprawiają, że rozwiązanie to jest atrakcyjne przy zmiennym obciążeniu.


Pamięć długoterminowa: pamiętanie między sesjami

Pamięć hot obsługuje bieżącą rozmowę. Pamięć długoterminowa dotyczy użytkownika, który wraca w następnym tygodniu: przechowuje fakty, preferencje i historię interakcji utrzymujące się między wątkami.

LangGraph udostępnia interfejs Store do pamięci między wątkami za pośrednictwem klasy BaseStore. Każdy element pamięci jest parą (namespace, key) zawierającą wartość JSON i opcjonalny embedding wektorowy. Przestrzeń nazw zazwyczaj koduje użytkownika lub organizację: ("user", "user-123", "preferences").

Ścieżka pobierania pamięci cold: osadź zapytanie, wyszukaj w Qdrant z filtrem użytkownika, przelicz wynik i wstrzyknijŚcieżka pobierania pamięci cold: osadź zapytanie, wyszukaj w Qdrant z filtrem użytkownika, przelicz wynik i wstrzyknij

Magazyn wektorowy: semantyczne przypominanie z Qdrant

Gdy agent musi przypomnieć sobie nieustrukturyzowane fakty („Co użytkownik powiedział o swoim horyzoncie inwestycyjnym?”), wyszukiwanie wektorowe zapewnia semantyczne przypominanie. Zamiast dokładnych wyszukiwań po kluczu agent odpytuje magazyn według znaczenia.

Qdrant to wyspecjalizowana baza wektorowa napisana w Rust, obsługująca przechowywanie embeddingów, indeksowanie (Hierarchical Navigable Small World, czyli HNSW) i wyszukiwanie z filtrami. Szczegółowo omawiałem HNSW i jego kompromisy w moim wpisie o rankingu wyszukiwania. Qdrant oferuje również serwer MCP, który działa jako semantyczna warstwa pamięci — przydatna, jeśli framework agenta obsługuje Model Context Protocol.

Poniższy przykład to niezależny, ilustracyjny projekt z Qdrant. Nie jest uproszczoną wersją bieżącego memory/long.py. Obecny projekt przechowuje profile użytkowników z dokładnym filtrowaniem po user_id i placeholderem w postaci wektora zerowego. Rzeczywista integracja embeddingów pozostaje zadaniem na przyszłość. Handler żądania musi uwierzytelnić żądanie i utworzyć principal na podstawie zweryfikowanej tożsamości; klient nigdy jej nie dostarcza. Filtr Qdrant określa zakres pobierania, a nie autoryzację.

from qdrant_client import QdrantClient
from qdrant_client.models import (
    PointStruct, Distance, VectorParams, Filter, FieldCondition, MatchValue,
)
import hashlib
import json
from dataclasses import dataclass

@dataclass(frozen=True)
class AuthenticatedPrincipal:
    """Created by the server after authentication, never from request JSON."""
    user_id: str

class UserMemoryStore:
    """Long-term memory backed by Qdrant vector search.

    Stores user facts as embedded vectors for semantic retrieval.
    Each fact is a short natural-language statement about the user.
    """

    def __init__(self, qdrant_url: str, collection_name: str = "user_memory"):
        self.client = QdrantClient(url=qdrant_url)
        self.collection_name = collection_name
        self._ensure_collection()

    def _ensure_collection(self):
        """Create the collection if it doesn't exist."""
        collections = [c.name for c in self.client.get_collections().collections]
        if self.collection_name not in collections:
            self.client.create_collection(
                collection_name=self.collection_name,
                vectors_config=VectorParams(
                    size=1536,  # text-embedding-3-small dimensions
                    distance=Distance.COSINE,
                ),
            )

    def store_fact(
        self, principal: AuthenticatedPrincipal, fact: str, embedding: list[float]
    ):
        """Store a user fact with its embedding."""
        identity = json.dumps([principal.user_id, fact], ensure_ascii=False).encode()
        point_id = hashlib.sha256(identity).hexdigest()[:32]
        self.client.upsert(
            collection_name=self.collection_name,
            points=[PointStruct(
                id=point_id,
                vector=embedding,
                payload={"user_id": principal.user_id, "fact": fact},
            )],
        )

    def recall(
        self,
        principal: AuthenticatedPrincipal,
        query_embedding: list[float],
        top_k: int = 5,
    ):
        """Retrieve the most relevant facts for a user given a query."""
        results = self.client.query_points(
            collection_name=self.collection_name,
            query=query_embedding,
            query_filter=Filter(
                must=[FieldCondition(
                    key="user_id", match=MatchValue(value=principal.user_id)
                )]
            ),
            limit=top_k,
        )
        return [hit.payload["fact"] for hit in results.points]

ID punktu jest skrótem tablicy JSON zawierającej ID użytkownika i fakt, dzięki czemu separatory wewnątrz którejkolwiek wartości nie mogą połączyć dwóch tożsamości. Na przykład użytkownik a:b z faktem c musi różnić się od użytkownika a z faktem b:c. 32 znaki szesnastkowe pasują do reprezentacji ID punktu UUID w Qdrant.

Przepływ składa się z trzech kroków. W tym ilustracyjnym projekcie LLM wyodrębnia kluczowe fakty z interakcji („użytkownik ma wysoką tolerancję ryzyka”, „użytkownik interesuje się akcjami spółek półprzewodnikowych”). Fakty te są osadzane i przechowywane w Qdrant. Na początku kolejnej rozmowy serwer przekazuje uwierzytelnionego principal, a agent odpytuje Qdrant nową wiadomością użytkownika, aby przypomnieć sobie istotny kontekst. Obecny Market Analyst Agent nie implementuje jeszcze tego przepływu ekstrakcji semantycznej i embeddingów.

Scoring pobierania: więcej niż podobieństwo cosinusowe

Surowe podobieństwo cosinusowe to punkt wyjścia, ale produkcyjne systemy pamięci wymagają bogatszego pobierania. Artykuł Generative Agents (Park i in., 2023) wprowadził funkcję scoringową łączącą trzy sygnały:

  • Świeżość: oparte na regułach wygaszanie, dzięki któremu nowsze wspomnienia otrzymują wyższe wyniki. Wykładnicza funkcja zaniku sprawia, że fakt z wczoraj wyprzedza równoważny fakt sprzed sześciu miesięcy.
  • Ważność: znaczenie oceniane przez LLM w skali 1–10. „Portfel użytkownika stracił 40%” otrzymuje wyższy wynik niż „użytkownik powiedział cześć”.
  • Trafność: podobieństwo cosinusowe embeddingu zapytania i zapisanego faktu.

Artykuł normalizuje wszystkie trzy sygnały do porównywalnych skal przed ich połączeniem. Zrób to samo przed dostrojeniem wag; w przeciwnym razie surowa ważność 1–10 zdominuje sygnał 0–1. Końcowy wynik pobierania jest sumą ważoną: score = alpha * recency + beta * importance + gamma * relevance. Dzięki temu świeże i ważne fakty nie zostaną zakopane pod starymi, ale semantycznie podobnymi. Dla prototypu analizy finansowej zacząłbym od alpha = 0.3 dla świeżości, beta = 0.2 dla ważności i gamma = 0.5 dla trafności, ponieważ to bieżące zapytanie zwykle decyduje, który z poprawnych skądinąd faktów powinien trafić do kontekstu. Artykuł Generative Agents używał równych wag; podane wartości są proponowanym punktem wyjścia, a nie zmierzoną poprawą. Dostrój je na podstawie held-out recall i kontroli jakości zadania, zanim zaczniesz na nich polegać.

Alternatywy dla wyszukiwania wektorowego

Wyszukiwanie wektorowe jest potężne, ale nie zawsze jest właściwym narzędziem. Oto kiedy warto użyć alternatyw:

PodejścieNajlepsze zastosowanieGłówny koszt operacyjny
Wyszukiwanie wektorowe (Qdrant)Semantyczne przypominanie nieustrukturyzowanych faktówCykl życia embeddingów i indeksu
Magazyn klucz-wartość (Redis)Ustrukturyzowane profile i preferencje użytkownikówZużycie pamięci i polityka trwałości
Magazyn dokumentów (pliki)Wiedza projektowa i notatki zarządzane przez agentaWspółbieżność, uprawnienia i wyszukiwanie
Wyszukiwanie pełnotekstowe (PostgreSQL indeks GIN)Wyszukiwanie słów kluczowych w historii rozmówWzrost indeksu i strojenie zapytań
Graf wiedzy (Neo4j)Relacje encji i zapytania wieloskokoweModelowanie grafu i kolejny system danych
Hybrydowe (wektorowe + słowa kluczowe)Przypominanie przy zmiennym znaczeniu zapytaniaDwie ścieżki scoringu do strojenia i ewaluacji

Magazyny klucz-wartość dobrze sprawdzają się dla danych ustrukturyzowanych. Jeśli pamięć długoterminowa to profil użytkownika — tolerancja ryzyka, horyzont inwestycyjny, preferowane sektory — hash Redis lub kolumna JSONB w PostgreSQL są prostsze i szybsze niż osadzanie oraz odpytywanie wektorów. Wyszukiwania wektorowego używaj, gdy pamięć jest nieustrukturyzowana, a zapytania pobierające różnią się sposobem sformułowania.

Wbudowany Store LangGraph udostępnia interfejs klucz-wartość oparty na przestrzeni nazw, z opcjonalnym wyszukiwaniem wektorowym. API BaseStore jest proste: put(), get(), search() i delete() z hierarchicznym zakresem przestrzeni nazw. Dostępne są trzy implementacje:

  • InMemoryStore — do programowania i testów (dane są tracone po zakończeniu procesu)
  • PostgresStore — produkcyjny, trwały magazyn z pełnym odpytywaniem SQL
  • AsyncRedisStore — pamięć między wątkami z wyszukiwaniem wektorowym, obsługą TTL i filtrowaniem metadanych

Konfiguracja index włącza wyszukiwanie wektorowe zapisanych elementów z użyciem konfigurowalnego modelu embeddingowego. W wielu przypadkach ten wbudowany Store wystarczy bez sięgania po dedykowaną bazę wektorową.

import asyncio
from langgraph.store.memory import InMemoryStore

# Create a store with vector search enabled
store = InMemoryStore(
    index={
        "dims": 1536,
        "embed": my_embedding_function,  # e.g., OpenAI text-embedding-3-small
    }
)

async def main() -> None:
    # Store a user preference (namespace scopes to user).
    await store.aput(
        namespace=("user", "user-123", "preferences"),
        key="risk-profile",
        value={"risk_tolerance": "high", "horizon": "long-term"},
    )

    # Semantic search across the user's memories.
    # The namespace prefix is positional here — `search`/`asearch` declare it
    # as positional-only `namespace_prefix`, unlike `aput`.
    results = await store.asearch(
        ("user", "user-123"),
        query="What is their investment style?",
        limit=5,
    )

asyncio.run(main())

Wybór strategii pamięci długoterminowej

Zacznij od klucz-wartość, jeśli pamięć jest ustrukturyzowana i dobrze zdefiniowana (profile użytkowników, ustawienia, nazwane encje). Dodaj wyszukiwanie wektorowe, gdy potrzebujesz semantycznego pobierania nieustrukturyzowanych faktów lub gdy sposób formułowania zapytań jest nieprzewidywalnie zmienny.

Grafy wiedzy są uzasadnione, gdy znaczenie mają relacje między encjami, np. „O które firmy pytał użytkownik, które są konkurentami NVDA?”. Najciekawszym nowszym projektem jest Graphiti (firmy Zep), który buduje graf wiedzy uwzględniający czas, śledzący kiedy fakty były prawdziwe, a nie tylko co było prawdziwe. Jego relacje czasowe mogą zachowywać przedziały ważności i wartości zastąpione nowszymi; logika ekstrakcji i aktualizacji nadal decyduje, czy fakt jest aktualny. Artykuł Zep raportuje dokładność DMR na poziomie 94,8% dla ocenianego systemu Zep opartego na Graphiti i GPT-4 Turbo, w porównaniu z 94,4% dla pełnego kontekstu. DMR wykorzystuje rozmowy liczące 60 wiadomości i ograniczone zadanie pobierania faktów. Tak niewielka różnica nie dowodzi ogólnej przewagi grafów czasowych.

Problemem jest operacyjność. Uruchamianie bazy grafowej nie jest trywialne, a w większości aplikacji agentowych wyszukiwanie wektorowe z filtrowaniem metadanych zapewnia podobne możliwości przy mniejszej infrastrukturze.

Zarządzane frameworki pamięci, takie jak Mem0 i Letta (wcześniej MemGPT), obsługują za Ciebie pipeline ekstrakcji, konsolidacji i pobierania. Podejście Mem0 jest warte uwagi: LLM wyodrębnia potencjalne wspomnienia, silnik decyzyjny porównuje każdy nowy fakt z istniejącymi wpisami w magazynie wektorowym, a resolver decyduje, czy dodać, zaktualizować, usunąć wpis czy nie robić nic. Dzięki temu magazyn pamięci pozostaje spójny i pozbawiony redundancji. Letta przyjmuje perspektywę systemu operacyjnego: agenci zarządzają własnym context window za pomocą narzędzi do zarządzania pamięcią, autonomicznie przenosząc dane między „pamięcią podstawową” (w kontekście) i „pamięcią archiwalną” (poza kontekstem). Oba rozwiązania warto ocenić, jeśli zależy Ci na szybszym wejściu na produkcję i nie potrzebujesz pełnej kontroli nad pipeline’em pamięci.


Pamięć dokumentowa: szafka z dokumentami agenta

Magazyny wektorowe i backendy klucz-wartość dobrze obsługują semantyczne przypominanie i ustrukturyzowane wyszukiwanie. Zgromadzony kontekst projektu — konwencje, notatki badawcze i decyzje przenoszone między sesjami — często powinien znajdować się w plikach, które ludzie mogą czytać, przeglądać i wersjonować.

To jest pamięć dokumentowa: agent odczytuje i zapisuje ustrukturyzowane pliki (Markdown, JSON, YAML) w znanym katalogu. Bez embeddingów, bez bazy danych i bez infrastruktury. Po prostu pliki na dysku, które zarówno agent, jak i deweloper mogą cat, grep, git diff oraz ręcznie edytować.

W jednej ewaluacji przeprowadzonej przez dostawcę Letta zaraportowała 74,0% dokładności na LoCoMo — benchmarku pytań i odpowiedzi dla długich rozmów — dla agenta GPT-4o mini korzystającego z dołączonych plików, automatycznych embeddingów, semantycznego search_files oraz obowiązkowych reguł używania narzędzi wyszukiwania. Najlepszy wariant grafowy Mem0 uzyskał 68,5%. To wynik jednego dostawcy, modelu, benchmarku i harnessu. Pokazuje, że interfejs oparty na plikach może dobrze działać w tej konfiguracji; nie dowodzi, że surowy Markdown lub wyszukiwanie słów kluczowych wystarczą. Korzyść operacyjna jest odrębna: deweloperzy mogą bezpośrednio czytać, edytować i porównywać diffem przechowywaną wiedzę.

Większe context window sprawiają również, że odczytywanie całych plików jest praktyczne w przypadku niektórych dokumentów projektowych. Chunked retrieval nadal pasuje do dużych korpusów, ale krótki plik z konwencjami lub przekazaniem pracy często można załadować bezpośrednio. Wybór zależy od rozmiaru dokumentu, precyzji pobierania, budżetu kontekstu oraz częstotliwości, z jaką ludzie muszą przeglądać lub edytować pamięć.

Dlaczego pliki?

W przypadku długotrwałego projektu agenta używaj katalogu dobrze uporządkowanych notatek, gdy ludzie potrzebują możliwego do przeglądania zapisu. Rozważ agenta kodującego, który pracuje nad jednym projektem przez kilka tygodni:

  • Uczy się, że projekt używa Pydantic v2, a nie v1
  • Odkrywa, że testy muszą być uruchamiane za pomocą pytest -x --tb=short
  • Gromadzi wiedzę o architekturze bazy kodu
  • Poznaje preferencje dewelopera („zawsze używaj pathlib, nigdy os.path”)

Fakty te mogłyby znajdować się w systemie wektorowym lub klucz-wartość. W tym przypadku lepszym wyborem domyślnym są pliki, ponieważ deweloper musi czytać, edytować, przeglądać i wersjonować powiązane notatki. Wyszukiwanie słów kluczowych lub semantyczne dodaj dopiero wtedy, gdy wymaga tego korpus dokumentów i wzorzec zapytań. Jeśli agent nauczy się czegoś błędnego, otwórz plik i popraw tę informację.

Claude Code, Cursor i Devin Desktop używają wariantów tego wzorca. Poniższe przykłady pokazują, jak każdy z nich przechowuje i ładuje swoje pliki.

Implementacja magazynu pamięci plikowej

Implementacja jest celowo prosta. Agent otrzymuje cztery operacje: zapis dokumentu, odczyt dokumentu, wyświetlenie dostępnych dokumentów oraz wyszukiwanie słów kluczowych we wszystkich dokumentach.

Poniżej znajduje się niezależny, ilustracyjny magazyn plików z surowym Markdownem. Nie jest on uproszczoną wersją obecnego memory/document.py. Obecny projekt używa DocumentMemory, które wymaga przestrzeni nazw i klucza oraz zapisuje kopertę JSON zawierającą content, metadata i created_at. Ten szkic definiuje inny projekt, aby pokazać kompromisy związane z czytelnymi dla człowieka plikami Markdown:

from pathlib import Path
import json

class FileMemory:
    """Document memory backed by the local filesystem.

    Stores agent knowledge as human-readable files organized by topic.
    No embeddings, no database — just files that both the agent and
    the developer can read, edit, and version-control.
    """

    def __init__(self, base_dir: str | Path):
        self.base_dir = Path(base_dir).resolve()
        self.base_dir.mkdir(parents=True, exist_ok=True)

    def _resolve_path(self, path: str) -> Path:
        """Return a path inside base_dir, rejecting escapes and symlinks."""
        requested = Path(path)
        if requested.is_absolute() or ".." in requested.parts:
            raise ValueError("path must be relative to base_dir without traversal")
        resolved = (self.base_dir / requested).resolve()
        try:
            resolved.relative_to(self.base_dir)
        except ValueError as error:
            raise ValueError("path must stay inside base_dir") from error
        return resolved

    def write_doc(self, path: str, content: str, metadata: dict | None = None):
        """Write or overwrite a document at the given path.

        Paths are relative to base_dir. Directories are created automatically.
        Metadata (if provided) is stored as a JSON sidecar file.
        """
        full_path = self._resolve_path(path)
        full_path.parent.mkdir(parents=True, exist_ok=True)
        full_path.write_text(content, encoding="utf-8")

        if metadata:
            meta_path = self._resolve_path(
                str(full_path.relative_to(self.base_dir).with_suffix(full_path.suffix + ".meta"))
            )
            meta_path.write_text(json.dumps(metadata, indent=2), encoding="utf-8")

    def read_doc(self, path: str) -> str | None:
        """Read a document by path. Returns None if not found."""
        full_path = self._resolve_path(path)
        if full_path.exists():
            return full_path.read_text(encoding="utf-8")
        return None

    def list_docs(self, pattern: str = "**/*") -> list[str]:
        """List documents matching a glob pattern."""
        self._resolve_path(pattern)
        return [
            str(self._resolve_path(str(p.relative_to(self.base_dir))).relative_to(self.base_dir))
            for p in self.base_dir.glob(pattern)
            if self._resolve_path(str(p.relative_to(self.base_dir))).is_file()
            and not p.name.endswith(".meta")
        ]

    def search_docs(self, query: str, pattern: str = "**/*.md") -> list[dict]:
        """Search documents by keyword. Returns matching files with context.

        This is intentionally simple — grep-style keyword search.
        For semantic search, use a vector store instead.

        # ponytail: linear scan of file bytes; add an index when measured
        # latency, concurrency, or retrieval quality requires it.
        """
        self._resolve_path(pattern)
        results = []
        for path in self.base_dir.glob(pattern):
            path = self._resolve_path(str(path.relative_to(self.base_dir)))
            if not path.is_file() or path.name.endswith(".meta"):
                continue
            content = path.read_text(encoding="utf-8")
            if query.lower() in content.lower():
                # Return the paragraph containing the match for context
                for paragraph in content.split("\n\n"):
                    if query.lower() in paragraph.lower():
                        results.append({
                            "path": str(path.relative_to(self.base_dir)),
                            "match": paragraph.strip()[:500],
                        })
        return results

Helper ścieżki jest celowo współdzielony przez odczyty, zapisy i wyniki glob: ścieżki względne nadal mogą wyprowadzić poza katalog przez .. lub istniejący symlink. Ta ilustracyjna klasa jest przeznaczona dla zaufanego systemu jednoużytkownikowego lub kontrolowanego systemu plików. Sprawdza ścieżkę po rozwiązaniu przed użyciem; na wrogiej granicy wielu tenantów używaj operacji względnych względem deskryptora z no-follow, aby mutacja systemu plików nie mogła wyścignąć tego sprawdzenia. Po skopiowaniu klasy uruchom tę małą kontrolę regresji:

from tempfile import TemporaryDirectory

with TemporaryDirectory() as root:
    memory = FileMemory(root)
    memory.write_doc("notes/ok.md", "safe memory")
    assert memory.read_doc("notes/ok.md") == "safe memory"
    assert memory.list_docs() == ["notes/ok.md"]
    assert memory.search_docs("safe")[0]["path"] == "notes/ok.md"

    (Path(root) / "escape").symlink_to(Path(root).parent, target_is_directory=True)
    for operation in (
        lambda: memory.write_doc("../escape.md", "nope"),
        lambda: memory.read_doc("/tmp/escape.md"),
        lambda: memory.read_doc("escape/outside.md"),
        lambda: memory.list_docs("../**/*"),
        lambda: memory.search_docs("safe", "../**/*.md"),
    ):
        try:
            operation()
        except ValueError:
            pass
        else:
            raise AssertionError("FileMemory accepted an escaped path")

Struktura katalogów

Większość wartości pamięci dokumentowej wynika ze sposobu ułożenia katalogu. Oto struktura, której użyłbym dla agenta badawczego. Market Analyst Agent używa przestrzeni nazw pod memory/documents/, ale jego obecne DocumentMemory zapisuje każdy wpis jako kopertę JSON z łańcuchem content, a nie jako surowy Markdown. Układ surowego Markdownu poniżej należy do niezależnego, ilustracyjnego projektu FileMemory przedstawionego wyżej:

.agent-memory/
    README.md                  # What this directory is, for human readers
    PROGRESS.md                # Handoff for the next session: what is done, what is next
    user-profiles/
        user-123.md            # Preferences, history, risk profile
        user-456.md
    research/
        NVDA-2026-02.md        # Research notes from recent analysis
        TSLA-2026-01.md
    conventions/
        analysis-format.md     # How to structure analysis reports
        data-sources.md        # Preferred data sources and API patterns
    learnings/
        common-errors.md       # Mistakes the agent has learned to avoid
        tool-patterns.md       # Effective tool call sequences

Katalog pamięci dokumentowej i cztery operacje wykonywane przez agenta: odczyt, zapis, listowanie i wyszukiwanieKatalog pamięci dokumentowej i cztery operacje wykonywane przez agenta: odczyt, zapis, listowanie i wyszukiwanie

W ilustracyjnym projekcie FileMemory każdy dokument jest zapisany w Markdownie, a jego przeznaczenie wynika wprost ze ścieżki. Możesz git diff cały katalog pamięci, aby zobaczyć, czego agent nauczył się podczas sesji, git revert błędną informację lub skopiować katalog do innego projektu. Koperty JSON obecnego projektu zachowują strukturę przestrzeni nazw i klucza, ale nie zapewniają takiego samego doświadczenia diffowania surowego Markdownu.

Kiedy użyć pamięci dokumentowej, wektorowej lub klucz-wartość

Trzy backendy pamięci obsługują różne wzorce dostępu:

WymiarMagazyn wektorowyMagazyn klucz-wartośćMagazyn dokumentów
Wzorzec zapytania„Znajdź fakty podobne do X”„Pobierz wartość dla klucza”„Odczytaj dokument ze ścieżki”
Najlepsze zastosowanieNieustrukturyzowane, zmienne pobieranieUstrukturyzowane wyszukiwanieKontekst projektu, notatki
Czytelność dla człowiekaCzytelne payloady tekstoweCzęściowa (JSON)Tak (Markdown)
Możliwość debugowaniaInspekcja payloadów i wynikówŁatwa (dokładne klucze)Inspekcja plików i wyszukiwanie
Możliwość wersjonowaniaPrzez eksporty lub dzienniki zmianMożliwaTak (natywnie z git)
Infrastruktura embeddingówWymaganaNiepotrzebnaNiepotrzebna
SkalaMiliony faktówMiliony kluczyZależna od liczby bajtów i indeksu
Możliwości wyszukiwaniaPodobieństwo semantyczneDokładne dopasowanieŚcieżka, słowo kluczowe, opcjonalny indeks

Używaj pamięci dokumentowej, gdy:

  • Agent gromadzi wiedzę projektową podczas wielu sesji
  • Deweloperzy muszą przeglądać, edytować lub nadpisywać to, co agent „wie”
  • Wiedza ma postać dokumentów (notatek, podsumowań, konwencji), a nie odizolowanych faktów
  • Chcesz wersjonować pamięć agenta za pomocą git
  • Wymaganiem bezwzględnym jest brak infrastruktury

Używaj magazynów wektorowych, gdy:

  • Potrzebujesz rozmytego wyszukiwania semantycznego („znajdź wspomnienia związane z X”)
  • Sposób formułowania zapytań jest nieprzewidywalnie zmienny
  • Masz od tysięcy do milionów pojedynczych faktów

Używaj magazynów klucz-wartość, gdy:

  • Potrzebujesz dokładnych i szybkich wyszukiwań danych ustrukturyzowanych (profile użytkowników, ustawienia)
  • Schemat danych jest dobrze zdefiniowany

Trzy magazyny mogą współistnieć, ale nie jest to wymagane. Obecny Market Analyst Agent używa checkpointów PostgreSQL do pamięci hot, Qdrant do dokładnego przechowywania profili użytkowników z placeholderami wektorów oraz namespacowanego magazynu dokumentów z kopertami JSON. Warianty semantycznego przypominania i surowego Markdownu w tym artykule są ilustracyjnymi rozszerzeniami.

Przykłady z rzeczywistych systemów

Ten wzorzec jest już szeroko stosowany w asystentach AI do kodowania:

  • Claude Code odczytuje pliki CLAUDE.md z katalogu głównego projektu i katalogów nadrzędnych oraz utrzymuje plik pamięci dla projektu w ~/.claude/projects/ na potrzeby wiedzy między sesjami. System pamięci składa się ze zwykłych plików Markdown, a pliki na poziomie projektu można commitować razem z kodem.
  • Cursor ładuje reguły projektu z .cursor/rules jako pliki .mdc — konwencje kodowania, preferencje frameworka i decyzje architektoniczne — z frontmatterem sterującym tym, kiedy każda reguła ma zastosowanie.
  • Starszy agent Cascade w Devin Desktop odczytuje reguły z .devin/rules/, przy czym .windsurf/rules/ i znajdujący się w katalogu głównym .windsurfrules pozostają starszymi mechanizmami awaryjnymi. Cascade przechowuje automatycznie generowaną pamięć lokalnie dla każdego workspace’u i pobiera ją później; domyślny agent Devin Local dla nowych kart nie zapisuje pamięci.
  • Narzędzie pamięci Anthropic dla Claude API jest narzędziem po stronie klienta, którym model steruje za pomocą operacji na plikach — view, create, str_replace, insert, delete i rename — w katalogu /memories. To aplikacja implementuje każde polecenie, więc decyduje, gdzie faktycznie znajdują się pliki (dysk lokalny, S3, baza danych).

Warianty oparte na plikach przechowują wiedzę agenta jako czytelny dla człowieka tekst z jawnymi operacjami odczytu i zapisu, a żaden z nich nie wymaga pipeline’u embeddingów. Agent decyduje, co zapisać; gdy tekst znajduje się w lokalnym katalogu zarządzanym przez Git, deweloper może zobaczyć i edytować go w git diff. Gdy handler narzędzia pamięci Anthropic mapuje /memories na S3 lub bazę danych, możliwości inspekcji i wersjonowania zależą od tej implementacji.

Deklaratywne notatki i wykonywalne umiejętności

Wiedza oparta na plikach występuje również poza asystentami programistycznymi, ale format magazynowania nie mówi, jak jest używana. Voyager przechowuje programy JavaScript wielokrotnego użytku, które agent może wykonywać. Główna metoda Agent Workflow Memory dodaje natomiast wyprowadzone przepływy webowe do kontekstu promptu jako wskazówki dla kolejnych działań. Osobny eksperyment AWM_AS udostępnia przepływy jako wywoływalne akcje. Procedura opisana w kontekście i procedura wykonywalna wymagają różnych kontroli.

Testuj wywoływalne umiejętności, uruchamiając je w kontrolowanym środowisku i sprawdzając efekty. Przeglądaj notatki projektowe i przepływy kontekstowe pod kątem dostarczanych faktów, ograniczeń i wskazówek dotyczących działań, a następnie sprawdź, czy instrukcje poprawiają zachowanie downstream. Każda z tych form może doprowadzić do szkodliwej akcji; żadna nie nadaje dodatkowych uprawnień.

Ta sama granica oddziela pamięć od umiejętności i narzędzi. Standard Agent Skills używa plików SKILL.md, aby powiedzieć agentowi, jak wykonywać określoną klasę pracy; pamięć rejestruje fakty poznane podczas pracy nad projektem lub wcześniejszego uruchomienia. Część 3 wyznacza sąsiednią granicę między umiejętnością a narzędziem. Wybierz magazyn plikowy dla możliwego do inspekcji kontekstu poznanego przez agenta; umiejętność lub narzędzie wybieraj tylko wtedy, gdy wymaganiem jest procedura wielokrotnego użytku lub określona zdolność.

Skalowanie pamięci dokumentowej do produkcji

Opisana wyżej implementacja oparta na plikach pasuje do kontrolowanego systemu plików jednoużytkownikowego. Wielu tenantów i współbieżni writerzy wymagają jawnej kontroli dostępu oraz koordynacji zapisów, niezależnie od liczby dokumentów.

Surowy magazyn nie ma koordynacji współbieżnych zapisów, modelu tenantów ani indeksu wyszukiwania. Zanim go zastąpisz, zmierz te wymagania. Baza danych lub magazyn obiektowy może zapewnić inne kontrakty współbieżności i dostępu; pliki również można indeksować.

Trzy popularne podejścia:

Podejście A: hybryda z cienką warstwą bazy danych

Pozostaw pliki jako format authoringu (deweloperzy edytują Markdown lokalnie), ale w runtime serwuj dane z bazy. Podczas wdrożenia synchronizuj pliki z wierszami PostgreSQL. Agent odczytuje dane z bazy, a nie z dysku. Zapewnia to:

  • Wygodę dla deweloperów (edycja Markdownu i commitowanie do gita)
  • Wydajność zapytań produkcyjnych (indeksowane odczyty z bazy)
  • Czyste rozdzielenie authoringu i serwowania

Podejście B: magazyn obiektowy + sidecar z indeksem wektorowym

Przechowuj dokumenty w S3/GCS jako obiekty, a kolekcja Qdrant niech indeksuje ich embeddingi. Agent odpytuje Qdrant o odpowiednie ID dokumentów, a następnie pobiera treść z magazynu obiektowego. Rozwiązanie skaluje się horyzontalnie i obsługuje wyszukiwanie semantyczne, ale dodaje złożoność: trzeba zarządzać dwoma systemami, utrzymywać pipeline embeddingów i obsługiwać eventual consistency między magazynem a indeksem.

Podejście C: ustrukturyzowany magazyn dokumentów z PostgreSQL (rekomendowane)

Przechowuj dokumenty jako wiersze JSONB w PostgreSQL, z wyszukiwaniem pełnotekstowym (indeks GIN) i opcjonalnymi embeddingami wektorowymi (pgvector). Zapewnia to wyszukiwanie hybrydowe (słowa kluczowe + semantyka), transakcje ACID i jeden system operacyjny.

Szkic podejścia C. Połączony wynik wykonuje dokładny scoring na ograniczonym korpusie tenanta; nie używa indeksu approximate nearest-neighbor (ANN). pgvector wymaga bezpośredniego sortowania rosnącego po odległości z LIMIT-em dla tej ścieżki indeksowania. W przypadku większego korpusu pobierz oddzielnie ograniczone zbiory kandydatów słów kluczowych i wektorów, a następnie połącz ich rankingi. To wzorzec RLS, a nie gotowy do bezpośredniego użycia kod aplikacji: jego rola bazy danych musi być dostępna wyłącznie dla zaufanego serwera aplikacji. Serwer uwierzytelnia żądanie i tworzy principal; nie przyjmuje ID tenanta od wywołującego. PostgreSQL RLS sprawia następnie, że zakres ten jest egzekwowalny nawet wtedy, gdy zapytanie pominie później predykat tenanta.

from typing import Optional
from dataclasses import dataclass
import asyncpg

@dataclass(frozen=True)
class AuthenticatedPrincipal:
    """The verified identity returned by the application's authentication layer."""
    tenant_id: str

class ProductionDocumentMemory:
    """Illustrative PostgreSQL document memory with hybrid search and RLS.

    Apply this schema and policy as the table owner during deployment:

        CREATE TABLE documents (
            id SERIAL PRIMARY KEY,
            tenant_id TEXT NOT NULL,
            path TEXT NOT NULL,
            content TEXT NOT NULL,
            metadata JSONB,
            embedding vector(1536),  -- pgvector extension
            ts_vector tsvector GENERATED ALWAYS AS (to_tsvector('english', content)) STORED,
            created_at TIMESTAMPTZ DEFAULT NOW(),
            UNIQUE(tenant_id, path)
        );
        CREATE INDEX ON documents USING GIN(ts_vector);

        ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
        ALTER TABLE documents FORCE ROW LEVEL SECURITY;
        CREATE POLICY tenant_documents ON documents
            USING (tenant_id = current_setting('app.tenant_id', true))
            WITH CHECK (tenant_id = current_setting('app.tenant_id', true));

    `FORCE` also subjects the table owner to the policy. Superusers and roles with
    `BYPASSRLS` still bypass it, so neither belongs in the application's pool.
    """

    def __init__(self, pool: asyncpg.Pool):
        self.pool = pool

    async def write(
        self,
        principal: AuthenticatedPrincipal,
        path: str,
        content: str,
        metadata: Optional[dict] = None,
        embedding: Optional[list[float]] = None,
    ):
        """Write or update a document.

        Sketch: on a real pool you must register codecs first, or asyncpg
        raises DataError — `set_type_codec` for the JSONB metadata column
        and pgvector's `register_vector` for the embedding.
        """
        async with self.pool.acquire() as conn:
            async with conn.transaction():
                # true keeps this trusted context to this transaction only.
                await conn.execute(
                    "SELECT set_config('app.tenant_id', $1, true)", principal.tenant_id
                )
                await conn.execute(
                    """
                    INSERT INTO documents (tenant_id, path, content, metadata, embedding)
                    VALUES ($1, $2, $3, $4, $5)
                    ON CONFLICT (tenant_id, path) DO UPDATE
                    SET content = EXCLUDED.content,
                        metadata = EXCLUDED.metadata,
                        embedding = EXCLUDED.embedding
                    """,
                    principal.tenant_id, path, content, metadata, embedding,
                )

    async def search(
        self,
        principal: AuthenticatedPrincipal,
        query: str,
        embedding: Optional[list[float]] = None,
        limit: int = 5,
    ) -> list[dict]:
        """Hybrid search: full-text + optional vector similarity."""
        async with self.pool.acquire() as conn:
            async with conn.transaction():
                await conn.execute(
                    "SELECT set_config('app.tenant_id', $1, true)", principal.tenant_id
                )
                if embedding:
                    # Hybrid scoring: 0.6 * text relevance + 0.4 * vector similarity
                    rows = await conn.fetch(
                        """
                        SELECT path, content, metadata,
                               (0.6 * ts_rank(ts_vector, plainto_tsquery('english', $1)) +
                                0.4 * COALESCE(1 - (embedding <=> $2), 0)) AS score
                        FROM documents
                        WHERE ts_vector @@ plainto_tsquery('english', $1)
                           OR (embedding <=> $2) < 0.5
                        ORDER BY score DESC
                        LIMIT $3
                        """,
                        query, embedding, limit,
                    )
                else:
                    # Full-text search only
                    rows = await conn.fetch(
                        """
                        SELECT path, content, metadata,
                               ts_rank(ts_vector, plainto_tsquery('english', $1)) AS score
                        FROM documents
                        WHERE ts_vector @@ plainto_tsquery('english', $1)
                        ORDER BY score DESC
                        LIMIT $2
                        """,
                        query, limit,
                    )
                return [dict(row) for row in rows]

set_config(..., true) ma zakres transakcji, więc połączenie z puli nie może zachować kontekstu jednego tenanta dla kolejnego żądania. OR w pierwszej gałęzi zapewnia hybrydowość rozwiązania. COALESCE zachowuje w zbiorze wyników dokument dopasowany słowami kluczowymi, ale pozbawiony embeddingu, wraz z jego wynikiem tekstowym; nie wnosi podobieństwa wektorowego. Przy samym predykacie @@ dokument, który ma właściwe znaczenie, ale nie współdzieli żadnych słów kluczowych z zapytaniem, zostanie odfiltrowany, zanim w ogóle rozpocznie się scoring — to wyszukiwanie słów kluczowych z semantycznym rerankingiem, a nie wyszukiwanie hybrydowe. Wagi 0,6/0,4 są ilustracyjne: ranking tekstowy i podobieństwo cosinusowe mają różne skale. Normalizuj je na podstawie ewaluacji pobierania albo użyj fuzji rankingów, zanim zinterpretujesz te wagi jako względne znaczenie. Próg odległości jest pokrętłem: zaostrz go, jeśli ramię wektorowe zalewa wyniki, albo poluzuj, jeśli dopasowania semantyczne nigdy się nie pojawiają.

Poniższa regresja opisuje zachowanie, które należy przetestować na rzeczywistej bazie po migracjach. Przy tenant-a odczyt tenant-b zwraca zero wierszy, a bezpośredni insert między tenantami kończy się błędem RLS:

BEGIN;
SELECT set_config('app.tenant_id', 'tenant-a', true);
SELECT path FROM documents WHERE tenant_id = 'tenant-b'; -- 0 rows
INSERT INTO documents (tenant_id, path, content)
VALUES ('tenant-b', 'leak.md', 'must fail'); -- ERROR: row-level security policy
ROLLBACK;

Otrzymujesz:

  • Wyszukiwanie hybrydowe: dopasowanie słów kluczowych (indeks GIN) + podobieństwo semantyczne (pgvector) oceniane wspólnie
  • Wielu tenantów: tożsamość wynikająca z serwera oraz RLS egzekwowane przez bazę danych
  • Gwarancje ACID: transakcje na primary commitują się atomowo; odczyty z repliki mogą być opóźnione
  • Jeden system operacyjny: brak osobnej bazy wektorowej do zarządzania
  • Skalowanie: repliki odczytowe mogą obsługiwać zapytania tolerujące nieaktualne dane. Natywne partycjonowanie może pomóc w eliminacji partycji i utrzymaniu, ale nie rozdziela zapisów między serwery; do tego potrzebny jest jawny projekt shardingu. Ścieżki read-after-write kieruj do primary albo zmierz odpowiednią politykę synchroniczną

Pliki świetnie sprawdzają się w przepływach pracy pojedynczego dewelopera. W produkcji wielotenantowej ustrukturyzowany magazyn dokumentów na PostgreSQL zwykle zapewnia najlepszy kompromis między prostotą, wydajnością i dojrzałością operacyjną.


Połączenie elementów: pełna architektura

Oto jak wszystkie trzy warstwy pamięci mogą współpracować w architekturze inspirowanej Market Analyst Agent. Diagram pokazuje ilustracyjny przepływ od żądania użytkownika do odpowiedzi, z aktywnymi wszystkimi warstwami pamięci.

Wszystkie trzy warstwy pamięci podłączone wokół jednego agenta oraz ich ścieżki odczytu i aktualizacjiWszystkie trzy warstwy pamięci podłączone wokół jednego agenta oraz ich ścieżki odczytu i aktualizacji

Architektura ma trzy ścieżki pamięci:

  1. Ścieżka hot (magazyn checkpointów): LangGraph zapisuje stan grafu możliwy do wznowienia w magazynie checkpointów na każdej granicy super-kroku. Gdy graf dociera do węzła interrupt_before (takiego jak węzeł publish w Części 1), wykonanie zostaje wstrzymane. Użytkownik może zamknąć aplikację, a po powrocie graf wznowi działanie z checkpointu. Dzienniki zdarzeń runtime i trace’y to oddzielne kwestie produkcyjne.

  2. Ścieżka cold (magazyn długoterminowy): Po wybraniu trasy przez router planner odpytuje magazyn długoterminowy o istotny kontekst użytkownika. Planner nie może personalizować działania, dopóki ten odczyt się nie zakończy. Wyszukiwanie oparte na wektorach może obejmować embedding zapytania i pobieranie z indeksu; wyszukiwanie klucz-wartość nie. Nowe fakty można wyodrębnić i zapisać po zakończeniu rozmowy, dzięki czemu zapis nie opóźnia pętli rozumowania.

  3. Ścieżka dokumentowa (magazyn plików): Podczas planowania agent odczytuje konwencje projektowe i notatki badawcze potrzebne do obsługi żądania. Podczas wykonania zapisuje podsumowania badań i poznane wzorce z powrotem na dysku. Odczyty te wpływają na bieżące zadanie, więc rozmiar plików, szybkość systemu plików i stan cache wpływają na czas odpowiedzi. Cache’uj tylko wtedy, gdy cache ma jasno zdefiniowane zasady unieważniania i izolacji tenantów. Zapisy mogą odbywać się później.

Podłączenie w LangGraph jest proste — magazyn checkpointów i magazyn długoterminowy są przekazywane podczas kompilacji grafu, a magazyn dokumentów jest wstrzykiwany jako zależność. Poniższy lokalny szkic kompiluje już skonfigurowany builder StateGraph wraz z węzłami przyjmującymi magazyn. Rozszerza to sposób podłączania grafu; helpery z Części 1 i projektu towarzyszącego create_graph nie przyjmują argumentu store. Używa InMemoryStore, aby zachować mały rozmiar fragmentu; referencyjna topologia Docker używa Qdrant do tej samej roli semantycznego przypominania.

import asyncio
from langgraph.store.memory import InMemoryStore

# Cold memory: local sketch with vector search
# (The reference Docker topology uses Qdrant for persistent recall.)
memory_store = InMemoryStore(
    index={"dims": 1536, "embed": embedding_function}
)

# Document memory: illustrative file-based store for project knowledge
# FileMemory is the illustrative class defined above, not the project's current
# DocumentMemory implementation.
doc_memory = FileMemory(base_dir=".agent-memory")

async def main() -> None:
    # Hot memory: PostgreSQL for durable checkpoints. postgres_checkpointer() is
    # the async context manager defined earlier, so the graph runs inside it.
    async with postgres_checkpointer(pg_connection_string) as checkpointer:
        # builder is the configured StateGraph for this extended design.
        # The Part 1/companion create_graph helper does not accept store.
        graph = builder.compile(
            checkpointer=checkpointer,
            store=memory_store,
        )
        # ... run the graph here, while the connection is still open

asyncio.run(main())

# The store is accessible inside any node via the store parameter
def planner_node(state: AgentState, *, store: BaseStore) -> dict:
    """Plan with user context from long-term memory."""

    # Recall relevant user facts from vector store.
    # Namespace prefix is positional — see the store example above.
    user_memories = store.search(
        ("user", state.user_id),
        query=state.messages[-1].content,
        limit=5,
    )

    # Load project conventions from document memory
    conventions = doc_memory.read_doc("conventions/analysis-format.md")

    # Inject both into planning context
    # Each stored value is a dict; render whatever keys it carries
    memory_context = "\n".join(str(m.value) for m in user_memories)
    # ... rest of planning logic with personalized context and conventions

Pełny przepływ

W rozszerzonym projekcie powyżej żądanie powracającego użytkownika „Analyze TSLA” mogłoby przejść następujący przepływ. Semantyczne przypominanie i asynchroniczna ekstrakcja faktów są proponowanymi rozszerzeniami, a nie obecnym zachowaniem projektu towarzyszącego:

  1. Ładowanie pamięci dokumentowej: Gdy uruchamia się planner, odczytuje z magazynu dokumentów konwencje projektowe: preferowany format analizy, preferowane źródła danych i wzorce użycia narzędzi. Ustanawiają one bazowe zachowanie dla tego planu.

  2. Router: Router klasyfikuje żądanie jako DEEP_RESEARCH. W tym przykładzie routing wykorzystuje samo żądanie, a nie preferencje długoterminowe.

  3. Przypominanie z pamięci cold + planner: Planner odpytuje magazyn długoterminowy wiadomością użytkownika. Pobiera informacje: „Użytkownik ma wysoką tolerancję ryzyka”, „Użytkownik preferuje szczegółową analizę konkurencji”, „Użytkownik wcześniej badał NVDA i AMD”. Następnie tworzy pięciostopniowy plan badań spersonalizowany zgodnie z tymi preferencjami. Uwzględnia krok analizy konkurencji, ponieważ historia użytkownika pokazuje, że tego oczekuje. Plan jest zgodny z formatem z dokumentu konwencji.

  4. Pętla wykonawcza (pamięć hot): Każdy krok jest wykonywany według wzorca ReAct z Części 1 — myślenie, działanie, obserwacja, powtarzane do ukończenia kroku. LangGraph zapisuje checkpoint po każdym super-kroku (router, planner i każdy sekwencyjny krok executora w tym przykładzie). Odzyskiwanie rozpoczyna się od najnowszego utrwalonego checkpointu. Jeśli zapis kroku 3 został ukończony, graf może przejść do kroku 4; przy asynchronicznej trwałości awaria może wymagać powtórzenia ukończonego kroku.

  5. Przerwanie HITL: Reporter zapisuje szkic. Oddzielna sesja modelu, bez historii tego uruchomienia, odczytuje szkic i zapisuje ocenę. Graf dociera następnie do publish, gdzie interrupt_before wstrzymuje go niezależnie od tej oceny. Checkpoint przechowuje zarówno szkic, jak i ocenę, więc człowiek przegląda oba elementy przed podjęciem decyzji o publikacji. Kilka godzin później graf ponownie ładuje checkpoint i postępuje zgodnie z tą decyzją.

  6. Aktualizacje pamięci: Po zakończeniu rozmowy proces asynchroniczny wyodrębnia nowe fakty o użytkowniku („użytkownik śledzi teraz TSLA”, „użytkownik zaakceptował format raportu”) i zapisuje je w długoterminowym magazynie wektorowym. Agent zapisuje również podsumowanie badań w magazynie dokumentów (research/TSLA-2026-02) do wykorzystania w przyszłości.

Wzorzec trzech warstw przejrzyście rozdziela odpowiedzialności. Magazyn checkpointów odpowiada za trwałość i wznowienie; jest infrastrukturą. Magazyn długoterminowy odpowiada za personalizację; jest logiką produktu. Magazyn dokumentów przechowuje zgromadzoną wiedzę projektową; jest notatnikiem agenta.


Kompromisy i kwestie do rozważenia

Pamięć przynosi wartość, ale dodaje również koszty i złożoność:

  • Koszt embeddingów: Każdy fakt zapisany w bazie wektorowej wymaga wygenerowania embeddingu. Hostowany dostawca embeddingów dodaje wywołanie API, koszt zależny od dostawcy i opóźnienie sieciowe; we wrześniu 2026 r. OpenAI podajetext-embedding-3-small w cenie $0.02 za milion tokenów. Koszt hostowanego modelu na fakt jest pomijalny, ale rośnie przy tysiącach użytkowników i sesji. Grupuj wywołania hostowane i cache’uj wyniki. W czasie zapytania wyszukiwanie wektorowe może obejmować embedding zapytania, indeks i opóźnienie sieci; wyszukiwanie klucz-wartość nie. Zmierz tę ścieżkę w swoim wdrożeniu, a następnie cache’uj typowe embeddingi zapytań lub użyj lokalnego modelu embeddingowego, jeśli opóźnienie ma znaczenie.

  • Nieaktualna pamięć: Preferencje użytkownika się zmieniają. Fakt zapisany sześć miesięcy temu („użytkownik preferuje konserwatywne inwestycje”) może już nie być prawdziwy. Ustal politykę wygasania. Na przykład zespół może wygaszać preferencje po 365 dniach, a zdarzenia epizodyczne po 90 dniach, jeśli jego reguły prywatności, tempo aktualizacji i ewaluacja pobierania uzasadniają takie okna; są to proponowane zasady, a nie przenośne wartości domyślne. Wpis o inżynierii kontekstu odrzuca stałe reguły retencji jako przenośną politykę. Wygasanie jest podejściem uproszczonym. Stan typowany kierowany schematem oferuje precyzyjniejsze rozwiązanie: ważność czasową i pochodzenie każdego faktu, dzięki czemu wartość zastąpiona nowszą przegrywa z aktualną podczas pobierania, a nie dopiero w momencie wygaśnięcia.

  • Koszt pamięci w kontekście: Każdy przypomniany fakt zużywa tokeny w context window LLM. Jeśli dla każdego zapytania pobierzesz 20 faktów, pamięć kontekstowa zajmie kilkaset tokenów konkurujących z właściwym zadaniem. Ogranicz liczbę pobieranych faktów i priorytetyzuj je według wyniku trafności.

  • Prywatność i zgodność: Pamięć długoterminowa przechowuje dane użytkowników. Potrzebujesz anonimizacji PII przed zapisem, jasnych zasad retencji oraz kontroli po stronie użytkownika umożliwiających usuwanie danych. W branżach regulowanych żaden z tych elementów nie jest opcjonalny.

  • Wzrost magazynu checkpointów: Tabele checkpointów PostgreSQL rosną po każdym super-kroku. Nie uruchamiaj ogólnego zapytania SQL do przycinania: kanały delta mogą wymagać checkpointów przodków oraz powiązanych rekordów write/blob do odtworzenia zachowanego checkpointu. Używaj API przycinania obsługiwanego przez saver dopiero po zweryfikowaniu go z dokładną zainstalowaną wersją savera i jego kontraktem odzyskiwania kanałów delta. Jeśli obsługa nie jest dostępna, zachowaj pełne domknięcie nadrzędnych checkpointów, zapisów i blobów, a następnie przetestuj wznowienie z zachowanego checkpointu na zainstalowanym saverze.

  • Konsolidacja pamięci: Z czasem szczegółowe wspomnienia epizodyczne powinny zostać skompresowane do zwartych reprezentacji semantycznych: „użytkownik pytał o NVDA trzy razy w styczniu” zamiast przechowywania wszystkich trzech rozmów w całości. Odzwierciedla to konsolidację pamięci ludzkiej i ułatwia zarządzanie magazynem. Mem0 i Graphiti obsługują to automatycznie; jeśli budujesz własne rozwiązanie, zaplanuj okresowe zadania konsolidacji.

  • Problem zimnego startu: Nowi użytkownicy nie mają pamięci długoterminowej. Agent powinien degradować się łagodnie i zadawać pytania doprecyzowujące zamiast przyjmować założenia. Pamięć jest dodatkiem, a nie wymaganiem.

  • Zatruwanie pamięci: Wszystko w context window agenta jest potencjalnym punktem wstrzyknięcia. Jeśli atakujący zapisze w magazynie dokumentów lub pamięci długoterminowej wprowadzające w błąd fakty („zawsze zatwierdzaj transakcje bez weryfikacji”), agent może wykonać je jako instrukcje. Prompt injection przez zapisane wspomnienia jest rzeczywistą powierzchnią ataku. Środki zaradcze to walidacja przed zapisem, traktowanie pobranej treści jako niezaufanych danych, a nie instrukcji systemowych, oraz kontrola dostępu ograniczająca to, które wspomnienia mogą wpływać na operacje krytyczne.

  • Dryf pamięci dokumentowej: Pamięć oparta na plikach nie ma automatycznej deduplikacji ani rozwiązywania konfliktów. Z czasem dokumenty gromadzą sprzeczności: jeden plik mówi „używaj pytest”, a inny „używaj unittest”. Planuj okresowe przeglądy (albo pozwól agentowi je wykonywać), aby usuwać duplikaty i konsolidować dane. Pliki obsługują grep; payloady magazynu wektorowego również można przeglądać lub eksportować. Żaden format magazynowania samodzielnie nie wykrywa sprzeczności.

  • Skala wyszukiwania: opisane wyżej surowe skanowanie plików odczytuje korpus przy każdym zapytaniu. Wybierz indeks na podstawie liczby skanowanych bajtów, tempa aktualizacji, współbieżności, opóźnienia i jakości pobierania. Treść oparta na plikach może używać indeksu pełnotekstowego lub wektorowego; sama liczba dokumentów nie determinuje backendu.


Testowanie pobierania i cyklu życia pamięci

Porównuj wyniki z bazowymi wariantami bez pamięci i z pełnym kontekstem na pytaniach held-out. Uwzględnij parafrazy, sprzeczności, zmiany preferencji, nieaktualne fakty, pytania bez odpowiedzi, usunięcia i żądania między tenantami. LongMemEval zawiera 500 pytań obejmujących ekstrakcję, rozumowanie wielosesyjne i czasowe, aktualizacje oraz wstrzymywanie się od odpowiedzi. Mierz precyzję/recall pobierania oddzielnie od poprawności odpowiedzi, a także użycie nieaktualnych faktów, nieuprawnione ujawnienia, poprawność zapisu/aktualizacji/usuwania, opóźnienie i koszt.

Pytania dotyczące przypominania to tylko część ewaluacji. MemoryArena dodaje zależne od siebie zadania między sesjami, w których wcześniejsze działanie i jego informacja zwrotna muszą zmienić późniejsze zachowanie. Zadania obejmują zakupy, planowanie podróży, progresywne wyszukiwanie i rozumowanie formalne. Użyj tego projektu, gdy produkt obiecuje uczenie się na podstawie pracy, a nie tylko odpowiadanie na pytania dotyczące zapisanych rozmów. Są to zadania badawcze, a nie pomiary wdrożonej usługi pamięci.

EvoMemBench również rozdziela wiedzę od doświadczenia wykonawczego oraz pamięć wewnątrz epizodu od pamięci między epizodami. Porównanie 15 metod nie wskazuje jednej, konsekwentnie najsilniejszej formy pamięci; bazowe rozwiązania z długim kontekstem pozostają konkurencyjne w ramach tego protokołu. To argument za zachowaniem prostych bazowych wariantów w ewaluacji, a nie za zastępowaniem każdego magazynu najnowszym frameworkiem.

Przechowuj pochodzenie i ważność obok pobranych faktów. Wyniki ważności nie mogą ustanawiać zaufania ani zmieniać uprawnień. Polityka usuwania musi obejmować indeksy, cache’owane podsumowania i zachowane artefakty, a nie tylko oryginalny rekord.

Kolejna warstwa to działanie

Części 5 i 6 wracają do pamięci od strony operacyjnej, ale zajmują się różnymi jej połowami. Runtime odpowiada za checkpoint: miejsce zatrzymania wykonania i sposób jego ponownego uruchomienia. Harness odpowiada za przekazanie: znaczenie wykonanej pracy i to, co pozostało, zapisane jako pamięć dokumentowa dla kolejnej sesji modelu — jeden ciągły odcinek kontekstu modelu, zgodnie z terminologią doprecyzowaną w Części 5. Odtworzenie procesu nie jest tym samym co odtworzenie zadania.

Bibliografia

Artykuły

Dokumentacja LangGraph

Backendy checkpointów

Bazy wektorowe i narzędzia pamięci

  • Qdrant — Otwarta baza wektorowa z indeksowaniem HNSW i filtrowaniem
  • Qdrant Agentic Builders Guide — Praktyczny przewodnik budowania pamięci agenta z Qdrant
  • pgvector — Rozszerzenie PostgreSQL do wyszukiwania podobieństwa wektorowego
  • Graphiti — Otwarty silnik czasowego grafu wiedzy firmy Zep

Pamięć dokumentowa i oparta na plikach

  • Claude Code Memory — CLAUDE.md i katalog pamięci projektu
  • Anthropic Memory Tool — Pamięć oparta na plikach po stronie klienta dla agentów Claude API
  • Cursor Rules — Reguły projektu jako pliki .mdc w .cursor/rules
  • Devin Desktop Memories — Reguły Cascade i automatycznie generowana pamięć lokalna dla workspace’u; domyślny Devin Local ich nie zapisuje

Frameworki pamięci

  • Mem0 — Zarządzana warstwa pamięci z pipeline’em ekstrakcji i konsolidacji
  • Letta (MemGPT) — Inspirowane systemem operacyjnym wirtualne zarządzanie kontekstem agentów
  • LangMem SDK — Narzędzia zarządzania pamięcią dla LangGraph

Warsztaty

Projekt demonstracyjny

  • Market Analyst Agent — Referencyjna implementacja ścieżek checkpointów oraz bieżącego przechowywania profili i dokumentów