728x90
반응형

부인 성명:Neo4j에서는 LightRAG를 GraphRAG로 분류된 기술로 봅니다. 현재 LightRAG가 실제 기업 환경에서 의미 있는 개선을 제공한다는 것을 보여주는 프로덕션 수준 통계는 없습니다.

여러분, 혹시 순진한 Retrieval-Augmented Generation(RAG)이 마법처럼 느껴지던 시절을 기억하시나요? 몇 가지 문서를 vector store에 업로드하고 LLM에 연결하면 짜잔! 즉각적인 답변이 뿅! 하고 나타났죠! 하지만 요즘은 그런 순진한 RAG 설정이 새로운 "Hello, World!"나 다름없어졌어요. 게다가 계속해서 개선되고 있죠. 현재 약 30개 이상의 RAG 기술이 존재하는데요 (RAG_Techniques 저장소, NirDiamant 제공), 모두 더 높은 정확성, 신뢰성, 풍부한 상황 인식 답변이라는 동일한 목표를 추구하고 있어요. 그리고 가장 중요한 건 최소한의 노력으로 그걸 해내는 거죠!

이번 블로그 시리즈에서는 최근 주목을 받고 있는 또 다른 대체 RAG 기술인 LightRAG에 대해 자세히 알아볼 거예요. 관계 및 Knowledge Graph의 힘을 활용해서 최신 RAG 시스템이 할 수 있는 일의 경계를 넓혀보려고 해요.

TL;DR

  • 다양한 질문 유형에 대한 더 나은 답변: 이중 레벨 키워드 추출 및 하이브리드 검색 아키텍처는 구체적이고 엔터티 중심의 질문과 더 광범위하고 개념적인 질문을 모두 처리할 수 있어요. 사용자가 세부적인 사실에 대해 질문하든, 전체적인 개념에 대해 질문하든 좋은 답변을 얻을 수 있다는 뜻이죠.
  • 중요한 것에 더욱 스마트하게 집중: entity와 relationship은 node와 edge 등급을 기준으로 순위가 매겨져 구조적으로 가장 중요한 정보를 검색해요. 이렇게 하면 응답이 의미적으로 관련될 뿐만 아니라 Knowledge Graph 내에서 가장 중요한 항목에 고정되죠.
  • 새로운 정보로 쉽게 업데이트 가능: 유연한 schema Knowledge Graph를 기반으로 새로운 entity, 사실, relationship을 쉽게 추가할 수 있어요. 이렇게 하면 재교육 및 재색인 생성의 필요성이 줄어들어 데이터가 자주 발생하거나 변화하는 조직에 딱 알맞죠.

소개

LightRAG 프레임워크의 전체 아키텍처 — LightRAG 저장소의 이미지

그렇다면 LightRAG는 대체 뭘까요? GraphRAG와 유사한 아키텍처 DNA를 공유하는데요. Knowledge Graph(예: Neo4j)를 사용해서 구조화되고 상황에 맞는 정보로 검색을 강화하는 방식이에요. 하지만 많은 접근 방식에서 그래프를 선택적인 추가 기능으로 취급하는 반면, LightRAG는 그래프를 검색 프로세스의 중심으로 만들어요. 기존 RAG 파이프라인은 vector 유사성과 플랫 청크에 너무 많이 의존하는 경향이 있는데, 이는 얕은 조회에는 유용하지만 연결 방식을 이해하는 데 필요한 질문에 대답하는 데는 적합하지 않다는 게 증명되었죠. 실제 데이터는 본질적으로 관계형이기 때문에 그래프가 빛을 발하는 거예요.

추출 파이프라인

LightRAG는 기본적으로 원시 문서에서 추출된 구조화된 지식(entity 및 relationship)이 검색 품질을 향상시킬 수 있지만, Microsoft의 GraphRAG 버전과 같은 커뮤니티 요약 계층이 필요하지 않다는 아이디어를 기반으로 구축되었어요. 후자는 쿼리 중심 요약 기술로도 알려져 있는데, 사용자 질문과 커뮤니티 수준이 주어지면 이 LLM에 제공되기 전에 보강 단계로 검색되는 방식이에요. 다양한 기술에 대한 자세한 내용은 GraphRAG.com에서 확인할 수 있어요.

LightRAG의 추출 파이프라인 워크플로 – 작성자의 이미지

LightRAG의 파이프라인은 좀 더 효율적인 경로를 택해요. 원시 문서를 정리하고 청크하는 것으로 시작한 다음, LLM을 사용해서 entity, relationship 및 키워드 형태로 구조화된 지식을 추출하죠. 이렇게 추출된 구조는 Knowledge Graph에 저장되고, 병렬 방식으로 빠른 Semantic Search를 위해 vector database에 index돼요. 추출 프로세스는 GraphRAG의 프로세스와 유사하지만, 아래에서 설명할 몇 가지 개선 사항이 제안되어 있답니다.

쿼리 중심 요약, 커뮤니티 요약의 힘을 활용해서 LightRAG의 기능을 한번 살펴볼까요? LightRAG는 검색 시 Vector Embedding 검색과 Graph 쿼리를 정렬해서 근거 있고 설명 가능한 답변을 제공하는 방식이에요.

여기서 중요한 점은 LightRAG가 다층 검색 표면을 구축한다는 거죠.

  • Vector Embedding 검색을 위한 의미적 청크
  • 추론 및 관련성 추적을 위한 Graph 엔터티 및 관계
  • 더 심층적인 필터링 또는 순회를 위한 메타데이터 강화 관계

개념 간의 원래 관계는 유지되고, 엔터티 수준 및 관계 수준의 임베딩은 청크 수준의 Vector Embedding과 함께 검색에 사용돼요. 이렇게 계층화된 검색 전략은 LLM에 대한 추적성과 풍부한 컨텍스트를 제공해서 더 정확하고 근거 있는 답변이 가능하게 해준답니다.

LightRAG는 대략적으로 다음과 같은 방식으로 문서를 처리해요.

서류 준비 단계

이 단계에서는 원시 문서 수집, 정리, 중복 제거 및 메타데이터 준비를 처리해요.

문서 수집 및 텍스트 정리

  • null 바이트 삭제

clean_text 함수는 텍스트에서 널 바이트와 선행/후행 공백을 제거해서 처리할 준비가 되었는지 확인해줘요.

# lightrag/utils.py
def clean_text(text: str) -> str:
    """Clean text by removing null bytes (0x00) and whitespace

    Args:
        text: Input text to clean

    Returns:
        Cleaned text
    """
    return text.strip().replace("\x00", "")

중복 제거

  • 중복된 콘텐츠 확인
  • 고유한 콘텐츠 재구성

추출된 콘텐츠는 반복되어서 콘텐츠를 고유하게 식별하고 중복을 방지해요. 그런 다음 고유한 항목만 포함하도록 콘텐츠 사전이 재구성된답니다.

# lightrag/lightrag.py
unique_contents = {}
for id_, content_data in contents.items():
    content = content_data["content"]
    file_path = content_data["file_path"]
    if content not in unique_contents:
        unique_contents[content] = (id_, file_path)

contents = {
    id_: {"content": content, "file_path": file_path}
    for content, (id_, file_path) in unique_contents.items()
}
  • 콘텐츠 요약 생성
  • 최대 길이를 초과하면 잘립니다.

get_content_summary 함수는 내용을 지정된 최대 길이로 자르고 텍스트가 해당 제한을 초과하는 경우 줄임표를 추가하는 간단한 함수에요. 이 함수는 효과적으로 간략한 발췌문을 생성하지만, 이걸 "요약"이라고 부르는 건 약간 오해의 소지가 있을 수 있겠네요.

제 생각에는 이 코드 조각은 빠른 미리 보기, 대용량 문서 처리 시 보다 효율적인 저장 등의 다른 실용적인 목적에도 도움이 될 것 같아요.

# lightrag/utils.py
def get_content_summary(content: str, max_length: int = 250) -> str:
    """Get summary of document content

    Args:
        content: Original document content
        max_length: Maximum length of summary

    Returns:
        Truncated content with ellipsis if needed
    """
    content = content.strip()
    if len(content) <= max_length:
        return content
    return content[:max_length] + "..."
  • 문서 메타데이터 준비
  • 이미 수집된 필터
  • 문서 상태/메타데이터 업데이트

각 문서는 감사를 위한 타임스탬프 및 추적을 위한 원본 파일 경로와 함께 콘텐츠의 이전에 잘린 미리 보기를 포함하는 메타데이터로 강화돼요.

# lightrag/lightrag.py # 3. Generate document initial status ```python new_docs: dict[str, Any] = { id_: { "status": DocStatus.PENDING, "content": content_data["content"], "content_summary": get_content_summary(content_data["content"]), "content_length": len(content_data["content"]), "created_at": datetime.now().isoformat(), "updated_at": datetime.now().isoformat(), "file_path": content_data[ "file_path" ], # Store file path in document status } for id_, content_data in contents.items() } # 4. Filter out already processed documents # Get docs ids all_new_doc_ids = set(new_docs.keys()) # Exclude IDs of documents that are already in progress unique_new_doc_ids = await self.doc_status.filter_keys(all_new_doc_ids) ```

문서의 고유성을 확인하기 위해 MD5 해시 기반 문서 ID를 사용하여 데이터베이스 쿼리가 수행돼요.

```python # for entities compute_mdhash_id(dp["entity_name"], prefix="ent-") # for relationships compute_mdhash_id(dp["src_id"] + dp["tgt_id"], prefix="rel-") ```

그렇다면, filter_keys 함수는 추가 처리에서 이를 제거하죠. 다음은 샘플 입력인 new_docs입니다. doc-a1b2c3이 이미 처리되어 데이터베이스에 존재하는 경우, 파이프라인이 이를 추가 처리에서 제외하고 각 문서의 메타데이터 레코드가 키-값 저장소로 수집된다고 가정해 볼게요.

```python # From new_docs = { "doc-a1b2c3": { "status": "PENDING", "content_summary": "Sample content 1", "content_length": 29, "file_path": "file1.txt", ... }, "doc-123abc": { "status": "PENDING", "content_summary": "Sample content 2", "content_length": 24, "file_path": "file2.txt", ... } } # To new_docs = { "doc-123abc": { "status": "PENDING", "content_summary": "Sample content 2", "content_length": 24, "file_path": "file2.txt", ... } } ```

의미 강화 단계

다음 단계에서는 정리된 텍스트를 사용 가능한 의미 체계 및 그래프 구조로 변환하는 데 중점을 둬요. 문서가 초기 단계에서 정리, 중복 제거 및 필터링되면 LightRAG는 실제 변환이 시작되는 두 번째 전처리 단계로 이동해요. 여기서는 구조화되지 않은 텍스트를 가져와서 덩어리로 만들고, 포함하고, 추출하고 구조화합니다. Vector EmbeddingGraph 말이죠. 이것이 Semantic Search와 그래프 기반 추론의 기반이에요.

청킹 및 임베딩

  • 토큰 크기별 청크

오버랩 청킹 전략은 겹치는 창 전체에서 의미적 컨텍스트를 유지하는 데 사용돼요. 기본적으로 중첩 토큰 크기는 128이에요. 여기서는 "완벽한" 청크 크기에 대해 논의하지 않을게요. 이는 다른 게시물에서 다룰 미묘한 주제거든요. 하지만 청킹 기능은 LightRAG.chunking_func를 통해 완전히 구성할 수 있어요.

```python # lightrag/operate.py def chunking_by_token_size( content: str, split_by_character: str | None = None, overlap_token_size: int = 128, max_token_size: int = 1024, ... ) -> list[dict[str, Any]]: ```

콘텐츠가 청크화되면 각 청크는 OpenAI, Claude 또는 로컬 임베딩 모델을 사용할 수 있는 구성된 임베딩 기능을 통해 전달돼요.

임베딩 결과는 full_doc_id, file_path, 그리고 content와 같은 메타데이터와 함께 저장되죠. Vector 인덱스나 데이터베이스로 변환하여 추적 가능하고 설명 가능한 Semantic Search가 가능하게 되는 거예요.

# lightrag/lightrag.py ```html

# lightrag/lightrag.py
self.chunks_vdb: BaseVectorStorage = self.vector_db_storage_cls(  # type: ignore
    namespace=make_namespace(
        self.namespace_prefix, NameSpace.VECTOR_STORE_CHUNKS
    ),
    embedding_func=self.embedding_func,
    meta_fields={"full_doc_id", "content", "file_path"},
)

chunks_vdb_task = asyncio.create_task(
    self.chunks_vdb.upsert(chunks)
)

Entity and Relationship Extraction

  • Extraction을 위한 청크 처리
  • LLM 추출 Prompt

텍스트 청크에서 Entity와 Relationship을 추출하기 전에 LightRAG는 LLM을 위한 고도로 구조화된 Prompt를 준비해요. 구조화된 Prompt는 구성 가능한 item type과 몇 가지 예시를 사용해서 구성되기 때문에 사용자가 더 많은 제어를 할 수 있죠.

다음 entity_types는 기본적으로 추출되도록 구성되어 있어요. 도메인에 맞게 추출 프로세스를 조정하기 위해 맞춤 item type을 전달해서 기본값을 재정의할 수 있어요.

# lightrag/prompt.py
PROMPTS["DEFAULT_ENTITY_TYPES"] = ["organization", "person", "geo", "event", "category"]

# lightrag/operate.py
entity_types = global_config["addon_params"].get(
    "entity_types", PROMPTS["DEFAULT_ENTITY_TYPES"]
)

Few-shot 예시가 명시적으로 제공되지 않으면 기본적으로 다음 위치에 있는 Prompt 템플릿에서 사전 정의된 Few-shot 예시 세트를 사용하게 돼요.

# lightrag/prompt.py
PROMPTS["entity_extraction_examples"]

각 예는 LLM이 반환할 것으로 예상되는 것과 동일한 형식을 따르고 있어요.

:

while Alex clenched his jaw, the buzz of frustration dull against the backdrop of Taylor's authoritarian certainty. It was this competitive undercurrent that kept him alert, the sense that his and Jordan's shared commitment to discovery was an unspoken rebellion against Cruz's narrowing vision of control and order.

Then Taylor did something unexpected. They paused beside Jordan and, for a moment, observed the device with something akin to reverence. "If this tech can be understood..." Taylor said, their voice quieter, "It could change the game for us. For all of us."

The underlying dismissal earlier seemed to falter, replaced by a glimpse of reluctant respect for the gravity of what lay in their hands. Jordan looked up, and for a fleeting heartbeat, their eyes locked with Taylor's, a wordless clash of wills softening into an uneasy truce.

It was a small transformation, barely perceptible, but one that Alex noted with an inward nod. They had all been brought here by different paths

:

# lightrag/prompt.py
("entity"{tuple_delimiter}"Alex"{tuple_delimiter}"person"{tuple_delimiter}"Alex is a character...")
("relationship"{tuple_delimiter}"Alex"{tuple_delimiter}"Taylor"{tuple_delimiter}"Power dynamic..."{tuple_delimiter}"conflict"{tuple_delimiter}7)
("content_keywords"{tuple_delimiter}"discovery, control, rebellion")

Prompt는 다음과 같은 기본값과 함께 동적으로 삽입되는 자리표시자 구분 기호(예: '{tuple_delimiter}')를 사용해요.

# lightrag/prompt.py
PROMPTS["DEFAULT_TUPLE_DELIMITER"] = "<|>"
PROMPTS["DEFAULT_RECORD_DELIMITER"] = "##"
PROMPTS["DEFAULT_COMPLETION_DELIMITER"] = "<|COMPLETE|>"

위에 자세히 설명된 요소를 결합해서 전체 추출이 형성되는 거죠.

# lightrag/prompt.py

# lightrag/prompt.py
PROMPTS["entity_extraction"] = """---Goal---
Given a text document that is potentially relevant to this activity and a list of entity types, identify all entities of those types from the text and all relationships among the identified entities.
Use {language} as output language.

---Steps---
1. Identify all entities. For each identified entity, extract the following information:
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name.
- entity_type: One of the following types: [{entity_types}]
- entity_description: Comprehensive description of the entity's attributes and activities
Format each entity as ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>)

2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
For each pair of related entities, extract the following information:
- source_entity: name of the source entity, as identified in step 1
- target_entity: name of the target entity, as identified in step 1
- relationship_description: explanation as to why you think the source entity and the target entity are related to each other
- relationship_strength: a numeric score indicating strength of the relationship between the source entity and target entity
- relationship_keywords: one or more high-level key words that summarize the overarching nature of the relationship, focusing on concepts or themes rather than specific details
Format each relationship as ("relationship"{tuple_delimiter}<source_entity>{tuple_delimiter}<target_entity>{tuple_delimiter}<relationship_description>{tuple_delimiter}<relationship_keywords>{tuple_delimiter}<relationship_strength>)

3. Identify high-level key words that summarize the main concepts, themes, or topics of the entire text. These should capture the overarching ideas present in the document.
Format the content-level key words as ("content_keywords"{tuple_delimiter}<high_level_keywords>)

4. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **{record_delimiter}** as the list delimiter.

5. When finished, output {completion_delimiter}

######################
---Examples---
######################
{examples}

#############################
---Real Data---
######################
Entity_types: [{entity_types}]
Text:
{input_text}
######################
Output:"""

구문 분석 추출 결과:

원시 final_result 문자열은 추가로 처리되어 두 개의 사전을 반환해요.

  • maybe_nodes: 엔터티 이름 → 엔터티 사전 목록
  • maybe_edges: (소스, 타겟) → 관계 사전 목록
# lightrag/operate.py
maybe_nodes, maybe_edges = await _process_extraction_result(
    final_result, chunk_key, file_path
)

final_result은 이후 단계에서 추가 변환을 거치게 돼요. 정의된 구분 기호를 사용하여 LightRAG는 전체 결과를 개별 레코드로 분할하죠. 하지만 더 흥미로운 점은 전통적인 엔터티 및 관계 추출과 달리 LightRAG가 각 관계를 정량화하고 의미론적으로 특성화하는 데 한 단계 더 나아간다는 것이에요.

특히 추출된 각 관계에는 다음이 포함됩니다.

  • relationship_strength — 소스와 대상 엔터티 간의 관계가 얼마나 강력하거나 중요한지를 나타내는 숫자 점수에요. 이를 통해 우리는 두 가지가 서로 관련되어 있을 뿐만 아니라 두 가지가 얼마나 밀접하게, 얼마나 자주, 얼마나 중요하게 함께 발생하거나 상호 작용하는지 모델링할 수 있죠.

이 관계 강도 점수는 하드 데이터가 아닌 LLM의 해석에서 나온 것이라는 점은 주목할 가치가 있어요. 마치 누군가에게 “이 두 사람이 얼마나 친하다고 생각하시나요?”라고 묻는 것과 같아요. 실제 상호작용을 계산하는 것이 아니라 LLM은 맥락을 바탕으로 정보에 기반한 추측을 하고 있는 거죠.

  • relationship_keyword — 주제나 개념(예: "갈등", "협력", "영향")을 포착해서 관계의 성격을 요약하는 하나 이상의 고급 키워드예요. 필터링, 클러스터링, 그래프 시각화에 사용할 수 있는 간결한 Semantic 태그 역할을 하죠.

이건 단순히 "엔티티 A가 엔티티 B와 관련되어 있습니다."를 넘어서는 상황별 메타데이터로 Knowledge Graph를 풍부하게 만들어 줘요. 검색하는 동안 가중치를 기준으로 관계의 우선순위를 지정하거나 (예: "강한 연결만 표시") 점수가 낮은 노이즈 edge를 삭제할 수도 있고요. 그 결과 더 스마트한 그래프 탐색, 더 집중된 검색, 다운스트림 추론을 위한 더 나은 기반이 제공되는 거죠.

그리고 청크에 존재하는 중심 주제 또는 주제는 다음과 같이 요약돼요. content_keywords. 텍스트의 전반적인 내용을 반영하며 특정 개체나 관계에 묶여 있지는 않아요.

# Before
("entity"<|>"Alex"<|>"person"<|>"Alex is a character...")##
("relationship"<|>"Alex"<|>"Taylor"<|>"Power dynamic..."<|>"conflict"<|>"7")##
("content_keywords"<|>"discovery, control, rebellion")<|COMPLETE|>

# In Between
[
  '("entity"<|>"Alex"<|>"person"<|>"Alex is a character...")',
  '("relationship"<|>"Alex"<|>"Taylor"<|>"Power dynamic..."<|>"conflict"<|>"7")',
  '("content_keywords"<|>"discovery, control, rebellion")'
]

# After (Above: Entities, Below: Relationships)
["entity", "Alex", "person", "Alex is a character..."]

["relationship", "Alex", "Taylor", "Power dynamic...", "conflict", "7"]

처리된 기록은 엔터티인지 관계인지에 따라 별도의 변환을 거쳐요. 그러면 기록에 메타데이터 정보가 첨부되죠.

# maybe_nodes
{
  "entity_name": "Alex",
  "entity_type": "person",
  "description": "Alex is a character...",
  "source_id": "chunk-123",
  "file_path": "file1.txt"
}

# maybe_edges
{
  "src_id": "Alex",
  "tgt_id": "Taylor",
  "description": "Power dynamic...",
  "keywords": "conflict",
  "weight": 7.0,
  "source_id": "chunk-345",
  "file_path": "file1.txt"
}

Gleaning Loop(신뢰도가 낮은 청크에 대한 선택적 재시도)

  • 수집 및 추출 재시도
  • 새 엔터티 및 관계 병합
  • 추출 루프 확인
  • 모든 결과 결합

LLM이 첫 번째 단계에서, 특히 밀집된 텍스트 청크에서 엔터티나 관계를 놓치는 경우가 있을 수 있어요. LightRAG에는 다음과 같은 간단한 재시도 메커니즘이 포함되어 있는데요, 바로 Gleaning이에요. 이전에 간과되었을 수 있는 모든 항목을 추출하도록 LLM에 다시 message를 보내는 거죠 (구성 가능한 한도까지). 새로운 엔터티나 관계가 발견되면 전체 결과에 추가돼요. 수집 단계에서 사용되는 Prompt는 모든 영역을 포괄하도록 강제하기 위해 이전 추출 Prompt보다 더 적극적이고 단호하답니다.

# lightrag/prompt.py
PROMPTS["entity_continue_extraction"] = """
MANY entities and relationships were missed in the last extraction.

---Remember Steps---

1. Identify all entities. For each identified entity, extract the following information:
- entity_name: Name of the entity, use same language as input text. If English, capitalized the name.
- entity_type: One of the following types: [{entity_types}]
- entity_description: Comprehensive description of the entity's attributes and activities
Format each entity as ("entity"{tuple_delimiter}<entity_name>{tuple_delimiter}<entity_type>{tuple_delimiter}<entity_description>

2. From the entities identified in step 1, identify all pairs of (source_entity, target_entity) that are *clearly related* to each other.
For each pair of related entities, extract the following information:
- source_entity: name of the source entity, as identified in step 1
- target_entity: name of the target entity, as identified in step 1
- relationship_description: explanation as to why you think the source entity and the target entity are related to each other
- relationship_strength: a numeric score indicating strength of the relationship between the source entity and target entity
- relationship_keywords: one or more high-level key words that summarize the overarching nature of the relationship, focusing on concepts or themes rather than specific details
Format each relationship as ("relationship"{tuple_delimiter}<source_entity>{tuple_delimiter}<target_entity>{tuple_delimiter}<relationship_description>{tuple_delimiter}<relationship_keywords>{tuple_delimiter}<relationship_strength>)

3. Identify high-level key words that summarize the main concepts, themes, or topics of the entire text. These should capture the overarching ideas present in the document.
Format the content-level key words as ("content_keywords"{tuple_delimiter}<high_level_keywords>)

4. Return output in {language} as a single list of all the entities and relationships identified in steps 1 and 2. Use **{record_delimiter}** as the list delimiter.

5. When finished, output {completion_delimiter}

---Output---

Add them below using the same format:\n
""".strip()

최종 병합, 중복 제거

  • 엔터티 병합 및 요약
  • 관계 병합 및 요약

수집 단계가 끝나면 LightRAG는 기어를 집계 모드로 전환해요. 여기가 조각을 결합하고 정리한 후 Knowledge Graph와 벡터 데이터베이스에 저장하는 곳이죠.

동일한 엔터티의 모든 발생은 결합되어 청크로 그룹화돼요. 예를 들어, "Alex"가 설명이 다른 세 개의 서로 다른 청크로 나타나는 경우 이제 세 항목이 모두 다음으로 그룹화됩니다.all_nodes["Alex"].

관계는 추출된 순서에 상관없이 일관성 있는 정식 키를 제공하기 위해 여러 청크로 통합돼요. 이것은 본질적으로 관계를 다음과 같이 취급합니다..

# lightrag/operate.py
for edge_key, edges in maybe_edges.items():
    sorted_edge_key = tuple(sorted(edge_key))
    all_edges[sorted_edge_key].extend(edges)

유사한 가장자리가 동일한 관계로 처리되는지 확인합니다.

# From
("Alex", "Taylor")
("Taylor", "Alex")

# To
sorted(("Alex", "Taylor")) → ("Alex", "Taylor")
sorted(("Taylor", "Alex")) → ("Alex", "Taylor")

Knowledge Graph 및 벡터 데이터베이스로 수집

모든 청크 수준 엔터티 및 관계 추출이 완료되면 LightRAG는 이 정보를 Knowledge Graph 및 벡터 데이터베이스에 병합, 중복 제거 및 업데이트해요. 그 과정이 어떻게 진행되는지 한번 살펴볼까요?

처리 Node(엔티티)

각 엔터티는 다음을 사용하여 검색됩니다.entity_name Knowledge Graph의 속성이에요. 일치하는 Node가 이미 존재하는 경우 기존 메타데이터가 검색됩니다.

  • 지배적인 것을 고르는 것entity_type

The entity_type 빈도 카운터를 사용하여 빈도 수가 가장 높은 항목을 선택합니다(예: 여기에서 '사람'의 발생 빈도가 가장 높습니다).

# example 
entity_types = ["person", "character", "person", "persona", "person", "protagonist", "character"]

# lightrag/operate.py
entity_type = sorted(
    Counter(
        [dp["entity_type"] for dp in nodes_data] + already_entity_types
    ).items(),
    key=lambda x: x[1],
    reverse=True,
)[0][0]

# result
entity_type = "person"

description 값은 ||| 구분 기호를 사용해서 결합돼요. 이렇게 컨텍스트를 보존하면서 병합된 조각 수가 기본 임계값인 6을 넘어가면, LLM을 사용해서 조각들을 하나의 간결한 설명으로 요약하죠.

merged_description = (
  "A highly observant character involved in power dynamics.|||Alex is a character..."
)

이렇게 하면 완전성과 가독성 사이의 균형을 유지하면서 Knowledge Graph가 너무 커지는 걸 방지할 수 있어요.

  • 결합 source_id and file_path

source_idfile_path는 함께 결합돼요. 이 필드들은 추적성이나 출처 확인에 사용되기 때문에 요약되지 않아요.

source_id = "chunk-123|||chunk-456"
file_path = "file1.txt|||file2.txt"

관계는 각 edge (src_id, tgt_id) 쌍이 그래프에서 조회되는 방식으로 처리돼요. 만약 일치하는 관계가 이미 존재한다면, 기존 메타데이터를 가져오게 되죠.

  • 설명과 키워드 결합

모든 Edge 언급에 대한 설명과 키워드가 연결돼요. 마찬가지로, 병합된 조각 수가 정해진 임계값을 초과하면 LLM을 사용해서 조각들을 하나의 간결한 설명으로 요약하게 돼요.

{
  "description": "Power dynamic...|||Mentorship dynamic between Taylor and Alex.",
  "keywords": "conflict, guidance, imbalance"
}

각 관계에는 추출된 강도 또는 신뢰도를 나타내는 가중치가 있어요. 동일한 관계가 여러 번 나타나면 가중치가 합산되죠.

weight = 7.0 + 5.0 = 12.0
  • 결합 source_id and file_path

다시 말하지만, 모든 source_idfile_path 값은 완전한 추적성을 위해 보존돼요.

Node/관계를 Knowledge Graph에 추가

LightRAG는 Neo4j를 그래프 저장소로 지원하고, Cypher 쿼리를 사용해서 entity와 관계를 삽입하거나 업데이트해요. 이렇게 하면 그래프를 쿼리 가능하고 설명 가능하게 만들 수 있죠.

// Upsert of nodes
MERGE (n:base {entity_id: $entity_id})
SET n += $properties
SET n:`%s`

제 생각에는 이 접근 방식을 사용하면 검색과 인덱싱이 더 쉬워져요. 하지만 Semantic Search 명확성은 entity_type을 기본 label이 아니라 :Person, :Technology, 또는 :Organization처럼 Cypher에서 그래프를 상황에 맞게 더 쉽게 쿼리할 수 있도록 사용하는 방식으로 향상될 수 있을 것 같아요.

// Upsert of relationships
MATCH (source:base {entity_id: $source_entity_id})
WITH source
MATCH (target:base {entity_id: $target_entity_id})
MERGE (source)-[r:DIRECTED]-(target)
SET r += $properties
RETURN r, source, target

Vector Database에 Upserting

Semantic Search를 활성화하기 위해서 LightRAG는 각 entity와 관계를 Vector Embedding하고, 이를 Vector Database에 저장해요.

각 항목 및 관계에 대한 콘텐츠 문자열을 만들어요.

# For entities
content = f"{entity_name}\n{description}"
# e.g., "Alex\nAlex is a character..."

# For relationship
content = f"{src_id}\t{tgt_id}\n{keywords}\n{description}"
# e.g., "Alex\tTaylor\nconflict, guidance\nPower dynamic between them"

content 필드가 임베딩 모델을 통해서 고차원 벡터에 임베딩되는 것이죠.

이 각각은 별도의 벡터 문서에 저장돼요.

# entities
{
  "_id": "ent-8b14c7...",
  "entity_name": "Alex",
  "entity_type": "person",
  "content": "Alex\nAlex is a character who is highly observant of power dynamics.",
  "source_id": "chunk-123|||chunk-456",
  "file_path": "file1.txt|||file2.txt",
  "vector": [0.015, -0.782, 0.431, ...] 
}

# relationships
{
  "_id": "rel-7f91de...",
  "src_id": "Alex",
  "tgt_id": "Taylor",
  "keywords": "conflict, guidance",
  "content": "...",
  "source_id": "chunk-345|||chunk-567",
  "file_path": "file1.txt|||file2.txt",
  "vector": [0.73, -0.46, 0.2, ...] 
}

Graph Database와 Vector Database 모두에 대한 지식을 활용해서 LightRAG는 GraphRAG와 유사한 강력한 하이브리드 검색 모델을 만들 수 있어요. LightRAG를 사용한 비밀: 검색을 참고해보세요.

# lightrag/operate.py

if entity_vdb is not None and entities_data:
    data_for_vdb = {
        compute_mdhash_id(dp["entity_name"], prefix="ent-"): {
            "entity_name": dp["entity_name"],
            "entity_type": dp["entity_type"],
            "content": f"{dp['entity_name']}\n{dp['description']}",
            "source_id": dp["source_id"],
            "file_path": dp.get("file_path", "unknown_source"),
        }
        for dp in entities_data
    }
    await entity_vdb.upsert(data_for_vdb)

if relationships_vdb is not None and relationships_data:
    data_for_vdb = {
        compute_mdhash_id(dp["src_id"] + dp["tgt_id"], prefix="rel-"): {
            "src_id": dp["src_id"],
            "tgt_id": dp["tgt_id"],
            "keywords": dp["keywords"],
            "content": f"{dp['src_id']}\t{dp['tgt_id']}\n{dp['keywords']}\n{dp['description']}",
            "source_id": dp["source_id"],
            "file_path": dp.get("file_path", "unknown_source"),
        }
        for dp in relationships_data
    }
    await relationships_vdb.upsert(data_for_vdb)

Neo4j는 Knowledge Graph와 Vector Database의 기능을 모두 제공해서 단일 플랫폼 내에서 하이브리드 검색이 가능하게 해줘요.

요약

LightRAG는 사일로화되고 구조화되지 않은 문서를 연결하고, 연결된 지식의 풍부한 웹으로 변환하는 방법에 대한 대안적인 방법을 제시했어요.

핵심은 이거에요: 추출은 엉망인 데이터를 정리하고, 콘텐츠를 소화하기 쉬운 덩어리로 나누고, LLM이 중요한 엔터티와 연결 방법을 식별하도록 하는 세 단계로 진행돼요. 이 정보를 Knowledge Graph와 Vector Database에 저장함으로써 의미를 이해하고 추론을 설명할 수 있죠. Neo4j는 이러한 시스템이 생성하는 복잡한 관계 데이터를 저장하고 쿼리하는 데 아주 적합하기 때문에, 이런 그래프 기반 RAG 구현에 딱 맞는 선택이에요. 이 조합을 사용하면 Semantic Search(Vector Embedding을 통해)와 구조적 관계 순회(그래프 쿼리를 통해)가 모두 가능해져요.

잘 구조화된 지식을 갖는 건 차고에 페라리를 두는 것과 같아요. 멋지긴 하지만, 열쇠()가 없으면 효과적으로 운전할 수 없겠죠?

다음 단계

다음에 또 만나요!


  • Extraction
  • GraphRAG
  • LightRAG
  • Retrieval-Augmented Generation

에이치시스템즈LogTree는 Neo4j 기반 GraphRAG 플랫폼으로, 데이터를 자동으로 지식그래프화하고 자연어 질의로 즉시 답을 제공합니다.

👉 에이치시스템즈 홈페이지

728x90
반응형

+ Recent posts