원본 문서(HTML, PDF, Markdown 등)에서 텍스트를 추출합니다.
문서 구조와 의미를 기반으로 텍스트를 특정 크기로 청크합니다.
청크 임베딩으로 키가 지정된 벡터 데이터베이스에 청크를 저장합니다.
답변을 생성할 때 컨텍스트로 사용하기 위해 질문과 관련된 청크를 검색합니다.
그러나 벡터 유사성에 기반한 RAG에는 몇 가지 약점이 있습니다. 질문과 유사한 정보에 초점을 맞추기 때문에 예를 들어 여러 주제 및/또는 여러 홉이 필요한 질문에 대답하기가 더 어렵습니다. 또한 검색되는 청크 수를 제한합니다.
각 청크는 고유한 소스에서 나오므로 대체로 유사한 정보가 여러 위치에 존재하는 경우 정보의 여러 복사본을 검색하거나(다른 정보를 놓칠 수도 있음) 더 많은 정보를 얻기 위해 하나의 복사본만 선택하는 것 중에서 선택해야 합니다. 그러면 다른 소스의 뉘앙스를 놓치게 됩니다.
이 접근 방식은 유사성 기반 접근 방식에 비해 몇 가지 이점이 있습니다.
많은 사실이 단일 소스에서 추출되어 지식 그래프 내의 다양한 엔터티와 연관될 수 있습니다. 이를 통해 관련 없는 정보를 포함하여 전체 청크가 아닌 특정 소스에서 관련 사실만 검색할 수 있습니다.
여러 소스가 동일한 내용을 말하면 동일한 노드 또는 에지를 생성합니다. 이를 별개의 사실로 처리하고 여러 복사본을 검색하는 대신 동일한 노드 또는 에지로 처리하고 한 번만 검색할 수 있습니다. 이를 통해 더욱 다양한 사실을 검색하거나 여러 소스에 나타나는 사실에만 집중할 수 있습니다.
그래프는 문제의 엔터티와 직접적으로 관련된 정보를 검색하는 것뿐만 아니라 2~3단계 떨어진 항목을 가져오는 등 여러 단계를 통해 탐색될 수 있습니다. 기존 RAG 접근 방식에서는 여러 차례의 쿼리가 필요합니다.
RAG에 지식 그래프를 사용하면 얻을 수 있는 이점 외에도 LLM을 사용하면 지식 그래프를 더 쉽게 만들 수 있습니다. 해당 분야 전문가에게 지식 그래프를 주의 깊게 작성하도록 요구하는 대신 LLM과 프롬프트를 사용하여 문서에서 정보를 추출할 수 있습니다.
이 게시물에서는 다음을 사용하여 RAG에 대한 지식 그래프 사용을 살펴봅니다.
그런 다음 질문에서 엔터티를 추출하고 관련 하위 그래프를 검색하기 위한 LangChain 실행 가능 파일을 생성합니다. 지식 그래프를 사용하여 RAG를 구현하는 데 필요한 작업에는 그래프 데이터베이스나 그래프 쿼리 언어가 필요하지 않으므로 이미 사용하고 있는 일반적인 데이터 저장소를 사용하여 접근 방식을 적용할 수 있다는 점을 살펴보겠습니다.
앞서 언급했듯이 지식 그래프는 고유한 엔터티를 노드로 나타냅니다. 예를 들어 노드는 사람을 "마리 퀴리"로 나타내거나 언어를 "프랑스어"로 나타낼 수 있습니다. LangChain에서 각 노드에는 이름과 유형이 있습니다. 노드를 고유하게 식별할 때 언어인 "프랑스어"와 국적인 "프랑스어"를 구별하기 위해 두 가지를 모두 고려할 것입니다.
엔터티 간의 관계는 그래프의 가장자리에 해당합니다. 각 에지는 소스(예: 사람 Marie Curie), 대상(노벨상 수상) 및 소스와 대상과의 관계를 나타내는 유형(예: "승리")을 포함합니다.
LangChain을 사용하여 Marie Curie에 관한 단락에서 추출한 지식 그래프의 예는 다음과 같습니다.
목표에 따라 노드와 에지에 속성을 추가하도록 선택할 수 있습니다. 예를 들어 속성을 사용하여 노벨상 수상 시기와 카테고리를 식별할 수 있습니다. 이는 검색 중에 그래프를 탐색할 때 가장자리와 노드를 필터링하는 데 유용할 수 있습니다.
지식 그래프를 구성하는 엔터티와 관계는 직접 생성하거나 기존의 알려진 양호한 데이터 소스에서 가져올 수 있습니다. 이는 지식을 신중하게 관리하고 싶을 때 유용하지만 새로운 정보를 빠르게 통합하거나 많은 양의 정보를 처리하기가 어렵습니다.
다행히 LLM을 사용하면 콘텐츠에서 정보를 쉽게 추출할 수 있으므로 이를 지식 그래프 추출에 사용할 수 있습니다.
아래에서는
LangChain은 다음과 같은 다른 옵션을 지원합니다.
from langchain_experimental.graph_transformers import LLMGraphTransformer from langchain_openai import ChatOpenAI from langchain_core.documents import Document # Prompt used by LLMGraphTransformer is tuned for Gpt4. llm = ChatOpenAI(temperature=0, model_name="gpt-4") llm_transformer = LLMGraphTransformer(llm=llm) text = """ Marie Curie, was a Polish and naturalised-French physicist and chemist who conducted pioneering research on radioactivity. She was the first woman to win a Nobel Prize, the first person to win a Nobel Prize twice, and the only person to win a Nobel Prize in two scientific fields. Her husband, Pierre Curie, was a co-winner of her first Nobel Prize, making them the first-ever married couple to win the Nobel Prize and launching the Curie family legacy of five Nobel Prizes. She was, in 1906, the first woman to become a professor at the University of Paris. """ documents = [Document(page_content=text)] graph_documents = llm_transformer.convert_to_graph_documents(documents) print(f"Nodes:{graph_documents[0].nodes}") print(f"Relationships:{graph_documents[0].relationships}")
LangChain의 LLMGraphTransformer
사용하여 지식 그래프를 추출하는 방법을 보여줍니다. 저장소에 있는 render_graph_document
사용하여 시각적 검사를 위해 LangChain GraphDocument
렌더링할 수 있습니다.
향후 게시물에서는 지식 그래프 전체와 각 문서에서 추출된 하위 그래프를 모두 검토하는 방법과 프롬프트 엔지니어링 및 지식 엔지니어링을 적용하여 자동화된 추출을 개선하는 방법에 대해 논의하겠습니다.
지식 그래프를 사용하여 질문에 답하려면 여러 단계가 필요합니다. 먼저 지식 그래프 탐색을 시작할 위치를 식별합니다. 이 예에서는 LLM에게 질문에서 엔터티를 추출하도록 요청하겠습니다. 그런 다음 지식 그래프를 탐색하여 해당 시작점에서 주어진 거리 내의 모든 관계를 검색합니다. 기본 순회 깊이는 3입니다. 검색된 관계와 원래 질문은 LLM이 질문에 답할 수 있는 프롬프트와 컨텍스트를 만드는 데 사용됩니다.
지식 그래프 추출과 마찬가지로 질문의 엔터티 추출은 특수 모델이나 특정 프롬프트가 있는 LLM을 사용하여 수행할 수 있습니다. 단순화를 위해 추출할 형식에 대한 질문과 정보가 모두 포함된 다음 프롬프트와 함께 LLM을 사용하겠습니다. 우리는 적절한 구조를 얻기 위해 이름과 유형이 있는 Pydantic 모델을 사용합니다.
QUERY_ENTITY_EXTRACT_PROMPT = ( "A question is provided below. Given the question, extract up to 5 " "entity names and types from the text. Focus on extracting the key entities " "that we can use to best lookup answers to the question. Avoid stopwords.\n" "---------------------\n" "{question}\n" "---------------------\n" "{format_instructions}\n" ) def extract_entities(llm): prompt = ChatPromptTemplate.from_messages([keyword_extraction_prompt]) class SimpleNode(BaseModel): """Represents a node in a graph with associated properties.""" id: str = Field(description="Name or human-readable unique identifier.") type: str = optional_enum_field(node_types, description="The type or label of the node.") class SimpleNodeList(BaseModel): """Represents a list of simple nodes.""" nodes: List[SimpleNode] output_parser = JsonOutputParser(pydantic_object=SimpleNodeList) return ( RunnablePassthrough.assign( format_instructions=lambda _: output_parser.get_format_instructions(), ) | ChatPromptTemplate.from_messages([QUERY_ENTITY_EXTRACT_PROMPT]) | llm | output_parser | RunnableLambda( lambda node_list: [(n["id"], n["type"]) for n in node_list["nodes"]]) )
위의 예제를 실행하면 추출된 엔터티를 볼 수 있습니다.
# Example showing extracted entities (nodes) extract_entities(llm).invoke({ "question": "Who is Marie Curie?"}) # Output: [Marie Curie(Person)]
물론 LangChain Runnable을 체인에서 사용하여 질문에서 엔터티를 추출할 수 있습니다.
앞으로는 노드 속성을 고려하거나 벡터 임베딩 및 유사성 검색을 사용하여 관련 시작점을 식별하는 등 엔터티 추출을 개선하는 방법에 대해 논의하겠습니다. 이 첫 번째 게시물을 단순하게 유지하기 위해 위의 프롬프트를 고수하고 지식 그래프를 순회하여 knowledge-subgraph
검색하고 이를 프롬프트의 컨텍스트로 포함시킵니다.
이전 체인은 문제의 노드를 제공합니다. 해당 엔터티와 그래프 저장소를 사용하여 관련 지식 트리플을 검색할 수 있습니다. RAG와 마찬가지로 이를 컨텍스트의 일부로 프롬프트에 추가하고 답변을 생성합니다.
def _combine_relations(relations): return "\n".join(map(repr, relations)) ANSWER_PROMPT = ( "The original question is given below." "This question has been used to retrieve information from a knowledge graph." "The matching triples are shown below." "Use the information in the triples to answer the original question.\n\n" "Original Question: {question}\n\n" "Knowledge Graph Triples:\n{context}\n\n" "Response:" ) chain = ( { "question": RunnablePassthrough() } # extract_entities is provided by the Cassandra knowledge graph library # and extracts entitise as shown above. | RunnablePassthrough.assign(entities = extract_entities(llm)) | RunnablePassthrough.assign( # graph_store.as_runnable() is provided by the CassandraGraphStore # and takes one or more entities and retrieves the relevant sub-graph(s). triples = itemgetter("entities") | graph_store.as_runnable()) | RunnablePassthrough.assign( context = itemgetter("triples") | RunnableLambda(_combine_relations)) | ChatPromptTemplate.from_messages([ANSWER_PROMPT]) | llm )
위의 체인은 질문에 답하기 위해 실행될 수 있습니다. 예를 들어:
chain.invoke("Who is Marie Curie?") # Output AIMessage( content="Marie Curie is a Polish and French chemist, physicist, and professor who " "researched radioactivity. She was married to Pierre Curie and has worked at " "the University of Paris. She is also a recipient of the Nobel Prize.", response_metadata={ 'token_usage': {'completion_tokens': 47, 'prompt_tokens': 213, 'total_tokens': 260}, 'model_name': 'gpt-4', ... } )
그래프 DB를 사용하여 지식 그래프를 저장하는 것이 직관적으로 보일 수 있지만 실제로는 반드시 필요한 것은 아닙니다. 몇 개의 노드 주변의 하위 지식 그래프를 검색하는 것은 간단한 그래프 탐색인 반면, 그래프 DB는 특정 속성 시퀀스가 있는 경로를 검색하는 훨씬 더 복잡한 쿼리를 위해 설계되었습니다. 또한, 더 멀리 제거된 노드는 꽤 빨리 질문과 관련이 없게 되기 때문에 순회는 깊이 2 또는 3까지만 수행되는 경우가 많습니다. 이는 몇 차례의 간단한 쿼리(각 단계당 하나씩) 또는 SQL 조인으로 표현될 수 있습니다.
별도의 그래프 데이터베이스가 필요하지 않으므로 지식 그래프를 더 쉽게 사용할 수 있습니다. 또한 Astra DB 또는 Apache Cassandra를 사용하면 동일한 위치에 저장된 그래프와 기타 데이터에 대한 트랜잭션 쓰기가 단순화되고 확장성이 향상될 가능성이 높습니다. 이러한 오버헤드는 Gremlin이나 Cypher 또는 이와 유사한 것을 사용하여 그래프 쿼리를 생성하고 실행하려는 경우에만 가치가 있습니다.
그러나 이는 하위 지식 그래프를 검색하기에는 과잉일 뿐이며 성능 측면에서 궤도를 벗어나는 쿼리와 같은 다른 문제의 호스트에 대한 문을 열어줍니다.
이 순회는 Python에서 구현하기 쉽습니다. CQL 및 Cassandra 드라이버를 사용하여 이를(동기적 및 비동기적으로) 구현하는 전체 코드는 다음에서 찾을 수 있습니다.
def fetch_relation(tg: asyncio.TaskGroup, depth: int, source: Node) -> AsyncPagedQuery: paged_query = AsyncPagedQuery( depth, session.execute_async(query, (source.name, source.type)) ) return tg.create_task(paged_query.next()) results = set() async with asyncio.TaskGroup() as tg: if isinstance(start, Node): start = [start] discovered = {t: 0 for t in start} pending = {fetch_relation(tg, 1, source) for source in start} while pending: done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED) for future in done: depth, relations, more = future.result() for relation in relations: results.add(relation) # Schedule the future for more results from the same query. if more is not None: pending.add(tg.create_task(more.next())) # Schedule futures for the next step. if depth < steps: # We've found a path of length `depth` to each of the targets. # We need to update `discovered` to include the shortest path. # And build `to_visit` to be all of the targets for which this is # the new shortest path. to_visit = set() for r in relations: previous = discovered.get(r.target, steps + 1) if depth < previous: discovered[r.target] = depth to_visit.add(r.target) for source in to_visit: pending.add(fetch_relation(tg, depth + 1, source)) return results
이 기사에서는 질문 답변을 위한 지식 그래프 추출 및 검색을 구축하고 사용하는 방법을 보여주었습니다. 여기서 중요한 점은 현재 이 작업을 수행하기 위해 Gremlin이나 Cypher와 같은 그래프 쿼리 언어가 포함된 그래프 데이터베이스가 필요하지 않다는 것입니다. 많은 쿼리를 병렬로 효율적으로 처리하는 Astra와 같은 훌륭한 데이터베이스는 이미 이를 처리할 수 있습니다.
실제로 특정 쿼리에 응답하는 데 필요한 하위 지식 그래프를 검색하기 위해 간단한 쿼리 시퀀스를 작성할 수 있습니다. 이를 통해 아키텍처를 단순하게 유지하고(추가 종속성 없음) 다음을 수행할 수 있습니다.
우리는 이와 동일한 아이디어를 사용하여 Cassandra 및 Astra DB에 대한 GraphRAG 패턴을 구현했습니다. 우리는 이를 LangChain에 기여하고 향후 LLM과 함께 지식 그래프 사용에 다른 개선 사항을 적용하기 위해 노력할 것입니다!
작성자: Ben Chambers, DataStax