지식 그래프를 활용한 Graph RAG 구현

그래프 검색 기반 생성 (Graph RAG)은 생성형 AI 애플리케이션이 도메인별 지식과 관련 정보를 활용할 수 있도록 하는 강력한 기법으로 부상하고 있습니다. 그래프 RAG는 벡터 데이터베이스를 사용하는 벡터 검색 방법의 대안입니다. 

지식 그래프는 Neo4j 또는 Amazon Neptune과 같은 그래프 데이터베이스가 구조화된 데이터를 나타낼 수 있는 지식 시스템입니다. 지식 그래프에서는 데이터 포인트 간의 관계(엣지)가 데이터 포인트(버텍스 또는 노드라고도 함) 사이의 연결만큼이나 의미가 있습니다. 지식 그래프는 네트워크를 쉽게 탐색하고 연결된 데이터에 대한 복잡한 쿼리를 처리할 수 있게 해줍니다. 지식 그래프는 챗봇, ID 확인, 네트워크 분석, 추천 엔진, 고객 360도 및 사기 탐지와 관련된 사용 사례에 특히 적합합니다.

Graph RAG 방식은 그래프 데이터베이스의 구조적 특성을 활용하여, 네트워크나 복잡한 관계에 대한 검색 정보에 더 깊이 있는 통찰과 컨텍스트를 제공합니다.  그래프 데이터베이스가 대규모 언어 모델(LLM) 과 결합되면 개발자는 텍스트와 같은 비정형 데이터로부터 그래프 생성 프로세스의 상당 부분을 자동화할 수 있습니다. LLM은 텍스트 데이터를 처리하여 엔터티를 식별하고, 이들 간의 관계를 파악한 후 그래프 구조로 나타낼 수 있습니다.

Graph RAG 애플리케이션을 만드는 방법에는 Microsoft의 GraphRAG나 GPT-4와 LlamaIndex를 결합하는 등 여러 가지가 있습니다. 이 튜토리얼에서는 오픈 소스 그래프 데이터베이스인 Memgraph를 사용하여 watsonx에서 Meta의 Llama-3을 이용해 RAG 시스템을 구축합니다. Memgraph는 선언형 쿼리 언어인 Cypher를 사용합니다. Cypher는 SQL과 유사점이 있으나, 테이블과 행이 아닌 노드와 관계에 초점을 맞춥니다. Llama 3을 활용하여 비정형 텍스트로부터 그래프 데이터베이스를 생성하고 채우며, 데이터베이스 내 정보를 쿼리합니다.

1단계

여러 툴 중에서 선택할 수 있지만, 이 튜토리얼에서는 Jupyter Notebook을 사용하기 위해 IBM 계정을 설정하는 방법을 안내합니다.

IBM® Cloud 계정을 사용하여 watsonx.ai에 로그인합니다.

watsonx.ai 프로젝트를 생성합니다.

프로젝트 내에서 프로젝트 ID를 가져올 수 있습니다. 관리 탭을 클릭합니다. 그런 다음 일반 페이지의 세부 정보 섹션에서 프로젝트 ID를 복사합니다. 해당 튜토리얼에서는 이 프로젝트 ID가 필요합니다.

다음으로 프로젝트를 watsonx.ai Runtime과 연결합니다.

watsonx.ai Runtime 서비스 인스턴스를 만듭니다(무료 인스턴스인 Lite 요금제 선택).

b. watsonx.ai Runtime에서 API 키를 생성합니다. 이 튜토리얼에서 사용할 수 있도록 이 API 키를 저장하세요.

c. 프로젝트로 이동하여 관리 탭을 선택합니다.

d.  왼쪽 탭에서 서비스 및 통합을 선택합니다.

e. IBM 서비스를 선택합니다.

f. 서비스 연결을 선택하고 watsonx.ai Runtime을 선택하세요.

g. watsonx.ai에서 생성한 프로젝트에 watsonx.ai Runtime을 연결하세요.

2단계

이제 Docker를 설치해야 합니다.

Docker를 설치한 후에는 Docker 컨테이너를 사용하여 Memgraph를 설치합니다. OSX 또는 Linux에서는 터미널에서 다음 명령을 사용할 수 있습니다.

curl https://install.memgraph.com | sh

Windows 컴퓨터에서는 다음을 사용합니다.

iwr https://windows.memgraph.com | iex

설치 단계에 따라 Memgraph 엔진과 Memgraph 랩을 설치하고 실행합니다.

이 프로젝트를 위해 컴퓨터에서 새로운 가상 환경을 생성하세요.

virtualenv kg_rag --python=python3.12

노트북의 Python 환경에서 다음 Python 라이브러리를 설치합니다.

./kg_rag/bin/pip install langchain langchain-openai langchain_experimental langchain-community==0.3.15 neo4j langchain_ibm jupyterlab json-repair getpass4

이제 Memgraph에 연결할 준비가 되었습니다.

3단계

Memgraph에서 사용자 이름과 비밀번호를 사용하도록 구성했다면 여기에서 해당 값을 입력하세요. 그렇지 않으면 둘 다 입력하지 않는 기본값을 사용할 수 있습니다. 운영 환경에서는 권장되지 않지만, 민감한 데이터를 저장하지 않는 로컬 개발 환경에서는 문제가 되지 않습니다.

import os
from langchain_community.chains.graph_qa.memgraph import MemgraphQAChain
from langchain_community.graphs import MemgraphGraph

url = os.environ.get("MEMGRAPH_URI", "bolt://localhost:7687")
username = os.environ.get("MEMGRAPH_USERNAME", "")
password = os.environ.get("MEMGRAPH_PASSWORD", "")

#initialize memgraph connection
graph = MemgraphGraph(
    url=url, username=username, password=password, refresh_schema=True
)

이제 LLM 시스템의 그래프 생성 기능을 테스트하기 위해 관계 데이터 세트를 설명하는 샘플 문자열을 생성합니다. 더 복잡한 데이터 소스를 사용할 수도 있지만 이 간단한 예제가 알고리즘 시연에는 적합합니다.

graph_text = “””
John’s title is Director of the Digital Marketing Group.
John works with Jane whose title is Chief Marketing Officer.
Jane works in the Executive Group.
Jane works with Sharon whose title is the Director of Client Outreach.
Sharon works in the Sales Group.
“””

첫 번째 단계에서 생성한 watsonx API 키를 입력하세요.

from getpass import getpass

watsonx_api_key = getpass()
os.environ[“WATSONX_APIKEY”] = watsonx_api_key
watsonx_project_id = getpass()
os.environ[“WATSONX_PROJECT_ID”] = watsonx_project_id

이제 텍스트를 생성하도록 WatsonxLLM 인스턴스를 구성합니다. temperature는 비교적 낮게 설정하고 토큰 수는 높게 지정해야 합니다. 이렇게 하면 모델이 실제로 존재하지 않는 엔터티나 관계를 환각 없이 가능한 한 많은 세부 정보를 생성할 수 있습니다.

from langchain_ibm import WatsonxLLM
from ibm_watsonx_ai.metanames import GenTextParamsMetaNames

graph_gen_parameters = {   
    GenTextParamsMetaNames.DECODING_METHOD: “sample”,
    GenTextParamsMetaNames.MAX_NEW_TOKENS: 1000,
    GenTextParamsMetaNames.MIN_NEW_TOKENS: 1,
    GenTextParamsMetaNames.TEMPERATURE: 0.3,
    GenTextParamsMetaNames.TOP_K: 10,
    GenTextParamsMetaNames.TOP_P: 0.8
}
watsonx_llm = WatsonxLLM(
    model_id=”meta-llama/llama-3-3-70b-instruct”,
    url=”https://us-south.ml.cloud.ibm.com”,
    project_id=os.getenv(“WATSONX_PROJECT_ID”),
    params=graph_gen_parameters,
)

LLMGraphTransformer를 사용하면 LLM에서 생성할 노드와 관계의 유형을 직접 지정할 수 있습니다. 사용자의 예시에서는 텍스트가 한 회사의 직원, 그들이 속한 그룹, 직함을 설명하고 있습니다. LLM이 이러한 엔터티로만 제한되도록 설정하면 그래프 내에서 지식을 더 정확하게 표현할 수 있습니다.

convert_to_graph_documents를 호출하면 LLMGraphTransformer가 텍스트로부터 지식 그래프를 생성합니다. 이 단계에서는 관련 컨텍스트와 엔터티를 표현하기 위해 해당 정보를 그래프 데이터베이스에 삽입할 Neo4j 쿼리 구문이 생성됩니다.

from langchain_experimental.graph_transformers.llm import LLMGraphTransformer
from langchain_core.documents import Document

llm_transformer = LLMGraphTransformer(
    llm=watsonx_llm,
    allowed_nodes=[“Person”, “Title”, “Group”],
    allowed_relationships=[“TITLE”, “COLLABORATES”, “GROUP”]
)
documents = [Document(page_content=graph_text)]
graph_documents = llm_transformer.convert_to_graph_documents(documents)

이제 Memgraph 데이터베이스의 기존 데이터를 모두 삭제하고 새로운 노드와 엣지를 삽입합니다.

# make sure the database is empty
graph.query(“STORAGE MODE IN_MEMORY_ANALYTICAL”)
graph.query(“DROP GRAPH”)
graph.query(“STORAGE MODE IN_MEMORY_TRANSACTIONAL”)

# create knowledge graph
graph.add_graph_documents(graph_documents)

생성된 Cypher 구문은 graph_documents 객체에 저장되며 문자열로 출력해 내용을 확인할 수 있습니다.

print(f”{graph_documents}”)

Cypher로 생성된 스키마와 데이터 유형은 그래프 'get_schema' 속성에서 확인할 수 있습니다.

graph.refresh_schema()
print(graph.get_schema)

다음과 같이 출력됩니다.

Node labels and properties (name and type) are:
- labels: (:Title)
properties:
- id: string
- labels: (:Group)
properties:
- id: string
- labels: (:Person)
properties:
- id: string

Nodes are connected with the following relationships:
(:Person)-[:COLLABORATES]->(:Person)
(:Person)-[:GROUP]->(:Group)
(:Person)-[:TITLE]->(:Title)

Memgraph Labs 뷰어에서도 그래프 구조를 확인할 수 있습니다.

 

노드와 엣지를 보여주는 그래프 네트워크 이미지 입력 텍스트에서 생성된 Memgraph 네트워크

LLM은 올바른 노드와 관계를 적절히 생성하는 데 성공했습니다. 이제 생성된 지식 그래프에 쿼리를 수행할 단계입니다.

4단계

LLM을 올바르게 프롬프트하려면 프롬프트 엔지니어링이 필요합니다. LangChain은 프롬프트에 여러 예시를 제공할 수 있는 FewShotPromptTemplate을 지원하여 LLM이 올바르고 간결한 Cypher 구문을 생성하도록 돕습니다. 다음 코드는 LLM이 사용해야 하는 질문 및 쿼리의 몇 가지 예를 제공합니다. 또한 모델의 아웃풋을 쿼리로만 제한하는 방법도 보여줍니다. 지나치게 대화형으로 응답하는 LLM은 불필요한 정보를 추가해 잘못된 Cypher 쿼리를 만들 수 있으므로 프롬프트 템플릿은 모델이 오직 쿼리만 출력하도록 지시합니다.

지시적인 접두어를 추가하면 모델의 동작을 제약하고, LLM이 올바른 Cypher 구문을 출력할 가능성을 높일 수 있습니다.

from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate

examples = [
{
“question”: “<|begin_of_text|>What group is Charles in?<|eot_id|>“,
“query”: “<|begin_of_text|>MATCH (p:Person {{id: ‘Charles’}})-[:GROUP]->(g:Group) RETURN g.id<|eot_id|>“,
},
{
“question”: “<|begin_of_text|>Who does Paul work with?<|eot_id|>“,
“query”: “<|begin_of_text|>MATCH (a:Person {{id: ‘Paul’}})-[:COLLABORATES]->(p:Person) RETURN p.id<|eot_id|>“,
},
{
“question”: “What title does Rico have?<|eot_id|>“,
“query”: “<|begin_of_text|>MATCH (p:Person {{id: ‘Rico’}})-[:TITLE]->(t:Title) RETURN t.id<|eot_id|>“,
}
]

example_prompt = PromptTemplate.from_template(
“<|begin_of_text|>{query}<|eot_id|>“
)

prefix = “””
Instructions:
- Respond with ONE and ONLY ONE query.
- Use provided node and relationship labels and property names from the
schema which describes the database’s structure. Upon receiving a user
question, synthesize the schema to craft a precise Cypher query that
directly corresponds to the user’s intent.
- Generate valid executable Cypher queries on top of Memgraph database.
Any explanation, context, or additional information that is not a part
of the Cypher query syntax should be omitted entirely.
- Use Memgraph MAGE procedures instead of Neo4j APOC procedures.
- Do not include any explanations or apologies in your responses. Only answer the question asked.
- Do not include additional questions. Only the original user question.
- Do not include any text except the generated Cypher statement.
- For queries that ask for information or functionalities outside the direct
generation of Cypher queries, use the Cypher query format to communicate
limitations or capabilities. For example: RETURN “I am designed to generate Cypher queries based on the provided schema only.”

Here is the schema information

{schema}

With all the above information and instructions, generate Cypher query for the
user question.

The question is:

{question}

Below are a number of examples of questions and their corresponding Cypher queries.”””

cypher_prompt = FewShotPromptTemplate(
    examples=examples,
    example_prompt=example_prompt,
    prefix=prefix,
    suffix=”User input: {question}\nCypher query: “,
    input_variables=[“question”, “schema”],
)

다음으로, Memgraph에서 반환된 정보로 LLM이 질문에 답하는 방법을 제어하는 프롬프트를 생성합니다. LLM이 그래프 데이터베이스에서 컨텍스트 정보를 가져온 후 응답하는 방법에 대한 몇 가지 예제와 지침을 제공합니다.

 

qa_examples = [
    {
        “question”: “<|begin_of_text|>What group is Charles in?<|eot_id|>“,
        “context”: “[{{‘g.id’: ‘Executive Group’}}]”,
        “response”: “Charles is in the Executive Group<|eot_id|>“
    },
    {
        “question”: “<|begin_of_text|>Who does Paul work with?<|eot_id|>“,
        “context”: “[{{‘p.id’: ‘Greg’}}, {{‘p2.id’: ‘Norma’}}]”,
        “response”: “Paul works with Greg and Norma<|eot_id|>“
    },
    {
        “question”: “What title does Rico have?<|eot_id|>“,
        “context”: “[{{‘t.id’: ‘Vice President of Sales’}}]”,
        “response”: “Vice President of Sales<|eot_id|>“
    }
]

qa_template = “””
Use the provided question and context to create an answer.Question: {question}

Context: {context}
Use only names departments or titles contained within {question} and {context}.
“””
qa_example_prompt = PromptTemplate.from_template(“”)

qa_prompt = FewShotPromptTemplate(
    examples=qa_examples,
    prefix=qa_template,
    input_variables=[“question”, “context”],
    example_prompt=qa_example_prompt,
    suffix=” “
)

이제 질문 답변 체인을 만들 차례입니다. MemgraphQAChain을 통해 사용할 LLM, 적용할 그래프 스키마, 그리고 디버깅 관련 정보를 지정할 수 있습니다. temperature를 0으로 설정하고 length penalty를 적용하면 LLM이 Cypher 프롬프트를 간결하고 명확하게 유지하도록 유도할 수 있습니다.

query_gen_parameters = {
    GenTextParamsMetaNames.DECODING_METHOD: “sample”,
    GenTextParamsMetaNames.MAX_NEW_TOKENS: 100,
    GenTextParamsMetaNames.MIN_NEW_TOKENS: 1,
    GenTextParamsMetaNames.TEMPERATURE: 0.0,
    GenTextParamsMetaNames.TOP_K: 1,
    GenTextParamsMetaNames.TOP_P: 0.9,
    GenTextParamsMetaNames.LENGTH_PENALTY: {‘decay_factor’: 1.2, ‘start_index’: 20}
}

chain = MemgraphQAChain.from_llm(
        llm = WatsonxLLM(
        model_id=”meta-llama/llama-3-3-70b-instruct”,
        url=”https://us-south.ml.cloud.ibm.com”,
        project_id=”dfe8787b-1f6f-4e18-b36a-e22c00f141d1”,
        params=query_gen_parameters
    ),
    graph = graph,
    allow_dangerous_requests = True,
    verbose = True,
    return_intermediate_steps = True, # for debugging
    cypher_prompt=cypher_prompt,
    qa_prompt=qa_prompt
)

이제 자연어 질문으로 체인을 실행할 수 있습니다. LLM은 완전히 결정론적으로 동작하지 않기 때문에 응답 결과는 다소 달라질 수 있습니다.

chain.invoke(“What is Johns title?”)

그러면 다음이 출력됩니다.

> Entering new MemgraphQAChain chain...
Generated Cypher:
 MATCH (p:Person {id: 'John'})-[:TITLE]->(t:Title) RETURN t.id
Full Context:
[{'t.id': 'Director of the Digital Marketing Group'}]

> Finished chain.
{'query': 'What is Johns title?',
 'result': ' \nAnswer: Director of the Digital Marketing Group.',
 'intermediate_steps': [{'query': " MATCH (p:Person {id: 'John'})-[:TITLE]->(t:Title) RETURN t.id"},
  {'context': [{'t.id': 'Director of the Digital Marketing Group'}]}]}

다음 질문에서는 체인에 조금 더 복잡한 질문을 합니다.

chain.invoke(“Who does John collaborate with?”)

그러면 다음이 반환됩니다.

> Entering new MemgraphQAChain chain...
Generated Cypher:
MATCH (p:Person {id: ‘John’})-[:COLLABORATES]->(c:Person) RETURN c
Full Context:
[{‘c’: {‘id’: ‘Jane’}}]

> Finished chain.
{‘query’: ‘Who does John collaborate with?’,
‘result’: ‘ \nAnswer: John collaborates with Jane.’,
‘intermediate_steps’: [{‘query’: “ MATCH (p:Person {id: ‘John’})-[:COLLABORATES]->(c:Person) RETURN c”},
{‘context’: [{‘c’: {‘id’: ‘Jane’}}]}]}

정답은 응답에 포함되어 있습니다. 경우에 따라 최종 사용자에게 답변을 반환하기 전에 제거하고 싶은 추가 텍스트가 있을 수 있습니다.

Memgraph 체인에 Group 관계에 대해 질문할 수 있습니다.

chain.invoke(“What group is Jane in?”)

그러면 다음이 반환됩니다.

> Entering new MemgraphQAChain chain...
Generated Cypher:
MATCH (p:Person {id: ‘Jane’})-[:GROUP]->(g:Group) RETURN g.id
Full Context:
[{‘g.id’: ‘Executive Group’}]

> Finished chain.
{‘query’: ‘What group is Jane in?’,
‘result’: ‘Jane is in Executive Group.’,
‘intermediate_steps’: [{‘query’: “ MATCH (p:Person {id: ‘Jane’})-[:GROUP]->(g:Group) RETURN g.id”},
{‘context’: [{‘g.id’: ‘Executive Group’}]}]}

이것이 올바른 답변입니다.

마지막으로, 체인에 두 개의 결과를 반환하는 질문을 합니다.

chain.invoke(“Who does Jane collaborate with?”)

그러면 다음과 같은 출력이 반환됩니다.

> Entering new MemgraphQAChain chain...
Generated Cypher:
MATCH (p:Person {id: ‘Jane’})-[:COLLABORATES]->(c:Person) RETURN c
Full Context:
[{‘c’: {‘id’: ‘Sharon’}}]

> Finished chain.
{‘query’: ‘Who does Jane collaborate with?’,
‘result’: ‘ Jane collaborates with Sharon.’,
‘intermediate_steps’: [{‘query’: “ MATCH (p:Person {id: ‘Jane’})-[:COLLABORATES]->(c:Person) RETURN c”},
{‘context’: [{‘c’: {‘id’: ‘Sharon’}}]}]}

체인이 두 협력자를 모두 올바르게 식별합니다.

결론

이 튜토리얼에서는 Memgraph와 watsonx를 활용하여 그래프 데이터 구조를 생성하고 쿼리하는 Graph RAG 애플리케이션을 구축했습니다. watsonx를 통한 LLM을 사용하여 자연어 소스 텍스트에서 노드 및 엣지 정보를 추출하고, 그래프 데이터베이스를 채우기 위한 Cypher 쿼리 구문을 생성했습니다. 그런 다음 watsonx를 사용하여 해당 소스 텍스트에 대한 자연어 질문을 Cypher 쿼리로 변환했습니다. 이 쿼리를 통해 그래프 데이터베이스에서 정보를 추출했습니다. 프롬프트 엔지니어링 기법을 통해 LLM이 Memgraph 데이터베이스의 결과를 자연어 응답으로 변환했습니다.

관련 솔루션
IBM® watsonx.ai

AI 빌더를 위한 차세대 엔터프라이즈 스튜디오인 IBM watsonx.ai로 생성형 AI, 파운데이션 모델 및 머신 러닝 기능을 학습, 검증, 조정 및 배포하세요. 적은 데이터로 짧은 시간 내에 AI 애플리케이션을 구축하세요.

watsonx.ai 살펴보기
인공 지능 솔루션

업계 최고의 AI 전문성과 솔루션 포트폴리오를 보유한 IBM과 함께 AI를 비즈니스에 활용하세요.

AI 솔루션 살펴보기
인공 지능(AI) 컨설팅 및 서비스

IBM Consulting AI 서비스는 기업이 AI 활용 방식을 재구상하여 혁신을 달성하도록 지원합니다.

AI 서비스 살펴보기
다음 단계 안내

IBM Concert는 AI를 사용하여 운영에 관한 중요한 인사이트를 발견하고 개선을 위한 애플리케이션별 권장 사항을 제공합니다. Concert를 통해 비즈니스를 발전시키는 방법을 알아보세요.

Concert 살펴보기 비즈니스 프로세스 자동화 솔루션 살펴보기