首页 / 文章

Neo4j与LangChain4j:GraphRAG数据导入工具、过滤功能及聊天记忆功能

基于LangChain4j的Neo4j在GraphRAG中的应用:模式发现、LLM图结构构建、Cypher DSL语法、索引内过滤、父子节点检索以及以图为支撑的聊天记忆功能。

发布者:REACTAPP.TOP阅读约 18 分钟4156 词

来源: Mastering Neo4j & LangChain4j: GraphRAG, Persistent AI Memory and more

超越基础向量存储

仅在 Java 应用中运行的普通 Neo4j 向量存储仅是入门级方案。LangChain4j 社区仓库的贡献者们随后添加了诸多功能,远超简单的相似度搜索功能:自动图结构构建、优化过的向量过滤机制、结构化的 GraphRAG 模式,以及以图形式存储的聊天记忆。以下各部分将通过社区自身测试所使用的代码来介绍这些功能。

此处所有内容均基于 1.0.0-beta19 版本,后续版本可能会改变某些行为或接口。

依赖项与基础设置相同,Maven 文件中包含 Neo4j 获取模块及其相关组件:

<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>

文本转查询语句的模式发现

当模型能够直接从自然语言生成数据库查询语句(文本转查询语句)时,它必须了解数据库的确切结构:哪些节点相互连接,以及每个节点包含哪些属性。若缺乏这些信息,它就无法生成有效的查询语句,只能随意编造内容。

Neo4jGraph 类隐藏了这一拓扑结构问题。由于该组件会动态获取并格式化模式,因此无需在 Java 应用程序中硬编码模式。PR #119 大幅提升了处理速度。此前,收集节点、属性和关系可能需要多次与数据库的往返操作。该 PR 将模式提取功能整合为基于 Neo4j 内部 apoc.meta.data 函数的优化后的 Cypher 查询。此外,它还增加了可配置的采样大小,以及包含示例值的增强版模式,从而为模型提供更好的上下文信息。

以这个数据集为例:

将其模式暴露给模型的方式如下:

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);

在拥有数十亿节点的庞大企业图中,如果数据库试图扫描所有数据,提取元数据将会耗费巨大成本。sample和maxRels这两个设置与apoc.meta.data直接相关,可用于控制这一情况。而增强模式选项则会提供部分属性值样本,让模型能够了解数据的实际结构。

// 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.");

由于元数据可通过一次查询获取且采样信息也会被呈现,文本转密码流程能够自动适应数据库的变化,即便在规模庞大的生产数据集中也能保持快速提取速度。

从文本中构建图结构

许多数据库已经包含大量的文本字段,将这些文本转化为知识图谱是实现诸如以查询为导向的摘要生成和主题聚类等高级GraphRAG方法的基础。过去,这需要依赖脆弱且基于规则的命名实体识别流程。LLMGraphTransformer则将这项任务交由大语言模型来完成。

该模型与特定数据库无关,因为它会将文本转换为也可用于其他用途的节点和边。为防止模型产生幻觉或破坏本体结构,例如随意创建意外标签,开发者会通过具体示例(少样本提示)以及明确定义的架构来引导信息提取过程。社区还提供了一种原生方式,可将提取出的实体链接回其来源文档,从而保留原始元数据。

JSON输出必须遵循固定结构,包含head、head_type、relation、tail和tail_type这些键,而示例提示则用于教会模型这种格式。以下是提供给大型语言模型的示例JSON片段:

[
   {
      "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"
   }
]

结合这些示例,变换器能够处理包含文本和元数据的真实输入文档,例如“基努·里维斯在《黑客帝国》中出演”,该文本带有元数据key33: value3。

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
*/

一旦GraphDocument对象存在于内存中,就需要将其保存下来。在实际应用中,同一份文档可能会被多次处理,因此写入操作必须避免出现重复内容,比如不能有两个独立的“基努·里维斯”节点。KnowledgeGraphWriter负责处理这一问题。

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.");

数据库最终会呈现为这样:

保留来源信息

对于高级的 GraphRAG 模型而言,准确知道是哪段文本产生了特定的关系非常重要。设置 includeSource 标志不仅可以保存实体,还会创建一个包含原始文本及元数据的 Document 节点,并将其与提取出的实体关联起来。

// 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.");

企业环境不能依赖 __Entity__ 或 HAS_ENTITY 这类通用标签,而来自外部输入的属性名称则可能带来 Cypher 注入风险。编写者会通过 SchemaNames.sanitize 对所有输入进行净化处理,开发者也可以重新编写整个本体,包括在数据库中创建的唯一性约束的名称。

// 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'})

该配置会生成一种自定义的拓扑结构,此处展示的是各实体与其来源之间的关联:

通过这种方式配置后,基于大语言模型的图数据提取过程可实现自动化,借助来源链接便于追踪,且具备幂等性,可在结构化数据管道中安全运行。

通过 Cypher DSL 实现更安全的查询

社区仓库中的一个重要架构变化就是对Cypher DSL 库的深度整合。早期的 GraphRAG 系统通常通过拼接原始字符串来构建查询,但由于存在 Cypher 注入风险和语法错误,这种做法已不被推荐。LangChain4j 的内部组件现在会以抽象语法树(AST)的形式程序化地构建查询。虽然这些操作大多在后台进行,但这对提升应用程序安全性而言意义重大。

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);

DSL引擎理解Cypher的语法,因此其API不会生成语法错误的查询。这为该集成提供了可靠的安全保障。

索引内预过滤

Neo4j的向量搜索功能已得到显著优化,LangChain4j集成现在支持更新的向量搜索语法,包括2026.01版本的优化内容。其主要优势在于索引内过滤。

可采用相似度搜索结合元数据约束的方式,例如筛选出语义上与“机器学习”相近但仅发表于2024年的文档。传统的向量数据库方法会先获取最相似的向量,再剔除年份不正确的向量。这种后过滤方式会浪费资源,且通常返回的文档数量少于需求,从而影响检索精度。索引内过滤则是在遍历HNSW(分层可导航小世界)向量索引的过程中就应用元数据约束。Neo4j的文档中提供了关于该语法及其性能优势的更多详细信息。

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

GraphRAG检索模式

向量搜索在处理语义相似性方面表现良好,但在处理分析类问题时则力不从心,比如聚合操作或多跳遍历,例如“参与X项目的工程师的同事是谁?”向量无法进行计数、求和,也无法可靠地遍历结构化层次结构。GraphRAG概念正是为了解决这类分析需求,graphrag.com上有相关介绍。

在RAG中,分块处理会带来一个常见难题:约200个标记的小块虽然能获得精确的向量匹配结果,但缺乏上下文信息,从而导致生成效果不佳;而约2000个标记的大块虽然能为大语言模型提供充足上下文,却会大幅降低搜索精度。一组结构化数据摄取器和检索器通过利用图的双分特性解决了这一问题,该特性将可搜索单元与可检索单元分开。

父子关系

文档会被分割成仅用于向量搜索的小型“子”片段,每个片段都通过某种关联与完整的“父”文档相连。嵌入向量是在这些语义上连贯的小单元上计算得出的,从而提升检索质量。若要分割大型文档,可先对这些片段进行嵌入处理,再使用ParentChildGraphIngestor将它们关联到Neo4j中的父节点上。

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.");

在摄入数据后,检索模块会查找这些小片段,并通过EmbeddingStoreContentRetriever将完整的父数据传递给大语言模型:

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
  }
}
*/

以摘要作为返回单元

直接返回整个父节点会占用过多资源。一种更高效的方法是在处理过程中生成父节点的摘要,当有子节点片段匹配时再返回该摘要。SummaryGraphIngestor会让大语言模型撰写摘要,将其作为独立节点存储,并将其与可搜索的片段关联起来:

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.");

Neo4jSummaryRetriever负责执行检索,这样在某个数据块上找到向量匹配项时,会返回预先计算好的摘要而非原始文本:

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
  }
}
*/

假设性问题

嵌入模型对表述方式很敏感。像“约翰·多伊是谁?”这样的简短问题,在结构上与关于约翰·多伊的冗长段落截然不同,因此相似度得分会下降,召回率也会受到影响。在这种假设性问题模式下,大语言模型在数据输入时会为每个片段撰写对应的问题。系统会嵌入这些问题而非原始文本,并附上指向父文档的链接。随后用户的查询会与这些嵌入的问题进行匹配,准确率便会显著提升。

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
  }
}

*/

自定义 Cypher

有时,父子关系、汇总关系以及假设性的拓扑结构并不符合特定的企业数据模型,这时就需要明确指定在数据导入和检索过程中应运行哪条 确切的 Cypher 语句。

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);

尽管该材料主要围绕 Neo4j 的原生图处理优势展开,但 LangChain4j 社区对这些结构模式进行了泛化处理。上述数据导入工具实际上依赖于 ParentChildEmbeddingStoreIngestor,因此只要任何 LangChain4j 嵌入存储能够正确处理元数据,就能实现通用的父子节点检索功能。这样一来,一些普通的向量数据库也能具备基本的图式 RAG 功能。

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();

图中的聊天记忆

对话型智能体需要在多次交互之间保持状态,而没有记忆功能的 AI 本质上是无状态函数。LangChain4j 的内存存储方式属于临时性存储,不适合需要应用重启和水平扩展的生产环境。Neo4jChatMemoryStore则将聊天历史持久化存储在 Neo4j 中。它并非简单的文本堆砌,而是一系列相互关联的节点,实际上就是以图形式呈现的链表,能够为用户的交互提供可持久化且可查询的记录。

该存储机制会自动创建一个以 id 属性作为键的 Memory 根节点,随后通过 LAST_MESSAGE 和 NEXT 关系形成一串 Message 节点。使用时需要提供 Neo4j 会话配置或现有的 Driver 实例,同时该机制还支持以下可选设置:

  • 内存节点标签(默认为 Memory)和消息节点标签(默认为 Message);
  • 用于标识聊天会话的内存ID属性键(默认为 id)以及消息文本属性键(默认为 message);
  • 最后一条消息的关系类型(默认为 LAST_MESSAGE)以及下一条消息的关系类型(默认为 NEXT);
  • Neo4j数据库名称(默认为 neo4j);
  • 消息检索数量(默认为 10),用于限制上下文窗口的大小。

为使不同用户的会话相互隔离,该存储系统通过唯一的 memoryId 对其进行区分。现代大型语言模型能够处理多模态输入,该存储系统会序列化并保存图像等复杂内容,同时也会将其删除。

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);

存储后的结果如下所示:

企业级数据库通常不接受Memory或Message这类通用标签。对于那些已经遵循严格命名规则的数据库,构建工具可以覆盖引擎使用的所有标签、属性和关系类型。

// 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.");

这种灵活性使得该内存模块不会显得像一个外来的插件,而是成为现有图架构中结构完善的一部分。

窗口大小

加载数千条消息的历史记录会超出令牌限制,增加API成本并降低回答质量。构造函数中的size参数用于控制链表上的滑动窗口范围。

// 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.");

有时需要查看完整情况,比如用于汇总一个月内所有支持工单的背景分析任务。当size值为0或负数时,系统会要求从列表开头开始逐一查看所有记录。

// 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.");

在小型应用或无服务器函数中,管理全局的 Neo4j Driver 对象可能并非明智之举。构建器提供的简写基本认证方式可直接使用凭证。

// 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.");

唯一需要设置的便是 Neo4j Java Driver 的连接信息。另一种方法是使用 Neo4jChatMemoryStore.builder().withBasicAuth(, , ) 在内部创建一个 Driver 实例。

由于内存是 Neo4j 的原生功能,因此可以通过自定义图算法或后台任务来分析这些节点,提取对话中提到的实体,并将用户的聊天记录与核心知识图谱关联起来,从而实现高度个性化的 AI 体验。

LangChain4j 与 Neo4j 的集成已经相当成熟。随着模型性能的提升以及图数据库对向量运算的不断优化,未来肯定还会有更多改进和功能出现。