Skip to content

课 1 · 实体关系抽取

本课目标

用 LLM 从文档中抽取实体和关系,构建可写入 Neo4j 的知识图谱数据。

先建立直觉:文档是线性文本,图谱是网状结构。 抽取实体关系就是把文本里隐含的网状结构显式化。

为什么需要图谱

向量检索擅长找"语义相关的文档",但有一类问题它处理不好:

问题类型例子向量检索图谱检索
多跳关系"A 和 B 的共同上级是谁?"
路径查询"从 X 到 Y 有几条关联路径?"不支持原生支持
聚合"和 GPT-4 有关的所有论文"
关系推理"谁开发了 LangGraph 的前身?"

实体关系的数据结构

typescript
// packages/shared/src/types/graph.ts
export interface Entity {
  id: string               // 唯一 ID(如 "openai-inc")
  name: string             // 显示名称(如 "OpenAI")
  type: string             // 实体类型(如 "Company", "Person", "Paper")
  description?: string
  sourceChunkIds: string[] // 来自哪些文档片段
}

export interface Relationship {
  id: string
  fromEntityId: string
  toEntityId: string
  type: string             // 关系类型(如 "DEVELOPED_BY", "AUTHORED_BY")
  description?: string
  sourceChunkIds: string[]
}

LLM 抽取实体关系

typescript
// packages/rag-core/src/graph/extractor.ts
import { generateObject } from 'ai'
import { z } from 'zod'

const extractionSchema = z.object({
  entities: z.array(
    z.object({
      name: z.string(),
      type: z.enum(['Person', 'Organization', 'Product', 'Technology', 'Concept', 'Other']),
      description: z.string().optional(),
    })
  ),
  relationships: z.array(
    z.object({
      from: z.string(),   // 实体 name(与 entities 对应)
      to: z.string(),
      type: z.string(),   // 大写下划线格式,如 DEVELOPED_BY
      description: z.string().optional(),
    })
  ),
})

export async function extractEntitiesAndRelationships(
  chunk: { id: string; content: string },
) {
  const { object } = await generateObject({
    model: getModel(),
    schema: extractionSchema,
    prompt: `
从以下文档片段中抽取实体和关系。

要求:
- 只抽取文档中明确提到的实体,不要推断
- 实体类型:Person(人)、Organization(机构/公司)、Product(产品)、Technology(技术)、Concept(概念)
- 关系类型用大写下划线格式(如 DEVELOPED_BY、WORKS_FOR、BASED_ON)
- 每个关系必须双方都是已抽取的实体

文档片段:
${chunk.content}
    `.trim(),
  })

  // 规范化:生成唯一 ID
  const entityMap = new Map<string, Entity>()
  for (const e of object.entities) {
    const id = e.name.toLowerCase().replace(/\s+/g, '-')
    entityMap.set(e.name, {
      id,
      name: e.name,
      type: e.type,
      description: e.description,
      sourceChunkIds: [chunk.id],
    })
  }

  const relationships: Relationship[] = object.relationships
    .filter((r) => entityMap.has(r.from) && entityMap.has(r.to))
    .map((r) => ({
      id: `${entityMap.get(r.from)!.id}-${r.type}-${entityMap.get(r.to)!.id}`,
      fromEntityId: entityMap.get(r.from)!.id,
      toEntityId: entityMap.get(r.to)!.id,
      type: r.type,
      description: r.description,
      sourceChunkIds: [chunk.id],
    }))

  return {
    entities: Array.from(entityMap.values()),
    relationships,
  }
}

批量抽取与去重

同一个实体可能在多个文档片段中出现,需要合并:

typescript
// packages/rag-core/src/graph/batch-extract.ts
export async function buildGraphFromChunks(
  chunks: Array<{ id: string; content: string }>,
) {
  const entityMap = new Map<string, Entity>()
  const relationshipMap = new Map<string, Relationship>()

  for (const chunk of chunks) {
    const { entities, relationships } = await extractEntitiesAndRelationships(chunk)

    for (const entity of entities) {
      if (entityMap.has(entity.id)) {
        // 实体已存在:合并来源引用
        const existing = entityMap.get(entity.id)!
        existing.sourceChunkIds.push(...entity.sourceChunkIds)
      } else {
        entityMap.set(entity.id, entity)
      }
    }

    for (const rel of relationships) {
      if (!relationshipMap.has(rel.id)) {
        relationshipMap.set(rel.id, rel)
      } else {
        relationshipMap.get(rel.id)!.sourceChunkIds.push(...rel.sourceChunkIds)
      }
    }
  }

  return {
    entities: Array.from(entityMap.values()),
    relationships: Array.from(relationshipMap.values()),
  }
}

本节产物

packages/rag-core/src/graph/
  extractor.ts            # LLM 实体关系抽取
  batch-extract.ts        # 批量抽取与去重
packages/shared/src/types/
  graph.ts                # Entity / Relationship 类型

面试追问

实体抽取的质量怎么保证?

三个措施:1)在 prompt 里给明确的类型范围和格式约束(大写下划线关系类型);2)用结构化输出(generateObject + Zod)强制格式,避免自由文本解析;3)加后处理验证:关系的两端实体必须都在 entities 里,否则丢弃。质量问题主要来自歧义(同名不同实体),生产环境需要实体消歧或人工审核。

如果同一个实体在不同文档里叫法不同怎么办?

这是"实体解析(Entity Resolution)"问题,NLP 里的经典难题。简单方案:用 LLM 在抽取时标准化名称(统一用全称),或者在图谱写入前做相似实体合并(基于名称相似度阈值)。进阶课不深入这个方向,了解问题存在即可。

面向前端工程师和独立开发者的 AI 应用工程课程