RAG를 위한 테스트셋 생성(Testset Generation for RAG)
RAG를 위한 테스트셋 생성(Testset Generation for RAG)
RAG 애플리케이션에서 사용자가 문서 세트와 상호작용할 때, 시스템이 마주칠 수 있는 쿼리 패턴은 여러 가지예요. Ragas는 이런 다양한 쿼리를 만들어 내기 위해 Knowledge Graph(지식 그래프) 기반 접근을 사용해요. 먼저 RAG에서 마주칠 수 있는 쿼리 유형부터 이해해볼게요.
출처: 문서
본문
RAG의 쿼리 유형(Query types in RAG)
graph TD
A[Queries] --> B[Single-Hop Query]
A --> C[Multi-Hop Query]
B --> D1[Specific Query]
B --> E1[Abstract Query]
C --> F1[Specific Query]
C --> G1[Abstract Query]
Single-Hop 쿼리(Single-Hop Query)
single-hop 쿼리는 관련 답변을 얻기 위해 단일 문서 또는 소스에서 정보를 검색해야 하는 단순한 질문이에요. 답에 도달하는 데 오직 한 단계만 걸려요.
예시(특정 쿼리, Specific Query):
- “What year did Albert Einstein publish the theory of relativity?” 이것은 그 정보를 담은 문서에서 한 번의 검색으로 답할 수 있는 구체적이고 사실 기반 질문이에요.
예시(추상 쿼리, Abstract Query):
- “How did Einstein’s theory change our understanding of time and space?” 이 쿼리는 여전히 단일 개념(상대성 이론)을 가리키지만, 출처 자료에서 더 추상적이거나 해석적인 설명이 필요해요.
Multi-Hop 쿼리(Multi-Hop Query)
multi-hop 쿼리는 두 개 이상의 소스에서 정보가 필요한 여러 단계의 추론을 포함해요. 시스템은 다양한 문서에서 정보를 검색하고 점들을 연결해 정확한 답을 만들어야 해요.
예시(특정 쿼리, Specific Query):
- “Which scientist influenced Einstein’s work on relativity, and what theory did they propose?” 이것은 Einstein에게 영향을 준 과학자와 특정 이론 둘 다에 대한 정보를, 아마도 두 개의 다른 소스에서 검색해야 해요.
예시(추상 쿼리, Abstract Query):
- “How have scientific theories on relativity evolved since Einstein’s original publication?” 이 추상 쿼리는 시간에 따라 여러 소스에 걸쳐 여러 정보 조각을 검색해 이론의 진화에 대한 광범위하고 해석적인 응답을 형성해야 해요.
RAG에서의 특정 vs 추상 쿼리(Specific vs. Abstract Queries in a RAG)
-
특정 쿼리(Specific Query): 명확하고 사실 기반의 검색에 초점을 맞춰요. RAG의 목표는 구체적인 질문을 직접 다루는 하나 이상의 문서에서 매우 관련성 높은 정보를 검색하는 거예요.
-
추상 쿼리(Abstract Query): 더 광범위하고 해석적인 응답이 필요해요. RAG에서 추상 쿼리는 단순한 사실이 아닌 상위 수준의 추론, 설명, 의견을 담은 문서를 검색하도록 검색 시스템에 도전을 줘요.
single-hop과 multi-hop 두 경우 모두에서 특정·추상 쿼리의 구분은 정밀도(특정)에 초점을 맞출지, 더 넓은 아이디어의 종합(추상)에 초점을 맞출지 결정함으로써 검색·생성 과정을 형성해요.
서로 다른 유형의 쿼리는 서로 다른 컨텍스트를 합성해서 만들어져야 해요. 이 문제를 해결하기 위해 Ragas는 테스트셋 생성에 Knowledge Graph 기반 접근을 사용해요.
Knowledge Graph 생성(Knowledge Graph Creation)
주어진 문서 세트에서 다양한 유형의 쿼리를 만들고 싶다면, LLM이 쿼리를 만들 수 있게 하는 올바른 청크(chunk) 또는 문서 세트를 식별하는 것이 주요 도전 과제예요. 이 문제를 해결하기 위해 Ragas는 테스트셋 생성에 Knowledge Graph 기반 접근을 사용해요.
knowledge graph creation
Knowledge graph는 다음 구성 요소를 사용해 생성돼요:
문서 분할기(Document Splitter)
문서는 계층적 노드를 형성하도록 청킹(chunking)돼요. 청킹은 서로 다른 분할기(splitter)로 할 수 있어요. 예를 들어 금융 문서의 경우 손익계산서(Income Statement), 대차대조표(Balance Sheet), 현금흐름표(Cash Flow Statement) 같은 섹션에 따라 문서를 분할하는 splitter로 청킹할 수 있어요. 자신의 도메인에 관련된 섹션을 기준으로 문서를 분할하는 커스텀 splitter를 직접 작성할 수도 있어요.
예시(Example)
from ragas.testset.graph import Node
sample_nodes = [Node(
properties={"page_content": "Einstein's theory of relativity revolutionized our understanding of space and time. It introduced the concept that time is not absolute but can change depending on the observer's frame of reference."}
),Node(
properties={"page_content": "Time dilation occurs when an object moves close to the speed of light, causing time to pass slower relative to a stationary observer. This phenomenon is a key prediction of Einstein's special theory of relativity."}
)]
sample_nodes
출력(Output):
[Node(id: 4f6b94, type: , properties: ['page_content']),
Node(id: 952361, type: , properties: ['page_content'])]
graph TD
A[Node: 4f6b94] -.-> |Properties| A1[page_content]
B[Node: 952361] -.-> |Properties| B1[page_content]
추출기(Extractors)
각 노드에서 정보를 추출하는 데 서로 다른 추출기(extractor)가 사용되며, 이 정보는 노드 간 관계를 수립하는 데 쓰여요. 예를 들어 금융 문서의 경우 회사명 같은 엔티티를 추출하는 엔티티 추출기, 각 노드에 있는 중요한 키프레이즈를 추출하는 키프레이즈 추출기 등을 사용할 수 있어요. 자신의 도메인에 관련된 정보를 추출하는 커스텀 추출기를 직접 작성할 수도 있어요.
추출기는 LLMBasedExtractor에서 상속받은 LLM 기반 또는 Extractor에서 상속받은 규칙 기반일 수 있어요.
예시(Example)
knowledge graph에서 샘플 노드가 있다고 해볼게요. NERExtractor를 사용해 노드에서 명명된 엔티티(named entities)를 추출할 수 있어요.
from ragas.testset.transforms.extractors import NERExtractor
extractor = NERExtractor()
output = [await extractor.extract(node) for node in sample_nodes]
output[0]
추출기 유형과 추출된 정보의 튜플을 반환해요.
('entities', ['Einstein', 'theory of relativity', 'space', 'time', "observer's frame of reference"])
추출된 정보를 노드에 추가해요.
_ = [node.properties.update({key:val}) for (key,val), node in zip(output, sample_nodes)]
sample_nodes[0].properties
출력(Output):
{'page_content': "Einstein's theory of relativity revolutionized our understanding of space and time. It introduced the concept that time is not absolute but can change depending on the observer's frame of reference.",
'entities': ['Einstein', 'theory of relativity', 'space', 'time', 'observer']}
graph TD
A[Node: 4f6b94] -.-> |Properties| A1[page_content]
A -.-> |Properties| A2[entities]
B[Node: 952361] -.-> |Properties| B1[page_content]
B -.-> |Properties| B2[entities]
관계 구축기(Relationship builder)
추출된 정보는 노드 간 관계를 수립하는 데 사용돼요. 예를 들어 금융 문서의 경우 노드에 있는 엔티티를 기반으로 노드 간 관계를 수립할 수 있어요. 자신의 도메인에 관련된 정보를 기반으로 노드 간 관계를 수립하는 커스텀 relationship builder를 직접 작성할 수 있어요.
예시(Example)
from ragas.testset.graph import KnowledgeGraph
from ragas.testset.transforms.relationship_builders.traditional import JaccardSimilarityBuilder
kg = KnowledgeGraph(nodes=sample_nodes)
rel_builder = JaccardSimilarityBuilder(property_name="entities", key_name="PER", new_property_name="entity_jaccard_similarity")
relationships = await rel_builder.transform(kg)
relationships
출력(Output):
[Relationship(Node(id: 4f6b94) <-> Node(id: 952361), type: jaccard_similarity, properties: ['entity_jaccard_similarity'])]
두 노드 모두 "Einstein"이라는 같은 엔티티를 가지므로, 엔티티 유사성을 기반으로 노드 간 관계가 수립돼요.
graph TD
A[Node: 4f6b94] -.-> |Properties| A1[page_content]
A -.-> |Properties| A2[entities]
B[Node: 952361] -.-> |Properties| B1[page_content]
B -.-> |Properties| B2[entities]
A ===|entity_jaccard_similarity| B
이제 위 구성 요소를 transform으로 사용해 knowledge graph를 구축하는 방법을 이해해볼게요. transform은 당신의 작업을 더 쉽게 만들어줘요.
변환(Transforms)
knowledge graph를 구축하는 데 사용된 모든 구성 요소는 단일 transform으로 결합할 수 있고, 이 transform을 knowledge graph에 적용해 knowledge graph를 구축할 수 있어요. Transform은 knowledge graph에 순차적으로 적용되는 구성 요소 목록으로 이루어져요. 구성 요소의 병렬 처리도 처리할 수 있어요. apply_transforms 메서드는 transform을 knowledge graph에 적용하는 데 사용돼요.
예시(Example)
위 구성 요소로 transform을 사용해 위의 knowledge graph를 구축해볼게요.
from ragas.testset.transforms import apply_transforms
transforms = [
extractor,
rel_builder
]
apply_transforms(kg,transforms)
구성 요소 중 일부를 병렬로 적용하려면 Parallel 클래스로 감싸면 돼요.
from ragas.testset.transforms import KeyphraseExtractor, NERExtractor
from ragas.testset.transforms import apply_transforms, Parallel
tranforms = [
Parallel(
KeyphraseExtractor(),
NERExtractor()
),
rel_builder
]
apply_transforms(kg,transforms)
knowledge graph가 생성되면 그래프를 탐색해 다양한 유형의 쿼리를 생성할 수 있어요. 예를 들어 "Compare the revenue growth of Company X and Company Y from FY2020 through FY2023" 쿼리를 생성하려면, 그래프를 탐색해 Company X와 Company Y의 FY2020부터 FY2023까지 매출 성장 정보를 담은 노드를 찾을 수 있어요.
시나리오 생성(Scenario Generation)
이제 어떤 유형의 쿼리든 만들 수 있는 올바른 컨텍스트를 제조하는 데 쓸 수 있는 knowledge graph가 생겼어요. 사용자 집단이 RAG 시스템과 상호작용할 때, 그들은 자신의 페르소나(예: Senior Engineer, Junior Engineer 등), 쿼리 길이(Short, Long 등), 쿼리 스타일(Formal, Informal 등)에 따라 다양한 방식으로 쿼리를 구성할 수 있어요. 이 모든 시나리오를 다루는 쿼리를 생성하기 위해 Ragas는 테스트셋 생성에 Scenario 기반 접근을 사용해요.
테스트셋 생성에서 각 Scenario는 다음 파라미터의 결합이에요.
- Nodes : 쿼리를 생성하는 데 사용되는 노드
- Query Length : 원하는 쿼리의 길이, short, medium, long 등이 될 수 있어요.
- Query Style : 쿼리의 스타일, web search, chat 등이 될 수 있어요.
- Persona : 사용자의 페르소나, Senior Engineer, Junior Engineer 등이 될 수 있어요. (곧 제공 예정)
Scenario in Test Generation
쿼리 합성기(Query Synthesizer)
QuerySynthesizer는 단일 쿼리 유형에 대해 서로 다른 시나리오를 생성하는 역할을 해요. generate_scenarios 메서드는 단일 쿼리 유형에 대한 시나리오를 생성하는 데 사용돼요. generate_sample 메서드는 단일 시나리오에 대해 쿼리와 reference 답변을 생성하는 데 사용돼요. 예시로 이해해볼게요.
예시(Example)
이전 예시에서 엔티티 유사성을 기반으로 서로 관련된 두 노드를 포함하는 knowledge graph를 만들었어요. 이제 KG에 엔티티 유사성을 기반으로 서로 관련된 노드 쌍이 20개 있다고 상상해보세요.
두 엔티티를 비교하는 추상 질문에 관한 50개의 서로 다른 쿼리를 만드는 것이 목표라고 해볼게요. 먼저 KG를 쿼리해 엔티티 유사성을 기반으로 서로 관련된 노드 쌍을 얻어야 해요. 그런 다음 50개의 다른 시나리오를 얻을 때까지 각 노드 쌍에 대한 시나리오를 생성해야 해요. 이 로직은 generate_scenarios 메서드에 구현돼 있어요.
from dataclasses import dataclass
from ragas.testset.synthesizers.base_query import QuerySynthesizer
@dataclass
class EntityQuerySynthesizer(QuerySynthesizer):
async def _generate_scenarios( self, n, knowledge_graph, callbacks):
"""
logic to query nodes with entity
logic describing how to combine nodes,styles,length,persona to form n scenarios
"""
return scenarios
async def _generate_sample(
self, scenario, callbacks
):
"""
logic on how to use tranform each scenario to EvalSample (Query,Context,Reference)
you may create singleturn or multiturn sample
"""
return SingleTurnSample(user_input=query, reference_contexs=contexts, reference=reference)