知识图谱构建
从非结构化文本构建知识图谱:实体抽取、关系挖掘、去重
🌐 查看英文原文 | 源码 Notebook
使用 Claude 构建知识图谱
You have a pile of unstructured documents and need to answer questions that span them — “who works with people who worked on project X”, “which vendors are connected to this incident”. No single document contains the answer. RAG retrieval won’t chain the facts for you. You need a knowledge graph: entities as nodes, typed relations as edges, so that multi-hop reasoning becomes graph traversal.
Building one used to mean training a named-entity recognizer on your domain, training a relation classifier, writing entity-resolution heuristics, and maintaining all three as your data shifted. With Claude, each of those stages becomes a prompt.
你将学到什么
By the end of this guide you will be able to:
- Use structured outputs to extract typed entities and subject–predicate–object triples from arbitrary text with no training data
- Apply Claude-driven entity resolution to collapse surface-form variants into canonical nodes, replacing brittle string-similarity heuristics
- Assemble and query an in-memory graph, and run multi-hop questions by serializing subgraphs back to Claude
- Measure extraction quality with precision/recall against a gold set and reason about the cost/quality tradeoff between Haiku and Sonnet
Everything runs in memory with no database. The techniques transfer directly to Neo4j, Neptune, or a Postgres adjacency table when you need to scale.
前置条件
- Python 3.11+
- Anthropic API key (get one here)
- Basic familiarity with graphs (nodes, edges, traversal)
环境配置
%%capture
%pip install anthropic requests networkx matplotlib python-dotenv pydanticimport json
from collections import defaultdict
from pathlib import Path
from typing import Literal
from urllib.parse import quote
import anthropic
import matplotlib.pyplot as plt
import networkx as nx
import requests
from dotenv import load_dotenv
from pydantic import BaseModel
load_dotenv()
client = anthropic.Anthropic()
EXTRACTION_MODEL = "claude-haiku-4-5"
SYNTHESIS_MODEL = "claude-sonnet-4-6"
```我们使用两种模型。Haiku 处理高量级、模式约束的提取工作,在这种情况下,速度和成本比细微差别更重要。Sonnet 处理实体解析和摘要,模型需要权衡文档中的矛盾证据。
## 构建语料库
我们需要一些讨论重叠实体的文档,这样实体解析才有实际的工作可做。阿波罗计划是一个很好的测试平台:六篇简短的维基百科摘要,都提到了NASA、月球、几位宇航员和一枚运载火箭——但每篇文章对它们的命名略有不同。
我们从维基百科REST API获取摘要而不是完整文章,以保持token成本较低。对于生产流水线,您会分块处理完整文档;提取逻辑是相同的。
```python
def extract_entities(document):
# 提取实体的逻辑
pass{
"entities": [
"NASA",
"月球",
"宇航员",
"运载火箭"
]
}SELECT * FROM documents WHERE contains('NASA');
``````python
ARTICLE_TITLES = [
"Apollo program",
"Apollo 11",
"Neil Armstrong",
"Saturn V",
"Buzz Aldrin",
"Kennedy Space Center",
]
WIKI_API = "https://en.wikipedia.org/api/rest_v1/page/summary/"
HEADERS = {"User-Agent": "claude-cookbooks/1.0 (https://github.com/anthropics/claude-cookbooks)"}
def fetch_summary(title: str) -> str:
slug = quote(title.replace(" ", "_"), safe="")
r = requests.get(WIKI_API + slug, headers=HEADERS, timeout=10)
r.raise_for_status()
return r.json()["extract"]
documents = []
for i, title in enumerate(ARTICLE_TITLES):
try:
documents.append({"id": i, "title": title, "text": fetch_summary(title)})
except requests.RequestException as e:
print(f"Skipping {title}: {e}")
if not documents:
raise RuntimeError("No documents loaded — check network and Wikipedia API availability")
print(f"Loaded {len(documents)} documents\n")
print(f"Sample — {documents[0]['title']}:\n{documents[0]['text'][:300]}...")Loaded 6 documents
Sample — Apollo program:
The Apollo program, also known as Project Apollo, was the United States human spaceflight program led by NASA, which landed the first humans on the Moon in 1969. Apollo was conceived during Project Mercury and executed after Project Gemini. It was conceived in 1960 as a three-person spacecraft durin...
```## 实体和关系抽取
经典的命名实体识别(NER)标签将文本片段标记为标签(如PERSON,ORG,LOC)。经典的关系抽取随后将文本片段对分类为关系类型。两者都传统上需要每个领域都有标记的训练数据。
我们将这两个阶段合并为每个文档的单个Claude调用。关键是**结构化输出**:我们定义输出形状为Pydantic模型,并将其传递给`client.messages.parse()`。Claude的响应将保证与该模式进行验证,并以类型化的Python对象返回——无需正则表达式解析,无需JSON解码错误,无需防御性的`isinstance`检查。
::: callout-notice
请注意,以下代码示例仅供参考,实际应用中可能需要根据具体情况进行调整。
```python
# 示例代码
def extract_entities_and_relations(text):
# 实体和关系抽取逻辑
pass
``````python
EntityType = Literal["PERSON", "ORGANIZATION", "LOCATION", "EVENT", "ARTIFACT"]
ENTITY_TYPES = ["PERSON", "ORGANIZATION", "LOCATION", "EVENT", "ARTIFACT"]
class Entity(BaseModel):
name: str
type: EntityType
description: str
class Relation(BaseModel):
source: str
predicate: str
target: str
class ExtractedGraph(BaseModel):
entities: list[Entity]
relations: list[Relation]
EXTRACTION_PROMPT = """Extract a knowledge graph from the document below.
<document>
{text}
</document>
Guidelines:
- Extract only entities that are central to what this document is about — skip incidental mentions.
- For each entity, write a one-sentence description grounded in this document. These descriptions are used later to disambiguate entities with similar names.
- Predicates should be short verb phrases ("commanded", "launched from", "part of").
- Every relation must connect two entities you extracted."""
def extract(text: str) -> ExtractedGraph:
response = client.messages.parse(
model=EXTRACTION_MODEL,
max_tokens=2048,
messages=[{"role": "user", "content": EXTRACTION_PROMPT.format(text=text)}],
output_format=ExtractedGraph,
)
return response.parsed_output
raw_entities = []
raw_relations = []
for doc in documents:
try:
result = extract(doc["text"])
except anthropic.APIError as e:
print(f"Skipping {doc['title']}: {e}")
continue
for ent in result.entities:
raw_entities.append({**ent.model_dump(), "source_doc": doc["title"]})
for rel in result.relations:
raw_relations.append({**rel.model_dump(), "source_doc": doc["title"]})
print(
f"{doc['title']:<25} {len(result.entities):>3} entities {len(result.relations):>3} relations"
)
print(f"\nTotal: {len(raw_entities)} raw entities, {len(raw_relations)} raw relations")Apollo program 8 entities 7 relations
Apollo 11 6 entities 5 relations
Neil Armstrong 3 entities 2 relations
Saturn V 5 entities 4 relations
Buzz Aldrin 6 entities 6 relations
Kennedy Space Center 8 entities 10 relations
Total: 36 raw entities, 34 raw relations
```让我们看看提取了什么。注意同一个真实世界实体在不同文档中以不同的表面形式出现——这是我们接下来要解决的实体解析问题。
```html
::: callout-attention
请注意,以下代码块中的变量名和函数名将保持不变。
:::
```python
def process_data(data):
# 处理数据
pass
``````python
by_type = defaultdict(list)
for e in raw_entities:
by_type[e["type"]].append(e["name"])
for etype, names in sorted(by_type.items()):
print(f"{etype} ({len(names)}):")
for name in sorted(set(names)):
print(f" {name}")
print()
ARTIFACT (3):
Launch Complex 39
Saturn V
Skylab
EVENT (11):
Apollo 11
Apollo program
Gemini 12
Project Gemini
Project Mercury
Skylab program
Space Shuttle program
LOCATION (5):
Merritt Island
Moon
ORGANIZATION (7):
Cape Canaveral Space Force Station
John F. Kennedy Space Center
NASA
U.S. Congress
PERSON (10):
Buzz Aldrin
Dwight D. Eisenhower
Edwin Aldrin
Jim Lovell
John F. Kennedy
Michael Collins
Neil Alden Armstrong
Neil Armstrong
```## 实体消解
原始提取给我们带来了重叠的提及:“NASA”和“National Aeronautics and Space Administration”,“Neil Armstrong”和“Armstrong”,可能还有“the Moon”和“Moon”。如果我们直接从这个信息构建一个图,我们会得到一个破碎的混乱,其中相同的概念被分割在断开的节点之间。
传统方法使用字符串相似度(编辑距离,基于token的Jaccard)加上阻塞规则。这适用于拼写错误,但“Edwin Aldrin”与“Buzz Aldrin”则不适用——这两个名字没有字符重叠,但指的是同一个人。
我们相反要求Claude对每种类型的实体进行聚类,使用提取中的一行描述作为消歧上下文。描述很重要:“Armstrong — first person to walk on the Moon”和“Armstrong — jazz trumpeter”有相同的名字,但不应该合并。
::: callout-注意
请注意,实体消解是一个复杂的过程,需要仔细处理以避免错误合并或遗漏。
:::```python
class Cluster(BaseModel):
canonical: str
aliases: list[str]
class ResolvedClusters(BaseModel):
clusters: list[Cluster]
RESOLVE_PROMPT = """Below are {entity_type} entities extracted from several documents. Some are different surface forms of the same real-world entity.
<entities>
{entity_list}
</entities>
Cluster them. Each input name must appear in exactly one cluster's aliases list. Entities that are genuinely distinct get their own single-element cluster. Use the descriptions to avoid merging entities that merely share a name. The canonical name should be the most complete, unambiguous form."""
def resolve(entity_type: str, entities: list[dict]) -> list[Cluster]:
unique = {}
for e in entities:
unique.setdefault(e["name"], e["description"])
entity_list = "\n".join(f"- {name}: {desc}" for name, desc in unique.items())
response = client.messages.parse(
model=SYNTHESIS_MODEL,
max_tokens=2048,
messages=[
{
"role": "user",
"content": RESOLVE_PROMPT.format(entity_type=entity_type, entity_list=entity_list),
}
],
output_format=ResolvedClusters,
)
return response.parsed_output.clusters
```两个需要注意的故障模式。首先,Claude在每一个簇中遗漏的任何原始名称都会在图中静默消失,因为`alias_to_canonical`没有为其创建条目——一个生产解析器应该将未匹配的名称回退到单元素簇,以确保不会丢失任何信息。其次,解析器可能会**过度合并**:像“Gemini 12”这样的特定任务可能会被折叠到更广泛的“Gemini项目”中,因为描述重叠。第一个会丢失节点,第二个会丢失精度。两者都值得在下面的输出中进行抽查。
```markdown
Two failure modes to watch for. First, any raw name Claude leaves out of every cluster silently disappears from the graph, because `alias_to_canonical` has no entry for it — a production resolver should fall back to a single-element cluster for unmatched names so nothing is lost. Second, the resolver can **over-merge**: a specific mission like "Gemini 12" may get folded into the broader "Project Gemini" because the descriptions overlap. The first loses nodes, the second loses precision. Both are worth spot-checking in the output below.
``````python
alias_to_canonical = {}
canonical_info = {}
for etype in ENTITY_TYPES:
entities_of_type = [e for e in raw_entities if e["type"] == etype]
if not entities_of_type:
continue
try:
clusters = resolve(etype, entities_of_type)
except anthropic.APIError as e:
print(f"Resolve failed for {etype}: {e}; treating each name as its own cluster")
clusters = [
Cluster(canonical=n, aliases=[n]) for n in {x["name"] for x in entities_of_type}
]
for cluster in clusters:
canonical_info[cluster.canonical] = {"type": etype, "aliases": cluster.aliases}
for alias in cluster.aliases:
alias_to_canonical[alias] = cluster.canonical
before = len({e["name"] for e in raw_entities})
after = len(canonical_info)
print(f"Entity resolution: {before} unique names → {after} canonical entities\n")
for canonical, info in sorted(canonical_info.items()):
aliases = [a for a in info["aliases"] if a != canonical]
alias_str = f" (also: {', '.join(aliases)})" if aliases else ""
print(f"{info['type']:<14} {canonical}{alias_str}")
Entity resolution: 24 unique names → 22 canonical entities
EVENT Apollo 11
EVENT Apollo program
PERSON Buzz Aldrin (also: Edwin Aldrin)
ORGANIZATION Cape Canaveral Space Force Station
PERSON Dwight D. Eisenhower
EVENT Gemini 12
PERSON Jim Lovell
PERSON John F. Kennedy
ORGANIZATION John F. Kennedy Space Center
ARTIFACT Launch Complex 39
LOCATION Merritt Island
PERSON Michael Collins
LOCATION Moon
ORGANIZATION NASA
PERSON Neil Alden Armstrong (also: Neil Armstrong)
EVENT Project Gemini
EVENT Project Mercury
ARTIFACT Saturn V
ARTIFACT Skylab
EVENT Skylab program
EVENT Space Shuttle program
ORGANIZATION U.S. Congress
```## 组装图谱
使用干净的别名映射,我们将每个关系端点重写为其规范形式,并将结果加载到NetworkX中。我们使用`MultiDiGraph`,因为两个实体可以通过多个不同的谓词连接(例如“由...启动”和“由...运营”),并且方向很重要(“Armstrong指挥阿波罗11号”与“阿波罗11号指挥Armstrong”不是同一条边)。
每个节点携带其类型、提及该实体的文档集合以及提及次数。每条边携带其谓词和源文档。
- - - callout-attention - -
请注意,以下内容是技术术语,保持英文:
- RAG
- embedding
- token
- prompt
- Agent
- API
- Python
- JSON
- SQL
- - - callout-end - -```python
G = nx.MultiDiGraph()
for e in raw_entities:
canonical = alias_to_canonical.get(e["name"])
if canonical is None:
continue
if canonical not in G:
G.add_node(
canonical,
type=canonical_info[canonical]["type"],
description=e["description"],
source_docs=[],
mentions=0,
)
G.nodes[canonical]["source_docs"].append(e["source_doc"])
G.nodes[canonical]["mentions"] += 1
for r in raw_relations:
src = alias_to_canonical.get(r["source"])
tgt = alias_to_canonical.get(r["target"])
if src and tgt and src != tgt:
G.add_edge(src, tgt, predicate=r["predicate"], source_doc=r["source_doc"])
for n in G.nodes:
G.nodes[n]["source_docs"] = sorted(set(G.nodes[n]["source_docs"]))
print(f"Graph: {G.number_of_nodes()} nodes, {G.number_of_edges()} edges")
print(f"Connected components: {nx.number_weakly_connected_components(G)}")
print("\nMost connected entities:")
for node, deg in sorted(G.degree(), key=lambda x: -x[1])[:5]:
print(f" {node:<35} degree {deg:>2} ({G.nodes[node]['type']})")
Graph: 22 nodes, 34 edges
Connected components: 1
Most connected entities:
Apollo program degree 9 (EVENT)
Apollo 11 degree 9 (EVENT)
John F. Kennedy Space Center degree 7 (ORGANIZATION)
Neil Alden Armstrong degree 5 (PERSON)
Buzz Aldrin degree 5 (PERSON)
COLOR = {
"PERSON": "#4e79a7",
"ORGANIZATION": "#f28e2c",
"LOCATION": "#76b7b2",
"EVENT": "#e15759",
"ARTIFACT": "#af7aa1",
}
plt.figure(figsize=(14, 10))
pos = nx.spring_layout(G, k=1.5, seed=42)
node_colors = [COLOR[G.nodes[n]["type"]] for n in G.nodes]
node_sizes = [300 + 200 * G.degree(n) for n in G.nodes]
nx.draw_networkx_nodes(G, pos, node_color=node_colors, node_size=node_sizes, alpha=0.9)
nx.draw_networkx_labels(G, pos, font_size=8)
nx.draw_networkx_edges(G, pos, alpha=0.3, arrows=True, arrowsize=10)
handles = [
plt.Line2D([0], [0], marker="o", color="w", markerfacecolor=c, markersize=10, label=t)
for t, c in COLOR.items()
if any(G.nodes[n]["type"] == t for n in G.nodes)
]
plt.legend(handles=handles, loc="upper left")
plt.title("Apollo Program Knowledge Graph")
plt.axis("off")
plt.tight_layout()
plt.show()<Figure size 1400x1000 with 1 Axes>
```节点大小与度数成正比——中心节点是连接语料库的实体。颜色编码类型:如果你的图主要是单一颜色,你的语料库很窄;良好的混合意味着提取器正在找到完整的人员、地点和事物阵容。单个连通组件意味着实体解析已经完成了它的任务——破碎的岛屿将表明应该合并但未合并的变体。
## 实体摘要
每个节点目前只携带来自提及它的第一个文档的一行描述。对于中心节点——那些出现在许多文档中的节点——我们可以做得更好:汇集每个提及,添加图邻域作为上下文,并让Claude合成一个合适的简介。
这是将标签图转换为知识图的步骤。摘要成为你在搜索结果中呈现的节点内容或馈送到下游QA的内容。
```python
def extract_entities(document):
# 提取实体的代码
pass
{
"entities": [
{
"name": "Entity1",
"description": "A description of Entity1."
},
{
"name": "Entity2",
"description": "A description of Entity2."
}
]
}请注意,实体摘要的生成是一个复杂的过程,需要考虑上下文和相关性。 :::```python class TimeRange(BaseModel): start: str # YYYY or YYYY-MM, or “unknown” end: str # YYYY or YYYY-MM, or “ongoing”
class EntityProfile(BaseModel): summary: str key_facts: list[str] time_range: TimeRange
SUMMARIZE_PROMPT = “““Generate a knowledge-graph profile for this entity.
Entity: {name} ({etype})
Source excerpts mentioning this entity: {excerpts}
Known relations in the graph: {relations}
Write a 2-3 paragraph factual summary synthesized from the excerpts, resolving any contradictions by preferring the most specific claim. Include 3-5 atomic key facts, each traceable to the sources. For the time range, use YYYY or YYYY-MM format, or “unknown”/“ongoing” where appropriate. Do not invent facts not supported by the excerpts.”“”
def summarize_entity(name: str) -> EntityProfile: # Reads module-level G and documents built earlier in the notebook. docs_with_entity = G.nodes[name][“source_docs”] excerpts = “”.join( f”[{d[‘title’]}]” for d in documents if d[“title”] in docs_with_entity ) relations = ( “”.join( f”- {name} –{d[‘predicate’]}–> {tgt}” for , tgt, d in G.out_edges(name, data=True) ) + “” + “”.join( f”- {src} –{d[‘predicate’]}–> {name}” for src, , d in G.in_edges(name, data=True) ) )
response = client.messages.parse(
model=SYNTHESIS_MODEL,
max_tokens=1500,
messages=[
{
"role": "user",
"content": SUMMARIZE_PROMPT.format(
name=name, etype=G.nodes[name]["type"], excerpts=excerpts, relations=relations
),
}
],
output_format=EntityProfile,
)
return response.parsed_output
```python
hub_nodes = [n for n, _ in sorted(G.degree(), key=lambda x: -x[1])[:3]]
for node in hub_nodes:
profile = summarize_entity(node)
G.nodes[node]["profile"] = profile.model_dump()
print(f"═══ {node} ═══")
print(profile.summary)
print(f"\nTime range: {profile.time_range.start} – {profile.time_range.end}")
print("Key facts:")
for fact in profile.key_facts:
print(f" • {fact}")
print()
═══ Apollo program ═══
The Apollo program, also known as Project Apollo, was a United States human spaceflight program led by NASA, primarily aimed at landing humans on the Moon. It was conceived in 1960 during the presidency of Dwight D. Eisenhower as a three-person spacecraft program, building upon the foundations laid by Project Mercury. The program was later formally dedicated to President John F. Kennedy's national goal, announced before the U.S. Congress on May 25, 1961, of landing a man on the Moon and returning him safely to Earth. Apollo followed Project Gemini in NASA's sequential human spaceflight efforts.
The Apollo program successfully landed the first humans on the Moon in 1969. It relied on the Saturn V, a super heavy-lift launch vehicle developed by NASA specifically for the program, which flew from 1967 to 1973 and supported nine crewed flights to the Moon. Launch operations were conducted from Launch Complex 39 at Kennedy Space Center on Merritt Island, Florida, which has served as NASA's primary launch center since 1968.
Time range: 1960 – 1973
Key facts:
• The Apollo program was conceived in 1960 under the presidency of Dwight D. Eisenhower as a three-person spacecraft program.
• Apollo was dedicated to President John F. Kennedy's goal of landing a man on the Moon, as stated in his address to Congress on May 25, 1961.
• The Apollo program successfully landed the first humans on the Moon in 1969.
• The Saturn V rocket, developed under the Apollo program, flew from 1967 to 1973 and supported nine crewed lunar flights.
• Launch operations for the Apollo program were carried out from Kennedy Space Center Launch Complex 39.
═══ Apollo 11 ═══
Apollo 11 was the American spaceflight that achieved the historic first crewed lunar landing, representing the fifth crewed mission of NASA's Apollo program. The mission was commanded by Neil Armstrong, with Michael Collins serving as Command Module Pilot and Edwin "Buzz" Aldrin as Lunar Module Pilot. All three crew members were on their second and final spaceflights.
During the mission, Neil Armstrong became the first person to walk on the Moon, followed by Buzz Aldrin, making him the second. Michael Collins remained in the Command Module in lunar orbit. The mission stands as a landmark achievement in human spaceflight history. As of 2025, Buzz Aldrin is the last surviving crew member of Apollo 11, following the deaths of Armstrong in 2012 and Collins in 2021.
Time range: 1969 – 1969
Key facts:
• Apollo 11 was the fifth crewed mission of NASA's Apollo program and the first to land humans on the Moon.
• The crew consisted of Commander Neil Armstrong, Command Module Pilot Michael Collins, and Lunar Module Pilot Buzz Aldrin.
• Neil Armstrong was the first person to walk on the Moon, with Buzz Aldrin becoming the second.
• All three Apollo 11 crew members were on their second and final spaceflights.
• Buzz Aldrin is the last surviving member of the Apollo 11 crew, following the deaths of Armstrong in 2012 and Collins in 2021.
═══ John F. Kennedy Space Center ═══
The John F. Kennedy Space Center (KSC) is a NASA field center situated on Merritt Island, Florida, on the east coast of the state. It is one of ten field centers operated by NASA and has served as the agency's primary launch center for American spaceflight, research, and technology since 1968. Launch operations for several major human spaceflight programs, including Apollo, Skylab, and the Space Shuttle, were conducted from KSC's Launch Complex 39 and managed by the center.
KSC operates in close proximity to Cape Canaveral Space Force Station (CCSFS), with the two entities maintaining a collaborative relationship. Their management teams work closely together, share resources, and operate facilities on each other's property, reflecting a high degree of integration between the two installations.
Time range: 1968 – ongoing
Key facts:
• John F. Kennedy Space Center is located on Merritt Island, Florida, and is one of ten NASA field centers.
• KSC has been NASA's primary launch center for American spaceflight, research, and technology since 1968.
• Launch operations for the Apollo, Skylab, and Space Shuttle programs were carried out from KSC's Launch Complex 39.
• KSC is adjacent to Cape Canaveral Space Force Station, and the two entities share resources and operate facilities on each other's property.
```## 查询图谱
构建知识图谱的回报是多跳推理:回答那些需要将从未在同一文档中共同出现的事实链式连接起来的问题。“哪些地点与乘坐阿波罗11号飞行的人有关联?”这个问题需要提取器在一个文档中找到人→任务边,在另一个文档中找到人→地点边,然后解析器需要统一人节点,这样那些边实际上才能相遇。
我们将相关的子图序列化为三元组,并让Claude进行推理。为了比较,我们首先在没有图上下文的情况下提出相同的问题。
- - - callout-info
**注意**:在多跳推理中,需要确保提取器和解析器之间的正确交互,以实现有效的知识图谱查询。
- - -```python
def serialize_subgraph(center: str, hops: int = 2) -> str:
nodes = {center}
frontier = {center}
for _ in range(hops):
nxt = set()
for n in frontier:
nxt |= set(G.successors(n)) | set(G.predecessors(n))
frontier = nxt - nodes
nodes |= frontier
sub = G.subgraph(nodes)
lines = [f"({s}) --[{d['predicate']}]--> ({t})" for s, t, d in sub.edges(data=True)]
return "\n".join(sorted(set(lines)))
def ask(question: str, graph_context: str | None = None) -> str:
if graph_context is not None:
prompt = f"""Answer using only the knowledge graph below. Cite the specific edges that support your answer.
<graph>
{graph_context}
</graph>
Question: {question}"""
else:
prompt = question
response = client.messages.create(
model=SYNTHESIS_MODEL,
max_tokens=500,
messages=[{"role": "user", "content": prompt}],
)
text_block = next((b for b in response.content if b.type == "text"), None)
if text_block is None:
raise ValueError(f"No text block in response (stop_reason={response.stop_reason})")
return text_block.text
center = next((n for n in G.nodes if "Apollo" in n), hub_nodes[0])
print(f"Querying 2-hop neighborhood of: {center}\n")
subgraph = serialize_subgraph(center, hops=2)
question = "Which locations are connected to people who were part of Apollo 11, and how?"
print("WITHOUT graph context:")
print(ask(question))
print("\n" + "─" * 60 + "\n")
print("WITH graph context:")
print(ask(question, subgraph))Querying 2-hop neighborhood of: Apollo program
WITHOUT graph context:
# Locations Connected to Apollo 11 Personnel
## Neil Armstrong (Commander)
- **Wapakoneta, Ohio** – birthplace and hometown; the **Neil Armstrong Air & Space Museum** is located there
- **Purdue University, Indiana** – where he studied aeronautical engineering
- **Edwards Air Force Base, California** – where he worked as a test pilot
- **Cincinnati, Ohio** – where he lived later in life and died (2012)
## Buzz Aldrin (Lunar Module Pilot)
- **Montclair, New Jersey** – birthplace
- **West Point, New York** – attended the U.S. Military Academy
- **MIT, Massachusetts** – earned his doctorate in astronautics
- **Los Angeles area** – longtime residence after NASA career
## Michael Collins (Command Module Pilot)
- **Rome, Italy** – birthplace (son of a U.S. Army officer)
- **Washington, D.C.** – attended school; later directed the **Smithsonian National Air and Space Museum**
- **West Point** – attended the Military Academy
## Shared/Mission Locations
- **Kennedy Space Center, Florida** – launch site (July 16, 1969)
- **Houston, Texas** – Mission Control and astronaut training (Johnson Space Center)
- **Pacific Ocean** – splashdown and recovery site
- **Honolulu, Hawaii** – where the crew was initially quarantined aboard the USS Hornet
Would you like more detail on any individual?
────────────────────────────────────────────────────────────
WITH graph context:
## Locations Connected to Apollo 11 Personnel
Based on the knowledge graph, here is what can be determined about people connected to Apollo 11 and their location relationships:
### Neil Alden Armstrong
Neil Armstrong is the person directly identified as part of Apollo 11, supported by:
- **(Apollo 11) --[commanded by]--> (Neil Alden Armstrong)**
- **(Neil Alden Armstrong) --[commanded]--> (Apollo 11)**
He is connected to the following location:
#### 🌕 The Moon
- **(Neil Alden Armstrong) --[walked on]--> (Moon)**
This is the only **direct** person-to-location edge in the graph for an Apollo 11 crew member.
---
### Indirect Location Connection via Apollo 11 itself:
Apollo 11 as a mission is also connected to the Moon:
- **(Apollo 11) --[landed on]--> (Moon)**
---
### Limitations
The knowledge graph does **not** include location data for any other Apollo 11 crew members (e.g., Buzz Aldrin or Michael Collins), so no connections can be drawn for them. The only person-location relationship supported by the graph is **Neil Armstrong → walked on → the Moon**.
```未接地答案借鉴了Claude的预训练,可能是正确的——阿波罗11号是著名的。但接地答案是**可追溯的**:每个主张都引用了我们从一个特定文档中提取的边缘。在一个Claude没有先验知识的私有语料库中,只有接地答案才能完全起作用。
## 评估
知识图谱的质量是通过与黄金集的精确度和召回率来衡量的。我们在`data/sample_triples.json`中提供了一个小型人工标注集,覆盖了两篇文章,以及`data/alias_map.json`,它将表面形式的变化归一化到黄金名称,以便“月球”和“Moon”被视为相同的命中。
下面的检查并列评分两项内容:原始提取器输出,以及通过“alias_to_canonical”步骤后的相同实体。当解析器选择解析器所知的规范形式(或与黄金文本完全匹配的形式)时,解析召回率会上升。当它选择解析器未覆盖的详细形式——比如“尼尔·阿姆斯特朗”时,解析召回率可能会*下降*,因为解析前匹配黄金文本的名字在解析后不再匹配。这并不是解析器的错误;这是一个评分伪象。修复方法是,每当您看到评分器不认识的规范形式时,就扩展`alias_map.json`。
此单元格仅评分实体。独立的脚本还评分关系,通过(源,目标)对匹配,忽略谓词措辞——因此其关系召回率是一个上限。从存储库根目录运行它:
```python
# 代码示例
``````bash
uv run python capabilities/knowledge_graph/evaluation/eval_extraction.py
# Expects the kernel launched from this notebook's directory
# (capabilities/knowledge_graph/). Falls back to repo root.
data_dir = Path("data")
if not data_dir.exists():
data_dir = Path("capabilities/knowledge_graph/data")
with open(data_dir / "sample_triples.json", encoding="utf-8") as f:
gold = json.load(f)
with open(data_dir / "alias_map.json", encoding="utf-8") as f:
ALIASES = json.load(f)
def norm(name: str) -> str:
lower = name.lower().strip()
return ALIASES.get(lower, lower)
def prf(predicted: set, gold: set) -> tuple[float, float, float]:
tp = len(predicted & gold)
p = tp / len(predicted) if predicted else 0.0
r = tp / len(gold) if gold else 0.0
f1 = 2 * p * r / (p + r) if (p + r) else 0.0
return p, r, f1
print("Raw extraction vs resolved-graph recall against gold:\n")
for doc_title, labels in gold.items():
gold_names = {norm(e["name"]) for e in labels["entities"]}
raw = {norm(e["name"]) for e in raw_entities if e["source_doc"] == doc_title}
rp, rr, rf = prf(raw, gold_names)
resolved = {
norm(alias_to_canonical.get(e["name"], e["name"]))
for e in raw_entities
if e["source_doc"] == doc_title
}
_, resolved_r, _ = prf(resolved, gold_names)
print(f"{doc_title:<20} raw F1={rf:.2f} (P={rp:.2f} R={rr:.2f}) resolved R={resolved_r:.2f}")
missed = gold_names - resolved
if missed:
print(f" still missed after resolution: {', '.join(sorted(missed))}")``` Raw extraction vs resolved-graph recall against gold:
Apollo 11 raw F1=0.71 (P=1.00 R=0.55) resolved R=0.55 still missed after resolution: apollo program, columbia, kennedy space center, lunar module eagle, saturn v Neil Armstrong raw F1=0.55 (P=1.00 R=0.38) resolved R=0.38 still missed after resolution: gemini 8, korean war, nasa, purdue university, united states navy ```## 规模化
This notebook processed six documents in memory. Production knowledge graphs are built from thousands. A few notes on scaling:
Extraction cost. Haiku is cheap enough to run on large corpora, but prompt caching cuts costs further when your extraction schema and instructions stay fixed — cache the system prompt and schema, pay full price only for the document text. The Message Batches API gives 50% off for jobs that can wait up to 24 hours.
Entity resolution at scale. Feeding ten thousand PERSON entities to Claude in one prompt doesn’t work. Block first: group candidates by cheap signals (same last name, overlapping tokens, embedding similarity) so Claude only arbitrates within small blocks. The resolution prompt above works unchanged on blocks of 50–100.
Incremental updates. When a new document arrives, extract its entities, resolve them against the existing canonical set (not against each other), and add only the new edges. Re-summarize an entity only when its source-document set changes materially.
Storage. NetworkX is fine to a few hundred thousand edges. Beyond that, the schema maps directly onto a property graph (Neo4j, Neptune) or three Postgres tables: entities(id, name, type, summary), relations(source_id, target_id, predicate), aliases(entity_id, alias). The extraction and resolution code doesn’t change — only the persistence layer does.
总结
You’ve built a complete knowledge graph pipeline with nothing but prompts:
- Extraction — a single structured-output call per document replaced what used to be a trained NER model plus a trained relation classifier. The Pydantic schema is the only “training” required.
- 分辨率 — Claude使用提取描述作为上下文,对表面形式进行聚类,捕捉到如“Edwin Aldrin” → “Buzz Aldrin”这样的字符串相似度完全忽略的案例。
- 摘要 — 中心节点获得了丰富的配置文件,这些配置文件综合了所有提及它们的文档,包括结构化的时间范围和可追溯的关键事实。
- 查询 — 序列化子图让Claude能够通过边缘级别的引用回答多跳问题,将答案建立在您构建的图中,而不是预训练中。
evaluation/ 目录中的评估工具为您提供了一个反馈循环:更改提取提示,重新运行评分器,观察F1分数的变化。这个循环是将演示变成生产系统的关键。
相关教程
- 提取结构化JSON — 相同提取模式的工具使用方法,当您已经在工具调用流程中时很有用
- 检索增强生成 — 当您需要文档检索而不是事实遍历时使用的补充方法
- 上下文嵌入 — 在嵌入之前丰富块,将“在索引之前添加上下文”的想法应用于向量搜索
中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。