☰
agent-core-java 知识图谱三元组模型 Triple:从 GraphKnowledgeBase 索引到图检索扩展的全链路解析
2026/10/14 8:23:57 网站建设 项目流程
  • 人工智能
  • AI Agent
  • Agent 框架
  • 工具调用
  • Agent 工作流
  • RAG
  • MCP Clients
  • 强化学习

【免费下载链接】agent-core-java

openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。

项目地址:https://gitcode.com/openJiuwen/agent-core-java
点击查看免费下载

在基于向量检索的 RAG(Retrieval-Augmented Generation)方案中,纯文本块召回难以捕捉"实体—关系—实体"之间的结构化语义。com.openjiuwen.retrieval.common.Triple是 openJiuwen agent-core-java 检索模块中承载知识图谱三元组的核心数据模型,它以subject(主语)、predicate(谓词)、object(宾语)三个必填字段和可选的metadata元数据,为GraphKnowledgeBase的图模式索引构建与图扩展检索提供了统一的数据载体。读完本文,你可以掌握Triple的字段定义、构造与校验规则、元数据使用惯例,以及它在三元组抽取、图索引构建、图扩展检索整条链路中的具体作用,从而在 Java 项目里正确构建并调试图谱增强检索能力。

一、Triple 在检索模块中的定位

Triple位于 Triple.java,其类注释标明它镜像 Python 版openjiuwen/core/retrieval/common/triple.py中的同名类,属于 Java 版与 Python 版行为对齐(parity)的实现。该模型在整个检索链路中承担"关系原子"的角色:

  • 索引侧:GraphKnowledgeBase开启图模式(useGraph=true)后,切分出的TextChunk会经由Extractor抽取为List<Triple>,再由知识库转换为文本块写入独立的三元组索引;
  • 检索侧:GraphRetriever通过三元组索引召回相关关系,并借助三元组中的实体首尾做图扩展(beam search),最终把扩展到的文本块与原召回结果做 RRF 融合;
  • 配套模型:与Triple同包下的 TripleBeam.java(一条三元组路径及其评分)和 TripleMemory.java(AgenticRetriever 中用于多轮改写时缓存已见三元组)都围绕该模型工作。

原文档(Triple.md)说明:"subject、predicate、object均不能为空;GraphKnowledgeBase图模式索引与检索依赖该模型。"下面结合源码逐层展开。

二、字段结构与构造规则

2.1 字段一览

从 Triple.java 的源码看,Triple是一个普通 POJO,包含四个成员:

字段类型是否必填说明
subjectString是主语,即关系的起点实体
predicateString是谓词,即实体间的关系描述
objectString是宾语,即关系的终点实体
metadataMap<String, Object>否附加元数据,默认初始化为空的LinkedHashMap,用于携带doc_id、chunk_id等溯源信息

需要说明的是:原文档描述中提到"可选置信度",但从当前 Java 源码看,Triple并未单独定义 confidence 字段;如果需要保存置信度一类的数值信息,可以推断应通过metadata中的自定义键承载,这一点请以实际代码为准。

2.2 构造器与校验

Triple提供三个构造入口(见 Triple.java):

public Triple() { } public Triple(String subject, String predicate, String object) { this(subject, predicate, object, null); } public Triple(String subject, String predicate, String object, Map<String, Object> metadata) { this.subject = requireText(subject, "subject"); this.predicate = requireText(predicate, "predicate"); this.object = requireText(object, "object"); setMetadata(metadata); }

校验逻辑由私有静态方法requireText完成(Triple.java):

private static String requireText(String value, String fieldName) { if (value == null) { throw RetrievalExceptions.validation(fieldName + " is required"); } return value; }

几个使用上需要注意的行为:

  1. 空值快速失败:subject、predicate、object任一为null时,构造器立即抛出RetrievalExceptions.validation生成的BaseError(错误信息形如subject is required),属于"构造即校验"的防御式设计;
  2. metadata 防御性拷贝:setMetadata会对入参做一次浅拷贝(new LinkedHashMap<>(metadata)),传null时回退为空LinkedHashMap。这保证外部修改原始 Map 不会影响已构造的Triple内部状态;
  3. LinkedHashMap 保持插入序:元数据采用LinkedHashMap而非HashMap,遍历metadata时顺序与插入顺序一致,便于日志排查和序列化输出稳定;
  4. 全量 getter/setter:三个文本字段与metadata均提供标准访问器,setter 不做二次校验,因此在运行时通过setSubject(null)写入空值是允许的,校验只发生在构造阶段。

基本用法示例:

// 最简构造,metadata 为空 Map Triple triple = new Triple("Alice", "knows", "Bob"); // 带溯源元数据的构造 Triple withMeta = new Triple( "Elden Ring", "publisher", "FromSoftware", Map.of("doc_id", "doc_1", "chunk_id", "chunk_7"));

以上用法与单元测试 TripleTest.java 完全一致:测试createTripleUsesEmptyMetadataDefault验证了三参构造后metadata为空,createTripleWithMetadataStoresMetadata验证了元数据完整存储,missingRequiredFieldsAreRejected则断言(null, null, null)、("Alice", null, null)、("Alice", "knows", null)三种缺字段场景均抛出BaseError。

三、metadata 的约定键:溯源三元组来源

Triple本身只是载体,真正的语义来自metadata中约定俗成的键。在仓库中,两个抽取器和知识库都统一使用了doc_id与chunk_id两个键,用于把三元组回溯到来源文档与文本块:

  1. TripleExtractor.parseTriples在解析 LLM 输出后,为每个三元组写入Map.of("doc_id", docId, "chunk_id", chunkId)(见 TripleExtractor.java);
  2. LLMTripleExtractor.parseTriples在复制 chunk 原有 metadata 的基础上再覆盖写入doc_id、chunk_id(见 LLMTripleExtractor.java);
  3. SimpleTripleExtractor同样遵循该惯例,并额外写入序列化后的triple键(见下文)。

这种"约定键"机制使三元组索引中的每条记录都能反查回kb_<kbId>_chunks中的原文块,是后续图扩展检索(由三元组找回关联 chunk)能够成立的根基。

四、Triple 的生成:三种 Extractor 实现

Triple实例不是手工逐条构造的,而是由索引管线的Extractor从TextChunk批量抽取而来。retrieval模块提供了三种实现(基类为 Extractor.java):

4.1 TripleExtractor:基于 LlmInvoker 的 LLM 抽取

TripleExtractor.java 是功能最完整的实现:

  • 提示词工程:内置PROMPT_TEMPLATE(L39-L144),要求 LLM 从"标题 + 段落"中构造 RDF 风格图,输出严格 JSON:顶层恰好含named_entities与triples两个键,triples中每项是恰好三个字符串的数组;模板还内置了两个 few-shot 示例(Magic Johnson、Elden Ring)并要求"能解析代词为具体名称""不要输出重复三元组"等;
  • 并发控制:每个 chunk 的抽取任务提交到名为triple-extractor-io的有界模块线程池(OpenJiuwenExecutors.newBoundedModulePool),并用Semaphore限制最大并发(默认 50);
  • 容错解析:parseTriples先做normalizeJsonContent清理(剥离 Markdown 代码围栏、去除尾逗号),再逐项校验三元组是否为恰好三元素的数组且三个元素均非 null/容器节点,非法项计数并告警跳过;解析整体失败时抛出RETRIEVAL_KB_TRIPLE_EXTRACTION_PROCESS_ERROR;
  • 单 chunk 失败策略:单个 chunk 抽取失败会被记录为该 chunk 的错误并在mergeResults中抛出首个错误,同时保留其他 chunk 已成功抽取的三元组逻辑上可见于日志。

构造参数为TripleExtractor(LlmInvoker llmClient, String modelName)或带temperature(默认 0.0)与maxConcurrent的四参形式,其中LlmInvoker是类内定义的函数式接口,便于测试注入 mock 客户端。

4.2 LLMTripleExtractor:直连 BaseModelClient 的变体

LLMTripleExtractor.java 与上述实现同源,区别在于它直接依赖BaseModelClient,并通过自建的ThreadPoolExecutor(核心 0、最大maxConcurrent、SynchronousQueue、CallerRunsPolicy)并发抽取。其extractJson/repairJson对 LLM 输出做了更强的兜底:先剥离代码围栏,再按"最外层数组优先、否则取最外层对象"的策略截取 JSON 片段,最后用正则修复尾逗号。解析成功后同样以node.get(0/1/2).asText().trim()生成Triple,并把 chunk 原有 metadata 合并进三元组元数据。

4.3 SimpleTripleExtractor:无 LLM 的规则兜底

SimpleTripleExtractor.java 不调用任何模型,按句子边界(.!?。!?之后的空白,见SENTENCE_SPLIT)切分文本,将每句按空白拆词:第 1 个词作subject、第 2 个词作predicate、其余拼作object。它显式把序列化后的三元组写入metadata.put("triple", ...),供下游直接解析。该实现适合作为离线测试、低质量语料的兜底方案,其输出质量明显低于 LLM 抽取,选型时应注意。

三者对应的 API 文档见 LLMTripleExtractor.md 与 SimpleTripleExtractor.md。

五、下游消费:GraphKnowledgeBase 如何把 Triple 变成索引

GraphKnowledgeBase.java 是Triple最主要的生产者和消费者。

5.1 索引构建链路

addDocuments的流程(L76-L129)为:

  1. 校验chunker、indexManager非空,缺少时抛出RETRIEVAL_KB_CHUNKER_NOT_FOUND/RETRIEVAL_KB_INDEX_MANAGER_NOT_FOUND;
  2. 若文档无 id,则用UUID.randomUUID()补齐;
  3. chunker.chunkDocuments切分后,以kb_<kbId>_chunks为索引名构建 chunk 索引;
  4. 仅当config.isUseGraph() == true且注入了extractor时,才执行extractor.extract(chunks),并把得到的List<Triple>交给buildTripleIndex。

buildTripleIndex与tripleChunks展示了Triple到存储层的转换细节(L313-L352):

private List<TextChunk> tripleChunks(List<Triple> triples) { for (int index = 0; index < triples.size(); index++) { Triple triple = triples.get(index); String text = triple.getSubject() + " " + triple.getPredicate() + " " + triple.getObject(); Map<String, Object> metadata = new LinkedHashMap<>(triple.getMetadata()); metadata.put("triple", serializeTriple(triple)); // JSON: ["s","p","o"] metadata.put("chunk_index", index); ... chunks.add(new TextChunk(UUID.randomUUID().toString(), text, docId, metadata)); } }

要点:

  • 每个Triple被拼成"subject predicate object"形式的纯文本作为向量化/BM25 的索引文本;
  • 原始三元组以 JSON 数组字符串形式存入metadata.triple,这是GraphRetriever后续 beam 搜索时重建实体边的关键(parseTripleStrict会按 JSON 数组反解析该字段);
  • metadata.chunk_id直接继承自抽取阶段写入的来源 chunk id,形成"三元组 → 原文块"的反向指针;
  • 三元组索引用kb_<kbId>_triples命名(tripleIndexName()),与 chunk 索引kb_<kbId>_chunks分离存储。

5.2 删除链路

deleteDocuments(L191-L225)对每个docId先删 chunk 索引,若useGraph为真则再删kb_<kbId>_triples中的对应记录;三元组删除失败仅记录warn日志,不阻断整体流程。updateDocuments则是"先删后加"的组合操作,天然保证三元组索引随文档更新而刷新。

5.3 统计能力

getStatisticsAsync会在useGraph=true时同时返回chunk_index_info与triple_index_info,便于运维侧核对三元组索引规模(L237-L261)。

六、检索侧:三元组索引如何驱动图扩展

GraphKnowledgeBase.retrieve在useGraph生效时构造 GraphRetriever,其graphExpansion逻辑(L191-L287)完整体现了三元组索引的检索价值:

  1. 先召回 chunk:按vector/sparse/hybrid模式从 chunk 索引取初始结果;
  2. 由 chunk 反查三元组:fetchTriples收集命中 chunk 的chunk_id,以chunk_ids过滤条件从三元组索引拉取候选三元组;
  3. beam search 图扩展:TripleBeamSearch(同文件内 L581 起)把每条三元组的metadata.triple解析回[subject, predicate, object],取首尾两个实体作为查询词继续检索相邻三元组,逐跳扩展出TripleBeam路径(默认num_beams=10、num_candidates_per_beam=100、max_length即graph_hops默认 2,encoder_batch_size=256),扩展路径的得分用 query 向量与路径拼接文本向量的余弦相似度计算;
  4. 回捞并融合:扩展出的三元组再经fetchChunks反查回原文块,与新 chunk 一起通过FusionUtils.rrfFusionRetrieval(RRF k=60)融合,最后截断到topK。

整个过程中Triple的metadata.chunk_id与metadata.triple两个约定键被反复使用——前者保证"三元组↔原文块"的双向可达,后者保证实体边的可解析性。此外RetrievalConfig中的graphExpansion开关会经optionsFrom透传给检索器(L354-L364),开启agentic时则由AgenticRetriever接管,其中TripleMemory(TripleMemory.java)以小写归一化的三元组字符串去重,把已见关系以(s p o)文本形式喂给查询改写模型,避免多轮循环检索重复引用同一关系。

相关 API 文档可进一步参阅 GraphRetriever.md、TripleBeamSearch.md、TripleBeam.md、TripleMemory.md 与 GraphKnowledgeBase.md。

七、工程实践要点小结

结合源码与测试,使用Triple时有几条可直接落地的建议:

  1. 构造时机:优先使用带 metadata 的四参构造器;若手工构造三元组(如从外部图谱导入),务必自行补齐doc_id/chunk_id,否则图扩展阶段将失去回捞原文块的能力;
  2. 空值语义:校验只拦null不拦空串,若上游 LLM 可能返回空字符串三元组,建议在进入GraphKnowledgeBase前自行过滤;
  3. metadata 可变性:getMetadata()返回的是内部引用(setter 做了拷贝,getter 未做),外部代码应避免直接修改已注册三元组的 metadata,以免污染索引构建时的序列化输出;
  4. 抽取器选型:正式图谱构建用TripleExtractor/LLMTripleExtractor(质量高、有并发与容错),离线调试或无模型环境用SimpleTripleExtractor兜底;
  5. 验证方式:修改Triple相关逻辑后,可运行 TripleTest.java 与 GraphKnowledgeBaseTest.java(其中使用KnowledgeBaseConfig.builder().kbId("graph").useGraph(true).build()验证图模式链路)做回归确认。

八、相关文档索引

  • 模型文档:Triple.md、TextChunk.md、RetrievalConfig.md、KnowledgeBaseConfig.md
  • 抽取器文档:Extractor.md、LLMTripleExtractor.md、SimpleTripleExtractor.md
  • 检索文档:GraphKnowledgeBase.md、GraphRetriever.md

综上,Triple虽是一个字段极少的轻量模型,但它是 agent-core-java 图谱增强检索的枢纽数据结构:其"构造即校验"保证了索引数据的干净性,metadata中doc_id/chunk_id/triple三枚约定键串联起"抽取 → 索引 → 图扩展 → 回捞融合"的完整链路,是理解并定制GraphKnowledgeBase行为不可绕开的一环。

  • 人工智能
  • AI Agent
  • Agent 框架
  • 工具调用
  • Agent 工作流
  • RAG
  • MCP Clients
  • 强化学习

【免费下载链接】agent-core-java

openJiuwen agent core Java是 openJiuwen Core的 Java版本,提供AI Agent开发、运行、调优与演进相关的全套SDK能力。

项目地址:https://gitcode.com/openJiuwen/agent-core-java
点击查看免费下载

相关推荐

上一篇:Apache APISIX 版本演进全览:从 0.6.0 到 3.18.0 的变更记录深度解析
下一篇:Swift 标准库 Buffer API 重命名演进:SE-0114 如何将 "Value" 更名为 "Header"

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询