Example Selector는 특정 작업에 적합한 예제를 동적으로 선택하여 프롬프트에 포함시키는 데 사용됩니다. 특히, 유사도 검색 기반 Example Selector는 입력 쿼리와 저장된 예제 간의 의미적 유사성을 평가하여 가장 관련성 높은 예제를 선택합니다. 그러나 유사도 검색 과정에서 발생할 수 있는 문제들—예를 들어, 부정확한 임베딩, 검색 결과의 낮은 품질, 또는 계산 비용 문제—는 모델 성능에 직접적인 영향을 미칩니다. 이 챕터에서는 유사도 검색 문제를 해결하는 방법에 대해 다룹니다.
유사도 검색의 핵심 개념
유사도 검색은 입력 쿼리와 예제 데이터를 임베딩 벡터로 변환한 뒤, 코사인 유사도(Cosine Similarity)와 같은 메트릭을 사용하여 가장 유사한 예제를 선택하는 과정입니다. EXAONE 3.5와 같은 모델은 고품질의 텍스트 임베딩을 생성할 수 있는 강력한 언어 모델로, LangChain의 SemanticSimilarityExampleSelector와 통합하여 효율적인 예제 선택을 지원합니다. 하지만 다음과 같은 문제들이 발생할 수 있습니다:
- 임베딩 품질 문제: 입력 쿼리나 예제 데이터의 의미가 부정확하게 표현될 경우.
- 벡터 저장소 검색 효율성: 대규모 데이터셋에서 검색 속도가 느려지는 문제.
- 컨텍스트 불일치: 선택된 예제가 작업 컨텍스트와 맞지 않는 경우.
이러한 문제들을 다음과 같은 방법으로 해결해볼 수 있습니다.
1) 고품질 임베딩 생성
LLM 모델은 다국어 지원과 고차원 임베딩 생성 능력이 뛰어나지만, 입력 데이터의 전처리가 중요합니다. 아래는 임베딩 품질을 개선하기 위한 방법입니다:
- 텍스트 정규화: 입력 쿼리와 예제 데이터를 정규화하여 불필요한 노이즈(예: 특수 문자, 불용어)를 제거합니다.
- 도메인 특화 fine-tuning: LLM 모델을 특정 도메인 데이터로 미세 조정하여 임베딩의 도메인 적합성을 높입니다. 예를 들어, 법률 관련 작업이라면 법률 문서로 fine-tuning을 진행합니다.
- 배치 처리 최적화: Ollama의 API를 활용하여 대량의 예제 데이터를 한 번에 임베딩 처리함으로써 계산 효율성을 높입니다.
2) 벡터 저장소 최적화
LangChain에서 벡터 저장소는 유사도 검색의 핵심 구성 요소입니다. Ollama 기반 환경에서는 로컬 벡터 저장소(예: FAISS)와 LLM 모델의 임베딩을 결합하여 효율적인 검색을 구현할 수 있습니다. 최적화 방법은 다음과 같습니다:
- 인덱싱 전략: FAISS의 HNSW(Hierarchical Navigable Small World) 인덱스를 사용하여 검색 속도를 높입니다.
- 차원 축소: PCA(Principal Component Analysis) 또는 UMAP을 활용하여 임베딩 차원을 축소함으로써 검색 속도를 개선하고 메모리 사용량을 줄입니다.
- 캐싱: 자주 사용되는 쿼리의 검색 결과를 캐싱하여 반복적인 계산을 방지합니다.
3) 컨텍스트 일치도 향상
유사도 검색 결과가 작업 컨텍스트와 맞지 않는 경우, 선택된 예제가 프롬프트에 부정적인 영향을 미칠 수 있습니다. 이를 해결하기 위한 방법은 다음과 같습니다:
- 메타데이터 필터링: 예제 데이터에 태그나 카테고리와 같은 메타데이터를 추가하여 검색 범위를 좁힙니다. 예를 들어, “질문 유형”이나 “도메인”별로 예제를 분류합니다.
- 하이브리드 검색: 키워드 기반 검색과 유사도 검색을 결합하여 의미적 유사성과 키워드 일치를 모두 고려합니다.
- 예제 다양성 관리: 너무 유사한 예제만 선택되지 않도록, 선택된 예제 간의 다양성을 보장하는 알고리즘(예: Maximal Marginal Relevance, MMR)을 적용합니다.
4) 계산 비용 절감
Ollama 기반 로컬 환경에서는 계산 리소스가 제한적일 수 있습니다. 이를 해결하기 위해:
- 모델 경량화: 경량화된 LLM 모델버전을 사용하거나, 양자화(Quantization)된 모델을 사용합니다.
- 비동기 처리: LangChain의 비동기 API(async/await)를 활용하여 병렬 처리를 구현합니다.
- 샘플링: 전체 예제 데이터셋 대신, 작업에 적합한 샘플 데이터만 사용하여 검색 부담을 줄입니다.
아래는 FAISS와 EXAONE 3.5를 사용하여 SemanticSimilarityExampleSelector를 구현하는 예제 코드입니다.
from langchain.prompts import SemanticSimilarityExampleSelector
from langchain_community.vectorstores import FAISS
from langchain_ollama import OllamaEmbeddings
from langchain.prompts import FewShotPromptTemplate, PromptTemplate
# 예제 데이터
examples = [
{"input": "프로그래밍에서 함수란 무엇인가?", "output": "함수는 특정 작업을 수행하는 코드 블록으로..."},
{"input": "법률 문서에서 계약 해지 조건은 무엇인가?", "output": "계약 해지 조건은 계약서에 명시된 조항에 따라 다르며..."},
{"input": "이 공의 색깔은 무엇인가요?", "output": "발간색입니다."},
{"input": "집에서 회사까지 얼마나 걸리나요?", "output": "1시간 정도..."},
]
# EXAONE 3.5 임베딩 설정 (Ollama 기반)
embeddings = OllamaEmbeddings(model="exaone3.5")
# FAISS 벡터 저장소 생성 (메타데이터 수정)
vectorstore = FAISS.from_texts(
texts=[example["input"] for example in examples],
embedding=embeddings,
metadatas=examples # 전체 예제 객체를 메타데이터로 저장
)
# Example Selector 설정
example_selector = SemanticSimilarityExampleSelector(
vectorstore=vectorstore,
k=1,
)
# 프롬프트 템플릿 정의
example_prompt = PromptTemplate(
input_variables=["input", "output"],
template="입력: {input}\n출력: {output}\n"
)
prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
prefix="다음은 질문과 답변 예제입니다:",
suffix="입력: {user_input}\n출력:",
input_variables=["user_input"]
)
# 테스트
user_input = "계약 해지 절차는 어떻게 되나요?"
formatted_prompt = prompt.format(user_input=user_input)
print(formatted_prompt)
다음은 질문과 답변 예제입니다:
입력: 법률 문서에서 계약 해지 조건은 무엇인가?
출력: 계약 해지 조건은 계약서에 명시된 조항에 따라 다르며…
입력: 계약 해지 절차는 어떻게 되나요?
출력:
구현 후에는 다음과 같은 방법으로 성능을 평가하고 개선할 수 있습니다:
- 정확도 평가: 선택된 예제가 작업 결과에 미치는 영향을 정량적으로 측정합니다. 예를 들어, 생성된 답변의 BLEU 점수나 사용자 피드백을 활용합니다.
- A/B 테스트: 서로 다른 임베딩 모델이나 검색 알고리즘을 비교하여 최적의 조합을 찾습니다.
- 로그 분석: Ollama의 로그를 분석하여 병목 지점을 식별하고 최적화합니다.
유사도 검색 문제를 해결하려면 임베딩 품질, 벡터 저장소 효율성, 컨텍스트 일치도, 그리고 계산 비용을 종합적으로 고려해야 합니다. 고품질 임베딩 생성, 벡터 저장소 최적화, 하이브리드 검색, 그리고 비동기 처리와 같은 전략을 통해 Example Selector의 성능을 극대화할 수 있습니다.