Wskazówki praktyczne: Opanowanie Neo4j i LangChain4j: GraphRAG, trwała pamięć AI
Krok po kroku instrukcja obsługi Notatek praktycznych: Opanowanie Neo4j i LangChain4j: GraphRAG, trwała pamięć AI: kontrakty, sprawdzania oraz gotowe elementy kodu dla zespołów wdrażających ten wzorzec.
Poniższe notatki przedstawiają praktyczną ścieżkę nauki „Mastering Neo4j & LangChain4j: GraphRAG, Persistent AI Memory and more”. Nacisk kładziony jest na umowy, sprawdzenia oraz miejsca zastępcze dla kodu, a nie na motywacyjne aspekty. Podczas przechodzenia przez etap przeglądu, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolno preferować małe, testowalne jednostki zamiast rozbudowanych skryptów. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany proces.
<dependencies>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-neo4j-retriever</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j</artifactId>
<version>${langchain.version}</version>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-neo4j</artifactId>
<version>${langchain.version}</version>
</dependency>
<!-- other deps -->
</dependencies>
Abstrakcja dynamicznego schematu: Neo4jGraph
Etap Neo4jGraph oparty na abstrakcji dynamicznego schematu działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Oddziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
Pobieranie schematu za pomocą pojedynczego zapytania
Etap pobierania schematu przy użyciu pojedynczego zapytania działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań, zanim rozszerzysz zakres pracy. Zapisuj czasy wykonywania operacji oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy przechodzi się z środowiska demonstracyjnego do współdzielonych środowisk. Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
import dev.langchain4j.store.graph.neo4j.Neo4jGraph;
// If I want to initialize the graph abstraction and load the schema:
Neo4jGraph graph = Neo4jGraph.builder()
.driver(driver)
.build();
// Under the hood, this executes a single, consolidated APOC query that yields
// labels, element types, and properties all at once, avoiding multiple DB calls.
graph.refreshSchema();
Neo4jGraph.StructuredSchema schema = graph.getStructuredSchema();
// We expect a well-formatted string logically divided into three sections,
// exactly as formatted by the new Neo4jGraphSchemaUtils class:
//
// `schema.nodesProperties()` is the following:
// :Person {name: STRING}, :Company {name: STRING}
//
// `schema.relationshipsProperties()` is the following:
// :WORKS_FOR {since: INTEGER}
//
// `schema.patterns()` is the following:
// (:Person)-[:WORKS_FOR]->(:Company)
System.out.println("Current Database Schema Context:\n" + schema);
Konfiguracja próbkowania schematu i ulepszeń
Najlepiej funkcjonuje etap konfiguracji próbkowania schematu, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny zapis transakcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Oddziel zasadę dzielenia na fragmenty od zasady pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej w przypadku zmian wskaźników jakości. Najlepiej funkcjonuje etap konfiguracji próbkowania schematu, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny zapis transakcji, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną ścieżkę przetwarzania.
// If I want to scan a very large database efficiently by configuring the APOC sampling,
// and I want to enhance the LLM's understanding with sample property values:
Neo4jGraph optimizedGraph = Neo4jGraph.builder()
.driver(driver)
// We can configure the underlying apoc.meta.data parameters.
// 'sample' limits the number of nodes inspected per label to speed up execution.
// 'maxRels' limits the number of relationships inspected per node.
// (Note: These are passed internally to the getSchemaFromMetadata utility)
.build();
// When the schema is refreshed, the underlying query runs:
// CALL apoc.meta.data({maxRels: $maxRels, sample: $sample})
optimizedGraph.refreshSchema();
// The LLM now receives a fast, accurately sampled schema representation,
// protecting database performance during application startup or schema refreshes.
System.out.println("Optimized Schema loaded successfully.");
Automaticzne tworzenie grafu wiedzy i łączenie źródeł
Na etapie automatycznego tworzenia grafu wiedzy należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć przypadkowe, częściowe ukończenie zadań. Wymieniaj fragmenty tekstu, które faktycznie stanowiły podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu.
Początkowy zbiór danych i promptowanie typu few-shot
Dla początkowego zestawu danych i etapu należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy przechodzi się z środowiska demonstracyjnego do współdzielonych środowisk. W przypadku, gdy następnym krokiem jest kod lub wywołanie narzędzia, lepiej używać ustrukturyzowanych wyników z walidacją schematu niż tekstu w formie swobodnej.
[
{
"tail": "Microsoft",
"head": "Adam",
"head_type": "Person",
"text": "Adam is a software engineer in Microsoft since 2009...",
"relation": "WORKS_FOR",
"tail_type": "Company"
},
{
"tail": "Microsoft Word",
"head": "Microsoft",
"head_type": "Company",
"text": "Microsoft is a tech company that provides several products...",
"relation": "PRODUCED_BY",
"tail_type": "Product"
}
]
import dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformer;
import dev.langchain4j.community.data.document.graph.GraphDocument;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.DefaultDocument;
import dev.langchain4j.data.document.Metadata;
import java.util.List;
ChatModel chatModel = /* dev.langchain4j.model.chat instance */
Driver driver = /* org.neo4j.driver.Driver instance */
// If I want to guide the extraction by providing a structured set of examples:
LLMGraphTransformer transformer = LLMGraphTransformer.builder()
.model(chatModel)
.examples(EXAMPLES_PROMPT) // Injects the above JSON dataset into the system prompt
.build();
Document docKeanu = new DefaultDocument(
"Keanu Reeves acted in Matrix",
Metadata.from("key33", "value3")
);
// The LLM will transform the text, structuring nodes and relationships based on the examples
List<GraphDocument> graphDocs = transformer.transformAll(List.of(docKeanu));
/*
The above `graphDocs` returns this result:
GraphDocument
├─ Nodes
│ ├─ GraphNode
│ │ ├─ id: Matrix
│ │ ├─ type: Movie
│ │ └─ properties: {}
│ │
│ └─ GraphNode
│ ├─ id: Keanu Reeves
│ ├─ type: Person
│ └─ properties: {}
│
├─ Relationships
│ └─ GraphEdge
│ ├─ type: ACTED_IN
│ ├─ sourceNode
│ │ ├─ id: Keanu Reeves
│ │ └─ type: Person
│ ├─ targetNode
│ │ ├─ id: Matrix
│ │ └─ type: Movie
│ └─ properties: {}
│
└─ Source
├─ text: "Keanu Reeves acted in Matrix"
└─ metadata
└─ key33: value3
*/
Idempotentna trwałość grafu
W fazie trwałości grafu idempotentnego należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całego grafu. Należy podawać konkretne fragmenty tekstu, które stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu. W fazie trwałości grafu idempotentnego należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, testowalne jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powinien wskazywać na konkretną odpowiedzialność, a nie na całą strukturę.
rurka pod kątem.import dev.langchain4j.community.rag.content.retriever.neo4j.KnowledgeGraphWriter;
Neo4jGraph neo4jGraph = /* dev.langchain4j.store.graph.neo4j.Neo4jGraph instance */;
LLMGraphTransformer graphTransformer = /* dev.langchain4j.community.data.document.transformer.graph.LLMGraphTransformer instance */;
Document docKeanu = new DefaultDocument(
"Keanu Reeves acted in Matrix",
Metadata.from("key33", "value3")
);
List<GraphDocument> graphDocs = graphTransformer.transformAll(List.of(docKeanu));
// If I want to persist the extracted entities safely:
KnowledgeGraphWriter writer = KnowledgeGraphWriter.builder()
.graph(neo4jGraph)
.build();
// The first write populates the database
writer.addGraphDocuments(graphDocs, false);
// Executing the exact same command again is safe.
// The internal UNWIND and MERGE logic generated by the writer guarantees
// that no duplicate entities or relationships are created.
writer.addGraphDocuments(graphDocs, false);
// Expected resulting topology in the database:
// (:__Entity__ {id: 'keanu'})-[:ACTED]->(:__Entity__ {id: 'matrix'})
System.out.println("Entities persisted successfully. No duplicates created.");
Łączenie z dokumentem źródłowym (includeSource)
Podczas pracy nad etapem łączenia z dokumentem źródłowym za pomocą funkcji includeSource najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i unikaj cichego ukończenia zadania w sposób niepełny. Zmierz stopień przywoływania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabą skuteczność wyszukiwania.
// If I want to maintain data provenance and link entities back to their source:
writer.addGraphDocuments(graphDocs, true); // true = includeSource
// Behind the scenes, the writer executes three crucial operations:
// 1. It creates the Document node, copying the original metadata (e.g., key33: value3) and the text.
// 2. If the original document lacks an ID, the writer automatically generates
// an MD5 hash of the text to use as a unique identifier.
// 3. It links the document to the extracted entities using a relationship (default: HAS_ENTITY).
// Expected resulting topology in the database:
// (:Document {id: '<MD5_hash>', text: 'Keanu Reeves...', key33: 'value3'})-[:HAS_ENTITY]->(:__Entity__ {id: 'keanu'})
System.out.println("Source document successfully linked to the extracted entities.");
Dostosowywanie głębokiego schematu i bezpieczeństwo
Gdy pracujesz nad etapem dostosowywania głębokiego schematu, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zapisz czas trwania oraz koszt tokena lub zapytania obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Zmierz stopień odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabe możliwości wyszukiwania.
// If I want to adapt the ingestion to a pre-existing enterprise schema,
// and customize the relationship that links the document to the entities:
KnowledgeGraphWriter customWriter = KnowledgeGraphWriter.builder()
.graph(neo4jGraph)
.label("ActorOrMovie") // Replaces "__Entity__"
.idProperty("customId") // Replaces "id"
.textProperty("customText") // Replaces "text" for the Document node
.relType("MENTIONED_IN_SOURCE") // Replaces "HAS_ENTITY"
.constraintName("unique_custom") // Sets a specific name for the Neo4j CONSTRAINT
.build();
// Inserting the data with includeSource set to true will now use the new nomenclature:
customWriter.addGraphDocuments(graphDocs, true);
// Example of the resulting Cypher pattern generated by the writer:
// (:Document {customText: '...'})-[:MENTIONED_IN_SOURCE]->(:ActorOrMovie {customId: 'keanu'})
Bезpieczeństwo poprzez integrację Cypher DSL
Gdy przechodzisz przez etap Security via Cypher DSL, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Zmierz stopę odzyskiwania informacji na ustalonej grupie pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe mechanizmy wyszukiwania. Gdy przechodzisz przez etap Security via Cypher DSL, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną sekwencję operacji.
import org.neo4j.cypherdsl.core.Cypher;
import org.neo4j.cypherdsl.core.Statement;
import org.neo4j.cypherdsl.core.Node;
Driver driver = /* org.neo4j.driver.Driver instance */
// Demonstrating how internal queries are constructed safely via the DSL
// We define a Node representation first
Node documentNode = Cypher.node("Document").named("d");
// We build the query programmatically using the fluent API.
// Notice how literal values are wrapped safely, preventing injection.
Statement statement = Cypher.match(documentNode)
.where(Cypher.property("d", "id").isEqualTo(Cypher.literalOf("doc-123")))
.returning(Cypher.property("d", "text"))
.build();
// The DSL engine traverses the AST and compiles it into a syntactically safe string.
String safeCypherQuery = statement.getCypher();
// Expected output: MATCH (d:`Document`) WHERE d.id = 'doc-123' RETURN d.text
System.out.println("Generated safe Cypher via DSL: " + safeCypherQuery);
Wstępne filtrowanie w indeksie przy użyciu składni 2026.01
Faza wstępnego filtrowania w indeksie z wykorzystaniem składni 2026 działa najlepiej, gdy traktuje się ją jako mierzalną powierzchnię do analizy. Zapisz jeden idealny przykład transkrypcji, jeden przypadek niepowodzenia oraz notatkę dotyczącą cofnięcia działań przed rozszerzeniem zakresu pracy. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Oddziel zasady dzielenia na fragmenty od zasad wyszukiwania. Zmiana jednych nie powinna zmuszać do przepisywania drugich, gdy zmieniają się metryki jakości.
import dev.langchain4j.store.embedding.neo4j.Neo4jEmbeddingStore;
import dev.langchain4j.store.embedding.filter.Filter;
import static dev.langchain4j.store.embedding.filter.MetadataFilterBuilder.metadataKey;
Driver driver = /* org.neo4j.driver.Driver instance */
Embedding embedding = /* dev.langchain4j.data.embedding.Embedding instance */
MatchSearchClauseStrategy matchSearchClauseStrategy = new MatchSearchClauseStrategy();
// Configure the store with the new syntax enabled
Neo4jEmbeddingStore store = Neo4jEmbeddingStore.builder()
.driver(driver)
.dimension(1536)
.searchStrategy(matchSearchClauseStrategy) // Crucial flag: Enables the 2026.01 optimized native vector search syntax
.filterMetadata(Arrays.asList("year", "department")) // Enable filtering for 'year' and 'department', which translates to the `WITH [indexName.year, indexName.department]` clause during index creation
.build();
// Build a metadata filter combining multiple boolean conditions
Filter filter = metadataKey("year").isEqualTo(2024)
.and(metadataKey("department").isEqualTo("Engineering"));
// Execute the search request
EmbeddingSearchRequest request = EmbeddingSearchRequest.builder()
.queryEmbedding(embedding)
.maxResults(5)
.filter(filter) // Filter is executed natively inside the Neo4j Vector Index block
.build();
SearchResult<TextSegment> results = store.search(request);
// The results will natively exclude any documents not matching the criteria specified in the `filter` instance
// returning the final Top-K, ensuring you always get 5 highly relevant segments if they exist.
System.out.println("Search executed with in-index filtering.");
// Execute results.matches() to verify in-index filtering
Koncepcje wyszukiwania w GraphRAG: zaawansowani mechanizmy strukturalne do wyszukiwania i przetwarzania danych
Etap zaawansowany GraphRAG Retrieval Concepts działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Zapisuj czasy wykonywania operacji oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy przechodzi się z środowiska demonstracyjnego do współdzielonych środowisk. Rozdziel politykę dzielenia na fragmenty od polityki wyszukiwania. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
Wzorzec rodzic-dziecko
Etap wzorca Rodzic-Dziecko funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Oddziel zasadę dzielenia na fragmenty od zasady pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej w przypadku zmian wskaźników jakości. Etap wzorca Rodzic-Dziecko funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia. Zapisz jeden idealny przepis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną ścieżkę przetwarzania.
import dev.langchain4j.store.graph.neo4j.Neo4jParentChildIngestor;
import dev.langchain4j.data.document.Document;
import dev.langchain4j.data.document.splitter.DocumentSplitters;
// embeddingModel, embeddingStore, childSplitter instances...
// If I want to automatically ingest a document into a Parent-Child graph topology:
Neo4jEmbeddingStoreIngestor ingestor = ParentChildGraphIngestor.builder()
.driver(driver)
.embeddingModel(embeddingModel)
// We define how the document should be chunked before ingestion
.documentSplitter(DocumentSplitters.recursive(200, 20))
.documentChildSplitter(childSplitter)
.build();
Document document = Document.from( """Artificial Intelligence (AI) is a field of computer science. It focuses on creating intelligent agents capable of performing tasks that require human intelligence.
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions. Deep Learning is a specialized form of ML based on neural networks.
""");
ingestor.ingest(List.of(document));
// The graph now contains one 'Document' node connected via 'HAS_CHILD'
// to multiple embedded 'DocumentChunk' nodes.
System.out.println("Parent and child nodes successfully ingested and linked.");
import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever;
// If I want to search against chunks but retrieve the rich parent document:
final EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingStore(embeddingStore)
.maxResults(1)
.minScore(0.4)
.build();
List<Content> contents = retriever.retrieve(Query.from("specific configuration detail"));
// The retriever hits the small 'DocumentChunk' index, traverses the 'HAS_CHILD'
// relationship, and returns the entire 'Document' node.
System.out.println("Retrieved full parent document context.");
/*
The result of `contents` is :
DefaultContent {
textSegment = TextSegment {
text = """
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions.
Deep Learning is a specialized form of ML based on neural networks.
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions.
Deep Learning is a specialized form of ML based on neural networks.
Artificial Intelligence (AI) is a field of computer science. It focuses on creating intelligent agents
capable of performing tasks that require human intelligence.
""",
metadata = {
index = 1,
source = Wikipedia link,
title = AI Basics,
url = https://example.com/ai,
parentId = parent_1af3e080-5029-40ab-b3d7-0829064d800d
}
},
metadata = {
EMBEDDING_ID = null,
SCORE = 0.8560410737991333
}
}
*/
Wzorzec podsumowania
W fazie The Summary Pattern należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzuć przypadkowe, częściowe ukończenie zadania. Wskazuj fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu.
import dev.langchain4j.community.store.embedding.neo4j.Neo4jEmbeddingStoreIngestor;
import dev.langchain4j.community.store.embedding.neo4j.SummaryGraphIngestor;
// If I want the LLM to summarize my document during ingestion and store the summary:
/* Neo4jEmbeddingStore, ChatModel and DocumentSplitter instances... */
final Neo4jEmbeddingStoreIngestor ingestor = SummaryGraphIngestor.builder()
.driver(driver)
.embeddingModel(embeddingModel)
.questionModel(chatModel)
.documentSplitter(parentSplitter)
.build();
ingestor.ingest(List.of(document));
// The graph now has a 'Summary' node (containing the LLM-generated summary)
// linked to the specific 'DocumentChunk' nodes.
System.out.println("Document ingested and summarized successfully.");
import dev.langchain4j.community.store.embedding.neo4j.Neo4jEmbeddingStoreIngestor;
import dev.langchain4j.community.store.embedding.neo4j.SummaryGraphIngestor;
/* required instances */
Document document = Document.from("""
Artificial Intelligence (AI) is a field of computer science. It focuses on creating intelligent agents capable of performing tasks that require human intelligence.
Machine Learning (ML) is a subset of AI. It uses data to learn patterns and make predictions. Deep Learning is a specialized form of ML based on neural networks.
""");
ingestor.ingest(document);
final EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.6)
.embeddingStore(ingestor.getEmbeddingStore())
.build();
/*
The result is something like this:
DefaultContent {
textSegment = TextSegment {
text = "Machine Learning (ML) is a subset of AI",
metadata = {
index = 0,
source = Wikipedia link,
title = Quantum Mechanics,
url = https://example.com/ai
}
},
metadata = {
EMBEDDING_ID = null,
SCORE = 0.8425111770629883
}
}
*/
The Hypothetical Question Pattern
W fazie „Hipotetyczny wzorzec pytań” należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy rejestrować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy ścieżka przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Należy podawać fragmenty tekstu, które faktycznie stanowiły podstawę odpowiedzi. Bez tych odniesień operatorzy nie mogą odróżnić halucynacji od luki w indeksowaniu.
import dev.langchain4j.community.store.embedding.neo4j.Neo4jEmbeddingStoreIngestor;
import dev.langchain4j.community.store.embedding.neo4j.HypotheticalQuestionGraphIngestor;
/* ... Neo4jEmbeddingStore, ChatModel and DocumentSplitter instances.. */
Neo4jEmbeddingStoreIngestor ingestor = HypotheticalQuestionGraphIngestor.builder()
.embeddingModel(embeddingModel)
.driver(driver)
.documentSplitter(splitter)
.questionModel(chatModel)
.embeddingStore(embeddingStore)
.build();
Document document = Document.from("""
Quantum mechanics studies how particles behave. It is a fundamental theory in physics.
Gradient descent and backpropagation algorithms.
Spaghetti carbonara and Italian dishes.
John Doe is a Super Saiyan.
""");
ingestor.ingest(document);
EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.maxResults(2)
.minScore(0.5)
.embeddingStore(ingestor.getEmbeddingStore())
.build();
List<Content> results = retriever.retrieve(Query.from("Who is John Doe?"));
System.out.println("Retrieved Hypothetical context: " + results);
/*
The result is something like this:
DefaultContent {
textSegment = TextSegment {
text = "John Doe is a Super Saiyan.",
metadata = {
index = 2,
source = Wikipedia link,
title = Quantum Mechanics,
url = https://example.com/ai
}
},
metadata = {
SCORE = 0.8234479427337646,
EMBEDDING_ID = 2002cabe-2a3e-4c6e-96ec-a0292e26e817
}
}
*/
Uogólniony wzorzec
W fazie The Generic Pattern należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Należy podawać konkretne fragmenty tekstu, na których opiera się odpowiedź. Bez tych odniesień operatorzy nie są w stanie odróżnić halucynacji od braku danych w indeksie. W fazie The Generic Pattern należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, testowalne jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną strukturę przepływu.
final Neo4jEmbeddingStore neo4jEmbeddingStore = /* Neo4jEmbeddingStore instance */
final EmbeddingStoreContentRetriever retriever = EmbeddingStoreContentRetriever.builder()
.embeddingModel(embeddingModel)
.maxResults(5)
.minScore(0.4)
.embeddingStore(neo4jEmbeddingStore)
.build();
// other required instances ...
Document doc = Document.from("""
Quantum mechanics studies how particles behave. It is a fundamental theory in physics.
Gradient descent and backpropagation algorithms.
Spaghetti carbonara and Italian dishes.
John Doe is a Super Saiyan.
""");
// Ingest the document into Neo4j as parent-child nodes
final Neo4jEmbeddingStoreIngestor ingestor = Neo4jEmbeddingStoreIngestor.builder()
.documentSplitter(parentSplitter)
.documentChildSplitter(childSplitter)
.driver(driver)
.query("CREATE (:MainDoc $metadata)") // a Cypher query template used for storing the processed segment data in Neo4j
.embeddingStore(neo4jEmbeddingStore)
.embeddingModel(embeddingModel)
.build();
ingestor.ingest(doc);
final String retrieveQuery = "Machine Learning";
List<Content> results = retriever.retrieve(Query.from(retrieveQuery));
System.out.println("Retrieved Generic context: " + results);
Narzędzie do wyszukiwania relacji rodzic-dziecko niezależne od bazy danych
Podczas prace nad etapem niezależnym od bazy danych, najpierw spisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i unikaj cichego ukończenia zadania w sposób niepełny. Zmierz stopień przywoływalności na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabą skuteczność wyszukiwania.
import dev.langchain4j.community.store.embedding.ParentChildEmbeddingStoreIngestor;
/* Required instances */
ParentChildEmbeddingStoreIngestor ingestor = ParentChildEmbeddingStoreIngestor.builder()
.documentTransformer(documentTransformer)
.documentSplitter(documentSplitter)
.textSegmentTransformer(textSegmentTransformer)
.embeddingModel(embeddingModel)
.embeddingStore(embeddingStore)
.documentChildSplitter(documentChildSplitter)
.childTextSegmentTransformer(childTextSegmentTransformer)
.build();
Sztuczna inteligencja z pamięcią stanową: trwała pamięć rozmów w grafie
Gdy pracujesz nad etapem Stateful AI Persistent Conversation, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Zapisz czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega niespodziewanym rachunkom, gdy przechodzi się od wersji demonstracyjnej do środowisk współdzielonych. Zmierz stopień odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko poprawiają słabą efektywność wyszukiwania.
Konfiguracje
Gdy przechodzisz przez etap konfiguracji, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Zmierz stopień odzyskiwania informacji na ustalonej serii pytań przed dostosowywaniem promptów. Częste zmiany promptów rzadko naprawiają słabe mechanizmy wyszukiwania. Gdy przechodzisz przez etap konfiguracji, najpierw zapisz umowę: wymagane dane wejściowe, sygnał sukcesu oraz to, co dzieje się w przypadku częściowego niepowodzenia. Taka lista kontrolna zapewnia uczciwość późniejszych zmian w kodzie. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok się nie powiedzie, błąd powinien wskazywać na jedną konkretne odpowiedzialność, a nie na skomplikowaną sekwencję operacji.
Zarządzanie historią czatów wielu użytkowników i danymi wejściowymi multimodalnymi
Etap zarządzania historią czatów wielu użytkowników funkcjonuje najlepiej, gdy traktowany jest jako mierzalna powierzchnia do analizy. Zapisz jeden idealny zapis rozmowy, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia działań przed rozszerzaniem zakresu. Traktuj ten etap jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nadaj nazwy poszczególnym elementom, zdefiniuj kryteria sukcesu i odrzucaj ciche, częściowe ukończenie zadań. Oddziel politykę dzielenia danych na fragmenty od polityki ich pobierania. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
import dev.langchain4j.store.memory.chat.neo4j.Neo4jChatMemoryStore;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.ImageContent;
import java.util.List;
// 1. Initialize the store using an existing driver
Neo4jChatMemoryStore memoryStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.build();
// 2. Identify the specific user sessions
String sessionId1 = "user-alice-123";
String sessionId2 = "user-bob-456";
// 3. Append standard text messages for Alice
List<ChatMessage> aliceMessages = List.of(
new UserMessage("Hi, I'm Alice."),
new AiMessage("Hello Alice!")
);
memoryStore.updateMessages(sessionId1, aliceMessages);
// 4. Append multimodal messages (text + images) for Bob
List<ChatMessage> bobMessages = List.of(
new UserMessage("What do you see in this image?", List.of(new ImageContent("https://...")))
);
memoryStore.updateMessages(sessionId2, bobMessages);
// When we retrieve or delete messages using sessionId1,
// the graph guarantees that Bob's linked list of messages remains completely untouched.
System.out.println("Isolated memory chains created for both Alice and Bob.");
// 5. Optionally delete messages
// memoryStore.deleteMessages(sessionId1);
// memoryStore.deleteMessages(sessionId2);
Dostosowywanie schematu grafu
Etap dostosowywania schematu grafu działa najlepiej, gdy traktuje się go jako mierzalną powierzchnię. Zapisz jeden idealny przykład działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Zapisuj czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy przechodzi się z środowiska demonstracyjnego do współdzielonych środowisk. Rozdziel politykę dzielenia na fragmenty od polityki pobierania danych. Zmiana jednej z nich nie powinna zmuszać do przepisywania drugiej, gdy zmieniają się metryki jakości.
// If I want to align the memory storage with my specific domain ontology:
Neo4jChatMemoryStore customMemoryStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.memoryLabel("UserSession") // Overrides the default "Memory" label
.messageLabel("ChatTurn") // Overrides the default "Message" label
.lastMessageRelType("LATEST_CHAT") // Overrides the default "LAST_MESSAGE" rel
.nextMessageRelType("FOLLOWED_BY") // Overrides the default "NEXT" rel
.idProperty("sessionKey") // Overrides the default "id" property
.messageProperty("textContent") // Overrides the default "message" property
.build();
// Now, when the system persists a chat, it will execute domain-specific Cypher queries like:
// MERGE (m:UserSession {sessionKey: 'user-alice-123'})
// CREATE (msg:ChatTurn {textContent: 'Hi...'})
// MERGE (m)-[:LATEST_CHAT]->(msg)
System.out.println("Custom memory store initialized with domain-specific schema.");
Zarządzanie limitem tokenów (rozmiar okna kontekstowego)
Faza zarządzania ograniczeniami tokenów działa najlepiej, gdy jest traktowana jako mierzalna powierzchnia do analizy. Zapisz jeden idealny zapis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Trzymaj konfigurację poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Ustal budżet tokenów na jeden ruch i jedną sesję. Narzędzia typu agentic intensywnie rozszerzają kontekst; sztywne limity zapobiegają temu, by demonstracje przerodziły się w niespodziewane rachunki. Faza zarządzania ograniczeniami tokenów działa najlepiej, gdy jest traktowana jako mierzalna powierzchnia do analizy. Zapisz jeden idealny zapis działania, jeden przypadek awarii oraz notatkę dotyczącą cofnięcia zmian, zanim rozszerzysz zakres pracy. Wolij małe, testowalne jednostki nad rozbudowane skrypty. Gdy jakiś krok zawiedzie, awaria powinna wskazywać na konkretną odpowiedzialność, a nie na skomplikowaną sekwencję działań.
// If I want to heavily restrict the context window to only the most recent interactions:
Neo4jChatMemoryStore slidingWindowStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.size(3) // The default is 10. We limit it to the 3 most recent messages.
.build();
// Assuming the user has sent 20 messages in this session over the past month.
List<ChatMessage> recentHistory = slidingWindowStore.getMessages("user-alice-123");
// The system efficiently traverses the graph starting from the LATEST_CHAT relationship
// and walks backwards via the FOLLOWED_BY relationships, stopping after it collects the
// limited batch of recent messages. The older historical messages remain safely in the
// database, but are not loaded into memory, saving precious tokens.
System.out.println("Loaded only the " + recentHistory.size() + " most recent messages.");
// If I want to extract the complete history for analytics or summarization:
Neo4jChatMemoryStore completeHistoryStore = Neo4jChatMemoryStore.builder()
.driver(driver)
.size(0) // 0 disables the sliding window limit
.build();
List<ChatMessage> fullHistory = completeHistoryStore.getMessages("user-alice-123");
System.out.println("Extracted the complete session history containing " + fullHistory.size() + " messages.");
Społeczna obsługa połączeń
W fazie spółecznej obsługi połączeń należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed modyfikacją kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanej punktacji kontrolnej, bez konieczności zgadywania ukrytego stanu. Traktuj tę fazę jako umowę pomiędzy danymi wejściowymi a zweryfikowanymi wynikami. Nazwij poszczególne elementy, zdefiniuj kryteria sukcesu i odrzuć przypadkowe, częściowe ukończenie zadań. Podaj źródła, na których faktycznie opiera się odpowiedź. Bez tych odniesień operatorzy nie będą w stanie odróżnić halucynacji od braków w indeksowaniu.
// If I want to instantiate the store directly without managing an external Driver instance:
Neo4jChatMemoryStore standaloneStore = Neo4jChatMemoryStore.builder()
.withBasicAuth("bolt://localhost:7687", "neo4j", "password")
.build();
System.out.println("Memory store connected directly via Basic Auth.");
Wniosek
W fazie podsumowania należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok na podstawie znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy odnotować czasy wykonywania oraz koszt tokenów lub zapytań obok wyników funkcjonalnych. Wczesna widoczność kosztów zapobiega nieoczekiwanym rachunkom, gdy proces przechodzi z środowiska demonstracyjnego do współdzielonych środowisk. Należy podawać fragmenty tekstu, które faktycznie stanowiły podstawę odpowiedzi. Bez tych odniesień operatorzy nie są w stanie odróżnić halucynacji od luki w indeksowaniu.
Zasoby
W fazie Zasobów należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Konfigurację należy przechowywać poza kodem aplikacji. Pliki środowiskowe, magazyny tajnych danych oraz flagi funkcjonalne powinny znajdować się w jednym miejscu, które operatorzy mogą sprawdzić bez konieczności czytania całej struktury. Należy podawać konkretne fragmenty tekstu, na których opiera się odpowiedź. Bez tych odniesień operatorzy nie są w stanie odróżnić halucynacji od braku danych w indeksie. W fazie Zasobów należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie uruchomić ten krok od znanego punktu kontrolnego, bez konieczności zgadywania ukrytego stanu. Należy preferować małe, łatwe do przetestowania jednostki nad rozbudowanymi skryptami. Gdy dany krok zawiedzie, powinien wskazywać na konkretną przyczynę, a nie na skomplikowaną strukturę procesów.
List kontrolny operacyjny
Na etapie listy kontrolnej operacyjnej należy zdefiniować dane wejściowe, osobę odpowiedzialną za dany krok oraz kryteria zakończenia przed zmianą kodu. Operatorzy powinni móc ponownie wykonać dany krok na podstawie znanej punktacji kontrolnej, bez konieczności zgadywania ukrytego stanu.
Zdokumentuj razem ścieżkę prawidłowego działania oraz ścieżkę naprawczą. Próby ponownych działań, kontrolne punkty ludzkie oraz obsługa wiadomości błędnych stanowią część produktu, a nie elementy dodawane później.
Cytuj fragmenty, które faktycznie stanowią podstawę odpowiedzi. Bez cytatów operatorzy nie będą w stanie odróżnić halucynacji od luki w indeksowaniu.
Napisz krótki podręcznik obsługi: jak rotować klucze, jak opróżnić kolej z zadań, jak cofnąć ostatnie zaimportowane dane.
Niech lepiej będą małe, testowalne jednostki niż rozbudowane skrypty. Gdy jakiś krok zawiedzie, błąd powinien wskazywać na konkretną odpowiedzialność, a nie na skomplikowany łańcuch operacji.
Należy podać fragmenty tekstu, które faktycznie stanowią podstawę odpowiedzi. Bez cytatów operatorzy nie mogą odróżnić halucynacji od luki w indeksowaniu.
Zanim uruchomi się cały system, należy zamrozić wersje, utworzyć „złoty” zapis transkrypcji dla kluczowych ścieżek oraz potwierdzić kroki odwracania zmian. Środowiska współdzielone wymagają ograniczeń szybkości, weryfikacji dostępności oraz wyraźnego właściciela odpowiedzialnego za rotację haseł. Lepiej wybrać nudną niezawodność niż sprytnie przygotowane jednorazowe demonstracje.
Uwaga dotycząca wersji 9f23f8fe623e: unikaj przechowywania kluczy dostawcy w repozytorium, ustaw ograniczenie liczby tokenów na sesję oraz przechowuj transkrypcje obok plików testowych, aby późniejsze zmiany modeli pozostały porównywalne.
Literatura pokrewna
- Praktyczne notatki: Praktyczna architektura GraphRAG z użyciem LangExtract i Neo4j — Szczegółowy przewodnik po Praktycznych notatkach: Praktyczna architektura GraphRAG z użyciem LangExtract i Neo4j: kontrakty, sprawdzenia oraz gotowe fragmenty kodu dla zespołów wdrażających ten wzorzec.
- Praktyczne notatki: Lokalne LLM-y do wyodrębniania danych z Graph RAG: Re-Benchmark na połowę 2026 roku — Szczegółowy przewodnik po Praktycznych notatkach: Lokalne LLM-y do wyodrębniania danych z Graph RAG: Re-Benchmark na połowę 2026 roku: kontrakty, sprawdzenia oraz gotowe fragmenty kodu dla zespołów wdrażających ten wzorzec.