Pamięć agenta AI: stan sterowany schematem i proweniencja

Tłumaczenie automatyczne

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

Ten artykuł jest dla inżynierów budujących agentów działających długo lub obsługujących wielu tenantów. Zwykła pamięć semantyczna nie ma reguły określającej, która wartość faktu jest bieżąca, więc agenci zwracają nieaktualne fakty. Gdy użytkownik zmienia termin dotyczący paszportu z 15 lipca na 30 czerwca, wyszukiwanie wektorowe może zwrócić oba stwierdzenia. Warstwa pamięci utrzymująca stan musi zapisać, że wartość z 30 czerwca zastępuje wcześniejszą.

Dowiesz się, jak przechowywać pamięć jako typowane rekordy z oknami ważności, zakresem tenanta i odnośnikami do źródeł, jak zapisać korektę bez utraty historii oraz jak testować odczyty bieżące i odczyty z określonego momentu.


Dlaczego kontekst i wyszukiwanie wektorowe zwracają nieaktualne fakty

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

Agenci działający długo muszą pamiętać preferencje, stan zadań, fakty o klientach, decyzje dotyczące narzędzi, notatki zgodności i dawne błędy. Najprostsze rozwiązanie to dopisywanie podsumowań albo wrzucanie starych notatek do magazynu wektorowego. Działa to do momentu, gdy jeden z zapamiętanych faktów się zmieni.

Wtedy agent ma dwa terminy dotyczące paszportu, dwa preferowane formaty albo dwie decyzje projektowe. Wyszukiwanie semantyczne może zwrócić obie. Podsumowanie może nadpisać jedną z nich. Długi kontekst może zawierać nieaktualną obok aktywnej. Takie projekty zwracają tekst, ale nie potrafią powiedzieć, która wartość jest bieżąca.

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 wcześniejszy fakt zastąpił ten fakt?
  • Czy mogę go usunąć albo wygasić?

Schema-Guided Agent Memory (SGAM) zapisuje te odpowiedzi jako pola i relacje, zamiast zostawiać je domyślnie w prozie.


Co oznacza SGAM

Zakres SGAM wyznaczają dwa podobnie nazwane pomysły z badań.

Schema-Guided Memory (SGM) to termin badawczy używany przez Mei i in. w According to Me: Long-Term Personalized Referential Memory QA. Praca porównuje Descriptive Memory (DM), czyli pamięć w tekście swobodnym, z pamięcią o stałym schemacie złożoną z elementów klucz-wartość. Obie reprezentacje zawierają te same informacje źródłowe w różnych strukturach. Wcześniejsze użycie tej nazwy, zbiór Schema-Guided Dialogue, śledzi stan dialogu dla API usług i nie ma związku z pamięcią długoterminową.

W tym artykule Schema-Guided Agent Memory (SGAM) oznacza wzorzec inżynierski, w którym schematy rządzą zapisami, aktualizacjami, wyszukiwaniem i usuwaniem. Schemat definiuje stan aplikacji i jego cykl życia.

ATM-Bench pokazuje, dlaczego reprezentacja ma znaczenie. Korzysta z danych osobistych z około czterech lat: e-maili, obrazów i filmów. Pytania wymagają odwołań osobistych, lokalizacji, wielu dowodów i uwzględnienia zmian w czasie. SGM udostępnia w stałej reprezentacji pola takie jak czas, źródło, lokalizacja, encje i tagi. Na trudnym podzbiorze 25 pytań tabela 3 w pracy podaje wzrost wyniku ogólnego (QS) z 25,6 dla DM do 47,3 dla SGM, gdy model odpowiadający dostaje dowody referencyjne. Przy wyszukiwaniu w systemie ATM-RAG autorów wynik na trudnym podzbiorze rośnie tylko z 14,1 do 16,3, więc główną luką nadal jest znajdowanie dowodów.

Wykres zestawia obok siebie cztery wyniki na trudnym podzbiorze. Porównaj wzrost dzięki stałym polom z dowodami referencyjnymi i z wyszukiwaniem.

Wyniki ATM-Bench na trudnym podzbiorze: z dowodami referencyjnymi DM 25,6 i SGM 47,3; z wyszukiwaniem ATM-RAG DM 14,1 i SGM 16,3Wyniki ATM-Bench na trudnym podzbiorze: z dowodami referencyjnymi DM 25,6 i SGM 47,3; z wyszukiwaniem ATM-RAG DM 14,1 i SGM 16,3

SGM kontra DM odpowiada na pytanie o przechowywanie: czy pamięć ma pozostać tekstem swobodnym, czy używać nazwanych pól? Agent produkcyjny ma jednak wcześniejszy problem. Musi zamienić nieustrukturyzowaną rozmowę w proponowaną aktualizację pamięci. Następna sekcja pokazuje, jak Schema-Guided Reasoning (SGR) kształtuje takie wywołanie modelu i gdzie SGAM przejmuje odpowiedzialność.


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

Zapis do pamięci przechodzi przez trzy warstwy. Structured Output (SO) wymusza kształt obiektu kandydata. Schema-Guided Reasoning (SGR) sprawia, że zamierzone pola modelu i intencja decyzji są widoczne w ustrukturyzowanej odpowiedzi. Schema-Guided Agent Memory (SGAM) zarządza kandydatem jako trwałym stanem po wywołaniu modelu. Zależności, kolejność i reguły cyklu życia wymuszają osobne wywołania, walidatory i polityka aplikacji.

Wcześniejsze artykuły stosują SGR w ustrukturyzowanych sędziach, planistach i routerach oraz rubrykach RAG. W każdym z nich werdykt, trasa lub plan zwykle wygasa wraz z bieżącym żądaniem. Inne uruchomienie może odczytać kandydata do pamięci po kilku dniach 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 układa pola jednej odpowiedzi modelu w kolejności, w jakiej model powinien je wypełniać. W zapisie do pamięci odpowiedź może zawierać dowód ze źródła, znormalizowany podmiot i atrybut, porównanie z bieżącym stanem oraz proponowaną aktualizację. Pydantic lub JSON Schema opisuje te pola. Natywne ustrukturyzowane dane wyjściowe dostawcy albo środowisko kierowanego dekodowania, takie jak XGrammar, utrzymuje odpowiedź w tym kształcie. Kształt nie gwarantuje poprawnego wniosku ani nie dowodzi, że model użył tych pól w tej kolejności.

SGAM decyduje, co dzieje się po powstaniu tego obiektu. Czy go zapisać? 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 pokazuje, za co odpowiada każda warstwa i jak zawodzi:

WymiarSOSGRSGAM
CelZwrócić obiekt zgodny ze schematemUczynić zamierzone pola i intencję decyzji widocznymiZarządzać trwałą pamięcią po wywołaniu modelu
ZakresJedna wygenerowana odpowiedźJedna odpowiedź modelu; polityka obejmująca kilka kroków może rozciągać się na 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
EgzekwowanieDekodowanie z ograniczeniami blokuje dane niezgodne ze schematemTylko kształt; zależności i kolejność wymuszają osobne wywołania, walidatory i politykaCykl życia wymuszają walidacja aplikacji, ograniczenia bazy danych i reguły konfliktów
Czas życiaBieżące wywołanie, chyba że aplikacja zapisze obiektŚlad rozumowania zwykle jest odrzucany po podjęciu decyzjiTrwa do aktualizacji, wygaśnięcia lub usunięcia
Typowy błądPoprawny kształt, ale błędne znaczeniePola są obecne, ale rozumowanie lub obsługa zależności nadal może być błędnaNieaktualny, zanieczyszczony, pozbawiony zakresu lub niemożliwy do zaudytowania stan

Diagram śledzi korektę z poniższego przykładu aż do jednego zapisanego wiersza i pokazuje, kto ustawia każdą kolumnę: model, usługa zapisu na podstawie żądania albo replace_fact.

Zapisany wiersz dla epizodu e2: wywołanie modelu ustawia subject, attribute, value i valid_from; usługa zapisu dodaje tenant_id i source_episode_id z żądania; replace_fact ustawia valid_to i supersedes_fact_idZapisany wiersz dla epizodu e2: wywołanie modelu ustawia subject, attribute, value i valid_from; usługa zapisu dodaje tenant_id i source_episode_id z żądania; replace_fact ustawia valid_to i supersedes_fact_id

Poniższy schemat definiuje obiekt przekazywany z ekstrakcji do usługi zapisu SGAM. Wymaga Pydantic. Aplikacja umieszcza aktualnie przechowywaną wartość w tekście prompt, żeby model mógł porównać ją z nowym dowodem:

from datetime import datetime
from typing import Literal

from pydantic import BaseModel, Field

class MemoryUpdate(BaseModel):
    evidence: str = Field(description="Exact quote from the episode")
    subject: str = Field(description="Normalized entity ID, e.g. mira")
    attribute: str = Field(description="Property being updated")
    current_value: str | None = Field(description="Stored value shown in the prompt")
    change: Literal["new", "update", "no_change"]
    value: str | None = Field(description="New value; None when change is no_change")
    valid_from: datetime = Field(description="When the new value starts to apply")

Usługa zapisu odrzuca wyniki no_change, dodaje tenant_id i source_episode_id z żądania, a resztę przekazuje do replace_fact w poniższym przykładzie. Model nigdy nie wybiera tenanta ani epizodu źródłowego.


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

Stan przechowywany zmienia tylko ścieżka zapisu. Ścieżka odczytu wybiera rekordy dla bieżącego żądania.

Przepływ przyjmowania danych to ścieżka zapisu:

  1. Zarejestruj surowy epizod z wiadomości, wyników narzędzi lub zdarzeń biznesowych. Zapisz jego tenanta i klasę retencji.
  2. Wyodrębnij typowanych kandydatów za pomocą ustrukturyzowanych danych wyjściowych.
  3. Zwaliduj ładunek za pomocą Pydantic lub warstwy schematów, której już używa twój stos, i odrzucaj błędne zapisy.
  4. Rozstrzygnij konflikty przed wstawieniem: zdecyduj, czy nowy rekord zastępuje stary, i zamknij nieaktualny fakt.
  5. Zachowaj wskaźnik do źródła rekordu: surowego epizodu, wyniku narzędzia, pliku, zgłoszenia lub potwierdzenia użytkownika.
  6. Zatwierdź rekord w magazynie SGAM razem z jego schema_version, zamiast zostawiać wersję tylko w kodzie aplikacji.

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

  1. Zacznij od pytania użytkownika.
  2. Zdecyduj, czy pytanie wymaga stanu bieżącego, czy stanu z określonego momentu.
  3. Filtruj według tenanta, typu pamięci, podmiotu, atrybutu i okna ważności.
  4. Dodaj rozszerzenie wektorowe lub grafowe tylko wtedy, gdy dokładne wyszukanie stanu nie wystarcza. Używaj go do dowodów pomocniczych, powiązanych encji i przykładów, a nie jako autorytetu dla faktów bieżących.
  5. Złóż najmniejszy kontekst z cytatami dla modelu.

Ścieżka zapisu dla epizodu e2 zamyka wiersz e1 i wstawia wiersz e2 w memory_facts; odczyt stanu bieżącego tych samych wierszy zwraca 2026-06-30, a odczyt z 2 czerwca zwraca 2026-07-15Ścieżka zapisu dla epizodu e2 zamyka wiersz e1 i wstawia wiersz e2 w memory_facts; odczyt stanu bieżącego tych samych wierszy zwraca 2026-06-30, a odczyt z 2 czerwca zwraca 2026-07-15

Czytaj ten diagram od góry do dołu. Ścieżka zapisu zamyka e1 i wstawia e2 w jednym magazynie. Ścieżka odczytu wybiera filtr stanu bieżącego lub z określonego momentu, a potem wybiera wiersze z tego samego magazynu.


Co powinno należeć do schematu pamięci

Minimalny rekord SGAM potrzebuje więcej niż text. Każde pole odpowiada na jedno pytanie z pierwszej sekcji albo pełni jedną rolę w cyklu życia:

PoleNa jakie pytanie odpowiada lub jaką rolę pełni
tenant_idDo którego tenanta należy rekord? Każdy odczyt i zapis filtruje według tego pola.
memory_idStabilny identyfikator, na który wskazują inne rekordy i zadania usuwania.
subject, attributeKlucz: która encja i która właściwość, na przykład mira i passport_deadline.
valuePrzechowywana wartość.
memory_typeRodzaj pamięci: semantyczna (stabilne fakty i profile), epizodyczna (przeszłe zdarzenia i ślady) albo proceduralna (instrukcje i reguły prompt), zgodnie z opisem w LangGraph.
schema_versionKtóry schemat zapisał rekord, żeby migracja mogła znaleźć i przekonwertować stare rekordy.
valid_from, valid_toCo jest prawdą teraz? valid_from <= now i (valid_to jest null lub now < valid_to). Co było prawdą 2 czerwca? Przedział zawiera ten moment.
supersedes_memory_idKtóry wcześniejszy fakt zastąpił ten fakt?
source_episode_idKto to powiedział? Wskazuje surowy epizod.
confidenceWynik, który usługa zapisu porównuje z progiem: awansuje kandydata do stanu bieżącego albo prosi użytkownika o potwierdzenie.
retention_policyCzy mogę go usunąć albo wygasić? Mówi zadaniom usuwania, co jeszcze muszą skasować.

Dzięki tym polom nowy termin dotyczący paszportu może zamknąć poprzedni termin bez wymazywania historii. Ta sama tabela odpowiada na zapytania o stan bieżący i z określonego momentu, a następnie wskazuje epizod źródłowy wyniku.

Używaj RAG do wyszukiwania dokumentów, a SGAM do utrzymywania zmiennego stanu. Wyszukiwanie wektorowe nadal ma miejsce w systemie przy przybliżonym przywoływaniu, klastrowaniu i rozszerzaniu. Bieżąca wartość mira.passport_deadline powinna pochodzić z rekordu pamięci o właściwym zakresie, a nie z fragmentu, który akurat znalazł się na pierwszym miejscu w rankingu.


Przykład nieaktualnego faktu

Weźmy syntetyczny ślad z dwóch epizodów, przedstawiony jako punkt odniesienia 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.

Punkt odniesienia DM przechowuje oba epizody jako tekst swobodny, więc wyszukiwanie tekstowe może zwrócić e1, bo zawiera właściwe słowa. SGAM wyodrębnia typowany fakt z każdego epizodu, z kluczem złożonym z tenanta, podmiotu i atrybutu. Powinien zwrócić e2 jako stan bieżący i zachować e1 na potrzeby zapytania historycznego.

Poniższy skrypt odpowiada na dwa z wcześniejszych pytań: co jest prawdą teraz i co było prawdą 2 czerwca. Zwróć uwagę na replace_fact: zamyka stary wiersz zamiast go usuwać i zapisuje, który wiersz zastępuje nowy. Kod wywołujący musi normalizować każdy znacznik czasu zapisany w bazie lub używany w zapytaniu do jednego formatu tekstowego UTC ze stałą dokładnością do sekund: YYYY-MM-DDTHH:MM:SSZ. SQLite porównuje te wartości TEXT leksykograficznie, więc różne przesunięcia stref czasowych lub dokładności mogą ustawić fakty w niewłaściwej kolejności. Każdy wiersz jest ważny od valid_from do valid_to, bez samej wartości valid_to. Tabela odrzuca niepoprawne przedziały i ma częściowy indeks unikalny, który dopuszcza jeden otwarty fakt na tenanta, podmiot i atrybut. replace_fact zarządza jedną transakcją i rozpoczyna ją poleceniem BEGIN IMMEDIATE przed odczytem jakichkolwiek wierszy. Wywołuj tę funkcję na połączeniu bez aktywnej transakcji. SQLite serializuje współbieżne operacje zapisu, więc po błędzie blokady lub unikalności kod wywołujący powinien ponowić całą transakcję. Na końcu dopasowanie słów kluczowych zastępuje wyszukiwanie wektorowe. Każdy ranking, który nie zna korekty, może zwrócić e1.

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,
            supersedes_fact_id integer references memory_facts (fact_id),
            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:
        db.execute("BEGIN IMMEDIATE")
        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,
                 supersedes_fact_id, source_episode_id)
            values (?, ?, ?, ?, ?, ?, ?, ?)
            """,
            (
                fact.tenant_id,
                fact.subject,
                fact.attribute,
                fact.value,
                fact.valid_from,
                successor_boundary,
                containing["fact_id"] if containing else None,
                fact.source_episode_id,
            ),
        )

def fact_at(
    db: sqlite3.Connection, tenant_id: str, subject: str, attribute: str, timestamp: str
) -> sqlite3.Row | None:
    return db.execute(
        """
        select fact_id, value, valid_from, supersedes_fact_id, 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
        """,
        (tenant_id, subject, attribute, timestamp, timestamp),
    ).fetchone()

KEY = ("acme", "mira", "passport_deadline")

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

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

# A delayed extraction predates e1. It ends at e1's existing boundary,
# rather than closing e2, which remains current.
replace_fact(db, MemoryFact(*KEY, "2026-08-01", "2026-05-30T08:00:00Z", "e0"))
delayed = fact_at(db, *KEY, "2026-05-31T00:00:00Z")
assert (delayed["value"], delayed["source_episode_id"]) == ("2026-08-01", "e0")
current = fact_at(db, *KEY, "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:")

print("Naive text memory:")
print(f"  returned episode: {naive_match}")
print()
print("SGAM current state:")
print(f"  mira.passport_deadline = {current['value']}")
print(f"  valid_from={current['valid_from']}, source={current['source_episode_id']}")
print()
print("SGAM point-in-time state:")
print(f"  on 2026-06-02, mira.passport_deadline = {historical['value']}")

Uruchomienie skryptu wypisuje:

Naive text memory:
  returned episode: e1: Mira prefers concise answers. Her 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

replace_fact najpierw odrzuca równe valid_from: taki konflikt wymaga polityki aplikacji, na przykład pierwszeństwa źródeł albo korekty potwierdzonej przez użytkownika, a nie cichego nadpisania. W przeciwnym razie szuka przedziału zawierającego nadchodzący znacznik czasu. Jeśli go znajdzie, zamyka tego poprzednika na nadchodzącej granicy, nadaje wstawianemu wierszowi dawny koniec poprzednika i zapisuje poprzednika w supersedes_fact_id. Jeśli znacznik czasu leży przed każdym przechowywanym przedziałem, używa valid_from następnego przedziału jako końca nowego wiersza. Dlatego opóźniony fakt sprzed e1 dostaje przedział [2026-05-30, 2026-06-01) i zostawia nietknięte e1 oraz bieżące e2. Częściowy indeks unikalny to zabezpieczenie bazy danych dla jednego otwartego wiersza. DM nie ma odpowiednika kroku aktualizacji, więc stary tekst nadal może wyprzedzić korektę w rankingu.

Oś czasu pokazuje wiersze po każdym wywołaniu replace_fact w skrypcie, a potem trzy odczyty. Zwróć uwagę na krok 2, w którym e2 zamyka e1, oraz na krok 3, w którym opóźnione e0 kończy się tam, gdzie zaczyna się e1.

Wiersze dla mira.passport_deadline na osi dat po każdym wywołaniu replace_fact: e1 otwarty od 1 czerwca; potem e1 zamknięty 3 czerwca i otwarty e2; potem dodane e0 od 30 maja do 1 czerwca; odczyty z 31 maja, 2 czerwca i 4 czerwca zwracają e0, e1 i e2Wiersze dla mira.passport_deadline na osi dat po każdym wywołaniu replace_fact: e1 otwarty od 1 czerwca; potem e1 zamknięty 3 czerwca i otwarty e2; potem dodane e0 od 30 maja do 1 czerwca; odczyty z 31 maja, 2 czerwca i 4 czerwca zwracają e0, e1 i e2

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


Wybór magazynu wynika ze wzorca wyszukiwania

Projekty używają kilku nazw dla części tego wzorca: magazyny pamięci, grafy kontekstu, profile, magazyny długoterminowe, graph RAG i agenci ze stanem.

Narzędzie lub frameworkGłówna warstwa przechowywaniaMechanizm stanu czasowegoMechanizm schematuUżywaj, gdy
GraphitiNeo4j, FalkorDB i Amazon Neptune; Kuzu jest przestarzałyPrzedziały ważności faktów plus proweniencja epizodów źródłowychTypy encji i krawędzi w Pydantic, krawędzie czasowe, proweniencjaPotrzebujesz przedziałów ważności i przechodzenia po grafie, a bazę grafową będziesz uruchamiać sam
ZepWłasnościowy Context Graph EngineZarządzane czasowe grafy kontekstu, które zachowują zmieniające się faktyDomyślne i własne typy encji i krawędziChcesz tego samego czasowego modelu grafu jako usługi zarządzanej
LangGraph / LangMemMagazyny LangGraph, magazyny oparte na PostgresieZnaczniki czasu i pola w rekordach magazynu należą do aplikacjiMagazyny JSON oraz ekstrakcja profilu lub kolekcji przez PydanticJuż używasz LangGraph i godzisz się na samodzielne zarządzanie znacznikami czasu i regułami konfliktów
Mem0Stos zarządzany, Valkey / Redis / backendy wektorowe w instalacjach OSSAktualizacje pamięci; polityka czasowa pozostaje po stronie aplikacjiTypy pamięci, własne kategorie, prompt do ekstrakcjiChcesz zarządzanych operacji dodawania, wyszukiwania, aktualizacji i usuwania, z zakresem użytkownika, agenta, aplikacji lub uruchomienia, i sam przejmiesz politykę czasową
Letta / MemGPTStan agenta i bloki pamięci oparte na bazie danychEdytowalne bloki bez przedziałów ważności na poziomie pólEdytowalne, oznaczone etykietami bloki pamięciAgent edytuje własne bloki pamięci albo kilku agentów potrzebuje jednego wspólnego bloku, który wszyscy widzą
CogneeGraf plus backendy wektorowe i relacyjneHistoria zależy od ontologii i wybranego backenduEkstrakcja i walidacja oparte na ontologiiWalidujesz ekstrakcję względem ontologii dziedziny
LlamaIndex property graphMagazyny grafów właściwości plus magazyny wektorowePola czasu zależą od schematu grafu i magazynuSchemaLLMPathExtractor z dozwolonymi encjami i relacjamiWyodrębniasz graf właściwości z dokumentów lub śladów

LangGraph oddziela zapisy checkpoint wątków, które wznawiają jedną rozmowę, od magazynów międzywątkowych, które przechowują dane odczytywane przez późniejsze wątki. Praca o Zep podaje wzrosty na Deep Memory Retrieval i LongMemEval, a praca o Mem0 podaje wzrosty na LoCoMo. Każdy dostawca napisał własną pracę, więc traktuj te liczby jako wyniki dostawców, a nie gwarancje dla twojego obciążenia.

Zacznij od modelu danych. Jeśli główną operacją jest dokładne wyszukiwanie faktów, zwykle wystarczy tabela relacyjna z ładunkami JSON, kolumnami ważności, indeksami tenantów i pomocniczym magazynem wektorowym. Dodaj graf, gdy zapytania produktu podążają za relacjami.


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 zapamiętywać poziom konta, otwarte sprawy i trwałe preferencje kontaktu. Nie powinien awansować każdej sfrustrowanej uwagi na stan profilu. Agent programistyczny może zapamiętywać konwencje repozytorium i nierozwiązane zadania. Nie powinien przechowywać prywatnej notatki w nieskończoność tylko dlatego, że raz została wyszukana.

Pierwszy magazyn SGAM może być tabelą relacyjną z kolumną JSON i kilkoma indeksami, zapisywaną sześcioma krokami ścieżki zapisu opisanej wyżej. Graf staje się przydatny, gdy produkt musi przechodzić po relacjach, takich jak klient-konto, konto-polityka, zadanie-artefakt czy projekt-decyzja.

Zapisy natychmiastowe i zapisy w tle

Natychmiastowa ekstrakcja ma sens, gdy następna tura zależy od nowej pamięci. Jeśli użytkownik mówi „zapamiętaj, że wolę krótkie odpowiedzi”, system nie powinien potrzebować nocnego zadania, żeby zacząć zachowywać się inaczej.

Większość tur nie wymaga natychmiastowego zapisu. Zapisz surowy epizod z metadanymi tenanta, sesji i narzędzi, a potem pozwól procesowi w tle wyodrębnić kandydatów później. Przy konsolidacji opartej na powtarzalności proces buforuje słabe sygnały i awansuje fakt dopiero wtedy, gdy podobny dowód się powtórzy albo użytkownik go potwierdzi. Dodaje to opóźnienie świeżości. Jest ono akceptowalne dla „użytkownik często prosi o eksporty CSV” i ryzykowne dla „klient zmienił adres dostawy”.

Traktuj migrację schematu jak zmianę produktu, bo zmienia to, co agent może przywołać, zacytować lub usunąć. Może też zmienić, które fakty historyczne uchodzą za bieżące. Zaplanuj skrypty migracji, uzupełnianie danych, okna podwójnego odczytu i zachowanie przy usuwaniu w tym samym wydaniu.


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łać
  • fakty o kliencie lub koncie objęte wymaganiami audytu
  • stan zadań dla długo działających asystentów
  • pamięć projektowa agenta programistycznego
  • wspólny stan wielu agentów
  • notatki zgodności, w których liczy się 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 kilku tur ciągłości, wystarczą checkpoint i przycięta historia wiadomości. QA na statycznych dokumentach może wymagać tylko RAG. A jeśli dziedzina jest tak niestabilna, że schemat zmienia się codziennie, typowana pamięć spowolni zespół.

Siatka umieszcza każdy projekt według dwóch pytań: czy twoje fakty się zmieniają i czy zapytania podążają za powiązaniami między rekordami?

Siatka dwa na dwa projektów pamięci: historia i RAG, graf dokumentów, tabela ważności oznaczona „zacznij tutaj” oraz graf temporalny z Graphiti lub ZepSiatka dwa na dwa projektów pamięci: historia i RAG, graf dokumentów, tabela ważności oznaczona „zacznij tutaj” oraz graf temporalny z Graphiti lub Zep


Lista kontrolna ewaluacji

Oceniaj cykl życia pamięci, a nie tylko końcową odpowiedź. System może wytworzyć wiarygodną odpowiedź po zapisaniu błędnego faktu, pobraniu nieaktualnego albo przekroczeniu granicy tenanta.

Stosuję ten sam podział etap po etapie co w artykule o ewaluacji RAG. Mierz etap, na którym może wystąpić błąd, zamiast ograniczać ewaluację do wygenerowanego tekstu. Zastosowanie ma też dyscyplina śladów z artykułu o ewaluacji agentów, bo błąd pamięci często widać w historii uruchomienia, zanim dotrze do odpowiedzi.

SGAM testowałbym przez odtwarzanie. Podaj procesowi zapisu pamięci stałą sekwencję epizodów i sprawdzaj rejestr po każdej istotnej turze. Potem zadawaj pytania o stan bieżący i stan z określonego momentu względem powstałego magazynu.

WarstwaBłąd, którego szukaszMiary
Ekstrakcja zapisuAgent pominął fakt, wymyślił go albo wytworzył niepoprawny kształtOdsetek zapisów zgodnych ze schematem, precyzja i 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 przy odczycieWłaściwy rekord istnieje, ale czytnik go nie pobrałDokładność stanu bieżącego, dokładność stanu z określonego momentu, recall@k dla rekordów pamięci
Ugruntowanie odpowiedziOdpowiedź użyła pamięci bez poparcia albo zacytowała złe źródłoPoparcie twierdzeń epizodami źródłowymi, poprawność cytowań, poprawność rozstrzygania konfliktów
OperacjeŚcieżka pamięci jest zbyt wolna, zbyt nieaktualna lub zbyt drogaOpóźnienie zapisu p95, opóźnienie świeżości, opóźnienie odczytu, koszt zapytania

Benchmarki takie jak LoCoMo, LongMemEval i ATM-Bench dostarczają publiczne przypadki testowe. Nie zastępują zestawu testów dla twojej dziedziny. Asystent programistyczny, bot wsparcia klienta i copilot zgodności potrzebują różnych schematów, filtrów, reguł retencji i testów błędów.

From traces to better memory prowadzi pętlę usprawnień na narzędziu pamięci, które przechowuje fakty z właścicielem, zdarzeniem źródłowym i przedziałem ważności, według tych reguł.


Zastrzeżenia

SGAM to moja etykieta dla wzorca, a nie standard. Narzędzia z tabeli wyżej używają innych nazw dla jego części: magazyny pamięci, czasowe grafy kontekstu, bloki pamięci i grafy wiedzy. Microsoft GraphRAG opisuje powiązaną część problemu jako wyszukiwanie oparte na grafie nad dokumentami.

Profil użytkownika, log epizodów, graf dokumentów i edytowalny przez agenta blok pamięci rozwiązują różne problemy wyszukiwania i aktualizacji. Nazwę SGAM rezerwuję dla trwałej pamięci, która reprezentuje bieżący stan aplikacji i dlatego potrzebuje schematu, ważności, proweniencji, obsługi konfliktów, retencji i migracji.

Typowana pamięć nadal może być błędna. Schemat ułatwia kontrolę złych zapisów, ale nie czyni ich wiarygodnymi. Nadal potrzebujesz zaufania do źródeł, potwierdzenia przez użytkownika dla faktów wrażliwych, polityki konfliktów, usuwania i monitorowania.


Referencje