Appearance
课 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 在抽取时标准化名称(统一用全称),或者在图谱写入前做相似实体合并(基于名称相似度阈值)。进阶课不深入这个方向,了解问题存在即可。