Strona główna / Artykuły

Daj agentom sztucznej inteligencji zewnętrzną pamięć zamiast wypełniania okna kontekstowego.

Zastąp agentów w postaci złotych rybek pętlą Read-Reason-Act-Write działającą na hybrydowej pamięci wektorowej i graficznej, wraz z mechanizmami konsolidacji, przycinania danych, oceny oraz kontroli prywatności.

Wydawca: REACTAPP.TOP11 min czytania2623 słów

Źródła: Your AI Agent’s Memory Problem: The New Architecture You Need

Chattyboty bez stanu są resetowane po każdej sesji. Agenci, którzy piszą kod, analizują rynki lub koordynują procesy, często postępują podobnie: gdy proces się zakończy, preferencje, błędy oraz kontekst projektu znikają. Rozwiązaniem nie jest coraz dłuższy prompt – chodzi o traktowanie pamięci jako pierwszorzędnego zewnętrznego podsystemu, dzięki któremu agenci mogliby uczyć się przez tysiące interakcji.

Problem złotej rybki: Dlaczego dzisiejsze agenty AI zapominają

Nazwijmy to agentem ryby złotej: każda interakcja zaczyna się od zera, bez możliwości przeniesienia wcześniejszych lekcji, preferencji użytkownika czy błędów. Zatrudnienie elitarnego konsultanta, który za każdym razem po zamknięciu laptopa zapomina twoje imię, kod bazowy oraz cele, spowodowałoby marnotrawstwo połowy budżetu na ponowne informowanie. Stosy agentów bez stanu powodują takie samo marnotrawstwo. Dema produktowe ukrywają ten problem, ponieważ każda sesja oceny trwa krótko; problemy pojawiają się w produkcji, gdy użytkownik wraca po kilku dniach, a agent ponownie prosi o preferencje, które już zostały zebrane, lub proponuje ponownie projekt odrzucony przez zespół w poprzednim sprintzie. Agenty narzędzi wieloetapowych zwiększają te koszty: bez możliwości zapisywania informacji, każdy długi proces musi na nowo odkrywać specyfiki środowiska, których wcześniejsze sesje już się nauczyły.

+-------------------------------------------------------------+
|                     User Session Ends                       |
|                              |                              |
|                              ▼                              |
|   +-----------------------------------------------------+   |
|   |             Stateless LLM Call Clears               |   |
|   |  - System Prompts: Reset                            |   |
|   |  - Session Context: Erased                          |   |
|   |  - Learned Preferences: Forgotten                   |   |
|   +-----------------------------------------------------+   |
|                              |                              |
|                              ▼                              |
|                   Next Run = Day One Again                  |
+-------------------------------------------------------------+

Iluzja dużych okien kontekstowych

Zespoły często udają przywołanie kontekstu, wkładając pełne historie i pliki do okna kontekstowego przy każdej turze. Okna o pojemności miliona tokenów sprawiają, że jest to kuszące, ale wykorzystywanie ich jako długoterminowej pamięci to antypatern. Natychmiast pojawiają się trzy koszty:

  • Rostące wydatki, ponieważ każde wezwanie przetwarza ogromne zapytania
  • Opóźnienia, gdy czas do otrzymania pierwszego tokena wzrasta wraz z długością zapytania
  • Zakłócenia w kontekście spowodowane efektami „zagubienia się pośrodku”, gdy kluczowe fakty giną w dużych ilościach tekstu

Większe okna pomagają w interaktywnych rozmowach, ale nie zastępują trwałego przechowywania stanu. Ponadto łączą cenę i opóźnienia z długością historii, przez co najskuteczniejsi użytkownicy stają się tymi najdroższymi do obsługi.

Przejście od większego kontekstu do mądrzejszego kontekstu

Systemy autonomiczne potrzebują pamięci jako zewnętrznego komponentu architektonicznego, a nie jakiegoś triku z promptami. Ta zmiana przekształca agenta z funkcji bez stanu w strukturę posiadającą stan, która może być przez niego czytana, zapisywana i syntetyzowana z upływem czasu. Następne pokolenie inżynierii agentów skupia się mniej na rozszerzaniu zakresu uwagi, a bardziej na oddzielonej pamięci, która przetrwa dłużej niż jeden pojedynczy żądanie.

Pętla pamięci: plan architektoniczny dla poznawania agentów

Agent nie może traktować każdego kroku jako czystej kartki. Potrzebuje pętli poznawczej, która ciągle pobiera, filtrowa i przechowuje informacje – to właśnie stanowi podstawę przekształcania modeli LLM z silników predykcyjnych w pracowniki posiadające stan.

Pomyśl o dniu doświadczonego inżyniera: czytanie kodu, planowanie, implementacja, zapisywanie zmian. Pamięć agenta formalizuje tę ludzką pętlę w cykl powtarzalny.

Cykl Czytaj-Rozważ-Działaj-Zapisz

Read-Reason-Act-Write (RRAW) dzieli proces wykonywania na cztery kolejne fazy:

  • Czytanie — przed stworzeniem zapytania wyszukiwane są w długoterminowych bazach dane dotyczące istotnej historii, preferencji oraz wcześniejszych decyzji
  • Rozumowanie — model łączy aktualne żądanie z pozyskanymi danymi, aby zaplanować działania
  • Działanie — zgodnie z tym planem wykorzystywane są narzędzia, API lub bazy danych
  • Pisanie — syntetyzowane są wyniki i informacje zwrotne, a następnie umieszczane w pamięci

Zapisy do pamięci powinny odbywać się asynchronicznie. Blokowanie odpowiedzi dla użytkownika podczas aktualizacji wektorów lub grafów powoduje nieakceptowalne opóźnienia. Praktycznym rozwiązaniem jest natychmiastowe potwierdzenie ruchu użytkownika z drogi powodu/działania, umieszczenie zadania konsolidacji w kolejce wraz z surowym ciężarem interakcji oraz pozostawienie pracownikom możliwości aktualizacji indeksów oddzielnie, z powtórkami i obsługą wiadomości błędnych.

Przepływ danych w architekturze

Wdrożenie tego cyklu oznacza sformalizowanie sposobu, w jaki dane przemieszczają się pomiędzy użytkownikiem, orkiestratorem, modelem a systemem przechowywania:

[User Input]
     │
     ▼
[Agent Orchestrator] <─────────────────────────────┐
     │                                             │ (Loopback)
     ├─────────────────► [Memory Retrieval]        │
     │                           │                 │
     ▼                           ▼                 │
[LLM Prompt Assembler] <─── [Context]              │
     │                                             │
     ▼                                             │
[Large Language Model (LLM)]                       │
     │                                             │
     ▼                                             │
[Action Executor]                                  │
     │                                             │
     ▼                                             │
[Memory Writeback Module] ─────────────────────────┘
     │
     ▼
[(Episodic / Semantic / Procedural Stores)]

Trzy filary pamięci agentowej

Jedna, nierozróżniona baza danych powoduje szum przy wyszukiwaniu i nieelastyczne schematy. Długoterminową pamięć należy podzielić na trzy role:

  • Pamięć epizodyczna — chronologiczne zapisy interakcji; narracyjna autobiografia
  • Pamięć semantyczna — usunięcie duplikatów faktów i relacji między entytetami; to, co jest prawdą o świecie
  • Pamięć proceduralna — skuteczne schematy pracy i narzędzia; jak wykonać zadanie
  • Rozdzielenie mózgu od pamięci

    Zaletą tego podejścia inżynieryjnego jest oddzielenie procesowania (LLM) od stanu (magazynów danych). Dzięki temu unika się ograniczeń związanych z rozmiarem okna kontekstowego oraz kosztów ciągłej optymalizacji. Magazyn danych może się skalować horyzontalnie w miarę gromadzenia większej ilości danych, podczas gdy model pozostaje ograniczonym, w dużej mierze bezstanowym wykonawcą. Zespoły mogą wymieniać modele embeddingowe lub schematy grafów bez konieczności ponownego szkolenia modelu do rozmów, a także dzielić pamięć według użytkowników, tak aby wzrost aktywności jednego klienta nie wpływał negatywnie na dostępność danych dla innego.

    Budowa magazynu pamięci: hybrydowy zestaw technologii

    Kognicja ludzka oddziela przywoływanie epizodyczne od wiedzy semantycznej. Umieszczanie ich obu w jednym repozytorium skutkuje systemami, które są albo zbyt niejasne, albo zbyt sztywne. Dlatego też stacki produkcyjne łączą bazy danych wektorowych do wyszukiwania asocjacyjnego z bazami danych grafowymi do reprezentowania relacji deterministycznych.

    Bazy danych wektorowych do przywoływania semantycznego i epizodycznego

    Pinecone, Milvus, Weaviate oraz podobne rozwiązania przechowują embeddingi zdarzeń i dokumentów. Metryki bliskości, takie jak podobieństwo kosinowe, ujawniają powiązane wspomnienia:

    import numpy as np
    from openai import OpenAI
    
    client = OpenAI()
    
    def get_embedding(text: str) -> list[float]:
        """Generates a 1536-dimensional vector embedding for semantic search."""
        response = client.embeddings.create(
            model="text-embedding-3-small",
            input=text
        )
        return response.data[0].embedding
    
    # Store an episodic memory with its vector representation
    episodic_memory = "Met with Sarah yesterday. She greenlit the budget for Project Titan."
    memory_vector = get_embedding(episodic_memory)
    
    # This vector allows us to find this memory later using natural language queries
    # like 'What did we agree on with Sarah?' even if 'budget' is omitted.
    

    Szukanie wektorowe doskonale sprawdza się przy otwartych pytaniach typu „O czym rozmawialiśmy w zeszłym kwartale na temat migracji do chmury?”. Jest mniej skuteczne, gdy odpowiedzią musi być precyzyjny, ustrukturyzowany fakt – sąsiedzi probabilistyczni nie stanowią gwarancji. Podobieństwo jest odpowiednim narzędziem do „przypomnienia mi, co mówiliśmy o migracji”, ale nie do zarządzania wypłatami ani relacjami kontroli dostępu, które muszą być rozstrzygnięte na podstawie jednej poprawnej informacji.

    Bazy danych grafowych do deterministycznego mapowania wiedzy

    Zapytanie „Kto kieruje projektem Titan?” może zwrócić dokument, w którym wymienione są oba nazwiska, bez udowodnienia istnienia między nimi związku. Bazy takie jak Neo4j modelują entity jako węzły, a relacje jako krawędzie, aby zapewnić pewność wynikającą z przeszukiwania grafu:

    (Project: Titan) <-[:LEADS]- (Employee: Sarah) -[:REPORTS_to]-> (VP: Marcus)
    

    Agent przemierza zweryfikowaną ścieżkę LEADS, zamiast zgadywać na podstawie podobieństwa.

    Rozwiązanie hybrydowe: zorganizowane pobieranie danych od wektorów do grafów

    Pamięć produkcyjna zazwyczaj koordynuje oba silniki. Wektory przekształcają niejasne intencje na identyfikatory entytetów; następnie graf zwraca dokładne relacje i granice:

    # Real-world query: "Who is the lead on that document-indexing project?"
    
    def hybrid_memory_lookup(user_query: str) -> dict:
        # Step 1: Query Vector DB to find the most contextually relevant project
        query_vector = get_embedding(user_query)
    
        # Mock Vector DB search returning the closest matching entity ID
        matched_project_id = "project_titan_404"  # Found via match on "document-indexing"
    
        # Step 2: Query Graph DB using the strict entity ID to find the exact lead
        # Cypher Query: MATCH (p:Project {id: $id})<-[:LEADS]-(e:Employee) RETURN e.name
        graph_record = mock_neo4j_driver.run(
            "MATCH (p:Project {id: $id})<-[:LEADS]-(e:Employee) RETURN e.name",
            id=matched_project_id
        )
    
        return {
            "project_id": matched_project_id,
            "lead_name": graph_record["e.name"]
        }
    
    # This hybrid flow eliminates vector hallucination by anchoring
    # fuzzy natural language inputs to hard, deterministic graph relationships.
    

    Najpierw wyodrębnij entytety z zapytania użytkownika i użyj tych identyfikatorów do siania danych w grafie, zamiast wprowadzać surowy tekst do zapytań przypominających Cypher. Łączenie baz danych asocjacyjnych i strukturalnych pozwala agentom utrzymywać kontekst rozmowy, jednocześnie zachowując dokładność w odniesieniu do faktów korporacyjnych. W praktyce koordynator może wykonać tani test wektorowy, wyodrębnić kandydatów na identyfikatory entytetów, zweryfikować je w grafie i dopiero wtedy wprowadzić kompaktowy zestaw zweryfikowanych trójek do promptu.

    Zapisywanie każdej wypowiedzi bezpośrednio do indeksu wektorowego powoduje przepełnienie pamięci oraz szum przy wyszukiwaniu — biurko pokryte notatkami z każdego spotkania, włączając powitania. Znalezienie decyzji podjętej trzy miesiące temu staje się niemożliwe. Bez konsolidacji opóźnienie przy wyszukiwaniu rośnie, wyniki typu top-k wypełniają się powitaniami, a model zaczyna cytować przestarzałe lub sprzeczne fragmenty. Asynchroniczny proces konsolidacji przekształca surowe logi w wartościowe informacje:

    [Raw Interaction Data] → [Extraction] → [Synthesis] → [Pruning] → [Long-Term Memory]
    

    Trzy etapy konsolidacji pamięci

    Konsolidację należy przeprowadzać poza głównym szlakiem obsługi zapytań — za pomocą zdarzeń typu cron lub na końcu sesji — w trzech etapach:

    1. Ekstrakcja — wyodrębnienie faktów, preferencji i decyzji z niepotrzebnych informacji z rozmowy
    2. Synteza — połączenie z istniejącymi długoterminowymi записami, rozwiązanie konfliktów, odświeżenie przestarzałych danych
  • Oczyszczanie — usuwanie pamięci o niskiej użyteczności lub tymczasowych informacji na podstawie oceny
  • Syntetyzowanie rozmów z ustrukturyzowanymi wynikami

    Ekstrakcja i syntetyzacja wymagają generowania zgodnego ze schematem. Modele w stylu Pydantic zapewniają, że dane są gotowe do zapisu w bazie danych:

    import os
    from typing import List
    from pydantic import BaseModel, Field
    from openai import OpenAI
    
    # Define the target structure for consolidated memory
    class MemoryItem(BaseModel):
        fact: str = Field(description="A single, atomic statement of fact about the user or their environment.")
        category: str = Field(description="The functional domain (e.g., tech_stack, preference, constraint).")
        importance: int = Field(description="A subjective importance rating from 1 (low) to 5 (high).")
    
    class ConsolidatedMemory(BaseModel):
        memories: List[MemoryItem]
    
    def synthesize_transcript(transcript: str) -> ConsolidatedMemory:
        """
        Parses a raw transcript and extracts high-signal, deduplicated facts.
        This structured parsing prevents raw noise from polluting the vector store.
        """
        client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))
    
        prompt = f"""
        Analyze the following chat transcript between a user and an AI.
        Extract key facts, user preferences, and technical constraints.
        Ignore conversational noise, greetings, and transition phrases.
    
        Transcript:
        {transcript}
        """
    
        # Use structured outputs to guarantee the response matches the Pydantic schema
        completion = client.beta.chat.completions.parse(
            model="gpt-4o-mini",
            messages=[
                {"role": "system", "content": "You are an expert data consolidation engine."},
                {"role": "user", "content": prompt}
            ],
            response_format=ConsolidatedMemory,
        )
    
        return completion.choices[0].message.parsed
    # Example Usage
    raw_chat = """
    User: Hey! I'm starting a new web project. I want to use FastAPI and PostgreSQL.
    AI: That's a great stack! Do you want to use Docker?
    User: Yes, Docker is a must. And remember, I hate using MongoDB, so let's avoid that completely.
    """
    
    structured_output = synthesize_transcript(raw_chat)
    for item in structured_output.memories:
        print(f"[{item.category.upper()}] Fact: {item.fact} (Importance: {item.importance}/5)")
    

    Algorytmiczne oczyszczanie i ocena istotności

    Nie każda zsyntetyzowana informacja zasługuje na trwałość. Oceniaj użyteczność na podstawie ważności, świeżości i częstotliwości, na przykład:

    Użyteczność = (Ważność * LiczbaDostępów) / (1 + (TempoZanikania * DniOdOstatniegoDostępu))

    Dane poniżej określonego progu są usuwane, aby indeksy pozostały małe i istotne. Konsolidacja partiami w okresach o niskim obciążeniu służy ograniczeniu wydatków na tokeny oraz uniknięciu konfliktów przy zapisywaniu danych w intensywnie używanych indeksach. Wyniki oceny ważności uzyskane z modeli LLM powinny być regularnie kalibrowane na podstawie próbek przeglądanych przez ludzi, aby system nie przechowywał zbyt wielu błahostek ani zbyt mało rzadkich, lecz kluczowych decyzji.

    Zastosowania w praktyce

    Model bez stanu jest jak konsultant cierpiący na amnezję – genialny w izolacji, lecz wyczerpujący w codziennej pracy. Pamięć trwała przekształca generatorów w partnerów, którzy zwiększają wartość:

    • Kolegów z AI, którzy przechowują strukturę kodu, umowy API oraz wcześniejsze decyzje projektowe
    • Agentów obsługi klienta, którzy pamiętają informacje o właścicielu produktu oraz nastrój klienta między sesjami
    • Pomocników CRM, którzy śledzą subtelności w relacjach z klientami, aby umożliwić lepsze działania następcze
  • Pomocnicy w badaniach, którzy syntetyzują literaturę naukową, tworząc wzajemnie powiązane hipotezy
  • Zdobywanie informacji według potrzeb: stan lokalny sesji do celów wsparcia kontra głęboki kontekst strukturalny dla architektury kodu. Należy unikać jednego monolitycznego zbiornika pamięci. Boty pomocnicze często potrzebują ostatnich kilku sesji oraz metadanych konta; agenci programistyczni wymagają map repozytoriów oraz streszczeń ADR. Kierowanie tymi rodzajami zapytań do różnych baz danych – lub różnych kolekcji o różnym okresie przechowywania – zapewnia spójność każdego indeksu.

    Zasady bezpieczeństwa w produkcji: od prototypu do niezawodnego systemu

    Podłączenie bazy danych wektorowej to nie wystarczy. Powszechnym problemem jest pamięć tylko do zapisu – gromadzenie wszystkich logów narzędzi i odpowiedzi aż system zawodzi pod wpływem nadmiaru danych. Agenty produkcyjne wymagają ścisłego cyklu RRAW z filtrowaniem, syntezą oraz okresowym konsolidowaniem; w przeciwnym razie dominuje szum kontekstowy i dewiacje wektorów. Zasięg obserwacji powinien obejmować głębokość kolejki zadań konsolidacyjnych, szybkość usuwania niepotrzebnych danych oraz progi alarmowe po spadku wskaźnika skuteczności po zmianie schematu lub metod embeddingu.

    Ilościowe określanie wartości pamięci za pomocą metryk oceny

    Nadzoruj pamięć tak jak każdą inną usługę:

    • Wskaźnik skuteczności pamięci (MHR) — udział odzyskanych fragmentów faktycznie wykorzystywanych w odpowiedzi
    • Ocena jakości odpowiedzi (RQS) — porównanie odpowiedzi opartych na pamięci z tymi w stanie zerowym

    Zarządzanie kosztami, opóźnieniami i prywatnością

    Każde przeszukiwanie pamięci kosztuje czas i pieniądze. Zachowaj asynchroniczną konsolidację, ustaw twardy czas wygaśnięcia przy odczytywaniu danych, aby powolne zapytania nie blokowały głównego pętla, oraz traktuj przechowywane dane w pamięci jako informacje poufne:

    import asyncio
    import logging
    from typing import List
    
    logging.basicConfig(level=logging.INFO)
    
    async def fetch_memories_from_db(user_id: str, query: str) -> List[str]:
        # Simulate a vector database retrieval latency
        await asyncio.sleep(0.1)
        return ["User prefers Python for data engineering tasks."]
    
    async def retrieve_memory_safely(
        user_id: str,
        query: str,
        timeout_seconds: float = 0.15
    ) -> List[str]:
        """
        Retrieves memories asynchronously with a strict timeout guardrail.
        This prevents slow vector DB lookups from blocking the main agent response loop.
        """
        try:
            # Enforce a strict latency budget using asyncio.wait_for
            memories = await asyncio.wait_for(
                fetch_memories_from_db(user_id, query),
                timeout=timeout_seconds
            )
            return memories
        except asyncio.TimeoutError:
            logging.warning(f"Memory timeout for user {user_id}. Falling back to empty memory.")
            # Fallback to preserve user experience latency
            return []
    
    # Example execution of the guarded memory retrieval
    if __name__ == "__main__":
        memories = asyncio.run(retrieve_memory_safely("usr_90210", "Get programming language"))
        print(f"Retrieved Memories: {memories}")
    

    Zanonimizuj dane przy ich wprowadzaniu, wdroż RBAC, aby Agent A nie mógł czytać pamięci Agenta B, oraz dostosuj się do wymogów GDPR/CCPA. Nigdy nie umieszczaj surowego, nieredagowanego tekstu użytkownika; przeprowadź redakcję danych osobowych przed indeksowaniem.

    Lista kontrolna operacyjna dla pierwszej wersji produkcyjnej

    Niech najpierw zostanie wysłana cienka, pionowa warstwa danych przed stworzeniem doskonałej taksonomii. Zacznij od dzienników sesji epizodycznych wraz z jedną kolekcją semantyczną przechowującą trwałe preferencje użytkowników; dodaj niewielki graf przedstawiający jednostki organizacyjne tylko wtedy, gdy pytanie dotyczące produktu wymaga deterministycznych połączeń, a od samego początku wdrażaj mechanizmy MHR i RQS. Gdy pamięć działa wolno, przedkładaj czasowe wyłączenia i mniej dokładne odpowiedzi nad zablokowane rozmowy. Dokumentuj okres przechowywania danych dla każdego użytkownika, ćwicz przywracanie z kopii zapasowych i określ, kto może na żądanie usunąć pamięci użytkownika. Te nawyki są ważniejsze niż wybór najmodniejszej marki biblioteki wektorowej, ponieważ systemy pamięci przestają funkcjonować na poziomie operacyjnym znacznie wcześniej, niż ulegają awarii koncepcyjnej. Nudna architektura z jasno określonymi SLO, mechanizmami redakcji i zadań usuwania danych będzie wydajniejsza od modnej bazy danych, której nikt nie monitoruje, gdy wskaźnik udanej obsługi cichutko spada.

    Czego dąży do optymalizacji architektura

    Ta zmiana stawia na zdolność do ewolucji i zachowanie stanu zamiast na pomysłowość jednorazowych promptów. LLM to silnik działający w czasie rzeczywistym; zewnętrzna pamięć stanowi stan systemu. Ustrukturyzowany cykl zwrotny pozwala agencie doskonalić swój sposób działania z każdym cyklem – od tymczasowej aplikacji CLI w kierunku ciągłego systemu produkcyjnego.

    Atakuje również koszt, opóźnienie i trafność: zmuszanie modelu do przeszukiwania ogromnej ilości kontekstu obciąża zasoby poświęcone uwadze i tokenom. Pamięć zewnętrzna filtrowała ten obszar danych przed przetwarzaniem. Prawdziwa wartość agentów wynika mniej ze rozmiaru okna kontekstowego, a bardziej z precyzji systemów wyszukiwania, które dostarczają dane modelowi. Zespoły, które inwestują w hybrydową pamięć, mechanizmy konsolidacji, ocenę oraz kontrole prywatności, uzyskują agenty, które poprawiają się z czasem wraz z użytkowaniem, zamiast pogarszać swoje funkcjonowanie pod wpływem własnej historii. Gdy brakuje takich kontroli, historia staje się obciążeniem: głośniejsze polecenia, wolniejsze odpowiedzi oraz użytkownicy, którzy przestają ufać asystentowi, który co tydzień podaje sprzeczne informacje. Staranne wykorzystanie pamięci zewnętrznej pozwala produktom opartym na agentach gromadzić przewagę, zamiast hałasu.

    Literatura pokrewna

  • Konfiguracja, stan, magazyn, prompt: inżynieria kontekstu w LangGraph — Oddzielna, niezmienialna konfiguracja w czasie wykonywania, zmienny stan roboczy, długoterminowy magazyn oraz przygotowane prompty, dzięki czemu agenci są tańsze, bezpieczniejsze i łatwiejsze do debugowania.
  • Krótkie okna, długie pamięci: budowanie zewnętrznego mechanizmu przywoływania informacji dla agentów LLM — Ograniczenia kontekstowe, wektorowe mechanizmy długoterminowej pamięci, projekty buforów i systemów wyszukiwania w LangChain, hybrydowe magazyny oraz aspekty bezpieczeństwa, prywatności i skalowalności w produkcji.