1 LLMGraphTransformer: 문서 → 지식 그래프
1.1 핵심 아이디어
"일론 머스크는 2003년 테슬라를 설립했고, 테슬라는 텍사스 오스틴에 본사를 두고 있다."
│
▼ LLMGraphTransformer
│
├── 노드: Person(Elon Musk), Company(Tesla), Location(Austin)
└── 관계: FOUNDED(year=2003), LOCATED_IN
LLM이 텍스트를 읽고 누가, 무엇을, 어떤 관계로 연결되어 있는지 추출한다.
1.2 기본 사용법
from langchain_experimental.graph_transformers import LLMGraphTransformer
from langchain_openai import ChatOpenAI
from langchain_core.documents import Document
from langchain_neo4j import Neo4jGraph
# LLM 설정 (GPT-4o 권장: 관계 추출 정확도 높음)
llm = ChatOpenAI(model="gpt-4o", temperature=0)
# Transformer 초기화
transformer = LLMGraphTransformer(llm=llm)
# 문서 준비
docs = [
Document(page_content="""
Elon Musk founded Tesla in 2003. Tesla is headquartered in Austin, Texas.
Musk also founded SpaceX in 2002, which is located in Hawthorne, California.
Before that, Musk co-founded PayPal with Peter Thiel in 1998.
"""),
]
# 그래프 문서로 변환
graph_docs = transformer.convert_to_graph_documents(docs)
# 결과 확인
for gd in graph_docs:
print("=== 노드 ===")
for node in gd.nodes:
print(f" {node.id} ({node.type}): {node.properties}")
print("=== 관계 ===")
for rel in gd.relationships:
print(f" ({rel.source.id})-[:{rel.type}]->({rel.target.id})")
if rel.properties:
print(f" properties: {rel.properties}")출력:
=== 노드 ===
Elon Musk (Person): {}
Tesla (Company): {}
Austin (Location): {}
SpaceX (Company): {}
Hawthorne (Location): {}
Peter Thiel (Person): {}
PayPal (Company): {}
=== 관계 ===
(Elon Musk)-[:FOUNDED]->(Tesla)
(Tesla)-[:HEADQUARTERED_IN]->(Austin)
(Elon Musk)-[:FOUNDED]->(SpaceX)
(SpaceX)-[:LOCATED_IN]->(Hawthorne)
(Elon Musk)-[:CO_FOUNDED]->(PayPal)
(Peter Thiel)-[:CO_FOUNDED]->(PayPal)
1.3 Neo4j에 저장
graph = Neo4jGraph(
url="bolt://localhost:7687",
username="neo4j",
password="password"
)
# 그래프 문서를 Neo4j에 저장
graph.add_graph_documents(
graph_docs,
baseEntityLabel=True, # 모든 노드에 __Entity__ 레이블 추가 (검색 편의)
include_source=True, # 원본 문서 노드도 함께 저장
)
# 저장된 데이터 확인
result = graph.query("""
MATCH (n)-[r]->(m)
RETURN labels(n)[0] AS from_type, n.id AS from,
type(r) AS relation,
labels(m)[0] AS to_type, m.id AS to
LIMIT 20
""")
for row in result:
print(f"({row['from_type']}:{row['from']})-[:{row['relation']}]->({row['to_type']}:{row['to']})")1.4 스키마 정의: 추출할 타입 제한
스키마를 정의하지 않으면 LLM이 임의의 타입을 생성하여 일관성이 떨어진다. 도메인에 맞는 타입을 미리 정의하는 것이 권장된다.
transformer = LLMGraphTransformer(
llm=llm,
# 허용할 노드 타입
allowed_nodes=["Person", "Company", "Location", "Product", "Technology"],
# 허용할 관계 타입
allowed_relationships=[
"FOUNDED",
"WORKS_AT",
"LOCATED_IN",
"ACQUIRED",
"INVESTED_IN",
"DEVELOPED",
"COMPETES_WITH",
],
# 노드 속성 추출 여부
node_properties=True,
# 관계 속성 추출 여부
relationship_properties=True,
)스키마 정의의 중요성:
스키마 없음:
TESLA_FOUNDED_BY, IS_CEO_OF, HEADQUARTERED_IN, BASED_AT
→ 같은 의미가 다른 타입으로 저장 → 탐색 불가
스키마 있음:
FOUNDED, LOCATED_IN
→ 일관된 관계 타입 → 예측 가능한 탐색
1.5 대규모 문서 배치 처리
import time
from tqdm import tqdm
def process_documents_batch(docs, transformer, graph, batch_size=10):
"""대규모 문서를 배치로 처리."""
all_graph_docs = []
for i in tqdm(range(0, len(docs), batch_size)):
batch = docs[i:i + batch_size]
try:
graph_docs = transformer.convert_to_graph_documents(batch)
graph.add_graph_documents(
graph_docs,
baseEntityLabel=True,
include_source=True,
)
all_graph_docs.extend(graph_docs)
except Exception as e:
print(f"배치 {i//batch_size} 처리 실패: {e}")
continue
# API 레이트 리밋 방지
time.sleep(1)
return all_graph_docs
# 실행
from langchain_community.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 문서 로드 & 청크 분할
loader = TextLoader("data/company_news.txt")
raw_docs = loader.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=100)
chunks = splitter.split_documents(raw_docs)
print(f"총 {len(chunks)}개 청크 처리 예정")
graph_docs = process_documents_batch(chunks, transformer, graph)1.6 비용 추정
LLMGraphTransformer는 문서당 LLM 호출이 1~2번 발생한다.
GPT-4o 기준:
Input: $5 / 1M tokens
Output: $15 / 1M tokens
문서 1개 (1000 tokens 기준):
≈ $0.005 ~ $0.015
문서 1000개:
≈ $5 ~ $15
문서 100만 개 (Wikipedia):
≈ $5,000 ~ $15,000
비용 절감 방안: - gpt-4o-mini 사용 (약 10배 저렴, 정확도 약간 낮음) - allowed_nodes, allowed_relationships 로 추출 범위 제한 - 청크 크기 최적화 (너무 작으면 컨텍스트 손실, 너무 크면 비용 증가)
1.7 엔티티 해결 (Entity Resolution)
같은 엔티티가 다른 이름으로 등장하는 문제를 해결한다.
"Elon Musk" = "Elon R. Musk" = "머스크" = "musk"
"Tesla" = "Tesla Motors" = "TSLA"
1.7.1 방법 1: MERGE로 중복 방지
1.7.2 방법 2: LLM에게 표준화 요청
1.7.3 방법 3: 사후 APOC 처리
// 유사한 노드 병합 (APOC 필요)
MATCH (n:Person)
WITH n.name AS name, collect(n) AS nodes
WHERE size(nodes) > 1
CALL apoc.refactor.mergeNodes(nodes, {properties: 'combine'})
YIELD node
RETURN node1.8 구축된 그래프 검증
# 전체 통계
stats = graph.query("""
MATCH (n)
RETURN labels(n)[0] AS type, count(n) AS count
ORDER BY count DESC
""")
print("=== 노드 통계 ===")
for s in stats: print(f" {s['type']}: {s['count']}개")
rel_stats = graph.query("""
MATCH ()-[r]->()
RETURN type(r) AS type, count(r) AS count
ORDER BY count DESC
""")
print("\n=== 관계 통계 ===")
for s in rel_stats: print(f" {s['type']}: {s['count']}개")
# 연결도 높은 노드 (허브 노드)
hubs = graph.query("""
MATCH (n)
WITH n, size((n)-[]-()) AS degree
ORDER BY degree DESC
LIMIT 10
RETURN labels(n)[0] AS type, n.id AS name, degree
""")
print("\n=== 허브 노드 TOP 10 ===")
for h in hubs: print(f" {h['type']}:{h['name']} - {h['degree']}개 연결")1.9 정리
LLMGraphTransformer 파이프라인:
문서 → convert_to_graph_documents() → GraphDocument
↓
graph.add_graph_documents()
↓
Neo4j: 노드 + 관계 저장
핵심 파라미터:
allowed_nodes = ["Person", "Company", ...]
allowed_relationships = ["FOUNDED", "WORKS_AT", ...]
node_properties = True
relationship_properties = True
주의사항:
- 스키마 정의 필수 (없으면 일관성 깨짐)
- 대규모 처리 시 배치 + 레이트 리밋 처리
- Entity Resolution 전략 수립 필요
다음 파일에서는 Neo4j에 벡터 인덱스를 추가하여 의미적 검색을 구현한다.