Pamięć agenta AI: stan sterowany schematem i proweniencja

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

Agenci działający przez długi czas często pobierają nieaktualne fakty, ponieważ zwykła pamięć semantyczna nie ma reguły określającej, która wartość jest bieżąca.

Gdy użytkownik zmienia termin ważności paszportu z 15 lipca na 30 czerwca, wyszukiwanie wektorowe może zwrócić oba stwierdzenia. Warstwa pamięci utrzymująca stan musi odnotować, że wartość 30 czerwca zastępuje wcześniejszą.

Dla inżynierów budujących agentów działających przez długi czas lub w środowisku multi-tenant taki błąd związany z nieaktualnym faktem pokazuje, dlaczego pamięć musi przetrwać osobne uruchomienia, a jednocześnie egzekwować bieżący stan i granice tenantów. Poniższy projekt pokazuje, jak zapisywać rekordy typowane i testować odczyty bieżące oraz odczyty z określonego momentu, bez traktowania wyszukiwania wektorowego jako źródła prawdy.


Pułapka okna kontekstowego

Okno kontekstowe to dane wejściowe dostępne podczas pojedynczego wywołania modelu. Aplikacja może przenosić wiadomości do kolejnych wywołań, ale to polityka aplikacji musi określić, które wcześniejsze fakty pozostają prawdziwe i kto może je zobaczyć.

Agenci działający przez długi czas muszą pamiętać preferencje, status zadań, informacje o klientach, decyzje dotyczące narzędzi, notatki związane ze zgodnością oraz wcześniejsze błędy. Najprostsze rozwiązanie polega na dopisywaniu podsumowań albo wrzucaniu starych notatek do vector store. Działa to do momentu, gdy któryś z zapamiętanych faktów się zmieni.

Wtedy agent ma dwa terminy ważności paszportu, dwa preferowane formaty albo dwie decyzje projektowe. Wyszukiwanie semantyczne może zwrócić oba. Podsumowanie może nadpisać jeden z nich. Długi kontekst może zawierać nieaktualny fakt obok aktywnego. Takie projekty przypominają tekst, ale nie egzekwują bieżącej wartości.

Kontrakt pamięci musi odpowiadać na konkretne pytania:

  • Co jest prawdą teraz?
  • Co było prawdą 2 czerwca?
  • Kto to powiedział?
  • Do którego tenanta to należy?
  • Który starszy fakt został zastąpiony przez ten fakt?
  • Czy można go usunąć lub wygasić?

Schema-Guided Agent Memory (SGAM) przechowuje te odpowiedzi w polach i relacjach, zamiast pozostawiać je domyślne w prozie.


Co oznacza SGAM

Trzy podobnie nazwane koncepcje wyznaczają zakres SGAM.

Schema-Guided Dialogue (SGD) to zbiór danych Google do dialogów zadaniowych z 2019 roku. Jego schemat opisuje API usług, intencje i sloty, aby model dialogowy mógł śledzić stan usług, których wcześniej nie widział. To użyteczny precedens dla śledzenia opartego na schemacie, ale o zakresie ograniczonym do usług dialogowych.

Schema-Guided Memory (SGM) to termin badawczy używany przez Mei i in. w pracy According to Me: Long-Term Personalized Referential Memory QA. Artykuł porównuje swobodnotekstową Descriptive Memory (DM) z elementami pamięci key-value o ustalonym schemacie. Obie reprezentacje zawierają te same informacje źródłowe, ale w innej strukturze.

W tym artykule używam określenia Schema-Guided Agent Memory (SGAM) na wzorzec inżynieryjny, w którym schematy sterują zapisem, aktualizacją, wyszukiwaniem i usuwaniem. Schemat definiuje stan aplikacji oraz jego cykl życia.

ATM-Bench pokazuje, dlaczego reprezentacja ma znaczenie. Wykorzystuje około czterech lat danych osobistych z wiadomości e-mail, obrazów i nagrań wideo. Pytania wymagają odwołań osobistych, lokalizacji, wielu elementów dowodowych oraz aktualizacji w czasie. Dla trudnego podziału artykuł podaje, że SGM przewyższa DM w wyszukiwaniu i odpowiadaniu na pytania w testowanej konfiguracji. SGM udostępnia w ustalonej reprezentacji pola takie jak czas, źródło, lokalizacja, encje i tagi. Uznaję tę reprezentację oraz raportowane wyniki za wniosek poparty danymi; artykuł nie ustanawia bezpośredniego mechanizmu adresowania pól.

SGM kontra DM odpowiada na pytanie dotyczące przechowywania: czy pamięć powinna pozostać swobodnym tekstem, czy używać nazwanych pól? Produkcyjny agent ma jednak jeszcze jeden problem przed etapem zapisu. Musi przekształcić nieustrukturyzowaną rozmowę w proponowaną aktualizację pamięci. Schema-Guided Reasoning (SGR) sprawia, że zamierzona ścieżka decyzyjna jest możliwa do inspekcji: odpowiedź może zawierać dowody, podmiot i atrybut, porównanie z bieżącym stanem oraz proponowany zapis. Odpowiedź modelu nie egzekwuje zależności ani kolejności między tymi polami. Osobne wywołania, walidatory i polityka aplikacji egzekwują te reguły; SGAM stosuje reguły przechowywania i cyklu życia po wywołaniu modelu.


Oddziel ekstrakcję modelu od własności pamięci

Zapis do pamięci przebiega przez trzy warstwy. Structured Output (SO) egzekwuje kształt obiektu kandydata. Schema-Guided Reasoning (SGR) sprawia, że zamierzone pola modelu i intencja decyzyjna są możliwe do inspekcji w ustrukturyzowanej odpowiedzi. Schema-Guided Agent Memory (SGAM) zarządza kandydatem jako trwałym stanem po wywołaniu modelu. Osobne wywołania, walidatory i polityka aplikacji egzekwują zależności, kolejność i cykl życia.

Werdykt, routing lub plan zwykle wygasają wraz z bieżącym żądaniem. Inne uruchomienie może odczytać kandydata pamięci kilka dni później albo użyć go do wyboru wywołania narzędzia. Taki dłuższy czas życia wymaga reguł przechowywania, których SGR nie zapewnia.

SGR organizuje jedno wywołanie modelu wokół zamierzonej topologii rozumowania. W przypadku zapisu pamięci odpowiedź może zawierać dowody źródłowe, znormalizowany podmiot i atrybut, porównanie z bieżącym stanem oraz proponowaną aktualizację. Pydantic lub JSON Schema opisuje te pola. Provider-native Structured Output albo runtime guided decoding, taki jak XGrammar, utrzymuje odpowiedź w tym kształcie.

Taki kształt sprawia, że zadeklarowane pola i intencja modelu są możliwe do inspekcji. Nie gwarantuje jednak poprawnego wniosku ani nie dowodzi, że model wykorzystał te pola w określonej kolejności. Osobne wywołania, walidatory i polityka aplikacji stanowią granicę egzekwowania zależności, kolejności i cyklu życia.

SGAM decyduje, co dzieje się po utworzeniu tego obiektu. Czy należy go przechować? Czy zastępuje starszy fakt? Który tenant może go zobaczyć? Czy jest bieżący, czy historyczny? Który epizod źródłowy go potwierdza?

Tabela przedstawia zakres odpowiedzialności i tryb awarii każdej warstwy:

WymiarSOSGRSGAM
CelZwrócenie obiektu zgodnego ze schematemUmożliwienie inspekcji zamierzonych pól i intencji decyzyjnejZarządzanie trwałą pamięcią po wywołaniu modelu
ZakresJedna wygenerowana odpowiedźJedna odpowiedź modelu; polityka obejmująca wiele kroków może obejmować wywołaniaRekordy używane między wywołaniami, sesjami i uruchomieniami
Rola schematuDefiniuje pola wyjściowe, typy i dozwolone wartościOpisuje dowody, pola pośrednie i decyzję kandydataDefiniuje przechowywane rekordy, relacje i cykl życia
EgzekwowanieConstrained decoding blokuje dane wyjściowe niezgodne ze schematemTylko kształt; osobne wywołania, walidatory i polityka egzekwują zależności i kolejnośćWalidacja aplikacyjna, ograniczenia bazy danych i reguły konfliktów egzekwują cykl życia
Czas życiaBieżące wywołanie, chyba że aplikacja przechowa obiektŚlad rozumowania jest zwykle odrzucany po podjęciu decyzjiTrwa do aktualizacji, wygaśnięcia lub usunięcia
Tryb awariiPoprawny kształt, ale błędne znaczeniePola są obecne, lecz rozumowanie lub obsługa zależności nadal może być błędnaNieaktualny, zanieczyszczony, nieodizolowany lub niemożliwy do audytu stan

Na ścieżce zapisu kolejność jest następująca:

SGR reasoning schema + Structured Output -> candidate write -> SGAM policy and persistence

Poniższy ilustracyjny fragment z memory_models.py definiuje obiekt przekazywany z ekstrakcji do usługi zapisu SGAM. Ten blok wymaga Pydantic, dlatego w przykładowych testach środowisk, które nie instalują tej opcjonalnej zależności, jest oznaczony jako no-run:

from datetime import datetime
from pydantic import BaseModel, Field

class MemoryDelta(BaseModel):
    tenant_id: str = Field(description="Isolation boundary, e.g. acme")
    subject: str = Field(description="Normalized entity ID, e.g. mira")
    attribute: str = Field(description="Property being updated")
    value: str = Field(description="New value")
    valid_from: datetime
    source_episode_id: str

MemoryDelta rejestruje to, co wyodrębnił model. Usługa zapisu SGAM nadal decyduje, czy odrzucić, scalić czy przechować dane.


Ścieżki zapisu i odczytu mają różne zadania

Tylko ścieżka zapisu modyfikuje przechowywany stan. Ścieżka odczytu wybiera rekordy dla bieżącego żądania.

Przepływ ingestii to ścieżka zapisu:

  1. Przechwyć surowy epizod z wiadomości, wyników narzędzi lub zdarzeń biznesowych.
  2. Wyodrębnij typowanych kandydatów przez ustrukturyzowane dane wyjściowe.
  3. Zweryfikuj schemat i odrzuć niepoprawne zapisy.
  4. Uzgodnij konflikty, zamknij nieaktualne fakty i zachowaj proweniencję.
  5. Zatwierdź rekord w magazynie SGAM.

Przepływ żądania to ścieżka odczytu:

  1. Zacznij od pytania użytkownika.
  2. Zdecyduj, czy pytanie wymaga bieżącego stanu, czy stanu z określonego momentu.
  3. Filtruj po tenancie, typie pamięci, podmiocie, atrybucie i oknie ważności.
  4. Dodaj rozszerzenie wektorowe lub grafowe tylko wtedy, gdy dokładne wyszukanie stanu nie wystarcza.
  5. Zbuduj najmniejszy kontekst dla modelu, opatrzony cytowaniami.

Architektura Schema-Guided Agent MemoryArchitektura Schema-Guided Agent Memory

Czytaj ten diagram od lewej do prawej, w dwóch ścieżkach. Górna ścieżka zapisuje pamięć, a dolna ją odczytuje. Obie korzystają z tego samego magazynu.


Co powinno należeć do schematu pamięci

Minimalny rekord SGAM musi zawierać więcej niż text.

tenant_id
memory_id
subject
attribute
value
memory_type
schema_version
valid_from
valid_to
supersedes_memory_id
source_episode_id
confidence
retention_policy

Dzięki tym polom nowy termin ważności paszportu może zamknąć poprzedni termin bez usuwania historii. Ta sama tabela może obsługiwać zapytania o stan bieżący i stan z określonego momentu, a następnie prześledzić wynik do epizodu źródłowego. schema_version obsługuje migracje, a retention_policy informuje zadania usuwania, co jeszcze należy usunąć.

Używaj RAG do pobierania dokumentów, a SGAM do utrzymywania zmiennego stanu. Wyszukiwanie wektorowe nadal ma miejsce w systemie jako mechanizm rozmytego przypominania, klastrowania i rozszerzania. Bieżąca wartość mira.passport_deadline powinna pochodzić ze scoped rekordu pamięci, a nie z fragmentu, który przypadkiem uzyskał najwyższą pozycję w rankingu.


Przykład nieaktualnego faktu

Rozważmy syntetyczny ślad złożony z dwóch epizodów, przedstawiony jako baza DM i rejestr SGAM:

e1: Mira prefers concise answers. Her passport deadline is 2026-07-15.
e2: Mira corrected the deadline. It is now 2026-06-30.

Baza DM przechowuje oba epizody jako swobodny tekst, więc wyszukiwanie tekstowe może zwrócić e1, ponieważ zawiera właściwe słowa. SGAM wyodrębnia z każdego epizodu typowany fakt, kluczowany przez tenanta, podmiot i atrybut. Powinien zwrócić e2 jako stan bieżący, a e1 zachować na potrzeby zapytania historycznego.

Poniżej znajduje się samowystarczalna ścieżka zapisu w SQLite. Używa ciągów UTC w formacie ISO-8601 dla znaczników czasu, konfiguruje sqlite3.Row przed odczytem po nazwie kolumny i reprezentuje fakty jako przedziały lewostronnie domknięte, prawostronnie otwarte: [valid_from, valid_to). Tabela odrzuca niepoprawne przedziały i ma częściowy unikalny indeks zapewniający jeden otwarty fakt na tenanta, podmiot i atrybut. replace_fact zarządza jedną transakcją; SQLite szereguje równoległe zapisy, dlatego wywołujący powinien ponowić całą transakcję po błędzie blokady lub unikalności.

import sqlite3
from dataclasses import dataclass

@dataclass(frozen=True)
class MemoryFact:
    tenant_id: str
    subject: str
    attribute: str
    value: str
    valid_from: str
    source_episode_id: str

def open_db() -> sqlite3.Connection:
    db = sqlite3.connect(":memory:")
    db.row_factory = sqlite3.Row
    db.executescript(
        """
        create table memory_facts (
            fact_id integer primary key,
            tenant_id text not null,
            subject text not null,
            attribute text not null,
            value text not null,
            valid_from text not null,
            valid_to text,
            source_episode_id text not null,
            check (valid_to is null or valid_from < valid_to)
        );
        create unique index one_open_fact
        on memory_facts (tenant_id, subject, attribute)
        where valid_to is null;
        """
    )
    return db

def replace_fact(db: sqlite3.Connection, fact: MemoryFact) -> None:
    with db:
        exact = db.execute(
            """
            select fact_id
            from memory_facts
            where tenant_id = ? and subject = ? and attribute = ? and valid_from = ?
            """,
            (fact.tenant_id, fact.subject, fact.attribute, fact.valid_from),
        ).fetchone()
        if exact:
            raise ValueError("equal valid_from requires an application conflict policy")

        containing = db.execute(
            """
            select fact_id, valid_to
            from memory_facts
            where tenant_id = ?
              and subject = ?
              and attribute = ?
              and valid_from < ?
              and (valid_to is null or valid_to > ?)
            limit 1
            """,
            (
                fact.tenant_id,
                fact.subject,
                fact.attribute,
                fact.valid_from,
                fact.valid_from,
            ),
        ).fetchone()
        if containing:
            db.execute(
                "update memory_facts set valid_to = ? where fact_id = ?",
                (fact.valid_from, containing["fact_id"]),
            )
            successor_boundary = containing["valid_to"]
        else:
            successor = db.execute(
                """
                select valid_from
                from memory_facts
                where tenant_id = ? and subject = ? and attribute = ? and valid_from > ?
                order by valid_from
                limit 1
                """,
                (fact.tenant_id, fact.subject, fact.attribute, fact.valid_from),
            ).fetchone()
            successor_boundary = successor["valid_from"] if successor else None

        db.execute(
            """
            insert into memory_facts
                (tenant_id, subject, attribute, value, valid_from, valid_to, source_episode_id)
            values (?, ?, ?, ?, ?, ?, ?)
            """,
            (
                fact.tenant_id,
                fact.subject,
                fact.attribute,
                fact.value,
                fact.valid_from,
                successor_boundary,
                fact.source_episode_id,
            ),
        )

def fact_at(db: sqlite3.Connection, timestamp: str) -> sqlite3.Row:
    return db.execute(
        """
        select value, source_episode_id
        from memory_facts
        where tenant_id = ?
          and subject = ?
          and attribute = ?
          and valid_from <= ?
          and (valid_to is null or valid_to > ?)
        limit 1
        """,
        ("acme", "mira", "passport_deadline", timestamp, timestamp),
    ).fetchone()

db = open_db()
replace_fact(
    db,
    MemoryFact("acme", "mira", "passport_deadline", "2026-07-15", "2026-06-01T09:00:00Z", "e1"),
)
replace_fact(
    db,
    MemoryFact("acme", "mira", "passport_deadline", "2026-06-30", "2026-06-03T10:00:00Z", "e2"),
)
current = fact_at(db, "2026-06-04T00:00:00Z")
assert (current["value"], current["source_episode_id"]) == ("2026-06-30", "e2")

historical = fact_at(db, "2026-06-02T00:00:00Z")
assert (historical["value"], historical["source_episode_id"]) == ("2026-07-15", "e1")

# A delayed extraction predates e1. It ends at e1's existing boundary,
# rather than closing e2, which remains current.
replace_fact(
    db,
    MemoryFact("acme", "mira", "passport_deadline", "2026-08-01", "2026-05-30T08:00:00Z", "e0"),
)
delayed = fact_at(db, "2026-05-31T00:00:00Z")
assert (delayed["value"], delayed["source_episode_id"]) == ("2026-08-01", "e0")
current = fact_at(db, "2026-06-04T00:00:00Z")
assert (current["value"], current["source_episode_id"]) == ("2026-06-30", "e2")
intervals = db.execute("select valid_from, valid_to from memory_facts").fetchall()
assert all(row["valid_to"] is None or row["valid_from"] < row["valid_to"] for row in intervals)

episodes = (
    "e1: Mira prefers concise answers. Her passport deadline is 2026-07-15.",
    "e2: Mira corrected the deadline. It is now 2026-06-30.",
)
naive_match = next(episode for episode in episodes if "passport deadline" in episode)
assert naive_match.startswith("e1:")

replace_fact najpierw odrzuca równy valid_from: ten konflikt wymaga polityki aplikacyjnej, takiej jak priorytet źródeł albo rewizja potwierdzona przez użytkownika, a nie ciche nadpisanie. Następnie wyszukuje przedział zawierający znacznik czasu przychodzącego faktu. Jeśli go znajdzie, zamyka poprzednik na granicy wejściowej i nadaje wstawianemu wierszowi wcześniejszy koniec poprzednika. Jeśli znacznik czasu przypada przed każdym zapisanym przedziałem, jako koniec nowego wiersza wykorzystuje valid_from kolejnego przedziału. W ten sposób opóźniony fakt sprzed e1 staje się [2026-05-30, 2026-06-01), pozostawiając e1 oraz bieżący e2 bez zmian. Fabryka wierszy sprawia, że row["fact_id"] i nazwane pola wyników działają na utworzonym tutaj domyślnym połączeniu. Częściowy unikalny indeks jest zabezpieczeniem bazodanowym dla jednego otwartego wiersza; wdrożenie z wieloma writerami powinno dobrać bazę danych i politykę ponawiania odpowiednie do obciążenia. DM nie ma równoważnego kroku aktualizacji, więc stary tekst nadal może uzyskać wyższą pozycję niż korekta.

Asercje weryfikują bieżący wiersz e2, historyczny wiersz e1, opóźnione wstawienie przed e1, niezmiennik przedziałów oraz pierwszy pasujący epizod naiwnego wyszukiwania tekstowego:

Naive text memory:
  returned episode: e1 -> passport deadline is 2026-07-15

SGAM current state:
  mira.passport_deadline = 2026-06-30
  valid_from=2026-06-03T10:00:00Z, source=e2

SGAM point-in-time state:
  on 2026-06-02, mira.passport_deadline = 2026-07-15

W produkcji połącz tę transakcję z ustrukturyzowaną ekstrakcją na ścieżce zapisu. Transakcja bazodanowa aktualizuje ważność czasową. Model wyodrębnia kandydata faktu, ale nie decyduje, który przechowywany wiersz pozostaje bieżący.


Wybór magazynu wynika ze wzorca wyszukiwania

Projekty używają kilku nazw dla elementów tego wzorca: memory stores, context graphs, profiles, long-term stores, graph RAG i stateful agents.

Narzędzie lub frameworkGłówna warstwa przechowywaniaMechanizm stanu czasowegoMechanizm schematuPraktyczne zastosowanie
GraphitiNeo4j, FalkorDB i Amazon Neptune; Kuzu jest przestarzałePrzedziały ważności faktów oraz proweniencja epizodu źródłowegoTypy encji i krawędzi Pydantic, krawędzie czasowe, proweniencjaSamodzielnie hostowana czasowa pamięć grafowa
ZepZastrzeżony Context Graph EngineZarządzane czasowe Context Graphs przechowujące zmieniające się faktyDomyślne oraz niestandardowe typy encji i krawędziZarządzana pamięć agentów i governance
LangGraph / LangMemMagazyny LangGraph, magazyny oparte na PostgresieZnaczniki czasu i pola zarządzane przez aplikację w rekordach magazynuMagazyny JSON oraz ekstrakcja profili lub kolekcji za pomocą PydanticAplikacje agentów zbudowane już na LangGraph
Mem0Zarządzany stack, Valkey / Redis / backendy wektorowe w konfiguracjach OSSAktualizacje pamięci; polityka czasowa pozostaje własnością aplikacjiTypy pamięci, własne kategorie, prompty ekstrakcjiPamięć użytkownika, agenta i sesji jako usługa
Letta / MemGPTStan i bloki pamięci agenta oparte na bazie danychEdytowalne bloki bez przedziałów ważności na poziomie pólEdytowalne etykietowane bloki pamięciAgenci stanowi z zarządzaniem kontekstem w stylu systemu operacyjnego
CogneeGraf oraz backendy wektorowe i relacyjneHistoria zależy od ontologii i wybranego backenduEkstrakcja i walidacja ukierunkowane na ontologięPamięć grafu wiedzy dla przedsiębiorstw
LlamaIndex property graphMagazyny grafów właściwości oraz magazyny wektorowePola czasu zależą od schematu grafu i magazynuSchemaLLMPathExtractor z dozwolonymi encjami i relacjamiEkstrakcja grafowa z dokumentów i śladów

Graphiti to konkretna implementacja open source relacyjnej pamięci czasowej. Śledzi zmiany faktów, przechowuje wskaźniki do epizodów źródłowych i obsługuje wyszukiwanie hybrydowe. LangGraph oddziela checkpointy wątków od magazynów współdzielonych między wątkami. Mem0 pakuje operacje pamięci jako zarządzaną usługę. Letta używa edytowalnych bloków kontekstu zamiast SGAM na poziomie pól, ale nadal traktuje stan agenta jako trwałe dane.

Zacznij od modelu danych. Jeśli główną operacją jest dokładne wyszukiwanie faktów, zwykle wystarczy tabela relacyjna z payloadami JSON, kolumnami ważności, indeksami tenantów i wektorowym sidecarem. Dodaj graf, gdy przechodzenie po relacjach jest częścią produktu, a nie dlatego, że demonstracja grafu wygląda efektownie.


Najpierw zbuduj ścieżkę zapisu, dopiero potem graf

Najpierw zdecyduj, co produkt może zapamiętywać. Wybór między grafem a wektorami przychodzi później.

Agent wsparcia może pamiętać poziom konta, otwarte sprawy i trwałe preferencje kontaktu. Nie powinien promować każdej sfrustrowanej uwagi do stanu profilu. Agent programistyczny może pamiętać konwencje repozytorium i nierozwiązane zadania. Nie powinien przechowywać prywatnej notatki w nieskończoność tylko dlatego, że została kiedyś pobrana.

Zacznij od ścieżki zapisu i traktuj pamięć jako niewielką mutację stanu:

  1. Nadaj nazwę typowi pamięci, podmiotowi, zakresowi tenanta i klasie retencji.
  2. Wyodrębnij kandydatów rekordów za pomocą ustrukturyzowanych danych wyjściowych.
  3. Zweryfikuj payload za pomocą Pydantic lub warstwy schematu używanej już przez twój stack.
  4. Rozwiąż konflikty przed wstawieniem, w tym ustal, czy nowy rekord zastępuje stary.
  5. Zachowaj wskaźnik źródłowy do surowego epizodu, wyniku narzędzia, pliku, zgłoszenia lub potwierdzenia użytkownika, które doprowadziły do utworzenia rekordu.
  6. Zapisuj wersję schematu wraz z każdym rekordem, zamiast przechowywać ją wyłącznie w kodzie aplikacji.

Pierwszym magazynem SGAM może być tabela relacyjna z kolumną JSON i kilkoma indeksami. Graf staje się użyteczny, gdy produkt musi przechodzić po relacjach, takich jak klient–konto, konto–polityka, zadanie–artefakt lub projekt–decyzja.

Ścieżka gorąca i zapisy w tle

Natychmiastowa ekstrakcja ma sens, gdy kolejna tura zależy od nowej pamięci. Jeśli użytkownik mówi „zapamiętaj, że wolę krótkie odpowiedzi”, system nie powinien czekać na nocne zadanie, zanim zacznie działać inaczej.

Większość tur nie wymaga natychmiastowego zapisu. Zapisz surowy epizod wraz z tenantem, sesją i metadanymi narzędzi, a następnie pozwól workerowi w tle wyodrębnić kandydatów później. Przy konsolidacji opartej na powtarzalności worker buforuje słabe sygnały i promuje fakt dopiero po powtórzeniu się podobnych dowodów albo potwierdzeniu przez użytkownika. Wprowadza to opóźnienie świeżości. Jest ono akceptowalne dla „użytkownik często prosi o eksport CSV”, ale ryzykowne dla „klient zmienił adres dostawy”.

Utrzymuj deterministyczną ścieżkę odczytu. Najpierw egzekwuj zakres tenanta i ważność, a dopiero potem korzystaj z wyszukiwania rozmytego, jeśli może ono dodać użyteczny kontekst.

  1. Filtruj po tenancie, typie pamięci i oknie ważności.
  2. Najpierw pobieraj dokładny stan strukturalny, a dopiero potem semantycznych sąsiadów.
  3. Używaj rozszerzania wektorowego lub grafowego do uzyskiwania dowodów pomocniczych, powiązanych encji i przykładów, a nie jako źródła bieżących faktów.
  4. Zbuduj najmniejszy kontekst z cytowaniami, który może odpowiedzieć na pytanie.

Traktuj migrację schematu jako zmianę produktu, ponieważ zmienia ona to, co agent może przywołać, cytować lub usunąć. Może również zmienić to, które historyczne fakty są uznawane za bieżące. Planuj skrypty migracji, backfille, okresy dual-read i zachowanie podczas usuwania w ramach tego samego wydania.


Kiedy SGAM jest warte złożoności

Używaj SGAM, gdy fakty mogą zmieniać się w czasie:

  • preferencje użytkownika, które można aktualizować lub odwoływać
  • informacje o kliencie lub koncie wymagające audytu
  • stan zadań dla asystentów działających przez długi czas
  • pamięć projektu agenta programistycznego
  • stan współdzielony przez wielu agentów
  • notatki dotyczące zgodności, gdy znaczenie ma proweniencja
  • pytania czasowe, takie jak „w co wierzyliśmy przed migracją?”

SGAM to przesada, gdy pamięć jest krótkotrwała, eksploracyjna lub tania do ponownego obliczenia. Jeśli agent potrzebuje tylko ciągłości przez kilka tur, wystarczą checkpoint i przycięta historia wiadomości. Statyczne pytania i odpowiedzi dotyczące dokumentów mogą wymagać wyłącznie RAG. Jeśli zaś domena jest tak niestabilna, że schemat zmienia się codziennie, typowana pamięć spowolni zespół.


Lista kontrolna ewaluacji

Oceniaj cykl życia pamięci, a także końcową odpowiedź. System może wygenerować wiarygodną odpowiedź po zapisaniu niewłaściwego faktu, pobraniu nieaktualnego faktu albo przekroczeniu granicy tenanta.

Używam tego samego podziału etapów co w moim artykule o ewaluacji RAG. Mierz etap, na którym może wystąpić błąd, zamiast ograniczać ewaluację do wygenerowanego tekstu. Dyscyplina śledzenia opisana w artykule o ewaluacji agentów również ma zastosowanie, ponieważ błąd pamięci często pojawia się w historii uruchomienia, zanim dotrze do odpowiedzi.

SGAM testowałbym przez replay. Przekaż ustaloną sekwencję epizodów do writera pamięci i po każdej istotnej turze sprawdź rejestr. Następnie zadawaj pytania o stan bieżący i stan z określonego momentu względem wynikowego magazynu.

WarstwaSzukany błądMetryki
Ekstrakcja zapisuAgent pominął fakt, wymyślił go albo utworzył niepoprawny kształtOdsetek zapisów poprawnych względem schematu, precision/recall ekstrakcji, pokrycie epizodów źródłowych
Obsługa konfliktówNieaktualny fakt pozostał bieżący albo poprawny stary fakt został nadpisanyPoprawność zastępowania, odsetek duplikatów, poprawność unieważniania nieaktualnych faktów
Izolacja i politykaPamięć wyciekła między użytkownikami albo przetrwała poza oknem politykiNaruszenia izolacji tenantów, poprawność usuwania, zgodność z retencją
Wyszukiwanie odczytuWłaściwy rekord istnieje, ale czytnik go nie pobrałDokładność stanu bieżącego, dokładność stanu z określonego momentu, recall@k względem rekordów pamięci
Ugruntowanie odpowiedziOdpowiedź wykorzystała pamięć bez wsparcia albo zacytowała niewłaściwe źródłoWsparcie twierdzeń względem epizodów źródłowych, dokładność cytowań, poprawność rozwiązywania konfliktów
OperacjeŚcieżka pamięci jest zbyt wolna, zbyt nieaktualna lub zbyt drogap95 opóźnienia zapisu, opóźnienie świeżości, opóźnienie odczytu, koszt zapytania

Benchmarki takie jak LoCoMo, LongMemEval i ATM-Bench udostępniają publiczne przypadki testowe. Nie zastępują one zestawu testów domenowych. Asystent programistyczny, bot wsparcia klienta i copilot ds. zgodności potrzebują różnych schematów, filtrów, reguł retencji i testów awarii.


Zastrzeżenia

SGAM to moje określenie wzorca, a nie standard. Istniejące projekty dzielą ten problem w różny sposób. Pamięć LangGraph i LangMem opisują magazyny krótko- i długoterminowe, profile, kolekcje, zapisy na ścieżce gorącej oraz menedżery pamięci działające w tle. Zep Graphiti używa określenia temporal Context Graph. Letta utrwala edytowalne bloki pamięci, a Mem0 oferuje zarządzaną warstwę pamięci. Microsoft GraphRAG, property graph w LlamaIndex i Cognee ujmują powiązane elementy problemu jako grafy wiedzy.

Profil użytkownika, dziennik epizodów, graf dokumentów i edytowalny przez agenta blok pamięci rozwiązują różne problemy związane z wyszukiwaniem i aktualizacją. Rezerwuję nazwę SGAM dla trwałej pamięci reprezentującej bieżący stan aplikacji, która dlatego wymaga schematu, ważności, proweniencji, obsługi konfliktów, retencji i migracji.

Typowana pamięć również może być błędna. Schemat ułatwia inspekcję niepoprawnych zapisów, ale nie czyni ich wiarygodnymi. Nadal potrzebujesz zaufania do źródła, potwierdzenia użytkownika w przypadku wrażliwych faktów, polityki konfliktów, usuwania i monitoringu.

Migracja schematu to praca. Gdy pamięć staje się stanem, bierzesz odpowiedzialność za wersjonowanie, backfille, stare rekordy i zachowanie podczas usuwania. Pomiń tę pracę, a stare rekordy przetrwają semantykę lub politykę retencji, które doprowadziły do ich utworzenia.


Referencje