介绍如何利用 LLM 自动解析文档并构建动态知识图,适用于文档管理、知识库、研究工具等场景。
文档本质上是一张概念之网,只是伪装成了文件列表。每个页面都在陈述关系(例如“CocoIndex 支持增量处理”),但这些关系被锁在自然语言里。本文将一个 Markdown 文档文件夹(CocoIndex 自己的文档)转换成 Neo4j 知识图谱:由 LLM 阅读每篇文档,提取其中讨论的概念及其关系;当文档发生变化时,CocoIndex 会以增量方式将这些变化同步到图谱中。
我们会生成两类关系:
主体与客体之间的关系。例如,“CocoIndex 支持 Incremental Processing”。
文档对实体的提及。例如,basics.md 提到了 CocoIndex 和 Incremental Processing。
最终得到的是一个小巧且定义清晰的图谱 schema:两个节点标签(Document 和 Entity),以及两种关系类型(实体之间的 RELATIONSHIP,以及连接文档和其所引用实体的 MENTION)。LLM 提取出的所有内容都会落入这四种结构之一。正因如此,这张图谱才能保持可查询,而不会沦为一堆松散的三元组。
源代码可以在 CocoIndex Examples 的 docs_to_knowledge_graph 示例中找到。
安装图数据库 Neo4j。下文“运行示例”部分提供了一条 Docker 命令。
准备一个 LLM API key。该示例默认使用 OpenAI;模型名称采用 LiteLLM id,因此也可以设置 LLM_MODEL=ollama/llama3.2,在本地运行提取流程。
Neo4j 中的所有节点都需要具备两项信息:
Label:节点的类型,例如 Document、Entity。
Primary key:能够唯一标识节点的字段,例如 Document 节点的 filename。
CocoIndex 使用 primary key 匹配节点并进行去重:即使使用同一个 key 声明两次,最终也只会生成一个节点。在 CocoIndex v1 中,schema 就是普通的 dataclass:
MENTION 不携带任何 payload,因此完全不需要 dataclass:Neo4j connector 会根据两个端点(document、entity)确定它的身份,每对端点只生成一条边。
Entity 会在不同文档之间共享:同一个概念,比如 CocoIndex,可能出现在许多文件提取出的三元组中,而这些引用都必须收敛到同一个 Entity 节点。因此,整个 pipeline 分为两个阶段。阶段 1 独立处理每篇文档:声明 Document 节点,并提取关系三元组。阶段 2 对所有三元组执行一次统一遍历:负责生成去重后的 Entity 节点,并声明 RELATIONSHIP 和 MENTION 边。
由于阶段 1 会为每个文件运行一个独立组件,因此编辑某篇文档时,只会重新提取这一篇。随后,图谱处理阶段会执行 diff:凡是已经不再被任何文档支持的节点和边,都会被自动删除。
Neo4j 连接只需在应用生命周期中提供一次,之后可以通过 ContextKey 在任意位置读取。LLM 模型名称同样是一个 context value,并使用 detect_change=True 声明。这样一来,更换模型后,系统就会使用新模型重新提取所有内容:
提取 schema 使用 Pydantic model 定义,其中的字段描述也会成为 prompt 的一部分。对于摘要,我们要求模型返回一个标题和一段正文:
提取逻辑由一个 CocoIndex function 完成。它通过构建在 LiteLLM 之上的 instructor 调用模型,并强制响应符合指定 schema。memo=True 会根据内容对结果进行 memoize,因此内容没有变化的文档绝不会被重复发送给模型:
对于每篇文档,模型都会返回一份类型明确的摘要:
一条关系是一个由 subject、predicate 和 object 构成的三元组。subject 和 object 会成为 Entity 节点,predicate 则成为连接它们的边上的标签:
Pydantic model 与这一结构一一对应,system prompt 则引导模型关注概念,而不是代码:
提取函数与摘要函数采用相同的结构,并以同样的方式进行 memoize:
对于 CocoIndex 文档,一个文件通常可以提取出几十个三元组:
process_file 负责处理单篇文档:读取内容、提取摘要、在图谱中声明 Document 节点,然后返回关系三元组,供阶段 2 使用。declare_record 会写入一个节点;CocoIndex 根据 primary key 将其同步到 Neo4j:
每条声明的 record 都会成为一个以 filename 为 key 的 Document 节点,同时把 title 和 summary 作为节点属性一并写入:
阶段 2 接收来自所有文档的三元组。由于 Entity 节点以 value 为 key,即使在十篇不同文档中声明 CocoIndex,最终也只会生成一个节点:CocoIndex 会根据 primary key 进行去重。随后,所有相关的边都会从这一个节点向外展开。这正是后续查询“哪些内容提到了这个概念?”时所需要的结构。
系统会在一次遍历中收集不同的实体和提及关系,然后统一声明所有内容。declare_relation 根据两个节点的 primary key 建立连接,generate_id 则会对每个三元组进行哈希,使同一事实无论被重新提取,还是再次被另一篇文档陈述,都始终映射到同一条边:
RELATIONSHIP 边连接两个 Entity 节点,predicate 则存储在边上:
MENTION 边记录哪篇文档提到了哪个概念。例如,basics.md 中有一句“CocoIndex supports Incremental Processing”,因此它同时提到了 CocoIndex 和 Incremental Processing:
app_main 会挂载两个节点表和两个关系 target,遍历 docs 文件夹,为每篇文档分派一个 process_file,最后对收集到的三元组执行一次统一的 build_graph:
RELATIONSHIP 使用 Relationship schema 挂载,因此每个由哈希 id 标识的不同三元组都会成为一条独立的边。MENTION 则在没有 schema 的情况下挂载,因此 connector 会根据端点为它确定 key。
启动 Neo4j,安装示例并构建图谱。示例自带一个包含样例文档的 markdown_files/ 文件夹,因此开箱即可运行:
如果要为自己的文档构建图谱,可以将 .md 或 .mdx 文件放入 markdown_files/,或者让 sourcedir 指向真实的文档目录,然后重新运行。只有发生变化的文档才会被重新提取;如果没有任何变化,再次运行时产生的 LLM 调用次数为零。
打开 Neo4j Browser,使用用户名 neo4j、密码 cocoindex 登录,然后运行 Cypher 查询。要查看概念之间的关系:
要找出被最多文档提及的概念:
或者运行 MATCH p=()-->() RETURN p 查看全部内容:
LLM 有时会用两种不同的名称指代同一个概念,例如“CocoIndex”和“Cocoindex”。会议记录图谱示例增加了一个结合 embedding 与 LLM 的实体解析步骤,用于合并这些近似重复项;这个步骤可以直接插入当前 pipeline 的两个阶段之间。
我们正在持续添加示例并改进 runtime。如果本文对你有所帮助,欢迎在 GitHub 上为 CocoIndex 点亮 star。
为长周期 Agent 提供全新上下文。
CocoIndex CEO 兼联合创始人。主要撰写增量式数据基础设施、Agent,以及引擎背后的工程决策。
使用 CocoIndex 处理文档:LLM 从每篇文档中提取 subject–predicate–object 关系,CocoIndex 再将这些关系声明为 Neo4j 等图数据库中的节点和边。在这个示例中,pipeline 会读取 CocoIndex 自己的 Markdown 文档,并在文档发生变化时以增量方式同步图谱。参见“The flow”。
定义一个包含 subject、predicate 和 object 字段的 Pydantic model,并通过构建在 LiteLLM 之上的 instructor,将其作为 response_model 传给 LLM 调用。字段描述能够引导模型生成高质量输出,而使用 @coco.fn(memo=True) 包装调用,则可以根据内容缓存提取结果。参见“Extract relationship triples”。
该示例会生成两类边。RELATIONSHIP 连接两个实体,例如“CocoIndex supports Incremental Processing”,predicate 存储在边上。MENTION 则记录某篇文档引用了某个实体,例如 basics.md 提到了 CocoIndex 和 Incremental Processing。它们通过不同的 relation target 分别声明。参见“Phase 2: build the concept graph”。
每个节点都有一个 label(节点类型,例如 Document 或 Entity)和一个 primary key(例如 Document 节点使用 filename,Entity 节点使用 value)。CocoIndex 根据 primary key 匹配节点:即使许多文档都声明了同一个 key,最终也只会生成一个节点。参见“Phase 2: build the concept graph”。
使用 neo4j.mount_table_target 为每种 label 挂载一个节点表,并使用 neo4j.mount_relation_target 为每种边类型挂载一个 relation target;然后通过 declare_record 声明数据行,通过 declare_relation(from_id=…, to_id=…) 声明边。带有 payload 的边(例如包含 predicate 的边)需要 schema 和自己的 primary key;像 MENTION 这样不带 payload 的边,则根据其端点确定 key。参见“Wire up the app”。
你需要使用 Neo4j 作为图数据库(文中提供了一条 Docker 命令),并准备一个 LLM API key。模型 id 使用 LiteLLM id,因此可以设置 LLM_MODEL=ollama/llama3.2,将 OpenAI 替换为本地模型。与 CocoIndex v0 不同,这里不需要 PostgreSQL:增量状态会记录在本地文件中。参见“Prerequisites”。
在 http://localhost:7474 打开 Neo4j Browser,使用开发环境凭据登录(用户名 neo4j,密码 cocoindex),然后运行 Cypher 查询。例如,可以运行 MATCH p=()-->() RETURN p 查看所有关系,也可以聚合 MENTION 边,对被引用次数最多的概念进行排名。参见“Browse the knowledge graph”。