지식 그래프를 활용한 RAG 추론 향상
작성자: Diego Carpintero, 번역: 최성우
지식 그래프는 상호 연결된 정보를 사람과 기계 모두가 이해할 수 있는 형식으로 모델링하고 저장하는 방법을 제공합니다. 이러한 그래프는 엔티티와 그들의 관계를 나타내는 노드와 엣지로 구성됩니다. 전통적인 데이터베이스와 달리, 그래프의 본질적인 표현력은 더 풍부한 의미론적 이해를 가능하게 하며, 고정된 스키마에 제약받지 않고 새로운 엔티티 유형과 관계를 수용할 수 있는 유연성을 제공합니다.
지식 그래프를 임베딩(벡터 검색)과 결합함으로써, 다중 홉 연결성과 정보의 맥락적 이해를 활용하여 LLM의 추론과 설명 가능성을 향상시킬 수 있습니다.
이 노트북은 이 접근 방식의 실제 구현을 탐구하며, 다음과 같은 방법을 보여줍니다.
- 합성 데이터셋을 사용하여 Neo4j에서 연구 출판물 관련 지식 그래프 구축
- 임베딩 모델을 사용하여 데이터 필드의 일부를 고차원 벡터 공간으로 투영
- 유사도 검색을 가능하게 하는 임베딩에 대한 벡터 인덱스 구축
- LangChain을 사용하여 사용자 쿼리를 cypher 문으로 쉽게 변환하여 자연어로 그래프에서 인사이트 추출
초기화
%pip install neo4j langchain langchain_openai langchain-community python-dotenv --quiet
Neo4j 인스턴스 설정
그래프 데이터베이스 기술을 전문으로 하는 오픈 소스 데이터베이스 관리 시스템인 Neo4j를 사용하여 지식 그래프를 생성할 것입니다.
빠르고 쉬운 설정을 위해 Neo4j Aura에서 무료 인스턴스를 시작할 수 있습니다.
그런 다음 .env
파일을 사용하여 NEO4J_URI
, NEO4J_USERNAME
, 및 NEO4J_PASSWORD
를 환경 변수로 설정할 수 있습니다.
import dotenv
dotenv.load_dotenv(".env", override=True)
Langchain은 Neo4j와 상호 작용하기 위한 Neo4jGraph
클래스를 제공합니다.
import os
from langchain_community.graphs import Neo4jGraph
graph = Neo4jGraph(
url=os.environ["NEO4J_URI"],
username=os.environ["NEO4J_USERNAME"],
password=os.environ["NEO4J_PASSWORD"],
)
데이터셋을 그래프로 로딩
아래 예제는 Neo4j
데이터베이스와 연결을 생성하고 연구 논문과 저자로 구성된 합성 데이터로 채웁니다.
엔티티는 다음과 같습니다.
- 연구원
- 논문
- 주제
관계는 다음과 같습니다.
- 연구원 —[출판하다]—> 논문
- 논문 —[주제이다]—> 주제
from langchain_community.graphs import Neo4jGraph
graph = Neo4jGraph()
q_load_articles = """
LOAD CSV WITH HEADERS
FROM 'https://raw.githubusercontent.com/dcarpintero/generative-ai-101/main/dataset/synthetic_articles.csv'
AS 행
FIELDTERMINATOR ';'
MERGE (a:논문 {제목:행.Title})
SET a.초록 = 행.Abstract,
a.출판일 = date(행.Publication_Date)
FOREACH (연구자 in split(행.Authors, ',') |
MERGE (p:연구원 {이름:trim(연구자)})
MERGE (p)-[:출판하다]->(a))
FOREACH (주제 in [행.Topic] |
MERGE (t:주제 {이름:trim(주제)})
MERGE (a)-[:주제이다]->(t))
"""
graph.query(q_load_articles)
노드와 관계가 올바르게 초기화되었는지 확인해 봅시다.
>>> graph.refresh_schema()
>>> print(graph.get_schema)
Node properties: 논문 {제목: STRING, 초록: STRING, 출판일: DATE, embedding: LIST} 연구원 {이름: STRING} 주제 {이름: STRING} Relationship properties: The relationships: (:논문)-[:주제이다]->(:주제) (:연구원)-[:출판하다]->(:논문)
지식 그래프는 Neo4j 작업 공간에서 검사할 수 있습니다.
벡터 인덱스 구축
이제 주제, 제목, 초록을 기반으로 관련 논문을 효율적으로 검색하기 위한 벡터 인덱스를 구축합니다. 이 과정에는 이러한 필드를 사용하여 각 논문의 임베딩을 계산하는 것이 포함됩니다. 쿼리 시, 시스템은 코사인 거리와 같은 유사도 메트릭을 사용하여 사용자 입력과 가장 유사한 논문을 찾습니다.
from langchain_community.vectorstores import Neo4jVector
from langchain_openai import OpenAIEmbeddings
vector_index = Neo4jVector.from_existing_graph(
OpenAIEmbeddings(),
url=os.environ["NEO4J_URI"],
username=os.environ["NEO4J_USERNAME"],
password=os.environ["NEO4J_PASSWORD"],
index_name="논문",
node_label="논문",
text_node_properties=["주제", "제목", "초록"],
embedding_node_property="embedding",
)
참고: OpenAI 임베딩 모델에 접근하려면 OpenAI 계정을 만들고 API 키를 받아 OPENAI_API_KEY
를 환경 변수로 설정해야 합니다. 다른 임베딩 모델 통합을 실험해 보는 것도 유용할 수 있습니다.
유사도 기반 Q&A
Langchain RetrievalQA
는 위의 벡터 인덱스를 검색기로 사용하는 질문-답변(QA) 체인을 생성합니다.
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI
vector_qa = RetrievalQA.from_chain_type(llm=ChatOpenAI(), chain_type="stuff", retriever=vector_index.as_retriever())
’AI가 우리의 일상 생활에 어떤 영향을 미칠 수 있는지 논의하는 논문은 어떤 것들이 있나요?‘라고 물어봅시다.
>>> r = vector_qa.invoke(
... {
... "query": "AI가 우리의 일상 생활에 어떤 영향을 미칠 수 있는지 논의하는 논문은 어떤 것들이 있나요? 논문 제목과 초록을 포함해서 말해주세요."
... }
... )
>>> print(r["result"])
주제: AI의 영향 1. 제목: The Impact of AI on Employment: A Comprehensive Study - 초록: 이 연구는 AI가 다양한 직업 부문에 미칠 수 있는 잠재적 영향을 분석하고 부정적 영향을 완화하기 위한 정책 권고를 제안합니다. 2. 제목: The Societal Implications of Advanced AI: A Multidisciplinary Analysis - 초록: 우리의 연구는 다양한 분야의 전문가들을 모아 고급 AI가 사회, 경제, 문화에 미칠 잠재적인 장기적 영향을 분석합니다. 3. 제목: The Role of AI in Combating Climate Change: Opportunities and Challenges - 초록: 우리의 연구는 AI가 기후 변화 대처에 어떻게 활용될 수 있는지 탐구하며 잠재적 이점과 관련된 윤리적 고려사항을 논의합니다. 4. 제목: Ethical Considerations in AI Development - 초록: 우리는 고속으로 진행되고 있는 AI 발전의 윤리적 함의를 탐구하고 책임있는 개발을 위한 지침을 제안합니다.
추론을 위한 지식 그래프 탐색
지식 그래프는 엔티티 간의 연결을 만드는 데 탁월하여 패턴을 추출하고 새로운 인사이트를 발견할 수 있게 합니다.
이 섹션에서는 이 프로세스를 구현하고 자연어 쿼리를 사용하여 LLM 파이프라인에 결과를 통합하는 방법을 보여줍니다.
LangChain을 사용한 그래프 Cypher 체인
표현력 있고 효율적인 쿼리를 구성하기 위해 Neo4j
는 SQL에서 영감을 받은 선언적 쿼리 언어인 Cypher
를 사용합니다. LangChain
은 GraphCypherQAChain
래퍼를 제공하여 자연어를 사용해 그래프 데이터베이스를 쿼리할 수 있게 하는 추상화 계층을 제공하여 LLM 파이프라인에 그래프 기반 데이터 검색을 더 쉽게 통합할 수 있게 합니다.
실제로 GraphCypherQAChain
은:
- 맥락 내 학습(프롬프트 엔지니어링)을 적용하여 사용자 입력(자연어)에서 Cypher 문(Neo4j와 같은 그래프 데이터베이스용 쿼리)을 생성하고,
- 해당 문을 그래프 데이터베이스에 대해 실행하며,
- LLM 응답을 정확하고 최신 정보로 뒷받침하기 위해 결과를 컨텍스트로 제공합니다.
참고: 이 구현에는 모델이 생성한 그래프 쿼리를 실행하는 것이 포함되어 있어, 데이터베이스의 민감한 데이터에 대한 의도하지 않은 접근이나 수정과 같은 고유한 위험이 있습니다. 이러한 위험을 완화하기 위해 데이터베이스 연결 권한을 체인/에이전트의 특정 요구 사항을 충족하는 데 필요한 최소한으로 제한해야 합니다. 이 접근 방식은 위험을 줄이지만 완전히 제거하지는 않습니다.
from langchain.chains import GraphCypherQAChain
from langchain_openai import ChatOpenAI
graph.refresh_schema()
cypher_chain = GraphCypherQAChain.from_llm(
cypher_llm=ChatOpenAI(temperature=0, model_name="gpt-4o"),
qa_llm=ChatOpenAI(temperature=0, model_name="gpt-4o"),
graph=graph,
verbose=True,
)
자연어를 사용한 쿼리 예시
다음 예제에서 cypher 쿼리 실행 결과가 LLM에 컨텍스트로 제공되는 방식에 주목하세요.
” Emily Chen이 발표한 논문은 몇 편인가요? ”
이 예제에서 우리의 질문 ’Emily Chen이 발표한 논문은 몇 편인가요?‘는 다음과 같은 Cypher 쿼리로 변환됩니다.
MATCH (:연구원 {이름: "Emily Chen"})-[:출판하다]->(n:논문)
RETURN COUNT(n) AS 논문_수
이 쿼리는 이름이 ‘Emily Chen’인 연구원
레이블이 있는 노드를 매칭하고 출판하다
관계를 통해 논문
노드로 이동합니다. 그런 다음 ‘Emily Chen’과 연결된 논문
노드의 수를 계산합니다.
>>> # 답은 '7'이어야 합니다.
>>> cypher_chain.invoke({"query": "Emily Chen이 발표한 논문은 몇 편인가요?"})
[1m> Entering new GraphCypherQAChain chain...[0m Generated Cypher: [32;1m[1;3mcypher MATCH (:연구원 {이름: "Emily Chen"})-[:출판하다]->(n:논문) RETURN COUNT(n) AS 논문_수 [0m Full Context: [32;1m[1;3m[{'논문_수': 7}][0m [1m> Finished chain.[0m
” 3편보다 많은 논문을 함께 발표한 연구원 쌍을 알려주세요. ”
이 예제에서 ’3편보다 많은 논문을 함께 발표한 연구원 쌍을 알려주세요.‘라는 쿼리는 다음과 같은 Cypher 쿼리로 변환됩니다.
MATCH (r1:연구원)-[:출판하다]->(p:논문)<-[:출판하다]-(r2:연구원)
WHERE r1 <> r2
WITH r1, r2, COUNT(p) AS 논문수
WHERE 논문수 > 3
RETURN r1.이름 AS 연구원1, r2.이름 AS 연구원2, 논문수
이 쿼리는 연구원
노드에서 출판하다
관계를 통해 연결된 논문
노드를 찾고, 다시 연구원
쌍을 찾기 위해 역으로 이동합니다.
>>> # 답은 David Johnson & Emily Chen, Robert Taylor & Emily Chen이어야 합니다.
>>> cypher_chain.invoke({"query": "3편보다 많은 논문을 함께 발표한 연구원 쌍을 알려주세요."})
[1m> Entering new GraphCypherQAChain chain...[0m Generated Cypher: [32;1m[1;3mcypher MATCH (r1:연구원)-[:출판하다]->(p:논문)<-[:출판하다]-(r2:연구원) WHERE r1 <> r2 WITH r1, r2, COUNT(p) AS 논문수 WHERE 논문수 > 3 RETURN r1.이름 AS 연구원1, r2.이름 AS 연구원2, 논문수 [0m Full Context: [32;1m[1;3m[{'연구원1': 'David Johnson', '연구원2': 'Emily Chen', '논문수': 4}, {'연구원1': 'Robert Taylor', '연구원2': 'Emily Chen', '논문수': 4}, {'연구원1': 'Emily Chen', '연구원2': 'David Johnson', '논문수': 4}, {'연구원1': 'Emily Chen', '연구원2': 'Robert Taylor', '논문수': 4}][0m [1m> Finished chain.[0m
” 가장 많은 동료와 협업한 연구원은 누구인가요? ”
가장 많은 동료와 협업한 연구자를 찾아봅시다. 우리의 쿼리 ’가장 많은 동료와 협업한 연구원은 누구인가요?‘는 이제 다음과 같은 Cypher 쿼리로 변환됩니다.
MATCH (r:연구원)-[:출판하다]->(:논문)<-[:출판하다]-(coauthor:연구원)
WHERE r <> coauthor
RETURN r.이름 AS 연구원, COUNT(DISTINCT coauthor) AS 동료수
ORDER BY 동료수 DESC
LIMIT 1
여기서는 모든 연구원
노드에서 시작하여 그들의 출판하다
관계를 통해 연결된 논문
노드를 찾습니다. 각 논문
노드에 대해 Neo4j는 다시 동일한 논문을 발표한 다른 연구원
노드를 찾기 위해 역으로 이동합니다.
>>> # 답은 'David Johnson'이어야 합니다.
>>> cypher_chain.invoke({"query": "가장 많은 동료와 협업한 연구원은 누구인가요?"})
[1m> Entering new GraphCypherQAChain chain...[0m Generated Cypher: [32;1m[1;3mcypher MATCH (r:연구원)-[:출판하다]->(:논문)<-[:출판하다]-(coauthor:연구원) WHERE r <> coauthor RETURN r.이름 AS 연구원, COUNT(DISTINCT coauthor) AS 동료수 ORDER BY 동료수 DESC LIMIT 1 [0m Full Context: [32;1m[1;3m[{'연구원': 'David Johnson', '동료수': 6}][0m [1m> Finished chain.[0m
< > Update on GitHub