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:
| Wymiar | SO | SGR | SGAM |
|---|---|---|---|
| Cel | Zwrócenie obiektu zgodnego ze schematem | Umożliwienie inspekcji zamierzonych pól i intencji decyzyjnej | Zarządzanie trwałą pamięcią po wywołaniu modelu |
| Zakres | Jedna wygenerowana odpowiedź | Jedna odpowiedź modelu; polityka obejmująca wiele kroków może obejmować wywołania | Rekordy używane między wywołaniami, sesjami i uruchomieniami |
| Rola schematu | Definiuje pola wyjściowe, typy i dozwolone wartości | Opisuje dowody, pola pośrednie i decyzję kandydata | Definiuje przechowywane rekordy, relacje i cykl życia |
| Egzekwowanie | Constrained decoding blokuje dane wyjściowe niezgodne ze schematem | Tylko 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 życia | Bieżące wywołanie, chyba że aplikacja przechowa obiekt | Ślad rozumowania jest zwykle odrzucany po podjęciu decyzji | Trwa do aktualizacji, wygaśnięcia lub usunięcia |
| Tryb awarii | Poprawny kształt, ale błędne znaczenie | Pola są obecne, lecz rozumowanie lub obsługa zależności nadal może być błędna | Nieaktualny, 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:
- Przechwyć surowy epizod z wiadomości, wyników narzędzi lub zdarzeń biznesowych.
- Wyodrębnij typowanych kandydatów przez ustrukturyzowane dane wyjściowe.
- Zweryfikuj schemat i odrzuć niepoprawne zapisy.
- Uzgodnij konflikty, zamknij nieaktualne fakty i zachowaj proweniencję.
- Zatwierdź rekord w magazynie SGAM.
Przepływ żądania to ścieżka odczytu:
- Zacznij od pytania użytkownika.
- Zdecyduj, czy pytanie wymaga bieżącego stanu, czy stanu z określonego momentu.
- Filtruj po tenancie, typie pamięci, podmiocie, atrybucie i oknie ważności.
- Dodaj rozszerzenie wektorowe lub grafowe tylko wtedy, gdy dokładne wyszukanie stanu nie wystarcza.
- Zbuduj najmniejszy kontekst dla modelu, opatrzony cytowaniami.
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 framework | Główna warstwa przechowywania | Mechanizm stanu czasowego | Mechanizm schematu | Praktyczne zastosowanie |
|---|---|---|---|---|
| Graphiti | Neo4j, FalkorDB i Amazon Neptune; Kuzu jest przestarzałe | Przedziały ważności faktów oraz proweniencja epizodu źródłowego | Typy encji i krawędzi Pydantic, krawędzie czasowe, proweniencja | Samodzielnie hostowana czasowa pamięć grafowa |
| Zep | Zastrzeżony Context Graph Engine | Zarządzane czasowe Context Graphs przechowujące zmieniające się fakty | Domyślne oraz niestandardowe typy encji i krawędzi | Zarządzana pamięć agentów i governance |
| LangGraph / LangMem | Magazyny LangGraph, magazyny oparte na Postgresie | Znaczniki czasu i pola zarządzane przez aplikację w rekordach magazynu | Magazyny JSON oraz ekstrakcja profili lub kolekcji za pomocą Pydantic | Aplikacje agentów zbudowane już na LangGraph |
| Mem0 | Zarządzany stack, Valkey / Redis / backendy wektorowe w konfiguracjach OSS | Aktualizacje pamięci; polityka czasowa pozostaje własnością aplikacji | Typy pamięci, własne kategorie, prompty ekstrakcji | Pamięć użytkownika, agenta i sesji jako usługa |
| Letta / MemGPT | Stan i bloki pamięci agenta oparte na bazie danych | Edytowalne bloki bez przedziałów ważności na poziomie pól | Edytowalne etykietowane bloki pamięci | Agenci stanowi z zarządzaniem kontekstem w stylu systemu operacyjnego |
| Cognee | Graf oraz backendy wektorowe i relacyjne | Historia zależy od ontologii i wybranego backendu | Ekstrakcja i walidacja ukierunkowane na ontologię | Pamięć grafu wiedzy dla przedsiębiorstw |
| LlamaIndex property graph | Magazyny grafów właściwości oraz magazyny wektorowe | Pola czasu zależą od schematu grafu i magazynu | SchemaLLMPathExtractor z dozwolonymi encjami i relacjami | Ekstrakcja 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:
- Nadaj nazwę typowi pamięci, podmiotowi, zakresowi tenanta i klasie retencji.
- Wyodrębnij kandydatów rekordów za pomocą ustrukturyzowanych danych wyjściowych.
- Zweryfikuj payload za pomocą Pydantic lub warstwy schematu używanej już przez twój stack.
- Rozwiąż konflikty przed wstawieniem, w tym ustal, czy nowy rekord zastępuje stary.
- 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.
- 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.
- Filtruj po tenancie, typie pamięci i oknie ważności.
- Najpierw pobieraj dokładny stan strukturalny, a dopiero potem semantycznych sąsiadów.
- 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.
- 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.
| Warstwa | Szukany błąd | Metryki |
|---|---|---|
| Ekstrakcja zapisu | Agent pominął fakt, wymyślił go albo utworzył niepoprawny kształt | Odsetek zapisów poprawnych względem schematu, precision/recall ekstrakcji, pokrycie epizodów źródłowych |
| Obsługa konfliktów | Nieaktualny fakt pozostał bieżący albo poprawny stary fakt został nadpisany | Poprawność zastępowania, odsetek duplikatów, poprawność unieważniania nieaktualnych faktów |
| Izolacja i polityka | Pamięć wyciekła między użytkownikami albo przetrwała poza oknem polityki | Naruszenia izolacji tenantów, poprawność usuwania, zgodność z retencją |
| Wyszukiwanie odczytu | Wł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 odpowiedzi | Odpowiedź wykorzystała pamięć bez wsparcia albo zacytowała niewłaściwe źródło | Wsparcie 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 droga | p95 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
- According to Me: Long-Term Personalized Referential Memory QA – artykuł Mei i in. wprowadzający ATM-Bench i Schema-Guided Memory.
- Towards Scalable Multi-Domain Conversational Agents: The Schema-Guided Dialogue Dataset – artykuł Rastogiego i in. o zbiorze danych SGD.
- LongMemEval: Benchmarking Chat Assistants on Long-Term Interactive Memory – benchmark Wu i in. do oceny długoterminowych zdolności pamięci.
- Graphiti: Build Temporal Context Graphs for AI Agents
- Zep: A Temporal Knowledge Graph Architecture for Agent Memory
- Mem0 Platform Overview
- Mem0: Building Production-Ready AI Agents with Scalable Long-Term Memory
- LangGraph Memory Overview
- LangGraph Persistence
- LangMem documentation
- Letta: Introduction to Stateful Agents
- Microsoft GraphRAG documentation
- LlamaIndex: Using a Property Graph Index
- Cognee Documentation
- Pydantic model validation docs
- Python sqlite3 documentation