用 Gemini 从非结构化文本构建知识图谱:一篇可落地教程

来源:HackerNoon《Building Knowledge Graphs with Gemini》及其配套 Google notebook。

原文 URL:https://hackernoon.com/building-knowledge-graphs-with-gemini

配套 notebook:https://github.com/GoogleCloudPlatform/generative-ai/blob/main/gemini/use-cases/knowledge-graph/knowledge_graph_generation.ipynb

本教程不是逐字翻译,而是把原文方法改写成一个可执行的工程落地流程。

你将做出什么

完成后,你会得到一个最小知识图谱抽取流水线:

  1. 输入一段非结构化文本。
  2. 用提示词让 Gemini 抽取实体和关系。
  3. 要求 Gemini 输出 TSV,而不是 JSON。
  4. 用 Python 解析 TSV。
  5. 构建节点和边。
  6. 验证输出是否可入库或可视化。

为了降低试错成本,教程先用 mock 输出跑通解析和校验,再给出 Gemini API 替换点。这样即使暂时没有 API key,也能先验证工程骨架。

适用场景

适合这些任务:

不适合直接用于这些任务:

总体架构

raw text
  │
  ▼
Gemini extraction prompt
  │
  ▼
TSV output
  │
  ├── entities.tsv: id, name, label
  │
  └── relations.tsv: source_id, link, target_id
  │
  ▼
Python parser + validator
  │
  ▼
Graph object / NetworkX / Neo4j / SQLite
  │
  ▼
visualization, search, analysis

核心设计选择:

| 设计点 | 推荐做法 | 原因 | |---|---|---| | 输出格式 | TSV 优先,JSON 备选 | 实体/关系本质是表,TSV 更省 Token | | 模型参数 | temperature=0.0, top_p=0.0 | 抽取任务要稳定,不要创作性 | | 提示词约束 | 明确“只使用输入数据” | 降低模型调用训练记忆补全事实的概率 | | 关系方向 | 明确双向/反向规则 | 避免图谱只抽到单向边 | | 生产落地 | 先校验,再入库 | LLM 输出不能直接信任 |

第 1 步:准备环境

如果你只跑 mock 版本,Python 标准库即可。

如果你要接 Gemini 和画图,安装:

python3 -m venv .venv
source .venv/bin/activate
pip install "google-genai>=2.6.0" "networkx[default]" tenacity

API key 不要写进代码。使用环境变量:

export GOOGLE_API_KEY="*** Gemini API Key ***"

说明:原文也支持 Vertex AI / Agent Platform 路线;本教程以 Google AI Studio API key 路线说明,便于最小化落地。

第 2 步:先定义输出契约

不要一开始就让模型“自由总结关系”。先固定输出协议。

实体表:

id	name	label
0	Henry Jones	person
1	Sophie Jones	person

关系表:

source_id	link	target_id
0	father_of	1
1	child_of	0

关系命名建议:

第 3 步:写一个可执行的 mock-first 解析器

先新建文件:

mkdir -p kg_demo
cd kg_demo
cat > kg_from_tsv.py <<'PY'
from __future__ import annotations

from dataclasses import dataclass
from typing import Iterable


@dataclass(frozen=True)
class Entity:
    id: str
    name: str
    label: str


@dataclass(frozen=True)
class Relation:
    source_id: str
    link: str
    target_id: str


@dataclass(frozen=True)
class KnowledgeGraph:
    entities: list[Entity]
    relations: list[Relation]


def parse_tsv_table(text: str, expected_header: list[str]) -> list[dict[str, str]]:
    lines = [line.strip() for line in text.strip().splitlines() if line.strip()]
    if not lines:
        return []

    header = lines[0].split("\t")
    if header != expected_header:
        raise ValueError(f"Unexpected header: {header}, expected: {expected_header}")

    rows: list[dict[str, str]] = []
    for line_number, line in enumerate(lines[1:], start=2):
        values = line.split("\t")
        if len(values) != len(expected_header):
            raise ValueError(f"Line {line_number} has {len(values)} columns: {line!r}")
        rows.append(dict(zip(expected_header, values, strict=True)))
    return rows


def parse_entities(text: str) -> list[Entity]:
    rows = parse_tsv_table(text, ["id", "name", "label"])
    return [Entity(id=row["id"], name=row["name"], label=row["label"]) for row in rows]


def parse_relations(text: str) -> list[Relation]:
    rows = parse_tsv_table(text, ["source_id", "link", "target_id"])
    return [
        Relation(source_id=row["source_id"], link=row["link"], target_id=row["target_id"])
        for row in rows
    ]


def validate_graph(graph: KnowledgeGraph) -> None:
    entity_ids = {entity.id for entity in graph.entities}
    if len(entity_ids) != len(graph.entities):
        raise ValueError("Duplicate entity ids found")

    for relation in graph.relations:
        if relation.source_id not in entity_ids:
            raise ValueError(f"Missing source entity: {relation.source_id}")
        if relation.target_id not in entity_ids:
            raise ValueError(f"Missing target entity: {relation.target_id}")


def format_edges(graph: KnowledgeGraph) -> list[str]:
    names = {entity.id: entity.name for entity in graph.entities}
    return [
        f"{names[relation.source_id]} --{relation.link}--> {names[relation.target_id]}"
        for relation in graph.relations
    ]


def build_graph_from_tsv(entities_tsv: str, relations_tsv: str) -> KnowledgeGraph:
    graph = KnowledgeGraph(
        entities=parse_entities(entities_tsv),
        relations=parse_relations(relations_tsv),
    )
    validate_graph(graph)
    return graph


def main() -> None:
    # Mock Gemini TSV output for the first validation loop.
    entities_tsv = """id	name	label
0	Henry Jones	person
1	Sophie Jones	person
2	William Smith	person
3	Acme Aerospace	organization
"""

    relations_tsv = """source_id	link	target_id
0	father_of	1
1	child_of	0
1	works_at	3
2	works_at	3
1	friend_of	2
2	friend_of	1
"""

    graph = build_graph_from_tsv(entities_tsv, relations_tsv)
    print(f"entities={len(graph.entities)} relations={len(graph.relations)}")
    for edge in format_edges(graph):
        print(edge)


if __name__ == "__main__":
    main()
PY
python3 kg_from_tsv.py

预期输出:

entities=4 relations=6
Henry Jones --father_of--> Sophie Jones
Sophie Jones --child_of--> Henry Jones
Sophie Jones --works_at--> Acme Aerospace
William Smith --works_at--> Acme Aerospace
Sophie Jones --friend_of--> William Smith
William Smith --friend_of--> Sophie Jones

这一轮的目标不是接入模型,而是确认:

第 4 步:设计 Gemini 抽取提示词

下面是可直接复用的提示词模板:

You are an information extraction engine.
Use only the input text. Do not use external knowledge.

Task:
Extract entities and relationships from the input text.

Entity schema:
- id: stable integer string starting from 0
- name: canonical entity name
- label: one of person, organization, place, system, document, concept, event, animal, other

Relationship schema:
- source_id: id of the source entity
- link: lowercase snake_case relationship type
- target_id: id of the target entity

Rules:
1. Output TSV only. No Markdown, no commentary.
2. Do not include tabs or newlines inside field values.
3. Use only facts explicitly present in the input text.
4. If a relationship is symmetric, output both directions.
5. If a relationship is asymmetric and an obvious inverse is needed for graph traversal, output the inverse relation too.
6. Prefer canonical names. Merge aliases when the input clearly says they refer to the same entity.
7. If there is no valid entity or relation, output only the headers.

Output format:
ENTITIES
id	name	label
...

RELATIONS
source_id	link	target_id
...

Input text:
{{INPUT_TEXT}}

为什么要写得这么死:

第 5 步:接入 Gemini API

把下面文件保存为 gemini_extract.py

from __future__ import annotations

import os
from textwrap import dedent

from google import genai
from google.genai.types import GenerateContentConfig


PROMPT_TEMPLATE = """
You are an information extraction engine.
Use only the input text. Do not use external knowledge.

Task:
Extract entities and relationships from the input text.

Entity schema:
- id: stable integer string starting from 0
- name: canonical entity name
- label: one of person, organization, place, system, document, concept, event, animal, other

Relationship schema:
- source_id: id of the source entity
- link: lowercase snake_case relationship type
- target_id: id of the target entity

Rules:
1. Output TSV only. No Markdown, no commentary.
2. Do not include tabs or newlines inside field values.
3. Use only facts explicitly present in the input text.
4. If a relationship is symmetric, output both directions.
5. If a relationship is asymmetric and an obvious inverse is needed for graph traversal, output the inverse relation too.
6. Prefer canonical names. Merge aliases when the input clearly says they refer to the same entity.
7. If there is no valid entity or relation, output only the headers.

Output format:
ENTITIES
id\tname\tlabel
...

RELATIONS
source_id\tlink\ttarget_id
...

Input text:
{input_text}
"""


def extract_with_gemini(input_text: str, model: str = "gemini-3.1-flash-lite") -> str:
    if not os.environ.get("GOOGLE_API_KEY"):
        raise RuntimeError("GOOGLE_API_KEY is not set")

    client = genai.Client()
    prompt = PROMPT_TEMPLATE.format(input_text=input_text)
    response = client.models.generate_content(
        model=model,
        contents=prompt,
        config=GenerateContentConfig(
            temperature=0.0,
            top_p=0.0,
            seed=42,
        ),
    )
    return response.text or ""


def main() -> None:
    sample_text = dedent(
        """
        Henry Jones is a famous archaeologist.
        Sophie is Henry's daughter and works as a software engineer.
        William Smith is Sophie's lifelong friend.
        Sophie and William both work at Acme Aerospace.
        """
    ).strip()
    print(extract_with_gemini(sample_text))


if __name__ == "__main__":
    main()

运行:

export GOOGLE_API_KEY="你的 Gemini API Key"
python3 gemini_extract.py

预期输出形态:

ENTITIES
id	name	label
0	Henry Jones	person
1	Sophie	person
2	William Smith	person
3	Acme Aerospace	organization

RELATIONS
source_id	link	target_id
0	father_of	1
1	child_of	0
1	friend_of	2
2	friend_of	1
1	works_at	3
2	works_at	3

注意:具体实体名可能有轻微差异,比如 Sophie 是否补成 Sophie Jones,取决于输入是否明确提供姓氏。生产环境不要依赖模型自动补全。

第 6 步:把 Gemini 输出拆成两个 TSV 表

Gemini 返回的是一个文本块,需要切出 ENTITIESRELATIONS 两段。

from __future__ import annotations


def split_gemini_output(output: str) -> tuple[str, str]:
    if "ENTITIES" not in output or "RELATIONS" not in output:
        raise ValueError("Gemini output must contain ENTITIES and RELATIONS sections")

    before_relations, relations_part = output.split("RELATIONS", maxsplit=1)
    _, entities_part = before_relations.split("ENTITIES", maxsplit=1)

    entities_tsv = entities_part.strip()
    relations_tsv = relations_part.strip()
    return entities_tsv, relations_tsv

接到第 3 步的解析器:

raw_output = extract_with_gemini(sample_text)
entities_tsv, relations_tsv = split_gemini_output(raw_output)
graph = build_graph_from_tsv(entities_tsv, relations_tsv)

第 7 步:加入 3 个实操用例

用例 A:人物关系图谱

输入:

Henry Jones is a famous archaeologist.
Sophie is Henry's daughter and works as a software engineer.
William Smith is Sophie's lifelong friend.
Sophie and William both work at Acme Aerospace.

验收标准:

用例 B:运维故障报告图谱

输入:

At 10:03, checkout-api returned HTTP 500 errors.
The root cause was a Redis connection timeout.
SRE Alice restarted the checkout-api deployment.
The incident affected the payment-service and order-service.

期望关系:

checkout-api --returned--> HTTP 500 errors
Redis connection timeout --root_cause_of--> checkout-api incident
Alice --restarted--> checkout-api deployment
checkout-api incident --affected--> payment-service
checkout-api incident --affected--> order-service

验收标准:

用例 C:合同义务图谱

输入:

Vendor must deliver the monthly security report to Customer by the fifth business day of each month.
Customer must pay the invoice within 30 days after receiving it.
Either party may terminate the agreement with 60 days written notice.

期望关系:

Vendor --must_deliver--> monthly security report
monthly security report --delivered_to--> Customer
Customer --must_pay--> invoice
Either party --may_terminate--> agreement
termination --requires_notice_period--> 60 days

验收标准:

第 8 步:可选,用 NetworkX 建图

安装:

pip install "networkx[default]"

示例:

from __future__ import annotations

import networkx as nx


def to_networkx(graph: KnowledgeGraph) -> nx.DiGraph:
    result = nx.DiGraph()
    for entity in graph.entities:
        result.add_node(entity.id, name=entity.name, label=entity.label)
    for relation in graph.relations:
        result.add_edge(relation.source_id, relation.target_id, label=relation.link)
    return result

基础查询:

nx_graph = to_networkx(graph)
print(nx_graph.number_of_nodes())
print(nx_graph.number_of_edges())
print(list(nx_graph.out_edges("1", data=True)))

你可以先不画图。对工程落地来说,先确认节点和边正确,比先做漂亮可视化更重要。

第 9 步:生产化前必须补的校验

LLM 输出不能直接入库,至少加这些 gate:

| Gate | 检查内容 | 失败处理 | |---|---|---| | 格式校验 | 是否包含 ENTITIES / RELATIONS,TSV 列数是否正确 | 重试一次;仍失败则进入人工队列 | | ID 校验 | 实体 ID 是否唯一,关系引用是否存在 | 拒绝入库 | | label 白名单 | label 是否属于允许集合 | 映射到 other 或拒绝 | | relation 白名单 | link 是否属于允许集合 | 进入人工确认或归一化 | | 规模校验 | 节点/边数量是否异常 | 标记为异常样本 | | 抽样复核 | 随机抽样比对原文 | 记录准确率和常见错误 |

推荐把模型输出保存下来:

runs/
  inputs/2026-xx-xx-sample.txt
  outputs/2026-xx-xx-sample.tsv
  reviews/2026-xx-xx-sample-review.md

这样后续提示词改动、模型升级、成本变化都能回放对比。

第 10 步:常见失败与修复

失败 1:模型输出了 Markdown 表格

现象:

| id | name | label |
|---|---|---|

修复:

失败 2:字段里出现制表符或换行

修复:

失败 3:同一个实体被抽成多个节点

例子:

William Smith
Bill

修复:

失败 4:关系方向反了

修复:

一个一周落地计划

| 天数 | 目标 | 产物 | |---|---|---| | Day 1 | 选定一个低风险领域,如故障报告或会议纪要 | 20 条样本文本 | | Day 2 | 跑通 mock 解析器和 TSV 校验 | kg_from_tsv.py | | Day 3 | 接入 Gemini,保存原文和输出 | runs/inputs / runs/outputs | | Day 4 | 建立 label/link 白名单 | schema.yaml 或 Python 常量 | | Day 5 | 人工复核 20 条样本 | 错误清单 | | Day 6 | 修提示词和校验器 | v2 prompt + validator | | Day 7 | 决定是否接入 NetworkX/Neo4j/SQLite | PoC 报告 |

最小验收清单

结论

这篇文章最值得复用的不是“用 Gemini 生成知识图谱”这句话,而是三个工程判断:

  1. 把 LLM 当抽取器,不当事实裁判。
  2. 对实体/关系这种表结构输出,TSV 往往比 JSON 更省、更快。
  3. 知识图谱落地的关键不是可视化,而是 schema、校验、复核和可回放。

如果只做 PoC,按本文流程一天内能跑通;如果要进生产,先用低风险数据做一周闭环,不要直接把模型输出写入正式图数据库。