Post

[LangChain] ๐Ÿ”€ Ensemble Retriever๋กœ RAG ๊ฒ€์ƒ‰ ์ •ํ™•๋„ ๋†’์ด๊ธฐ: BM25 + Vector ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰

LangChain Ensemble Retriever๋กœ BM25 ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰๊ณผ Vector ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ๊ฒฐํ•ฉํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ RAG ๊ตฌ์ถ• ๋ฐฉ๋ฒ•์„ ์ •๋ฆฌํ–ˆ์Šต๋‹ˆ๋‹ค. RRF ์•Œ๊ณ ๋ฆฌ์ฆ˜, weights ์„ค์ •, ์‹ค๋ฌด ์ฝ”๋“œ ์˜ˆ์‹œ๊นŒ์ง€ ๋‹ค๋ฃน๋‹ˆ๋‹ค.

[LangChain] ๐Ÿ”€ Ensemble Retriever๋กœ RAG ๊ฒ€์ƒ‰ ์ •ํ™•๋„ ๋†’์ด๊ธฐ: BM25 + Vector ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰

LangChain Ensemble Retriever๋Š” BM25 ํ‚ค์›Œ๋“œ ๊ฒ€์ƒ‰๊ณผ Vector ์˜๋ฏธ ๊ฒ€์ƒ‰์„ ๊ฒฐํ•ฉํ•˜์—ฌ ๋‹จ์ผ ๊ฒ€์ƒ‰๊ธฐ๋ณด๋‹ค ๋†’์€ ์ •ํ™•๋„๋ฅผ ์ œ๊ณตํ•˜๋Š” ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰ ๋ฐฉ์‹์ž…๋‹ˆ๋‹ค. ๋‘ ๊ฒ€์ƒ‰๊ธฐ์˜ ๊ฒฐ๊ณผ๋ฅผ RRF(Reciprocal Rank Fusion) ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์žฌ์ˆœ์œ„ํ™”ํ•˜์—ฌ ์ตœ์ ์˜ ๋ฌธ์„œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ์ด ๊ธ€์—์„œ๋Š” Ensemble Retriever์˜ ๋™์ž‘ ์›๋ฆฌ๋ถ€ํ„ฐ BM25 + FAISS ์กฐํ•ฉ ์ฝ”๋“œ, weights ํŠœ๋‹, ์‹ค๋ฌด ์ ์šฉ ํŒ๊นŒ์ง€ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.


๐Ÿค” ์™œ ๋‹จ์ผ ๊ฒ€์ƒ‰๊ธฐ๋กœ๋Š” ๋ถ€์กฑํ•œ๊ฐ€?

RAG(Retrieval-Augmented Generation) ์‹œ์Šคํ…œ์—์„œ ๊ฒ€์ƒ‰ ํ’ˆ์งˆ์€ LLM ๋‹ต๋ณ€์˜ ํ’ˆ์งˆ์„ ์ง์ ‘ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฐ๋ฐ ํ˜„์žฌ ๊ฐ€์žฅ ๋งŽ์ด ์“ฐ์ด๋Š” ๋‘ ๊ฐ€์ง€ ๊ฒ€์ƒ‰ ๋ฐฉ์‹์€ ๊ฐ๊ฐ ๋šœ๋ ทํ•œ ํ•œ๊ณ„๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค.

์ž„๋ฒ ๋”ฉ ๋ฒกํ„ฐ์˜ ์ฝ”์‚ฌ์ธ ์œ ์‚ฌ๋„๋กœ ์˜๋ฏธ์ ์œผ๋กœ ์œ ์‚ฌํ•œ ๋ฌธ์„œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. ๋‹จ์–ด๊ฐ€ ๋‹ฌ๋ผ๋„ ์˜๋ฏธ๊ฐ€ ๊ฐ™์œผ๋ฉด ๊ฒ€์ƒ‰๋˜๋Š” ๊ฒƒ์ด ๊ฐ•์ ์ด์ง€๋งŒ, ์ •ํ™•ํ•œ ์šฉ์–ดยท๊ณ ์œ ๋ช…์‚ฌยท์ฝ”๋“œ ์‹๋ณ„์ž ๊ฒ€์ƒ‰์—๋Š” ์ทจ์•ฝํ•ฉ๋‹ˆ๋‹ค.

Sparse Retrieval (ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜ โ€” BM25)

๋‹จ์–ด ๋นˆ๋„(TF-IDF ๊ธฐ๋ฐ˜ BM25)๋กœ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. โ€œ์ฟ ๋ฒ„๋„คํ‹ฐ์Šคโ€, โ€œCVE-2024-1234โ€ ๊ฐ™์€ ์ •ํ™•ํ•œ ํ‚ค์›Œ๋“œ ์ผ์น˜์— ๊ฐ•ํ•˜์ง€๋งŒ, ๋™์˜์–ดยท๋ฌธ๋งฅ ๊ธฐ๋ฐ˜ ์งˆ๋ฌธ์—๋Š” ์•ฝํ•ฉ๋‹ˆ๋‹ค.

๋ฐฉ์‹๊ฐ•์ ์•ฝ์ 
Vector (Dense)์˜๋ฏธ ์œ ์‚ฌ์„ฑ, ๋™์˜์–ด, ๋งฅ๋ฝ ์ดํ•ด์ •ํ™•ํ•œ ์šฉ์–ดยท๊ณ ์œ ๋ช…์‚ฌยท์ฝ”๋“œ ๊ฒ€์ƒ‰
BM25 (Sparse)ํ‚ค์›Œ๋“œ ์ •ํ™• ์ผ์น˜, ๋„๋ฉ”์ธ ์ „๋ฌธ์šฉ์–ด์˜คํƒ€, ๋™์˜์–ด, ๋ฌธ๋งฅ ํŒŒ์•…
Hybrid (Ensemble)๋‘ ๋ฐฉ์‹์˜ ์žฅ์  ๊ฒฐํ•ฉ์„ค์ • ๋ณต์žก๋„ ์ฆ๊ฐ€

Tip: ์‹ค๋ฌด์—์„œ๋Š” ๋‘ ๋ฐฉ์‹์„ ๊ฒฐํ•ฉํ•œ ํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰์ด ๋‹จ์ผ ๋ฐฉ์‹๋ณด๋‹ค ์ผ๊ด€๋˜๊ฒŒ ๋” ๋‚˜์€ ์„ฑ๋Šฅ์„ ๋ณด์ž…๋‹ˆ๋‹ค.


๐Ÿ”€ Ensemble Retriever๋ž€?

Ensemble Retriever๋Š” ์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰๊ธฐ์˜ ๊ฒฐ๊ณผ๋ฅผ RRF(Reciprocal Rank Fusion) ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ๋ณ‘ํ•ฉํ•˜๋Š” LangChain ์ปดํฌ๋„ŒํŠธ์ž…๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
์ฟผ๋ฆฌ
 โ”œโ”€ BM25Retriever  โ†’ [๋ฌธ์„œA(1์œ„), ๋ฌธ์„œC(2์œ„), ๋ฌธ์„œE(3์œ„), ...]
 โ””โ”€ FAISSRetriever โ†’ [๋ฌธ์„œB(1์œ„), ๋ฌธ์„œA(2์œ„), ๋ฌธ์„œD(3์œ„), ...]
         โ†“
   RRF ์žฌ์ˆœ์œ„ํ™” (weights ์ ์šฉ)
         โ†“
   ์ตœ์ข… ๊ฒฐ๊ณผ: [๋ฌธ์„œA, ๋ฌธ์„œB, ๋ฌธ์„œC, ...]

RRF ์•Œ๊ณ ๋ฆฌ์ฆ˜

RRF(Reciprocal Rank Fusion)๋Š” ๊ฐ ๊ฒ€์ƒ‰๊ธฐ์—์„œ ๋ฐ˜ํ™˜๋œ ๋ฌธ์„œ์˜ ์ˆœ์œ„๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ตœ์ข… ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.

1
RRF ์ ์ˆ˜ = ฮฃ (weight_i / (rank_i + c))
  • rank_i โ€” ๊ฐ ๊ฒ€์ƒ‰๊ธฐ์—์„œ์˜ ์ˆœ์œ„ (1๋ถ€ํ„ฐ ์‹œ์ž‘)
  • weight_i โ€” ํ•ด๋‹น ๊ฒ€์ƒ‰๊ธฐ์˜ ๊ฐ€์ค‘์น˜
  • c โ€” ์ƒ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’ 60, ๋†’์„์ˆ˜๋ก ์ˆœ์œ„ ์ฐจ์ด ์™„ํ™”)

์ˆœ์œ„๋งŒ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์„œ๋กœ ๋‹ค๋ฅธ ์ ์ˆ˜ ์Šค์ผ€์ผ์„ ๊ฐ€์ง„ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ง์ ‘ ๊ฒฐํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค๋Š” ๊ฒƒ์ด ํ•ต์‹ฌ ์žฅ์ ์ž…๋‹ˆ๋‹ค.


๐Ÿš€ ์„ค์น˜

1
pip install langchain langchain-community langchain-openai faiss-cpu rank_bm25
ํŒจํ‚ค์ง€์šฉ๋„
langchain์ฝ”์–ด ํ”„๋ ˆ์ž„์›Œํฌ
langchain-communityBM25Retriever, FAISS ๋“ฑ ์ปค๋ฎค๋‹ˆํ‹ฐ ์ปดํฌ๋„ŒํŠธ
langchain-openaiOpenAI ์ž„๋ฒ ๋”ฉ
faiss-cpuFacebook AI ์œ ์‚ฌ๋„ ๊ฒ€์ƒ‰ (GPU ๋ฒ„์ „: faiss-gpu)
rank_bm25BM25 ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„์ฒด

1๏ธโƒฃ ๊ธฐ๋ณธ ์˜ˆ์ œ: BM25 + FAISS ๊ฒฐํ•ฉ

๊ฐ€์žฅ ์ผ๋ฐ˜์ ์ธ ์กฐํ•ฉ์ธ BM25์™€ FAISS๋ฅผ ์‚ฌ์šฉํ•œ Ensemble Retriever ๊ตฌ์„ฑ์ž…๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
from langchain.retrievers import EnsembleRetriever
from langchain_community.retrievers import BM25Retriever
from langchain_community.vectorstores import FAISS
from langchain_openai import OpenAIEmbeddings
from langchain_core.documents import Document

# ์ƒ˜ํ”Œ ๋ฌธ์„œ
docs = [
    Document(page_content="์ฟ ๋ฒ„๋„คํ‹ฐ์Šค(Kubernetes)๋Š” ์ปจํ…Œ์ด๋„ˆ ์˜ค์ผ€์ŠคํŠธ๋ ˆ์ด์…˜ ํ”Œ๋žซํผ์ž…๋‹ˆ๋‹ค."),
    Document(page_content="Docker๋Š” ์ปจํ…Œ์ด๋„ˆ ๊ธฐ๋ฐ˜ ๊ฐ€์ƒํ™” ๊ธฐ์ˆ ๋กœ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ํŒจํ‚ค์ง•ํ•ฉ๋‹ˆ๋‹ค."),
    Document(page_content="Helm์€ ์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ํŒจํ‚ค์ง€ ๋งค๋‹ˆ์ €๋กœ ์ฐจํŠธ๋ฅผ ํ†ตํ•ด ๋ฐฐํฌ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค."),
    Document(page_content="Prometheus๋Š” ์‹œ๊ณ„์—ด ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๊ธฐ๋ฐ˜์˜ ๋ชจ๋‹ˆํ„ฐ๋ง ์†”๋ฃจ์…˜์ž…๋‹ˆ๋‹ค."),
    Document(page_content="Istio๋Š” ์„œ๋น„์Šค ๋ฉ”์‹œ๋กœ ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๊ฐ„ ํ†ต์‹ ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค."),
]

# BM25 ๋ฆฌํŠธ๋ฆฌ๋ฒ„ (ํ‚ค์›Œ๋“œ ๊ธฐ๋ฐ˜)
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 3  # ๋ฐ˜ํ™˜ํ•  ๋ฌธ์„œ ์ˆ˜

# FAISS ๋ฆฌํŠธ๋ฆฌ๋ฒ„ (์˜๋ฏธ ๊ธฐ๋ฐ˜)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(docs, embeddings)
faiss_retriever = vectorstore.as_retriever(search_kwargs={"k": 3})

# Ensemble Retriever ๊ตฌ์„ฑ
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever],
    weights=[0.4, 0.6],  # BM25 40%, FAISS 60%
)

# ๊ฒ€์ƒ‰ ์‹คํ–‰
results = ensemble_retriever.invoke("์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ํŒจํ‚ค์ง€ ๊ด€๋ฆฌ ๋„๊ตฌ๋Š”?")
for doc in results:
    print(doc.page_content)

2๏ธโƒฃ ์‹ค์ „ ์˜ˆ์ œ: RAG ํŒŒ์ดํ”„๋ผ์ธ์— ํ†ตํ•ฉ

๋ฌธ์„œ ๋กœ๋”ฉ๋ถ€ํ„ฐ LLM ๋‹ต๋ณ€ ์ƒ์„ฑ๊นŒ์ง€ ์ „์ฒด RAG ํŒŒ์ดํ”„๋ผ์ธ์— Ensemble Retriever๋ฅผ ํ†ตํ•ฉํ•ฉ๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import TextLoader
from langchain.chains import RetrievalQA
from langchain_openai import ChatOpenAI

# 1. ๋ฌธ์„œ ๋กœ๋”ฉ ๋ฐ ๋ถ„ํ• 
loader = TextLoader("knowledge_base.txt", encoding="utf-8")
raw_docs = loader.load()

splitter = RecursiveCharacterTextSplitter(
    chunk_size=500,
    chunk_overlap=50,
)
docs = splitter.split_documents(raw_docs)

# 2. BM25 ๋ฆฌํŠธ๋ฆฌ๋ฒ„
bm25_retriever = BM25Retriever.from_documents(docs)
bm25_retriever.k = 5

# 3. FAISS ๋ฒกํ„ฐ์Šคํ† ์–ด
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectorstore = FAISS.from_documents(docs, embeddings)
faiss_retriever = vectorstore.as_retriever(
    search_type="mmr",           # ๋‹ค์–‘์„ฑ ํ™•๋ณด (Maximum Marginal Relevance)
    search_kwargs={"k": 5, "fetch_k": 20, "lambda_mult": 0.7}
)

# 4. Ensemble Retriever
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever],
    weights=[0.3, 0.7],
)

# 5. RAG ์ฒด์ธ ๊ตฌ์„ฑ
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
qa_chain = RetrievalQA.from_chain_type(
    llm=llm,
    chain_type="stuff",
    retriever=ensemble_retriever,
    return_source_documents=True,
)

# 6. ์งˆ์˜
response = qa_chain.invoke({"query": "์„œ๋น„์Šค ๋ฉ”์‹œ์˜ ์—ญํ• ์€ ๋ฌด์—‡์ธ๊ฐ€์š”?"})
print(response["result"])
print("\n--- ์ฐธ์กฐ ๋ฌธ์„œ ---")
for doc in response["source_documents"]:
    print(f"- {doc.page_content[:100]}...")

3๏ธโƒฃ ChromaDB + BM25 ์กฐํ•ฉ

FAISS ๋Œ€์‹  ChromaDB๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. ์˜๊ตฌ ์ €์žฅ์ด ํ•„์š”ํ•œ ํ”„๋กœ๋•์…˜ ํ™˜๊ฒฝ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from langchain_community.vectorstores import Chroma
from langchain_openai import OpenAIEmbeddings

# ChromaDB ๋ฒกํ„ฐ์Šคํ† ์–ด ์ƒ์„ฑ
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_documents(
    docs,
    embeddings,
    collection_name="my_collection",
    persist_directory="./chroma_db",  # ๋””์Šคํฌ ์ €์žฅ
)
chroma_retriever = vectorstore.as_retriever(search_kwargs={"k": 4})

# Ensemble ๊ตฌ์„ฑ
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, chroma_retriever],
    weights=[0.5, 0.5],
)

โš–๏ธ weights ์„ค์ • ๊ฐ€์ด๋“œ

weights๋Š” ๊ฐ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์ตœ์ข… ์ˆœ์œ„์— ๋ฏธ์น˜๋Š” ์˜ํ–ฅ๋ ฅ์„ ์กฐ์ ˆํ•ฉ๋‹ˆ๋‹ค. ํ•ฉ๊ณ„๊ฐ€ ๋ฐ˜๋“œ์‹œ 1.0์ผ ํ•„์š”๋Š” ์—†์ง€๋งŒ, ์ƒ๋Œ€์  ๋น„์œจ๋กœ ์ดํ•ดํ•˜๋ฉด ๋ฉ๋‹ˆ๋‹ค.

์ƒํ™ฉ๊ถŒ์žฅ weights (BM25 : Vector)
์ผ๋ฐ˜ QA (๊ท ํ˜•)[0.5, 0.5]
๊ธฐ์ˆ  ๋ฌธ์„œ, ์ฝ”๋“œ, ์ „๋ฌธ์šฉ์–ด[0.6, 0.4] โ€” BM25 ๊ฐ•ํ™”
๊ณ ๊ฐ ์ƒ๋‹ด, ์ž์—ฐ์–ด ์งˆ๋ฌธ[0.3, 0.7] โ€” Vector ๊ฐ•ํ™”
๋‰ด์Šคยท๋ธ”๋กœ๊ทธ ๋“ฑ ์ผ๋ฐ˜ ๋ฌธ์„œ[0.4, 0.6] โ€” Vector ์•ฝ๊ฐ„ ์šฐ์„ธ
๋ฒ•๋ฅ ยท์˜๋ฃŒ ์ „๋ฌธ ๋„๋ฉ”์ธ[0.7, 0.3] โ€” ์ •ํ™•ํ•œ ์šฉ์–ด ์šฐ์„ 
1
2
3
4
5
# ์„ธ ๊ฐœ์˜ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ๊ฒฐํ•ฉํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, faiss_retriever, chroma_retriever],
    weights=[0.3, 0.4, 0.3],
)

Tip: weights๋Š” ๋„๋ฉ”์ธ๊ณผ ์งˆ์˜ ํŒจํ„ด์— ๋”ฐ๋ผ ๋‹ค๋ฅด๋ฏ€๋กœ, ํ‰๊ฐ€ ๋ฐ์ดํ„ฐ์…‹์œผ๋กœ ์‹คํ—˜ํ•˜์—ฌ ์ตœ์ ๊ฐ’์„ ์ฐพ๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.


๐Ÿ”„ ๋‹ค๋ฅธ ๋‹ค์ค‘ ๊ฒ€์ƒ‰๊ธฐ ์ „๋žต๊ณผ ๋น„๊ต

Ensemble Retriever ์™ธ์—๋„ LangChain์ด ์ œ๊ณตํ•˜๋Š” ๋‹ค์ค‘ ๊ฒ€์ƒ‰๊ธฐ ์ „๋žต๋“ค์ด ์žˆ์Šต๋‹ˆ๋‹ค.

์ „๋žต์„ค๋ช…์ ํ•ฉํ•œ ๊ฒฝ์šฐ
EnsembleRetriever์—ฌ๋Ÿฌ ๊ฒ€์ƒ‰๊ธฐ ๊ฒฐ๊ณผ๋ฅผ RRF๋กœ ๋ณ‘ํ•ฉํ•˜์ด๋ธŒ๋ฆฌ๋“œ ๊ฒ€์ƒ‰์˜ ๊ธฐ๋ณธ
MultiQueryRetriever์งˆ๋ฌธ์„ ์—ฌ๋Ÿฌ ๋ณ€ํ˜•์œผ๋กœ ์žฌ์ƒ์„ฑ ํ›„ ๊ฒ€์ƒ‰๋ชจํ˜ธํ•œ ์งˆ๋ฌธ, ๋‹ค๊ฐ๋„ ๊ฒ€์ƒ‰
ContextualCompression๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ LLM์œผ๋กœ ์••์ถ•ยทํ•„ํ„ฐ๋ง๋ถˆํ•„์š”ํ•œ ์ •๋ณด ์ œ๊ฑฐ
ParentDocumentRetriever์ž‘์€ ์ฒญํฌ๋กœ ๊ฒ€์ƒ‰ ํ›„ ์›๋ณธ ์ƒ์œ„ ๋ฌธ์„œ ๋ฐ˜ํ™˜๋ฌธ๋งฅ ์†์‹ค ๋ฐฉ์ง€
SelfQueryRetrieverLLM์ด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ•„ํ„ฐ๋ฅผ ์ž๋™ ์ƒ์„ฑ๊ตฌ์กฐํ™”๋œ ํ•„ํ„ฐ ์กฐ๊ฑด ์ฒ˜๋ฆฌ

MultiQueryRetriever์™€ Ensemble ์กฐํ•ฉ

1
2
3
4
5
6
7
8
9
10
11
12
13
from langchain.retrievers.multi_query import MultiQueryRetriever

# MultiQuery๋กœ ์งˆ๋ฌธ์„ ๋‹ค๊ฐ๋„๋กœ ํ™•์žฅ
multi_query_retriever = MultiQueryRetriever.from_llm(
    retriever=faiss_retriever,
    llm=ChatOpenAI(temperature=0),
)

# MultiQuery + BM25๋ฅผ Ensemble๋กœ ๊ฒฐํ•ฉ
ensemble_retriever = EnsembleRetriever(
    retrievers=[bm25_retriever, multi_query_retriever],
    weights=[0.4, 0.6],
)

๐Ÿ—๏ธ ํ”„๋กœ๋•์…˜ ๊ณ ๋ ค์‚ฌํ•ญ

์„ฑ๋Šฅ ์ตœ์ ํ™”

1
2
3
4
5
6
7
8
9
10
11
# FAISS ๊ฒ€์ƒ‰ ํƒ€์ž…๋ณ„ ํŠน์„ฑ
faiss_retriever = vectorstore.as_retriever(
    search_type="mmr",           # ๋‹ค์–‘์„ฑ + ๊ด€๋ จ์„ฑ ๊ท ํ˜•
    # search_type="similarity",  # ์ˆœ์ˆ˜ ์œ ์‚ฌ๋„ (๊ธฐ๋ณธ๊ฐ’)
    # search_type="similarity_score_threshold",  # ์ตœ์†Œ ์ ์ˆ˜ ํ•„ํ„ฐ
    search_kwargs={
        "k": 5,
        "fetch_k": 20,           # mmr์—์„œ ํ›„๋ณด ํ’€ ํฌ๊ธฐ
        "lambda_mult": 0.5,      # 0=๋‹ค์–‘์„ฑ ์ตœ๋Œ€, 1=์œ ์‚ฌ๋„ ์ตœ๋Œ€
    }
)

๋Œ€์šฉ๋Ÿ‰ ๋ฌธ์„œ ์ฒ˜๋ฆฌ

1
2
3
4
5
6
7
8
9
# BM25 ๋ฆฌํŠธ๋ฆฌ๋ฒ„๋ฅผ ํŒŒ์ผ์—์„œ ์ง์ ‘ ๋กœ๋”ฉ (๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ)
from langchain_community.retrievers import BM25Retriever

# ์ฒญํฌ ํฌ๊ธฐ๋ฅผ ๊ณ ๋ คํ•œ k ์„ค์ •
bm25_retriever = BM25Retriever.from_documents(
    docs,
    bm25_params={"k1": 1.5, "b": 0.75}  # BM25 ํ•˜์ดํผํŒŒ๋ผ๋ฏธํ„ฐ ์กฐ์ •
)
bm25_retriever.k = 10

๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ‰๊ฐ€

1
2
3
4
5
6
7
8
9
10
11
12
# ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ’ˆ์งˆ ๊ฐ„๋‹จ ํ™•์ธ
def evaluate_retrieval(retriever, test_queries):
    for query, expected_keyword in test_queries:
        results = retriever.invoke(query)
        hit = any(expected_keyword in doc.page_content for doc in results)
        print(f"{'โœ…' if hit else 'โŒ'} [{query}] โ†’ {len(results)}๊ฐœ ๋ฐ˜ํ™˜")

test_cases = [
    ("์ฟ ๋ฒ„๋„คํ‹ฐ์Šค ์„ค์น˜ ๋ฐฉ๋ฒ•", "Kubernetes"),
    ("์ปจํ…Œ์ด๋„ˆ ํŒจํ‚ค์ง•", "Docker"),
]
evaluate_retrieval(ensemble_retriever, test_cases)

๐Ÿ’ก ์‹ค๋ฌด ์ ์šฉ ํŒ

BM25 ํ† ํฌ๋‚˜์ด์ € ์ปค์Šคํ„ฐ๋งˆ์ด์ง• โ€” ํ•œ๊ตญ์–ด ๋ฌธ์„œ๋Š” ๊ธฐ๋ณธ ํ† ํฌ๋‚˜์ด์ €๊ฐ€ ํ˜•ํƒœ์†Œ๋ฅผ ๊ณ ๋ คํ•˜์ง€ ์•Š์•„ ์„ฑ๋Šฅ์ด ๋–จ์–ด์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. konlpy ๋“ฑ์˜ ํ•œ๊ตญ์–ด ํ˜•ํƒœ์†Œ ๋ถ„์„๊ธฐ๋ฅผ ์ „์ฒ˜๋ฆฌ ๋‹จ๊ณ„์— ์ถ”๊ฐ€ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค.

์ฒญํฌ ํฌ๊ธฐ์™€ k์˜ ๊ท ํ˜• โ€” ์ฒญํฌ๊ฐ€ ์ž‘์„์ˆ˜๋ก ๋” ๋งŽ์€ k๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. ์ผ๋ฐ˜์ ์œผ๋กœ 500~1000์ž ์ฒญํฌ์— k=4~8์ด ์ ์ ˆํ•ฉ๋‹ˆ๋‹ค.

์ค‘๋ณต ๋ฌธ์„œ ์ฒ˜๋ฆฌ โ€” ๋‘ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๊ฐ™์€ ๋ฌธ์„œ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉด EnsembleRetriever๊ฐ€ ์ž๋™์œผ๋กœ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๊ณ  ์ ์ˆ˜๋ฅผ ํ•ฉ์‚ฐํ•ฉ๋‹ˆ๋‹ค.

์ ์ง„์  ๋„์ž… โ€” ๊ธฐ์กด Vector ๊ฒ€์ƒ‰๋งŒ ์‚ฌ์šฉํ•˜๋˜ ์‹œ์Šคํ…œ์ด๋ผ๋ฉด, weights=[0.2, 0.8]์ฒ˜๋Ÿผ BM25 ๋น„์ค‘์„ ๋‚ฎ๊ฒŒ ์‹œ์ž‘ํ•˜์—ฌ ์ ์ง„์ ์œผ๋กœ ์กฐ์ •ํ•˜์„ธ์š”.


โ“ ์ž์ฃผ ๋ฌป๋Š” ์งˆ๋ฌธ

Q. BM25Retriever์™€ EnsembleRetriever ์ค‘ ๋ฌด์—‡์ด ๋” ๋‚˜์€๊ฐ€์š”?

๋‹จ๋…์œผ๋กœ๋Š” ์šฐ์—ด์„ ๊ฐ€๋ฆด ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. BM25๋Š” ํ‚ค์›Œ๋“œ ์ผ์น˜์— ๊ฐ•ํ•˜๊ณ , Vector๋Š” ์˜๋ฏธ ์ดํ•ด์— ๊ฐ•ํ•ฉ๋‹ˆ๋‹ค. ๋Œ€๋ถ€๋ถ„์˜ ์‹ค๋ฌด RAG์—์„œ๋Š” ๋‘ ๋ฐฉ์‹์„ ๊ฒฐํ•ฉํ•œ Ensemble์ด ๋‹จ์ผ ๋ฐฉ์‹๋ณด๋‹ค ์ผ๊ด€๋˜๊ฒŒ ์ข‹์€ ์„ฑ๋Šฅ์„ ๋ณด์ž…๋‹ˆ๋‹ค.

Q. weights ํ•ฉ์ด ๋ฐ˜๋“œ์‹œ 1.0์ด์–ด์•ผ ํ•˜๋‚˜์š”?

์•„๋‹™๋‹ˆ๋‹ค. [0.4, 0.6]์ด๋“  [2, 3]์ด๋“  ์ƒ๋Œ€์  ๋น„์œจ๋งŒ ์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค. ๋‚ด๋ถ€์ ์œผ๋กœ ์ •๊ทœํ™”๋ฉ๋‹ˆ๋‹ค.

Q. rank_bm25 ํŒจํ‚ค์ง€ ์—†์ด BM25Retriever๋ฅผ ์“ธ ์ˆ˜ ์žˆ๋‚˜์š”?

์—†์Šต๋‹ˆ๋‹ค. BM25Retriever๋Š” ๋‚ด๋ถ€์ ์œผ๋กœ rank_bm25๋ฅผ ์˜์กดํ•ฉ๋‹ˆ๋‹ค. pip install rank_bm25๊ฐ€ ํ•„์ˆ˜์ž…๋‹ˆ๋‹ค.

Q. RRF์˜ c ํŒŒ๋ผ๋ฏธํ„ฐ๋Š” ์–ด๋–ป๊ฒŒ ์กฐ์ •ํ•˜๋‚˜์š”?

ํ˜„์žฌ LangChain์˜ EnsembleRetriever๋Š” c ํŒŒ๋ผ๋ฏธํ„ฐ๋ฅผ ์ง์ ‘ ๋…ธ์ถœํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. ๊ธฐ๋ณธ๊ฐ’ 60์ด ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ ์ ํ•ฉํ•˜๋ฉฐ, ์ปค์Šคํ„ฐ๋งˆ์ด์ง•์ด ํ•„์š”ํ•˜๋‹ค๋ฉด ์†Œ์Šค๋ฅผ ์ƒ์†ํ•˜์—ฌ ์˜ค๋ฒ„๋ผ์ด๋“œํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

Q. ํ•œ๊ตญ์–ด ๋ฌธ์„œ์—์„œ BM25 ์„ฑ๋Šฅ์ด ๋‚ฎ์Šต๋‹ˆ๋‹ค.

ํ•œ๊ตญ์–ด๋Š” ํ˜•ํƒœ์†Œ ๋ถ„์„ ์—†์ด ์–ด์ ˆ ๋‹จ์œ„๋กœ ๋ถ„๋ฆฌ๋˜๋ฉด ๊ฒ€์ƒ‰ ์ •ํ™•๋„๊ฐ€ ๋‚ฎ์Šต๋‹ˆ๋‹ค. konlpy์˜ Okt๋‚˜ Mecab์œผ๋กœ ํ˜•ํƒœ์†Œ ๋ถ„์„ ํ›„ ํ† ํฐ์„ ์ง์ ‘ BM25์— ์ „๋‹ฌํ•˜๋ฉด ์„ฑ๋Šฅ์ด ํ–ฅ์ƒ๋ฉ๋‹ˆ๋‹ค.


๐Ÿ“š ์ฐธ๊ณ 

This post is licensed under CC BY 4.0 by the author.